import json import urllib import json import logging import urlparse from flask import request, make_response, jsonify, abort from functools import wraps from data import model from app import app, mixpanel from auth.auth import (process_auth, get_authenticated_user, get_validated_token) from util.names import parse_namespace_repository, parse_repository_name from util.email import send_confirmation_email from auth.permissions import (ModifyRepositoryPermission, UserPermission, ReadRepositoryPermission, CreateRepositoryPermission) logger = logging.getLogger(__name__) def generate_headers(role='read'): def decorator_method(f): @wraps(f) def wrapper(namespace, repository, *args, **kwargs): response = f(namespace, repository, *args, **kwargs) # We run our index and registry on the same hosts for now registry_server = urlparse.urlparse(request.url).netloc response.headers['X-Docker-Endpoints'] = registry_server has_token_request = request.headers.get('X-Docker-Token', '') 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 decorator_method @app.route('/v1/users', methods=['POST']) @app.route('/v1/users/', methods=['POST']) def create_user(): user_data = request.get_json() 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) if verified: return make_response('Verified', 201) else: abort(401) else: # New user case new_user = model.create_user(username, password, user_data['email']) code = model.create_confirm_email_code(new_user) send_confirmation_email(new_user.username, new_user.email, code.code) return make_response('Created', 201) @app.route('/v1/users', methods=['GET']) @app.route('/v1/users/', methods=['GET']) @process_auth def get_user(): if get_authenticated_user(): return jsonify({ 'username': get_authenticated_user().username, 'email': get_authenticated_user().email, }) abort(404) @app.route('/v1/users//', methods=['PUT']) @process_auth def update_user(username): permission = UserPermission(username) if permission.can(): update_request = request.get_json() if 'password' in update_request: logger.debug('Updating user password.') model.change_password(get_authenticated_user(), update_request['password']) if 'email' in update_request: logger.debug('Updating user email') model.update_email(get_authenticated_user(), update_request['email']) return jsonify({ 'username': get_authenticated_user().username, 'email': get_authenticated_user().email, }) abort(403) @app.route('/v1/repositories/', methods=['PUT']) @process_auth @parse_repository_name @generate_headers(role='write') def create_repository(namespace, repository): image_descriptions = json.loads(request.data) repo = model.get_repository(namespace, repository) 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) else: permission = CreateRepoPermission('namespace') if not permission.can(): logger.info('Attempt to create a new repo with insufficient perms.') abort(403) logger.debug('Creaing repository with owner: %s' % get_authenticated_user().username) repo = model.create_repository(namespace, repository, get_authenticated_user()) new_repo_images = {desc['id']: desc for desc in image_descriptions} added_images = dict(new_repo_images) for existing in model.get_repository_images(namespace, repository): if existing.docker_image_id in new_repo_images: added_images.pop(existing.docker_image_id) else: logger.debug('Deleting existing image with id: %s' % existing.docker_image_id) existing.delete_instance(recursive=True) for image_description in added_images.values(): image = model.create_image(image_description['id'], repo) response = make_response('Created', 201) if get_authenticated_user(): mixpanel.track(get_authenticated_user().username, 'push_repo') else: mixpanel.track(get_validated_token().code, 'push_repo') return response @app.route('/v1/repositories//images', methods=['PUT']) @process_auth @parse_repository_name @generate_headers(role='write') def update_images(namespace, repository): permission = ModifyRepositoryPermission(namespace, repository) if permission.can(): repository = model.get_repository(namespace, repository) image_with_checksums = json.loads(request.data) for image in image_with_checksums: logger.debug('Setting checksum for image id: %s to %s' % (image['id'], image['checksum'])) model.set_image_checksum(image['id'], repository, image['checksum']) return make_response('Updated', 204) abort(403) @app.route('/v1/repositories//images', methods=['GET']) @process_auth @parse_repository_name @generate_headers(role='read') def get_repository_images(namespace, repository): permission = ReadRepositoryPermission(namespace, repository) # TODO invalidate token? if permission.can() or model.repository_is_public(namespace, repository): all_images = [] for image in model.get_repository_images(namespace, repository): new_image_view = { 'id': image.docker_image_id, 'checksum': image.checksum, } all_images.append(new_image_view) resp = make_response(json.dumps(all_images), 200) resp.mimetype = 'application/json' pull_username = 'anonymous' if get_authenticated_user(): pull_username = get_authenticated_user().username mixpanel.track(pull_username, 'pull_repo') return resp abort(403) @app.route('/v1/repositories//images', methods=['DELETE']) @process_auth @parse_repository_name @generate_headers(role='write') def delete_repository_images(namespace, repository): return make_response('Not Implemented', 501) @app.route('/v1/repositories//auth', methods=['PUT']) @parse_repository_name def put_repository_auth(namespace, repository): return make_response('Not Implemented', 501) @app.route('/v1/search', methods=['GET']) def get_search(): return make_response('Not Implemented', 501) @app.route('/_ping') @app.route('/v1/_ping') def ping(): response = make_response('true', 200) response.headers['X-Docker-Registry-Version'] = '0.6.0' return response