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