Merge pull request #2695 from coreos-inc/oidc-internal-auth

OIDC internal auth support
This commit is contained in:
josephschorr 2017-10-02 16:51:17 -04:00 committed by GitHub
commit 3bef21253d
29 changed files with 341 additions and 38 deletions

View file

@ -2,8 +2,8 @@ import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), "../"))
from util.log import logfile_path
from Crypto import Random
from util.log import logfile_path
logconfig = logfile_path(debug=True)

View file

@ -2,8 +2,8 @@ import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), "../"))
from util.log import logfile_path
from Crypto import Random
from util.log import logfile_path
logconfig = logfile_path(debug=False)

View file

@ -2,8 +2,8 @@ import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), "../"))
from util.log import logfile_path
from Crypto import Random
from util.log import logfile_path
logconfig = logfile_path(debug=False)

View file

@ -2,9 +2,8 @@ import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), "../"))
from util.log import logfile_path
from Crypto import Random
from util.log import logfile_path
logconfig = logfile_path(debug=False)

View file

@ -2,8 +2,8 @@ import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), "../"))
from util.log import logfile_path
from Crypto import Random
from util.log import logfile_path
logconfig = logfile_path(debug=False)

View file

@ -23,7 +23,7 @@ CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'MIXPANEL_KEY',
'CONTACT_INFO', 'AVATAR_KIND', 'LOCAL_OAUTH_HANDLER', 'DOCUMENTATION_LOCATION',
'DOCUMENTATION_METADATA', 'SETUP_COMPLETE', 'DEBUG', 'MARKETO_MUNCHKIN_ID',
'STATIC_SITE_BUCKET', 'RECAPTCHA_SITE_KEY', 'CHANNEL_COLORS',
'TAG_EXPIRATION_OPTIONS']
'TAG_EXPIRATION_OPTIONS', 'INTERNAL_OIDC_SERVICE_ID']
def frontend_visible_config(config_dict):

View file

@ -10,6 +10,7 @@ from data.users.database import DatabaseUsers
from data.users.externalldap import LDAPUsers
from data.users.externaljwt import ExternalJWTAuthN
from data.users.keystone import get_keystone_users
from data.users.oidc import OIDCInternalAuth
from util.security.aes import AESCipher
logger = logging.getLogger(__name__)
@ -24,6 +25,12 @@ def get_federated_service_name(authentication_type):
if authentication_type == 'Keystone':
return 'keystone'
if authentication_type == 'OIDC':
return None
if authentication_type == 'Database':
return None
raise Exception('Unknown auth type: %s' % authentication_type)
@ -74,8 +81,15 @@ def get_users_handler(config, _, override_config_dir):
keystone_admin_password = config.get('KEYSTONE_ADMIN_PASSWORD')
keystone_admin_tenant = config.get('KEYSTONE_ADMIN_TENANT')
return get_keystone_users(auth_version, auth_url, keystone_admin_username,
keystone_admin_password, keystone_admin_tenant, timeout,
requires_email=features.MAILING)
keystone_admin_password, keystone_admin_tenant, timeout,
requires_email=features.MAILING)
if authentication_type == 'OIDC':
if features.DIRECT_LOGIN:
raise Exception('Direct login feature must be disabled to use OIDC internal auth')
login_service = config.get('INTERNAL_OIDC_SERVICE_ID')
return OIDCInternalAuth(config, login_service, requires_email=features.MAILING)
raise RuntimeError('Unknown authentication type: %s' % authentication_type)
@ -160,6 +174,11 @@ class UserAuthentication(object):
"""
return self.state.federated_service
@property
def supports_encrypted_credentials(self):
""" Returns whether this auth system supports using encrypted credentials. """
return self.state.supports_encrypted_credentials
def query_users(self, query, limit=20):
""" Performs a lookup against the user system for the specified query. The returned tuple
will be of the form (results, federated_login_id, err_msg). If the method is unsupported,

View file

@ -9,6 +9,10 @@ class DatabaseUsers(object):
""" Always assumed to be working. If the DB is broken, other checks will handle it. """
return (True, None)
@property
def supports_encrypted_credentials(self):
return True
def verify_credentials(self, username_or_email, password):
""" Simply delegate to the model implementation. """
result = model.user.verify_user(username_or_email, password)

View file

