Andrew Bogott has submitted this change and it was merged.

Change subject: Support totp auth in keystone
......................................................................


Support totp auth in keystone

Bug: T105690
Change-Id: I5e067bcd326a345616139e307336e44591734aad
---
M hieradata/codfw/labtest.yaml
M hieradata/eqiad.yaml
M manifests/role/mariadb.pp
A modules/openstack/files/kilo/keystone/wmtotp.py
A modules/openstack/files/liberty/keystone/wmtotp.py
M modules/openstack/manifests/keystone/service.pp
M modules/openstack/templates/kilo/keystone/keystone.conf.erb
M modules/openstack/templates/liberty/keystone/keystone.conf.erb
M modules/role/manifests/labs/openstack/nova.pp
M templates/mariadb/grants-wikitech.sql.erb
10 files changed, 275 insertions(+), 0 deletions(-)

Approvals:
  Andrew Bogott: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/hieradata/codfw/labtest.yaml b/hieradata/codfw/labtest.yaml
index b6a5b69..675b957 100644
--- a/hieradata/codfw/labtest.yaml
+++ b/hieradata/codfw/labtest.yaml
@@ -94,6 +94,8 @@
   auth_host: 208.80.153.47
   admin_project_name: 'admin'
   admin_project_id: '93f988e6a8a34da087f5fbec50aca26b'
+  oath_dbname: 'labtestwiki'
+  oath_dbhost: 'labtestweb2001.wikimedia.org'
 
 
 designateconfig:
diff --git a/hieradata/eqiad.yaml b/hieradata/eqiad.yaml
index ee21e26..a65b3e5 100644
--- a/hieradata/eqiad.yaml
+++ b/hieradata/eqiad.yaml
@@ -127,6 +127,8 @@
   auth_host: 208.80.154.92
   admin_project_id: 'admin'
   admin_project_name: 'admin'
+  oath_dbname: 'labswiki'
+  oath_dbhost: 'silver.wikimedia.org'
 
 designateconfig:
   db_host:  'm5-master.eqiad.wmnet'
diff --git a/manifests/role/mariadb.pp b/manifests/role/mariadb.pp
index 13be8ca..2720702 100644
--- a/manifests/role/mariadb.pp
+++ b/manifests/role/mariadb.pp
@@ -466,6 +466,8 @@
 
     include passwords::misc::scripts
     $wikiadmin_pass = $passwords::misc::scripts::wikiadmin_pass
