Andrew Bogott has submitted this change and it was merged. Change subject: Support totp auth for horizon ......................................................................
Support totp auth for horizon Bug: T105690 Change-Id: I3ad1a48cda39f5878afcf5287a652d5a3f1a2b99 --- A modules/openstack/files/liberty/horizon/forms.py A modules/openstack/files/liberty/horizon/wmtotp.py A modules/openstack/files/liberty/keystoneclient/__init__.py A modules/openstack/files/liberty/keystoneclient/wmtotp.py M modules/openstack/manifests/horizon/service.pp M modules/openstack/templates/liberty/horizon/local_settings.py.erb 6 files changed, 381 insertions(+), 0 deletions(-) Approvals: Andrew Bogott: Looks good to me, approved jenkins-bot: Verified diff --git a/modules/openstack/files/liberty/horizon/forms.py b/modules/openstack/files/liberty/horizon/forms.py new file mode 100644 index 0000000..0aeac58 --- /dev/null +++ b/modules/openstack/files/liberty/horizon/forms.py @@ -0,0 +1,146 @@ +# 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 collections +import logging + +import django +from django.conf import settings +from django.contrib.auth import authenticate # noqa +from django.contrib.auth import forms as django_auth_forms +from django import forms +from django.utils.translation import ugettext_lazy as _ +from django.views.decorators.debug import sensitive_variables # noqa + +from openstack_auth import exceptions +from openstack_auth import utils + + +LOG = logging.getLogger(__name__) + + +class Login(django_auth_forms.AuthenticationForm): + """Form used for logging in a user. + + Handles authentication with Keystone by providing the domain name, username + and password. A scoped token is fetched after successful authentication. + + A domain name is required if authenticating with Keystone V3 running + multi-domain configuration. + + If the user authenticated has a default project set, the token will be + automatically scoped to their default project. + + If the user authenticated has no default project set, the authentication + backend will try to scope to the projects returned from the user's assigned + projects. The first successful project scoped will be returned. + + Inherits from the base ``django.contrib.auth.forms.AuthenticationForm`` + class for added security features. + """ + region = forms.ChoiceField(label=_("Region"), required=False) + username = forms.CharField( + label=_("User Name"), + widget=forms.TextInput(attrs={"autofocus": "autofocus"})) + password = forms.CharField(label=_("Password"), + widget=forms.PasswordInput(render_value=False)) + totptoken = forms.CharField(label=_("Totp Token"), + widget=forms.TextInput()) + + def __init__(self, *args, **kwargs): + super(Login, self).__init__(*args, **kwargs) + fields_ordering = ['username', 'password', 'totptoken', 'region'] + if getattr(settings, + 'OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT', + False): + self.fields['domain'] = forms.CharField( + label=_("Domain"), + required=True, + widget=forms.TextInput(attrs={"autofocus": "autofocus"})) + self.fields['username'].widget = forms.widgets.TextInput() + fields_ordering = ['domain', 'username', 'password', + 'totptoken', 'region'] + self.fields['region'].choices = self.get_region_choices() + if len(self.fields['region'].choices) == 1: + self.fields['region'].initial = self.fields['region'].choices[0][0] + self.fields['region'].widget = forms.widgets.HiddenInput() + elif len(self.fields['region'].choices) > 1: + self.fields['region'].initial = self.request.COOKIES.get( + 'login_region') + + # if websso is enabled and keystone version supported + # prepend the websso_choices select input to the form + if utils.is_websso_enabled(): + initial = getattr(settings, 'WEBSSO_INITIAL_CHOICE', 'credentials') + self.fields['auth_type'] = forms.ChoiceField( + label=_("Authenticate using"), + choices=getattr(settings, 'WEBSSO_CHOICES', ()), + required=False, + initial=initial) + # add auth_type to the top of the list + fields_ordering.insert(0, 'auth_type') + + # websso is enabled, but keystone version is not supported + elif getattr(settings, 'WEBSSO_ENABLED', False): + msg = ("Websso is enabled but horizon is not configured to work " + + "with keystone version 3 or above.") + LOG.warning(msg) + # Starting from 1.7 Django uses OrderedDict for fields and keyOrder + # no longer works for it + if django.VERSION >= (1, 7): + self.fields = collections.OrderedDict( + (key, self.fields[key]) for key in fields_ordering) + else: + self.fields.keyOrder = fields_ordering + + @staticmethod + def get_region_choices(): + default_region = (settings.OPENSTACK_KEYSTONE_URL, "Default Region") + regions = getattr(settings, 'AVAILABLE_REGIONS', []) + if not regions: + regions = [default_region] + return regions + + @sensitive_variables() + def clean(self): + default_domain = getattr(settings, + 'OPENSTACK_KEYSTONE_DEFAULT_DOMAIN', + 'Default') + username = self.cleaned_data.get('username') + password = self.cleaned_data.get('password') + token = self.cleaned_data.get('totptoken') + region = self.cleaned_data.get('region') + domain = self.cleaned_data.get('domain', default_domain) + + if not (username and password and token): + # Don't authenticate, just let the other validators handle it. + return self.cleaned_data + + try: + self.user_cache = authenticate(request=self.request, + username=username, + password=password, + totp=token, + user_domain_name=domain, + auth_url=region) + msg = 'Login successful for user "%(username)s".' % \ + {'username': username} + LOG.info(msg) + except exceptions.KeystoneAuthException as exc: + msg = 'Login failed for user "%(username)s".' % \ + {'username': username} + LOG.warning(msg) + raise forms.ValidationError(exc) + if hasattr(self, 'check_for_test_cookie'): # Dropped in django 1.7 + self.check_for_test_cookie() + return self.cleaned_data diff --git a/modules/openstack/files/liberty/horizon/wmtotp.py b/modules/openstack/files/liberty/horizon/wmtotp.py new file mode 100644 index 0000000..75b78d1 --- /dev/null +++ b/modules/openstack/files/liberty/horizon/wmtotp.py @@ -0,0 +1,47 @@ +# 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 logging + +from keystoneclient.auth.identity import v2 as v2_auth +from keystoneclient.auth.identity import v3 as v3_auth + +from openstack_auth.plugin import base +from openstack_auth import utils + +LOG = logging.getLogger(__name__) + +__all__ = ['WmtotpPlugin'] + + +class WmtotpPlugin(base.BasePlugin): + """Authenticate against keystone given a username, password, totp token. + """ + + def get_plugin(self, auth_url=None, username=None, password=None, + user_domain_name=None, totp=None, **kwargs): + if not all((auth_url, username, password, totp)): + return None + + LOG.debug('Attempting to authenticate for %s', username) + + if utils.get_keystone_version() >= 3: + return v3_auth.Wmtotp(auth_url=auth_url, + username=username, + password=password, + totp=totp, + user_domain_name=user_domain_name, + unscoped=True) + + else: + msg = "Totp authentication requires the keystone v3 api." + raise exceptions.KeystoneAuthException(msg) diff --git a/modules/openstack/files/liberty/keystoneclient/__init__.py b/modules/openstack/files/liberty/keystoneclient/__init__.py new file mode 100644 index 0000000..c9ecd12 --- /dev/null +++ b/modules/openstack/files/liberty/keystoneclient/__init__.py @@ -0,0 +1,34 @@ +# 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 keystoneclient.auth.identity.v3.base import * # noqa +from keystoneclient.auth.identity.v3.federated import * # noqa +from keystoneclient.auth.identity.v3.password import * # noqa +from keystoneclient.auth.identity.v3.token import * # noqa +from keystoneclient.auth.identity.v3.wmtotp import * # noqa + + +__all__ = ['Auth', + 'AuthConstructor', + 'AuthMethod', + 'BaseAuth', + + 'FederatedBaseAuth', + + 'Password', + 'PasswordMethod', + + 'Mwtotp', + 'MwtotpMethod', + + 'Token', + 'TokenMethod'] diff --git a/modules/openstack/files/liberty/keystoneclient/wmtotp.py b/modules/openstack/files/liberty/keystoneclient/wmtotp.py new file mode 100644 index 0000000..cc52f93 --- /dev/null +++ b/modules/openstack/files/liberty/keystoneclient/wmtotp.py @@ -0,0 +1,112 @@ +# +# Custom addition for Wikimedia Labs to add a totp plugin to keystoneclient +# +# 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 keystoneclient.auth.identity.v3 import base +from keystoneclient import utils + +__all__ = ['WmtotpMethod', 'Wmtotp'] + + +class WmtotpMethod(base.AuthMethod): + """Construct a User/Password/totp based authentication method. + + :param string password: Password for authentication. + :param string totp: Totp token for authentication. + :param string username: Username for authentication. + :param string user_id: User ID for authentication. + :param string user_domain_id: User's domain ID for authentication. + :param string user_domain_name: User's domain name for authentication. + """ + + _method_parameters = ['user_id', + 'username', + 'user_domain_id', + 'user_domain_name', + 'password', + 'totp'] + + def get_auth_data(self, session, auth, headers, **kwargs): + user = {'password': self.password, 'totp': self.totp} + + if self.user_id: + user['id'] = self.user_id + elif self.username: + user['name'] = self.username + + if self.user_domain_id: + user['domain'] = {'id': self.user_domain_id} + elif self.user_domain_name: + user['domain'] = {'name': self.user_domain_name} + + return 'wmtotp', {'user': user} + + +class Wmtotp(base.AuthConstructor): + """A plugin for authenticating with a username, password, totp token + + :param string auth_url: Identity service endpoint for authentication. + :param string password: Password for authentication. + :param string totp: totp token for authentication + :param string username: Username for authentication. + :param string user_id: User ID for authentication. + :param string user_domain_id: User's domain ID for authentication. + :param string user_domain_name: User's domain name for authentication. + :param string trust_id: Trust ID for trust scoping. + :param string domain_id: Domain ID for domain scoping. + :param string domain_name: Domain name for domain scoping. + :param string project_id: Project ID for project scoping. + :param string project_name: Project name for project scoping. + :param string project_domain_id: Project's domain ID for project. + :param string project_domain_name: Project's domain name for project. + :param bool reauthenticate: Allow fetching a new token if the current one + is going to expire. (optional) default True + """ + + _auth_method_class = WmtotpMethod + + @classmethod + def get_options(cls): + options = super(Wmtotp, cls).get_options() + + options.extend([ + cfg.StrOpt('user-id', help='User ID'), + cfg.StrOpt('user-name', dest='username', help='Username', + deprecated_name='username'), + cfg.StrOpt('user-domain-id', help="User's domain id"), + cfg.StrOpt('user-domain-name', help="User's domain name"), + cfg.StrOpt('password', secret=True, help="User's password"), + cfg.StrOpt('totp', secret=True, help="Totp token"), + ]) + + return options + + @classmethod + def load_from_argparse_arguments(cls, namespace, **kwargs): + if not (kwargs.get('password') or namespace.os_password): + kwargs['password'] = utils.prompt_user_password() + + if not kwargs.get('totp') and (hasattr(sys.stdin, 'isatty') and + sys.stdin.isatty()): + try: + kwargs['totp'] = getpass.getpass('Totp token: ') + except EOFError: + pass + + return super(Wmtotp, cls).load_from_argparse_arguments(namespace, + **kwargs) diff --git a/modules/openstack/manifests/horizon/service.pp b/modules/openstack/manifests/horizon/service.pp index 8f6d4ac..0e551d0 100644 --- a/modules/openstack/manifests/horizon/service.pp +++ b/modules/openstack/manifests/horizon/service.pp @@ -12,6 +12,12 @@ ensure => present, require => Class['openstack::repo', '::apache::mod::wsgi']; } + package { 'python-keystoneclient': + ensure => present, + } + package { 'python-openstack-auth': + ensure => present, + } include ::apache include ::apache::mod::ssl @@ -85,6 +91,40 @@ mode => '0444', } + # Homemade totp plugin for keystoneclient + file { '/usr/lib/python2.7/dist-packages/keystoneclient/auth/identity/v3/wmtotp.py': + source => "puppet:///modules/openstack/${openstack_version}/keystoneclient/wmtotp.py", + owner => 'root', + group => 'root', + require => Package['python-keystoneclient'], + mode => '0644', + } + file { '/usr/lib/python2.7/dist-packages/keystoneclient/auth/identity/v3/__init__.py': + source => "puppet:///modules/openstack/${openstack_version}/keystoneclient/__init__.py", + owner => 'root', + group => 'root', + require => Package['python-keystoneclient'], + mode => '0644', + } + + # 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", + owner => 'root', + group => 'root', + require => Package['python-openstack-auth'], + mode => '0644', + } + + # 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", + owner => 'root', + group => 'root', + require => Package['python-openstack-auth'], + mode => '0644', + } + apache::site { $webserver_hostname: content => template("openstack/${$openstack_version}/horizon/${webserver_hostname}.erb"), require => File['/etc/openstack-dashboard/local_settings.py'], diff --git a/modules/openstack/templates/liberty/horizon/local_settings.py.erb b/modules/openstack/templates/liberty/horizon/local_settings.py.erb index cbe3385..6e8b0b3 100644 --- a/modules/openstack/templates/liberty/horizon/local_settings.py.erb +++ b/modules/openstack/templates/liberty/horizon/local_settings.py.erb @@ -18,6 +18,8 @@ # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts ALLOWED_HOSTS = ['horizon.wikimedia.org', ] +AUTHENTICATION_PLUGINS = ['openstack_auth.plugin.wmtotp.WmtotpPlugin', 'openstack_auth.plugin.token.TokenPlugin'] + # Set SSL proxy settings: # For Django 1.4+ pass this header from the proxy after terminating the SSL, # and don't forget to strip it from the client's request. -- To view, visit https://gerrit.wikimedia.org/r/274173 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I3ad1a48cda39f5878afcf5287a652d5a3f1a2b99 Gerrit-PatchSet: 15 Gerrit-Project: operations/puppet Gerrit-Branch: production Gerrit-Owner: Andrew Bogott <[email protected]> Gerrit-Reviewer: Alex Monk <[email protected]> Gerrit-Reviewer: Andrew Bogott <[email protected]> Gerrit-Reviewer: CSteipp <[email protected]> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