@ -24,6 +24,10 @@ class FederatedUsers(object):
def federated_service(self):
return self._federated_service
@property
def supports_encrypted_credentials(self):
return True
def get_user(self, username_or_email):
""" Retrieves the user with the given username or email, returning a tuple containing
a UserInformation (if success) and the error message (on failure).

83
data/users/oidc.py Normal file
View file

@ -0,0 +1,83 @@
import logging
from data import model
from oauth.loginmanager import OAuthLoginManager
from oauth.oidc import PublicKeyLoadException
from util.security.jwtutil import InvalidTokenError
logger = logging.getLogger(__name__)
class UnknownServiceException(Exception):
pass
class OIDCInternalAuth(object):
""" Handles authentication by delegating authentication to a signed OIDC JWT produced by the
configured OIDC service.
"""
def __init__(self, config, login_service_id, requires_email):
login_manager = OAuthLoginManager(config)
self.login_service_id = login_service_id
self.login_service = login_manager.get_service(login_service_id)
if self.login_service is None:
raise UnknownServiceException('Unknown OIDC login service %s' % login_service_id)
@property
def federated_service(self):
return None
@property
def supports_encrypted_credentials(self):
# Since the "password" is already a signed JWT.
return False
def verify_credentials(self, username_or_email, id_token):
# Parse the ID token.
try:
payload = self.login_service.decode_user_jwt(id_token)
except InvalidTokenError as ite:
logger.exception('Got invalid token error on OIDC decode: %s. Token: %s', ite.message, id_token)
return (None, 'Could not validate OIDC token')
except PublicKeyLoadException as pke:
logger.exception('Could not load public key during OIDC decode: %s. Token: %s', pke.message, id_token)
return (None, 'Could not validate OIDC token')
# Find the user ID.
user_id = payload['sub']
# Lookup the federated login and user record with that matching ID and service.
user_found = model.user.verify_federated_login(self.login_service_id, user_id)
if user_found is None:
return (None, 'User does not exist')
if not user_found.enabled:
return (None, 'User account is disabled. Please contact your administrator.')
return (user_found, None)
def verify_and_link_user(self, username_or_email, password):
return self.verify_credentials(username_or_email, password)
def confirm_existing_user(self, username, password):
return self.verify_credentials(username, password)
def link_user(self, username_or_email):
return (None, 'Unsupported for this authentication system')
def get_and_link_federated_user_info(self, user_info):
return (None, 'Unsupported for this authentication system')
def query_users(self, query, limit):
return (None, '', '')
def check_group_lookup_args(self, group_lookup_args):
return (False, 'Not supported')
def iterate_group_members(self, group_lookup_args, page_size=None, disable_pagination=False):
return (None, 'Not supported')
def service_metadata(self):
return {}

View file

@ -0,0 +1,38 @@
import pytest
from httmock import HTTMock
from data import model
from data.users.oidc import OIDCInternalAuth
from oauth.test.test_oidc import *
from test.fixtures import *
@pytest.mark.parametrize('username, expect_success', [
('devtable', True),
('disabled', False)
])
def test_oidc_login(username, expect_success, app_config, id_token, jwks_handler,
discovery_handler, app):
internal_auth = OIDCInternalAuth(app_config, 'someoidc', False)
with HTTMock(jwks_handler, discovery_handler):
# Try an invalid token.
(user, err) = internal_auth.verify_credentials('someusername', 'invalidtoken')
assert err is not None
assert user is None
# Try a valid token for an unlinked user.
(user, err) = internal_auth.verify_credentials('someusername', id_token)
assert err is not None
assert user is None
# Link the user to the service.
model.user.attach_federated_login(model.user.get_user(username), 'someoidc', 'cooluser')
# Try a valid token for a linked user.
(user, err) = internal_auth.verify_credentials('someusername', id_token)
if expect_success:
assert err is None
assert user.username == username
else:
assert err is not None
assert user is None

View file

@ -217,9 +217,10 @@ class SuperUserConfig(ApiResource):
# Write the configuration changes to the config override file.
config_provider.save_config(config_object)
# If the authentication system is not the database, link the superuser account to the
# If the authentication system is federated, link the superuser account to the
# the authentication system chosen.
if config_object.get('AUTHENTICATION_TYPE', 'Database') != 'Database':
service_name = get_federated_service_name(config_object['AUTHENTICATION_TYPE'])
if service_name is not None:
current_user = get_authenticated_user()
if current_user is None:
abort(401)

View file

@ -529,6 +529,9 @@ class ClientKey(ApiResource):
@validate_json_request('GenerateClientKey')
def post(self):
""" Return's the user's private client key. """
if not authentication.supports_encrypted_credentials:
raise NotFound()
username = get_authenticated_user().username
password = request.get_json()['password']
(result, error_message) = authentication.confirm_existing_user(username, password)
@ -744,7 +747,7 @@ class ExternalLoginInformation(ApiResource):
'kind': {
'type': 'string',
'description': 'The kind of URL',
'enum': ['login', 'attach'],
'enum': ['login', 'attach', 'cli'],
},
},
},
@ -762,7 +765,7 @@ class ExternalLoginInformation(ApiResource):
csrf_token = generate_csrf_token(OAUTH_CSRF_TOKEN_NAME)
kind = request.get_json()['kind']
redirect_suffix = '/attach' if kind == 'attach' else ''
redirect_suffix = '' if kind == 'login' else '/' + kind
try:
login_scopes = login_service.get_login_scopes()

View file

@ -252,6 +252,24 @@ def _register_service(login_service):
auth_url = login_service.get_auth_url(app.config, '', csrf_token, login_scopes)
return redirect(auth_url)
@require_session_login
@oauthlogin_csrf_protect
def cli_token_func():
# Check for a callback error.
error = request.args.get('error', None)
if error:
return _render_ologin_error(login_service.service_name(), error)
# Exchange the OAuth code for the ID token.
code = request.args.get('code')
try:
idtoken, _ = login_service.exchange_code_for_tokens(app.config, client, code, '/cli')
except OAuthLoginException as ole:
return _render_ologin_error(login_service.service_name(), ole.message)
user_obj = get_authenticated_user()
return redirect(url_for('web.user_view', path=user_obj.username, tab='settings',
idtoken=idtoken))
oauthlogin.add_url_rule('/%s/callback/captcha' % login_service.service_id(),
'%s_oauth_captcha' % login_service.service_id(),
@ -268,6 +286,11 @@ def _register_service(login_service):
attach_func,
methods=['GET'])
oauthlogin.add_url_rule('/%s/callback/cli' % login_service.service_id(),
'%s_oauth_cli' % login_service.service_id(),
cli_token_func,
methods=['GET'])
# Register the routes for each of the login services.
for current_service in oauth_login.services:
_register_service(current_service)

View file

@ -267,6 +267,7 @@ def initialize_database():
LoginService.create(name='jwtauthn')
LoginService.create(name='keystone')
LoginService.create(name='dex')
LoginService.create(name='oidc')
BuildTriggerService.create(name='github')
BuildTriggerService.create(name='custom-git')

View file

@ -97,7 +97,6 @@ class OAuthService(object):
def get_user_info(self, http_client, token):
token_param = {
'access_token': token,
'alt': 'json',
}

View file

@ -1,7 +1,6 @@
from oauth.services.github import GithubOAuthService
from oauth.services.google import GoogleOAuthService
from oauth.oidc import OIDCLoginService
from data.users import UserAuthentication
CUSTOM_LOGIN_SERVICES = {
'GITHUB_LOGIN_CONFIG': GithubOAuthService,

View file

@ -39,7 +39,7 @@ class OIDCLoginService(OAuthService):
self._public_key_cache = TTLCache(1, PUBLIC_KEY_CACHE_TTL, missing=self._load_public_key)
self._id = key_name[0:key_name.find('_')].lower()
self._http_client = client or config['HTTPCLIENT']
self._http_client = client or config.get('HTTPCLIENT')
self._mailing = config.get('FEATURE_MAILING', False)
def service_id(self):
@ -89,7 +89,7 @@ class OIDCLoginService(OAuthService):
'OIDC': True,
}
def exchange_code_for_login(self, app_config, http_client, code, redirect_suffix):
def exchange_code_for_tokens(self, app_config, http_client, code, redirect_suffix):
# Exchange the code for the access token and id_token
try:
json_data = self.exchange_code(app_config, http_client, code,
@ -109,9 +109,16 @@ class OIDCLoginService(OAuthService):
logger.debug('Missing id_token in response: %s', json_data)
raise OAuthLoginException('Missing `id_token` in OIDC response')
return id_token, access_token
def exchange_code_for_login(self, app_config, http_client, code, redirect_suffix):
# Exchange the code for the access token and id_token
id_token, access_token = self.exchange_code_for_tokens(app_config, http_client, code,
redirect_suffix)
# Decode the id_token.
try:
decoded_id_token = self._decode_user_jwt(id_token)
decoded_id_token = self.decode_user_jwt(id_token)
except InvalidTokenError as ite:
logger.exception('Got invalid token error on OIDC decode: %s', ite.message)
raise OAuthLoginException('Could not decode OIDC token')
@ -181,7 +188,7 @@ class OIDCLoginService(OAuthService):
logger.exception('Could not parse OIDC discovery for url: %s', discovery_url)
raise DiscoveryFailureException("Could not parse OIDC discovery information")
def _decode_user_jwt(self, token):
def decode_user_jwt(self, token):
""" Decodes the given JWT under the given provider and returns it. Raises an InvalidTokenError
exception on an invalid token or a PublicKeyLoadException if the public key could not be
loaded for decoding.