+    $keystoneconfig  = hiera_hash('keystoneconfig', {})
+    $oathreader_pass = $keystoneconfig['oath_dbpass']
 
     file { '/etc/mysql/grants-wikitech.sql':
         ensure  => present,
diff --git a/modules/openstack/files/kilo/keystone/wmtotp.py 
b/modules/openstack/files/kilo/keystone/wmtotp.py
new file mode 100644
index 0000000..eb9254b
--- /dev/null
+++ b/modules/openstack/files/kilo/keystone/wmtotp.py
@@ -0,0 +1,111 @@
+# Copyright 2016 Wikimedia Foundation
+#
+#  (this is a custom hack local to the Wikimedia Labs deployment)
+#
+# 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_config import cfg
+
+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 _
+
+import oath
+import base64
+import mysql.connector
+
+METHOD_NAME = 'wmtotp'
+
+LOG = log.getLogger(__name__)
+CONF = cfg.CONF
+
+oathoptions = [
+    cfg.StrOpt('dbuser',
+               default='wiki_user',
+               help='Database user for retrieving OATH secret.'),
+    cfg.StrOpt('dbpass',
+               default='12345',
+               help='Database password for retrieving OATH secret.'),
+    cfg.StrOpt('dbhost',
+               default='localhost',
+               help='Database host for retrieving OATH secret.'),
+    cfg.StrOpt('dbname',
+               default='labswiki',
+               help='Database name for retrieving OATH secret.'),
+]
+
+for option in oathoptions:
+    CONF.register_opt(option, group='oath')
+
+
[email protected]('identity_api')
+class Wmtotp(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)
+
+        # Password auth succeeded, check two-factor
+        # LOG.debug("OATH: Doing 2FA for user_info " +
+        #     ( "%s(%r)" % (user_info.__class__, user_info.__dict__) ) )
+        # LOG.debug("OATH: Doing 2FA for auth_payload " +
+        #     ( "%s(%r)" % (auth_payload.__class__, auth_payload) ) )
+        cnx = mysql.connector.connect(
+            user=CONF.oath.dbuser,
+            password=CONF.oath.dbpass,
+            database=CONF.oath.dbname,
+            host=CONF.oath.dbhost)
+        cur = cnx.cursor(buffered=True)
+        sql = ('SELECT oath.secret as secret from user '
+               'left join oathauth_users as oath on oath.id = user.user_id '
+               'where user.user_name = %s LIMIT 1')
+        cur.execute(sql, (user_info.user_ref['name'], ))
+        secret = cur.fetchone()[0]
+
+        if secret:
+            if 'totp' in auth_payload['user']:
+                (p, d) = oath.accept_totp(
+                    base64.b16encode(base64.b32decode(secret)),
+                    auth_payload['user']['totp'])
+                if p:
+                    LOG.debug("OATH: 2FA passed")
+                else:
+                    LOG.debug("OATH: 2FA failed")
+                    msg = _('Invalid two-factor token')
+                    raise exception.Unauthorized(msg)
+            else:
+                LOG.debug("OATH: 2FA failed, missing totp param")
+                msg = _('Missing two-factor token')
+                raise exception.Unauthorized(msg)
+        else:
+            LOG.debug("OATH: user '%s' does not have 2FA enabled.",
+                      user_info.user_ref['name'])
+
+        auth_context['user_id'] = user_info.user_id
diff --git a/modules/openstack/files/liberty/keystone/wmtotp.py 
b/modules/openstack/files/liberty/keystone/wmtotp.py
new file mode 100644
index 0000000..eb9254b
--- /dev/null
+++ b/modules/openstack/files/liberty/keystone/wmtotp.py
@@ -0,0 +1,111 @@
+# Copyright 2016 Wikimedia Foundation
+#
+#  (this is a custom hack local to the Wikimedia Labs deployment)
+#
+# 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_config import cfg
+
+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 _
+
+import oath
+import base64
+import mysql.connector
+
+METHOD_NAME = 'wmtotp'
+
+LOG = log.getLogger(__name__)
+CONF = cfg.CONF
+
+oathoptions = [
+    cfg.StrOpt('dbuser',
+               default='wiki_user',
+               help='Database user for retrieving OATH secret.'),
+    cfg.StrOpt('dbpass',
+               default='12345',
+               help='Database password for retrieving OATH secret.'),
+    cfg.StrOpt('dbhost',
+               default='localhost',
+               help='Database host for retrieving OATH secret.'),
+    cfg.StrOpt('dbname',
+               default='labswiki',
+               help='Database name for retrieving OATH secret.'),
+]
+
+for option in oathoptions:
+    CONF.register_opt(option, group='oath')
+
+
[email protected]('identity_api')
+class Wmtotp(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)
+
+        # Password auth succeeded, check two-factor
+        # LOG.debug("OATH: Doing 2FA for user_info " +
+        #     ( "%s(%r)" % (user_info.__class__, user_info.__dict__) ) )
+        # LOG.debug("OATH: Doing 2FA for auth_payload " +
+        #     ( "%s(%r)" % (auth_payload.__class__, auth_payload) ) )
+        cnx = mysql.connector.connect(
+            user=CONF.oath.dbuser,
+            password=CONF.oath.dbpass,
+            database=CONF.oath.dbname,
+            host=CONF.oath.dbhost)
+        cur = cnx.cursor(buffered=True)
+        sql = ('SELECT oath.secret as secret from user '
+               'left join oathauth_users as oath on oath.id = user.user_id '
+               'where user.user_name = %s LIMIT 1')
+        cur.execute(sql, (user_info.user_ref['name'], ))
+        secret = cur.fetchone()[0]
+
+        if secret:
+            if 'totp' in auth_payload['user']:
+                (p, d) = oath.accept_totp(
+                    base64.b16encode(base64.b32decode(secret)),
+                    auth_payload['user']['totp'])
+                if p:
+                    LOG.debug("OATH: 2FA passed")
+                else:
+                    LOG.debug("OATH: 2FA failed")
+                    msg = _('Invalid two-factor token')
+                    raise exception.Unauthorized(msg)
+            else:
+                LOG.debug("OATH: 2FA failed, missing totp param")
+                msg = _('Missing two-factor token')
+                raise exception.Unauthorized(msg)
+        else:
+            LOG.debug("OATH: user '%s' does not have 2FA enabled.",
+                      user_info.user_ref['name'])
+
+        auth_context['user_id'] = user_info.user_id
diff --git a/modules/openstack/manifests/keystone/service.pp 
b/modules/openstack/manifests/keystone/service.pp
index 19d50c9..f487f8e 100644
--- a/modules/openstack/manifests/keystone/service.pp
+++ b/modules/openstack/manifests/keystone/service.pp
@@ -7,6 +7,12 @@
         ensure  => present,
         require => Class['openstack::repo'];
     }
