2015-06-22 21:37:13 +00:00
|
|
|
import logging
|
|
|
|
|
|
|
|
from functools import wraps
|
2016-03-09 21:20:28 +00:00
|
|
|
|
|
|
|
from jsonschema import validate, ValidationError
|
2015-12-09 20:07:37 +00:00
|
|
|
from flask import request, url_for
|
2016-09-29 00:17:14 +00:00
|
|
|
from flask_principal import identity_changed, Identity
|
2015-06-22 21:37:13 +00:00
|
|
|
|
2016-05-31 20:48:19 +00:00
|
|
|
from app import app, get_app_url, instance_keys
|
2017-05-16 00:41:43 +00:00
|
|
|
from auth.auth_context import (set_grant_context, get_grant_context)
|
|
|
|
from auth.permissions import repository_read_grant, repository_write_grant, repository_admin_grant
|
2015-07-16 19:49:06 +00:00
|
|
|
from util.http import abort
|
2017-05-16 00:41:43 +00:00
|
|
|
from util.names import parse_namespace_repository
|
2016-08-24 16:55:33 +00:00
|
|
|
from util.security.registry_jwt import (ANONYMOUS_SUB, decode_bearer_header,
|
2016-05-31 20:48:19 +00:00
|
|
|
InvalidBearerTokenException)
|
2015-12-09 21:10:39 +00:00
|
|
|
from data import model
|
2015-06-22 21:37:13 +00:00
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2017-05-16 00:41:43 +00:00
|
|
|
|
2017-12-08 22:05:59 +00:00
|
|
|
CONTEXT_KINDS = ['user', 'token', 'oauth', 'app_specific_token']
|
2015-09-10 16:24:33 +00:00
|
|
|
|
2017-05-16 00:41:43 +00:00
|
|
|
|
2015-09-10 16:24:33 +00:00
|
|
|
ACCESS_SCHEMA = {
|
|
|
|
'type': 'array',
|
|
|
|
'description': 'List of access granted to the subject',
|
|
|
|
'items': {
|
|
|
|
'type': 'object',
|
|
|
|
'required': [
|
|
|
|
'type',
|
|
|
|
'name',
|
|
|
|
'actions',
|
|
|
|
],
|
|
|
|
'properties': {
|
|
|
|
'type': {
|
|
|
|
'type': 'string',
|
|
|
|
'description': 'We only allow repository permissions',
|
|
|
|
'enum': [
|
|
|
|
'repository',
|
|
|
|
],
|
|
|
|
},
|
|
|
|
'name': {
|
|
|
|
'type': 'string',
|
|
|
|
'description': 'The name of the repository for which we are receiving access'
|
|
|
|
},
|
|
|
|
'actions': {
|
|
|
|
'type': 'array',
|
|
|
|
'description': 'List of specific verbs which can be performed against repository',
|
|
|
|
'items': {
|
|
|
|
'type': 'string',
|
|
|
|
'enum': [
|
|
|
|
'push',
|
|
|
|
'pull',
|
2016-11-28 15:43:38 +00:00
|
|
|
'*',
|
2015-09-10 16:24:33 +00:00
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
2015-06-22 21:37:13 +00:00
|
|
|
|
|
|
|
|
2015-07-16 19:49:06 +00:00
|
|
|
class InvalidJWTException(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2015-12-09 21:10:39 +00:00
|
|
|
class GrantedEntity(object):
|
2017-12-08 22:05:59 +00:00
|
|
|
def __init__(self, user=None, token=None, oauth=None, app_specific_token=None):
|
2015-12-09 21:10:39 +00:00
|
|
|
self.user = user
|
|
|
|
self.token = token
|
|
|
|
self.oauth = oauth
|
2017-12-08 22:05:59 +00:00
|
|
|
self.app_specific_token = app_specific_token
|
2015-12-09 21:10:39 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_granted_entity():
|
|
|
|
""" Returns the entity granted in the current context, if any. Returns the GrantedEntity or None
|
|
|
|
if none.
|
|
|
|
"""
|
|
|
|
context = get_grant_context()
|
|
|
|
if not context:
|
|
|
|
return None
|
|
|
|
|
|
|
|
kind = context.get('kind', 'anonymous')
|
|
|
|
|
|
|
|
if not kind in CONTEXT_KINDS:
|
|
|
|
return None
|
|
|
|
|
2017-12-08 22:05:59 +00:00
|
|
|
if kind == 'app_specific_token':
|
|
|
|
app_specific_token = model.appspecifictoken.get_token_by_uuid(context.get('ast', ''))
|
|
|
|
if app_specific_token is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
return GrantedEntity(app_specific_token=app_specific_token, user=app_specific_token.user)
|
|
|
|
|
2015-12-09 21:10:39 +00:00
|
|
|
if kind == 'user':
|
|
|
|
user = model.user.get_user(context.get('user', ''))
|
|
|
|
if not user:
|
|
|
|
return None
|
|
|
|
|
|
|
|
return GrantedEntity(user=user)
|
|
|
|
|
|
|
|
if kind == 'token':
|
2015-12-15 21:52:22 +00:00
|
|
|
token = model.token.load_token_data(context.get('token'))
|
|
|
|
if not token:
|
|
|
|
return None
|
|
|
|
|
|
|
|
return GrantedEntity(token=token)
|
2015-12-09 21:10:39 +00:00
|
|
|
|
|
|
|
if kind == 'oauth':
|
|
|
|
user = model.user.get_user(context.get('user', ''))
|
|
|
|
if not user:
|
|
|
|
return None
|
|
|
|
|
|
|
|
oauthtoken = model.oauth.lookup_access_token_for_user(user, context.get('oauth', ''))
|
|
|
|
if not oauthtoken:
|
|
|
|
return None
|
|
|
|
|
|
|
|
return GrantedEntity(oauth=oauthtoken, user=user)
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def get_granted_username():
|
|
|
|
""" Returns the username inside the grant, if any. """
|
|
|
|
granted = get_granted_entity()
|
|
|
|
if not granted or not granted.user:
|
|
|
|
return None
|
|
|
|
|
|
|
|
return granted.user.username
|
|
|
|
|
|
|
|
|
2016-03-09 23:09:20 +00:00
|
|
|
def get_auth_headers(repository=None, scopes=None):
|
2015-12-09 20:07:37 +00:00
|
|
|
""" 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'])
|
2016-03-09 23:09:20 +00:00
|
|
|
if repository:
|
2016-10-17 18:32:43 +00:00
|
|
|
scopes_string = "repository:{0}".format(repository)
|
2016-03-09 23:09:20 +00:00
|
|
|
if scopes:
|
2016-10-17 18:32:43 +00:00
|
|
|
scopes_string += ':' + ','.join(scopes)
|
|
|
|
|
|
|
|
authenticate += ',scope="{0}"'.format(scopes_string)
|
2016-03-09 23:09:20 +00:00
|
|
|
|
2015-12-09 20:07:37 +00:00
|
|
|
headers['WWW-Authenticate'] = authenticate
|
|
|
|
headers['Docker-Distribution-API-Version'] = 'registry/2.0'
|
|
|
|
return headers
|
|
|
|
|
|
|
|
|
2016-08-24 16:55:33 +00:00
|
|
|
def identity_from_bearer_token(bearer_header):
|
|
|
|
""" Process a bearer header and return the loaded identity, or raise InvalidJWTException if an
|
2015-07-16 19:49:06 +00:00
|
|
|
identity could not be loaded. Expects tokens and grants in the format of the Docker registry
|
|
|
|
v2 auth spec: https://docs.docker.com/registry/spec/auth/token/
|
|
|
|
"""
|
2016-08-24 16:55:33 +00:00
|
|
|
logger.debug('Validating auth header: %s', bearer_header)
|
2015-07-16 19:49:06 +00:00
|
|
|
|
|
|
|
try:
|
2016-09-19 20:19:29 +00:00
|
|
|
payload = decode_bearer_header(bearer_header, instance_keys, app.config)
|
2016-05-31 20:48:19 +00:00
|
|
|
except InvalidBearerTokenException as bte:
|
|
|
|
logger.exception('Invalid bearer token: %s', bte)
|
|
|
|
raise InvalidJWTException(bte)
|
2015-07-16 19:49:06 +00:00
|
|
|
|
2015-12-09 21:10:39 +00:00
|
|
|
loaded_identity = Identity(payload['sub'], 'signed_jwt')
|
2015-07-16 19:49:06 +00:00
|
|
|
|
|
|
|
# Process the grants from the payload
|
|
|
|
if 'access' in payload:
|
2015-09-10 16:24:33 +00:00
|
|
|
try:
|
|
|
|
validate(payload['access'], ACCESS_SCHEMA)
|
|
|
|
except ValidationError:
|
|
|
|
logger.exception('We should not be minting invalid credentials')
|
|
|
|
raise InvalidJWTException('Token contained invalid or malformed access grants')
|
2015-07-16 19:49:06 +00:00
|
|
|
|
2016-01-21 20:40:51 +00:00
|
|
|
lib_namespace = app.config['LIBRARY_NAMESPACE']
|
2015-09-10 16:24:33 +00:00
|
|
|
for grant in payload['access']:
|
2016-01-21 20:40:51 +00:00
|
|
|
namespace, repo_name = parse_namespace_repository(grant['name'], lib_namespace)
|
2015-07-16 19:49:06 +00:00
|
|
|
|
2016-11-28 15:43:38 +00:00
|
|
|
if '*' in grant['actions']:
|
|
|
|
loaded_identity.provides.add(repository_admin_grant(namespace, repo_name))
|
|
|
|
elif 'push' in grant['actions']:
|
2015-07-16 19:49:06 +00:00
|
|
|
loaded_identity.provides.add(repository_write_grant(namespace, repo_name))
|
|
|
|
elif 'pull' in grant['actions']:
|
|
|
|
loaded_identity.provides.add(repository_read_grant(namespace, repo_name))
|
|
|
|
|
2015-12-09 21:10:39 +00:00
|
|
|
default_context = {
|
|
|
|
'kind': 'anonymous'
|
|
|
|
}
|
|
|
|
|
|
|
|
if payload['sub'] != ANONYMOUS_SUB:
|
|
|
|
default_context = {
|
|
|
|
'kind': 'user',
|
|
|
|
'user': payload['sub'],
|
|
|
|
}
|
|
|
|
|
|
|
|
return loaded_identity, payload.get('context', default_context)
|
2015-07-16 19:49:06 +00:00
|
|
|
|
|
|
|
|
2016-03-09 23:09:20 +00:00
|
|
|
def process_registry_jwt_auth(scopes=None):
|
|
|
|
def inner(func):
|
|
|
|
@wraps(func)
|
|
|
|
def wrapper(*args, **kwargs):
|
|
|
|
logger.debug('Called with params: %s, %s', args, kwargs)
|
|
|
|
auth = request.headers.get('authorization', '').strip()
|
|
|
|
if auth:
|
|
|
|
try:
|
2016-05-31 20:48:19 +00:00
|
|
|
extracted_identity, context = identity_from_bearer_token(auth)
|
2016-03-09 23:09:20 +00:00
|
|
|
identity_changed.send(app, identity=extracted_identity)
|
|
|
|
set_grant_context(context)
|
|
|
|
logger.debug('Identity changed to %s', extracted_identity.id)
|
|
|
|
except InvalidJWTException as ije:
|
|
|
|
repository = None
|
|
|
|
if 'namespace_name' in kwargs and 'repo_name' in kwargs:
|
|
|
|
repository = kwargs['namespace_name'] + '/' + kwargs['repo_name']
|
|
|
|
|
|
|
|
abort(401, message=ije.message, headers=get_auth_headers(repository=repository,
|
|
|
|
scopes=scopes))
|
|
|
|
else:
|
|
|
|
logger.debug('No auth header.')
|
|
|
|
|
|
|
|
return func(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
return inner
|