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
|
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)
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Reference in a new issue