Add an AppSpecificAuthToken data model for app-specific auth tokens. These will be used for the Docker CLI in place of username+password
This commit is contained in:
parent
53b762a875
commit
524d77f527
50 changed files with 943 additions and 289 deletions
|
@ -9,7 +9,7 @@ from app import analytics, userevents, ip_resolver
|
|||
from data import model
|
||||
from auth.registry_jwt_auth import get_granted_entity
|
||||
from auth.auth_context import (get_authenticated_user, get_validated_token,
|
||||
get_validated_oauth_token)
|
||||
get_validated_oauth_token, get_validated_app_specific_token)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -27,20 +27,28 @@ def track_and_log(event_name, repo_obj, analytics_name=None, analytics_sample=1,
|
|||
authenticated_oauth_token = get_validated_oauth_token()
|
||||
authenticated_user = get_authenticated_user()
|
||||
authenticated_token = get_validated_token() if not authenticated_user else None
|
||||
app_specific_token = get_validated_app_specific_token()
|
||||
|
||||
if not authenticated_user and not authenticated_token and not authenticated_oauth_token:
|
||||
if (not authenticated_user and not authenticated_token and not authenticated_oauth_token and
|
||||
not app_specific_token):
|
||||
entity = get_granted_entity()
|
||||
if entity:
|
||||
authenticated_user = entity.user
|
||||
authenticated_token = entity.token
|
||||
authenticated_oauth_token = entity.oauth
|
||||
app_specific_token = entity.app_specific_token
|
||||
|
||||
logger.debug('Logging the %s to Mixpanel and the log system', event_name)
|
||||
if authenticated_oauth_token:
|
||||
metadata['oauth_token_id'] = authenticated_oauth_token.id
|
||||
metadata['oauth_token_application_id'] = authenticated_oauth_token.application.client_id
|
||||
metadata['oauth_token_application'] = authenticated_oauth_token.application.name
|
||||
metadata['username'] = authenticated_user.username
|
||||
analytics_id = 'oauth:{0}'.format(authenticated_oauth_token.id)
|
||||
elif app_specific_token:
|
||||
metadata['app_specific_token'] = app_specific_token.uuid
|
||||
metadata['username'] = authenticated_user.username
|
||||
analytics_id = 'appspecifictoken:{0}'.format(app_specific_token.uuid)
|
||||
elif authenticated_user:
|
||||
metadata['username'] = authenticated_user.username
|
||||
analytics_id = authenticated_user.username
|
||||
|
|
|
@ -20,6 +20,7 @@ def add_enterprise_config_defaults(config_obj, current_secret_key, hostname):
|
|||
config_obj['FEATURE_CHANGE_TAG_EXPIRATION'] = config_obj.get('FEATURE_CHANGE_TAG_EXPIRATION',
|
||||
True)
|
||||
config_obj['FEATURE_DIRECT_LOGIN'] = config_obj.get('FEATURE_DIRECT_LOGIN', True)
|
||||
config_obj['FEATURE_APP_SPECIFIC_TOKENS'] = config_obj.get('FEATURE_APP_SPECIFIC_TOKENS', True)
|
||||
config_obj['FEATURE_PARTIAL_USER_AUTOCOMPLETE'] = config_obj.get('FEATURE_PARTIAL_USER_AUTOCOMPLETE', True)
|
||||
|
||||
# Default features that are off.
|
||||
|
|
|
@ -23,7 +23,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
|
||||
from util.config.validators.validate_apptokenauth import AppTokenAuthValidator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -62,7 +62,7 @@ VALIDATORS = {
|
|||
TimeMachineValidator.name: TimeMachineValidator.validate,
|
||||
AccessSettingsValidator.name: AccessSettingsValidator.validate,
|
||||
ActionLogArchivingValidator.name: ActionLogArchivingValidator.validate,
|
||||
OIDCAuthValidator.name: OIDCAuthValidator.validate,
|
||||
AppTokenAuthValidator.name: AppTokenAuthValidator.validate,
|
||||
}
|
||||
|
||||
def validate_service_for_config(service, config, password=None):
|
||||
|
|
29
util/config/validators/test/test_validate_apptokenauth.py
Normal file
29
util/config/validators/test/test_validate_apptokenauth.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
import pytest
|
||||
|
||||
from util.config.validators import ConfigValidationException
|
||||
from util.config.validators.validate_apptokenauth import AppTokenAuthValidator
|
||||
|
||||
from test.fixtures import *
|
||||
|
||||
@pytest.mark.parametrize('unvalidated_config', [
|
||||
({'AUTHENTICATION_TYPE': 'AppToken'}),
|
||||
({'AUTHENTICATION_TYPE': 'AppToken', 'FEATURE_APP_SPECIFIC_TOKENS': False}),
|
||||
({'AUTHENTICATION_TYPE': 'AppToken', 'FEATURE_APP_SPECIFIC_TOKENS': True,
|
||||
'FEATURE_DIRECT_LOGIN': True}),
|
||||
])
|
||||
def test_validate_invalid_auth_config(unvalidated_config, app):
|
||||
validator = AppTokenAuthValidator()
|
||||
|
||||
with pytest.raises(ConfigValidationException):
|
||||
validator.validate(unvalidated_config, None, None)
|
||||
|
||||
|
||||
def test_validate_auth(app):
|
||||
config = {
|
||||
'AUTHENTICATION_TYPE': 'AppToken',
|
||||
'FEATURE_APP_SPECIFIC_TOKENS': True,
|
||||
'FEATURE_DIRECT_LOGIN': False,
|
||||
}
|
||||
|
||||
validator = AppTokenAuthValidator()
|
||||
validator.validate(config, None, None)
|
|
@ -1,34 +0,0 @@
|
|||
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'}),
|
||||
({'AUTHENTICATION_TYPE': 'OIDC', 'INTERNAL_OIDC_SERVICE_ID': 'someservice',
|
||||
'SOMESERVICE_LOGIN_CONFIG': {}, 'FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH': True}),
|
||||
])
|
||||
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)
|
19
util/config/validators/validate_apptokenauth.py
Normal file
19
util/config/validators/validate_apptokenauth.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
from util.config.validators import BaseValidator, ConfigValidationException
|
||||
|
||||
class AppTokenAuthValidator(BaseValidator):
|
||||
name = "apptoken-auth"
|
||||
|
||||
@classmethod
|
||||
def validate(cls, config, user, user_password):
|
||||
if config.get('AUTHENTICATION_TYPE', 'Database') != 'AppToken':
|
||||
return
|
||||
|
||||
# Ensure that app tokens are enabled, as they are required.
|
||||
if not config.get('FEATURE_APP_SPECIFIC_TOKENS', False):
|
||||
msg = 'Application token support must be enabled to use External Application Token auth'
|
||||
raise ConfigValidationException(msg)
|
||||
|
||||
# Ensure that direct login is disabled.
|
||||
if config.get('FEATURE_DIRECT_LOGIN', True):
|
||||
msg = 'Direct login must be disabled to use External Application Token auth'
|
||||
raise ConfigValidationException(msg)
|
|
@ -1,25 +0,0 @@
|
|||
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
|
||||
|
||||
# Ensure that encrypted passwords are not required, as they do not work with OIDC auth.
|
||||
if config.get('FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH', False):
|
||||
raise ConfigValidationException('Encrypted passwords must be disabled to use OIDC auth')
|
||||
|
||||
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)
|
|
@ -103,7 +103,8 @@ def _generate_jwt_object(audience, subject, context, access, lifetime_s, issuer,
|
|||
return jwt.encode(token_data, private_key, ALGORITHM, headers=token_headers)
|
||||
|
||||
|
||||
def build_context_and_subject(user=None, token=None, oauthtoken=None, tuf_root=None):
|
||||
def build_context_and_subject(user=None, token=None, oauthtoken=None, appspecifictoken=None,
|
||||
tuf_root=None):
|
||||
""" Builds the custom context field for the JWT signed token and returns it,
|
||||
along with the subject for the JWT signed token. """
|
||||
|
||||
|
@ -123,6 +124,14 @@ def build_context_and_subject(user=None, token=None, oauthtoken=None, tuf_root=N
|
|||
})
|
||||
return (context, user.username)
|
||||
|
||||
if appspecifictoken:
|
||||
context.update({
|
||||
'kind': 'app_specific_token',
|
||||
'user': user.username,
|
||||
'ast': appspecifictoken.uuid,
|
||||
})
|
||||
return (context, user.username)
|
||||
|
||||
if user:
|
||||
context.update({
|
||||
'kind': 'user',
|
||||
|
@ -141,5 +150,3 @@ def build_context_and_subject(user=None, token=None, oauthtoken=None, tuf_root=N
|
|||
'kind': 'anonymous',
|
||||
})
|
||||
return (context, ANONYMOUS_SUB)
|
||||
|
||||
|
||||
|
|
Reference in a new issue