import logging import re import time import jwt from flask import request, jsonify, abort from cachetools import lru_cache from app import app from data import model from auth.auth import process_auth from auth.auth_context import get_authenticated_user, get_validated_token from auth.permissions import (ModifyRepositoryPermission, ReadRepositoryPermission, CreateRepositoryPermission) from endpoints.v2 import v2_bp from util.cache import no_cache from util.names import parse_namespace_repository, REPOSITORY_NAME_REGEX from endpoints.decorators import anon_protect logger = logging.getLogger(__name__) TOKEN_VALIDITY_LIFETIME_S = 60 * 60 # 1 hour SCOPE_REGEX = re.compile( r'^repository:([\.a-zA-Z0-9_\-]+/[\.a-zA-Z0-9_\-]+):(((push|pull|\*),)*(push|pull|\*))$' ) ANONYMOUS_SUB = '(anonymous)' @lru_cache(maxsize=1) def load_certificate_bytes(certificate_file_path): with open(certificate_file_path) as cert_file: return ''.join(cert_file.readlines()[1:-1]).rstrip('\n') @lru_cache(maxsize=1) def load_private_key(private_key_file_path): with open(private_key_file_path) as private_key_file: return private_key_file.read() @v2_bp.route('/auth') @process_auth @no_cache @anon_protect def generate_registry_jwt(): """ This endpoint will generate a JWT conforming to the Docker registry v2 auth spec: https://docs.docker.com/registry/spec/auth/token/ """ audience_param = request.args.get('service') logger.debug('Request audience: %s', audience_param) scope_param = request.args.get('scope') logger.debug('Scope request: %s', scope_param) user = get_authenticated_user() logger.debug('Authenticated user: %s', user) token = get_validated_token() logger.debug('Authenticated token: %s', token) access = [] if scope_param is not None: match = SCOPE_REGEX.match(scope_param) if match is None: logger.debug('Match: %s', match) logger.debug('len: %s', len(scope_param)) logger.warning('Unable to decode repository and actions: %s', scope_param) abort(400) logger.debug('Match: %s', match.groups()) namespace_and_repo = match.group(1) actions = match.group(2).split(',') namespace, reponame = parse_namespace_repository(namespace_and_repo) # Ensure that we are never creating an invalid repository. if not REPOSITORY_NAME_REGEX.match(reponame): abort(400) final_actions = [] if 'push' in actions: # If there is no valid user or token, then the repository cannot be # accessed. if user is None and token is None: abort(401) # Lookup the repository. If it exists, make sure the entity has modify # permission. Otherwise, make sure the entity has create permission. repo = model.repository.get_repository(namespace, reponame) if repo: if not ModifyRepositoryPermission(namespace, reponame).can(): abort(403) else: if not CreateRepositoryPermission(namespace).can() or user is None: abort(403) logger.debug('Creating repository: %s/%s', namespace, reponame) model.repository.create_repository(namespace, reponame, user) final_actions.append('push') if 'pull' in actions: # Grant pull if the user can read the repo or it is public. We also # grant it if the user already has push, as they can clearly change # the repository. if (ReadRepositoryPermission(namespace, reponame).can() or model.repository.repository_is_public(namespace, reponame) or 'push' in final_actions): final_actions.append('pull') else: abort(403) access.append({ 'type': 'repository', 'name': namespace_and_repo, 'actions': final_actions, }) elif user is None and token is None: # In this case, we are doing an auth flow, and it's not an anonymous pull return abort(401) token_data = { 'iss': app.config['JWT_AUTH_TOKEN_ISSUER'], 'aud': audience_param, 'nbf': int(time.time()), 'iat': int(time.time()), 'exp': int(time.time() + TOKEN_VALIDITY_LIFETIME_S), 'sub': user.username if user else ANONYMOUS_SUB, 'access': access, } certificate = load_certificate_bytes(app.config['JWT_AUTH_CERTIFICATE_PATH']) token_headers = { 'x5c': [certificate], } private_key = load_private_key(app.config['JWT_AUTH_PRIVATE_KEY_PATH']) return jsonify({'token':jwt.encode(token_data, private_key, 'RS256', headers=token_headers)})