"Fossies" - the Fresh Open Source Software Archive

Member "keystone-16.0.2/keystone/identity/backends/sql.py" (7 Jun 2021, 17959 Bytes) of package /linux/misc/openstack/keystone-16.0.2.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "sql.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 16.0.1_vs_16.0.2.

    1 # Copyright 2012 OpenStack Foundation
    2 #
    3 # Licensed under the Apache License, Version 2.0 (the "License"); you may
    4 # not use this file except in compliance with the License. You may obtain
    5 # a copy of the License at
    6 #
    7 #      http://www.apache.org/licenses/LICENSE-2.0
    8 #
    9 # Unless required by applicable law or agreed to in writing, software
   10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   12 # License for the specific language governing permissions and limitations
   13 # under the License.
   14 
   15 import datetime
   16 
   17 from oslo_db import api as oslo_db_api
   18 import sqlalchemy
   19 
   20 from keystone.common import driver_hints
   21 from keystone.common import password_hashing
   22 from keystone.common import resource_options
   23 from keystone.common import sql
   24 import keystone.conf
   25 from keystone import exception
   26 from keystone.i18n import _
   27 from keystone.identity.backends import base
   28 from keystone.identity.backends import resource_options as options
   29 from keystone.identity.backends import sql_model as model
   30 
   31 
   32 CONF = keystone.conf.CONF
   33 
   34 
   35 def _stale_data_exception_checker(exc):
   36     return isinstance(exc, sqlalchemy.orm.exc.StaleDataError)
   37 
   38 
   39 class Identity(base.IdentityDriverBase):
   40     # NOTE(henry-nash): Override the __init__() method so as to take a
   41     # config parameter to enable sql to be used as a domain-specific driver.
   42     def __init__(self, conf=None):
   43         self.conf = conf
   44         super(Identity, self).__init__()
   45 
   46     @property
   47     def is_sql(self):
   48         return True
   49 
   50     def _check_password(self, password, user_ref):
   51         """Check the specified password against the data store.
   52 
   53         Note that we'll pass in the entire user_ref in case the subclass
   54         needs things like user_ref.get('name')
   55         For further justification, please see the follow up suggestion at
   56         https://blueprints.launchpad.net/keystone/+spec/sql-identiy-pam
   57 
   58         """
   59         return password_hashing.check_password(password, user_ref.password)
   60 
   61     # Identity interface
   62     def authenticate(self, user_id, password):
   63         with sql.session_for_read() as session:
   64             try:
   65                 user_ref = self._get_user(session, user_id)
   66                 user_dict = base.filter_user(user_ref.to_dict())
   67             except exception.UserNotFound:
   68                 raise AssertionError(_('Invalid user / password'))
   69         if self._is_account_locked(user_id, user_ref):
   70             raise exception.AccountLocked(user_id=user_id)
   71         elif not self._check_password(password, user_ref):
   72             self._record_failed_auth(user_id)
   73             raise AssertionError(_('Invalid user / password'))
   74         elif not user_ref.enabled:
   75             raise exception.UserDisabled(user_id=user_id)
   76         elif user_ref.password_is_expired:
   77             raise exception.PasswordExpired(user_id=user_id)
   78         # successful auth, reset failed count if present
   79         if user_ref.local_user.failed_auth_count:
   80             self._reset_failed_auth(user_id)
   81         return user_dict
   82 
   83     def _is_account_locked(self, user_id, user_ref):
   84         """Check if the user account is locked.
   85 
   86         Checks if the user account is locked based on the number of failed
   87         authentication attempts.
   88 
   89         :param user_id: The user ID
   90         :param user_ref: Reference to the user object
   91         :returns Boolean: True if the account is locked; False otherwise
   92 
   93         """
   94         ignore_option = user_ref.get_resource_option(
   95             options.IGNORE_LOCKOUT_ATTEMPT_OPT.option_id)
   96         if ignore_option and ignore_option.option_value is True:
   97             return False
   98 
   99         attempts = user_ref.local_user.failed_auth_count or 0
  100         max_attempts = CONF.security_compliance.lockout_failure_attempts
  101         lockout_duration = CONF.security_compliance.lockout_duration
  102         if max_attempts and (attempts >= max_attempts):
  103             if not lockout_duration:
  104                 return True
  105             else:
  106                 delta = datetime.timedelta(seconds=lockout_duration)
  107                 last_failure = user_ref.local_user.failed_auth_at
  108                 if (last_failure + delta) > datetime.datetime.utcnow():
  109                     return True
  110                 else:
  111                     self._reset_failed_auth(user_id)
  112         return False
  113 
  114     def _record_failed_auth(self, user_id):
  115         with sql.session_for_write() as session:
  116             user_ref = session.query(model.User).get(user_id)
  117             if not user_ref.local_user.failed_auth_count:
  118                 user_ref.local_user.failed_auth_count = 0
  119             user_ref.local_user.failed_auth_count += 1
  120             user_ref.local_user.failed_auth_at = datetime.datetime.utcnow()
  121 
  122     def _reset_failed_auth(self, user_id):
  123         with sql.session_for_write() as session:
  124             user_ref = session.query(model.User).get(user_id)
  125             user_ref.local_user.failed_auth_count = 0
  126             user_ref.local_user.failed_auth_at = None
  127 
  128     # user crud
  129 
  130     @sql.handle_conflicts(conflict_type='user')
  131     def create_user(self, user_id, user):
  132         with sql.session_for_write() as session:
  133             user_ref = model.User.from_dict(user)
  134             if self._change_password_required(user_ref):
  135                 user_ref.password_ref.expires_at = datetime.datetime.utcnow()
  136             user_ref.created_at = datetime.datetime.utcnow()
  137             session.add(user_ref)
  138             # Set resource options passed on creation
  139             resource_options.resource_options_ref_to_mapper(
  140                 user_ref, model.UserOption)
  141             return base.filter_user(user_ref.to_dict())
  142 
  143     def _change_password_required(self, user):
  144         if not CONF.security_compliance.change_password_upon_first_use:
  145             return False
  146         ignore_option = user.get_resource_option(
  147             options.IGNORE_CHANGE_PASSWORD_OPT.option_id)
  148         return not (ignore_option and ignore_option.option_value is True)
  149 
  150     def _create_password_expires_query(self, session, query, hints):
  151         for filter_ in hints.filters:
  152             if 'password_expires_at' == filter_['name']:
  153                 # Filter on users who's password expires based on the operator
  154                 # specified in `filter_['comparator']`
  155                 query = query.filter(sqlalchemy.and_(
  156                     model.LocalUser.id == model.Password.local_user_id,
  157                     filter_['comparator'](model.Password.expires_at,
  158                                           filter_['value'])))
  159         # Removes the `password_expired_at` filters so there are no errors
  160         # if the call is filtered further. This is because the
  161         # `password_expires_at` value is not stored in the `User` table but
  162         # derived from the `Password` table's value `expires_at`.
  163         hints.filters = [x for x in hints.filters if x['name'] !=
  164                          'password_expires_at']
  165         return query, hints
  166 
  167     @driver_hints.truncated
  168     def list_users(self, hints):
  169         with sql.session_for_read() as session:
  170             query = session.query(model.User).outerjoin(model.LocalUser)
  171             query, hints = self._create_password_expires_query(session, query,
  172                                                                hints)
  173             user_refs = sql.filter_limit_query(model.User, query, hints)
  174             return [base.filter_user(x.to_dict()) for x in user_refs]
  175 
  176     def unset_default_project_id(self, project_id):
  177         with sql.session_for_write() as session:
  178             query = session.query(model.User)
  179             query = query.filter(model.User.default_project_id == project_id)
  180 
  181             for user in query:
  182                 user.default_project_id = None
  183 
  184     def _get_user(self, session, user_id):
  185         user_ref = session.query(model.User).get(user_id)
  186         if not user_ref:
  187             raise exception.UserNotFound(user_id=user_id)
  188         return user_ref
  189 
  190     def get_user(self, user_id):
  191         with sql.session_for_read() as session:
  192             return base.filter_user(
  193                 self._get_user(session, user_id).to_dict())
  194 
  195     def get_user_by_name(self, user_name, domain_id):
  196         with sql.session_for_read() as session:
  197             query = session.query(model.User).join(model.LocalUser)
  198             query = query.filter(sqlalchemy.and_(
  199                 model.LocalUser.name == user_name,
  200                 model.LocalUser.domain_id == domain_id))
  201             try:
  202                 user_ref = query.one()
  203             except sql.NotFound:
  204                 raise exception.UserNotFound(user_id=user_name)
  205             return base.filter_user(user_ref.to_dict())
  206 
  207     @sql.handle_conflicts(conflict_type='user')
  208     # Explicitly retry on StaleDataErrors, which can happen if two clients
  209     # update the same user's password and the second client has stale password
  210     # information.
  211     @oslo_db_api.wrap_db_retry(exception_checker=_stale_data_exception_checker)
  212     def update_user(self, user_id, user):
  213         with sql.session_for_write() as session:
  214             user_ref = self._get_user(session, user_id)
  215             old_user_dict = user_ref.to_dict()
  216             for k in user:
  217                 old_user_dict[k] = user[k]
  218             new_user = model.User.from_dict(old_user_dict)
  219             for attr in model.User.attributes:
  220                 if attr not in model.User.readonly_attributes:
  221                     setattr(user_ref, attr, getattr(new_user, attr))
  222             # Move the "_resource_options" attribute over to the real user_ref
  223             # so that resource_options.resource_options_ref_to_mapper can
  224             # handle the work.
  225             setattr(user_ref, '_resource_options',
  226                     getattr(new_user, '_resource_options', {}))
  227 
  228             # Move options into the proper attribute mapper construct
  229             resource_options.resource_options_ref_to_mapper(
  230                 user_ref, model.UserOption)
  231 
  232             if 'password' in user:
  233                 user_ref.password = user['password']
  234                 if self._change_password_required(user_ref):
  235                     expires_now = datetime.datetime.utcnow()
  236                     user_ref.password_ref.expires_at = expires_now
  237 
  238             user_ref.extra = new_user.extra
  239             return base.filter_user(
  240                 user_ref.to_dict(include_extra_dict=True))
  241 
  242     def _validate_password_history(self, password, user_ref):
  243         unique_cnt = CONF.security_compliance.unique_last_password_count
  244         # Validate the new password against the remaining passwords.
  245         if unique_cnt > 0:
  246             for password_ref in user_ref.local_user.passwords[-unique_cnt:]:
  247                 if password_hashing.check_password(
  248                         password, password_ref.password_hash):
  249                     raise exception.PasswordHistoryValidationError(
  250                         unique_count=unique_cnt)
  251 
  252     def change_password(self, user_id, new_password):
  253         with sql.session_for_write() as session:
  254             user_ref = session.query(model.User).get(user_id)
  255             lock_pw_opt = user_ref.get_resource_option(
  256                 options.LOCK_PASSWORD_OPT.option_id)
  257             if lock_pw_opt is not None and lock_pw_opt.option_value is True:
  258                 raise exception.PasswordSelfServiceDisabled()
  259             if user_ref.password_ref and user_ref.password_ref.self_service:
  260                 self._validate_minimum_password_age(user_ref)
  261             self._validate_password_history(new_password, user_ref)
  262             user_ref.password = new_password
  263             user_ref.password_ref.self_service = True
  264 
  265     def _validate_minimum_password_age(self, user_ref):
  266         min_age_days = CONF.security_compliance.minimum_password_age
  267         min_age = (user_ref.password_created_at +
  268                    datetime.timedelta(days=min_age_days))
  269         if datetime.datetime.utcnow() < min_age:
  270             days_left = (min_age - datetime.datetime.utcnow()).days
  271             raise exception.PasswordAgeValidationError(
  272                 min_age_days=min_age_days, days_left=days_left)
  273 
  274     def add_user_to_group(self, user_id, group_id):
  275         with sql.session_for_write() as session:
  276             self.get_group(group_id)
  277             self.get_user(user_id)
  278             query = session.query(model.UserGroupMembership)
  279             query = query.filter_by(user_id=user_id)
  280             query = query.filter_by(group_id=group_id)
  281             rv = query.first()
  282             if rv:
  283                 return
  284 
  285             session.add(model.UserGroupMembership(user_id=user_id,
  286                                                   group_id=group_id))
  287 
  288     def check_user_in_group(self, user_id, group_id):
  289         with sql.session_for_read() as session:
  290             self.get_group(group_id)
  291             self.get_user(user_id)
  292             query = session.query(model.UserGroupMembership)
  293             query = query.filter_by(user_id=user_id)
  294             query = query.filter_by(group_id=group_id)
  295             if not query.first():
  296                 raise exception.NotFound(_("User '%(user_id)s' not found in"
  297                                            " group '%(group_id)s'") %
  298                                          {'user_id': user_id,
  299                                           'group_id': group_id})
  300 
  301     def remove_user_from_group(self, user_id, group_id):
  302         # We don't check if user or group are still valid and let the remove
  303         # be tried anyway - in case this is some kind of clean-up operation
  304         with sql.session_for_write() as session:
  305             query = session.query(model.UserGroupMembership)
  306             query = query.filter_by(user_id=user_id)
  307             query = query.filter_by(group_id=group_id)
  308             membership_ref = query.first()
  309             if membership_ref is None:
  310                 # Check if the group and user exist to return descriptive
  311                 # exceptions.
  312                 self.get_group(group_id)
  313                 self.get_user(user_id)
  314                 raise exception.NotFound(_("User '%(user_id)s' not found in"
  315                                            " group '%(group_id)s'") %
  316                                          {'user_id': user_id,
  317                                           'group_id': group_id})
  318             session.delete(membership_ref)
  319 
  320     def list_groups_for_user(self, user_id, hints):
  321         with sql.session_for_read() as session:
  322             self.get_user(user_id)
  323             query = session.query(model.Group).join(model.UserGroupMembership)
  324             query = query.filter(model.UserGroupMembership.user_id == user_id)
  325             query = sql.filter_limit_query(model.Group, query, hints)
  326             return [g.to_dict() for g in query]
  327 
  328     def list_users_in_group(self, group_id, hints):
  329         with sql.session_for_read() as session:
  330             self.get_group(group_id)
  331             query = session.query(model.User).outerjoin(model.LocalUser)
  332             query = query.join(model.UserGroupMembership)
  333             query = query.filter(
  334                 model.UserGroupMembership.group_id == group_id)
  335             query, hints = self._create_password_expires_query(session, query,
  336                                                                hints)
  337             query = sql.filter_limit_query(model.User, query, hints)
  338             return [base.filter_user(u.to_dict()) for u in query]
  339 
  340     @oslo_db_api.wrap_db_retry(retry_on_deadlock=True)
  341     def delete_user(self, user_id):
  342         with sql.session_for_write() as session:
  343             ref = self._get_user(session, user_id)
  344 
  345             q = session.query(model.UserGroupMembership)
  346             q = q.filter_by(user_id=user_id)
  347             q.delete(False)
  348 
  349             session.delete(ref)
  350 
  351     # group crud
  352 
  353     @sql.handle_conflicts(conflict_type='group')
  354     def create_group(self, group_id, group):
  355         with sql.session_for_write() as session:
  356             ref = model.Group.from_dict(group)
  357             session.add(ref)
  358             return ref.to_dict()
  359 
  360     @driver_hints.truncated
  361     def list_groups(self, hints):
  362         with sql.session_for_read() as session:
  363             query = session.query(model.Group)
  364             refs = sql.filter_limit_query(model.Group, query, hints)
  365             return [ref.to_dict() for ref in refs]
  366 
  367     def _get_group(self, session, group_id):
  368         ref = session.query(model.Group).get(group_id)
  369         if not ref:
  370             raise exception.GroupNotFound(group_id=group_id)
  371         return ref
  372 
  373     def get_group(self, group_id):
  374         with sql.session_for_read() as session:
  375             return self._get_group(session, group_id).to_dict()
  376 
  377     def get_group_by_name(self, group_name, domain_id):
  378         with sql.session_for_read() as session:
  379             query = session.query(model.Group)
  380             query = query.filter_by(name=group_name)
  381             query = query.filter_by(domain_id=domain_id)
  382             try:
  383                 group_ref = query.one()
  384             except sql.NotFound:
  385                 raise exception.GroupNotFound(group_id=group_name)
  386             return group_ref.to_dict()
  387 
  388     @sql.handle_conflicts(conflict_type='group')
  389     def update_group(self, group_id, group):
  390         with sql.session_for_write() as session:
  391             ref = self._get_group(session, group_id)
  392             old_dict = ref.to_dict()
  393             for k in group:
  394                 old_dict[k] = group[k]
  395             new_group = model.Group.from_dict(old_dict)
  396             for attr in model.Group.attributes:
  397                 if attr != 'id':
  398                     setattr(ref, attr, getattr(new_group, attr))
  399             ref.extra = new_group.extra
  400             return ref.to_dict()
  401 
  402     def delete_group(self, group_id):
  403         with sql.session_for_write() as session:
  404             ref = self._get_group(session, group_id)
  405 
  406             q = session.query(model.UserGroupMembership)
  407             q = q.filter_by(group_id=group_id)
  408             q.delete(False)
  409 
  410             session.delete(ref)