Add ability to configure OIDC internal auth engine via superuser panel

This commit is contained in:
Joseph Schorr 2017-06-09 17:12:05 -04:00
parent e724125459
commit bc82edb2d1
7 changed files with 103 additions and 14 deletions

View file

@ -9,6 +9,10 @@ from util.security.jwtutil import InvalidTokenError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class UnknownServiceException(Exception):
pass
class OIDCInternalAuth(FederatedUsers): class OIDCInternalAuth(FederatedUsers):
""" Handles authentication by delegating authentication to a signed OIDC JWT produced by the """ Handles authentication by delegating authentication to a signed OIDC JWT produced by the
configured OIDC service. configured OIDC service.
@ -18,7 +22,7 @@ class OIDCInternalAuth(FederatedUsers):
login_manager = OAuthLoginManager(config) login_manager = OAuthLoginManager(config)
self.login_service = login_manager.get_service(login_service_id) self.login_service = login_manager.get_service(login_service_id)
if self.login_service is None: if self.login_service is None:
raise Exception('Unknown OIDC login service %s' % login_service_id) raise UnknownServiceException('Unknown OIDC login service %s' % login_service_id)
@property @property
def supports_encrypted_credentials(self): def supports_encrypted_credentials(self):

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._public_key_cache = TTLCache(1, PUBLIC_KEY_CACHE_TTL, missing=self._load_public_key)
self._id = key_name[0:key_name.find('_')].lower() 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) self._mailing = config.get('FEATURE_MAILING', False)
def service_id(self): def service_id(self):

View file

@ -622,13 +622,14 @@
<div class="co-panel-body"> <div class="co-panel-body">
<div class="description"> <div class="description">
<p> <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>
<p> <p>
Additional <strong>external</strong> authentication providers (such as GitHub) can be used in addition for <strong>login into the UI</strong>. Additional <strong>external</strong> authentication providers (such as GitHub) can be used in addition for <strong>login into the UI</strong>.
</p> </p>
</div> </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"> <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>! 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>. <a ng-click="config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH = true">Enable this requirement now</a>.
@ -638,6 +639,7 @@
Note: The "Require Encrypted Client Passwords" feature is currently enabled which will Note: The "Require Encrypted Client Passwords" feature is currently enabled which will
prevent passwords from being saved as plaintext by the Docker client. prevent passwords from being saved as plaintext by the Docker client.
</div> </div>
</div>
<table class="config-table" style="margin-bottom: 20px;"> <table class="config-table" style="margin-bottom: 20px;">
<tr> <tr>
@ -648,6 +650,7 @@
<option value="LDAP">LDAP</option> <option value="LDAP">LDAP</option>
<option value="Keystone">Keystone (OpenStack Identity)</option> <option value="Keystone">Keystone (OpenStack Identity)</option>
<option value="JWT">JWT Custom Authentication</option> <option value="JWT">JWT Custom Authentication</option>
<option value="OIDC">OIDC Token Authentication</option>
</select> </select>
</td> </td>
</tr> </tr>
@ -687,6 +690,21 @@
</tr> </tr>
</table> </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 --> <!-- Keystone Authentication -->
<table class="config-table" ng-if="config.AUTHENTICATION_TYPE == 'Keystone'"> <table class="config-table" ng-if="config.AUTHENTICATION_TYPE == 'Keystone'">
<tr> <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> <span style="display: inline-block; margin-left: 10px">(<a href="javascript:void(0)" ng-click="removeOIDCProvider(provider)">Delete</a>)</span>
</div> </div>
<div class="co-panel-body"> <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. 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> </div>
@ -1134,7 +1152,7 @@
</div> </div>
</td> </td>
</tr> </tr>
<tr ng-if="config.AUTHENTICATION_TYPE != 'Database'"> <tr ng-if="config.AUTHENTICATION_TYPE != 'Database' && config.AUTHENTICATION_TYPE != 'OIDC'">
<td>Binding Field:</td> <td>Binding Field:</td>
<td> <td>
<select class="form-control" ng-model="config[provider].LOGIN_BINDING_FIELD"> <select class="form-control" ng-model="config[provider].LOGIN_BINDING_FIELD">
@ -1262,7 +1280,7 @@
</div> </div>
<div class="co-panel-body"> <div class="co-panel-body">
<div class="description"> <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>
<div class="config-bool-field" binding="config.FEATURE_BUILD_SUPPORT"> <div class="config-bool-field" binding="config.FEATURE_BUILD_SUPPORT">

View file

@ -41,6 +41,10 @@ angular.module("core-config-setup", ['angularFileUpload'])
return config.AUTHENTICATION_TYPE == 'Keystone'; return config.AUTHENTICATION_TYPE == 'Keystone';
}, 'password': true}, }, 'password': true},
{'id': 'oidc-auth', 'title': 'OIDC Authentication', 'condition': function(config) {
return config.AUTHENTICATION_TYPE == 'OIDC';
}},
{'id': 'signer', 'title': 'ACI Signing', 'condition': function(config) { {'id': 'signer', 'title': 'ACI Signing', 'condition': function(config) {
return config.FEATURE_ACI_CONVERSION; return config.FEATURE_ACI_CONVERSION;
}}, }},
@ -201,7 +205,7 @@ angular.module("core-config-setup", ['angularFileUpload'])
return null; return null;
} }
return key.substr(0, index); return key.substr(0, index).toLowerCase();
}; };
$scope.getOIDCProviders = function(config) { $scope.getOIDCProviders = function(config) {
@ -685,6 +689,12 @@ angular.module("core-config-setup", ['angularFileUpload'])
$scope.configform.$setValidity('storageConfig', valid); $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() { $scope.$watch('config.FEATURE_STORAGE_REPLICATION', function() {
refreshStorageConfig(); refreshStorageConfig();
}); });

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_timemachine import TimeMachineValidator
from util.config.validators.validate_access import AccessSettingsValidator from util.config.validators.validate_access import AccessSettingsValidator
from util.config.validators.validate_actionlog_archiving import ActionLogArchivingValidator from util.config.validators.validate_actionlog_archiving import ActionLogArchivingValidator
from util.config.validators.validate_oidcauth import OIDCAuthValidator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -59,6 +60,7 @@ VALIDATORS = {
TimeMachineValidator.name: TimeMachineValidator.validate, TimeMachineValidator.name: TimeMachineValidator.validate,
AccessSettingsValidator.name: AccessSettingsValidator.validate, AccessSettingsValidator.name: AccessSettingsValidator.validate,
ActionLogArchivingValidator.name: ActionLogArchivingValidator.validate, ActionLogArchivingValidator.name: ActionLogArchivingValidator.validate,
OIDCAuthValidator.name: OIDCAuthValidator.validate,
} }
def validate_service_for_config(service, config, password=None): 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,23 @@
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)