def delete_domain(self, domain_id): # explicitly forbid deleting the default domain (this should be a # carefully orchestrated manual process involving configuration # changes, etc) if domain_id == CONF.identity.default_domain_id: raise exception.ForbiddenAction(action=_('delete the default ' 'domain')) domain = self.driver.get_domain(domain_id) # To help avoid inadvertent deletes, we insist that the domain # has been previously disabled. This also prevents a user deleting # their own domain since, once it is disabled, they won't be able # to get a valid token to issue this delete. if domain['enabled']: raise exception.ForbiddenAction( action=_('cannot delete a domain that is enabled, ' 'please disable it first.')) self._delete_domain_contents(domain_id) # TODO(henry-nash): Although the controller will ensure deletion of # all users & groups within the domain (which will cause all # assignments for those users/groups to also be deleted), there # could still be assignments on this domain for users/groups in # other domains - so we should delete these here by making a call # to the backend to delete all assignments for this domain. # (see Bug #1277847) self.driver.delete_domain(domain_id) self.get_domain.invalidate(self, domain_id) self.get_domain_by_name.invalidate(self, domain['name'])
def create_project(self, tenant_id, tenant): tenant = tenant.copy() tenant.setdefault('enabled', True) tenant['enabled'] = clean.project_enabled(tenant['enabled']) tenant.setdefault('description', '') tenant.setdefault('parent_id', None) if tenant.get('parent_id') is not None: parent_ref = self.get_project(tenant.get('parent_id')) parents_list = self.list_project_parents(parent_ref['id']) parents_list.append(parent_ref) for ref in parents_list: if ref.get('domain_id') != tenant.get('domain_id'): raise exception.ForbiddenAction( action=_('cannot create a project within a different ' 'domain than its parents.')) if not ref.get('enabled', True): raise exception.ForbiddenAction( action=_('cannot create a project in a ' 'branch containing a disabled ' 'project: %s') % ref['id']) self._assert_max_hierarchy_depth(tenant.get('parent_id'), parents_list) ret = self.driver.create_project(tenant_id, tenant) if SHOULD_CACHE(ret): self.get_project.set(ret, self, tenant_id) self.get_project_by_name.set(ret, self, ret['name'], ret['domain_id']) return ret
def token_authenticate(token): response_data = {} try: # Do not allow tokens used for delegation to # create another token, or perform any changes of # state in Keystone. To do so is to invite elevation of # privilege attacks json_body = flask.request.get_json(silent=True, force=True) or {} project_scoped = 'project' in json_body['auth'].get('scope', {}) domain_scoped = 'domain' in json_body['auth'].get('scope', {}) if token.oauth_scoped: raise exception.ForbiddenAction( action=_('Using OAuth-scoped token to create another token. ' 'Create a new OAuth-scoped token instead')) elif token.trust_scoped: raise exception.ForbiddenAction( action=_('Using trust-scoped token to create another token. ' 'Create a new trust-scoped token instead')) elif token.system_scoped and (project_scoped or domain_scoped): raise exception.ForbiddenAction(action=_( 'Using a system-scoped token to create a project-scoped ' 'or domain-scoped token is not allowed.')) if not CONF.token.allow_rescope_scoped_token: # Do not allow conversion from scoped tokens. if token.project_scoped or token.domain_scoped: raise exception.ForbiddenAction( action=_('rescope a scoped token')) # New tokens maintain the audit_id of the original token in the # chain (if possible) as the second element in the audit data # structure. Look for the last element in the audit data structure # which will be either the audit_id of the token (in the case of # a token that has not been rescoped) or the audit_chain id (in # the case of a token that has been rescoped). try: token_audit_id = token.parent_audit_id or token.audit_id except IndexError: # NOTE(morganfainberg): In the case this is a token that was # issued prior to audit id existing, the chain is not tracked. token_audit_id = None # To prevent users from never having to re-authenticate, the original # token expiration time is maintained in the new token. Not doing this # would make it possible for a user to continuously bump token # expiration through token rescoping without proving their identity. response_data.setdefault('expires_at', token.expires_at) response_data['audit_id'] = token_audit_id response_data.setdefault('user_id', token.user_id) return response_data except AssertionError as e: LOG.error(six.text_type(e)) raise exception.Unauthorized(e)
def token_authenticate(request, token_ref): response_data = {} try: # Do not allow tokens used for delegation to # create another token, or perform any changes of # state in Keystone. To do so is to invite elevation of # privilege attacks if token_ref.oauth_scoped: raise exception.ForbiddenAction( action=_( 'Using OAuth-scoped token to create another token. ' 'Create a new OAuth-scoped token instead')) elif token_ref.trust_scoped: raise exception.ForbiddenAction( action=_( 'Using trust-scoped token to create another token. ' 'Create a new trust-scoped token instead')) if not CONF.token.allow_rescope_scoped_token: # Do not allow conversion from scoped tokens. if token_ref.project_scoped or token_ref.domain_scoped: raise exception.ForbiddenAction( action=_('rescope a scoped token')) wsgi.validate_token_bind(request.context_dict, token_ref) # New tokens maintain the audit_id of the original token in the # chain (if possible) as the second element in the audit data # structure. Look for the last element in the audit data structure # which will be either the audit_id of the token (in the case of # a token that has not been rescoped) or the audit_chain id (in # the case of a token that has been rescoped). try: token_audit_id = token_ref.get('audit_ids', [])[-1] except IndexError: # NOTE(morganfainberg): In the case this is a token that was # issued prior to audit id existing, the chain is not tracked. token_audit_id = None # To prevent users from never having to re-authenticate, the original # token expiration time is maintained in the new token. Not doing this # would make it possible for a user to continuously bump token # expiration through token rescoping without proving their identity. response_data.setdefault('expires_at', token_ref.expires) response_data['audit_id'] = token_audit_id response_data.setdefault('user_id', token_ref.user_id) # TODO(morganfainberg: determine if token 'extras' can be removed # from the response_data response_data.setdefault('extras', {}).update( token_ref.get('extras', {})) return response_data except AssertionError as e: LOG.error(six.text_type(e)) raise exception.Unauthorized(e)
def token_authenticate(request, token_ref): response_data = {} try: # Do not allow tokens used for delegation to # create another token, or perform any changes of # state in Keystone. To do so is to invite elevation of # privilege attacks if token_ref.oauth_scoped: raise exception.ForbiddenAction( action=_('Using OAuth-scoped token to create another token. ' 'Create a new OAuth-scoped token instead')) elif token_ref.trust_scoped: raise exception.ForbiddenAction( action=_('Using trust-scoped token to create another token. ' 'Create a new trust-scoped token instead')) if not CONF.token.allow_rescope_scoped_token: # Do not allow conversion from scoped tokens. if token_ref.project_scoped or token_ref.domain_scoped: raise exception.ForbiddenAction( action=_('rescope a scoped token')) wsgi.validate_token_bind(request.context_dict, token_ref) # New tokens maintain the audit_id of the original token in the # chain (if possible) as the second element in the audit data # structure. Look for the last element in the audit data structure # which will be either the audit_id of the token (in the case of # a token that has not been rescoped) or the audit_chain id (in # the case of a token that has been rescoped). try: token_audit_id = token_ref.get('audit_ids', [])[-1] except IndexError: # NOTE(morganfainberg): In the case this is a token that was # issued prior to audit id existing, the chain is not tracked. token_audit_id = None response_data.setdefault('expires_at', token_ref.expires) response_data['audit_id'] = token_audit_id response_data.setdefault('user_id', token_ref.user_id) # TODO(morganfainberg: determine if token 'extras' can be removed # from the response_data response_data.setdefault('extras', {}).update(token_ref.get('extras', {})) # NOTE(notmorgan): The Token auth method is *very* special and sets the # previous values to the method_names. This is because it can be used # for re-scoping and we want to maintain the values. Most # AuthMethodHandlers do no such thing and this is not required. response_data.setdefault('method_names', []).extend(token_ref.methods) return response_data except AssertionError as e: LOG.error(six.text_type(e)) raise exception.Unauthorized(e)
def test_forbidden_action_exposure_in_debug(self): self.opt(debug=True) risky_info = uuid.uuid4().hex e = exception.ForbiddenAction(message=risky_info) self.assertValidJsonRendering(e) self.assertIn(risky_info, str(e)) e = exception.ForbiddenAction(action=risky_info) self.assertValidJsonRendering(e) self.assertIn(risky_info, str(e))
def test_forbidden_action_exposure_in_debug(self): self.config_fixture.config(debug=True) risky_info = uuid.uuid4().hex e = exception.ForbiddenAction(message=risky_info) self.assertValidJsonRendering(e) self.assertIn(risky_info, six.text_type(e)) e = exception.ForbiddenAction(action=risky_info) self.assertValidJsonRendering(e) self.assertIn(risky_info, six.text_type(e))
def test_forbidden_action_exposure(self): self.opt(debug=False) risky_info = uuid.uuid4().hex action = uuid.uuid4().hex e = exception.ForbiddenAction(message=risky_info, action=action) self.assertValidJsonRendering(e) self.assertNotIn(risky_info, unicode(e)) self.assertIn(action, unicode(e)) e = exception.ForbiddenAction(action=risky_info) self.assertValidJsonRendering(e) self.assertIn(risky_info, unicode(e))
def test_forbidden_action_exposure_in_debug(self): self.config_fixture.config(debug=True, insecure_debug=True) risky_info = uuid.uuid4().hex action = uuid.uuid4().hex e = exception.ForbiddenAction(message=risky_info, action=action) self.assertValidJsonRendering(e) self.assertIn(risky_info, six.text_type(e)) self.assertIn(exception.SecurityError.amendment, six.text_type(e)) e = exception.ForbiddenAction(action=action) self.assertValidJsonRendering(e) self.assertIn(action, six.text_type(e)) self.assertNotIn(exception.SecurityError.amendment, six.text_type(e))
def _create_base_saml_assertion(self, context, auth): issuer = CONF.saml.idp_entity_id sp_id = auth['scope']['service_provider']['id'] service_provider = self.federation_api.get_sp(sp_id) utils.assert_enabled_service_provider_object(service_provider) sp_url = service_provider['sp_url'] token_id = auth['identity']['token']['id'] token_data = self.token_provider_api.validate_token(token_id) token_ref = token_model.KeystoneToken(token_id, token_data) if not token_ref.project_scoped: action = _('Use a project scoped token when attempting to create ' 'a SAML assertion') raise exception.ForbiddenAction(action=action) subject = token_ref.user_name roles = token_ref.role_names project = token_ref.project_name # NOTE(rodrigods): the domain name is necessary in order to distinguish # between projects and users with the same name in different domains. project_domain_name = token_ref.project_domain_name subject_domain_name = token_ref.user_domain_name generator = keystone_idp.SAMLGenerator() response = generator.samlize_token(issuer, sp_url, subject, subject_domain_name, roles, project, project_domain_name) return (response, service_provider)
def _check_unrestricted(self): token = self.auth_context['token'] if 'application_credential' in token.methods: if not token.application_credential['unrestricted']: action = _("Using method 'application_credential' is not " "allowed for managing trusts.") raise exception.ForbiddenAction(action=action)
def list_trusts(self, request): trusts = [] trustor_user_id = request.params.get('trustor_user_id') trustee_user_id = request.params.get('trustee_user_id') if not request.params: self.assert_admin(request) trusts += self.trust_api.list_trusts() action = _('Cannot list trusts for another user') if trustor_user_id: if trustor_user_id != request.context.user_id: raise exception.Forbidden(action=action) trusts += self.trust_api.list_trusts_for_trustor(trustor_user_id) if trustee_user_id: if trustee_user_id != request.context.user_id: raise exception.ForbiddenAction(action=action) trusts += self.trust_api.list_trusts_for_trustee(trustee_user_id) for trust in trusts: # get_trust returns roles, list_trusts does not # It seems in some circumstances, roles does not # exist in the query response, so check first if 'roles' in trust: del trust['roles'] if trust.get('expires_at') is not None: trust['expires_at'] = utils.isotime(trust['expires_at'], subsecond=True) return TrustV3.wrap_collection(request.context_dict, trusts)
def _trustor_trustee_only(trust, user_id): if user_id not in [ trust.get('trustee_user_id'), trust.get('trustor_user_id') ]: raise exception.ForbiddenAction( action=_('Requested user has no relation to this trust'))
def test_forbidden_action_no_message(self): # When no custom message is given when the ForbiddenAction (or other # SecurityError subclass) is created the exposed message is the same # whether debug is enabled or not. action = uuid.uuid4().hex self.config_fixture.config(debug=False) e = exception.ForbiddenAction(action=action) exposed_message = six.text_type(e) self.assertIn(action, exposed_message) self.assertNotIn(exception.SecurityError.amendment, six.text_type(e)) self.config_fixture.config(debug=True) e = exception.ForbiddenAction(action=action) self.assertEqual(exposed_message, six.text_type(e))
def update_project(self, tenant_id, tenant): original_tenant = self.driver.get_project(tenant_id) tenant = tenant.copy() parent_id = original_tenant.get('parent_id') if 'parent_id' in tenant and tenant.get('parent_id') != parent_id: raise exception.ForbiddenAction( action=_('Update of `parent_id` is not allowed.')) if 'enabled' in tenant: tenant['enabled'] = clean.project_enabled(tenant['enabled']) # NOTE(rodrigods): for the current implementation we only allow to # disable a project if all projects below it in the hierarchy are # already disabled. This also means that we can not enable a # project that has disabled parents. original_tenant_enabled = original_tenant.get('enabled', True) tenant_enabled = tenant.get('enabled', True) if not original_tenant_enabled and tenant_enabled: self._assert_all_parents_are_enabled(tenant_id) if original_tenant_enabled and not tenant_enabled: self._assert_whole_subtree_is_disabled(tenant_id) self._disable_project(tenant_id) ret = self.driver.update_project(tenant_id, tenant) self.get_project.invalidate(self, tenant_id) self.get_project_by_name.invalidate(self, original_tenant['name'], original_tenant['domain_id']) return ret
def create_application_credential(self, request, user_id, application_credential): validation.lazy_validate(schema.application_credential_create, application_credential) token = request.auth_context['token'] self._check_unrestricted(token) if request.context.user_id != user_id: action = _("Cannot create an application credential for another " "user") raise exception.ForbiddenAction(action=action) project_id = request.context.project_id app_cred = self._assign_unique_id(application_credential) if not app_cred.get('secret'): app_cred['secret'] = self._generate_secret() app_cred['user_id'] = user_id app_cred['project_id'] = project_id app_cred['roles'] = self._normalize_role_list( app_cred.get('roles', token.roles)) if app_cred.get('expires_at'): app_cred['expires_at'] = utils.parse_expiration_date( app_cred['expires_at']) app_cred = self._normalize_dict(app_cred) app_cred_api = PROVIDERS.application_credential_api try: ref = app_cred_api.create_application_credential( app_cred, initiator=request.audit_initiator) except exception.RoleAssignmentNotFound as e: # Raise a Bad Request, not a Not Found, in accordance with the # API-SIG recommendations: # https://specs.openstack.org/openstack/api-wg/guidelines/http.html#failure-code-clarifications raise exception.ApplicationCredentialValidationError(detail=str(e)) return ApplicationCredentialV3.wrap_member(request.context_dict, ref)
def update(self, id, values, old_obj=None): if not self.allow_update: msg = 'LDAP backend does not allow %s update' % self.options_name raise exception.ForbiddenAction(msg) if old_obj is None: old_obj = self.get(id) modlist = [] for k, v in values.iteritems(): if k == 'id' or k in self.attribute_ignore: continue if v is None: if old_obj[k] is not None: modlist.append( (ldap.MOD_DELETE, self.attribute_mapping.get(k, k), None)) elif old_obj[k] != v: if old_obj[k] is None: op = ldap.MOD_ADD else: op = ldap.MOD_REPLACE modlist.append((op, self.attribute_mapping.get(k, k), [v])) conn = self.get_connection() conn.modify_s(self._id_to_dn(id), modlist)
def enforce(credentials, action, target): """Verifies that the action is valid on the target in this context. :param credentials: user credentials :param action: string representing the action to be checked this should be colon separated for clarity. i.e. compute:create_instance compute:attach_volume volume:attach_volume :param object: dictionary representing the object of the action for object creation this should be a dictionary representing the location of the object e.g. {'tenant_id': object.tenant_id} :raises: `exception.Forbidden` if verification fails. """ init() match_list = ('rule:%s' % action, ) try: common_policy.enforce(match_list, target, credentials) except common_policy.NotAuthorized: raise exception.ForbiddenAction(action=action)
def _check_unrestricted_application_credential(token): if 'application_credential' in token.methods: if not token.application_credential['unrestricted']: action = _("Using method 'application_credential' is not " "allowed for managing additional application " "credentials.") raise ks_exception.ForbiddenAction(action=action)
def delete(self, trust_id): ENFORCER.enforce_call(action='identity:delete_trust', build_target=_build_trust_target_enforcement) self._check_unrestricted() # NOTE(cmurphy) As of Train, the default policies enforce the # identity:delete_trust rule. However, in case the # identity:delete_trust rule has been locally overridden by the # default that would have been produced by the sample config, we need # to enforce it again and warn that the behavior is changing. rules = policy._ENFORCER._enforcer.rules.get('identity:delete_trust') # rule check_str is "" if isinstance(rules, op_checks.TrueCheck): LOG.warning( "The policy check string for rule \"identity:delete_trust\" " "has been overridden to \"always true\". In the next release, " "this will cause the" "\"identity:delete_trust\" action to " "be fully permissive as hardcoded enforcement will be " "removed. To correct this issue, either stop overriding the " "\"identity:delete_trust\" rule in config to accept the " "defaults, or explicitly set a rule that is not empty.") trust = PROVIDERS.trust_api.get_trust(trust_id) if (self.oslo_context.user_id != trust.get('trustor_user_id') and not self.oslo_context.is_admin): action = _('Only admin or trustor can delete a trust') raise exception.ForbiddenAction(action=action) PROVIDERS.trust_api.delete_trust(trust_id, initiator=self.audit_initiator) return '', http.client.NO_CONTENT
def create_base_saml_assertion(auth): issuer = CONF.saml.idp_entity_id sp_id = auth['scope']['service_provider']['id'] service_provider = PROVIDERS.federation_api.get_sp(sp_id) federation_utils.assert_enabled_service_provider_object(service_provider) sp_url = service_provider['sp_url'] token_id = auth['identity']['token']['id'] token = PROVIDERS.token_provider_api.validate_token(token_id) if not token.project_scoped: action = _('Use a project scoped token when attempting to create ' 'a SAML assertion') raise exception.ForbiddenAction(action=action) subject = token.user['name'] role_names = [] for role in token.roles: role_names.append(role['name']) project = token.project['name'] # NOTE(rodrigods): the domain name is necessary in order to distinguish # between projects and users with the same name in different domains. project_domain_name = token.project_domain['name'] subject_domain_name = token.user_domain['name'] generator = keystone_idp.SAMLGenerator() response = generator.samlize_token(issuer, sp_url, subject, subject_domain_name, role_names, project, project_domain_name) return response, service_provider
def delete(self, id): if not self.allow_delete: msg = 'LDAP backend does not allow %s delete' % self.options_name raise exception.ForbiddenAction(msg) conn = self.get_connection() conn.delete_s(self._id_to_dn(id))
def create_saml_assertion(self, context, auth): """Exchange a scoped token for a SAML assertion. :param auth: Dictionary that contains a token id and region id :returns: SAML Assertion based on properties from the token """ issuer = CONF.saml.idp_entity_id region_id = auth['scope']['region']['id'] region = self.catalog_api.get_region(region_id) recipient = region['url'] token_id = auth['identity']['token']['id'] token_data = self.token_provider_api.validate_token(token_id) token_ref = token_model.KeystoneToken(token_id, token_data) subject = token_ref.user_name roles = token_ref.role_names if not token_ref.project_scoped: action = _('Use a project scoped token when attempting to create ' 'a SAML assertion') raise exception.ForbiddenAction(action=action) project = token_ref.project_name generator = keystone_idp.SAMLGenerator() response = generator.samlize_token(issuer, recipient, subject, roles, project) return wsgi.render_response(body=response.to_string(), status=('200', 'OK'), headers=[('Content-Type', 'text/xml')])
def _assert_all_parents_are_enabled(self, project_id): parents_list = self.list_project_parents(project_id) for project in parents_list: if not project.get('enabled', True): raise exception.ForbiddenAction( action=_('cannot enable project %s since it has ' 'disabled parents') % project_id)
def _enforce(self, credentials, action, target, do_raise=True): """Verify that the action is valid on the target in this context. This method is for cases that exceed the base enforcer functionality (notably for compatibility with `@protected` style decorators. :param credentials: user credentials :param action: string representing the action to be checked, which should be colon separated for clarity. :param target: dictionary representing the object of the action for object creation this should be a dictionary representing the location of the object e.g. {'project_id': object.project_id} :raises keystone.exception.Forbidden: If verification fails. Actions should be colon separated for clarity. For example: * identity:list_users """ # Add the exception arguments if asked to do a raise extra = {} if do_raise: extra.update(exc=exception.ForbiddenAction, action=action, do_raise=do_raise) try: return self._enforcer.enforce(rule=action, target=target, creds=credentials, **extra) except common_policy.InvalidScope: raise exception.ForbiddenAction(action=action)
def _check_unrestricted(self, token): auth_methods = token['methods'] if 'application_credential' in auth_methods: if token.token_data['token']['application_credential_restricted']: action = _("Using method 'application_credential' is not " "allowed for managing trusts.") raise exception.ForbiddenAction(action=action)
def update(self, id, values, old_obj=None): if not self.allow_update: action = _('LDAP %s update') % self.options_name raise exception.ForbiddenAction(action=action) if old_obj is None: old_obj = self.get(id) modlist = [] for k, v in values.iteritems(): if k == 'id' or k in self.attribute_ignore: continue if v is None: if old_obj[k] is not None: modlist.append( (ldap.MOD_DELETE, self.attribute_mapping.get(k, k), None)) elif old_obj[k] != v: if old_obj[k] is None: op = ldap.MOD_ADD else: op = ldap.MOD_REPLACE modlist.append((op, self.attribute_mapping.get(k, k), [v])) if modlist: conn = self.get_connection() try: conn.modify_s(self._id_to_dn(id), modlist) except ldap.NO_SUCH_OBJECT: raise self._not_found(id) return self.get(id)
def create(self, values): if not self.allow_create: action = _('LDAP %s create') % self.options_name raise exception.ForbiddenAction(action=action) conn = self.get_connection() object_classes = self.structural_classes + [self.object_class] attrs = [('objectClass', object_classes)] for k, v in values.iteritems(): if k == 'id' or k in self.attribute_ignore: continue if v is not None: attr_type = self.attribute_mapping.get(k, k) attrs.append((attr_type, [v])) extra_attrs = [ attr for attr, name in self.extra_attr_mapping.iteritems() if name == k ] for attr in extra_attrs: attrs.append((attr, [v])) if 'groupOfNames' in object_classes and self.use_dumb_member: attrs.append(('member', [self.dumb_member])) conn.add_s(self._id_to_dn(values['id']), attrs) return values
def enforce(credentials, action, target, do_raise=True): """Verify that the action is valid on the target in this context. :param credentials: user credentials :param action: string representing the action to be checked, which should be colon separated for clarity. :param target: dictionary representing the object of the action for object creation this should be a dictionary representing the location of the object e.g. {'project_id': object.project_id} :raises keystone.exception.Forbidden: If verification fails. Actions should be colon separated for clarity. For example: * identity:list_users """ init() # Add the exception arguments if asked to do a raise extra = {} if do_raise: extra.update(exc=exception.ForbiddenAction, action=action, do_raise=do_raise) try: return _ENFORCER.enforce(action, target, credentials, **extra) except common_policy.InvalidScope: raise exception.ForbiddenAction(action=action)
def _trustor_trustee_only(trust): user_id = flask.request.environ.get(context.REQUEST_CONTEXT_ENV).user_id if user_id not in [ trust.get('trustee_user_id'), trust.get('trustor_user_id') ]: raise exception.ForbiddenAction( action=_('Requested user has no relation to this trust'))