Andrew Bogott has submitted this change and it was merged. Change subject: Move some hacked horizon files ......................................................................
Move some hacked horizon files forms.py and wmtotp.py are dropped directly into the openstack_auth directory, so organize them in puppet. Also add backend.py, directly from the upstream. I'm going to modify it in a future patch and want the diff to show up here. Change-Id: I7ca2efb6405ef2a9a707725f1a8b3f7d74ff16b7 --- A modules/openstack/files/liberty/horizon/openstack_auth/backend.py R modules/openstack/files/liberty/horizon/openstack_auth/forms.py R modules/openstack/files/liberty/horizon/openstack_auth/wmtotp.py R modules/openstack/files/mitaka/horizon/openstack_auth/forms.py R modules/openstack/files/mitaka/horizon/openstack_auth/wmtotp.py M modules/openstack/manifests/horizon/service.pp 6 files changed, 261 insertions(+), 2 deletions(-) Approvals: Andrew Bogott: Looks good to me, approved jenkins-bot: Verified diff --git a/modules/openstack/files/liberty/horizon/openstack_auth/backend.py b/modules/openstack/files/liberty/horizon/openstack_auth/backend.py new file mode 100644 index 0000000..c630fb0 --- /dev/null +++ b/modules/openstack/files/liberty/horizon/openstack_auth/backend.py @@ -0,0 +1,259 @@ +# 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. + +""" Module defining the Django auth backend class for the Keystone API. """ + +import datetime +import logging +import pytz + +from django.conf import settings +from django.utils.module_loading import import_string # noqa +from django.utils.translation import ugettext_lazy as _ +from keystoneclient import exceptions as keystone_exceptions + +from openstack_auth import exceptions +from openstack_auth import user as auth_user +from openstack_auth import utils + + +LOG = logging.getLogger(__name__) + + +KEYSTONE_CLIENT_ATTR = "_keystoneclient" + + +class KeystoneBackend(object): + """Django authentication backend for use with ``django.contrib.auth``.""" + + def __init__(self): + self._auth_plugins = None + + @property + def auth_plugins(self): + if self._auth_plugins is None: + plugins = getattr( + settings, + 'AUTHENTICATION_PLUGINS', + ['openstack_auth.plugin.password.PasswordPlugin', + 'openstack_auth.plugin.token.TokenPlugin']) + + self._auth_plugins = [import_string(p)() for p in plugins] + + return self._auth_plugins + + def check_auth_expiry(self, auth_ref, margin=None): + if not utils.is_token_valid(auth_ref, margin): + msg = _("The authentication token issued by the Identity service " + "has expired.") + LOG.warning("The authentication token issued by the Identity " + "service appears to have expired before it was " + "issued. This may indicate a problem with either your " + "server or client configuration.") + raise exceptions.KeystoneAuthException(msg) + return True + + def get_user(self, user_id): + """Returns the current user from the session data. + + If authenticated, this return the user object based on the user ID + and session data. + + Note: this required monkey-patching the ``contrib.auth`` middleware + to make the ``request`` object available to the auth backend class. + """ + if (hasattr(self, 'request') and + user_id == self.request.session["user_id"]): + token = self.request.session['token'] + endpoint = self.request.session['region_endpoint'] + services_region = self.request.session['services_region'] + user = auth_user.create_user_from_token(self.request, token, + endpoint, services_region) + return user + else: + return None + + def authenticate(self, auth_url=None, **kwargs): + """Authenticates a user via the Keystone Identity API.""" + LOG.debug('Beginning user authentication') + + if not auth_url: + auth_url = settings.OPENSTACK_KEYSTONE_URL + + auth_url = utils.fix_auth_url_version(auth_url) + + for plugin in self.auth_plugins: + unscoped_auth = plugin.get_plugin(auth_url=auth_url, **kwargs) + + if unscoped_auth: + break + else: + msg = _('No authentication backend could be determined to ' + 'handle the provided credentials.') + LOG.warn('No authentication backend could be determined to ' + 'handle the provided credentials. This is likely a ' + 'configuration error that should be addressed.') + raise exceptions.KeystoneAuthException(msg) + + session = utils.get_session() + keystone_client_class = utils.get_keystone_client().Client + + try: + unscoped_auth_ref = unscoped_auth.get_access(session) + except keystone_exceptions.ConnectionRefused as exc: + LOG.error(str(exc)) + msg = _('Unable to establish connection to keystone endpoint.') + raise exceptions.KeystoneAuthException(msg) + except (keystone_exceptions.Unauthorized, + keystone_exceptions.Forbidden, + keystone_exceptions.NotFound) as exc: + LOG.debug(str(exc)) + raise exceptions.KeystoneAuthException(_('Invalid credentials.')) + except (keystone_exceptions.ClientException, + keystone_exceptions.AuthorizationFailure) as exc: + msg = _("An error occurred authenticating. " + "Please try again later.") + LOG.debug(str(exc)) + raise exceptions.KeystoneAuthException(msg) + + # Check expiry for our unscoped auth ref. + self.check_auth_expiry(unscoped_auth_ref) + + projects = plugin.list_projects(session, + unscoped_auth, + unscoped_auth_ref) + # Attempt to scope only to enabled projects + projects = [project for project in projects if project.enabled] + + # Abort if there are no projects for this user + if not projects: + msg = _('You are not authorized for any projects.') + raise exceptions.KeystoneAuthException(msg) + + # the recent project id a user might have set in a cookie + recent_project = None + request = kwargs.get('request') + + if request: + # Grab recent_project found in the cookie, try to scope + # to the last project used. + recent_project = request.COOKIES.get('recent_project') + + # if a most recent project was found, try using it first + if recent_project: + for pos, project in enumerate(projects): + if project.id == recent_project: + # move recent project to the beginning + projects.pop(pos) + projects.insert(0, project) + break + + for project in projects: + token = unscoped_auth_ref.auth_token + scoped_auth = utils.get_token_auth_plugin(auth_url, + token=token, + project_id=project.id) + + try: + scoped_auth_ref = scoped_auth.get_access(session) + except (keystone_exceptions.ClientException, + keystone_exceptions.AuthorizationFailure): + pass + else: + break + else: + msg = _("Unable to authenticate to any available projects.") + raise exceptions.KeystoneAuthException(msg) + + # Check expiry for our new scoped token. + self.check_auth_expiry(scoped_auth_ref) + + interface = getattr(settings, 'OPENSTACK_ENDPOINT_TYPE', 'public') + + # If we made it here we succeeded. Create our User! + unscoped_token = unscoped_auth_ref.auth_token + user = auth_user.create_user_from_token( + request, + auth_user.Token(scoped_auth_ref, unscoped_token=unscoped_token), + scoped_auth_ref.service_catalog.url_for(endpoint_type=interface)) + + if request is not None: + request.session['unscoped_token'] = unscoped_token + request.user = user + timeout = getattr(settings, "SESSION_TIMEOUT", 3600) + token_life = user.token.expires - datetime.datetime.now(pytz.utc) + session_time = min(timeout, token_life.seconds) + request.session.set_expiry(session_time) + + scoped_client = keystone_client_class(session=session, + auth=scoped_auth) + + # Support client caching to save on auth calls. + setattr(request, KEYSTONE_CLIENT_ATTR, scoped_client) + + LOG.debug('Authentication completed.') + return user + + def get_group_permissions(self, user, obj=None): + """Returns an empty set since Keystone doesn't support "groups".""" + # Keystone V3 added "groups". The Auth token response includes the + # roles from the user's Group assignment. It should be fine just + # returning an empty set here. + return set() + + def get_all_permissions(self, user, obj=None): + """Returns a set of permission strings that the user has. + + This permission available to the user is derived from the user's + Keystone "roles". + + The permissions are returned as ``"openstack.{{ role.name }}"``. + """ + if user.is_anonymous() or obj is not None: + return set() + # TODO(gabrielhurley): Integrate policy-driven RBAC + # when supported by Keystone. + role_perms = set(["openstack.roles.%s" % role['name'].lower() + for role in user.roles]) + + services = [] + for service in user.service_catalog: + try: + service_type = service['type'] + except KeyError: + continue + service_regions = [utils.get_endpoint_region(endpoint) for endpoint + in service.get('endpoints', [])] + if user.services_region in service_regions: + services.append(service_type.lower()) + service_perms = set(["openstack.services.%s" % service + for service in services]) + return role_perms | service_perms + + def has_perm(self, user, perm, obj=None): + """Returns True if the given user has the specified permission.""" + if not user.is_active: + return False + return perm in self.get_all_permissions(user, obj) + + def has_module_perms(self, user, app_label): + """Returns True if user has any permissions in the given app_label. + + Currently this matches for the app_label ``"openstack"``. + """ + if not user.is_active: + return False + for perm in self.get_all_permissions(user): + if perm[:perm.index('.')] == app_label: + return True + return False diff --git a/modules/openstack/files/liberty/horizon/forms.py b/modules/openstack/files/liberty/horizon/openstack_auth/forms.py similarity index 100% rename from modules/openstack/files/liberty/horizon/forms.py rename to modules/openstack/files/liberty/horizon/openstack_auth/forms.py diff --git a/modules/openstack/files/liberty/horizon/wmtotp.py b/modules/openstack/files/liberty/horizon/openstack_auth/wmtotp.py similarity index 100% rename from modules/openstack/files/liberty/horizon/wmtotp.py rename to modules/openstack/files/liberty/horizon/openstack_auth/wmtotp.py diff --git a/modules/openstack/files/mitaka/horizon/forms.py b/modules/openstack/files/mitaka/horizon/openstack_auth/forms.py similarity index 100% rename from modules/openstack/files/mitaka/horizon/forms.py rename to modules/openstack/files/mitaka/horizon/openstack_auth/forms.py diff --git a/modules/openstack/files/mitaka/horizon/wmtotp.py b/modules/openstack/files/mitaka/horizon/openstack_auth/wmtotp.py similarity index 100% rename from modules/openstack/files/mitaka/horizon/wmtotp.py rename to modules/openstack/files/mitaka/horizon/openstack_auth/wmtotp.py diff --git a/modules/openstack/manifests/horizon/service.pp b/modules/openstack/manifests/horizon/service.pp index 4750a31..5927972 100644 --- a/modules/openstack/manifests/horizon/service.pp +++ b/modules/openstack/manifests/horizon/service.pp @@ -124,7 +124,7 @@ # Homemade totp plugin for openstack_auth file { '/usr/lib/python2.7/dist-packages/openstack_auth/plugin/wmtotp.py': - source => "puppet:///modules/openstack/${openstack_version}/horizon/wmtotp.py", + source => "puppet:///modules/openstack/${openstack_version}/horizon/openstack_auth/wmtotp.py", owner => 'root', group => 'root', require => Package['python-openstack-auth'], @@ -133,7 +133,7 @@ # Replace the standard horizon login form to support 2fa file { '/usr/lib/python2.7/dist-packages/openstack_auth/forms.py': - source => "puppet:///modules/openstack/${openstack_version}/horizon/forms.py", + source => "puppet:///modules/openstack/${openstack_version}/horizon/openstack_auth/forms.py", owner => 'root', group => 'root', require => Package['python-openstack-auth'], -- To view, visit https://gerrit.wikimedia.org/r/317991 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I7ca2efb6405ef2a9a707725f1a8b3f7d74ff16b7 Gerrit-PatchSet: 4 Gerrit-Project: operations/puppet Gerrit-Branch: production Gerrit-Owner: Andrew Bogott <abog...@wikimedia.org> Gerrit-Reviewer: Alex Monk <a...@wikimedia.org> Gerrit-Reviewer: Andrew Bogott <abog...@wikimedia.org> Gerrit-Reviewer: Volans <rcocci...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits