diff --git a/endpoints/v2/test/test_v2auth.py b/endpoints/v2/test/test_v2auth.py index 0989c6c4a..01fdaf603 100644 --- a/endpoints/v2/test/test_v2auth.py +++ b/endpoints/v2/test/test_v2auth.py @@ -1,18 +1,43 @@ import pytest -from endpoints.v2.v2auth import attach_metadata_root_name, CLAIM_APOSTILLE_ROOT +import flask +from flask import g +from flask_principal import Identity +from endpoints.v2.v2auth import get_tuf_root +from auth import permissions + +def admin_identity(namespace, reponame): + identity = Identity('admin') + identity.provides.add(permissions._RepositoryNeed(namespace, reponame, 'admin')) + identity.provides.add(permissions._OrganizationRepoNeed(namespace, 'admin')) + return identity -@pytest.mark.parametrize('context,access,expected', [ - ({}, None, {}), - ({}, [], {}), - ({}, [{}], {}), - ({}, [{"actions": None}], {}), - ({}, [{"actions": []}], {}), - ({}, [{"actions": ["pull"]}], {CLAIM_APOSTILLE_ROOT: 'quay'}), - ({}, [{"actions": ["push"]}], {CLAIM_APOSTILLE_ROOT: 'signer'}), - ({}, [{"actions": ["pull", "push"]}], {CLAIM_APOSTILLE_ROOT: 'signer'}), +def write_identity(namespace, reponame): + identity = Identity('writer') + identity.provides.add(permissions._RepositoryNeed(namespace, reponame, 'write')) + identity.provides.add(permissions._OrganizationRepoNeed(namespace, 'write')) + return identity + +def read_identity(namespace, reponame): + identity = Identity('reader') + identity.provides.add(permissions._RepositoryNeed(namespace, reponame, 'read')) + identity.provides.add(permissions._OrganizationRepoNeed(namespace, 'read')) + return identity + +@pytest.mark.parametrize('identity,expected', [ + (Identity('anon'), 'quay'), + (read_identity("namespace", "repo"), 'quay'), + (read_identity("different", "repo"), 'quay'), + (admin_identity("different", "repo"), 'quay'), + (write_identity("different", "repo"), 'quay'), + (admin_identity("namespace", "repo"), 'signer'), + (write_identity("namespace", "repo"), 'signer'), ]) -def test_attach_metadata_root_name(context, access, expected): - actual = attach_metadata_root_name(context, access) +def test_get_tuf_root(identity, expected): + app = flask.Flask(__name__) + + with app.test_request_context('/'): + g.identity = identity + actual = get_tuf_root("namespace", "repo") assert actual == expected, "should be %s, but was %s" % (expected, actual) diff --git a/endpoints/v2/v2auth.py b/endpoints/v2/v2auth.py index 7ae9b37a1..763838a09 100644 --- a/endpoints/v2/v2auth.py +++ b/endpoints/v2/v2auth.py @@ -17,8 +17,6 @@ from util.cache import no_cache from util.names import parse_namespace_repository, REPOSITORY_NAME_REGEX from util.security.registry_jwt import generate_bearer_token, build_context_and_subject -CLAIM_APOSTILLE_ROOT = 'com.apostille.root' - logger = logging.getLogger(__name__) @@ -67,6 +65,7 @@ def generate_registry_jwt(auth_result): user_event_data = { 'action': 'login', } + tuf_root = 'quay' if len(scope_param) > 0: match = get_scope_regex().match(scope_param) @@ -162,6 +161,8 @@ def generate_registry_jwt(auth_result): 'repository': reponame, 'namespace': namespace, } + + tuf_root = get_tuf_root(namespace, reponame) elif user is None and token is None: # In this case, we are doing an auth flow, and it's not an anonymous pull @@ -174,28 +175,14 @@ def generate_registry_jwt(auth_result): event.publish_event_data('docker-cli', user_event_data) # Build the signed JWT. - context, subject = build_context_and_subject(user, token, oauthtoken) - context = attach_metadata_root_name(context, access) + context, subject = build_context_and_subject(user, token, oauthtoken, tuf_root) token = generate_bearer_token(audience_param, subject, context, access, TOKEN_VALIDITY_LIFETIME_S, instance_keys) return jsonify({'token': token}) -def attach_metadata_root_name(context, access): - """ - Adds in metadata_root_name into JWT context when appropriate - """ - try: - actions = access[0]["actions"] - except(TypeError, IndexError, KeyError): - return context - - if not actions: - return context - - if "push" in actions: - context[CLAIM_APOSTILLE_ROOT] = 'signer' - else: - context[CLAIM_APOSTILLE_ROOT] = 'quay' - - return context +def get_tuf_root(namespace, reponame): + # Users with write access to a repo will see signer-rooted TUF metadata + if ModifyRepositoryPermission(namespace, reponame).can(): + return 'signer' + return 'quay' diff --git a/test/test_registry_v2_auth.py b/test/test_registry_v2_auth.py index bc5046463..b8cd0a9a2 100644 --- a/test/test_registry_v2_auth.py +++ b/test/test_registry_v2_auth.py @@ -25,10 +25,10 @@ class TestRegistryV2Auth(unittest.TestCase): def tearDown(self): finished_database_for_testing(self) - def _generate_token_data(self, access=[], audience=TEST_AUDIENCE, user=TEST_USER, iat=None, + def _generate_token_data(self, access=[], context=None, audience=TEST_AUDIENCE, user=TEST_USER, iat=None, exp=None, nbf=None, iss=None): - _, subject = build_context_and_subject(user, None, None) + _, subject = build_context_and_subject(user, None, None, None) return { 'iss': iss or instance_keys.service_name, 'aud': audience, @@ -37,6 +37,7 @@ class TestRegistryV2Auth(unittest.TestCase): 'exp': exp if exp is not None else int(time.time() + TOKEN_VALIDITY_LIFETIME_S), 'sub': subject, 'access': access, + 'context': context, } def _generate_token(self, token_data, key_id=None, private_key=None, skip_header=False, alg=None): diff --git a/util/secscan/api.py b/util/secscan/api.py index f90b3ba37..e44803e0b 100644 --- a/util/secscan/api.py +++ b/util/secscan/api.py @@ -105,7 +105,7 @@ class SecurityScannerAPI(object): # Generate the JWT which will authorize this audience = self._app.config['SERVER_HOSTNAME'] - context, subject = build_context_and_subject(None, None, None) + context, subject = build_context_and_subject(None, None, None, None) access = [{ 'type': 'repository', 'name': repository_and_namespace, diff --git a/util/security/registry_jwt.py b/util/security/registry_jwt.py index ff11f3db4..0f3479a9e 100644 --- a/util/security/registry_jwt.py +++ b/util/security/registry_jwt.py @@ -8,6 +8,7 @@ logger = logging.getLogger(__name__) ANONYMOUS_SUB = '(anonymous)' ALGORITHM = 'RS256' +CLAIM_TUF_ROOT = 'com.apostille.root' # The number of allowed seconds of clock skew for a JWT. The iat, nbf and exp are adjusted with this # count. @@ -99,14 +100,20 @@ def _generate_jwt_object(audience, subject, context, access, lifetime_s, issuer, return jwt.encode(token_data, private_key, ALGORITHM, headers=token_headers) -def build_context_and_subject(user, token, oauthtoken): +def build_context_and_subject(user, token, oauthtoken, tuf_root): """ Builds the custom context field for the JWT signed token and returns it, along with the subject for the JWT signed token. """ + + # Serve quay root if not explicitly granted permission to see signer root + if not tuf_root: + tuf_root = 'quay' + if oauthtoken: context = { 'kind': 'oauth', 'user': user.username, 'oauth': oauthtoken.uuid, + CLAIM_TUF_ROOT: tuf_root, } return (context, user.username) @@ -115,6 +122,7 @@ def build_context_and_subject(user, token, oauthtoken): context = { 'kind': 'user', 'user': user.username, + CLAIM_TUF_ROOT: tuf_root, } return (context, user.username) @@ -122,11 +130,13 @@ def build_context_and_subject(user, token, oauthtoken): context = { 'kind': 'token', 'token': token.code, + CLAIM_TUF_ROOT: tuf_root, } return (context, None) context = { 'kind': 'anonymous', + CLAIM_TUF_ROOT: tuf_root, } return (context, ANONYMOUS_SUB)