0
oauth/test/__init__.py Normal file
View file

View file

@ -60,7 +60,7 @@ def app_config(http_client, mailing_feature):
'SERVER_HOSTNAME': 'localhost',
'FEATURE_MAILING': mailing_feature,
'SOMEOIDC_TEST_SERVICE': {
'SOMEOIDC_LOGIN_CONFIG': {
'CLIENT_ID': 'foo',
'CLIENT_SECRET': 'bar',
'SERVICE_NAME': 'Some Cool Service',
@ -74,7 +74,7 @@ def app_config(http_client, mailing_feature):
@pytest.fixture()
def oidc_service(app_config):
return OIDCLoginService(app_config, 'SOMEOIDC_TEST_SERVICE')
return OIDCLoginService(app_config, 'SOMEOIDC_LOGIN_CONFIG')
@pytest.fixture()
def discovery_content(userinfo_supported):

View file

@ -622,21 +622,23 @@
<div class="co-panel-body">
<div class="description">
<p>
Authentication for the registry can be handled by either the registry itself, LDAP or external JWT endpoint.
Authentication for the registry can be handled by either the registry itself, LDAP, Keystone, OIDC or external JWT endpoint.
</p>
<p>
Additional <strong>external</strong> authentication providers (such as GitHub) can be used in addition for <strong>login into the UI</strong>.
</p>
</div>
<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 ng-click="config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH = true">Enable this requirement now</a>.
</div>
<div ng-if="config.AUTHENTICATION_TYPE != 'OIDC'">
<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 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 != 'Database' && config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH">
Note: The "Require Encrypted Client Passwords" feature is currently enabled which will
prevent passwords from being saved as plaintext by the Docker client.
<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 passwords from being saved as plaintext by the Docker client.
</div>
</div>
<table class="config-table" style="margin-bottom: 20px;">
@ -648,6 +650,7 @@
<option value="LDAP">LDAP</option>
<option value="Keystone">Keystone (OpenStack Identity)</option>
<option value="JWT">JWT Custom Authentication</option>
<option value="OIDC">OIDC Token Authentication</option>
</select>
</td>
</tr>
@ -687,6 +690,21 @@
</tr>
</table>
<!-- OIDC Token Authentication -->
<table class="config-table" ng-if="config.AUTHENTICATION_TYPE == 'OIDC'">
<tr>
<td>OIDC Provider:</td>
<td>
<select class="form-control" ng-model="config.INTERNAL_OIDC_SERVICE_ID" ng-if="getOIDCProviders(config).length">
<option value="{{ getOIDCProviderId(provider) }}" ng-repeat="provider in getOIDCProviders(config)">{{ config[provider]['SERVICE_NAME'] || getOIDCProviderId(provider) }}</option>
</select>
<div class="co-alert co-alert-danger" ng-if="!getOIDCProviders(config).length">
An OIDC provider must be configured to use this authentication system
</div>
</td>
</tr>
</table>
<!-- Keystone Authentication -->
<table class="config-table" ng-if="config.AUTHENTICATION_TYPE == 'Keystone'">
<tr>
@ -1073,7 +1091,7 @@
<span style="display: inline-block; margin-left: 10px">(<a href="javascript:void(0)" ng-click="removeOIDCProvider(provider)">Delete</a>)</span>
</div>
<div class="co-panel-body">
<div class="co-alert co-alert-warning" ng-if="config.AUTHENTICATION_TYPE != 'Database' && !(config[provider].LOGIN_BINDING_FIELD)">
<div class="co-alert co-alert-warning" ng-if="config.AUTHENTICATION_TYPE && config.AUTHENTICATION_TYPE != 'Database' && config.AUTHENTICATION_TYPE != 'OIDC' && !(config[provider].LOGIN_BINDING_FIELD)">
Warning: This OIDC provider is not bound to your <strong>{{ config.AUTHENTICATION_TYPE }}</strong> authentication. Logging in via this provider will create a <strong><span class="registry-name"></span>-only user</strong>, which is not the recommended approach. It is <strong>highly</strong> recommended to choose a "Binding Field" below.
</div>
@ -1134,7 +1152,7 @@
</div>
</td>
</tr>
<tr ng-if="config.AUTHENTICATION_TYPE != 'Database'">
<tr ng-if="config.AUTHENTICATION_TYPE != 'Database' && config.AUTHENTICATION_TYPE != 'OIDC'">
<td>Binding Field:</td>
<td>
<select class="form-control" ng-model="config[provider].LOGIN_BINDING_FIELD">
@ -1292,7 +1310,7 @@
</div>
<div class="co-panel-body">
<div class="description">
If enabled, users can submit Dockerfiles to be built and pushed by the Enterprise Registry.
If enabled, users can submit Dockerfiles to be built and pushed by <span class="registry-name"></span>.
</div>
<div class="config-bool-field" binding="config.FEATURE_BUILD_SUPPORT">

View file

@ -43,6 +43,10 @@ angular.module("core-config-setup", ['angularFileUpload'])
return config.AUTHENTICATION_TYPE == 'Keystone';
}, 'password': true},
{'id': 'oidc-auth', 'title': 'OIDC Authentication', 'condition': function(config) {
return config.AUTHENTICATION_TYPE == 'OIDC';
}},
{'id': 'signer', 'title': 'ACI Signing', 'condition': function(config) {
return config.FEATURE_ACI_CONVERSION;
}},
@ -203,7 +207,7 @@ angular.module("core-config-setup", ['angularFileUpload'])
return null;
}
return key.substr(0, index);
return key.substr(0, index).toLowerCase();
};
$scope.getOIDCProviders = function(config) {
@ -687,6 +691,12 @@ angular.module("core-config-setup", ['angularFileUpload'])
$scope.configform.$setValidity('storageConfig', valid);
};
$scope.$watch('config.INTERNAL_OIDC_SERVICE_ID', function(service_id) {
if (service_id) {
$scope.config['FEATURE_DIRECT_LOGIN'] = false;
}
});
$scope.$watch('config.FEATURE_STORAGE_REPLICATION', function() {
refreshStorageConfig();
});

