Merge pull request #60 from coreos-inc/jwtauthentication
Add support for an external JWT-based authentication system
This commit is contained in:
commit
2a2414d6af
10 changed files with 417 additions and 38 deletions
|
@ -0,0 +1,28 @@
|
|||
"""Add JWT Authentication login service
|
||||
|
||||
Revision ID: 41f4587c84ae
|
||||
Revises: 1f116e06b68
|
||||
Create Date: 2015-06-02 16:13:02.636590
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '41f4587c84ae'
|
||||
down_revision = '1f116e06b68'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade(tables):
|
||||
op.bulk_insert(tables.loginservice,
|
||||
[
|
||||
{'id': 5, 'name':'jwtauthn'},
|
||||
])
|
||||
|
||||
|
||||
def downgrade(tables):
|
||||
op.execute(
|
||||
(tables.loginservice.delete()
|
||||
.where(tables.loginservice.c.name == op.inline_literal('jwtauthn')))
|
||||
)
|
143
data/users.py
143
data/users.py
|
@ -5,11 +5,14 @@ import itertools
|
|||
import uuid
|
||||
import struct
|
||||
import os
|
||||
import urllib
|
||||
import jwt
|
||||
|
||||
from util.aes import AESCipher
|
||||
from util.validation import generate_valid_usernames
|
||||
from data import model
|
||||
from collections import namedtuple
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
if os.environ.get('LDAP_DEBUG') == '1':
|
||||
|
@ -20,6 +23,115 @@ if os.environ.get('LDAP_DEBUG') == '1':
|
|||
|
||||
logger.addHandler(ch)
|
||||
|
||||
|
||||
def _get_federated_user(username, email, federated_service, create_new_user):
|
||||
db_user = model.verify_federated_login(federated_service, username)
|
||||
if not db_user:
|
||||
if not create_new_user:
|
||||
return (None, 'Invalid user')
|
||||
|
||||
# We must create the user in our db
|
||||
valid_username = None
|
||||
for valid_username in generate_valid_usernames(username):
|
||||
if model.is_username_unique(valid_username):
|
||||
break
|
||||
|
||||
if not valid_username:
|
||||
logger.error('Unable to pick a username for user: %s', username)
|
||||
return (None, 'Unable to pick a username. Please report this to your administrator.')
|
||||
|
||||
db_user = model.create_federated_user(valid_username, email, federated_service, username,
|
||||
set_password_notification=False)
|
||||
else:
|
||||
# Update the db attributes from ldap
|
||||
db_user.email = email
|
||||
db_user.save()
|
||||
|
||||
return (db_user, None)
|
||||
|
||||
|
||||
class JWTAuthUsers(object):
|
||||
""" Delegates authentication to a REST endpoint that returns JWTs. """
|
||||
PUBLIC_KEY_FILENAME = 'jwt-authn.cert'
|
||||
|
||||
def __init__(self, exists_url, verify_url, issuer, public_key_path=None):
|
||||
from app import OVERRIDE_CONFIG_DIRECTORY
|
||||
|
||||
self.verify_url = verify_url
|
||||
self.exists_url = exists_url
|
||||
self.issuer = issuer
|
||||
|
||||
default_key_path = os.path.join(OVERRIDE_CONFIG_DIRECTORY, JWTAuthUsers.PUBLIC_KEY_FILENAME)
|
||||
public_key_path = public_key_path or default_key_path
|
||||
if not os.path.exists(public_key_path):
|
||||
error_message = ('JWT Authentication public key file "%s" not found in directory %s' %
|
||||
(JWTAuthUsers.PUBLIC_KEY_FILENAME, OVERRIDE_CONFIG_DIRECTORY))
|
||||
|
||||
raise Exception(error_message)
|
||||
|
||||
with open(public_key_path) as public_key_file:
|
||||
self.public_key = public_key_file.read()
|
||||
|
||||
def verify_user(self, username_or_email, password, create_new_user=True):
|
||||
from app import app
|
||||
client = app.config['HTTPCLIENT']
|
||||
result = client.get(self.verify_url, timeout=2, auth=(username_or_email, password))
|
||||
|
||||
if result.status_code != 200:
|
||||
return (None, result.text or 'Invalid username or password')
|
||||
|
||||
try:
|
||||
result_data = json.loads(result.text)
|
||||
except ValueError:
|
||||
raise Exception('Returned JWT Authentication body does not contain JSON')
|
||||
|
||||
# Load the JWT returned.
|
||||
encoded = result_data.get('token', '')
|
||||
try:
|
||||
payload = jwt.decode(encoded, self.public_key, algorithms=['RS256'],
|
||||
audience='quay.io/jwtauthn', issuer=self.issuer)
|
||||
except jwt.InvalidTokenError:
|
||||
logger.exception('Exception when decoding returned JWT')
|
||||
return (None, 'Invalid username or password')
|
||||
|
||||
if not 'sub' in payload:
|
||||
raise Exception('Missing username field in JWT')
|
||||
|
||||
if not 'email' in payload:
|
||||
raise Exception('Missing email field in JWT')
|
||||
|
||||
if not 'exp' in payload:
|
||||
raise Exception('Missing exp field in JWT')
|
||||
|
||||
# Verify that the expiration is no more than 300 seconds in the future.
|
||||
if datetime.fromtimestamp(payload['exp']) > datetime.utcnow() + timedelta(seconds=300):
|
||||
logger.debug('Payload expiration is outside of the 300 second window: %s', payload['exp'])
|
||||
return (None, 'Invalid username or password')
|
||||
|
||||
# Parse out the username and email.
|
||||
return _get_federated_user(payload['sub'], payload['email'], 'jwtauthn', create_new_user)
|
||||
|
||||
def user_exists(self, username):
|
||||
from app import app
|
||||
client = app.config['HTTPCLIENT']
|
||||
result = client.get(self.exists_url, auth=(username, ''), timeout=2)
|
||||
if result.status_code / 500 >= 1:
|
||||
raise Exception('Internal Error when trying to check if user exists: %s' % result.text)
|
||||
|
||||
return result.status_code == 200
|
||||
|
||||
def confirm_existing_user(self, username, password):
|
||||
db_user = model.get_user(username)
|
||||
if not db_user:
|
||||
return (None, 'Invalid user')
|
||||
|
||||
federated_login = model.lookup_federated_login(db_user, 'jwtauthn')
|
||||
if not federated_login:
|
||||
return (None, 'Invalid user')
|
||||
|
||||
return self.verify_user(federated_login.service_ident, password, create_new_user=False)
|
||||
|
||||
|
||||
class DatabaseUsers(object):
|
||||
def verify_user(self, username_or_email, password):
|
||||
""" Simply delegate to the model implementation. """
|
||||
|
@ -189,30 +301,7 @@ class LDAPUsers(object):
|
|||
|
||||
username = found_response[self._uid_attr][0].decode('utf-8')
|
||||
email = found_response[self._email_attr][0]
|
||||
db_user = model.verify_federated_login('ldap', username)
|
||||
|
||||
if not db_user:
|
||||
if not create_new_user:
|
||||
return (None, 'Invalid user')
|
||||
|
||||
# We must create the user in our db
|
||||
valid_username = None
|
||||
for valid_username in generate_valid_usernames(username):
|
||||
if model.is_username_unique(valid_username):
|
||||
break
|
||||
|
||||
if not valid_username:
|
||||
logger.error('Unable to pick a username for user: %s', username)
|
||||
return (None, 'Unable to pick a username. Please report this to your administrator.')
|
||||
|
||||
db_user = model.create_federated_user(valid_username, email, 'ldap', username,
|
||||
set_password_notification=False)
|
||||
else:
|
||||
# Update the db attributes from ldap
|
||||
db_user.email = email
|
||||
db_user.save()
|
||||
|
||||
return (db_user, None)
|
||||
return _get_federated_user(username, email, 'ldap', create_new_user)
|
||||
|
||||
def user_exists(self, username):
|
||||
found_user = self._ldap_user_search(username)
|
||||
|
@ -243,7 +332,11 @@ class UserAuthentication(object):
|
|||
email_attr = app.config.get('LDAP_EMAIL_ATTR', 'mail')
|
||||
|
||||
users = LDAPUsers(ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr)
|
||||
|
||||
elif authentication_type == 'JWT':
|
||||
verify_url = app.config.get('JWT_VERIFY_ENDPOINT')
|
||||
exists_url = app.config.get('JWT_EXISTS_ENDPOINT')
|
||||
issuer = app.config.get('JWT_AUTH_ISSUER')
|
||||
users = JWTAuthUsers(exists_url, verify_url, issuer)
|
||||
else:
|
||||
raise RuntimeError('Unknown authentication type: %s' % authentication_type)
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ from auth.auth_context import get_authenticated_user
|
|||
from data.database import User
|
||||
from util.config.configutil import add_enterprise_config_defaults
|
||||
from util.config.provider import CannotWriteConfigException
|
||||
from util.config.validator import validate_service_for_config, SSL_FILENAMES
|
||||
from util.config.validator import validate_service_for_config, CONFIG_FILENAMES
|
||||
from data.runmigration import run_alembic_migration
|
||||
|
||||
import features
|
||||
|
@ -224,7 +224,7 @@ class SuperUserConfigFile(ApiResource):
|
|||
@verify_not_prod
|
||||
def get(self, filename):
|
||||
""" Returns whether the configuration file with the given name exists. """
|
||||
if not filename in SSL_FILENAMES:
|
||||
if not filename in CONFIG_FILENAMES:
|
||||
abort(404)
|
||||
|
||||
if SuperUserPermission().can():
|
||||
|
@ -238,7 +238,7 @@ class SuperUserConfigFile(ApiResource):
|
|||
@verify_not_prod
|
||||
def post(self, filename):
|
||||
""" Updates the configuration file with the given name. """
|
||||
if not filename in SSL_FILENAMES:
|
||||
if not filename in CONFIG_FILENAMES:
|
||||
abort(404)
|
||||
|
||||
if SuperUserPermission().can():
|
||||
|
|
|
@ -201,6 +201,7 @@ def initialize_database():
|
|||
LoginService.create(name='github')
|
||||
LoginService.create(name='quayrobot')
|
||||
LoginService.create(name='ldap')
|
||||
LoginService.create(name='jwtauthn')
|
||||
|
||||
BuildTriggerService.create(name='github')
|
||||
BuildTriggerService.create(name='custom-git')
|
||||
|
|
|
@ -53,3 +53,4 @@ stringscore
|
|||
python-swiftclient
|
||||
python-keystoneclient
|
||||
Flask-Testing
|
||||
pyjwt
|
|
@ -14,6 +14,7 @@ Pillow==2.8.1
|
|||
PyMySQL==0.6.6
|
||||
PyPDF2==1.24
|
||||
PyYAML==3.11
|
||||
PyJWT==1.3.0
|
||||
SQLAlchemy==1.0.3
|
||||
WebOb==1.4.1
|
||||
Werkzeug==0.10.4
|
||||
|
|
|
@ -72,8 +72,8 @@
|
|||
line with a non-encrypted password and must generate an encrypted
|
||||
password to use.
|
||||
</div>
|
||||
<div class="help-text" ng-if="config.AUTHENTICATION_TYPE == 'LDAP'">
|
||||
This feature is <strong>highly recommended</strong> for setups with LDAP authentication, as Docker currently stores passwords in <strong>plaintext</strong> on user's machines.
|
||||
<div class="help-text" ng-if="config.AUTHENTICATION_TYPE != 'Database'">
|
||||
This feature is <strong>highly recommended</strong> for setups with external authentication, as Docker currently stores passwords in <strong>plaintext</strong> on user's machines.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -330,19 +330,20 @@
|
|||
<div class="co-panel-body">
|
||||
<div class="description">
|
||||
<p>
|
||||
Authentication for the registry can be handled by either the registry itself or LDAP.
|
||||
External authentication providers (such as GitHub) can be used on top of this choice.
|
||||
Authentication for the registry can be handled by either the registry itself, LDAP or external JWT endpoint.
|
||||
<br>
|
||||
Additional external authentication providers (such as GitHub) can be used on top of this choice.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="co-alert co-alert-warning" ng-if="config.AUTHENTICATION_TYPE == 'LDAP' && !config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH">
|
||||
It is <strong>highly recommended</strong> to require encrypted client passwords. LDAP passwords used in the Docker client will be stored in <strong>plaintext</strong>!
|
||||
<div class="co-alert co-alert-warning" ng-if="config.AUTHENTICATION_TYPE != 'Database' && !config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH">
|
||||
It is <strong>highly recommended</strong> to require encrypted client passwords. External passwords used in the Docker client will be stored in <strong>plaintext</strong>!
|
||||
<a href="javascript:void(0)" ng-click="config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH = true">Enable this requirement now</a>.
|
||||
</div>
|
||||
|
||||
<div class="co-alert co-alert-success" ng-if="config.AUTHENTICATION_TYPE == 'LDAP' && config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH">
|
||||
<div class="co-alert co-alert-success" ng-if="config.AUTHENTICATION_TYPE != 'Database' && config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH">
|
||||
Note: The "Require Encrypted Client Passwords" feature is currently enabled which will
|
||||
prevent LDAP passwords from being saved as plaintext by the Docker client.
|
||||
prevent passwords from being saved as plaintext by the Docker client.
|
||||
</div>
|
||||
|
||||
<table class="config-table">
|
||||
|
@ -352,11 +353,72 @@
|
|||
<select ng-model="config.AUTHENTICATION_TYPE">
|
||||
<option value="Database">Local Database</option>
|
||||
<option value="LDAP">LDAP</option>
|
||||
<option value="JWT">JWT Custom Authentication</option>
|
||||
</select>
|
||||
</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
|
||||
verifies user credentials on behalf of <span class="registry-name"></span>.
|
||||
<br>
|
||||
Documentation
|
||||
on the API required can be found here: <a href="https://coreos.com/docs/enterprise-registry/jwt-auth" target="_blank">https://coreos.com/docs/enterprise-registry/jwt-auth</a>.
|
||||
</div>
|
||||
|
||||
<table class="config-table" ng-if="config.AUTHENTICATION_TYPE == 'JWT'">
|
||||
<tr>
|
||||
<td>User Verification Endpoint:</td>
|
||||
<td>
|
||||
<span class="config-string-field" binding="config.JWT_VERIFY_ENDPOINT"
|
||||
pattern="http(s)?://.+"></span>
|
||||
<div class="help-text">
|
||||
The URL (starting with http or https) on the JWT authentication server for verifying username and password credentials.
|
||||
</div>
|
||||
|
||||
<div class="help-text" style="margin-top: 6px;">
|
||||
Credentials will be sent in the <code>Authorization</code> header as Basic Auth, and this endpoint should return <code>200 OK</code> on success (or a <code>4**</code> otherwise).
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>User Exists Endpoint:</td>
|
||||
<td>
|
||||
<span class="config-string-field" binding="config.JWT_EXISTS_ENDPOINT"
|
||||
pattern="http(s)?://.+"></span>
|
||||
<div class="help-text">
|
||||
The URL (starting with http or https) on the JWT authentication server for checking whether a username exists.
|
||||
</div>
|
||||
|
||||
<div class="help-text" style="margin-top: 6px;">
|
||||
The username will be sent in the <code>Authorization</code> header as Basic Auth, and this endpoint should return <code>200 OK</code> on success (or a <code>4**</code> otherwise).
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Authentication Issuer:</td>
|
||||
<td>
|
||||
<span class="config-string-field" binding="config.JWT_AUTH_ISSUER"></span>
|
||||
<div class="help-text">
|
||||
The id of the issuer signing the JWT token. Must be unique to your organization.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Public Key:</td>
|
||||
<td>
|
||||
<span class="config-file-field" filename="jwt-authn.cert"></span>
|
||||
<div class="help-text">
|
||||
A certificate containing the public key portion of the key pair used to sign
|
||||
the JSON Web Tokens. This file must be in PEM format.
|
||||
</div
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- LDAP Authentication -->
|
||||
<table class="config-table" ng-if="config.AUTHENTICATION_TYPE == 'LDAP'">
|
||||
<tr>
|
||||
<td>LDAP URI:</td>
|
||||
|
|
|
@ -27,6 +27,10 @@ angular.module("core-config-setup", ['angularFileUpload'])
|
|||
return config.AUTHENTICATION_TYPE == 'LDAP';
|
||||
}},
|
||||
|
||||
{'id': 'jwt', 'title': 'JWT Authentication', 'condition': function(config) {
|
||||
return config.AUTHENTICATION_TYPE == 'JWT';
|
||||
}},
|
||||
|
||||
{'id': 'mail', 'title': 'E-mail Support', 'condition': function(config) {
|
||||
return config.FEATURE_MAILING;
|
||||
}},
|
||||
|
|
156
test/test_jwt_auth.py
Normal file
156
test/test_jwt_auth.py
Normal file
|
@ -0,0 +1,156 @@
|
|||
import unittest
|
||||
import requests
|
||||
import jwt
|
||||
import base64
|
||||
|
||||
from app import app
|
||||
from flask import Flask, abort, jsonify, request, make_response
|
||||
from flask.ext.testing import LiveServerTestCase
|
||||
from initdb import setup_database_for_testing, finished_database_for_testing
|
||||
from data.users import JWTAuthUsers
|
||||
from tempfile import NamedTemporaryFile
|
||||
from Crypto.PublicKey import RSA
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class JWTAuthTestCase(LiveServerTestCase):
|
||||
maxDiff = None
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
public_key = NamedTemporaryFile(delete=True)
|
||||
|
||||
key = RSA.generate(1024)
|
||||
private_key_data = key.exportKey('PEM')
|
||||
|
||||
pubkey = key.publickey()
|
||||
public_key.write(pubkey.exportKey('OpenSSH'))
|
||||
public_key.seek(0)
|
||||
|
||||
JWTAuthTestCase.public_key = public_key
|
||||
JWTAuthTestCase.private_key_data = private_key_data
|
||||
|
||||
def create_app(self):
|
||||
users = [
|
||||
{ 'name': 'cooluser', 'email': 'user@domain.com', 'password': 'password' },
|
||||
{ 'name': 'some.neat.user', 'email': 'neat@domain.com', 'password': 'foobar'}
|
||||
]
|
||||
|
||||
jwt_app = Flask('testjwt')
|
||||
private_key = JWTAuthTestCase.private_key_data
|
||||
|
||||
def _get_basic_auth():
|
||||
data = base64.b64decode(request.headers['Authorization'][len('Basic '):])
|
||||
return data.split(':', 1)
|
||||
|
||||
@jwt_app.route('/user/exists', methods=['GET'])
|
||||
def user_exists():
|
||||
username, _ = _get_basic_auth()
|
||||
for user in users:
|
||||
if user['name'] == username or user['email'] == username:
|
||||
return 'OK'
|
||||
|
||||
abort(404)
|
||||
|
||||
@jwt_app.route('/user/verify', methods=['GET'])
|
||||
def verify_user():
|
||||
username, password = _get_basic_auth()
|
||||
|
||||
if username == 'disabled':
|
||||
return make_response('User is currently disabled', 401)
|
||||
|
||||
for user in users:
|
||||
if user['name'] == username or user['email'] == username:
|
||||
if password != user['password']:
|
||||
return make_response('', 404)
|
||||
|
||||
token_data = {
|
||||
'iss': 'authy',
|
||||
'aud': 'quay.io/jwtauthn',
|
||||
'nbf': datetime.utcnow(),
|
||||
'exp': datetime.utcnow() + timedelta(seconds=60),
|
||||
'sub': user['name'],
|
||||
'email': user['email']
|
||||
}
|
||||
|
||||
encoded = jwt.encode(token_data, private_key, 'RS256')
|
||||
return jsonify({
|
||||
'token': encoded
|
||||
})
|
||||
|
||||
return make_response('', 404)
|
||||
|
||||
jwt_app.config['TESTING'] = True
|
||||
jwt_app.config['DEBUG'] = True
|
||||
return jwt_app
|
||||
|
||||
|
||||
def setUp(self):
|
||||
setup_database_for_testing(self)
|
||||
self.app = app.test_client()
|
||||
self.ctx = app.test_request_context()
|
||||
self.ctx.__enter__()
|
||||
|
||||
self.session = requests.Session()
|
||||
|
||||
self.jwt_auth = JWTAuthUsers(
|
||||
self.get_server_url() + '/user/exists',
|
||||
self.get_server_url() + '/user/verify',
|
||||
'authy', JWTAuthTestCase.public_key.name)
|
||||
|
||||
def tearDown(self):
|
||||
finished_database_for_testing(self)
|
||||
self.ctx.__exit__(True, None, None)
|
||||
|
||||
def test_user_exists(self):
|
||||
self.assertFalse(self.jwt_auth.user_exists('testuser'))
|
||||
self.assertFalse(self.jwt_auth.user_exists('anotheruser'))
|
||||
|
||||
self.assertTrue(self.jwt_auth.user_exists('cooluser'))
|
||||
self.assertTrue(self.jwt_auth.user_exists('user@domain.com'))
|
||||
|
||||
def test_verify_user(self):
|
||||
result, error_message = self.jwt_auth.verify_user('invaliduser', 'foobar')
|
||||
self.assertEquals('Invalid username or password', error_message)
|
||||
self.assertIsNone(result)
|
||||
|
||||
result, _ = self.jwt_auth.verify_user('cooluser', 'invalidpassword')
|
||||
self.assertIsNone(result)
|
||||
|
||||
result, _ = self.jwt_auth.verify_user('cooluser', 'password')
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEquals('cooluser', result.username)
|
||||
|
||||
result, _ = self.jwt_auth.verify_user('some.neat.user', 'foobar')
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEquals('some_neat_user', result.username)
|
||||
|
||||
def test_confirm_existing_user(self):
|
||||
# Create the users in the DB.
|
||||
result, _ = self.jwt_auth.verify_user('cooluser', 'password')
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
result, _ = self.jwt_auth.verify_user('some.neat.user', 'foobar')
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
# Confirm a user with the same internal and external username.
|
||||
result, _ = self.jwt_auth.confirm_existing_user('cooluser', 'password')
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEquals('cooluser', result.username)
|
||||
|
||||
# Fail to confirm the *external* username, which should return nothing.
|
||||
result, _ = self.jwt_auth.confirm_existing_user('some.neat.user', 'password')
|
||||
self.assertIsNone(result)
|
||||
|
||||
# Now confirm the internal username.
|
||||
result, _ = self.jwt_auth.confirm_existing_user('some_neat_user', 'foobar')
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEquals('some_neat_user', result.username)
|
||||
|
||||
def test_disabled_user_custom_erorr(self):
|
||||
result, error_message = self.jwt_auth.verify_user('disabled', 'password')
|
||||
self.assertIsNone(result)
|
||||
self.assertEquals('User is currently disabled', error_message)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -7,7 +7,7 @@ import OpenSSL
|
|||
import logging
|
||||
|
||||
from fnmatch import fnmatch
|
||||
from data.users import LDAPConnection
|
||||
from data.users import LDAPConnection, JWTAuthUsers
|
||||
from flask import Flask
|
||||
from flask.ext.mail import Mail, Message
|
||||
from data.database import validate_database_url, User
|
||||
|
@ -20,6 +20,8 @@ from bitbucket import BitBucket
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
SSL_FILENAMES = ['ssl.cert', 'ssl.key']
|
||||
JWT_FILENAMES = ['jwt-authn.cert']
|
||||
CONFIG_FILENAMES = SSL_FILENAMES + JWT_FILENAMES
|
||||
|
||||
def get_storage_provider(config):
|
||||
parameters = config.get('DISTRIBUTED_STORAGE_CONFIG', {}).get('local', ['LocalStorage', {}])
|
||||
|
@ -303,6 +305,36 @@ def _validate_ldap(config):
|
|||
raise Exception(values.get('desc', 'Unknown error'))
|
||||
|
||||
|
||||
def _validate_jwt(config):
|
||||
""" Validates the JWT authentication system. """
|
||||
if config.get('AUTHENTICATION_TYPE', 'Database') != 'JWT':
|
||||
return
|
||||
|
||||
verify_endpoint = config.get('JWT_VERIFY_ENDPOINT')
|
||||
exists_endpoint = config.get('JWT_EXISTS_ENDPOINT')
|
||||
issuer = config.get('JWT_AUTH_ISSUER')
|
||||
|
||||
if not verify_endpoint:
|
||||
raise Exception('Missing JWT Verification endpoint')
|
||||
|
||||
if not exists_endpoint:
|
||||
raise Exception('Missing JWT Exists endpoint')
|
||||
|
||||
if not issuer:
|
||||
raise Exception('Missing JWT Issuer ID')
|
||||
|
||||
# Try to instatiate the JWT authentication mechanism. This will raise an exception if
|
||||
# the key cannot be found.
|
||||
users = JWTAuthUsers(exists_endpoint, verify_endpoint, issuer)
|
||||
|
||||
# Verify that the superuser exists. If not, raise an exception.
|
||||
username = get_authenticated_user().username
|
||||
result = users.user_exists(username)
|
||||
if not result:
|
||||
raise Exception('Verification of superuser %s failed. The user either does not exist ' +
|
||||
'in the remote authentication system OR JWT auth is misconfigured.' % username)
|
||||
|
||||
|
||||
_VALIDATORS = {
|
||||
'database': _validate_database,
|
||||
'redis': _validate_redis,
|
||||
|
@ -315,4 +347,5 @@ _VALIDATORS = {
|
|||
'google-login': _validate_google_login,
|
||||
'ssl': _validate_ssl,
|
||||
'ldap': _validate_ldap,
|
||||
'jwt': _validate_jwt,
|
||||
}
|
Reference in a new issue