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
|
@ -60,6 +60,15 @@ def set_validated_oauth_token(token):
|
|||
ctx.validated_oauth_token = token
|
||||
|
||||
|
||||
def get_validated_app_specific_token():
|
||||
return getattr(_request_ctx_stack.top, 'validated_app_specific_token', None)
|
||||
|
||||
|
||||
def set_validated_app_specific_token(token):
|
||||
ctx = _request_ctx_stack.top
|
||||
ctx.validated_app_specific_token = token
|
||||
|
||||
|
||||
def get_validated_token():
|
||||
return getattr(_request_ctx_stack.top, 'validated_token', None)
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ import logging
|
|||
|
||||
from enum import Enum
|
||||
|
||||
import features
|
||||
|
||||
from app import authentication
|
||||
from auth.oauth import validate_oauth_token
|
||||
from auth.validateresult import ValidateResult, AuthKind
|
||||
|
@ -12,6 +14,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
ACCESS_TOKEN_USERNAME = '$token'
|
||||
OAUTH_TOKEN_USERNAME = '$oauthtoken'
|
||||
APP_SPECIFIC_TOKEN_USERNAME = '$app'
|
||||
|
||||
|
||||
class CredentialKind(Enum):
|
||||
|
@ -19,22 +22,44 @@ class CredentialKind(Enum):
|
|||
robot = 'robot'
|
||||
token = ACCESS_TOKEN_USERNAME
|
||||
oauth_token = OAUTH_TOKEN_USERNAME
|
||||
app_specific_token = APP_SPECIFIC_TOKEN_USERNAME
|
||||
|
||||
|
||||
def validate_credentials(auth_username, auth_password_or_token):
|
||||
""" Validates a pair of auth username and password/token credentials. """
|
||||
# Check for access tokens.
|
||||
if auth_username == ACCESS_TOKEN_USERNAME:
|
||||
logger.debug('Found basic auth header for access token')
|
||||
logger.debug('Found credentials for access token')
|
||||
try:
|
||||
token = model.token.load_token_data(auth_password_or_token)
|
||||
logger.debug('Successfully validated basic auth for access token %s', token.id)
|
||||
logger.debug('Successfully validated credentials for access token %s', token.id)
|
||||
return ValidateResult(AuthKind.credentials, token=token), CredentialKind.token
|
||||
except model.DataModelException:
|
||||
logger.warning('Failed to validate basic auth for access token %s', auth_password_or_token)
|
||||
logger.warning('Failed to validate credentials for access token %s', auth_password_or_token)
|
||||
return (ValidateResult(AuthKind.credentials, error_message='Invalid access token'),
|
||||
CredentialKind.token)
|
||||
|
||||
# Check for App Specific tokens.
|
||||
if features.APP_SPECIFIC_TOKENS and auth_username == APP_SPECIFIC_TOKEN_USERNAME:
|
||||
logger.debug('Found credentials for app specific auth token')
|
||||
token = model.appspecifictoken.access_valid_token(auth_password_or_token)
|
||||
if token is None:
|
||||
logger.debug('Failed to validate credentials for app specific token: %s',
|
||||
auth_password_or_token)
|
||||
return (ValidateResult(AuthKind.credentials, error_message='Invalid token'),
|
||||
CredentialKind.app_specific_token)
|
||||
|
||||
if not token.user.enabled:
|
||||
logger.debug('Tried to use an app specific token for a disabled user: %s',
|
||||
token.uuid)
|
||||
return (ValidateResult(AuthKind.credentials,
|
||||
error_message='This user has been disabled. Please contact your administrator.'),
|
||||
CredentialKind.app_specific_token)
|
||||
|
||||
logger.debug('Successfully validated credentials for app specific token %s', token.id)
|
||||
return (ValidateResult(AuthKind.credentials, appspecifictoken=token),
|
||||
CredentialKind.app_specific_token)
|
||||
|
||||
# Check for OAuth tokens.
|
||||
if auth_username == OAUTH_TOKEN_USERNAME:
|
||||
return validate_oauth_token(auth_password_or_token), CredentialKind.oauth_token
|
||||
|
@ -42,21 +67,21 @@ def validate_credentials(auth_username, auth_password_or_token):
|
|||
# Check for robots and users.
|
||||
is_robot = parse_robot_username(auth_username)
|
||||
if is_robot:
|
||||
logger.debug('Found basic auth header for robot %s', auth_username)
|
||||
logger.debug('Found credentials header for robot %s', auth_username)
|
||||
try:
|
||||
robot = model.user.verify_robot(auth_username, auth_password_or_token)
|
||||
logger.debug('Successfully validated basic auth for robot %s', auth_username)
|
||||
logger.debug('Successfully validated credentials for robot %s', auth_username)
|
||||
return ValidateResult(AuthKind.credentials, robot=robot), CredentialKind.robot
|
||||
except model.InvalidRobotException as ire:
|
||||
logger.warning('Failed to validate basic auth for robot %s: %s', auth_username, ire.message)
|
||||
logger.warning('Failed to validate credentials for robot %s: %s', auth_username, ire.message)
|
||||
return ValidateResult(AuthKind.credentials, error_message=ire.message), CredentialKind.robot
|
||||
|
||||
# Otherwise, treat as a standard user.
|
||||
(authenticated, err) = authentication.verify_and_link_user(auth_username, auth_password_or_token,
|
||||
basic_auth=True)
|
||||
if authenticated:
|
||||
logger.debug('Successfully validated basic auth for user %s', authenticated.username)
|
||||
logger.debug('Successfully validated credentials for user %s', authenticated.username)
|
||||
return ValidateResult(AuthKind.credentials, user=authenticated), CredentialKind.user
|
||||
else:
|
||||
logger.warning('Failed to validate basic auth for user %s: %s', auth_username, err)
|
||||
logger.warning('Failed to validate credentials for user %s: %s', auth_username, err)
|
||||
return ValidateResult(AuthKind.credentials, error_message=err), CredentialKind.user
|
||||
|
|
|
@ -19,7 +19,7 @@ from data import model
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CONTEXT_KINDS = ['user', 'token', 'oauth']
|
||||
CONTEXT_KINDS = ['user', 'token', 'oauth', 'app_specific_token']
|
||||
|
||||
|
||||
ACCESS_SCHEMA = {
|
||||
|
@ -66,10 +66,11 @@ class InvalidJWTException(Exception):
|
|||
|
||||
|
||||
class GrantedEntity(object):
|
||||
def __init__(self, user=None, token=None, oauth=None):
|
||||
def __init__(self, user=None, token=None, oauth=None, app_specific_token=None):
|
||||
self.user = user
|
||||
self.token = token
|
||||
self.oauth = oauth
|
||||
self.app_specific_token = app_specific_token
|
||||
|
||||
|
||||
def get_granted_entity():
|
||||
|
@ -85,6 +86,13 @@ def get_granted_entity():
|
|||
if not kind in CONTEXT_KINDS:
|
||||
return None
|
||||
|
||||
if kind == 'app_specific_token':
|
||||
app_specific_token = model.appspecifictoken.get_token_by_uuid(context.get('ast', ''))
|
||||
if app_specific_token is None:
|
||||
return None
|
||||
|
||||
return GrantedEntity(app_specific_token=app_specific_token, user=app_specific_token.user)
|
||||
|
||||
if kind == 'user':
|
||||
user = model.user.get_user(context.get('user', ''))
|
||||
if not user:
|
||||
|
|
|
@ -3,7 +3,8 @@ import pytest
|
|||
from base64 import b64encode
|
||||
|
||||
from auth.basic import validate_basic_auth
|
||||
from auth.credentials import ACCESS_TOKEN_USERNAME, OAUTH_TOKEN_USERNAME
|
||||
from auth.credentials import (ACCESS_TOKEN_USERNAME, OAUTH_TOKEN_USERNAME,
|
||||
APP_SPECIFIC_TOKEN_USERNAME)
|
||||
from auth.validateresult import AuthKind, ValidateResult
|
||||
from data import model
|
||||
|
||||
|
@ -21,6 +22,8 @@ def _token(username, password):
|
|||
('basic ', ValidateResult(AuthKind.basic, missing=True)),
|
||||
('basic some token', ValidateResult(AuthKind.basic, missing=True)),
|
||||
('basic sometoken', ValidateResult(AuthKind.basic, missing=True)),
|
||||
(_token(APP_SPECIFIC_TOKEN_USERNAME, 'invalid'), ValidateResult(AuthKind.basic,
|
||||
error_message='Invalid token')),
|
||||
(_token(ACCESS_TOKEN_USERNAME, 'invalid'), ValidateResult(AuthKind.basic,
|
||||
error_message='Invalid access token')),
|
||||
(_token(OAUTH_TOKEN_USERNAME, 'invalid'),
|
||||
|
@ -64,3 +67,12 @@ def test_valid_oauth(app):
|
|||
token = _token(OAUTH_TOKEN_USERNAME, oauth_token.access_token)
|
||||
result = validate_basic_auth(token)
|
||||
assert result == ValidateResult(AuthKind.basic, oauthtoken=oauth_token)
|
||||
|
||||
|
||||
def test_valid_app_specific_token(app):
|
||||
user = model.user.get_user('devtable')
|
||||
app_specific_token = model.appspecifictoken.create_token(user, 'some token')
|
||||
token = _token(APP_SPECIFIC_TOKEN_USERNAME, app_specific_token.token_code)
|
||||
result = validate_basic_auth(token)
|
||||
print result.tuple()
|
||||
assert result == ValidateResult(AuthKind.basic, appspecifictoken=app_specific_token)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from auth.credentials import (ACCESS_TOKEN_USERNAME, OAUTH_TOKEN_USERNAME, validate_credentials,
|
||||
CredentialKind)
|
||||
CredentialKind, APP_SPECIFIC_TOKEN_USERNAME)
|
||||
from auth.validateresult import AuthKind, ValidateResult
|
||||
from data import model
|
||||
|
||||
|
@ -16,6 +16,18 @@ def test_valid_robot(app):
|
|||
assert kind == CredentialKind.robot
|
||||
assert result == ValidateResult(AuthKind.credentials, robot=robot)
|
||||
|
||||
def test_valid_robot_for_disabled_user(app):
|
||||
user = model.user.get_user('devtable')
|
||||
user.enabled = False
|
||||
user.save()
|
||||
|
||||
robot, password = model.user.create_robot('somerobot', user)
|
||||
result, kind = validate_credentials(robot.username, password)
|
||||
assert kind == CredentialKind.robot
|
||||
|
||||
err = 'This user has been disabled. Please contact your administrator.'
|
||||
assert result == ValidateResult(AuthKind.credentials, error_message=err)
|
||||
|
||||
def test_valid_token(app):
|
||||
access_token = model.token.create_delegate_token('devtable', 'simple', 'sometoken')
|
||||
result, kind = validate_credentials(ACCESS_TOKEN_USERNAME, access_token.code)
|
||||
|
@ -34,3 +46,29 @@ def test_invalid_user(app):
|
|||
assert kind == CredentialKind.user
|
||||
assert result == ValidateResult(AuthKind.credentials,
|
||||
error_message='Invalid Username or Password')
|
||||
|
||||
def test_valid_app_specific_token(app):
|
||||
user = model.user.get_user('devtable')
|
||||
app_specific_token = model.appspecifictoken.create_token(user, 'some token')
|
||||
|
||||
result, kind = validate_credentials(APP_SPECIFIC_TOKEN_USERNAME, app_specific_token.token_code)
|
||||
assert kind == CredentialKind.app_specific_token
|
||||
assert result == ValidateResult(AuthKind.credentials, appspecifictoken=app_specific_token)
|
||||
|
||||
def test_valid_app_specific_token_for_disabled_user(app):
|
||||
user = model.user.get_user('devtable')
|
||||
user.enabled = False
|
||||
user.save()
|
||||
|
||||
app_specific_token = model.appspecifictoken.create_token(user, 'some token')
|
||||
|
||||
result, kind = validate_credentials(APP_SPECIFIC_TOKEN_USERNAME, app_specific_token.token_code)
|
||||
assert kind == CredentialKind.app_specific_token
|
||||
|
||||
err = 'This user has been disabled. Please contact your administrator.'
|
||||
assert result == ValidateResult(AuthKind.credentials, error_message=err)
|
||||
|
||||
def test_invalid_app_specific_token(app):
|
||||
result, kind = validate_credentials(APP_SPECIFIC_TOKEN_USERNAME, 'somecode')
|
||||
assert kind == CredentialKind.app_specific_token
|
||||
assert result == ValidateResult(AuthKind.credentials, error_message='Invalid token')
|
||||
|
|
|
@ -3,7 +3,7 @@ from flask_principal import Identity, identity_changed
|
|||
|
||||
from app import app
|
||||
from auth.auth_context import (set_authenticated_user, set_validated_token, set_grant_context,
|
||||
set_validated_oauth_token)
|
||||
set_validated_oauth_token, set_validated_app_specific_token)
|
||||
from auth.scopes import scopes_from_scope_string
|
||||
from auth.permissions import QuayDeferredPermissionUser
|
||||
|
||||
|
@ -19,19 +19,20 @@ class AuthKind(Enum):
|
|||
class ValidateResult(object):
|
||||
""" A result of validating auth in one form or another. """
|
||||
def __init__(self, kind, missing=False, user=None, token=None, oauthtoken=None,
|
||||
robot=None, signed_data=None, error_message=None):
|
||||
robot=None, appspecifictoken=None, signed_data=None, error_message=None):
|
||||
self.kind = kind
|
||||
self.missing = missing
|
||||
self.user = user
|
||||
self.robot = robot
|
||||
self.token = token
|
||||
self.oauthtoken = oauthtoken
|
||||
self.appspecifictoken = appspecifictoken
|
||||
self.signed_data = signed_data
|
||||
self.error_message = error_message
|
||||
|
||||
def tuple(self):
|
||||
return (self.kind, self.missing, self.user, self.token, self.oauthtoken, self.robot,
|
||||
self.signed_data, self.error_message)
|
||||
self.appspecifictoken, self.signed_data, self.error_message)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.tuple() == other.tuple()
|
||||
|
@ -42,6 +43,9 @@ class ValidateResult(object):
|
|||
if self.oauthtoken:
|
||||
set_authenticated_user(self.authed_user)
|
||||
set_validated_oauth_token(self.oauthtoken)
|
||||
elif self.appspecifictoken:
|
||||
set_authenticated_user(self.authed_user)
|
||||
set_validated_app_specific_token(self.appspecifictoken)
|
||||
elif self.authed_user:
|
||||
set_authenticated_user(self.authed_user)
|
||||
elif self.token:
|
||||
|
@ -60,7 +64,7 @@ class ValidateResult(object):
|
|||
def with_kind(self, kind):
|
||||
""" Returns a copy of this result, but with the kind replaced. """
|
||||
return ValidateResult(kind, self.missing, self.user, self.token, self.oauthtoken, self.robot,
|
||||
self.signed_data, self.error_message)
|
||||
self.appspecifictoken, self.signed_data, self.error_message)
|
||||
|
||||
@property
|
||||
def authed_user(self):
|
||||
|
@ -71,6 +75,9 @@ class ValidateResult(object):
|
|||
if self.oauthtoken:
|
||||
return self.oauthtoken.authorized_user
|
||||
|
||||
if self.appspecifictoken:
|
||||
return self.appspecifictoken.user
|
||||
|
||||
return self.user if self.user else self.robot
|
||||
|
||||
@property
|
||||
|
@ -104,4 +111,5 @@ class ValidateResult(object):
|
|||
@property
|
||||
def auth_valid(self):
|
||||
""" Returns whether authentication successfully occurred. """
|
||||
return self.user or self.token or self.oauthtoken or self.robot or self.signed_data
|
||||
return (self.user or self.token or self.oauthtoken or self.appspecifictoken or self.robot or
|
||||
self.signed_data)
|
||||
|
|
Reference in a new issue