View file

@ -21,7 +21,6 @@ angular.module('quay').directive('externalLoginButton', function () {
$scope.startSignin = function() {
$scope.signInStarted({'service': $scope.provider});
ExternalLoginService.getLoginUrl($scope.provider, $scope.action || 'login', function(url) {
// Save the redirect URL in a cookie so that we can redirect back after the service returns to us.
var redirectURL = $scope.redirectUrl || window.location.toString();
CookieService.putPermanent('quay.redirectAfterLoad', redirectURL);

View file

@ -237,11 +237,10 @@ import * as URI from 'urijs';
$scope.serializeDbUri = function(fields) {
if (!fields['server']) { return ''; }
if (!fields['database']) { return ''; }
var uri = URI();
try {
if (!fields['server']) { return ''; }
if (!fields['database']) { return ''; }
uri = uri && uri.host(fields['server']);
uri = uri && uri.protocol(fields['kind']);
uri = uri && uri.username(fields['username']);

View file

@ -13,6 +13,8 @@
function UserViewCtrl($scope, $routeParams, $timeout, ApiService, UserService, UIService, AvatarService, Config, ExternalLoginService) {
var username = $routeParams.username;
$scope.Config = Config;
$scope.showAppsCounter = 0;
$scope.showRobotsCounter = 0;
$scope.showBillingCounter = 0;
@ -25,7 +27,27 @@
$scope.hasSingleSignin = ExternalLoginService.hasSingleSignin();
$scope.context = {};
UserService.updateUserIn($scope);
$scope.oidcLoginProvider = null;
if (Config['INTERNAL_OIDC_SERVICE_ID']) {
ExternalLoginService.EXTERNAL_LOGINS.forEach(function(provider) {
if (provider.id == Config['INTERNAL_OIDC_SERVICE_ID']) {
$scope.oidcLoginProvider = provider;
}
});
}
UserService.updateUserIn($scope, function(user) {
if (user && user.username) {
if ($scope.oidcLoginProvider && $routeParams['idtoken']) {
$scope.context.idTokenCredentials = {
'username': UserService.getCLIUsername(),
'password': $routeParams['idtoken'],
'namespace': UserService.currentUser().username
};
}
}
});
var loadRepositories = function() {
var options = {

View file

@ -70,8 +70,25 @@
<!-- Settings -->
<cor-tab-pane id="settings">
<!-- OIDC Token -->
<div class="settings-section" ng-if="Config.AUTHENTICATION_TYPE == 'OIDC'">
<h3>Docker CLI Token</h3>
<div>
A generated token is <strong>required</strong> to login via the Docker CLI.
</div>
<table class="co-list-table" style="margin-top: 10px;">
<tr>
<td>CLI Token:</td>
<td>
<span class="external-login-button" is-link="true" action="cli" provider="oidcLoginProvider"></span>
</td>
</tr>
</table>
</div>
<!-- Encrypted Password -->
<div class="settings-section">
<div class="settings-section" ng-if="Config.AUTHENTICATION_TYPE != 'OIDC'">
<h3>Docker CLI Password</h3>
<div ng-if="!Features.REQUIRE_ENCRYPTED_BASIC_AUTH">
The Docker CLI stores passwords entered on the command line in <strong>plaintext</strong>. It is therefore highly recommended to generate an an encrypted version of your password to use for <code>docker login</code>.
@ -216,4 +233,7 @@
<!-- Credentials for encrypted passwords -->
<div class="credentials-dialog" credentials="context.encryptedPasswordCredentials" secret-title="Encrypted Password" entity-title="encrypted password" entity-icon="fa-key">
<!-- Credentials for ID token -->
<div class="credentials-dialog" credentials="context.idTokenCredentials" secret-title="CLI Token" entity-title="Docker CLI token" entity-icon="fa-key">
</div>

View file

@ -22,6 +22,7 @@ from util.config.validators.validate_oidc import OIDCLoginValidator
from util.config.validators.validate_timemachine import TimeMachineValidator
from util.config.validators.validate_access import AccessSettingsValidator
from util.config.validators.validate_actionlog_archiving import ActionLogArchivingValidator
from util.config.validators.validate_oidcauth import OIDCAuthValidator
logger = logging.getLogger(__name__)
@ -59,6 +60,7 @@ VALIDATORS = {
TimeMachineValidator.name: TimeMachineValidator.validate,
AccessSettingsValidator.name: AccessSettingsValidator.validate,
ActionLogArchivingValidator.name: ActionLogArchivingValidator.validate,
OIDCAuthValidator.name: OIDCAuthValidator.validate,
}
def validate_service_for_config(service, config, password=None):

View file

@ -0,0 +1,32 @@
import pytest
from util.config.validators import ConfigValidationException
from util.config.validators.validate_oidcauth import OIDCAuthValidator
from test.fixtures import *
@pytest.mark.parametrize('unvalidated_config', [
({'AUTHENTICATION_TYPE': 'OIDC'}),
({'AUTHENTICATION_TYPE': 'OIDC', 'INTERNAL_OIDC_SERVICE_ID': 'someservice'}),
])
def test_validate_invalid_oidc_auth_config(unvalidated_config, app):
validator = OIDCAuthValidator()
with pytest.raises(ConfigValidationException):
validator.validate(unvalidated_config, None, None)
def test_validate_oidc_auth(app):
config = {
'AUTHENTICATION_TYPE': 'OIDC',
'INTERNAL_OIDC_SERVICE_ID': 'someservice',
'SOMESERVICE_LOGIN_CONFIG': {
'CLIENT_ID': 'foo',
'CLIENT_SECRET': 'bar',
'OIDC_SERVER': 'http://someserver',
},
'HTTPCLIENT': None,
}
validator = OIDCAuthValidator()
validator.validate(config, None, None)

View file

@ -0,0 +1,21 @@
from app import app
from data.users.oidc import OIDCInternalAuth, UnknownServiceException
from util.config.validators import BaseValidator, ConfigValidationException
class OIDCAuthValidator(BaseValidator):
name = "oidc-auth"
@classmethod
def validate(cls, config, user, user_password):
if config.get('AUTHENTICATION_TYPE', 'Database') != 'OIDC':
return
login_service_id = config.get('INTERNAL_OIDC_SERVICE_ID')
if not login_service_id:
raise ConfigValidationException('Missing OIDC provider')
# By instantiating the auth engine, it will check if the provider exists and works.
try:
OIDCInternalAuth(config, login_service_id, False)
except UnknownServiceException as use:
raise ConfigValidationException(use.message)