From 066637f496e46ca82ae797619c464c77875546ac Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 13 Jul 2015 12:34:32 +0300 Subject: [PATCH] Basic Keystone Auth support Note: This has been verified as working by the end customer --- ...2bf8af5bad95_add_keystone_login_service.py | 26 +++++++++++ data/users.py | 41 ++++++++++++++++++ initdb.py | 1 + .../directives/config/config-setup-tool.html | 43 +++++++++++++++++++ static/js/core-config-setup.js | 4 ++ util/config/validator.py | 37 +++++++++++++++- 6 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 data/migrations/versions/2bf8af5bad95_add_keystone_login_service.py diff --git a/data/migrations/versions/2bf8af5bad95_add_keystone_login_service.py b/data/migrations/versions/2bf8af5bad95_add_keystone_login_service.py new file mode 100644 index 000000000..440e7ec98 --- /dev/null +++ b/data/migrations/versions/2bf8af5bad95_add_keystone_login_service.py @@ -0,0 +1,26 @@ +"""Add keystone login service + +Revision ID: 2bf8af5bad95 +Revises: 154f2befdfbe +Create Date: 2015-06-29 21:19:13.053165 + +""" + +# revision identifiers, used by Alembic. +revision = '2bf8af5bad95' +down_revision = '154f2befdfbe' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(tables): + op.bulk_insert(tables.loginservice, [{'id': 6, 'name': 'keystone'}]) + + +def downgrade(tables): + op.execute( + tables.loginservice.delete() + .where(tables.loginservice.c.name == op.inline_literal('keystone')) + ) + diff --git a/data/users.py b/data/users.py index 4b917162e..83b003401 100644 --- a/data/users.py +++ b/data/users.py @@ -8,6 +8,9 @@ import jwt from collections import namedtuple from datetime import datetime, timedelta +from keystoneclient.v2_0 import client as kclient +from keystoneclient.exceptions import AuthorizationFailure as KeystoneAuthorizationFailure +from keystoneclient.exceptions import Unauthorized as KeystoneUnauthorized import features @@ -53,6 +56,37 @@ def _get_federated_user(username, email, federated_service, create_new_user): return (db_user, None) +class KeystoneUsers(object): + """ Delegates authentication to OpenStack Keystone. """ + def __init__(self, auth_url, admin_username, admin_password, admin_tenant): + self.auth_url = auth_url + self.admin_username = admin_username + self.admin_password = admin_password + self.admin_tenant = admin_tenant + + def verify_user(self, username_or_email, password, create_new_user=True): + try: + keystone_client = kclient.Client(username=username_or_email, password=password, + auth_url=self.auth_url) + user_id = keystone_client.user_id + except KeystoneAuthorizationFailure as kaf: + logger.exception('Keystone auth failure for user: %s', username_or_email) + return (None, kaf.message or 'Invalid username or password') + except KeystoneUnauthorized as kut: + logger.exception('Keystone unauthorized for user: %s', username_or_email) + return (None, kut.message or 'Invalid username or password') + + try: + admin_client = kclient.Client(username=self.admin_username, password=self.admin_password, + tenant_name=self.admin_tenant, auth_url=self.auth_url) + user = admin_client.users.get(user_id) + except KeystoneUnauthorized as kut: + logger.exception('Keystone unauthorized admin') + return (None, 'Keystone admin credentials are invalid: %s' % kut.message) + + return _get_federated_user(username_or_email, user.email, 'keystone', create_new_user) + + class ExternalJWTAuthN(object): """ Delegates authentication to a REST endpoint that returns JWTs. """ PUBLIC_KEY_FILENAME = 'jwt-authn.cert' @@ -336,6 +370,13 @@ class UserAuthentication(object): max_fresh_s = app.config.get('JWT_AUTH_MAX_FRESH_S', 300) users = ExternalJWTAuthN(verify_url, issuer, override_config_dir, max_fresh_s, app.config['HTTPCLIENT']) + elif authentication_type == 'Keystone': + auth_url = app.config.get('KEYSTONE_AUTH_URL') + keystone_admin_username = app.config.get('KEYSTONE_ADMIN_USERNAME') + keystone_admin_password = app.config.get('KEYSTONE_ADMIN_PASSWORD') + keystone_admin_tenant = app.config.get('KEYSTONE_ADMIN_TENANT') + users = KeystoneUsers(auth_url, keystone_admin_username, keystone_admin_password, + keystone_admin_tenant) else: raise RuntimeError('Unknown authentication type: %s' % authentication_type) diff --git a/initdb.py b/initdb.py index bb9a3c141..d0181ebca 100644 --- a/initdb.py +++ b/initdb.py @@ -205,6 +205,7 @@ def initialize_database(): LoginService.create(name='quayrobot') LoginService.create(name='ldap') LoginService.create(name='jwtauthn') + LoginService.create(name='keystone') BuildTriggerService.create(name='github') BuildTriggerService.create(name='custom-git') diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index b1c0c7da4..43bff96f3 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -353,12 +353,55 @@ + + + + + + + + + + + + + + + + + + +
Keystone Authentication URL: + +
+ The URL (starting with http or https) of the Keystone Server endpoint for auth. +
+
Keystone Administrator Username: + +
+ The username for the Keystone admin. +
+
Keystone Administrator Password: + +
+ The password for the Keystone admin. +
+
Keystone Administrator Tenant: + +
+ The tenant (project/group) that contains the administrator user. +
+
+
JSON Web Token authentication allows your organization to provide an HTTP endpoint that diff --git a/static/js/core-config-setup.js b/static/js/core-config-setup.js index 2027d76cb..48f3081fb 100644 --- a/static/js/core-config-setup.js +++ b/static/js/core-config-setup.js @@ -31,6 +31,10 @@ angular.module("core-config-setup", ['angularFileUpload']) return config.AUTHENTICATION_TYPE == 'JWT'; }, 'password': true}, + {'id': 'keystone', 'title': 'Keystone Authentication', 'condition': function(config) { + return config.AUTHENTICATION_TYPE == 'Keystone'; + }, 'password': true}, + {'id': 'mail', 'title': 'E-mail Support', 'condition': function(config) { return config.FEATURE_MAILING; }}, diff --git a/util/config/validator.py b/util/config/validator.py index 3181b2811..2f80441c9 100644 --- a/util/config/validator.py +++ b/util/config/validator.py @@ -7,7 +7,7 @@ import OpenSSL import logging from fnmatch import fnmatch -from data.users import LDAPConnection, ExternalJWTAuthN, LDAPUsers +from data.users import LDAPConnection, ExternalJWTAuthN, LDAPUsers, KeystoneUsers from flask import Flask from flask.ext.mail import Mail, Message from data.database import validate_database_url, User @@ -352,6 +352,40 @@ def _validate_jwt(config, password): 'OR JWT auth is misconfigured.') % (username, err_msg)) +def _validate_keystone(config, password): + """ Validates the Keystone authentication system. """ + if config.get('AUTHENTICATION_TYPE', 'Database') != 'Keystone': + return + + auth_url = config.get('KEYSTONE_AUTH_URL') + admin_username = config.get('KEYSTONE_ADMIN_USERNAME') + admin_password = config.get('KEYSTONE_ADMIN_PASSWORD') + admin_tenant = config.get('KEYSTONE_ADMIN_TENANT') + + if not auth_url: + raise Exception('Missing authentication URL') + + if not admin_username: + raise Exception('Missing admin username') + + if not admin_password: + raise Exception('Missing admin password') + + if not admin_tenant: + raise Exception('Missing admin tenant') + + users = KeystoneUsers(auth_url, admin_username, admin_password, admin_tenant) + + # Verify that the superuser exists. If not, raise an exception. + username = get_authenticated_user().username + + (result, err_msg) = users.verify_user(username, password) + if not result: + raise Exception(('Verification of superuser %s failed: %s \n\nThe user either does not ' + + 'exist in the remote authentication system ' + + 'OR Keystone auth is misconfigured.') % (username, err_msg)) + + _VALIDATORS = { 'database': _validate_database, 'redis': _validate_redis, @@ -365,4 +399,5 @@ _VALIDATORS = { 'ssl': _validate_ssl, 'ldap': _validate_ldap, 'jwt': _validate_jwt, + 'keystone': _validate_keystone, } \ No newline at end of file