diff --git a/auth/jwt_auth.py b/auth/registry_jwt_auth.py similarity index 90% rename from auth/jwt_auth.py rename to auth/registry_jwt_auth.py index a253c9b72..ca9f71cd1 100644 --- a/auth/jwt_auth.py +++ b/auth/registry_jwt_auth.py @@ -3,13 +3,13 @@ import re from jsonschema import validate, ValidationError from functools import wraps -from flask import request +from flask import request, url_for from flask.ext.principal import identity_changed, Identity from cryptography.x509 import load_pem_x509_certificate from cryptography.hazmat.backends import default_backend from cachetools import lru_cache -from app import app +from app import app, get_app_url from .auth_context import set_grant_context, get_grant_context from .permissions import repository_read_grant, repository_write_grant from util.names import parse_namespace_repository @@ -152,6 +152,18 @@ def build_context_and_subject(user, token, oauthtoken): return (context, ANONYMOUS_SUB) +def get_auth_headers(): + """ Returns a dictionary of headers for auth responses. """ + headers = {} + realm_auth_path = url_for('v2.generate_registry_jwt') + authenticate = 'Bearer realm="{0}{1}",service="{2}"'.format(get_app_url(), + realm_auth_path, + app.config['SERVER_HOSTNAME']) + headers['WWW-Authenticate'] = authenticate + headers['Docker-Distribution-API-Version'] = 'registry/2.0' + return headers + + def identity_from_bearer_token(bearer_token, max_signed_s, public_key): """ Process a bearer token and return the loaded identity, or raise InvalidJWTException if an identity could not be loaded. Expects tokens and grants in the format of the Docker registry @@ -219,7 +231,7 @@ def load_public_key(certificate_file_path): return cert_obj.public_key() -def process_jwt_auth(func): +def process_registry_jwt_auth(func): @wraps(func) def wrapper(*args, **kwargs): logger.debug('Called with params: %s, %s', args, kwargs) @@ -237,8 +249,7 @@ def process_jwt_auth(func): set_grant_context(context) logger.debug('Identity changed to %s', extracted_identity.id) except InvalidJWTException as ije: - abort(401, message=ije.message) - + abort(401, message=ije.message, headers=get_auth_headers()) else: logger.debug('No auth header.') diff --git a/endpoints/trackhelper.py b/endpoints/trackhelper.py index e0961628e..2b35e4f1d 100644 --- a/endpoints/trackhelper.py +++ b/endpoints/trackhelper.py @@ -4,7 +4,7 @@ import random from app import analytics, app, userevents from data import model from flask import request -from auth.jwt_auth import get_granted_entity +from auth.registry_jwt_auth import get_granted_entity from auth.auth_context import (get_authenticated_user, get_validated_token, get_validated_oauth_token) diff --git a/endpoints/v1/registry.py b/endpoints/v1/registry.py index f2f64a9f8..f91dcfcd9 100644 --- a/endpoints/v1/registry.py +++ b/endpoints/v1/registry.py @@ -10,7 +10,7 @@ from time import time from app import storage as store, image_replication_queue, app from auth.auth import process_auth, extract_namespace_repo_from_session from auth.auth_context import get_authenticated_user -from auth.jwt_auth import get_granted_username +from auth.registry_jwt_auth import get_granted_username from digest import checksums from util.registry import changes from util.http import abort, exact_abort diff --git a/endpoints/v2/__init__.py b/endpoints/v2/__init__.py index f10e2fc19..06b400f20 100644 --- a/endpoints/v2/__init__.py +++ b/endpoints/v2/__init__.py @@ -9,16 +9,13 @@ import features from app import metric_queue from endpoints.decorators import anon_protect, anon_allowed from endpoints.v2.errors import V2RegistryException -from auth.jwt_auth import process_jwt_auth from auth.auth_context import get_grant_context from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission, AdministerRepositoryPermission) from data import model from util.http import abort from util.saas.metricqueue import time_blueprint -from util import get_app_url -from app import app - +from auth.registry_jwt_auth import process_registry_jwt_auth, get_auth_headers logger = logging.getLogger(__name__) v2_bp = Blueprint('v2', __name__) @@ -75,21 +72,15 @@ def route_show_if(value): @v2_bp.route('/') @route_show_if(features.ADVERTISE_V2) -@process_jwt_auth +@process_registry_jwt_auth @anon_allowed def v2_support_enabled(): response = make_response('true', 200) if get_grant_context() is None: response = make_response('true', 401) - realm_auth_path = url_for('v2.generate_registry_jwt') - authenticate = 'Bearer realm="{0}{1}",service="{2}"'.format(get_app_url(app.config), - realm_auth_path, - app.config['SERVER_HOSTNAME']) - response.headers['WWW-Authenticate'] = authenticate - - response.headers['Docker-Distribution-API-Version'] = 'registry/2.0' + response.headers.extend(get_auth_headers()) return response diff --git a/endpoints/v2/blob.py b/endpoints/v2/blob.py index 0d8dc4a4e..0f8feadde 100644 --- a/endpoints/v2/blob.py +++ b/endpoints/v2/blob.py @@ -4,12 +4,12 @@ import re from flask import make_response, url_for, request, redirect, Response, abort as flask_abort from app import storage, app +from auth.registry_jwt_auth import process_registry_jwt_auth from data import model, database from digest import digest_tools from endpoints.v2 import v2_bp, require_repo_read, require_repo_write, get_input_stream from endpoints.v2.errors import (BlobUnknown, BlobUploadInvalid, BlobUploadUnknown, Unsupported, NameUnknown) -from auth.jwt_auth import process_jwt_auth from endpoints.decorators import anon_protect from util.cache import cache_control from util.registry.filelike import wrap_with_handler, StreamSlice @@ -53,7 +53,7 @@ def _base_blob_fetch(namespace, repo_name, digest): @v2_bp.route(BLOB_DIGEST_ROUTE, methods=['HEAD']) -@process_jwt_auth +@process_registry_jwt_auth @require_repo_read @anon_protect @cache_control(max_age=31436000) @@ -68,7 +68,7 @@ def check_blob_exists(namespace, repo_name, digest): @v2_bp.route(BLOB_DIGEST_ROUTE, methods=['GET']) -@process_jwt_auth +@process_registry_jwt_auth @require_repo_read @anon_protect @cache_control(max_age=31536000) @@ -101,7 +101,7 @@ def _render_range(num_uploaded_bytes, with_bytes_prefix=True): @v2_bp.route('///blobs/uploads/', methods=['POST']) -@process_jwt_auth +@process_registry_jwt_auth @require_repo_write @anon_protect def start_blob_upload(namespace, repo_name): @@ -134,7 +134,7 @@ def start_blob_upload(namespace, repo_name): @v2_bp.route('///blobs/uploads/', methods=['GET']) -@process_jwt_auth +@process_registry_jwt_auth @require_repo_write @anon_protect def fetch_existing_upload(namespace, repo_name, upload_uuid): @@ -290,7 +290,7 @@ def _finish_upload(namespace, repo_name, upload_obj, expected_digest): @v2_bp.route('///blobs/uploads/', methods=['PATCH']) -@process_jwt_auth +@process_registry_jwt_auth @require_repo_write @anon_protect def upload_chunk(namespace, repo_name, upload_uuid): @@ -308,7 +308,7 @@ def upload_chunk(namespace, repo_name, upload_uuid): @v2_bp.route('///blobs/uploads/', methods=['PUT']) -@process_jwt_auth +@process_registry_jwt_auth @require_repo_write @anon_protect def monolithic_upload_or_last_chunk(namespace, repo_name, upload_uuid): @@ -326,7 +326,7 @@ def monolithic_upload_or_last_chunk(namespace, repo_name, upload_uuid): @v2_bp.route('///blobs/uploads/', methods=['DELETE']) -@process_jwt_auth +@process_registry_jwt_auth @require_repo_write @anon_protect def cancel_upload(namespace, repo_name, upload_uuid): @@ -345,7 +345,7 @@ def cancel_upload(namespace, repo_name, upload_uuid): @v2_bp.route('///blobs/', methods=['DELETE']) -@process_jwt_auth +@process_registry_jwt_auth @require_repo_write @anon_protect def delete_digest(namespace, repo_name, upload_uuid): diff --git a/endpoints/v2/manifest.py b/endpoints/v2/manifest.py index e70473470..6a6d5cb13 100644 --- a/endpoints/v2/manifest.py +++ b/endpoints/v2/manifest.py @@ -9,7 +9,7 @@ from jwkest.jws import SIGNER_ALGS, keyrep from datetime import datetime from app import docker_v2_signing_key -from auth.jwt_auth import process_jwt_auth +from auth.registry_jwt_auth import process_registry_jwt_auth from endpoints.decorators import anon_protect from endpoints.v2 import v2_bp, require_repo_read, require_repo_write from endpoints.v2.errors import (BlobUnknown, ManifestInvalid, ManifestUnverified, @@ -212,7 +212,7 @@ class SignedManifestBuilder(object): @v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['GET']) -@process_jwt_auth +@process_registry_jwt_auth @require_repo_read @anon_protect def fetch_manifest_by_tagname(namespace, repo_name, manifest_ref): @@ -241,7 +241,7 @@ def fetch_manifest_by_tagname(namespace, repo_name, manifest_ref): @v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['GET']) -@process_jwt_auth +@process_registry_jwt_auth @require_repo_read @anon_protect def fetch_manifest_by_digest(namespace, repo_name, manifest_ref): @@ -261,7 +261,7 @@ def fetch_manifest_by_digest(namespace, repo_name, manifest_ref): @v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['PUT']) -@process_jwt_auth +@process_registry_jwt_auth @require_repo_write @anon_protect def write_manifest_by_tagname(namespace, repo_name, manifest_ref): @@ -277,7 +277,7 @@ def write_manifest_by_tagname(namespace, repo_name, manifest_ref): @v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['PUT']) -@process_jwt_auth +@process_registry_jwt_auth @require_repo_write @anon_protect def write_manifest_by_digest(namespace, repo_name, manifest_ref): @@ -375,7 +375,7 @@ def _write_manifest(namespace, repo_name, manifest): @v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['DELETE']) -@process_jwt_auth +@process_registry_jwt_auth @require_repo_write @anon_protect def delete_manifest_by_digest(namespace, repo_name, manifest_ref): diff --git a/endpoints/v2/tag.py b/endpoints/v2/tag.py index 16ab87431..a1811b063 100644 --- a/endpoints/v2/tag.py +++ b/endpoints/v2/tag.py @@ -1,14 +1,14 @@ from flask import jsonify, url_for +from auth.registry_jwt_auth import process_registry_jwt_auth from endpoints.v2 import v2_bp, require_repo_read from endpoints.v2.errors import NameUnknown from endpoints.v2.v2util import add_pagination -from auth.jwt_auth import process_jwt_auth from endpoints.decorators import anon_protect from data import model @v2_bp.route('///tags/list', methods=['GET']) -@process_jwt_auth +@process_registry_jwt_auth @require_repo_read @anon_protect def list_all_tags(namespace, repo_name): diff --git a/endpoints/v2/v2auth.py b/endpoints/v2/v2auth.py index 80eecf5a0..913835de0 100644 --- a/endpoints/v2/v2auth.py +++ b/endpoints/v2/v2auth.py @@ -9,7 +9,7 @@ from cachetools import lru_cache from app import app from data import model from auth.auth import process_auth -from auth.jwt_auth import build_context_and_subject +from auth.registry_jwt_auth import build_context_and_subject from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token from auth.permissions import (ModifyRepositoryPermission, ReadRepositoryPermission, CreateRepositoryPermission) @@ -49,7 +49,7 @@ def generate_registry_jwt(): audience_param = request.args.get('service') logger.debug('Request audience: %s', audience_param) - scope_param = request.args.get('scope') + scope_param = request.args.get('scope') or '' logger.debug('Scope request: %s', scope_param) user = get_authenticated_user() @@ -62,7 +62,8 @@ def generate_registry_jwt(): logger.debug('Authenticated OAuth token: %s', oauthtoken) access = [] - if scope_param is not None: + + if len(scope_param) > 0: match = SCOPE_REGEX.match(scope_param) if match is None: logger.debug('Match: %s', match) diff --git a/test/registry_tests.py b/test/registry_tests.py index 644b3508d..6ad43d140 100644 --- a/test/registry_tests.py +++ b/test/registry_tests.py @@ -1193,6 +1193,9 @@ class V1LoginTests(V1RegistryLoginMixin, LoginTests, RegistryTestCaseMixin, Base class V2LoginTests(V2RegistryLoginMixin, LoginTests, RegistryTestCaseMixin, BaseRegistryMixin, LiveServerTestCase): """ Tests for V2 login. """ + def test_nouser_noscope(self): + self.do_login('', '', expected_code=401, scope='') + def test_validuser_unknownrepo(self): self.do_login('devtable', 'password', expect_success=False, scope='repository:invalidnamespace/simple:pull') diff --git a/test/test_registry_v2_auth.py b/test/test_registry_v2_auth.py index dcd4a6bbd..f72de245d 100644 --- a/test/test_registry_v2_auth.py +++ b/test/test_registry_v2_auth.py @@ -7,8 +7,8 @@ from cryptography.hazmat.primitives.asymmetric import rsa from app import app from endpoints.v2.v2auth import TOKEN_VALIDITY_LIFETIME_S, load_certificate_bytes, load_private_key -from auth.jwt_auth import (identity_from_bearer_token, load_public_key, InvalidJWTException, - build_context_and_subject, ANONYMOUS_SUB) +from auth.registry_jwt_auth import (identity_from_bearer_token, load_public_key, + InvalidJWTException, build_context_and_subject, ANONYMOUS_SUB) from util.morecollections import AttrDict