Determine which TUF root to show based on actual access, not requested
access
This commit is contained in:
		
							parent
							
								
									7b411b2c25
								
							
						
					
					
						commit
						43dd974dca
					
				
					 5 changed files with 61 additions and 38 deletions
				
			
		|  | @ -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) | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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): | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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) | ||||
| 
 | ||||
|  |  | |||
		Reference in a new issue