Basic Keystone Auth support

Note: This has been verified as working by the end customer
This commit is contained in:
Joseph Schorr 2015-07-13 12:34:32 +03:00
parent eb612d606c
commit 066637f496
6 changed files with 151 additions and 1 deletions

View file

@ -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'))
)

View file

@ -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)

View file

@ -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')

View file

@ -353,12 +353,55 @@
<select ng-model="config.AUTHENTICATION_TYPE">
<option value="Database">Local Database</option>
<option value="LDAP">LDAP</option>
<option value="Keystone">Keystone (OpenStack Identity)</option>
<option value="JWT">JWT Custom Authentication</option>
</select>
</td>
</tr>
</table>
<!-- Keystone Authentication -->
<table class="config-table" ng-if="config.AUTHENTICATION_TYPE == 'Keystone'">
<tr>
<td>Keystone Authentication URL:</td>
<td>
<span class="config-string-field" binding="config.KEYSTONE_AUTH_URL"
pattern="http(s)?://.+"></span>
<div class="help-text">
The URL (starting with http or https) of the Keystone Server endpoint for auth.
</div>
</td>
</tr>
<tr>
<td>Keystone Administrator Username:</td>
<td>
<span class="config-string-field" binding="config.KEYSTONE_ADMIN_USERNAME"></span>
<div class="help-text">
The username for the Keystone admin.
</div>
</td>
</tr>
<tr>
<td>Keystone Administrator Password:</td>
<td>
<input type="password" ng-model="config.KEYSTONE_ADMIN_PASSWORD"
class="form-control" required></span>
<div class="help-text">
The password for the Keystone admin.
</div>
</td>
</tr>
<tr>
<td>Keystone Administrator Tenant:</td>
<td>
<span class="config-string-field" binding="config.KEYSTONE_ADMIN_TENANT"></span>
<div class="help-text">
The tenant (project/group) that contains the administrator user.
</div>
</td>
</tr>
</table>
<!-- JWT Custom Authentication -->
<div class="co-alert co-alert-info" ng-if="config.AUTHENTICATION_TYPE == 'JWT'">
JSON Web Token authentication allows your organization to provide an HTTP endpoint that

View file

@ -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;
}},

View file

@ -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,
}