Determine which TUF root to show based on actual access, not requested

access
This commit is contained in:
Evan Cordell 2017-03-22 07:38:52 -04:00
parent 7b411b2c25
commit 43dd974dca
5 changed files with 61 additions and 38 deletions

View file

@ -1,18 +1,43 @@
import pytest 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
@pytest.mark.parametrize('context,access,expected', [ def admin_identity(namespace, reponame):
({}, None, {}), identity = Identity('admin')
({}, [], {}), identity.provides.add(permissions._RepositoryNeed(namespace, reponame, 'admin'))
({}, [{}], {}), identity.provides.add(permissions._OrganizationRepoNeed(namespace, 'admin'))
({}, [{"actions": None}], {}), return identity
({}, [{"actions": []}], {}),
({}, [{"actions": ["pull"]}], {CLAIM_APOSTILLE_ROOT: 'quay'}), def write_identity(namespace, reponame):
({}, [{"actions": ["push"]}], {CLAIM_APOSTILLE_ROOT: 'signer'}), identity = Identity('writer')
({}, [{"actions": ["pull", "push"]}], {CLAIM_APOSTILLE_ROOT: 'signer'}), 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): def test_get_tuf_root(identity, expected):
actual = attach_metadata_root_name(context, access) 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) assert actual == expected, "should be %s, but was %s" % (expected, actual)

View file

@ -17,8 +17,6 @@ from util.cache import no_cache
from util.names import parse_namespace_repository, REPOSITORY_NAME_REGEX from util.names import parse_namespace_repository, REPOSITORY_NAME_REGEX
from util.security.registry_jwt import generate_bearer_token, build_context_and_subject from util.security.registry_jwt import generate_bearer_token, build_context_and_subject
CLAIM_APOSTILLE_ROOT = 'com.apostille.root'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -67,6 +65,7 @@ def generate_registry_jwt(auth_result):
user_event_data = { user_event_data = {
'action': 'login', 'action': 'login',
} }
tuf_root = 'quay'
if len(scope_param) > 0: if len(scope_param) > 0:
match = get_scope_regex().match(scope_param) match = get_scope_regex().match(scope_param)
@ -163,6 +162,8 @@ def generate_registry_jwt(auth_result):
'namespace': namespace, 'namespace': namespace,
} }
tuf_root = get_tuf_root(namespace, reponame)
elif user is None and token is None: elif user is None and token is None:
# In this case, we are doing an auth flow, and it's not an anonymous pull # In this case, we are doing an auth flow, and it's not an anonymous pull
logger.debug('No user and no token sent for empty scope list') logger.debug('No user and no token sent for empty scope list')
@ -174,28 +175,14 @@ def generate_registry_jwt(auth_result):
event.publish_event_data('docker-cli', user_event_data) event.publish_event_data('docker-cli', user_event_data)
# Build the signed JWT. # Build the signed JWT.
context, subject = build_context_and_subject(user, token, oauthtoken) context, subject = build_context_and_subject(user, token, oauthtoken, tuf_root)
context = attach_metadata_root_name(context, access)
token = generate_bearer_token(audience_param, subject, context, access, token = generate_bearer_token(audience_param, subject, context, access,
TOKEN_VALIDITY_LIFETIME_S, instance_keys) TOKEN_VALIDITY_LIFETIME_S, instance_keys)
return jsonify({'token': token}) return jsonify({'token': token})
def attach_metadata_root_name(context, access): def get_tuf_root(namespace, reponame):
""" # Users with write access to a repo will see signer-rooted TUF metadata
Adds in metadata_root_name into JWT context when appropriate if ModifyRepositoryPermission(namespace, reponame).can():
""" return 'signer'
try: return 'quay'
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

View file

@ -25,10 +25,10 @@ class TestRegistryV2Auth(unittest.TestCase):
def tearDown(self): def tearDown(self):
finished_database_for_testing(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): exp=None, nbf=None, iss=None):
_, subject = build_context_and_subject(user, None, None) _, subject = build_context_and_subject(user, None, None, None)
return { return {
'iss': iss or instance_keys.service_name, 'iss': iss or instance_keys.service_name,
'aud': audience, '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), 'exp': exp if exp is not None else int(time.time() + TOKEN_VALIDITY_LIFETIME_S),
'sub': subject, 'sub': subject,
'access': access, 'access': access,
'context': context,
} }
def _generate_token(self, token_data, key_id=None, private_key=None, skip_header=False, alg=None): def _generate_token(self, token_data, key_id=None, private_key=None, skip_header=False, alg=None):

View file

@ -105,7 +105,7 @@ class SecurityScannerAPI(object):
# Generate the JWT which will authorize this # Generate the JWT which will authorize this
audience = self._app.config['SERVER_HOSTNAME'] 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 = [{ access = [{
'type': 'repository', 'type': 'repository',
'name': repository_and_namespace, 'name': repository_and_namespace,

View file

@ -8,6 +8,7 @@ logger = logging.getLogger(__name__)
ANONYMOUS_SUB = '(anonymous)' ANONYMOUS_SUB = '(anonymous)'
ALGORITHM = 'RS256' 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 # The number of allowed seconds of clock skew for a JWT. The iat, nbf and exp are adjusted with this
# count. # 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) 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, """ Builds the custom context field for the JWT signed token and returns it,
along with the subject for the JWT signed token. """ 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: if oauthtoken:
context = { context = {
'kind': 'oauth', 'kind': 'oauth',
'user': user.username, 'user': user.username,
'oauth': oauthtoken.uuid, 'oauth': oauthtoken.uuid,
CLAIM_TUF_ROOT: tuf_root,
} }
return (context, user.username) return (context, user.username)
@ -115,6 +122,7 @@ def build_context_and_subject(user, token, oauthtoken):
context = { context = {
'kind': 'user', 'kind': 'user',
'user': user.username, 'user': user.username,
CLAIM_TUF_ROOT: tuf_root,
} }
return (context, user.username) return (context, user.username)
@ -122,11 +130,13 @@ def build_context_and_subject(user, token, oauthtoken):
context = { context = {
'kind': 'token', 'kind': 'token',
'token': token.code, 'token': token.code,
CLAIM_TUF_ROOT: tuf_root,
} }
return (context, None) return (context, None)
context = { context = {
'kind': 'anonymous', 'kind': 'anonymous',
CLAIM_TUF_ROOT: tuf_root,
} }
return (context, ANONYMOUS_SUB) return (context, ANONYMOUS_SUB)