diff --git a/auth/auth.py b/auth/auth.py index 4b83ed7f4..8b214c17c 100644 --- a/auth/auth.py +++ b/auth/auth.py @@ -1,7 +1,7 @@ import logging from functools import wraps -from flask import request, make_response, _request_ctx_stack, abort +from flask import request, make_response, _request_ctx_stack, abort, session from flask.ext.principal import identity_changed, Identity from base64 import b64decode @@ -54,29 +54,32 @@ def process_token(): logger.debug('Validating auth token: %s' % auth) normalized = [part.strip() for part in auth.split(' ') if part] - if normalized[0].lower() != 'token' or len(normalized) != 3: + if normalized[0].lower() != 'token' or len(normalized) != 2: logger.debug('Invalid token format.') return False - token_details = normalized[2].split(',') + token_details = normalized[1].split(',') - if len(token_details) != 3: + if len(token_details) != 2: logger.debug('Invalid token format.') return False token_vals = {val[0]: val[1] for val in (detail.split('=') for detail in token_details)} - if ('signature' not in token_vals or 'access' not in token_vals or - 'repository' not in token_vals): + if ('signature' not in token_vals or 'repository' not in token_vals): logger.debug('Invalid token components.') return False 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']) + validated = model.verify_token(token_vals['signature'], namespace, + repository) if validated: + session['repository'] = repository + session['namespace'] = namespace + logger.debug('Successfully validated token: %s' % validated.code) ctx = _request_ctx_stack.top ctx.validated_token = validated @@ -97,3 +100,14 @@ def process_auth(f): process_basic_auth() return f(*args, **kwargs) return wrapper + + +def extract_namespace_repo_from_session(f): + @wraps(f) + def wrapper(*args, **kwargs): + if 'namespace' not in session or 'repository' not in session: + logger.debug('Unable to load namespace or repository from session.') + abort(400) + + return f(session['namespace'], session['repository'], *args, **kwargs) + return wrapper \ No newline at end of file diff --git a/data/model.py b/data/model.py index 3b0dff86e..14d9f254f 100644 --- a/data/model.py +++ b/data/model.py @@ -34,11 +34,14 @@ def verify_user(username, password): return None -def verify_token(code): - try: - return AccessToken.get(code=code) - except AccessToken.DoesNotExist: - return None +def verify_token(code, namespace_name, repository_name): + joined = AccessToken.select(AccessToken, Repository).join(Repository) + tokens = list(joined.where(AccessToken.code == code and + Repository.namespace == namespace_name and + Repository.name == repository_name)) + if tokens: + return tokens[0] + return None def change_password(user, new_password): diff --git a/endpoints/index.py b/endpoints/index.py index a39ce5ddb..345cbfe79 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -21,27 +21,25 @@ logger = logging.getLogger(__name__) REGISTRY_SERVER = 'localhost:5003' -def generate_headers(access): - def add_headers(f): - @wraps(f) - def wrapper(namespace, repository, *args, **kwargs): - response = f(namespace, repository, *args, **kwargs) +def generate_headers(f): + @wraps(f) + def wrapper(namespace, repository, *args, **kwargs): + response = f(namespace, repository, *args, **kwargs) - response.headers['X-Docker-Endpoints'] = REGISTRY_SERVER + response.headers['X-Docker-Endpoints'] = REGISTRY_SERVER - has_token_request = request.headers.get('X-Docker-Token', '') + has_token_request = request.headers.get('X-Docker-Token', '') - if has_token_request and get_authenticated_user(): - repo = model.get_repository(namespace, repository) - token = model.create_access_token(get_authenticated_user(), repo) - token_str = ('Token signature=%s,repository="%s/%s",access=%s' % - (token.code, namespace, repository, access)) - response.headers['WWW-Authenticate'] = token_str - response.headers['X-Docker-Token'] = token_str + if has_token_request and get_authenticated_user(): + 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 - return response - return wrapper - return add_headers + return response + return wrapper @app.route('/v1/users', methods=['POST']) @@ -94,7 +92,7 @@ def update_user(username): @app.route('/v1/repositories/', methods=['PUT']) @process_auth @parse_repository_name -@generate_headers(access='write') +@generate_headers def create_repository(namespace, repository): image_descriptions = json.loads(request.data) @@ -138,7 +136,7 @@ def create_repository(namespace, repository): @app.route('/v1/repositories//images', methods=['PUT']) @process_auth @parse_repository_name -@generate_headers(access='write') +@generate_headers def update_images(namespace, repository): permission = ModifyRepositoryPermission(namespace, repository) @@ -156,7 +154,7 @@ def update_images(namespace, repository): @app.route('/v1/repositories//images', methods=['GET']) @process_auth @parse_repository_name -@generate_headers(access='read') +@generate_headers def get_repository_images(namespace, repository): permission = ReadRepositoryPermission(namespace, repository) @@ -183,7 +181,7 @@ def get_repository_images(namespace, repository): @app.route('/v1/repositories//images', methods=['DELETE']) @process_auth @parse_repository_name -@generate_headers(access='delete') +@generate_headers def delete_repository_images(namespace, repository): pass diff --git a/endpoints/registry.py b/endpoints/registry.py index 3d80f53b5..7bad8c843 100644 --- a/endpoints/registry.py +++ b/endpoints/registry.py @@ -10,7 +10,7 @@ from time import time import storage from app import app -from auth.auth import process_auth +from auth.auth import process_auth, extract_namespace_repo_from_session from util import checksums @@ -38,10 +38,11 @@ class SocketReader(object): def require_completion(f): """This make sure that the image push correctly finished.""" @wraps(f) - def wrapper(*args, **kwargs): - if store.exists(store.image_mark_path(kwargs['image_id'])): + def wrapper(namespace, repository, *args, **kwargs): + if store.exists(store.image_mark_path(namespace, repository, + kwargs['image_id'])): abort(400) #'Image is being uploaded, retry later') - return f(*args, **kwargs) + return f(namespace, repository, *args, **kwargs) return wrapper @@ -71,25 +72,28 @@ def set_cache_headers(f): @app.route('/v1/images//layer', methods=['GET']) @process_auth +@extract_namespace_repo_from_session @require_completion @set_cache_headers -def get_image_layer(image_id, headers): +def get_image_layer(namespace, repository, image_id, headers): try: return Response(store.stream_read(store.image_layer_path( - image_id)), headers=headers) + namespace, repository, image_id)), headers=headers) except IOError: abort(404) #'Image not found', 404) @app.route('/v1/images//layer', methods=['PUT']) @process_auth -def put_image_layer(image_id): +@extract_namespace_repo_from_session +def put_image_layer(namespace, repository, image_id): try: - json_data = store.get_content(store.image_json_path(image_id)) + json_data = store.get_content(store.image_json_path(namespace, repository, + image_id)) except IOError: abort(404) #'Image not found', 404) - layer_path = store.image_layer_path(image_id) - mark_path = store.image_mark_path(image_id) + layer_path = store.image_layer_path(namespace, repository, image_id) + mark_path = store.image_mark_path(namespace, repository, image_id) if store.exists(layer_path) and not store.exists(mark_path): abort(409) #'Image already exists', 409) input_stream = request.stream @@ -114,7 +118,9 @@ def put_image_layer(image_id): logger.debug('put_image_layer: Error when computing tarsum ' '{0}'.format(e)) try: - checksum = store.get_content(store.image_checksum_path(image_id)) + checksum = store.get_content(store.image_checksum_path(namespace, + repository, + image_id)) except IOError: # We don't have a checksum stored yet, that's fine skipping the check. # Not removing the mark though, image is not downloadable yet. @@ -131,18 +137,19 @@ def put_image_layer(image_id): @app.route('/v1/images//checksum', methods=['PUT']) @process_auth -def put_image_checksum(image_id): +@extract_namespace_repo_from_session +def put_image_checksum(namespace, repository, image_id): checksum = request.headers.get('X-Docker-Checksum') if not checksum: abort(400) #'Missing Image\'s checksum') if not session.get('checksum'): abort(400) #'Checksum not found in Cookie') - if not store.exists(store.image_json_path(image_id)): + if not store.exists(store.image_json_path(namespace, repository, image_id)): abort(404) #'Image not found', 404) - mark_path = store.image_mark_path(image_id) + mark_path = store.image_mark_path(namespace, repository, image_id) if not store.exists(mark_path): abort(409) #'Cannot set this image checksum', 409) - err = store_checksum(image_id, checksum) + err = store_checksum(namespace, repository, image_id, checksum) if err: abort(err) if checksum not in session.get('checksum', []): @@ -155,15 +162,18 @@ def put_image_checksum(image_id): @app.route('/v1/images//json', methods=['GET']) @process_auth +@extract_namespace_repo_from_session @require_completion @set_cache_headers -def get_image_json(image_id, headers): +def get_image_json(namespace, repository, image_id, headers): try: - data = store.get_content(store.image_json_path(image_id)) + data = store.get_content(store.image_json_path(namespace, repository, + image_id)) except IOError: abort(404) #'Image not found', 404) try: - size = store.get_size(store.image_layer_path(image_id)) + size = store.get_size(store.image_layer_path(namespace, repository, + image_id)) headers['X-Docker-Size'] = str(size) except OSError: pass @@ -177,11 +187,13 @@ def get_image_json(image_id, headers): @app.route('/v1/images//ancestry', methods=['GET']) @process_auth +@extract_namespace_repo_from_session @require_completion @set_cache_headers -def get_image_ancestry(image_id, headers): +def get_image_ancestry(namespace, repository, image_id, headers): try: - data = store.get_content(store.image_ancestry_path(image_id)) + data = store.get_content(store.image_ancestry_path(namespace, repository, + image_id)) except IOError: abort(404) #'Image not found', 404) response = make_response(json.dumps(json.loads(data)), 200) @@ -189,43 +201,34 @@ def get_image_ancestry(image_id, headers): return response -def generate_ancestry(image_id, parent_id=None): +def generate_ancestry(namespace, repository, image_id, parent_id=None): if not parent_id: - store.put_content(store.image_ancestry_path(image_id), - json.dumps([image_id])) + store.put_content(store.image_ancestry_path(namespace, repository, + image_id), + json.dumps([image_id])) return - data = store.get_content(store.image_ancestry_path(parent_id)) + data = store.get_content(store.image_ancestry_path(namespace, repository, + parent_id)) data = json.loads(data) data.insert(0, image_id) - store.put_content(store.image_ancestry_path(image_id), json.dumps(data)) + store.put_content(store.image_ancestry_path(namespace, repository, + image_id), + json.dumps(data)) -def check_images_list(image_id): - full_repos_name = session.get('repository') - if not full_repos_name: - # We only enforce this check when there is a repos name in the session - # otherwise it means that the auth is disabled. - return True - try: - path = store.images_list_path(*full_repos_name.split('/')) - images_list = json.loads(store.get_content(path)) - except IOError: - return False - return (image_id in images_list) - - -def store_checksum(image_id, checksum): +def store_checksum(namespace, repository, image_id, checksum): checksum_parts = checksum.split(':') if len(checksum_parts) != 2: return 'Invalid checksum format' # We store the checksum - checksum_path = store.image_checksum_path(image_id) + checksum_path = store.image_checksum_path(namespace, repository, image_id) store.put_content(checksum_path, checksum) @app.route('/v1/images//json', methods=['PUT']) @process_auth -def put_image_json(image_id): +@extract_namespace_repo_from_session +def put_image_json(namespace, repository, image_id): try: data = json.loads(request.data) except json.JSONDecodeError: @@ -238,26 +241,26 @@ def put_image_json(image_id): checksum = request.headers.get('X-Docker-Checksum') if checksum: # Storing the checksum is optional at this stage - err = store_checksum(image_id, checksum) + err = store_checksum(namespace, repository, image_id, checksum) if err: abort(err) else: # We cleanup any old checksum in case it's a retry after a fail - store.remove(store.image_checksum_path(image_id)) + store.remove(store.image_checksum_path(namespace, repository, image_id)) if image_id != data['id']: abort(400) #'JSON data contains invalid id') - if check_images_list(image_id) is False: - abort(400) #'This image does not belong to the repository') parent_id = data.get('parent') - if parent_id and not store.exists(store.image_json_path(data['parent'])): + if parent_id and not store.exists(store.image_json_path(namespace, + repository, + data['parent'])): abort(400) #'Image depends on a non existing parent') - json_path = store.image_json_path(image_id) - mark_path = store.image_mark_path(image_id) + json_path = store.image_json_path(namespace, repository, image_id) + mark_path = store.image_mark_path(namespace, repository, image_id) if store.exists(json_path) and not store.exists(mark_path): abort(409) #'Image already exists', 409) # If we reach that point, it means that this is a new image or a retry # on a failed push store.put_content(mark_path, 'true') store.put_content(json_path, request.data) - generate_ancestry(image_id, parent_id) + generate_ancestry(namespace, repository, image_id, parent_id) return make_response('true', 200) diff --git a/storage/__init__.py b/storage/__init__.py index 7cb508176..2a67815f9 100644 --- a/storage/__init__.py +++ b/storage/__init__.py @@ -27,20 +27,25 @@ class Storage(object): namespace, repository) - def image_json_path(self, image_id): - return '{0}/{1}/json'.format(self.images, image_id) + def image_json_path(self, namespace, repository, image_id): + return '{0}/{1}/{2}/{3}/json'.format(self.images, namespace, + repository, image_id) - def image_mark_path(self, image_id): - return '{0}/{1}/_inprogress'.format(self.images, image_id) + def image_mark_path(self, namespace, repository, image_id): + return '{0}/{1}/{2}/{3}/_inprogress'.format(self.images, namespace, + repository, image_id) - def image_checksum_path(self, image_id): - return '{0}/{1}/_checksum'.format(self.images, image_id) + def image_checksum_path(self, namespace, repository, image_id): + return '{0}/{1}/{2}/{3}/_checksum'.format(self.images, namespace, + repository, image_id) - def image_layer_path(self, image_id): - return '{0}/{1}/layer'.format(self.images, image_id) + def image_layer_path(self, namespace, repository, image_id): + return '{0}/{1}/{2}/{3}/layer'.format(self.images, namespace, + repository, image_id) - def image_ancestry_path(self, image_id): - return '{0}/{1}/ancestry'.format(self.images, image_id) + def image_ancestry_path(self, namespace, repository, image_id): + return '{0}/{1}/{2}/{3}/ancestry'.format(self.images, namespace, + repository, image_id) def tag_path(self, namespace, repository, tagname=None): if not tagname: