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 from data.model import oauth from app import app, authentication, userevents, storage from auth.auth import process_auth, generate_signed_token from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token from util.names import parse_repository_name from util.useremails import send_confirmation_email from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission, ReadRepositoryPermission, CreateRepositoryPermission, AlwaysFailPermission, repository_read_grant, repository_write_grant) from util.http import abort from endpoints.trackhelper import track_and_log from endpoints.notificationhelper import spawn_notification from endpoints.decorators import anon_protect, anon_allowed import features logger = logging.getLogger(__name__) index = Blueprint('index', __name__) class GrantType(object): READ_REPOSITORY = 'read' WRITE_REPOSITORY = 'write' def generate_headers(scope=GrantType.READ_REPOSITORY, add_grant_for_status=None): 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 # 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', '') force_grant = (add_grant_for_status == response.status_code) if has_token_request or force_grant: grants = [] if scope == GrantType.READ_REPOSITORY: if force_grant or ReadRepositoryPermission(namespace, repository).can(): grants.append(repository_read_grant(namespace, repository)) elif scope == GrantType.WRITE_REPOSITORY: if force_grant or ModifyRepositoryPermission(namespace, repository).can(): grants.append(repository_write_grant(namespace, repository)) # Generate a signed token for the user (if any) and the grants (if any) if grants or get_authenticated_user(): 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 return response return wrapper return decorator_method @index.route('/users', methods=['POST']) @index.route('/users/', methods=['POST']) @anon_allowed def create_user(): user_data = request.get_json() if not user_data or not 'username' in user_data: abort(400, 'Missing username') username = user_data['username'] password = user_data.get('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 username == '$oauthtoken': validated = oauth.validate_access_token(password) if validated is not None: return success else: abort(400, 'Invalid oauth access token.', issue='invalid-oauth-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') (verified, error_message) = authentication.verify_user(username, password, basic_auth=True) if verified: # Mark that the user was logged in. event = userevents.get_event(username) event.publish_event_data('docker-cli', {'action': 'login'}) return success else: # Mark that the login failed. event = userevents.get_event(username) event.publish_event_data('docker-cli', {'action': 'loginfailure'}) abort(400, error_message, issue='login-failure') @index.route('/users', methods=['GET']) @index.route('/users/', methods=['GET']) @process_auth @anon_allowed def get_user(): if get_validated_oauth_token(): return jsonify({ 'username': '$oauthtoken', 'email': None, }) elif get_authenticated_user(): return jsonify({ 'username': get_authenticated_user().username, 'email': get_authenticated_user().email, }) elif get_validated_token(): return jsonify({ 'username': '$token', 'email': None, }) abort(404) @index.route('/users//', methods=['PUT']) @process_auth @anon_allowed def update_user(username): permission = UserAdminPermission(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(scope=GrantType.WRITE_REPOSITORY, add_grant_for_status=201) @anon_allowed def create_repository(namespace, repository): logger.debug('Parsing image descriptions for repository %s/%s', namespace, repository) image_descriptions = json.loads(request.data.decode('utf8')) logger.debug('Looking up repository %s/%s', namespace, repository) repo = model.get_repository(namespace, repository) logger.debug('Found repository %s/%s', namespace, repository) if not repo and get_authenticated_user() is None: logger.debug('Attempt to create repository %s/%s without user auth', namespace, repository) 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 %s/%s with insufficient perms', namespace, repository) abort(403, message='You do not have permission to create repositories in namespace "%(namespace)s"', issue='no-create-permission', namespace=namespace) # Attempt to create the new repository. logger.debug('Creating repository %s/%s with owner: %s', namespace, repository, get_authenticated_user().username) repo = model.create_repository(namespace, repository, get_authenticated_user()) if get_authenticated_user(): user_event_data = { 'action': 'push_start', 'repository': repository, 'namespace': namespace } event = userevents.get_event(get_authenticated_user().username) event.publish_event_data('docker-cli', user_event_data) return make_response('Created', 201) @index.route('/repositories//images', methods=['PUT']) @process_auth @parse_repository_name @generate_headers(scope=GrantType.WRITE_REPOSITORY) @anon_allowed def update_images(namespace, repository): permission = ModifyRepositoryPermission(namespace, repository) if permission.can(): logger.debug('Looking up repository') repo = model.get_repository(namespace, repository) if not repo: # Make sure the repo actually exists. abort(404, message='Unknown repository', issue='unknown-repo') logger.debug('GCing repository') model.garbage_collect_repository(namespace, repository) # Generate a job for each notification that has been added to this repo logger.debug('Adding notifications for repository') updated_tags = session.get('pushed_tags', {}) event_data = { 'updated_tags': updated_tags, } track_and_log('push_repo', repo) spawn_notification(repo, 'repo_push', event_data) return make_response('Updated', 204) abort(403) @index.route('/repositories//images', methods=['GET']) @process_auth @parse_repository_name @generate_headers(scope=GrantType.READ_REPOSITORY) @anon_protect def get_repository_images(namespace, repository): permission = ReadRepositoryPermission(namespace, repository) # TODO invalidate token? if permission.can() or model.repository_is_public(namespace, repository): # We can't rely on permissions to tell us if a repo exists anymore logger.debug('Looking up repository') repo = model.get_repository(namespace, repository) if not repo: abort(404, message='Unknown repository', issue='unknown-repo') logger.debug('Building repository image response') resp = make_response(json.dumps([]), 200) resp.mimetype = 'application/json' track_and_log('pull_repo', repo) return resp abort(403) @index.route('/repositories//images', methods=['DELETE']) @process_auth @parse_repository_name @generate_headers(scope=GrantType.WRITE_REPOSITORY) @anon_allowed def delete_repository_images(namespace, repository): abort(501, 'Not Implemented', issue='not-implemented') @index.route('/repositories//auth', methods=['PUT']) @parse_repository_name @anon_allowed def put_repository_auth(namespace, repository): abort(501, 'Not Implemented', issue='not-implemented') @index.route('/search', methods=['GET']) @process_auth @anon_protect def get_search(): def result_view(repo): return { "name": repo.namespace_user.username + '/' + repo.name, "description": repo.description } query = request.args.get('q') username = None user = get_authenticated_user() if user is not None: username = user.username if query: matching = model.get_matching_repositories(query, username) else: matching = [] results = [result_view(repo) for repo in matching if (repo.visibility.name == 'public' or ReadRepositoryPermission(repo.namespace_user.username, repo.name).can())] data = { "query": query, "num_results": len(results), "results" : results } resp = make_response(json.dumps(data), 200) resp.mimetype = 'application/json' return resp # Note: This is *not* part of the Docker index spec. This is here for our own health check, # since we have nginx handle the _ping below. @index.route('/_internal_ping') @anon_allowed def internal_ping(): return make_response('true', 200) @index.route('/_ping') @index.route('/_ping') @anon_allowed def ping(): # NOTE: any changes made here must also be reflected in the nginx config response = make_response('true', 200) response.headers['X-Docker-Registry-Version'] = '0.6.0' response.headers['X-Docker-Registry-Standalone'] = '0' return response