Make our JWT subjects better and log using the info

Fixes #1039
This commit is contained in:
Joseph Schorr 2015-12-09 16:10:39 -05:00
parent 35437c9f55
commit 4a4eee5e05
10 changed files with 199 additions and 35 deletions

View file

@ -14,7 +14,7 @@ import scopes
from data import model
from app import app, authentication
from permissions import QuayDeferredPermissionUser
from auth_context import (set_authenticated_user, set_validated_token, set_grant_user_context,
from auth_context import (set_authenticated_user, set_validated_token, set_grant_context,
set_validated_oauth_token)
from util.http import abort
@ -173,7 +173,13 @@ def _process_signed_grant(auth):
logger.debug('Successfully validated signed grant with data: %s', token_data)
loaded_identity = Identity(None, 'signed_grant')
set_grant_user_context(token_data['user_context'])
if token_data['user_context']:
set_grant_context({
'user': token_data['user_context'],
'kind': 'user',
})
loaded_identity.provides.update(token_data['grants'])
identity_changed.send(app, identity=loaded_identity)

View file

@ -36,13 +36,13 @@ def set_authenticated_user(user_or_robot):
ctx.authenticated_user = user_or_robot
def get_grant_user_context():
return getattr(_request_ctx_stack.top, 'grant_user_context', None)
def get_grant_context():
return getattr(_request_ctx_stack.top, 'grant_context', None)
def set_grant_user_context(username_or_robotname):
def set_grant_context(grant_context):
ctx = _request_ctx_stack.top
ctx.grant_user_context = username_or_robotname
ctx.grant_context = grant_context
def set_authenticated_user_deferred(user_or_robot_db_uuid):

View file

@ -10,18 +10,20 @@ from cryptography.hazmat.backends import default_backend
from cachetools import lru_cache
from app import app
from .auth_context import set_grant_user_context
from .auth_context import set_grant_context, get_grant_context
from .permissions import repository_read_grant, repository_write_grant
from util.names import parse_namespace_repository
from util.http import abort
from util.security import strictjwt
from data import model
logger = logging.getLogger(__name__)
TOKEN_REGEX = re.compile(r'^Bearer (([a-zA-Z0-9+/]+\.)+[a-zA-Z0-9+-_/]+)$')
ANONYMOUS_SUB = '(anonymous)'
CONTEXT_KINDS = ['user', 'token', 'oauth']
ACCESS_SCHEMA = {
'type': 'array',
@ -65,6 +67,91 @@ class InvalidJWTException(Exception):
pass
class GrantedEntity(object):
def __init__(self, user=None, token=None, oauth=None):
self.user = user
self.token = token
self.oauth = oauth
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
if kind == 'user':
user = model.user.get_user(context.get('user', ''))
if not user:
return None
return GrantedEntity(user=user)
if kind == 'token':
return GrantedEntity(token=context.get('token'))
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
def build_context_and_subject(user, token, oauthtoken):
""" Builds the custom context field for the JWT signed token and returns it,
along with the subject for the JWT signed token. """
if oauthtoken:
context = {
'kind': 'oauth',
'user': user.username,
'oauth': oauthtoken.uuid,
}
return (context, user.username)
if user:
context = {
'kind': 'user',
'user': user.username,
}
return (context, user.username)
if token:
context = {
'kind': 'token',
'token': token,
}
return (context, None)
context = {
'kind': 'anonymous',
}
return (context, ANONYMOUS_SUB)
def identity_from_bearer_token(bearer_token, max_signed_s, public_key):
""" Process a bearer token and return the loaded identity, or raise InvalidJWTException if an
identity could not be loaded. Expects tokens and grants in the format of the Docker registry
@ -94,11 +181,9 @@ def identity_from_bearer_token(bearer_token, max_signed_s, public_key):
if not 'sub' in payload:
raise InvalidJWTException('Missing sub field in JWT')
username = payload['sub']
loaded_identity = Identity(username, 'signed_jwt')
loaded_identity = Identity(payload['sub'], 'signed_jwt')
# Process the grants from the payload
if 'access' in payload:
try:
validate(payload['access'], ACCESS_SCHEMA)
@ -114,7 +199,17 @@ def identity_from_bearer_token(bearer_token, max_signed_s, public_key):
elif 'pull' in grant['actions']:
loaded_identity.provides.add(repository_read_grant(namespace, repo_name))
return loaded_identity
default_context = {
'kind': 'anonymous'
}
if payload['sub'] != ANONYMOUS_SUB:
default_context = {
'kind': 'user',
'user': payload['sub'],
}
return loaded_identity, payload.get('context', default_context)
@lru_cache(maxsize=1)
@ -135,9 +230,11 @@ def process_jwt_auth(func):
public_key = load_public_key(certificate_file_path)
try:
extracted_identity = identity_from_bearer_token(auth, max_signature_seconds, public_key)
extracted_identity, context = identity_from_bearer_token(auth, max_signature_seconds,
public_key)
identity_changed.send(app, identity=extracted_identity)
set_grant_user_context(extracted_identity.id)
set_grant_context(context)
logger.debug('Identity changed to %s', extracted_identity.id)
except InvalidJWTException as ije:
abort(401, message=ije.message)