import json import logging import urlparse from flask import request, make_response, jsonify, session, Blueprint from functools import wraps from collections import OrderedDict from data import model, userevent from data.queue import webhook_queue from app import mixpanel, app from auth.auth import (process_auth, get_authenticated_user, get_validated_token) from util.names import parse_repository_name from util.email import send_confirmation_email from auth.permissions import (ModifyRepositoryPermission, UserPermission, ReadRepositoryPermission, CreateRepositoryPermission) from util.http import abort logger = logging.getLogger(__name__) index = Blueprint('index', __name__) def generate_headers(role='read'): def decorator_method(f): @wraps(f) def wrapper(namespace, repository, *args, **kwargs): response = f(namespace, repository, *args, **kwargs) # Setting session namespace and repository session['namespace'] = namespace session['repository'] = repository if get_authenticated_user(): session['username'] = get_authenticated_user().username else: session.pop('username', None) # 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) if repo: 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 else: logger.info('Token request in non-existing repo: %s/%s' % (namespace, repository)) return response return wrapper return decorator_method @index.route('/users', methods=['POST']) @index.route('/users/', methods=['POST']) def create_user(): user_data = request.get_json() username = user_data['username'] password = user_data['password'] # UGH! we have to use this response when the login actually worked, in order # to get the CLI to try again with a get, and then tell us login succeeded. success = make_response('"Username or email already exists"', 400) if username == '$token': try: model.load_token_data(password) return success except model.InvalidTokenException: abort(400, 'Invalid access token.', issue='invalid-access-token') elif '+' in username: try: model.verify_robot(username, password) return success except model.InvalidRobotException: abort(400, 'Invalid robot account or password.', issue='robot-login-failure') existing_user = model.get_user(username) if existing_user: verified = model.verify_user(username, password) if verified: # Mark that the user was logged in. event = app.config['USER_EVENTS'].get_event(username) event.publish_event_data('docker-cli', {'action': 'login'}) return success else: # Mark that the login failed. event = app.config['USER_EVENTS'].get_event(username) event.publish_event_data('docker-cli', {'action': 'loginfailure'}) abort(400, 'Invalid password.', issue='login-failure') 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) @index.route('/users', methods=['GET']) @index.route('/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) @index.route('/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) @index.route('/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 without user auth.') abort(401, message='Cannot create a repository as a guest. Please login via "docker login" first.', issue='no-login') elif repo: permission = ModifyRepositoryPermission(namespace, repository) if not permission.can(): abort(403, message='You do not have permission to modify repository %(namespace)s/%(repository)s', issue='no-repo-write-permission', namespace=namespace, repository=repository) else: permission = CreateRepositoryPermission(namespace) if not permission.can(): logger.info('Attempt to create a new repo with insufficient perms.') abort(403, message='You do not have permission to create repositories in namespace "%(namespace)s"', issue='no-create-permission', namespace=namespace) logger.debug('Creaing repository with owner: %s' % get_authenticated_user().username) repo = model.create_repository(namespace, repository, get_authenticated_user()) added_images = OrderedDict([(desc['id'], desc) for desc in image_descriptions]) new_repo_images = dict(added_images) existing_image_translations = {} for existing in model.get_repository_images(namespace, repository): if existing.docker_image_id in new_repo_images: existing_image_translations[existing.docker_image_id] = existing.id added_images.pop(existing.docker_image_id) username = get_authenticated_user() and get_authenticated_user().username translations = {} for image_description in added_images.values(): model.create_or_link_image(image_description['id'], repo, username, translations, existing_image_translations) response = make_response('Created', 201) extra_params = { 'repository': '%s/%s' % (namespace, repository), } metadata = { 'repo': repository, 'namespace': namespace } if get_authenticated_user(): username = get_authenticated_user().username mixpanel.track(username, 'push_repo', extra_params) metadata['username'] = username # Mark that the user has started pushing the repo. user_data = { 'action': 'push_repo', 'repository': repository, 'namespace': namespace } event = app.config['USER_EVENTS'].get_event(username) event.publish_event_data('docker-cli', user_data) else: mixpanel.track(get_validated_token().code, 'push_repo', extra_params) metadata['token'] = get_validated_token().friendly_name metadata['token_code'] = get_validated_token().code model.log_action('push_repo', namespace, performer=get_authenticated_user(), ip=request.remote_addr, metadata=metadata, repository=repo) return response @index.route('/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(): repo = model.get_repository(namespace, repository) if not repo: # Make sure the repo actually exists. abort(404, message='Unknown repository', issue='unknown-repo') image_with_checksums = json.loads(request.data) updated_tags = {} for image in image_with_checksums: logger.debug('Setting checksum for image id: %s to %s' % (image['id'], image['checksum'])) updated_tags[image['Tag']] = image['id'] model.set_image_checksum(image['id'], repo, image['checksum']) if get_authenticated_user(): username = get_authenticated_user().username # Mark that the user has pushed the repo. user_data = { 'action': 'pushed_repo', 'repository': repository, 'namespace': namespace } event = app.config['USER_EVENTS'].get_event(username) event.publish_event_data('docker-cli', user_data) num_removed = model.garbage_collect_repository(namespace, repository) # Generate a job for each webhook that has been added to this repo webhooks = model.list_webhooks(namespace, repository) for webhook in webhooks: webhook_data = json.loads(webhook.parameters) repo_string = '%s/%s' % (namespace, repository) logger.debug('Creating webhook for repository \'%s\' for url \'%s\'' % (repo_string, webhook_data['url'])) webhook_data['payload'] = { 'repository': repo_string, 'namespace': namespace, 'name': repository, 'docker_url': 'quay.io/%s' % repo_string, 'homepage': 'https://quay.io/repository/%s' % repo_string, 'visibility': repo.visibility.name, 'updated_tags': updated_tags, 'pushed_image_count': len(image_with_checksums), 'pruned_image_count': num_removed, } webhook_queue.put(json.dumps(webhook_data)) return make_response('Updated', 204) abort(403) @index.route('/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? is_public = model.repository_is_public(namespace, repository) if permission.can() or is_public: # We can't rely on permissions to tell us if a repo exists anymore repo = model.get_repository(namespace, repository) if not repo: abort(404, message='Unknown repository', issue='unknown-repo') 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' metadata = { 'repo': repository, 'namespace': namespace, } if get_authenticated_user(): metadata['username'] = get_authenticated_user().username elif get_validated_token(): metadata['token'] = get_validated_token().friendly_name metadata['token_code'] = get_validated_token().code else: metadata['public'] = True pull_username = 'anonymous' if get_authenticated_user(): pull_username = get_authenticated_user().username extra_params = { 'repository': '%s/%s' % (namespace, repository), } mixpanel.track(pull_username, 'pull_repo', extra_params) model.log_action('pull_repo', namespace, performer=get_authenticated_user(), ip=request.remote_addr, metadata=metadata, repository=repo) return resp abort(403) @index.route('/repositories//images', methods=['DELETE']) @process_auth @parse_repository_name @generate_headers(role='write') def delete_repository_images(namespace, repository): abort(501, 'Not Implemented', issue='not-implemented') @index.route('/repositories//auth', methods=['PUT']) @parse_repository_name def put_repository_auth(namespace, repository): abort(501, 'Not Implemented', issue='not-implemented') @index.route('/search', methods=['GET']) def get_search(): abort(501, 'Not Implemented', issue='not-implemented') @index.route('/_ping') @index.route('/_ping') def ping(): response = make_response('true', 200) response.headers['X-Docker-Registry-Version'] = '0.6.0' return response