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) if 'pull' in actions and 'push' in actions: if user is None and token is None: abort(401) 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) elif 'pull' in actions: if (not ReadRepositoryPermission(namespace, reponame).can() and not model.repository.repository_is_public(namespace, reponame)): abort(403) access.append({ 'type': 'repository', 'name': namespace_and_repo, 'actions': actions, }) 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)})