diff options
author | WuKong <rebirthmonkey@gmail.com> | 2015-06-30 18:47:29 +0200 |
---|---|---|
committer | WuKong <rebirthmonkey@gmail.com> | 2015-06-30 18:47:29 +0200 |
commit | b8c756ecdd7cced1db4300935484e8c83701c82e (patch) | |
tree | 87e51107d82b217ede145de9d9d59e2100725bd7 /keystone-moon/keystone/auth | |
parent | c304c773bae68fb854ed9eab8fb35c4ef17cf136 (diff) |
migrate moon code from github to opnfv
Change-Id: Ice53e368fd1114d56a75271aa9f2e598e3eba604
Signed-off-by: WuKong <rebirthmonkey@gmail.com>
Diffstat (limited to 'keystone-moon/keystone/auth')
-rw-r--r-- | keystone-moon/keystone/auth/__init__.py | 17 | ||||
-rw-r--r-- | keystone-moon/keystone/auth/controllers.py | 647 | ||||
-rw-r--r-- | keystone-moon/keystone/auth/core.py | 94 | ||||
-rw-r--r-- | keystone-moon/keystone/auth/plugins/__init__.py | 15 | ||||
-rw-r--r-- | keystone-moon/keystone/auth/plugins/core.py | 186 | ||||
-rw-r--r-- | keystone-moon/keystone/auth/plugins/external.py | 186 | ||||
-rw-r--r-- | keystone-moon/keystone/auth/plugins/mapped.py | 252 | ||||
-rw-r--r-- | keystone-moon/keystone/auth/plugins/oauth1.py | 75 | ||||
-rw-r--r-- | keystone-moon/keystone/auth/plugins/password.py | 49 | ||||
-rw-r--r-- | keystone-moon/keystone/auth/plugins/saml2.py | 27 | ||||
-rw-r--r-- | keystone-moon/keystone/auth/plugins/token.py | 99 | ||||
-rw-r--r-- | keystone-moon/keystone/auth/routers.py | 57 |
12 files changed, 1704 insertions, 0 deletions
diff --git a/keystone-moon/keystone/auth/__init__.py b/keystone-moon/keystone/auth/__init__.py new file mode 100644 index 00000000..b1e4203e --- /dev/null +++ b/keystone-moon/keystone/auth/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystone.auth import controllers # noqa +from keystone.auth.core import * # noqa +from keystone.auth import routers # noqa diff --git a/keystone-moon/keystone/auth/controllers.py b/keystone-moon/keystone/auth/controllers.py new file mode 100644 index 00000000..065f1f01 --- /dev/null +++ b/keystone-moon/keystone/auth/controllers.py @@ -0,0 +1,647 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys + +from keystoneclient.common import cms +from oslo_config import cfg +from oslo_log import log +from oslo_serialization import jsonutils +from oslo_utils import importutils +from oslo_utils import timeutils +import six + +from keystone.common import controller +from keystone.common import dependency +from keystone.common import wsgi +from keystone import config +from keystone.contrib import federation +from keystone import exception +from keystone.i18n import _, _LI, _LW +from keystone.resource import controllers as resource_controllers + + +LOG = log.getLogger(__name__) + +CONF = cfg.CONF + +# registry of authentication methods +AUTH_METHODS = {} +AUTH_PLUGINS_LOADED = False + + +def load_auth_methods(): + global AUTH_PLUGINS_LOADED + + if AUTH_PLUGINS_LOADED: + # Only try and load methods a single time. + return + # config.setup_authentication should be idempotent, call it to ensure we + # have setup all the appropriate configuration options we may need. + config.setup_authentication() + for plugin in CONF.auth.methods: + if '.' in plugin: + # NOTE(morganfainberg): if '.' is in the plugin name, it should be + # imported rather than used as a plugin identifier. + plugin_class = plugin + driver = importutils.import_object(plugin) + if not hasattr(driver, 'method'): + raise ValueError(_('Cannot load an auth-plugin by class-name ' + 'without a "method" attribute defined: %s'), + plugin_class) + + LOG.info(_LI('Loading auth-plugins by class-name is deprecated.')) + plugin_name = driver.method + else: + plugin_name = plugin + plugin_class = CONF.auth.get(plugin) + driver = importutils.import_object(plugin_class) + if plugin_name in AUTH_METHODS: + raise ValueError(_('Auth plugin %(plugin)s is requesting ' + 'previously registered method %(method)s') % + {'plugin': plugin_class, 'method': driver.method}) + AUTH_METHODS[plugin_name] = driver + AUTH_PLUGINS_LOADED = True + + +def get_auth_method(method_name): + global AUTH_METHODS + if method_name not in AUTH_METHODS: + raise exception.AuthMethodNotSupported() + return AUTH_METHODS[method_name] + + +class AuthContext(dict): + """Retrofitting auth_context to reconcile identity attributes. + + The identity attributes must not have conflicting values among the + auth plug-ins. The only exception is `expires_at`, which is set to its + earliest value. + + """ + + # identity attributes need to be reconciled among the auth plugins + IDENTITY_ATTRIBUTES = frozenset(['user_id', 'project_id', + 'access_token_id', 'domain_id', + 'expires_at']) + + def __setitem__(self, key, val): + if key in self.IDENTITY_ATTRIBUTES and key in self: + existing_val = self[key] + if key == 'expires_at': + # special treatment for 'expires_at', we are going to take + # the earliest expiration instead. + if existing_val != val: + LOG.info(_LI('"expires_at" has conflicting values ' + '%(existing)s and %(new)s. Will use the ' + 'earliest value.'), + {'existing': existing_val, 'new': val}) + if existing_val is None or val is None: + val = existing_val or val + else: + val = min(existing_val, val) + elif existing_val != val: + msg = _('Unable to reconcile identity attribute %(attribute)s ' + 'as it has conflicting values %(new)s and %(old)s') % ( + {'attribute': key, + 'new': val, + 'old': existing_val}) + raise exception.Unauthorized(msg) + return super(AuthContext, self).__setitem__(key, val) + + +# TODO(blk-u): this class doesn't use identity_api directly, but makes it +# available for consumers. Consumers should probably not be getting +# identity_api from this since it's available in global registry, then +# identity_api should be removed from this list. +@dependency.requires('identity_api', 'resource_api', 'trust_api') +class AuthInfo(object): + """Encapsulation of "auth" request.""" + + @staticmethod + def create(context, auth=None): + auth_info = AuthInfo(context, auth=auth) + auth_info._validate_and_normalize_auth_data() + return auth_info + + def __init__(self, context, auth=None): + self.context = context + self.auth = auth + self._scope_data = (None, None, None, None) + # self._scope_data is (domain_id, project_id, trust_ref, unscoped) + # project scope: (None, project_id, None, None) + # domain scope: (domain_id, None, None, None) + # trust scope: (None, None, trust_ref, None) + # unscoped: (None, None, None, 'unscoped') + + def _assert_project_is_enabled(self, project_ref): + # ensure the project is enabled + try: + self.resource_api.assert_project_enabled( + project_id=project_ref['id'], + project=project_ref) + except AssertionError as e: + LOG.warning(six.text_type(e)) + six.reraise(exception.Unauthorized, exception.Unauthorized(e), + sys.exc_info()[2]) + + def _assert_domain_is_enabled(self, domain_ref): + try: + self.resource_api.assert_domain_enabled( + domain_id=domain_ref['id'], + domain=domain_ref) + except AssertionError as e: + LOG.warning(six.text_type(e)) + six.reraise(exception.Unauthorized, exception.Unauthorized(e), + sys.exc_info()[2]) + + def _lookup_domain(self, domain_info): + domain_id = domain_info.get('id') + domain_name = domain_info.get('name') + domain_ref = None + if not domain_id and not domain_name: + raise exception.ValidationError(attribute='id or name', + target='domain') + try: + if domain_name: + domain_ref = self.resource_api.get_domain_by_name( + domain_name) + else: + domain_ref = self.resource_api.get_domain(domain_id) + except exception.DomainNotFound as e: + LOG.exception(six.text_type(e)) + raise exception.Unauthorized(e) + self._assert_domain_is_enabled(domain_ref) + return domain_ref + + def _lookup_project(self, project_info): + project_id = project_info.get('id') + project_name = project_info.get('name') + project_ref = None + if not project_id and not project_name: + raise exception.ValidationError(attribute='id or name', + target='project') + try: + if project_name: + if 'domain' not in project_info: + raise exception.ValidationError(attribute='domain', + target='project') + domain_ref = self._lookup_domain(project_info['domain']) + project_ref = self.resource_api.get_project_by_name( + project_name, domain_ref['id']) + else: + project_ref = self.resource_api.get_project(project_id) + # NOTE(morganfainberg): The _lookup_domain method will raise + # exception.Unauthorized if the domain isn't found or is + # disabled. + self._lookup_domain({'id': project_ref['domain_id']}) + except exception.ProjectNotFound as e: + raise exception.Unauthorized(e) + self._assert_project_is_enabled(project_ref) + return project_ref + + def _lookup_trust(self, trust_info): + trust_id = trust_info.get('id') + if not trust_id: + raise exception.ValidationError(attribute='trust_id', + target='trust') + trust = self.trust_api.get_trust(trust_id) + if not trust: + raise exception.TrustNotFound(trust_id=trust_id) + return trust + + def _validate_and_normalize_scope_data(self): + """Validate and normalize scope data.""" + if 'scope' not in self.auth: + return + if sum(['project' in self.auth['scope'], + 'domain' in self.auth['scope'], + 'unscoped' in self.auth['scope'], + 'OS-TRUST:trust' in self.auth['scope']]) != 1: + raise exception.ValidationError( + attribute='project, domain, OS-TRUST:trust or unscoped', + target='scope') + if 'unscoped' in self.auth['scope']: + self._scope_data = (None, None, None, 'unscoped') + return + if 'project' in self.auth['scope']: + project_ref = self._lookup_project(self.auth['scope']['project']) + self._scope_data = (None, project_ref['id'], None, None) + elif 'domain' in self.auth['scope']: + domain_ref = self._lookup_domain(self.auth['scope']['domain']) + self._scope_data = (domain_ref['id'], None, None, None) + elif 'OS-TRUST:trust' in self.auth['scope']: + if not CONF.trust.enabled: + raise exception.Forbidden('Trusts are disabled.') + trust_ref = self._lookup_trust( + self.auth['scope']['OS-TRUST:trust']) + # TODO(ayoung): when trusts support domains, fill in domain data + if trust_ref.get('project_id') is not None: + project_ref = self._lookup_project( + {'id': trust_ref['project_id']}) + self._scope_data = (None, project_ref['id'], trust_ref, None) + else: + self._scope_data = (None, None, trust_ref, None) + + def _validate_auth_methods(self): + if 'identity' not in self.auth: + raise exception.ValidationError(attribute='identity', + target='auth') + + # make sure auth methods are provided + if 'methods' not in self.auth['identity']: + raise exception.ValidationError(attribute='methods', + target='identity') + + # make sure all the method data/payload are provided + for method_name in self.get_method_names(): + if method_name not in self.auth['identity']: + raise exception.ValidationError(attribute=method_name, + target='identity') + + # make sure auth method is supported + for method_name in self.get_method_names(): + if method_name not in AUTH_METHODS: + raise exception.AuthMethodNotSupported() + + def _validate_and_normalize_auth_data(self): + """Make sure "auth" is valid.""" + # make sure "auth" exist + if not self.auth: + raise exception.ValidationError(attribute='auth', + target='request body') + + self._validate_auth_methods() + self._validate_and_normalize_scope_data() + + def get_method_names(self): + """Returns the identity method names. + + :returns: list of auth method names + + """ + # Sanitizes methods received in request's body + # Filters out duplicates, while keeping elements' order. + method_names = [] + for method in self.auth['identity']['methods']: + if method not in method_names: + method_names.append(method) + return method_names + + def get_method_data(self, method): + """Get the auth method payload. + + :returns: auth method payload + + """ + if method not in self.auth['identity']['methods']: + raise exception.ValidationError(attribute=method, + target='identity') + return self.auth['identity'][method] + + def get_scope(self): + """Get scope information. + + Verify and return the scoping information. + + :returns: (domain_id, project_id, trust_ref, unscoped). + If scope to a project, (None, project_id, None, None) + will be returned. + If scoped to a domain, (domain_id, None, None, None) + will be returned. + If scoped to a trust, (None, project_id, trust_ref, None), + Will be returned, where the project_id comes from the + trust definition. + If unscoped, (None, None, None, 'unscoped') will be + returned. + + """ + return self._scope_data + + def set_scope(self, domain_id=None, project_id=None, trust=None, + unscoped=None): + """Set scope information.""" + if domain_id and project_id: + msg = _('Scoping to both domain and project is not allowed') + raise ValueError(msg) + if domain_id and trust: + msg = _('Scoping to both domain and trust is not allowed') + raise ValueError(msg) + if project_id and trust: + msg = _('Scoping to both project and trust is not allowed') + raise ValueError(msg) + self._scope_data = (domain_id, project_id, trust, unscoped) + + +@dependency.requires('assignment_api', 'catalog_api', 'identity_api', + 'resource_api', 'token_provider_api', 'trust_api') +class Auth(controller.V3Controller): + + # Note(atiwari): From V3 auth controller code we are + # calling protection() wrappers, so we need to setup + # the member_name and collection_name attributes of + # auth controller code. + # In the absence of these attributes, default 'entity' + # string will be used to represent the target which is + # generic. Policy can be defined using 'entity' but it + # would not reflect the exact entity that is in context. + # We are defining collection_name = 'tokens' and + # member_name = 'token' to facilitate policy decisions. + collection_name = 'tokens' + member_name = 'token' + + def __init__(self, *args, **kw): + super(Auth, self).__init__(*args, **kw) + config.setup_authentication() + + def authenticate_for_token(self, context, auth=None): + """Authenticate user and issue a token.""" + include_catalog = 'nocatalog' not in context['query_string'] + + try: + auth_info = AuthInfo.create(context, auth=auth) + auth_context = AuthContext(extras={}, + method_names=[], + bind={}) + self.authenticate(context, auth_info, auth_context) + if auth_context.get('access_token_id'): + auth_info.set_scope(None, auth_context['project_id'], None) + self._check_and_set_default_scoping(auth_info, auth_context) + (domain_id, project_id, trust, unscoped) = auth_info.get_scope() + + method_names = auth_info.get_method_names() + method_names += auth_context.get('method_names', []) + # make sure the list is unique + method_names = list(set(method_names)) + expires_at = auth_context.get('expires_at') + # NOTE(morganfainberg): define this here so it is clear what the + # argument is during the issue_v3_token provider call. + metadata_ref = None + + token_audit_id = auth_context.get('audit_id') + + (token_id, token_data) = self.token_provider_api.issue_v3_token( + auth_context['user_id'], method_names, expires_at, project_id, + domain_id, auth_context, trust, metadata_ref, include_catalog, + parent_audit_id=token_audit_id) + + # NOTE(wanghong): We consume a trust use only when we are using + # trusts and have successfully issued a token. + if trust: + self.trust_api.consume_use(trust['id']) + + return render_token_data_response(token_id, token_data, + created=True) + except exception.TrustNotFound as e: + raise exception.Unauthorized(e) + + def _check_and_set_default_scoping(self, auth_info, auth_context): + (domain_id, project_id, trust, unscoped) = auth_info.get_scope() + if trust: + project_id = trust['project_id'] + if domain_id or project_id or trust: + # scope is specified + return + + # Skip scoping when unscoped federated token is being issued + if federation.IDENTITY_PROVIDER in auth_context: + return + + # Do not scope if request is for explicitly unscoped token + if unscoped is not None: + return + + # fill in default_project_id if it is available + try: + user_ref = self.identity_api.get_user(auth_context['user_id']) + except exception.UserNotFound as e: + LOG.exception(six.text_type(e)) + raise exception.Unauthorized(e) + + default_project_id = user_ref.get('default_project_id') + if not default_project_id: + # User has no default project. He shall get an unscoped token. + return + + # make sure user's default project is legit before scoping to it + try: + default_project_ref = self.resource_api.get_project( + default_project_id) + default_project_domain_ref = self.resource_api.get_domain( + default_project_ref['domain_id']) + if (default_project_ref.get('enabled', True) and + default_project_domain_ref.get('enabled', True)): + if self.assignment_api.get_roles_for_user_and_project( + user_ref['id'], default_project_id): + auth_info.set_scope(project_id=default_project_id) + else: + msg = _LW("User %(user_id)s doesn't have access to" + " default project %(project_id)s. The token" + " will be unscoped rather than scoped to the" + " project.") + LOG.warning(msg, + {'user_id': user_ref['id'], + 'project_id': default_project_id}) + else: + msg = _LW("User %(user_id)s's default project %(project_id)s" + " is disabled. The token will be unscoped rather" + " than scoped to the project.") + LOG.warning(msg, + {'user_id': user_ref['id'], + 'project_id': default_project_id}) + except (exception.ProjectNotFound, exception.DomainNotFound): + # default project or default project domain doesn't exist, + # will issue unscoped token instead + msg = _LW("User %(user_id)s's default project %(project_id)s not" + " found. The token will be unscoped rather than" + " scoped to the project.") + LOG.warning(msg, {'user_id': user_ref['id'], + 'project_id': default_project_id}) + + def authenticate(self, context, auth_info, auth_context): + """Authenticate user.""" + + # The 'external' method allows any 'REMOTE_USER' based authentication + # In some cases the server can set REMOTE_USER as '' instead of + # dropping it, so this must be filtered out + if context['environment'].get('REMOTE_USER'): + try: + external = get_auth_method('external') + external.authenticate(context, auth_info, auth_context) + except exception.AuthMethodNotSupported: + # This will happen there is no 'external' plugin registered + # and the container is performing authentication. + # The 'kerberos' and 'saml' methods will be used this way. + # In those cases, it is correct to not register an + # 'external' plugin; if there is both an 'external' and a + # 'kerberos' plugin, it would run the check on identity twice. + LOG.debug("No 'external' plugin is registered.") + except exception.Unauthorized: + # If external fails then continue and attempt to determine + # user identity using remaining auth methods + LOG.debug("Authorization failed for 'external' auth method.") + + # need to aggregate the results in case two or more methods + # are specified + auth_response = {'methods': []} + for method_name in auth_info.get_method_names(): + method = get_auth_method(method_name) + resp = method.authenticate(context, + auth_info.get_method_data(method_name), + auth_context) + if resp: + auth_response['methods'].append(method_name) + auth_response[method_name] = resp + + if auth_response["methods"]: + # authentication continuation required + raise exception.AdditionalAuthRequired(auth_response) + + if 'user_id' not in auth_context: + msg = _('User not found') + raise exception.Unauthorized(msg) + + @controller.protected() + def check_token(self, context): + token_id = context.get('subject_token_id') + token_data = self.token_provider_api.validate_v3_token( + token_id) + # NOTE(morganfainberg): The code in + # ``keystone.common.wsgi.render_response`` will remove the content + # body. + return render_token_data_response(token_id, token_data) + + @controller.protected() + def revoke_token(self, context): + token_id = context.get('subject_token_id') + return self.token_provider_api.revoke_token(token_id) + + @controller.protected() + def validate_token(self, context): + token_id = context.get('subject_token_id') + include_catalog = 'nocatalog' not in context['query_string'] + token_data = self.token_provider_api.validate_v3_token( + token_id) + if not include_catalog and 'catalog' in token_data['token']: + del token_data['token']['catalog'] + return render_token_data_response(token_id, token_data) + + @controller.protected() + def revocation_list(self, context, auth=None): + if not CONF.token.revoke_by_id: + raise exception.Gone() + tokens = self.token_provider_api.list_revoked_tokens() + + for t in tokens: + expires = t['expires'] + if not (expires and isinstance(expires, six.text_type)): + t['expires'] = timeutils.isotime(expires) + data = {'revoked': tokens} + json_data = jsonutils.dumps(data) + signed_text = cms.cms_sign_text(json_data, + CONF.signing.certfile, + CONF.signing.keyfile) + + return {'signed': signed_text} + + def _combine_lists_uniquely(self, a, b): + # it's most likely that only one of these will be filled so avoid + # the combination if possible. + if a and b: + return {x['id']: x for x in a + b}.values() + else: + return a or b + + @controller.protected() + def get_auth_projects(self, context): + auth_context = self.get_auth_context(context) + + user_id = auth_context.get('user_id') + user_refs = [] + if user_id: + try: + user_refs = self.assignment_api.list_projects_for_user(user_id) + except exception.UserNotFound: + # federated users have an id but they don't link to anything + pass + + group_ids = auth_context.get('group_ids') + grp_refs = [] + if group_ids: + grp_refs = self.assignment_api.list_projects_for_groups(group_ids) + + refs = self._combine_lists_uniquely(user_refs, grp_refs) + return resource_controllers.ProjectV3.wrap_collection(context, refs) + + @controller.protected() + def get_auth_domains(self, context): + auth_context = self.get_auth_context(context) + + user_id = auth_context.get('user_id') + user_refs = [] + if user_id: + try: + user_refs = self.assignment_api.list_domains_for_user(user_id) + except exception.UserNotFound: + # federated users have an id but they don't link to anything + pass + + group_ids = auth_context.get('group_ids') + grp_refs = [] + if group_ids: + grp_refs = self.assignment_api.list_domains_for_groups(group_ids) + + refs = self._combine_lists_uniquely(user_refs, grp_refs) + return resource_controllers.DomainV3.wrap_collection(context, refs) + + @controller.protected() + def get_auth_catalog(self, context): + auth_context = self.get_auth_context(context) + user_id = auth_context.get('user_id') + project_id = auth_context.get('project_id') + + if not project_id: + raise exception.Forbidden( + _('A project-scoped token is required to produce a service ' + 'catalog.')) + + # The V3Controller base methods mostly assume that you're returning + # either a collection or a single element from a collection, neither of + # which apply to the catalog. Because this is a special case, this + # re-implements a tiny bit of work done by the base controller (such as + # self-referential link building) to avoid overriding or refactoring + # several private methods. + return { + 'catalog': self.catalog_api.get_v3_catalog(user_id, project_id), + 'links': {'self': self.base_url(context, path='auth/catalog')} + } + + +# FIXME(gyee): not sure if it belongs here or keystone.common. Park it here +# for now. +def render_token_data_response(token_id, token_data, created=False): + """Render token data HTTP response. + + Stash token ID into the X-Subject-Token header. + + """ + headers = [('X-Subject-Token', token_id)] + + if created: + status = (201, 'Created') + else: + status = (200, 'OK') + + return wsgi.render_response(body=token_data, + status=status, headers=headers) diff --git a/keystone-moon/keystone/auth/core.py b/keystone-moon/keystone/auth/core.py new file mode 100644 index 00000000..9da2c123 --- /dev/null +++ b/keystone-moon/keystone/auth/core.py @@ -0,0 +1,94 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import abc + +import six + +from keystone import exception + + +@six.add_metaclass(abc.ABCMeta) +class AuthMethodHandler(object): + """Abstract base class for an authentication plugin.""" + + def __init__(self): + pass + + @abc.abstractmethod + def authenticate(self, context, auth_payload, auth_context): + """Authenticate user and return an authentication context. + + :param context: keystone's request context + :param auth_payload: the content of the authentication for a given + method + :param auth_context: user authentication context, a dictionary shared + by all plugins. It contains "method_names" and + "extras" by default. "method_names" is a list and + "extras" is a dictionary. + + If successful, plugin must set ``user_id`` in ``auth_context``. + ``method_name`` is used to convey any additional authentication methods + in case authentication is for re-scoping. For example, if the + authentication is for re-scoping, plugin must append the previous + method names into ``method_names``. Also, plugin may add any additional + information into ``extras``. Anything in ``extras`` will be conveyed in + the token's ``extras`` attribute. Here's an example of ``auth_context`` + on successful authentication:: + + { + "extras": {}, + "methods": [ + "password", + "token" + ], + "user_id": "abc123" + } + + Plugins are invoked in the order in which they are specified in the + ``methods`` attribute of the ``identity`` object. For example, + ``custom-plugin`` is invoked before ``password``, which is invoked + before ``token`` in the following authentication request:: + + { + "auth": { + "identity": { + "custom-plugin": { + "custom-data": "sdfdfsfsfsdfsf" + }, + "methods": [ + "custom-plugin", + "password", + "token" + ], + "password": { + "user": { + "id": "s23sfad1", + "password": "secrete" + } + }, + "token": { + "id": "sdfafasdfsfasfasdfds" + } + } + } + } + + :returns: None if authentication is successful. + Authentication payload in the form of a dictionary for the + next authentication step if this is a multi step + authentication. + :raises: exception.Unauthorized for authentication failure + """ + raise exception.Unauthorized() diff --git a/keystone-moon/keystone/auth/plugins/__init__.py b/keystone-moon/keystone/auth/plugins/__init__.py new file mode 100644 index 00000000..5da54703 --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2015 CERN +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystone.auth.plugins.core import * # noqa diff --git a/keystone-moon/keystone/auth/plugins/core.py b/keystone-moon/keystone/auth/plugins/core.py new file mode 100644 index 00000000..96a5ecf8 --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/core.py @@ -0,0 +1,186 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys + +from oslo_config import cfg +from oslo_log import log +import six + +from keystone.common import dependency +from keystone import exception + +CONF = cfg.CONF +LOG = log.getLogger(__name__) + + +def construct_method_map_from_config(): + """Determine authentication method types for deployment. + + :returns: a dictionary containing the methods and their indexes + + """ + method_map = dict() + method_index = 1 + for method in CONF.auth.methods: + method_map[method_index] = method + method_index = method_index * 2 + + return method_map + + +def convert_method_list_to_integer(methods): + """Convert the method type(s) to an integer. + + :param methods: a list of method names + :returns: an integer representing the methods + + """ + method_map = construct_method_map_from_config() + + method_ints = [] + for method in methods: + for k, v in six.iteritems(method_map): + if v == method: + method_ints.append(k) + return sum(method_ints) + + +def convert_integer_to_method_list(method_int): + """Convert an integer to a list of methods. + + :param method_int: an integer representing methods + :returns: a corresponding list of methods + + """ + # If the method_int is 0 then no methods were used so return an empty + # method list + if method_int == 0: + return [] + + method_map = construct_method_map_from_config() + method_ints = [] + for k, v in six.iteritems(method_map): + method_ints.append(k) + method_ints.sort(reverse=True) + + confirmed_methods = [] + for m_int in method_ints: + # (lbragstad): By dividing the method_int by each key in the + # method_map, we know if the division results in an integer of 1, that + # key was used in the construction of the total sum of the method_int. + # In that case, we should confirm the key value and store it so we can + # look it up later. Then we should take the remainder of what is + # confirmed and the method_int and continue the process. In the end, we + # should have a list of integers that correspond to indexes in our + # method_map and we can reinflate the methods that the original + # method_int represents. + if (method_int / m_int) == 1: + confirmed_methods.append(m_int) + method_int = method_int - m_int + + methods = [] + for method in confirmed_methods: + methods.append(method_map[method]) + + return methods + + +@dependency.requires('identity_api', 'resource_api') +class UserAuthInfo(object): + + @staticmethod + def create(auth_payload, method_name): + user_auth_info = UserAuthInfo() + user_auth_info._validate_and_normalize_auth_data(auth_payload) + user_auth_info.METHOD_NAME = method_name + return user_auth_info + + def __init__(self): + self.user_id = None + self.password = None + self.user_ref = None + self.METHOD_NAME = None + + def _assert_domain_is_enabled(self, domain_ref): + try: + self.resource_api.assert_domain_enabled( + domain_id=domain_ref['id'], + domain=domain_ref) + except AssertionError as e: + LOG.warning(six.text_type(e)) + six.reraise(exception.Unauthorized, exception.Unauthorized(e), + sys.exc_info()[2]) + + def _assert_user_is_enabled(self, user_ref): + try: + self.identity_api.assert_user_enabled( + user_id=user_ref['id'], + user=user_ref) + except AssertionError as e: + LOG.warning(six.text_type(e)) + six.reraise(exception.Unauthorized, exception.Unauthorized(e), + sys.exc_info()[2]) + + def _lookup_domain(self, domain_info): + domain_id = domain_info.get('id') + domain_name = domain_info.get('name') + domain_ref = None + if not domain_id and not domain_name: + raise exception.ValidationError(attribute='id or name', + target='domain') + try: + if domain_name: + domain_ref = self.resource_api.get_domain_by_name( + domain_name) + else: + domain_ref = self.resource_api.get_domain(domain_id) + except exception.DomainNotFound as e: + LOG.exception(six.text_type(e)) + raise exception.Unauthorized(e) + self._assert_domain_is_enabled(domain_ref) + return domain_ref + + def _validate_and_normalize_auth_data(self, auth_payload): + if 'user' not in auth_payload: + raise exception.ValidationError(attribute='user', + target=self.METHOD_NAME) + user_info = auth_payload['user'] + user_id = user_info.get('id') + user_name = user_info.get('name') + user_ref = None + if not user_id and not user_name: + raise exception.ValidationError(attribute='id or name', + target='user') + self.password = user_info.get('password') + try: + if user_name: + if 'domain' not in user_info: + raise exception.ValidationError(attribute='domain', + target='user') + domain_ref = self._lookup_domain(user_info['domain']) + user_ref = self.identity_api.get_user_by_name( + user_name, domain_ref['id']) + else: + user_ref = self.identity_api.get_user(user_id) + domain_ref = self.resource_api.get_domain( + user_ref['domain_id']) + self._assert_domain_is_enabled(domain_ref) + except exception.UserNotFound as e: + LOG.exception(six.text_type(e)) + raise exception.Unauthorized(e) + self._assert_user_is_enabled(user_ref) + self.user_ref = user_ref + self.user_id = user_ref['id'] + self.domain_id = domain_ref['id'] diff --git a/keystone-moon/keystone/auth/plugins/external.py b/keystone-moon/keystone/auth/plugins/external.py new file mode 100644 index 00000000..2322649f --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/external.py @@ -0,0 +1,186 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Keystone External Authentication Plugins""" + +import abc + +from oslo_config import cfg +import six + +from keystone import auth +from keystone.common import dependency +from keystone import exception +from keystone.i18n import _ +from keystone.openstack.common import versionutils + + +CONF = cfg.CONF + + +@six.add_metaclass(abc.ABCMeta) +class Base(auth.AuthMethodHandler): + + method = 'external' + + def authenticate(self, context, auth_info, auth_context): + """Use REMOTE_USER to look up the user in the identity backend. + + auth_context is an in-out variable that will be updated with the + user_id from the actual user from the REMOTE_USER env variable. + """ + try: + REMOTE_USER = context['environment']['REMOTE_USER'] + except KeyError: + msg = _('No authenticated user') + raise exception.Unauthorized(msg) + try: + user_ref = self._authenticate(REMOTE_USER, context) + auth_context['user_id'] = user_ref['id'] + if ('kerberos' in CONF.token.bind and + (context['environment'].get('AUTH_TYPE', '').lower() + == 'negotiate')): + auth_context['bind']['kerberos'] = user_ref['name'] + except Exception: + msg = _('Unable to lookup user %s') % (REMOTE_USER) + raise exception.Unauthorized(msg) + + @abc.abstractmethod + def _authenticate(self, remote_user, context): + """Look up the user in the identity backend. + + Return user_ref + """ + pass + + +@dependency.requires('identity_api') +class DefaultDomain(Base): + def _authenticate(self, remote_user, context): + """Use remote_user to look up the user in the identity backend.""" + domain_id = CONF.identity.default_domain_id + user_ref = self.identity_api.get_user_by_name(remote_user, domain_id) + return user_ref + + +@dependency.requires('identity_api', 'resource_api') +class Domain(Base): + def _authenticate(self, remote_user, context): + """Use remote_user to look up the user in the identity backend. + + The domain will be extracted from the REMOTE_DOMAIN environment + variable if present. If not, the default domain will be used. + """ + + username = remote_user + try: + domain_name = context['environment']['REMOTE_DOMAIN'] + except KeyError: + domain_id = CONF.identity.default_domain_id + else: + domain_ref = self.resource_api.get_domain_by_name(domain_name) + domain_id = domain_ref['id'] + + user_ref = self.identity_api.get_user_by_name(username, domain_id) + return user_ref + + +@dependency.requires('assignment_api', 'identity_api') +class KerberosDomain(Domain): + """Allows `kerberos` as a method.""" + method = 'kerberos' + + def _authenticate(self, remote_user, context): + auth_type = context['environment'].get('AUTH_TYPE') + if auth_type != 'Negotiate': + raise exception.Unauthorized(_("auth_type is not Negotiate")) + return super(KerberosDomain, self)._authenticate(remote_user, context) + + +class ExternalDefault(DefaultDomain): + """Deprecated. Please use keystone.auth.external.DefaultDomain instead.""" + + @versionutils.deprecated( + as_of=versionutils.deprecated.ICEHOUSE, + in_favor_of='keystone.auth.external.DefaultDomain', + remove_in=+1) + def __init__(self): + super(ExternalDefault, self).__init__() + + +class ExternalDomain(Domain): + """Deprecated. Please use keystone.auth.external.Domain instead.""" + + @versionutils.deprecated( + as_of=versionutils.deprecated.ICEHOUSE, + in_favor_of='keystone.auth.external.Domain', + remove_in=+1) + def __init__(self): + super(ExternalDomain, self).__init__() + + +@dependency.requires('identity_api') +class LegacyDefaultDomain(Base): + """Deprecated. Please use keystone.auth.external.DefaultDomain instead. + + This plugin exists to provide compatibility for the unintended behavior + described here: https://bugs.launchpad.net/keystone/+bug/1253484 + + """ + + @versionutils.deprecated( + as_of=versionutils.deprecated.ICEHOUSE, + in_favor_of='keystone.auth.external.DefaultDomain', + remove_in=+1) + def __init__(self): + super(LegacyDefaultDomain, self).__init__() + + def _authenticate(self, remote_user, context): + """Use remote_user to look up the user in the identity backend.""" + # NOTE(dolph): this unintentionally discards half the REMOTE_USER value + names = remote_user.split('@') + username = names.pop(0) + domain_id = CONF.identity.default_domain_id + user_ref = self.identity_api.get_user_by_name(username, domain_id) + return user_ref + + +@dependency.requires('identity_api', 'resource_api') +class LegacyDomain(Base): + """Deprecated. Please use keystone.auth.external.Domain instead.""" + + @versionutils.deprecated( + as_of=versionutils.deprecated.ICEHOUSE, + in_favor_of='keystone.auth.external.Domain', + remove_in=+1) + def __init__(self): + super(LegacyDomain, self).__init__() + + def _authenticate(self, remote_user, context): + """Use remote_user to look up the user in the identity backend. + + If remote_user contains an `@` assume that the substring before the + rightmost `@` is the username, and the substring after the @ is the + domain name. + """ + names = remote_user.rsplit('@', 1) + username = names.pop(0) + if names: + domain_name = names[0] + domain_ref = self.resource_api.get_domain_by_name(domain_name) + domain_id = domain_ref['id'] + else: + domain_id = CONF.identity.default_domain_id + user_ref = self.identity_api.get_user_by_name(username, domain_id) + return user_ref diff --git a/keystone-moon/keystone/auth/plugins/mapped.py b/keystone-moon/keystone/auth/plugins/mapped.py new file mode 100644 index 00000000..abf44481 --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/mapped.py @@ -0,0 +1,252 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import functools + +from oslo_log import log +from oslo_serialization import jsonutils +from pycadf import cadftaxonomy as taxonomy +from six.moves.urllib import parse + +from keystone import auth +from keystone.auth import plugins as auth_plugins +from keystone.common import dependency +from keystone.contrib import federation +from keystone.contrib.federation import utils +from keystone import exception +from keystone.i18n import _ +from keystone.models import token_model +from keystone import notifications + + +LOG = log.getLogger(__name__) + +METHOD_NAME = 'mapped' + + +@dependency.requires('assignment_api', 'federation_api', 'identity_api', + 'token_provider_api') +class Mapped(auth.AuthMethodHandler): + + def _get_token_ref(self, auth_payload): + token_id = auth_payload['id'] + response = self.token_provider_api.validate_token(token_id) + return token_model.KeystoneToken(token_id=token_id, + token_data=response) + + def authenticate(self, context, auth_payload, auth_context): + """Authenticate mapped user and return an authentication context. + + :param context: keystone's request context + :param auth_payload: the content of the authentication for a + given method + :param auth_context: user authentication context, a dictionary + shared by all plugins. + + In addition to ``user_id`` in ``auth_context``, this plugin sets + ``group_ids``, ``OS-FEDERATION:identity_provider`` and + ``OS-FEDERATION:protocol`` + + """ + + if 'id' in auth_payload: + token_ref = self._get_token_ref(auth_payload) + handle_scoped_token(context, auth_payload, auth_context, token_ref, + self.federation_api, + self.identity_api, + self.token_provider_api) + else: + handle_unscoped_token(context, auth_payload, auth_context, + self.assignment_api, self.federation_api, + self.identity_api) + + +def handle_scoped_token(context, auth_payload, auth_context, token_ref, + federation_api, identity_api, token_provider_api): + utils.validate_expiration(token_ref) + token_audit_id = token_ref.audit_id + identity_provider = token_ref.federation_idp_id + protocol = token_ref.federation_protocol_id + user_id = token_ref.user_id + group_ids = token_ref.federation_group_ids + send_notification = functools.partial( + notifications.send_saml_audit_notification, 'authenticate', + context, user_id, group_ids, identity_provider, protocol, + token_audit_id) + + utils.assert_enabled_identity_provider(federation_api, identity_provider) + + try: + mapping = federation_api.get_mapping_from_idp_and_protocol( + identity_provider, protocol) + utils.validate_groups(group_ids, mapping['id'], identity_api) + + except Exception: + # NOTE(topol): Diaper defense to catch any exception, so we can + # send off failed authentication notification, raise the exception + # after sending the notification + send_notification(taxonomy.OUTCOME_FAILURE) + raise + else: + send_notification(taxonomy.OUTCOME_SUCCESS) + + auth_context['user_id'] = user_id + auth_context['group_ids'] = group_ids + auth_context[federation.IDENTITY_PROVIDER] = identity_provider + auth_context[federation.PROTOCOL] = protocol + + +def handle_unscoped_token(context, auth_payload, auth_context, + assignment_api, federation_api, identity_api): + + def is_ephemeral_user(mapped_properties): + return mapped_properties['user']['type'] == utils.UserType.EPHEMERAL + + def build_ephemeral_user_context(auth_context, user, mapped_properties, + identity_provider, protocol): + auth_context['user_id'] = user['id'] + auth_context['group_ids'] = mapped_properties['group_ids'] + auth_context[federation.IDENTITY_PROVIDER] = identity_provider + auth_context[federation.PROTOCOL] = protocol + + def build_local_user_context(auth_context, mapped_properties): + user_info = auth_plugins.UserAuthInfo.create(mapped_properties, + METHOD_NAME) + auth_context['user_id'] = user_info.user_id + + assertion = extract_assertion_data(context) + identity_provider = auth_payload['identity_provider'] + protocol = auth_payload['protocol'] + + utils.assert_enabled_identity_provider(federation_api, identity_provider) + + group_ids = None + # NOTE(topol): The user is coming in from an IdP with a SAML assertion + # instead of from a token, so we set token_id to None + token_id = None + # NOTE(marek-denis): This variable is set to None and there is a + # possibility that it will be used in the CADF notification. This means + # operation will not be mapped to any user (even ephemeral). + user_id = None + + try: + mapped_properties = apply_mapping_filter( + identity_provider, protocol, assertion, assignment_api, + federation_api, identity_api) + + if is_ephemeral_user(mapped_properties): + user = setup_username(context, mapped_properties) + user_id = user['id'] + group_ids = mapped_properties['group_ids'] + mapping = federation_api.get_mapping_from_idp_and_protocol( + identity_provider, protocol) + utils.validate_groups_cardinality(group_ids, mapping['id']) + build_ephemeral_user_context(auth_context, user, + mapped_properties, + identity_provider, protocol) + else: + build_local_user_context(auth_context, mapped_properties) + + except Exception: + # NOTE(topol): Diaper defense to catch any exception, so we can + # send off failed authentication notification, raise the exception + # after sending the notification + outcome = taxonomy.OUTCOME_FAILURE + notifications.send_saml_audit_notification('authenticate', context, + user_id, group_ids, + identity_provider, + protocol, token_id, + outcome) + raise + else: + outcome = taxonomy.OUTCOME_SUCCESS + notifications.send_saml_audit_notification('authenticate', context, + user_id, group_ids, + identity_provider, + protocol, token_id, + outcome) + + +def extract_assertion_data(context): + assertion = dict(utils.get_assertion_params_from_env(context)) + return assertion + + +def apply_mapping_filter(identity_provider, protocol, assertion, + assignment_api, federation_api, identity_api): + idp = federation_api.get_idp(identity_provider) + utils.validate_idp(idp, assertion) + mapping = federation_api.get_mapping_from_idp_and_protocol( + identity_provider, protocol) + rules = jsonutils.loads(mapping['rules']) + LOG.debug('using the following rules: %s', rules) + rule_processor = utils.RuleProcessor(rules) + mapped_properties = rule_processor.process(assertion) + + # NOTE(marek-denis): We update group_ids only here to avoid fetching + # groups identified by name/domain twice. + # NOTE(marek-denis): Groups are translated from name/domain to their + # corresponding ids in the auth plugin, as we need information what + # ``mapping_id`` was used as well as idenity_api and assignment_api + # objects. + group_ids = mapped_properties['group_ids'] + utils.validate_groups_in_backend(group_ids, + mapping['id'], + identity_api) + group_ids.extend( + utils.transform_to_group_ids( + mapped_properties['group_names'], mapping['id'], + identity_api, assignment_api)) + mapped_properties['group_ids'] = list(set(group_ids)) + return mapped_properties + + +def setup_username(context, mapped_properties): + """Setup federated username. + + Function covers all the cases for properly setting user id, a primary + identifier for identity objects. Initial version of the mapping engine + assumed user is identified by ``name`` and his ``id`` is built from the + name. We, however need to be able to accept local rules that identify user + by either id or name/domain. + + The following use-cases are covered: + + 1) If neither user_name nor user_id is set raise exception.Unauthorized + 2) If user_id is set and user_name not, set user_name equal to user_id + 3) If user_id is not set and user_name is, set user_id as url safe version + of user_name. + + :param context: authentication context + :param mapped_properties: Properties issued by a RuleProcessor. + :type: dictionary + + :raises: exception.Unauthorized + :returns: dictionary with user identification + :rtype: dict + + """ + user = mapped_properties['user'] + + user_id = user.get('id') + user_name = user.get('name') or context['environment'].get('REMOTE_USER') + + if not any([user_id, user_name]): + raise exception.Unauthorized(_("Could not map user")) + + elif not user_name: + user['name'] = user_id + + elif not user_id: + user['id'] = parse.quote(user_name) + + return user diff --git a/keystone-moon/keystone/auth/plugins/oauth1.py b/keystone-moon/keystone/auth/plugins/oauth1.py new file mode 100644 index 00000000..2f1cc2fa --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/oauth1.py @@ -0,0 +1,75 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log +from oslo_utils import timeutils + +from keystone import auth +from keystone.common import controller +from keystone.common import dependency +from keystone.contrib.oauth1 import core as oauth +from keystone.contrib.oauth1 import validator +from keystone import exception +from keystone.i18n import _ + + +LOG = log.getLogger(__name__) + + +@dependency.requires('oauth_api') +class OAuth(auth.AuthMethodHandler): + + method = 'oauth1' + + def authenticate(self, context, auth_info, auth_context): + """Turn a signed request with an access key into a keystone token.""" + + if not self.oauth_api: + raise exception.Unauthorized(_('%s not supported') % self.method) + + headers = context['headers'] + oauth_headers = oauth.get_oauth_headers(headers) + access_token_id = oauth_headers.get('oauth_token') + + if not access_token_id: + raise exception.ValidationError( + attribute='oauth_token', target='request') + + acc_token = self.oauth_api.get_access_token(access_token_id) + + expires_at = acc_token['expires_at'] + if expires_at: + now = timeutils.utcnow() + expires = timeutils.normalize_time( + timeutils.parse_isotime(expires_at)) + if now > expires: + raise exception.Unauthorized(_('Access token is expired')) + + url = controller.V3Controller.base_url(context, context['path']) + access_verifier = oauth.ResourceEndpoint( + request_validator=validator.OAuthValidator(), + token_generator=oauth.token_generator) + result, request = access_verifier.validate_protected_resource_request( + url, + http_method='POST', + body=context['query_string'], + headers=headers, + realms=None + ) + if not result: + msg = _('Could not validate the access token') + raise exception.Unauthorized(msg) + auth_context['user_id'] = acc_token['authorizing_user_id'] + auth_context['access_token_id'] = access_token_id + auth_context['project_id'] = acc_token['project_id'] diff --git a/keystone-moon/keystone/auth/plugins/password.py b/keystone-moon/keystone/auth/plugins/password.py new file mode 100644 index 00000000..c5770445 --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/password.py @@ -0,0 +1,49 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_log import log + +from keystone import auth +from keystone.auth import plugins as auth_plugins +from keystone.common import dependency +from keystone import exception +from keystone.i18n import _ + +METHOD_NAME = 'password' + +LOG = log.getLogger(__name__) + + +@dependency.requires('identity_api') +class Password(auth.AuthMethodHandler): + + method = METHOD_NAME + + def authenticate(self, context, auth_payload, auth_context): + """Try to authenticate against the identity backend.""" + user_info = auth_plugins.UserAuthInfo.create(auth_payload, self.method) + + # FIXME(gyee): identity.authenticate() can use some refactoring since + # all we care is password matches + try: + self.identity_api.authenticate( + context, + user_id=user_info.user_id, + password=user_info.password) + except AssertionError: + # authentication failed because of invalid username or password + msg = _('Invalid username or password') + raise exception.Unauthorized(msg) + + auth_context['user_id'] = user_info.user_id diff --git a/keystone-moon/keystone/auth/plugins/saml2.py b/keystone-moon/keystone/auth/plugins/saml2.py new file mode 100644 index 00000000..744f26a9 --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/saml2.py @@ -0,0 +1,27 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystone.auth.plugins import mapped + +""" Provide an entry point to authenticate with SAML2 + +This plugin subclasses mapped.Mapped, and may be specified in keystone.conf: + + [auth] + methods = external,password,token,saml2 + saml2 = keystone.auth.plugins.mapped.Mapped +""" + + +class Saml2(mapped.Mapped): + + method = 'saml2' diff --git a/keystone-moon/keystone/auth/plugins/token.py b/keystone-moon/keystone/auth/plugins/token.py new file mode 100644 index 00000000..5ca0b257 --- /dev/null +++ b/keystone-moon/keystone/auth/plugins/token.py @@ -0,0 +1,99 @@ +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_config import cfg +from oslo_log import log +import six + +from keystone import auth +from keystone.auth.plugins import mapped +from keystone.common import dependency +from keystone.common import wsgi +from keystone import exception +from keystone.i18n import _ +from keystone.models import token_model + + +LOG = log.getLogger(__name__) + +CONF = cfg.CONF + + +@dependency.requires('federation_api', 'identity_api', 'token_provider_api') +class Token(auth.AuthMethodHandler): + + method = 'token' + + def _get_token_ref(self, auth_payload): + token_id = auth_payload['id'] + response = self.token_provider_api.validate_token(token_id) + return token_model.KeystoneToken(token_id=token_id, + token_data=response) + + def authenticate(self, context, auth_payload, user_context): + if 'id' not in auth_payload: + raise exception.ValidationError(attribute='id', + target=self.method) + token_ref = self._get_token_ref(auth_payload) + if token_ref.is_federated_user and self.federation_api: + mapped.handle_scoped_token( + context, auth_payload, user_context, token_ref, + self.federation_api, self.identity_api, + self.token_provider_api) + else: + token_authenticate(context, auth_payload, user_context, token_ref) + + +def token_authenticate(context, auth_payload, user_context, token_ref): + 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 or token_ref.trust_scoped: + raise exception.Forbidden() + + 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.Forbidden(action=_("rescope a scoped token")) + + wsgi.validate_token_bind(context, 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 + + user_context.setdefault('expires_at', token_ref.expires) + user_context['audit_id'] = token_audit_id + user_context.setdefault('user_id', token_ref.user_id) + # TODO(morganfainberg: determine if token 'extras' can be removed + # from the user_context + user_context['extras'].update(token_ref.get('extras', {})) + user_context['method_names'].extend(token_ref.methods) + + except AssertionError as e: + LOG.error(six.text_type(e)) + raise exception.Unauthorized(e) diff --git a/keystone-moon/keystone/auth/routers.py b/keystone-moon/keystone/auth/routers.py new file mode 100644 index 00000000..c7a525c3 --- /dev/null +++ b/keystone-moon/keystone/auth/routers.py @@ -0,0 +1,57 @@ +# Copyright 2012 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystone.auth import controllers +from keystone.common import json_home +from keystone.common import wsgi + + +class Routers(wsgi.RoutersBase): + + def append_v3_routers(self, mapper, routers): + auth_controller = controllers.Auth() + + self._add_resource( + mapper, auth_controller, + path='/auth/tokens', + get_action='validate_token', + head_action='check_token', + post_action='authenticate_for_token', + delete_action='revoke_token', + rel=json_home.build_v3_resource_relation('auth_tokens')) + + self._add_resource( + mapper, auth_controller, + path='/auth/tokens/OS-PKI/revoked', + get_action='revocation_list', + rel=json_home.build_v3_extension_resource_relation( + 'OS-PKI', '1.0', 'revocations')) + + self._add_resource( + mapper, auth_controller, + path='/auth/catalog', + get_action='get_auth_catalog', + rel=json_home.build_v3_resource_relation('auth_catalog')) + + self._add_resource( + mapper, auth_controller, + path='/auth/projects', + get_action='get_auth_projects', + rel=json_home.build_v3_resource_relation('auth_projects')) + + self._add_resource( + mapper, auth_controller, + path='/auth/domains', + get_action='get_auth_domains', + rel=json_home.build_v3_resource_relation('auth_domains')) |