"Fossies" - the Fresh Open Source Software Archive

Member "keystone-18.0.0/keystone/identity/backends/ldap/core.py" (14 Oct 2020, 18497 Bytes) of package /linux/misc/openstack/keystone-18.0.0.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 "core.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 17.0.0_vs_18.0.0.

    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 import uuid
   15 
   16 import ldap.filter
   17 from oslo_log import log
   18 from oslo_log import versionutils
   19 
   20 import keystone.conf
   21 from keystone import exception
   22 from keystone.i18n import _
   23 from keystone.identity.backends import base
   24 from keystone.identity.backends.ldap import common as common_ldap
   25 from keystone.identity.backends.ldap import models
   26 
   27 
   28 CONF = keystone.conf.CONF
   29 LOG = log.getLogger(__name__)
   30 
   31 _DEPRECATION_MSG = ('%s for the LDAP identity backend has been deprecated in '
   32                     'the Mitaka release in favor of read-only identity LDAP '
   33                     'access. It will be removed in the "O" release.')
   34 
   35 READ_ONLY_LDAP_ERROR_MESSAGE = _("LDAP does not support write operations")
   36 
   37 LDAP_MATCHING_RULE_IN_CHAIN = "1.2.840.113556.1.4.1941"
   38 
   39 
   40 class Identity(base.IdentityDriverBase):
   41     def __init__(self, conf=None):
   42         super(Identity, self).__init__()
   43         if conf is None:
   44             self.conf = CONF
   45         else:
   46             self.conf = conf
   47         self.user = UserApi(self.conf)
   48         self.group = GroupApi(self.conf)
   49 
   50     def is_domain_aware(self):
   51         return False
   52 
   53     def generates_uuids(self):
   54         return False
   55 
   56     # Identity interface
   57 
   58     def authenticate(self, user_id, password):
   59         try:
   60             user_ref = self._get_user(user_id)
   61         except exception.UserNotFound:
   62             raise AssertionError(_('Invalid user / password'))
   63         if not user_id or not password:
   64             raise AssertionError(_('Invalid user / password'))
   65         conn = None
   66         try:
   67             conn = self.user.get_connection(user_ref['dn'],
   68                                             password, end_user_auth=True)
   69             if not conn:
   70                 raise AssertionError(_('Invalid user / password'))
   71         except Exception:
   72             raise AssertionError(_('Invalid user / password'))
   73         finally:
   74             if conn:
   75                 conn.unbind_s()
   76         return self.user.filter_attributes(user_ref)
   77 
   78     def _get_user(self, user_id):
   79         return self.user.get(user_id)
   80 
   81     def get_user(self, user_id):
   82         return self.user.get_filtered(user_id)
   83 
   84     def list_users(self, hints):
   85         return self.user.get_all_filtered(hints)
   86 
   87     def unset_default_project_id(self, project_id):
   88         # This function is not implemented for the LDAP backend. The LDAP
   89         # backend is readonly.
   90         self._disallow_write()
   91 
   92     def get_user_by_name(self, user_name, domain_id):
   93         # domain_id will already have been handled in the Manager layer,
   94         # parameter left in so this matches the Driver specification
   95         return self.user.filter_attributes(self.user.get_by_name(user_name))
   96 
   97     def get_group(self, group_id):
   98         return self.group.get_filtered(group_id)
   99 
  100     def get_group_by_name(self, group_name, domain_id):
  101         # domain_id will already have been handled in the Manager layer,
  102         # parameter left in so this matches the Driver specification
  103         return self.group.get_filtered_by_name(group_name)
  104 
  105     def list_groups_for_user(self, user_id, hints):
  106         user_ref = self._get_user(user_id)
  107         if self.conf.ldap.group_members_are_ids:
  108             user_dn = user_ref['id']
  109         else:
  110             user_dn = user_ref['dn']
  111         return self.group.list_user_groups_filtered(user_dn, hints)
  112 
  113     def list_groups(self, hints):
  114         return self.group.get_all_filtered(hints)
  115 
  116     def _transform_group_member_ids(self, group_member_list):
  117         for user_key in group_member_list:
  118             if self.conf.ldap.group_members_are_ids:
  119                 user_id = user_key
  120             else:
  121                 user_id = self.user._dn_to_id(user_key)
  122             yield user_id
  123 
  124     def list_users_in_group(self, group_id, hints):
  125         users = []
  126         group_members = self.group.list_group_users(group_id)
  127         for user_id in self._transform_group_member_ids(group_members):
  128             try:
  129                 users.append(self.user.get_filtered(user_id))
  130             except exception.UserNotFound:
  131                 msg = ('Group member `%(user_id)s` for group `%(group_id)s`'
  132                        ' not found in the directory. The user should be'
  133                        ' removed from the group. The user will be ignored.')
  134                 LOG.debug(msg, dict(user_id=user_id, group_id=group_id))
  135         return users
  136 
  137     def check_user_in_group(self, user_id, group_id):
  138         # Before doing anything, check that the user exists. This will raise
  139         # a not found error if the user doesn't exist so we avoid doing extra
  140         # work.
  141         self.get_user(user_id)
  142         member_list = self.group.list_group_users(group_id)
  143         for group_member_id in self._transform_group_member_ids(member_list):
  144             if group_member_id == user_id:
  145                 break
  146         else:
  147             raise exception.NotFound(_("User '%(user_id)s' not found in"
  148                                        " group '%(group_id)s'") %
  149                                      {'user_id': user_id,
  150                                       'group_id': group_id})
  151 
  152     # Unsupported methods
  153     def _disallow_write(self):
  154         if not common_ldap.WRITABLE:
  155             raise exception.Forbidden(READ_ONLY_LDAP_ERROR_MESSAGE)
  156 
  157     def create_user(self, user_id, user):
  158         self._disallow_write()
  159         return self._create_user(user_id, user)
  160 
  161     def update_user(self, user_id, user):
  162         self._disallow_write()
  163         return self._update_user(user_id, user)
  164 
  165     def delete_user(self, user_id):
  166         raise exception.Forbidden(READ_ONLY_LDAP_ERROR_MESSAGE)
  167 
  168     def change_password(self, user_id, new_password):
  169         raise exception.Forbidden(READ_ONLY_LDAP_ERROR_MESSAGE)
  170 
  171     def add_user_to_group(self, user_id, group_id):
  172         self._disallow_write()
  173         self._add_user_to_group(user_id, group_id)
  174 
  175     def remove_user_from_group(self, user_id, group_id):
  176         raise exception.Forbidden(READ_ONLY_LDAP_ERROR_MESSAGE)
  177 
  178     def create_group(self, group_id, group):
  179         self._disallow_write()
  180         return self._create_group(group_id, group)
  181 
  182     def update_group(self, group_id, group):
  183         self._disallow_write()
  184         return self._update_group(group_id, group)
  185 
  186     def delete_group(self, group_id):
  187         raise exception.Forbidden(READ_ONLY_LDAP_ERROR_MESSAGE)
  188 
  189     # Test implementations
  190     def _create_user(self, user_id, user):
  191         msg = _DEPRECATION_MSG % "create_user"
  192         versionutils.report_deprecated_feature(LOG, msg)
  193         user_ref = self.user.create(user)
  194         return self.user.filter_attributes(user_ref)
  195 
  196     def _update_user(self, user_id, user):
  197         msg = _DEPRECATION_MSG % "update_user"
  198         versionutils.report_deprecated_feature(LOG, msg)
  199         old_obj = self.user.get(user_id)
  200         if 'name' in user and old_obj.get('name') != user['name']:
  201             raise exception.Conflict(_('Cannot change user name'))
  202 
  203         if self.user.enabled_mask:
  204             self.user.mask_enabled_attribute(user)
  205         elif self.user.enabled_invert and not self.user.enabled_emulation:
  206             # We need to invert the enabled value for the old model object
  207             # to prevent the LDAP update code from thinking that the enabled
  208             # values are already equal.
  209             user['enabled'] = not user['enabled']
  210             old_obj['enabled'] = not old_obj['enabled']
  211 
  212         self.user.update(user_id, user, old_obj)
  213         return self.user.get_filtered(user_id)
  214 
  215     def _create_group(self, group_id, group):
  216         msg = _DEPRECATION_MSG % "create_group"
  217         versionutils.report_deprecated_feature(LOG, msg)
  218         return common_ldap.filter_entity(self.group.create(group))
  219 
  220     def _update_group(self, group_id, group):
  221         msg = _DEPRECATION_MSG % "update_group"
  222         versionutils.report_deprecated_feature(LOG, msg)
  223         return common_ldap.filter_entity(self.group.update(group_id, group))
  224 
  225     def _add_user_to_group(self, user_id, group_id):
  226         msg = _DEPRECATION_MSG % "add_user_to_group"
  227         versionutils.report_deprecated_feature(LOG, msg)
  228         user_ref = self._get_user(user_id)
  229         user_dn = user_ref['dn']
  230         self.group.add_user(user_dn, group_id, user_id)
  231 
  232 
  233 # TODO(termie): turn this into a data object and move logic to driver
  234 class UserApi(common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap):
  235     DEFAULT_OU = 'ou=Users'
  236     DEFAULT_STRUCTURAL_CLASSES = ['person']
  237     DEFAULT_ID_ATTR = 'cn'
  238     DEFAULT_OBJECTCLASS = 'inetOrgPerson'
  239     NotFound = exception.UserNotFound
  240     options_name = 'user'
  241     attribute_options_names = {'password': 'pass',
  242                                'email': 'mail',
  243                                'name': 'name',
  244                                'description': 'description',
  245                                'enabled': 'enabled',
  246                                'default_project_id': 'default_project_id'}
  247     immutable_attrs = ['id']
  248 
  249     model = models.User
  250 
  251     def __init__(self, conf):
  252         super(UserApi, self).__init__(conf)
  253         self.enabled_mask = conf.ldap.user_enabled_mask
  254         self.enabled_default = conf.ldap.user_enabled_default
  255         self.enabled_invert = conf.ldap.user_enabled_invert
  256         self.enabled_emulation = conf.ldap.user_enabled_emulation
  257 
  258     def _ldap_res_to_model(self, res):
  259         obj = super(UserApi, self)._ldap_res_to_model(res)
  260         if self.enabled_mask != 0:
  261             enabled = int(obj.get('enabled', self.enabled_default))
  262             obj['enabled'] = ((enabled & self.enabled_mask) !=
  263                               self.enabled_mask)
  264         elif self.enabled_invert and not self.enabled_emulation:
  265             # This could be a bool or a string.  If it's a string,
  266             # we need to convert it so we can invert it properly.
  267             enabled = obj.get('enabled', self.enabled_default)
  268             if isinstance(enabled, str):
  269                 if enabled.lower() == 'true':
  270                     enabled = True
  271                 else:
  272                     enabled = False
  273             obj['enabled'] = not enabled
  274         obj['dn'] = res[0]
  275 
  276         return obj
  277 
  278     def mask_enabled_attribute(self, values):
  279         value = values['enabled']
  280         values.setdefault('enabled_nomask', int(self.enabled_default))
  281         if value != ((values['enabled_nomask'] & self.enabled_mask) !=
  282                      self.enabled_mask):
  283             values['enabled_nomask'] ^= self.enabled_mask
  284         values['enabled'] = values['enabled_nomask']
  285         del values['enabled_nomask']
  286 
  287     def create(self, values):
  288         if 'options' in values:
  289             values.pop('options')  # can't specify options
  290         if self.enabled_mask:
  291             orig_enabled = values['enabled']
  292             self.mask_enabled_attribute(values)
  293         elif self.enabled_invert and not self.enabled_emulation:
  294             orig_enabled = values['enabled']
  295             if orig_enabled is not None:
  296                 values['enabled'] = not orig_enabled
  297             else:
  298                 values['enabled'] = self.enabled_default
  299         values = super(UserApi, self).create(values)
  300         if self.enabled_mask or (self.enabled_invert and
  301                                  not self.enabled_emulation):
  302             values['enabled'] = orig_enabled
  303         values['options'] = {}  # options always empty
  304         return values
  305 
  306     def get(self, user_id, ldap_filter=None):
  307         obj = super(UserApi, self).get(user_id, ldap_filter=ldap_filter)
  308         obj['options'] = {}  # options always empty
  309         return obj
  310 
  311     def get_filtered(self, user_id):
  312         try:
  313             user = self.get(user_id)
  314             return self.filter_attributes(user)
  315         except ldap.NO_SUCH_OBJECT:
  316             raise self.NotFound(user_id=user_id)
  317 
  318     def get_all(self, ldap_filter=None, hints=None):
  319         objs = super(UserApi, self).get_all(ldap_filter=ldap_filter,
  320                                             hints=hints)
  321         for obj in objs:
  322             obj['options'] = {}  # options always empty
  323         return objs
  324 
  325     def get_all_filtered(self, hints):
  326         query = self.filter_query(hints, self.ldap_filter)
  327         return [self.filter_attributes(user)
  328                 for user in self.get_all(query, hints)]
  329 
  330     def filter_attributes(self, user):
  331         return base.filter_user(common_ldap.filter_entity(user))
  332 
  333     def is_user(self, dn):
  334         """Return True if the entry is a user."""
  335         # NOTE(blk-u): It's easy to check if the DN is under the User tree,
  336         # but may not be accurate. A more accurate test would be to fetch the
  337         # entry to see if it's got the user objectclass, but this could be
  338         # really expensive considering how this is used.
  339 
  340         return common_ldap.dn_startswith(dn, self.tree_dn)
  341 
  342     def update(self, user_id, values, old_obj=None):
  343         if old_obj is None:
  344             old_obj = self.get(user_id)
  345         # don't support updating options
  346         if 'options' in old_obj:
  347             old_obj.pop('options')
  348         if 'options' in values:
  349             values.pop('options')
  350         values = super(UserApi, self).update(user_id, values, old_obj)
  351         values['options'] = {}  # options always empty
  352         return values
  353 
  354 
  355 class GroupApi(common_ldap.BaseLdap):
  356     DEFAULT_OU = 'ou=UserGroups'
  357     DEFAULT_STRUCTURAL_CLASSES = []
  358     DEFAULT_OBJECTCLASS = 'groupOfNames'
  359     DEFAULT_ID_ATTR = 'cn'
  360     DEFAULT_MEMBER_ATTRIBUTE = 'member'
  361     NotFound = exception.GroupNotFound
  362     options_name = 'group'
  363     attribute_options_names = {'description': 'desc',
  364                                'name': 'name'}
  365     immutable_attrs = ['name']
  366     model = models.Group
  367 
  368     def _ldap_res_to_model(self, res):
  369         model = super(GroupApi, self)._ldap_res_to_model(res)
  370         model['dn'] = res[0]
  371         return model
  372 
  373     def __init__(self, conf):
  374         super(GroupApi, self).__init__(conf)
  375         self.group_ad_nesting = conf.ldap.group_ad_nesting
  376         self.member_attribute = (conf.ldap.group_member_attribute
  377                                  or self.DEFAULT_MEMBER_ATTRIBUTE)
  378 
  379     def create(self, values):
  380         data = values.copy()
  381         if data.get('id') is None:
  382             data['id'] = uuid.uuid4().hex
  383         if 'description' in data and data['description'] in ['', None]:
  384             data.pop('description')
  385         return super(GroupApi, self).create(data)
  386 
  387     def update(self, group_id, values):
  388         old_obj = self.get(group_id)
  389         return super(GroupApi, self).update(group_id, values, old_obj)
  390 
  391     def add_user(self, user_dn, group_id, user_id):
  392         group_ref = self.get(group_id)
  393         group_dn = group_ref['dn']
  394         try:
  395             super(GroupApi, self).add_member(user_dn, group_dn)
  396         except exception.Conflict:
  397             raise exception.Conflict(_(
  398                 'User %(user_id)s is already a member of group %(group_id)s') %
  399                 {'user_id': user_id, 'group_id': group_id})
  400 
  401     def list_user_groups(self, user_dn):
  402         """Return a list of groups for which the user is a member."""
  403         user_dn_esc = ldap.filter.escape_filter_chars(user_dn)
  404         if self.group_ad_nesting:
  405             query = '(%s:%s:=%s)' % (
  406                 self.member_attribute,
  407                 LDAP_MATCHING_RULE_IN_CHAIN,
  408                 user_dn_esc)
  409         else:
  410             query = '(%s=%s)' % (self.member_attribute,
  411                                  user_dn_esc)
  412         return self.get_all(query)
  413 
  414     def list_user_groups_filtered(self, user_dn, hints):
  415         """Return a filtered list of groups for which the user is a member."""
  416         user_dn_esc = ldap.filter.escape_filter_chars(user_dn)
  417         if self.group_ad_nesting:
  418             # Hardcoded to member as that is how the Matching Rule in Chain
  419             # Mechanisms expects it.  The member_attribute might actually be
  420             # member_of elsewhere, so they are not the same.
  421             query = '(member:%s:=%s)' % (
  422                 LDAP_MATCHING_RULE_IN_CHAIN,
  423                 user_dn_esc)
  424         else:
  425             query = '(%s=%s)' % (self.member_attribute,
  426                                  user_dn_esc)
  427         return self.get_all_filtered(hints, query)
  428 
  429     def list_group_users(self, group_id):
  430         """Return a list of user dns which are members of a group."""
  431         group_ref = self.get(group_id)
  432         group_dn = group_ref['dn']
  433 
  434         try:
  435             if self.group_ad_nesting:
  436                 # NOTE(ayoung): LDAP_SCOPE is used here instead of hard-
  437                 # coding to SCOPE_SUBTREE to get through the unit tests.
  438                 # However, it is also probably more correct.
  439                 attrs = self._ldap_get_list(
  440                     self.tree_dn, self.LDAP_SCOPE,
  441                     query_params={
  442                         "member:%s:" % LDAP_MATCHING_RULE_IN_CHAIN:
  443                         group_dn},
  444                     attrlist=[self.member_attribute])
  445             else:
  446                 attrs = self._ldap_get_list(group_dn, ldap.SCOPE_BASE,
  447                                             attrlist=[self.member_attribute])
  448 
  449         except ldap.NO_SUCH_OBJECT:
  450             raise self.NotFound(group_id=group_id)
  451 
  452         users = []
  453         for dn, member in attrs:
  454             user_dns = member.get(self.member_attribute, [])
  455             for user_dn in user_dns:
  456                 users.append(user_dn)
  457         return users
  458 
  459     def get_filtered(self, group_id):
  460         group = self.get(group_id)
  461         return common_ldap.filter_entity(group)
  462 
  463     def get_filtered_by_name(self, group_name):
  464         group = self.get_by_name(group_name)
  465         return common_ldap.filter_entity(group)
  466 
  467     def get_all_filtered(self, hints, query=None):
  468         if self.ldap_filter:
  469             query = (query or '') + self.ldap_filter
  470         query = self.filter_query(hints, query)
  471         return [common_ldap.filter_entity(group)
  472                 for group in self.get_all(query, hints)]