diff --git a/auth/auth.py b/auth/auth.py index 61c6b9a2c..b8d9065e6 100644 --- a/auth/auth.py +++ b/auth/auth.py @@ -15,7 +15,7 @@ from data import model from data.model import oauth from app import app, authentication from permissions import QuayDeferredPermissionUser -from auth_context import (set_authenticated_user, set_validated_token, +from auth_context import (set_authenticated_user, set_validated_token, set_grant_user_context, set_authenticated_user_deferred, set_validated_oauth_token) from util.http import abort @@ -131,10 +131,11 @@ def _process_basic_auth(auth): logger.debug('Basic auth present but could not be validated.') -def generate_signed_token(grants): +def generate_signed_token(grants, user_context): ser = SecureCookieSessionInterface().get_signing_serializer(app) data_to_sign = { 'grants': grants, + 'user_context': user_context, } encrypted = ser.dumps(data_to_sign) @@ -164,6 +165,7 @@ def _process_signed_grant(auth): logger.debug('Successfully validated signed grant with data: %s', token_data) loaded_identity = Identity(None, 'signed_grant') + set_grant_user_context(token_data['user_context']) loaded_identity.provides.update(token_data['grants']) identity_changed.send(app, identity=loaded_identity) diff --git a/auth/auth_context.py b/auth/auth_context.py index cfc6c7b5d..d4ae381be 100644 --- a/auth/auth_context.py +++ b/auth/auth_context.py @@ -30,6 +30,15 @@ def set_authenticated_user(user_or_robot): ctx.authenticated_user = user_or_robot +def get_grant_user_context(): + return getattr(_request_ctx_stack.top, 'grant_user_context', None) + + +def set_grant_user_context(username_or_robotname): + ctx = _request_ctx_stack.top + ctx.grant_user_context = username_or_robotname + + def set_authenticated_user_deferred(user_or_robot_db_uuid): logger.debug('Deferring loading of authenticated user object with uuid: %s', user_or_robot_db_uuid) ctx = _request_ctx_stack.top diff --git a/conf/gunicorn_verbs.py b/conf/gunicorn_verbs.py index f329a8cbe..cbb5e6d6a 100644 --- a/conf/gunicorn_verbs.py +++ b/conf/gunicorn_verbs.py @@ -3,3 +3,4 @@ workers = 4 logconfig = 'conf/logging.conf' pythonpath = '.' preload_app = True +timeout = 2000 # Because sync workers diff --git a/data/model/legacy.py b/data/model/legacy.py index 311d566ad..94f799992 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -1668,6 +1668,7 @@ def _garbage_collect_storage(storage_id_whitelist): logger.debug('Garbage collecting derived storage from candidates: %s', storage_id_whitelist) with config.app_config['DB_TRANSACTION_FACTORY'](db): # Find out which derived storages will be removed, and add them to the whitelist + # The comma after ImageStorage.id is VERY important, it makes it a tuple, which is a sequence orphaned_from_candidates = list(orphaned_storage_query(ImageStorage.select(ImageStorage.id), storage_id_whitelist, (ImageStorage.id,))) @@ -1707,22 +1708,32 @@ def _garbage_collect_storage(storage_id_whitelist): paths_to_remove = placements_query_to_paths_set(placements_to_remove.clone()) # Remove the placements for orphaned storages - placements_subquery = list(placements_to_remove.clone().select(ImageStoragePlacement.id)) - if len(placements_subquery) > 0: - (ImageStoragePlacement - .delete() - .where(ImageStoragePlacement.id << list(placements_subquery)) - .execute()) + placements_subquery = (placements_to_remove + .clone() + .select(ImageStoragePlacement.id) + .alias('ps')) + inner = (ImageStoragePlacement + .select(placements_subquery.c.id) + .from_(placements_subquery)) + placements_removed = (ImageStoragePlacement + .delete() + .where(ImageStoragePlacement.id << inner) + .execute()) + logger.debug('Removed %s image storage placements', placements_removed) - # Remove the all orphaned storages - orphaned_storages = list(orphaned_storage_query(ImageStorage.select(ImageStorage.id), - storage_id_whitelist, - (ImageStorage.id,))) - if len(orphaned_storages) > 0: - (ImageStorage - .delete() - .where(ImageStorage.id << orphaned_storages) - .execute()) + # Remove all orphaned storages + # The comma after ImageStorage.id is VERY important, it makes it a tuple, which is a sequence + orphaned_storages = orphaned_storage_query(ImageStorage.select(ImageStorage.id), + storage_id_whitelist, + (ImageStorage.id,)).alias('osq') + orphaned_storage_inner = (ImageStorage + .select(orphaned_storages.c.id) + .from_(orphaned_storages)) + storages_removed = (ImageStorage + .delete() + .where(ImageStorage.id << orphaned_storage_inner) + .execute()) + logger.debug('Removed %s image storage records', storages_removed) # We are going to make the conscious decision to not delete image storage blobs inside # transactions. diff --git a/endpoints/index.py b/endpoints/index.py index a20c492d6..ca4b73362 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -60,7 +60,8 @@ def generate_headers(scope=GrantType.READ_REPOSITORY): if permission.can(): # Generate a signed grant which expires here - signature = generate_signed_token(grants) + user_context = get_authenticated_user() and get_authenticated_user().username + signature = generate_signed_token(grants, user_context) response.headers['WWW-Authenticate'] = signature response.headers['X-Docker-Token'] = signature else: diff --git a/endpoints/registry.py b/endpoints/registry.py index 07a33c4d9..73610910e 100644 --- a/endpoints/registry.py +++ b/endpoints/registry.py @@ -9,7 +9,7 @@ from time import time from app import storage as store, image_diff_queue, app from auth.auth import process_auth, extract_namespace_repo_from_session -from auth.auth_context import get_authenticated_user +from auth.auth_context import get_authenticated_user, get_grant_user_context from util import checksums, changes from util.http import abort, exact_abort from auth.permissions import (ReadRepositoryPermission, @@ -463,8 +463,9 @@ def put_image_json(namespace, repository, image_id): repo_image = model.get_repo_image_extended(namespace, repository, image_id) if not repo_image: - logger.debug('Image not found, creating image') - username = get_authenticated_user() and get_authenticated_user().username + username = (get_authenticated_user() and get_authenticated_user().username or + get_grant_user_context()) + logger.debug('Image not found, creating image with initiating user context: %s', username) repo_image = model.find_create_or_link_image(image_id, repo, username, {}, store.preferred_locations[0])