From 283f9b81ae3515eb79254c560cc6620be2b1dcbf Mon Sep 17 00:00:00 2001 From: yackob03 Date: Wed, 16 Oct 2013 14:24:10 -0400 Subject: [PATCH] First stab at token auth. The UI could use a little bit of polishing. --- auth/auth.py | 78 ++++++++++++++++------------ auth/permissions.py | 19 +++---- data/database.py | 7 ++- data/model.py | 89 +++++++++++++++++++++++++------- endpoints/api.py | 86 ++++++++++++++++++++++++++++++ endpoints/index.py | 57 ++++++++++++-------- static/js/controllers.js | 49 +++++++++++++++++- static/partials/repo-admin.html | 66 ++++++++++++++++++++++- test.db | Bin 69632 -> 69632 bytes 9 files changed, 360 insertions(+), 91 deletions(-) diff --git a/auth/auth.py b/auth/auth.py index 21c28d420..2d77d6094 100644 --- a/auth/auth.py +++ b/auth/auth.py @@ -32,19 +32,34 @@ def process_basic_auth(auth): credentials = b64decode(normalized[1]).split(':') if len(credentials) != 2: - logger.debug('Invalid basic auth credential formet.') + logger.debug('Invalid basic auth credential format.') - authenticated = model.verify_user(credentials[0], credentials[1]) + if credentials[0] == '$token': + # Use as token auth + try: + token = model.load_token_data(credentials[1]) + logger.debug('Successfully validated token: %s' % credentials[1]) + ctx = _request_ctx_stack.top + ctx.validated_token = token - if authenticated: - logger.debug('Successfully validated user: %s' % authenticated.username) - ctx = _request_ctx_stack.top - ctx.authenticated_user = authenticated + identity_changed.send(app, identity=Identity(token.code, 'token')) + return - new_identity = QuayDeferredPermissionUser(authenticated.username, - 'username') - identity_changed.send(app, identity=new_identity) - return + except model.DataModelException: + logger.debug('Invalid token: %s' % credentials[1]) + + else: + authenticated = model.verify_user(credentials[0], credentials[1]) + + if authenticated: + logger.debug('Successfully validated user: %s' % authenticated.username) + ctx = _request_ctx_stack.top + ctx.authenticated_user = authenticated + + new_identity = QuayDeferredPermissionUser(authenticated.username, + 'username') + identity_changed.send(app, identity=new_identity) + return # We weren't able to authenticate via basic auth. logger.debug('Basic auth present but could not be validated.') @@ -54,42 +69,37 @@ def process_basic_auth(auth): def process_token(auth): normalized = [part.strip() for part in auth.split(' ') if part] if normalized[0].lower() != 'token' or len(normalized) != 2: - logger.debug('Invalid token format.') + logger.debug('Not an auth token: %s' % auth) return token_details = normalized[1].split(',') - if len(token_details) != 2: - logger.debug('Invalid token format.') - return + if len(token_details) != 1: + logger.warning('Invalid token format: %s' % auth) + abort(401) token_vals = {val[0]: val[1] for val in (detail.split('=') for detail in token_details)} - if ('signature' not in token_vals or 'repository' not in token_vals): - logger.debug('Invalid token components.') - return + if 'signature' not in token_vals: + logger.warning('Token does not contain signature: %s' % auth) + abort(401) - unquoted = token_vals['repository'][1:-1] - namespace, repository = parse_namespace_repository(unquoted) - logger.debug('Validing signature: %s' % token_vals['signature']) - validated = model.verify_token(token_vals['signature'], namespace, - repository) + try: + token_data = model.load_token_data(token_vals['signature']) - if validated: - session['repository'] = repository - session['namespace'] = namespace + except model.InvalidTokenException: + logger.warning('Token could not be validated: %s' % + token_vals['signature']) + abort(401) - logger.debug('Successfully validated token: %s' % validated.code) - ctx = _request_ctx_stack.top - ctx.validated_token = validated + session['repository'] = token_data.repository.name + session['namespace'] = token_data.repository.namespace - identity_changed.send(app, identity=Identity(validated.code, 'token')) + logger.debug('Successfully validated token: %s' % token_data.code) + ctx = _request_ctx_stack.top + ctx.validated_token = token_data - return - - # WE weren't able to authenticate the token - logger.debug('Token present but could not be validated.') - abort(401) + identity_changed.send(app, identity=Identity(token_data.code, 'token')) def process_auth(f): diff --git a/auth/permissions.py b/auth/permissions.py index 7ce41d7c9..c0661bbf2 100644 --- a/auth/permissions.py +++ b/auth/permissions.py @@ -80,19 +80,14 @@ def on_identity_loaded(sender, identity): identity_changed.send(app, identity=switch_to_deferred) elif identity.auth_type == 'token': - logger.debug('Computing permissions for token: %s' % identity.id) + logger.debug('Loading permissions for token: %s' % identity.id) + token_data = model.load_token_data(identity.id) - token = model.get_token(identity.id) - - if token.user: - query = model.get_user_repo_permissions(token.user, token.repository) - for permission in query: - t_grant = _RepositoryNeed(token.repository.namespace, - token.repository.name, permission.role.name) - logger.debug('Token added permission: {0}'.format(t_grant)) - identity.provides.add(t_grant) - else: - logger.debug('Token was anonymous.') + repo_grant = _RepositoryNeed(token_data.repository.namespace, + token_data.repository.name, + token_data.role.name) + logger.debug('Delegate token added permission: {0}'.format(repo_grant)) + identity.provides.add(repo_grant) else: logger.error('Unknown identity auth type: %s' % identity.auth_type) diff --git a/data/database.py b/data/database.py index 8d2ad291f..04fd33b8a 100644 --- a/data/database.py +++ b/data/database.py @@ -99,10 +99,13 @@ def random_string_generator(length=16): class AccessToken(BaseModel): - code = CharField(default=random_string_generator(), unique=True, index=True) - user = ForeignKeyField(User, null=True) + friendly_name = CharField(null=True) + code = CharField(default=random_string_generator(length=64), unique=True, + index=True) repository = ForeignKeyField(Repository) created = DateTimeField(default=datetime.now) + role = ForeignKeyField(Role) + temporary = BooleanField(default=True) class EmailConfirmation(BaseModel): diff --git a/data/model.py b/data/model.py index 8148cf6b1..5a1a3ff57 100644 --- a/data/model.py +++ b/data/model.py @@ -26,6 +26,10 @@ class InvalidPasswordException(DataModelException): pass +class InvalidTokenException(DataModelException): + pass + + def create_user(username, password, email): if not validate_email(email): raise InvalidEmailAddressException('Invalid email address: %s' % email) @@ -159,25 +163,6 @@ def verify_user(username, password): return None -def create_access_token(user, repository): - new_token = AccessToken.create(user=user, repository=repository) - return new_token - - -def verify_token(code, namespace_name, repository_name): - joined = AccessToken.select(AccessToken, Repository).join(Repository) - tokens = list(joined.where(AccessToken.code == code, - Repository.namespace == namespace_name, - Repository.name == repository_name)) - if tokens: - return tokens[0] - return None - - -def get_token(code): - return AccessToken.get(AccessToken.code == code) - - def get_visible_repositories(username=None, include_public=True, limit=None, sort=False): if not username and not include_public: @@ -485,3 +470,69 @@ def get_private_repo_count(username): joined = Repository.select().join(Visibility) return joined.where(Repository.namespace == username, Visibility.name == 'private').count() + + +def create_access_token(repository, role): + role = Role.get(Role.name == role) + new_token = AccessToken.create(repository=repository, temporary=True, + role=role) + return new_token + + +def create_delegate_token(namespace_name, repository_name, friendly_name): + read_only = Role.get(name='read') + repo = Repository.get(Repository.name == repository_name, + Repository.namespace == namespace_name) + new_token = AccessToken.create(repository=repo, role=read_only, + friendly_name=friendly_name, temporary=False) + return new_token + + +def get_repository_delegate_tokens(namespace_name, repository_name): + selected = AccessToken.select(AccessToken, Role) + with_repo = selected.join(Repository) + with_role = with_repo.switch(AccessToken).join(Role) + return with_role.where(Repository.name == repository_name, + Repository.namespace == namespace_name, + AccessToken.temporary == False) + + +def get_repo_delegate_token(namespace_name, repository_name, code): + repo_query = get_repository_delegate_tokens(namespace_name, repository_name) + found = list(repo_query.where(AccessToken.code == code)) + + if found: + return found[0] + else: + raise InvalidTokenException('Unable to find token with code: %s' % code) + + +def set_repo_delegate_token_role(namespace_name, repository_name, code, role): + token = get_repo_delegate_token(namespace_name, repository_name, code) + + if role != 'read' and role != 'write': + raise DataModelException('Invalid role for delegate token: %s' % role) + + new_role = Role.get(Role.name == role) + token.role = new_role + token.save() + + return token + + +def delete_delegate_token(namespace_name, repository_name, code): + token = get_repo_delegate_token(namespace_name, repository_name, code) + token.delete_instance() + + +def load_token_data(code): + """ Load the permissions for any token by code. """ + selected = AccessToken.select(AccessToken, Repository, Role) + with_role = selected.join(Role) + with_repo = with_role.switch(AccessToken).join(Repository) + fetched = list(with_repo.where(AccessToken.code == code)) + + if fetched: + return fetched[0] + else: + raise InvalidTokenException('Invalid delegate token code: %s' % code) diff --git a/endpoints/api.py b/endpoints/api.py index 0a2ccf9a0..08344e38b 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -456,6 +456,92 @@ def delete_permissions(namespace, repository, username): abort(403) # Permission denied +def token_view(token_obj): + return { + 'friendlyName': token_obj.friendly_name, + 'code': token_obj.code, + 'role': token_obj.role.name, + } + + +@app.route('/api/repository//tokens/', methods=['GET']) +@api_login_required +@parse_repository_name +def list_repo_tokens(namespace, repository): + permission = AdministerRepositoryPermission(namespace, repository) + if permission.can(): + tokens = model.get_repository_delegate_tokens(namespace, repository) + + return jsonify({ + 'tokens': {token.code: token_view(token) for token in tokens} + }) + + abort(403) # Permission denied + + +@app.route('/api/repository//tokens/', methods=['GET']) +@api_login_required +@parse_repository_name +def get_tokens(namespace, repository, code): + permission = AdministerRepositoryPermission(namespace, repository) + if permission.can(): + perm = model.get_repo_delegate_token(namespace, repository, code) + return jsonify(token_view(perm)) + + abort(403) # Permission denied + + +@app.route('/api/repository//tokens/', methods=['POST']) +@api_login_required +@parse_repository_name +def create_token(namespace, repository): + permission = AdministerRepositoryPermission(namespace, repository) + if permission.can(): + token_params = request.get_json() + + token = model.create_delegate_token(namespace, repository, + token_params['friendlyName']) + + resp = jsonify(token_view(token)) + resp.status_code = 201 + return resp + + abort(403) # Permission denied + + +@app.route('/api/repository//tokens/', methods=['PUT']) +@api_login_required +@parse_repository_name +def change_token(namespace, repository, code): + permission = AdministerRepositoryPermission(namespace, repository) + if permission.can(): + new_permission = request.get_json() + + logger.debug('Setting permission to: %s for code %s' % + (new_permission['role'], code)) + + token = model.set_repo_delegate_token_role(namespace, repository, code, + new_permission['role']) + + resp = jsonify(token_view(token)) + return resp + + abort(403) # Permission denied + + +@app.route('/api/repository//tokens/', + methods=['DELETE']) +@api_login_required +@parse_repository_name +def delete_token(namespace, repository, code): + permission = AdministerRepositoryPermission(namespace, repository) + if permission.can(): + model.delete_delegate_token(namespace, repository, code) + return make_response('Deleted', 204) + + abort(403) # Permission denied + + def subscription_view(stripe_subscription, used_repos): return { 'currentPeriodStart': stripe_subscription.current_period_start, diff --git a/endpoints/index.py b/endpoints/index.py index ecf27d70e..375fa94b4 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -19,25 +19,26 @@ from auth.permissions import (ModifyRepositoryPermission, logger = logging.getLogger(__name__) -def generate_headers(f): - @wraps(f) - def wrapper(namespace, repository, *args, **kwargs): - response = f(namespace, repository, *args, **kwargs) +def generate_headers(role='read'): + def decorator_method(f): + @wraps(f) + def wrapper(namespace, repository, *args, **kwargs): + response = f(namespace, repository, *args, **kwargs) - response.headers['X-Docker-Endpoints'] = app.config['REGISTRY_SERVER'] + response.headers['X-Docker-Endpoints'] = app.config['REGISTRY_SERVER'] - has_token_request = request.headers.get('X-Docker-Token', '') + has_token_request = request.headers.get('X-Docker-Token', '') - if has_token_request: - repo = model.get_repository(namespace, repository) - token = model.create_access_token(get_authenticated_user(), repo) - token_str = 'signature=%s,repository="%s/%s"' % (token.code, namespace, - repository) - response.headers['WWW-Authenticate'] = token_str - response.headers['X-Docker-Token'] = token_str + if has_token_request: + repo = model.get_repository(namespace, repository) + token = model.create_access_token(repo, role) + token_str = 'signature=%s' % token.code + response.headers['WWW-Authenticate'] = token_str + response.headers['X-Docker-Token'] = token_str - return response - return wrapper + return response + return wrapper + return decorator_method @app.route('/v1/users', methods=['POST']) @@ -47,6 +48,13 @@ def create_user(): username = user_data['username'] password = user_data['password'] + if username == '$token': + try: + token = model.load_token_data(password) + return make_response('Verified', 201) + except model.InvalidTokenException: + abort(401) + existing_user = model.get_user(username) if existing_user: verified = model.verify_user(username, password) @@ -100,13 +108,17 @@ def update_user(username): @app.route('/v1/repositories/', methods=['PUT']) @process_auth @parse_repository_name -@generate_headers +@generate_headers(role='write') def create_repository(namespace, repository): image_descriptions = json.loads(request.data) repo = model.get_repository(namespace, repository) - if repo: + if not repo and get_authenticated_user() is None: + logger.debug('Attempt to create new repository with token auth.') + abort(400) + + elif repo: permission = ModifyRepositoryPermission(namespace, repository) if not permission.can(): abort(403) @@ -135,7 +147,10 @@ def create_repository(namespace, repository): response = make_response('Created', 201) - mixpanel.track(get_authenticated_user().username, 'push_repo') + if get_authenticated_user(): + mixpanel.track(get_authenticated_user().username, 'push_repo') + else: + mixpanel.track(get_validated_token().code, 'push_repo') return response @@ -143,7 +158,7 @@ def create_repository(namespace, repository): @app.route('/v1/repositories//images', methods=['PUT']) @process_auth @parse_repository_name -@generate_headers +@generate_headers(role='write') def update_images(namespace, repository): permission = ModifyRepositoryPermission(namespace, repository) @@ -164,7 +179,7 @@ def update_images(namespace, repository): @app.route('/v1/repositories//images', methods=['GET']) @process_auth @parse_repository_name -@generate_headers +@generate_headers(role='read') def get_repository_images(namespace, repository): permission = ReadRepositoryPermission(namespace, repository) @@ -196,7 +211,7 @@ def get_repository_images(namespace, repository): @app.route('/v1/repositories//images', methods=['DELETE']) @process_auth @parse_repository_name -@generate_headers +@generate_headers(role='write') def delete_repository_images(namespace, repository): pass diff --git a/static/js/controllers.js b/static/js/controllers.js index 534ce6020..d6765a309 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -469,6 +469,40 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) { }); }; + $scope.createToken = function() { + var friendlyName = { + 'friendlyName': $scope.newToken.friendlyName + }; + + var permissionPost = Restangular.one('repository/' + namespace + '/' + name + '/tokens/'); + permissionPost.customPOST(friendlyName).then(function(newToken) { + $scope.tokens[newToken.code] = newToken; + }); + }; + + $scope.deleteToken = function(tokenCode) { + var deleteAction = Restangular.one('repository/' + namespace + '/' + name + '/tokens/' + tokenCode); + deleteAction.customDELETE().then(function() { + delete $scope.tokens[tokenCode]; + }); + }; + + $scope.changeTokenAccess = function(tokenCode, newAccess) { + var role = { + 'role': newAccess + }; + + var deleteAction = Restangular.one('repository/' + namespace + '/' + name + '/tokens/' + tokenCode); + deleteAction.customPUT(role).then(function(updated) { + $scope.tokens[updated.code] = updated; + }); + }; + + $scope.showToken = function(tokenCode) { + $scope.shownToken = $scope.tokens[tokenCode]; + $('#tokenmodal').modal({}); + }; + $scope.askChangeAccess = function(newAccess) { $('#make' + newAccess + 'Modal').modal({}); }; @@ -512,7 +546,7 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) { var repositoryFetch = Restangular.one('repository/' + namespace + '/' + name); repositoryFetch.get().then(function(repo) { $scope.repo = repo; - $scope.loading = !($scope.permissions && $scope.repo); + $scope.loading = !($scope.permissions && $scope.repo && $scope.tokens); }, function() { $scope.permissions = null; $rootScope.title = 'Unknown Repository'; @@ -524,12 +558,23 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) { permissionsFetch.get().then(function(resp) { $rootScope.title = 'Settings - ' + namespace + '/' + name; $scope.permissions = resp.permissions; - $scope.loading = !($scope.permissions && $scope.repo); + $scope.loading = !($scope.permissions && $scope.repo && $scope.tokens); }, function() { $scope.permissions = null; $rootScope.title = 'Unknown Repository'; $scope.loading = false; }); + + // Fetch the tokens. + var tokensFetch = Restangular.one('repository/' + namespace + '/' + name + '/tokens/'); + tokensFetch.get().then(function(resp) { + $scope.tokens = resp.tokens; + $scope.loading = !($scope.permissions && $scope.repo && $scope.tokens); + }, function() { + $scope.tokens = null; + $scope.loading = false; + }); + } function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService, KeyService, $routeParams) { diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index 10ebaaee9..967a36892 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -56,7 +56,53 @@ -
+ + +
+
Access Token Permissions
+
+ + + + + + + + + + + + + + + + + + + + + + +
TokenPermissions
+ + {{ token.friendlyName }} + +
+ + +
+
+ + + + +
+ + + +
+
+
@@ -113,6 +159,24 @@
+ + + diff --git a/test.db b/test.db index 179b19de4a664e47ed3df83801c4a01a53cc08fb..70f675eb8b5456ef1685ecf21e299269f3c919c9 100644 GIT binary patch literal 69632 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 YRwAti_N;?bSmT_bz(Wged0B@44{!zg0ssI2 literal 69632 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