+    package { 'python-oath':
+        ensure  => present,
+    }
+    package { 'python-mysql.connector':
+        ensure  => present,
+    }
 
     if $keystoneconfig['token_driver'] == 'redis' {
         package { 'python-keystone-redis':
@@ -28,6 +34,12 @@
             owner   => 'root',
             group   => 'root',
             require => Package['keystone'];
+        '/usr/lib/python2.7/dist-packages/keystone/auth/plugins/wmtotp.py':
+            source  => 
"puppet:///modules/openstack/${openstack_version}/keystone/wmtotp.py",
+            mode    => '0644',
+            owner   => 'root',
+            group   => 'root',
+            require => Package['keystone'];
     }
 
     if $::fqdn == hiera('labs_nova_controller') {
diff --git a/modules/openstack/templates/kilo/keystone/keystone.conf.erb 
b/modules/openstack/templates/kilo/keystone/keystone.conf.erb
index 3e16967..1dc09fe 100644
--- a/modules/openstack/templates/kilo/keystone/keystone.conf.erb
+++ b/modules/openstack/templates/kilo/keystone/keystone.conf.erb
@@ -204,3 +204,15 @@
 use = egg:Paste#urlmap
 /v2.0 = admin_api
 / = admin_version_api
+
+[auth]
+methods = external,password,token,wmtotp
+
+wmtotp=password = keystone.auth.plugins.wmtotp.Wmtotp
+
+[oath]
+
+dbuser = <%= @keystoneconfig["oath_dbuser"] %>
+dbpass = <%= @keystoneconfig["oath_dbpass"] %>
+dbname = <%= @keystoneconfig["oath_dbname"] %>
+dbhost = <%= @keystoneconfig["oath_dbhost"] %>
diff --git a/modules/openstack/templates/liberty/keystone/keystone.conf.erb 
b/modules/openstack/templates/liberty/keystone/keystone.conf.erb
index 9947643..e776c6b 100644
--- a/modules/openstack/templates/liberty/keystone/keystone.conf.erb
+++ b/modules/openstack/templates/liberty/keystone/keystone.conf.erb
@@ -406,3 +406,15 @@
 user_name_attribute = <%= @keystoneconfig["ldap_user_name_attribute"] %>
 user = <%= @keystoneconfig["ldap_user_dn"] %>
 password = <%= @keystoneconfig["ldap_user_pass"] %>
+
+[auth]
+methods = external,password,token,wmtotp
+
+wmtotp=password = keystone.auth.plugins.wmtotp.Wmtotp
+
+[oath]
+
+dbuser = <%= @keystoneconfig["oath_dbuser"] %>
+dbpass = <%= @keystoneconfig["oath_dbpass"] %>
+dbname = <%= @keystoneconfig["oath_dbname"] %>
+dbhost = <%= @keystoneconfig["oath_dbhost"] %>
diff --git a/modules/role/manifests/labs/openstack/nova.pp 
b/modules/role/manifests/labs/openstack/nova.pp
index 6b60a07..7614003 100644
--- a/modules/role/manifests/labs/openstack/nova.pp
+++ b/modules/role/manifests/labs/openstack/nova.pp
@@ -84,6 +84,13 @@
         rule   => 'proto tcp dport ssh saddr $DEPLOYMENT_HOSTS ACCEPT;',
     }
 
+    # allow keystone to query the wikitech db
+    ferm::service { 'mysql_keystone':
+        proto  => 'tcp',
+        port   => '3306',
+        srange => '@resolve($keystone_host)',
+    }
+
     class { '::openstack::openstack_manager':
         novaconfig         => $novaconfig,
         webserver_hostname => $sitename,
diff --git a/templates/mariadb/grants-wikitech.sql.erb 
b/templates/mariadb/grants-wikitech.sql.erb
index 60b83da..2b2eb63 100644
--- a/templates/mariadb/grants-wikitech.sql.erb
+++ b/templates/mariadb/grants-wikitech.sql.erb
@@ -14,3 +14,7 @@
     ON `labswiki`.* TO 'wikiadmin'@'10.64.16.132'
     IDENTIFIED BY '<%= @wikiadmin_pass %>';
 
+-- queries from horizon.wikimedia.org
+GRANT select
+    ON `labswiki`.* TO 'oathreader'@'208.80.153.248'
+    IDENTIFIED BY '<%= @oathreader_pass %>';

-- 
To view, visit https://gerrit.wikimedia.org/r/274167
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I5e067bcd326a345616139e307336e44591734aad
Gerrit-PatchSet: 11
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

Reply via email to