Merge pull request #2967 from coreos-inc/joseph.schorr/QS-111/auth-refactor
Refactor auth code to be cleaner and more extensible
This commit is contained in:
commit
7cd2c00d4d
31 changed files with 822 additions and 436 deletions
5
app.py
5
app.py
|
@ -6,12 +6,11 @@ import os
|
|||
from functools import partial
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
from flask import Flask, request, Request, _request_ctx_stack
|
||||
from flask_login import LoginManager, UserMixin
|
||||
from flask import Flask, request, Request
|
||||
from flask_login import LoginManager
|
||||
from flask_mail import Mail
|
||||
from flask_principal import Principal
|
||||
from jwkest.jwk import RSAKey
|
||||
from werkzeug.routing import BaseConverter
|
||||
|
||||
import features
|
||||
from _init import CONF_DIR
|
||||
|
|
|
@ -1,78 +1,21 @@
|
|||
import logging
|
||||
|
||||
from flask import _request_ctx_stack
|
||||
from data import model
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_authenticated_context():
|
||||
""" Returns the auth context for the current request context, if any. """
|
||||
return getattr(_request_ctx_stack.top, 'authenticated_context', None)
|
||||
|
||||
def get_authenticated_user():
|
||||
user = getattr(_request_ctx_stack.top, 'authenticated_user', None)
|
||||
if not user:
|
||||
user_uuid = getattr(_request_ctx_stack.top, 'authenticated_user_uuid', None)
|
||||
if not user_uuid:
|
||||
logger.debug('No authenticated user or deferred user uuid.')
|
||||
return None
|
||||
|
||||
logger.debug('Loading deferred authenticated user.')
|
||||
loaded = model.user.get_user_by_uuid(user_uuid)
|
||||
if not loaded.enabled:
|
||||
return None
|
||||
|
||||
set_authenticated_user(loaded)
|
||||
user = loaded
|
||||
|
||||
if user:
|
||||
logger.debug('Returning authenticated user: %s', user.username)
|
||||
return user
|
||||
|
||||
|
||||
def set_authenticated_user(user_or_robot):
|
||||
if not user_or_robot.enabled:
|
||||
raise Exception('Attempt to authenticate a disabled user/robot: %s' % user_or_robot.username)
|
||||
|
||||
ctx = _request_ctx_stack.top
|
||||
ctx.authenticated_user = user_or_robot
|
||||
|
||||
|
||||
def get_grant_context():
|
||||
return getattr(_request_ctx_stack.top, 'grant_context', None)
|
||||
|
||||
|
||||
def set_grant_context(grant_context):
|
||||
ctx = _request_ctx_stack.top
|
||||
ctx.grant_context = grant_context
|
||||
|
||||
|
||||
def set_authenticated_user_deferred(user_or_robot_db_uuid):
|
||||
logger.debug('Deferring loading of authenticated user object with uuid: %s', user_or_robot_db_uuid)
|
||||
ctx = _request_ctx_stack.top
|
||||
ctx.authenticated_user_uuid = user_or_robot_db_uuid
|
||||
|
||||
""" Returns the authenticated user, if any, or None if none. """
|
||||
context = get_authenticated_context()
|
||||
return context.authed_user if context else None
|
||||
|
||||
def get_validated_oauth_token():
|
||||
return getattr(_request_ctx_stack.top, 'validated_oauth_token', None)
|
||||
""" Returns the authenticated and validated OAuth access token, if any, or None if none. """
|
||||
context = get_authenticated_context()
|
||||
return context.authed_oauth_token if context else None
|
||||
|
||||
|
||||
def set_validated_oauth_token(token):
|
||||
def set_authenticated_context(auth_context):
|
||||
""" Sets the auth context for the current request context to that given. """
|
||||
ctx = _request_ctx_stack.top
|
||||
ctx.validated_oauth_token = token
|
||||
|
||||
|
||||
def get_validated_app_specific_token():
|
||||
return getattr(_request_ctx_stack.top, 'validated_app_specific_token', None)
|
||||
|
||||
|
||||
def set_validated_app_specific_token(token):
|
||||
ctx = _request_ctx_stack.top
|
||||
ctx.validated_app_specific_token = token
|
||||
|
||||
|
||||
def get_validated_token():
|
||||
return getattr(_request_ctx_stack.top, 'validated_token', None)
|
||||
|
||||
|
||||
def set_validated_token(token):
|
||||
ctx = _request_ctx_stack.top
|
||||
ctx.validated_token = token
|
||||
ctx.authenticated_context = auth_context
|
||||
return auth_context
|
||||
|
|
413
auth/auth_context_type.py
Normal file
413
auth/auth_context_type.py
Normal file
|
@ -0,0 +1,413 @@
|
|||
import logging
|
||||
|
||||
from cachetools import lru_cache
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from six import add_metaclass
|
||||
|
||||
from app import app
|
||||
from data import model
|
||||
|
||||
from flask_principal import Identity, identity_changed
|
||||
|
||||
from auth.auth_context import set_authenticated_context
|
||||
from auth.context_entity import ContextEntityKind, CONTEXT_ENTITY_HANDLERS
|
||||
from auth.permissions import QuayDeferredPermissionUser
|
||||
from auth.scopes import scopes_from_scope_string
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@add_metaclass(ABCMeta)
|
||||
class AuthContext(object):
|
||||
"""
|
||||
Interface that represents the current context of authentication.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def entity_kind(self):
|
||||
""" Returns the kind of the entity in this auth context. """
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def is_anonymous(self):
|
||||
""" Returns true if this is an anonymous context. """
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def authed_oauth_token(self):
|
||||
""" Returns the authenticated OAuth token, if any. """
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def authed_user(self):
|
||||
""" Returns the authenticated user, whether directly, or via an OAuth or access token. Note that
|
||||
this property will also return robot accounts.
|
||||
"""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def has_nonrobot_user(self):
|
||||
""" Returns whether a user (not a robot) was authenticated successfully. """
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def identity(self):
|
||||
""" Returns the identity for the auth context. """
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def description(self):
|
||||
""" Returns a human-readable and *public* description of the current auth context. """
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def credential_username(self):
|
||||
""" Returns the username to create credentials for this context's entity, if any. """
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def analytics_id_and_public_metadata(self):
|
||||
""" Returns the analytics ID and public log metadata for this auth context. """
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def apply_to_request_context(self):
|
||||
""" Applies this auth result to the auth context and Flask-Principal. """
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def to_signed_dict(self):
|
||||
""" Serializes the auth context into a dictionary suitable for inclusion in a JWT or other
|
||||
form of signed serialization.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ValidatedAuthContext(AuthContext):
|
||||
""" ValidatedAuthContext represents the loaded, authenticated and validated auth information
|
||||
for the current request context.
|
||||
"""
|
||||
def __init__(self, user=None, token=None, oauthtoken=None, robot=None, appspecifictoken=None,
|
||||
signed_data=None):
|
||||
# Note: These field names *MUST* match the string values of the kinds defined in
|
||||
# ContextEntityKind.
|
||||
self.user = user
|
||||
self.robot = robot
|
||||
self.token = token
|
||||
self.oauthtoken = oauthtoken
|
||||
self.appspecifictoken = appspecifictoken
|
||||
self.signed_data = signed_data
|
||||
|
||||
def tuple(self):
|
||||
return vars(self).values()
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.tuple() == other.tuple()
|
||||
|
||||
@property
|
||||
def entity_kind(self):
|
||||
""" Returns the kind of the entity in this auth context. """
|
||||
for kind in ContextEntityKind:
|
||||
if hasattr(self, kind.value) and getattr(self, kind.value):
|
||||
return kind
|
||||
|
||||
return ContextEntityKind.anonymous
|
||||
|
||||
@property
|
||||
def authed_user(self):
|
||||
""" Returns the authenticated user, whether directly, or via an OAuth token. Note that this
|
||||
will also return robot accounts.
|
||||
"""
|
||||
authed_user = self._authed_user()
|
||||
if authed_user is not None and not authed_user.enabled:
|
||||
logger.warning('Attempt to reference a disabled user/robot: %s', authed_user.username)
|
||||
return None
|
||||
|
||||
return authed_user
|
||||
|
||||
@property
|
||||
def authed_oauth_token(self):
|
||||
return self.oauthtoken
|
||||
|
||||
def _authed_user(self):
|
||||
if self.oauthtoken:
|
||||
return self.oauthtoken.authorized_user
|
||||
|
||||
if self.appspecifictoken:
|
||||
return self.appspecifictoken.user
|
||||
|
||||
if self.signed_data:
|
||||
return model.user.get_user(self.signed_data['user_context'])
|
||||
|
||||
return self.user if self.user else self.robot
|
||||
|
||||
@property
|
||||
def is_anonymous(self):
|
||||
""" Returns true if this is an anonymous context. """
|
||||
return not self.authed_user and not self.token and not self.signed_data
|
||||
|
||||
@property
|
||||
def has_nonrobot_user(self):
|
||||
""" Returns whether a user (not a robot) was authenticated successfully. """
|
||||
return bool(self.authed_user and not self.robot)
|
||||
|
||||
@property
|
||||
def identity(self):
|
||||
""" Returns the identity for the auth context. """
|
||||
if self.oauthtoken:
|
||||
scope_set = scopes_from_scope_string(self.oauthtoken.scope)
|
||||
return QuayDeferredPermissionUser.for_user(self.oauthtoken.authorized_user, scope_set)
|
||||
|
||||
if self.authed_user:
|
||||
return QuayDeferredPermissionUser.for_user(self.authed_user)
|
||||
|
||||
if self.token:
|
||||
return Identity(self.token.code, 'token')
|
||||
|
||||
if self.signed_data:
|
||||
identity = Identity(None, 'signed_grant')
|
||||
identity.provides.update(self.signed_data['grants'])
|
||||
return identity
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def entity_reference(self):
|
||||
""" Returns the DB object reference for this context's entity. """
|
||||
if self.entity_kind == ContextEntityKind.anonymous:
|
||||
return None
|
||||
|
||||
return getattr(self, self.entity_kind.value)
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
""" Returns a human-readable and *public* description of the current auth context. """
|
||||
handler = CONTEXT_ENTITY_HANDLERS[self.entity_kind]()
|
||||
return handler.description(self.entity_reference)
|
||||
|
||||
@property
|
||||
def credential_username(self):
|
||||
""" Returns the username to create credentials for this context's entity, if any. """
|
||||
handler = CONTEXT_ENTITY_HANDLERS[self.entity_kind]()
|
||||
return handler.credential_username(self.entity_reference)
|
||||
|
||||
def analytics_id_and_public_metadata(self):
|
||||
""" Returns the analytics ID and public log metadata for this auth context. """
|
||||
handler = CONTEXT_ENTITY_HANDLERS[self.entity_kind]()
|
||||
return handler.analytics_id_and_public_metadata(self.entity_reference)
|
||||
|
||||
def apply_to_request_context(self):
|
||||
""" Applies this auth result to the auth context and Flask-Principal. """
|
||||
# Save to the request context.
|
||||
set_authenticated_context(self)
|
||||
|
||||
# Set the identity for Flask-Principal.
|
||||
if self.identity:
|
||||
identity_changed.send(app, identity=self.identity)
|
||||
|
||||
def to_signed_dict(self):
|
||||
""" Serializes the auth context into a dictionary suitable for inclusion in a JWT or other
|
||||
form of signed serialization.
|
||||
"""
|
||||
dict_data = {
|
||||
'version': 2,
|
||||
'entity_kind': self.entity_kind.value,
|
||||
}
|
||||
|
||||
if self.entity_kind != ContextEntityKind.anonymous:
|
||||
handler = CONTEXT_ENTITY_HANDLERS[self.entity_kind]()
|
||||
dict_data.update({
|
||||
'entity_reference': handler.get_serialized_entity_reference(self.entity_reference),
|
||||
})
|
||||
|
||||
# Add legacy information.
|
||||
# TODO(jschorr): Remove this all once the new code is fully deployed.
|
||||
if self.token:
|
||||
dict_data.update({
|
||||
'kind': 'token',
|
||||
'token': self.token.code,
|
||||
})
|
||||
|
||||
if self.oauthtoken:
|
||||
dict_data.update({
|
||||
'kind': 'oauth',
|
||||
'oauth': self.oauthtoken.uuid,
|
||||
'user': self.authed_user.username,
|
||||
})
|
||||
|
||||
if self.user or self.robot:
|
||||
dict_data.update({
|
||||
'kind': 'user',
|
||||
'user': self.authed_user.username,
|
||||
})
|
||||
|
||||
if self.appspecifictoken:
|
||||
dict_data.update({
|
||||
'kind': 'user',
|
||||
'user': self.authed_user.username,
|
||||
})
|
||||
|
||||
if self.is_anonymous:
|
||||
dict_data.update({
|
||||
'kind': 'anonymous',
|
||||
})
|
||||
|
||||
# End of legacy information.
|
||||
return dict_data
|
||||
|
||||
class SignedAuthContext(AuthContext):
|
||||
""" SignedAuthContext represents an auth context loaded from a signed token of some kind,
|
||||
such as a JWT. Unlike ValidatedAuthContext, SignedAuthContext operates lazily, only loading
|
||||
the actual {user, robot, token, etc} when requested. This allows registry operations that
|
||||
only need to check if *some* entity is present to do so, without hitting the database.
|
||||
"""
|
||||
def __init__(self, kind, signed_data, v1_dict_format):
|
||||
self.kind = kind
|
||||
self.signed_data = signed_data
|
||||
self.v1_dict_format = v1_dict_format
|
||||
|
||||
@classmethod
|
||||
def build_from_signed_dict(cls, dict_data, v1_dict_format=False):
|
||||
if not v1_dict_format:
|
||||
entity_kind = ContextEntityKind(dict_data.get('entity_kind', 'anonymous'))
|
||||
return SignedAuthContext(entity_kind, dict_data, v1_dict_format)
|
||||
|
||||
# Legacy handling.
|
||||
# TODO(jschorr): Remove this all once the new code is fully deployed.
|
||||
kind_string = dict_data.get('kind', 'anonymous')
|
||||
if kind_string == 'oauth':
|
||||
kind_string = 'oauthtoken'
|
||||
|
||||
kind = ContextEntityKind(kind_string)
|
||||
return SignedAuthContext(kind, dict_data, v1_dict_format)
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _get_validated(self):
|
||||
""" Returns a ValidatedAuthContext for this signed context, resolving all the necessary
|
||||
references.
|
||||
"""
|
||||
if not self.v1_dict_format:
|
||||
if self.kind == ContextEntityKind.anonymous:
|
||||
return ValidatedAuthContext()
|
||||
|
||||
serialized_entity_reference = self.signed_data['entity_reference']
|
||||
handler = CONTEXT_ENTITY_HANDLERS[self.kind]()
|
||||
entity_reference = handler.deserialize_entity_reference(serialized_entity_reference)
|
||||
if entity_reference is None:
|
||||
logger.debug('Could not deserialize entity reference `%s` under kind `%s`',
|
||||
serialized_entity_reference, self.kind)
|
||||
return ValidatedAuthContext()
|
||||
|
||||
return ValidatedAuthContext(**{self.kind.value: entity_reference})
|
||||
|
||||
# Legacy handling.
|
||||
# TODO(jschorr): Remove this all once the new code is fully deployed.
|
||||
kind_string = self.signed_data.get('kind', 'anonymous')
|
||||
if kind_string == 'oauth':
|
||||
kind_string = 'oauthtoken'
|
||||
|
||||
kind = ContextEntityKind(kind_string)
|
||||
if kind == ContextEntityKind.anonymous:
|
||||
return ValidatedAuthContext()
|
||||
|
||||
if kind == ContextEntityKind.user or kind == ContextEntityKind.robot:
|
||||
user = model.user.get_user(self.signed_data.get('user', ''))
|
||||
if not user:
|
||||
return None
|
||||
|
||||
return ValidatedAuthContext(robot=user) if user.robot else ValidatedAuthContext(user=user)
|
||||
|
||||
if kind == ContextEntityKind.token:
|
||||
token = model.token.load_token_data(self.signed_data.get('token'))
|
||||
if not token:
|
||||
return None
|
||||
|
||||
return ValidatedAuthContext(token=token)
|
||||
|
||||
if kind == ContextEntityKind.oauthtoken:
|
||||
user = model.user.get_user(self.signed_data.get('user', ''))
|
||||
if not user:
|
||||
return None
|
||||
|
||||
token_uuid = self.signed_data.get('oauth', '')
|
||||
oauthtoken = model.oauth.lookup_access_token_for_user(user, token_uuid)
|
||||
if not oauthtoken:
|
||||
return None
|
||||
|
||||
return ValidatedAuthContext(oauthtoken=oauthtoken)
|
||||
|
||||
raise Exception('Unknown auth context kind `%s` when deserializing %s' % (kind,
|
||||
self.signed_data))
|
||||
# End of legacy handling.
|
||||
|
||||
@property
|
||||
def entity_kind(self):
|
||||
""" Returns the kind of the entity in this auth context. """
|
||||
return self.kind
|
||||
|
||||
@property
|
||||
def is_anonymous(self):
|
||||
""" Returns true if this is an anonymous context. """
|
||||
return self.kind == ContextEntityKind.anonymous
|
||||
|
||||
@property
|
||||
def authed_user(self):
|
||||
""" Returns the authenticated user, whether directly, or via an OAuth or access token. Note that
|
||||
this property will also return robot accounts.
|
||||
"""
|
||||
if self.kind == ContextEntityKind.anonymous:
|
||||
return None
|
||||
|
||||
return self._get_validated().authed_user
|
||||
|
||||
@property
|
||||
def authed_oauth_token(self):
|
||||
if self.kind == ContextEntityKind.anonymous:
|
||||
return None
|
||||
|
||||
return self._get_validated().authed_oauth_token
|
||||
|
||||
@property
|
||||
def has_nonrobot_user(self):
|
||||
""" Returns whether a user (not a robot) was authenticated successfully. """
|
||||
if self.kind == ContextEntityKind.anonymous:
|
||||
return False
|
||||
|
||||
return self._get_validated().has_nonrobot_user
|
||||
|
||||
@property
|
||||
def identity(self):
|
||||
""" Returns the identity for the auth context. """
|
||||
return self._get_validated().identity
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
""" Returns a human-readable and *public* description of the current auth context. """
|
||||
return self._get_validated().description
|
||||
|
||||
@property
|
||||
def credential_username(self):
|
||||
""" Returns the username to create credentials for this context's entity, if any. """
|
||||
return self._get_validated().credential_username
|
||||
|
||||
def analytics_id_and_public_metadata(self):
|
||||
""" Returns the analytics ID and public log metadata for this auth context. """
|
||||
return self._get_validated().analytics_id_and_public_metadata()
|
||||
|
||||
def apply_to_request_context(self):
|
||||
""" Applies this auth result to the auth context and Flask-Principal. """
|
||||
return self._get_validated().apply_to_request_context()
|
||||
|
||||
def to_signed_dict(self):
|
||||
""" Serializes the auth context into a dictionary suitable for inclusion in a JWT or other
|
||||
form of signed serialization.
|
||||
"""
|
||||
return self.signed_data
|
|
@ -15,7 +15,7 @@ def has_basic_auth(username):
|
|||
"""
|
||||
auth_header = request.headers.get('authorization', '')
|
||||
result = validate_basic_auth(auth_header)
|
||||
return result.has_user and result.user.username == username
|
||||
return result.has_nonrobot_user and result.context.user.username == username
|
||||
|
||||
|
||||
def validate_basic_auth(auth_header):
|
||||
|
|
203
auth/context_entity.py
Normal file
203
auth/context_entity.py
Normal file
|
@ -0,0 +1,203 @@
|
|||
from abc import ABCMeta, abstractmethod
|
||||
from six import add_metaclass
|
||||
from enum import Enum
|
||||
|
||||
from data import model
|
||||
|
||||
from auth.credential_consts import (ACCESS_TOKEN_USERNAME, OAUTH_TOKEN_USERNAME,
|
||||
APP_SPECIFIC_TOKEN_USERNAME)
|
||||
|
||||
class ContextEntityKind(Enum):
|
||||
""" Defines the various kinds of entities in an auth context. Note that the string values of
|
||||
these fields *must* match the names of the fields in the ValidatedAuthContext class, as
|
||||
we fill them in directly based on the string names here.
|
||||
"""
|
||||
anonymous = 'anonymous'
|
||||
user = 'user'
|
||||
robot = 'robot'
|
||||
token = 'token'
|
||||
oauthtoken = 'oauthtoken'
|
||||
appspecifictoken = 'appspecifictoken'
|
||||
signed_data = 'signed_data'
|
||||
|
||||
|
||||
@add_metaclass(ABCMeta)
|
||||
class ContextEntityHandler(object):
|
||||
"""
|
||||
Interface that represents handling specific kinds of entities under an auth context.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def credential_username(self, entity_reference):
|
||||
""" Returns the username to create credentials for this entity, if any. """
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_serialized_entity_reference(self, entity_reference):
|
||||
""" Returns the entity reference for this kind of auth context, serialized into a form that can
|
||||
be placed into a JSON object and put into a JWT. This is typically a DB UUID or another
|
||||
unique identifier for the object in the DB.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def deserialize_entity_reference(self, serialized_entity_reference):
|
||||
""" Returns the deserialized reference to the entity in the database, or None if none. """
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def description(self, entity_reference):
|
||||
""" Returns a human-readable and *public* description of the current entity. """
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def analytics_id_and_public_metadata(self, entity_reference):
|
||||
""" Returns the analyitics ID and a dict of public metadata for the current entity. """
|
||||
pass
|
||||
|
||||
|
||||
class AnonymousEntityHandler(ContextEntityHandler):
|
||||
def credential_username(self, entity_reference):
|
||||
return None
|
||||
|
||||
def get_serialized_entity_reference(self, entity_reference):
|
||||
return None
|
||||
|
||||
def deserialize_entity_reference(self, serialized_entity_reference):
|
||||
return None
|
||||
|
||||
def description(self, entity_reference):
|
||||
return "anonymous"
|
||||
|
||||
def analytics_id_and_public_metadata(self, entity_reference):
|
||||
return "anonymous", {}
|
||||
|
||||
|
||||
class UserEntityHandler(ContextEntityHandler):
|
||||
def credential_username(self, entity_reference):
|
||||
return entity_reference.username
|
||||
|
||||
def get_serialized_entity_reference(self, entity_reference):
|
||||
return entity_reference.uuid
|
||||
|
||||
def deserialize_entity_reference(self, serialized_entity_reference):
|
||||
return model.user.get_user_by_uuid(serialized_entity_reference)
|
||||
|
||||
def description(self, entity_reference):
|
||||
return "user %s" % entity_reference.username
|
||||
|
||||
def analytics_id_and_public_metadata(self, entity_reference):
|
||||
return entity_reference.username, {
|
||||
'username': entity_reference.username,
|
||||
}
|
||||
|
||||
|
||||
class RobotEntityHandler(ContextEntityHandler):
|
||||
def credential_username(self, entity_reference):
|
||||
return entity_reference.username
|
||||
|
||||
def get_serialized_entity_reference(self, entity_reference):
|
||||
return entity_reference.username
|
||||
|
||||
def deserialize_entity_reference(self, serialized_entity_reference):
|
||||
return model.user.lookup_robot(serialized_entity_reference)
|
||||
|
||||
def description(self, entity_reference):
|
||||
return "robot %s" % entity_reference.username
|
||||
|
||||
def analytics_id_and_public_metadata(self, entity_reference):
|
||||
return entity_reference.username, {
|
||||
'username': entity_reference.username,
|
||||
'is_robot': True,
|
||||
}
|
||||
|
||||
|
||||
class TokenEntityHandler(ContextEntityHandler):
|
||||
def credential_username(self, entity_reference):
|
||||
return ACCESS_TOKEN_USERNAME
|
||||
|
||||
def get_serialized_entity_reference(self, entity_reference):
|
||||
return entity_reference.code
|
||||
|
||||
def deserialize_entity_reference(self, serialized_entity_reference):
|
||||
return model.token.load_token_data(serialized_entity_reference)
|
||||
|
||||
def description(self, entity_reference):
|
||||
return "token %s" % entity_reference.friendly_name
|
||||
|
||||
def analytics_id_and_public_metadata(self, entity_reference):
|
||||
return 'token:%s' % entity_reference.id, {
|
||||
'token': entity_reference.friendly_name,
|
||||
}
|
||||
|
||||
|
||||
class OAuthTokenEntityHandler(ContextEntityHandler):
|
||||
def credential_username(self, entity_reference):
|
||||
return OAUTH_TOKEN_USERNAME
|
||||
|
||||
def get_serialized_entity_reference(self, entity_reference):
|
||||
return entity_reference.uuid
|
||||
|
||||
def deserialize_entity_reference(self, serialized_entity_reference):
|
||||
return model.oauth.lookup_access_token_by_uuid(serialized_entity_reference)
|
||||
|
||||
def description(self, entity_reference):
|
||||
return "oauthtoken for user %s" % entity_reference.authorized_user.username
|
||||
|
||||
def analytics_id_and_public_metadata(self, entity_reference):
|
||||
return 'oauthtoken:%s' % entity_reference.id, {
|
||||
'oauth_token_id': entity_reference.id,
|
||||
'oauth_token_application_id': entity_reference.application.client_id,
|
||||
'oauth_token_application': entity_reference.application.name,
|
||||
'username': entity_reference.authorized_user.username,
|
||||
}
|
||||
|
||||
|
||||
class AppSpecificTokenEntityHandler(ContextEntityHandler):
|
||||
def credential_username(self, entity_reference):
|
||||
return APP_SPECIFIC_TOKEN_USERNAME
|
||||
|
||||
def get_serialized_entity_reference(self, entity_reference):
|
||||
return entity_reference.uuid
|
||||
|
||||
def deserialize_entity_reference(self, serialized_entity_reference):
|
||||
return model.appspecifictoken.get_token_by_uuid(serialized_entity_reference)
|
||||
|
||||
def description(self, entity_reference):
|
||||
tpl = (entity_reference.title, entity_reference.user.username)
|
||||
return "app specific token %s for user %s" % tpl
|
||||
|
||||
def analytics_id_and_public_metadata(self, entity_reference):
|
||||
return 'appspecifictoken:%s' % entity_reference.id, {
|
||||
'app_specific_token': entity_reference.uuid,
|
||||
'app_specific_token_title': entity_reference.title,
|
||||
'username': entity_reference.user.username,
|
||||
}
|
||||
|
||||
|
||||
class SignedDataEntityHandler(ContextEntityHandler):
|
||||
def credential_username(self, entity_reference):
|
||||
return None
|
||||
|
||||
def get_serialized_entity_reference(self, entity_reference):
|
||||
raise NotImplementedError
|
||||
|
||||
def deserialize_entity_reference(self, serialized_entity_reference):
|
||||
raise NotImplementedError
|
||||
|
||||
def description(self, entity_reference):
|
||||
return "signed"
|
||||
|
||||
def analytics_id_and_public_metadata(self, entity_reference):
|
||||
return 'signed', {'signed': entity_reference}
|
||||
|
||||
|
||||
CONTEXT_ENTITY_HANDLERS = {
|
||||
ContextEntityKind.anonymous: AnonymousEntityHandler,
|
||||
ContextEntityKind.user: UserEntityHandler,
|
||||
ContextEntityKind.robot: RobotEntityHandler,
|
||||
ContextEntityKind.token: TokenEntityHandler,
|
||||
ContextEntityKind.oauthtoken: OAuthTokenEntityHandler,
|
||||
ContextEntityKind.appspecifictoken: AppSpecificTokenEntityHandler,
|
||||
ContextEntityKind.signed_data: SignedDataEntityHandler,
|
||||
}
|
3
auth/credential_consts.py
Normal file
3
auth/credential_consts.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
ACCESS_TOKEN_USERNAME = '$token'
|
||||
OAUTH_TOKEN_USERNAME = '$oauthtoken'
|
||||
APP_SPECIFIC_TOKEN_USERNAME = '$app'
|
|
@ -7,15 +7,13 @@ import features
|
|||
from app import authentication
|
||||
from auth.oauth import validate_oauth_token
|
||||
from auth.validateresult import ValidateResult, AuthKind
|
||||
from auth.credential_consts import (ACCESS_TOKEN_USERNAME, OAUTH_TOKEN_USERNAME,
|
||||
APP_SPECIFIC_TOKEN_USERNAME)
|
||||
from data import model
|
||||
from util.names import parse_robot_username
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ACCESS_TOKEN_USERNAME = '$token'
|
||||
OAUTH_TOKEN_USERNAME = '$oauthtoken'
|
||||
APP_SPECIFIC_TOKEN_USERNAME = '$app'
|
||||
|
||||
|
||||
class CredentialKind(Enum):
|
||||
user = 'user'
|
||||
|
|
|
@ -69,7 +69,7 @@ def require_session_login(func):
|
|||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
result = validate_session_cookie()
|
||||
if result.has_user:
|
||||
if result.has_nonrobot_user:
|
||||
result.apply_to_context()
|
||||
metric_queue.authentication_count.Inc(labelvalues=[result.kind, True])
|
||||
return func(*args, **kwargs)
|
||||
|
|
|
@ -7,21 +7,18 @@ from flask import request, url_for
|
|||
from flask_principal import identity_changed, Identity
|
||||
|
||||
from app import app, get_app_url, instance_keys, metric_queue
|
||||
from auth.auth_context import (set_grant_context, get_grant_context)
|
||||
from auth.auth_context import set_authenticated_context
|
||||
from auth.auth_context_type import SignedAuthContext
|
||||
from auth.permissions import repository_read_grant, repository_write_grant, repository_admin_grant
|
||||
from util.http import abort
|
||||
from util.names import parse_namespace_repository
|
||||
from util.security.registry_jwt import (ANONYMOUS_SUB, decode_bearer_header,
|
||||
InvalidBearerTokenException)
|
||||
from data import model
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CONTEXT_KINDS = ['user', 'token', 'oauth', 'app_specific_token']
|
||||
|
||||
|
||||
ACCESS_SCHEMA = {
|
||||
'type': 'array',
|
||||
'description': 'List of access granted to the subject',
|
||||
|
@ -65,71 +62,6 @@ class InvalidJWTException(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class GrantedEntity(object):
|
||||
def __init__(self, user=None, token=None, oauth=None, app_specific_token=None):
|
||||
self.user = user
|
||||
self.token = token
|
||||
self.oauth = oauth
|
||||
self.app_specific_token = app_specific_token
|
||||
|
||||
|
||||
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 == '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)
|
||||
|
||||
if kind == 'user':
|
||||
user = model.user.get_user(context.get('user', ''))
|
||||
if not user:
|
||||
return None
|
||||
|
||||
return GrantedEntity(user=user)
|
||||
|
||||
if kind == 'token':
|
||||
token = model.token.load_token_data(context.get('token'))
|
||||
if not token:
|
||||
return None
|
||||
|
||||
return GrantedEntity(token=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 get_auth_headers(repository=None, scopes=None):
|
||||
""" Returns a dictionary of headers for auth responses. """
|
||||
headers = {}
|
||||
|
@ -198,6 +130,9 @@ def identity_from_bearer_token(bearer_header):
|
|||
|
||||
|
||||
def process_registry_jwt_auth(scopes=None):
|
||||
""" Processes the registry JWT auth token found in the authorization header. If none found,
|
||||
no error is returned. If an invalid token is found, raises a 401.
|
||||
"""
|
||||
def inner(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
|
@ -205,10 +140,15 @@ def process_registry_jwt_auth(scopes=None):
|
|||
auth = request.headers.get('authorization', '').strip()
|
||||
if auth:
|
||||
try:
|
||||
extracted_identity, context = identity_from_bearer_token(auth)
|
||||
extracted_identity, context_dict = identity_from_bearer_token(auth)
|
||||
identity_changed.send(app, identity=extracted_identity)
|
||||
set_grant_context(context)
|
||||
logger.debug('Identity changed to %s', extracted_identity.id)
|
||||
|
||||
auth_context = SignedAuthContext.build_from_signed_dict(context_dict)
|
||||
if auth_context is not None:
|
||||
logger.debug('Auth context set to %s', auth_context.signed_data)
|
||||
set_authenticated_context(auth_context)
|
||||
|
||||
except InvalidJWTException as ije:
|
||||
repository = None
|
||||
if 'namespace_name' in kwargs and 'repo_name' in kwargs:
|
||||
|
|
44
auth/test/test_auth_context_type.py
Normal file
44
auth/test/test_auth_context_type.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
import pytest
|
||||
|
||||
from auth.auth_context_type import SignedAuthContext, ValidatedAuthContext, ContextEntityKind
|
||||
from data import model
|
||||
|
||||
from test.fixtures import *
|
||||
|
||||
@pytest.mark.parametrize('kind, entity_reference, loader', [
|
||||
(ContextEntityKind.anonymous, None, None),
|
||||
(ContextEntityKind.appspecifictoken, 'test', model.appspecifictoken.access_valid_token),
|
||||
(ContextEntityKind.oauthtoken, 'test', model.oauth.validate_access_token),
|
||||
(ContextEntityKind.robot, 'devtable+dtrobot', model.user.lookup_robot),
|
||||
(ContextEntityKind.user, 'devtable', model.user.get_user),
|
||||
])
|
||||
@pytest.mark.parametrize('v1_dict_format', [
|
||||
(True),
|
||||
(False),
|
||||
])
|
||||
def test_signed_auth_context(kind, entity_reference, loader, v1_dict_format, initialized_db):
|
||||
if kind == ContextEntityKind.anonymous:
|
||||
validated = ValidatedAuthContext()
|
||||
assert validated.is_anonymous
|
||||
else:
|
||||
ref = loader(entity_reference)
|
||||
validated = ValidatedAuthContext(**{kind.value: ref})
|
||||
assert not validated.is_anonymous
|
||||
|
||||
assert validated.entity_kind == kind
|
||||
|
||||
signed = SignedAuthContext.build_from_signed_dict(validated.to_signed_dict(),
|
||||
v1_dict_format=v1_dict_format)
|
||||
|
||||
if not v1_dict_format:
|
||||
# Under legacy V1 format, we don't track the app specific token, merely its associated user.
|
||||
assert signed.entity_kind == kind
|
||||
assert signed.description == validated.description
|
||||
assert signed.credential_username == validated.credential_username
|
||||
assert signed.analytics_id_and_public_metadata() == validated.analytics_id_and_public_metadata()
|
||||
|
||||
assert signed.is_anonymous == validated.is_anonymous
|
||||
assert signed.authed_user == validated.authed_user
|
||||
assert signed.has_nonrobot_user == validated.has_nonrobot_user
|
||||
|
||||
assert signed.to_signed_dict() == validated.to_signed_dict()
|
|
@ -20,8 +20,8 @@ def test_invalidformatted_cookie(app):
|
|||
# Ensure we get an invalid session cookie format error.
|
||||
result = validate_session_cookie()
|
||||
assert result.authed_user is None
|
||||
assert result.identity is None
|
||||
assert not result.has_user
|
||||
assert result.context.identity is None
|
||||
assert not result.has_nonrobot_user
|
||||
assert result.error_message == 'Invalid session cookie format'
|
||||
|
||||
|
||||
|
@ -33,8 +33,8 @@ def test_disabled_user(app):
|
|||
# Ensure we get an invalid session cookie format error.
|
||||
result = validate_session_cookie()
|
||||
assert result.authed_user is None
|
||||
assert result.identity is None
|
||||
assert not result.has_user
|
||||
assert result.context.identity is None
|
||||
assert not result.has_nonrobot_user
|
||||
assert result.error_message == 'User account is disabled'
|
||||
|
||||
|
||||
|
@ -45,8 +45,8 @@ def test_valid_user(app):
|
|||
|
||||
result = validate_session_cookie()
|
||||
assert result.authed_user == someuser
|
||||
assert result.identity is not None
|
||||
assert result.has_user
|
||||
assert result.context.identity is not None
|
||||
assert result.has_nonrobot_user
|
||||
assert result.error_message is None
|
||||
|
||||
|
||||
|
@ -61,6 +61,6 @@ def test_valid_organization(app):
|
|||
|
||||
result = validate_session_cookie()
|
||||
assert result.authed_user is None
|
||||
assert result.identity is None
|
||||
assert not result.has_user
|
||||
assert result.context.identity is None
|
||||
assert not result.has_nonrobot_user
|
||||
assert result.error_message == 'Cannot login to organization'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from auth.credentials import (ACCESS_TOKEN_USERNAME, OAUTH_TOKEN_USERNAME, validate_credentials,
|
||||
CredentialKind, APP_SPECIFIC_TOKEN_USERNAME)
|
||||
from auth.credentials import validate_credentials, CredentialKind
|
||||
from auth.credential_consts import (ACCESS_TOKEN_USERNAME, OAUTH_TOKEN_USERNAME,
|
||||
APP_SPECIFIC_TOKEN_USERNAME)
|
||||
from auth.validateresult import AuthKind, ValidateResult
|
||||
from data import model
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ def test_valid_oauth(app):
|
|||
token = list(model.oauth.list_access_tokens_for_user(user))[0]
|
||||
|
||||
result = validate_bearer_auth('bearer ' + token.access_token)
|
||||
assert result.oauthtoken == token
|
||||
assert result.context.oauthtoken == token
|
||||
assert result.authed_user == user
|
||||
assert result.auth_valid
|
||||
|
||||
|
@ -32,7 +32,7 @@ def test_disabled_user_oauth(app):
|
|||
access_token='foo')
|
||||
|
||||
result = validate_bearer_auth('bearer ' + token.access_token)
|
||||
assert result.oauthtoken is None
|
||||
assert result.context.oauthtoken is None
|
||||
assert result.authed_user is None
|
||||
assert not result.auth_valid
|
||||
assert result.error_message == 'Granter of the oauth access token is disabled'
|
||||
|
@ -44,7 +44,7 @@ def test_expired_token(app):
|
|||
access_token='bar', expires_in=-1000)
|
||||
|
||||
result = validate_bearer_auth('bearer ' + token.access_token)
|
||||
assert result.oauthtoken is None
|
||||
assert result.context.oauthtoken is None
|
||||
assert result.authed_user is None
|
||||
assert not result.auth_valid
|
||||
assert result.error_message == 'OAuth access token has expired'
|
||||
|
|
|
@ -4,6 +4,7 @@ import jwt
|
|||
import pytest
|
||||
|
||||
from app import app, instance_keys
|
||||
from auth.auth_context_type import ValidatedAuthContext
|
||||
from auth.registry_jwt_auth import identity_from_bearer_token, InvalidJWTException
|
||||
from data import model # TODO(jzelinskie): remove this after service keys are decoupled
|
||||
from data.database import ServiceKeyApprovalType
|
||||
|
@ -12,7 +13,7 @@ from util.morecollections import AttrDict
|
|||
from util.security.registry_jwt import ANONYMOUS_SUB, build_context_and_subject
|
||||
|
||||
TEST_AUDIENCE = app.config['SERVER_HOSTNAME']
|
||||
TEST_USER = AttrDict({'username': 'joeuser'})
|
||||
TEST_USER = AttrDict({'username': 'joeuser', 'uuid': 'foobar', 'enabled': True})
|
||||
MAX_SIGNED_S = 3660
|
||||
TOKEN_VALIDITY_LIFETIME_S = 60 * 60 # 1 hour
|
||||
ANONYMOUS_SUB = '(anonymous)'
|
||||
|
@ -27,7 +28,8 @@ def _access(typ='repository', name='somens/somerepo', actions=None):
|
|||
return [{
|
||||
'type': typ,
|
||||
'name': name,
|
||||
'actions': actions,}]
|
||||
'actions': actions,
|
||||
}]
|
||||
|
||||
|
||||
def _delete_field(token_data, field_name):
|
||||
|
@ -38,7 +40,7 @@ def _delete_field(token_data, field_name):
|
|||
def _token_data(access=[], context=None, audience=TEST_AUDIENCE, user=TEST_USER, iat=None,
|
||||
exp=None, nbf=None, iss=None, subject=None):
|
||||
if subject is None:
|
||||
_, subject = build_context_and_subject(user=user)
|
||||
_, subject = build_context_and_subject(ValidatedAuthContext(user=user))
|
||||
return {
|
||||
'iss': iss or instance_keys.service_name,
|
||||
'aud': audience,
|
||||
|
@ -47,7 +49,8 @@ def _token_data(access=[], context=None, audience=TEST_AUDIENCE, user=TEST_USER,
|
|||
'exp': exp if exp is not None else int(time.time() + TOKEN_VALIDITY_LIFETIME_S),
|
||||
'sub': subject,
|
||||
'access': access,
|
||||
'context': context,}
|
||||
'context': context,
|
||||
}
|
||||
|
||||
|
||||
def _token(token_data, key_id=None, private_key=None, skip_header=False, alg=None):
|
||||
|
|
|
@ -1,45 +1,40 @@
|
|||
import pytest
|
||||
|
||||
from auth.auth_context import (
|
||||
get_authenticated_user, get_grant_context, get_validated_token, get_validated_oauth_token)
|
||||
from auth.auth_context import get_authenticated_context
|
||||
from auth.validateresult import AuthKind, ValidateResult
|
||||
from data import model
|
||||
from test.fixtures import *
|
||||
|
||||
|
||||
def get_user():
|
||||
return model.user.get_user('devtable')
|
||||
|
||||
def get_app_specific_token():
|
||||
return model.appspecifictoken.access_valid_token('test')
|
||||
|
||||
def get_robot():
|
||||
robot, _ = model.user.create_robot('somebot', get_user())
|
||||
return robot
|
||||
|
||||
|
||||
def get_token():
|
||||
return model.token.create_delegate_token('devtable', 'simple', 'sometoken')
|
||||
|
||||
|
||||
def get_oauthtoken():
|
||||
user = model.user.get_user('devtable')
|
||||
return list(model.oauth.list_access_tokens_for_user(user))[0]
|
||||
|
||||
|
||||
def get_signeddata():
|
||||
return {'grants': {'a': 'b'}, 'user_context': {'c': 'd'}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('get_entity,entity_kind', [
|
||||
(get_user, 'user'),
|
||||
(get_robot, 'robot'),
|
||||
(get_token, 'token'),
|
||||
(get_oauthtoken, 'oauthtoken'),
|
||||
(get_signeddata, 'signed_data'),])
|
||||
(get_signeddata, 'signed_data'),
|
||||
(get_app_specific_token, 'appspecifictoken'),
|
||||
])
|
||||
def test_apply_context(get_entity, entity_kind, app):
|
||||
assert get_authenticated_user() is None
|
||||
assert get_validated_token() is None
|
||||
assert get_validated_oauth_token() is None
|
||||
assert get_grant_context() is None
|
||||
assert get_authenticated_context() is None
|
||||
|
||||
entity = get_entity()
|
||||
args = {}
|
||||
|
@ -52,16 +47,16 @@ def test_apply_context(get_entity, entity_kind, app):
|
|||
if entity_kind == 'oauthtoken':
|
||||
expected_user = entity.authorized_user
|
||||
|
||||
if entity_kind == 'appspecifictoken':
|
||||
expected_user = entity.user
|
||||
|
||||
expected_token = entity if entity_kind == 'token' else None
|
||||
expected_oauth = entity if entity_kind == 'oauthtoken' else None
|
||||
expected_appspecifictoken = entity if entity_kind == 'appspecifictoken' else None
|
||||
expected_grant = entity if entity_kind == 'signed_data' else None
|
||||
|
||||
fake_grant = {
|
||||
'user': {
|
||||
'c': 'd'},
|
||||
'kind': 'user',}
|
||||
expected_grant = fake_grant if entity_kind == 'signed_data' else None
|
||||
|
||||
assert get_authenticated_user() == expected_user
|
||||
assert get_validated_token() == expected_token
|
||||
assert get_validated_oauth_token() == expected_oauth
|
||||
assert get_grant_context() == expected_grant
|
||||
assert get_authenticated_context().authed_user == expected_user
|
||||
assert get_authenticated_context().token == expected_token
|
||||
assert get_authenticated_context().oauthtoken == expected_oauth
|
||||
assert get_authenticated_context().appspecifictoken == expected_appspecifictoken
|
||||
assert get_authenticated_context().signed_data == expected_grant
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
from enum import Enum
|
||||
from flask_principal import Identity, identity_changed
|
||||
|
||||
from app import app
|
||||
from auth.auth_context import (set_authenticated_user, set_validated_token, set_grant_context,
|
||||
set_validated_oauth_token, set_validated_app_specific_token)
|
||||
from auth.scopes import scopes_from_scope_string
|
||||
from auth.permissions import QuayDeferredPermissionUser
|
||||
from auth.auth_context_type import ValidatedAuthContext, ContextEntityKind
|
||||
|
||||
|
||||
class AuthKind(Enum):
|
||||
|
@ -22,94 +16,37 @@ class ValidateResult(object):
|
|||
robot=None, appspecifictoken=None, signed_data=None, error_message=None):
|
||||
self.kind = kind
|
||||
self.missing = missing
|
||||
self.user = user
|
||||
self.robot = robot
|
||||
self.token = token
|
||||
self.oauthtoken = oauthtoken
|
||||
self.appspecifictoken = appspecifictoken
|
||||
self.signed_data = signed_data
|
||||
self.error_message = error_message
|
||||
self.context = ValidatedAuthContext(user=user, token=token, oauthtoken=oauthtoken, robot=robot,
|
||||
appspecifictoken=appspecifictoken, signed_data=signed_data)
|
||||
|
||||
def tuple(self):
|
||||
return (self.kind, self.missing, self.user, self.token, self.oauthtoken, self.robot,
|
||||
self.appspecifictoken, self.signed_data, self.error_message)
|
||||
return (self.kind, self.missing, self.error_message, self.context.tuple())
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.tuple() == other.tuple()
|
||||
|
||||
def apply_to_context(self):
|
||||
""" Applies this auth result to the auth context and Flask-Principal. """
|
||||
# Set the various pieces of the auth context.
|
||||
if self.oauthtoken:
|
||||
set_authenticated_user(self.authed_user)
|
||||
set_validated_oauth_token(self.oauthtoken)
|
||||
elif self.appspecifictoken:
|
||||
set_authenticated_user(self.authed_user)
|
||||
set_validated_app_specific_token(self.appspecifictoken)
|
||||
elif self.authed_user:
|
||||
set_authenticated_user(self.authed_user)
|
||||
elif self.token:
|
||||
set_validated_token(self.token)
|
||||
elif self.signed_data:
|
||||
if self.signed_data['user_context']:
|
||||
set_grant_context({
|
||||
'user': self.signed_data['user_context'],
|
||||
'kind': 'user',
|
||||
})
|
||||
|
||||
# Set the identity for Flask-Principal.
|
||||
if self.identity:
|
||||
identity_changed.send(app, identity=self.identity)
|
||||
self.context.apply_to_request_context()
|
||||
|
||||
def with_kind(self, kind):
|
||||
""" Returns a copy of this result, but with the kind replaced. """
|
||||
return ValidateResult(kind, self.missing, self.user, self.token, self.oauthtoken, self.robot,
|
||||
self.appspecifictoken, self.signed_data, self.error_message)
|
||||
result = ValidateResult(kind, missing=self.missing, error_message=self.error_message)
|
||||
result.context = self.context
|
||||
return result
|
||||
|
||||
@property
|
||||
def authed_user(self):
|
||||
""" Returns the authenticated user, whether directly, or via an OAuth token. """
|
||||
if not self.auth_valid:
|
||||
return None
|
||||
|
||||
if self.oauthtoken:
|
||||
return self.oauthtoken.authorized_user
|
||||
|
||||
if self.appspecifictoken:
|
||||
return self.appspecifictoken.user
|
||||
|
||||
return self.user if self.user else self.robot
|
||||
return self.context.authed_user
|
||||
|
||||
@property
|
||||
def identity(self):
|
||||
""" Returns the identity for the auth result. """
|
||||
if not self.auth_valid:
|
||||
return None
|
||||
|
||||
if self.oauthtoken:
|
||||
scope_set = scopes_from_scope_string(self.oauthtoken.scope)
|
||||
return QuayDeferredPermissionUser.for_user(self.oauthtoken.authorized_user, scope_set)
|
||||
|
||||
if self.authed_user:
|
||||
return QuayDeferredPermissionUser.for_user(self.authed_user)
|
||||
|
||||
if self.token:
|
||||
return Identity(self.token.code, 'token')
|
||||
|
||||
if self.signed_data:
|
||||
identity = Identity(None, 'signed_grant')
|
||||
identity.provides.update(self.signed_data['grants'])
|
||||
return identity
|
||||
|
||||
return None
|
||||
|
||||
@property
|
||||
def has_user(self):
|
||||
def has_nonrobot_user(self):
|
||||
""" Returns whether a user (not a robot) was authenticated successfully. """
|
||||
return bool(self.user)
|
||||
return self.context.has_nonrobot_user
|
||||
|
||||
@property
|
||||
def auth_valid(self):
|
||||
""" Returns whether authentication successfully occurred. """
|
||||
return (self.user or self.token or self.oauthtoken or self.appspecifictoken or self.robot or
|
||||
self.signed_data)
|
||||
return self.context.entity_kind != ContextEntityKind.anonymous
|
||||
|
|
|
@ -255,6 +255,13 @@ def delete_application(org, client_id):
|
|||
return application
|
||||
|
||||
|
||||
def lookup_access_token_by_uuid(token_uuid):
|
||||
try:
|
||||
return OAuthAccessToken.get(OAuthAccessToken.uuid == token_uuid)
|
||||
except OAuthAccessToken.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
def lookup_access_token_for_user(user_obj, token_uuid):
|
||||
try:
|
||||
return OAuthAccessToken.get(OAuthAccessToken.authorized_user == user_obj,
|
||||
|
|
|
@ -15,7 +15,8 @@ from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermissi
|
|||
AdministerRepositoryPermission, UserReadPermission,
|
||||
UserAdminPermission)
|
||||
from auth import scopes
|
||||
from auth.auth_context import get_authenticated_user, get_validated_oauth_token
|
||||
from auth.auth_context import (get_authenticated_context, get_authenticated_user,
|
||||
get_validated_oauth_token)
|
||||
from auth.decorators import process_oauth
|
||||
from endpoints.csrf import csrf_protect
|
||||
from endpoints.exception import (Unauthorized, InvalidRequest, InvalidResponse,
|
||||
|
@ -291,8 +292,7 @@ def require_fresh_login(func):
|
|||
if not user:
|
||||
raise Unauthorized()
|
||||
|
||||
oauth_token = get_validated_oauth_token()
|
||||
if oauth_token:
|
||||
if get_validated_oauth_token():
|
||||
return func(*args, **kwargs)
|
||||
|
||||
logger.debug('Checking fresh login for user %s', user.username)
|
||||
|
|
|
@ -46,8 +46,7 @@ def csrf_protect(session_token_name=_QUAY_CSRF_TOKEN_NAME,
|
|||
def inner(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
oauth_token = get_validated_oauth_token()
|
||||
if oauth_token is None:
|
||||
if get_validated_oauth_token() is None:
|
||||
if all_methods or (request.method != "GET" and request.method != "HEAD"):
|
||||
verify_csrf(session_token_name, request_token_name)
|
||||
|
||||
|
|
|
@ -6,8 +6,7 @@ from flask import abort, request, make_response
|
|||
import features
|
||||
|
||||
from app import app
|
||||
from auth.auth_context import (
|
||||
get_validated_oauth_token, get_authenticated_user, get_validated_token, get_grant_context)
|
||||
from auth.auth_context import get_authenticated_context
|
||||
from util.names import parse_namespace_repository
|
||||
|
||||
|
||||
|
@ -73,8 +72,7 @@ def check_anon_protection(func):
|
|||
return func(*args, **kwargs)
|
||||
|
||||
# Check for validated context. If none exists, fail with a 401.
|
||||
if (get_authenticated_user() or get_validated_oauth_token() or get_validated_token() or
|
||||
get_grant_context()):
|
||||
if get_authenticated_context() and not get_authenticated_context().is_anonymous:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
abort(401)
|
||||
|
|
|
@ -7,8 +7,7 @@ from functools import wraps
|
|||
from flask import request, make_response, jsonify, session
|
||||
|
||||
from app import userevents, metric_queue
|
||||
from auth.auth_context import (get_authenticated_user, get_validated_token,
|
||||
get_validated_oauth_token, get_validated_app_specific_token)
|
||||
from auth.auth_context import get_authenticated_context, get_authenticated_user
|
||||
from auth.credentials import validate_credentials, CredentialKind
|
||||
from auth.decorators import process_auth
|
||||
from auth.permissions import (
|
||||
|
@ -106,7 +105,7 @@ def create_user():
|
|||
# Default case: Just fail.
|
||||
abort(400, result.error_message, issue='login-failure')
|
||||
|
||||
if result.has_user:
|
||||
if result.has_nonrobot_user:
|
||||
# Mark that the user was logged in.
|
||||
event = userevents.get_event(username)
|
||||
event.publish_event_data('docker-cli', {'action': 'login'})
|
||||
|
@ -119,27 +118,14 @@ def create_user():
|
|||
@process_auth
|
||||
@anon_allowed
|
||||
def get_user():
|
||||
if get_validated_oauth_token():
|
||||
return jsonify({
|
||||
'username': '$oauthtoken',
|
||||
'email': None,
|
||||
})
|
||||
elif get_validated_app_specific_token():
|
||||
return jsonify({
|
||||
'username': "$app",
|
||||
'email': None,
|
||||
})
|
||||
elif get_authenticated_user():
|
||||
return jsonify({
|
||||
'username': get_authenticated_user().username,
|
||||
'email': get_authenticated_user().email,
|
||||
})
|
||||
elif get_validated_token():
|
||||
return jsonify({
|
||||
'username': '$token',
|
||||
'email': None,
|
||||
})
|
||||
abort(404)
|
||||
context = get_authenticated_context()
|
||||
if not context or context.is_anonymous:
|
||||
abort(404)
|
||||
|
||||
return jsonify({
|
||||
'username': context.credential_username,
|
||||
'email': None,
|
||||
})
|
||||
|
||||
|
||||
@v1_bp.route('/users/<username>/', methods=['PUT'])
|
||||
|
|
|
@ -11,7 +11,6 @@ from app import storage as store, app, metric_queue
|
|||
from auth.auth_context import get_authenticated_user
|
||||
from auth.decorators import extract_namespace_repo_from_session, process_auth
|
||||
from auth.permissions import (ReadRepositoryPermission, ModifyRepositoryPermission)
|
||||
from auth.registry_jwt_auth import get_granted_username
|
||||
from data import model, database
|
||||
from digest import checksums
|
||||
from endpoints.v1 import v1_bp
|
||||
|
@ -433,9 +432,6 @@ def put_image_json(namespace, repository, image_id):
|
|||
v1_metadata = model.docker_v1_metadata(namespace, repository, image_id)
|
||||
if v1_metadata is None:
|
||||
username = get_authenticated_user() and get_authenticated_user().username
|
||||
if not username:
|
||||
username = get_granted_username()
|
||||
|
||||
logger.debug('Image not found, creating or linking image with initiating user context: %s',
|
||||
username)
|
||||
location_pref = store.preferred_locations[0]
|
||||
|
|
|
@ -11,7 +11,7 @@ from semantic_version import Spec
|
|||
import features
|
||||
|
||||
from app import app, metric_queue, get_app_url, license_validator
|
||||
from auth.auth_context import get_grant_context
|
||||
from auth.auth_context import get_authenticated_context
|
||||
from auth.permissions import (
|
||||
ReadRepositoryPermission, ModifyRepositoryPermission, AdministerRepositoryPermission)
|
||||
from auth.registry_jwt_auth import process_registry_jwt_auth, get_auth_headers
|
||||
|
@ -146,7 +146,7 @@ def v2_support_enabled():
|
|||
|
||||
response = make_response('true', 200)
|
||||
|
||||
if get_grant_context() is None:
|
||||
if get_authenticated_context() is None:
|
||||
response = make_response('true', 401)
|
||||
|
||||
response.headers.extend(get_auth_headers())
|
||||
|
|
|
@ -2,7 +2,8 @@ import features
|
|||
|
||||
from flask import jsonify
|
||||
|
||||
from auth.registry_jwt_auth import process_registry_jwt_auth, get_granted_entity
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth.registry_jwt_auth import process_registry_jwt_auth
|
||||
from endpoints.decorators import anon_protect
|
||||
from endpoints.v2 import v2_bp, paginate
|
||||
from endpoints.v2.models_pre_oci import data_model as model
|
||||
|
@ -13,11 +14,7 @@ from endpoints.v2.models_pre_oci import data_model as model
|
|||
@anon_protect
|
||||
@paginate()
|
||||
def catalog_search(limit, offset, pagination_callback):
|
||||
username = None
|
||||
entity = get_granted_entity()
|
||||
if entity:
|
||||
username = entity.user.username
|
||||
|
||||
username = get_authenticated_user().username if get_authenticated_user() else None
|
||||
include_public = bool(features.PUBLIC_CATALOG)
|
||||
visible_repositories = model.get_visible_repositories(username, limit + 1, offset,
|
||||
include_public=include_public)
|
||||
|
|
|
@ -6,6 +6,7 @@ from flask import url_for
|
|||
from playhouse.test_utils import assert_query_count
|
||||
|
||||
from app import instance_keys, app as realapp
|
||||
from auth.auth_context_type import ValidatedAuthContext
|
||||
from data import model
|
||||
from data.cache import InMemoryDataModelCache
|
||||
from data.database import ImageStorageLocation
|
||||
|
@ -34,7 +35,7 @@ def test_blob_caching(method, endpoint, client, app):
|
|||
'actions': ['pull'],
|
||||
}]
|
||||
|
||||
context, subject = build_context_and_subject(user=user)
|
||||
context, subject = build_context_and_subject(ValidatedAuthContext(user=user))
|
||||
token = generate_bearer_token(realapp.config['SERVER_HOSTNAME'], subject, context, access, 600,
|
||||
instance_keys)
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ from flask import url_for
|
|||
from playhouse.test_utils import count_queries
|
||||
|
||||
from app import instance_keys, app as realapp
|
||||
from auth.auth_context_type import ValidatedAuthContext
|
||||
from data import model
|
||||
from endpoints.test.shared import conduct_call
|
||||
from util.security.registry_jwt import generate_bearer_token, build_context_and_subject
|
||||
|
@ -28,7 +29,7 @@ def test_e2e_query_count_manifest_norewrite(client, app):
|
|||
'actions': ['pull', 'push'],
|
||||
}]
|
||||
|
||||
context, subject = build_context_and_subject(user=user)
|
||||
context, subject = build_context_and_subject(ValidatedAuthContext(user=user))
|
||||
token = generate_bearer_token(realapp.config['SERVER_HOSTNAME'], subject, context, access, 600,
|
||||
instance_keys)
|
||||
|
||||
|
|
|
@ -2,12 +2,11 @@ import logging
|
|||
import re
|
||||
|
||||
from cachetools import lru_cache
|
||||
from flask import request, jsonify, abort
|
||||
from flask import request, jsonify
|
||||
|
||||
import features
|
||||
from app import app, userevents, instance_keys
|
||||
from auth.auth_context import (get_authenticated_user, get_validated_token,
|
||||
get_validated_oauth_token, get_validated_app_specific_token)
|
||||
from auth.auth_context import get_authenticated_context, get_authenticated_user
|
||||
from auth.decorators import process_basic_auth
|
||||
from auth.permissions import (ModifyRepositoryPermission, ReadRepositoryPermission,
|
||||
CreateRepositoryPermission, AdministerRepositoryPermission)
|
||||
|
@ -48,21 +47,14 @@ def generate_registry_jwt(auth_result):
|
|||
scope_param = request.args.get('scope') or ''
|
||||
logger.debug('Scope request: %s', scope_param)
|
||||
|
||||
user = get_authenticated_user()
|
||||
logger.debug('Authenticated user: %s', user)
|
||||
|
||||
token = get_validated_token()
|
||||
logger.debug('Authenticated token: %s', token)
|
||||
|
||||
oauthtoken = get_validated_oauth_token()
|
||||
logger.debug('Authenticated OAuth token: %s', oauthtoken)
|
||||
|
||||
appspecifictoken = get_validated_app_specific_token()
|
||||
logger.debug('Authenticated app specific token: %s', appspecifictoken)
|
||||
|
||||
auth_header = request.headers.get('authorization', '')
|
||||
auth_credentials_sent = bool(auth_header)
|
||||
if auth_credentials_sent and not user and not token:
|
||||
|
||||
has_valid_auth_context = False
|
||||
if get_authenticated_context():
|
||||
has_valid_auth_context = not get_authenticated_context().is_anonymous
|
||||
|
||||
if auth_credentials_sent and not has_valid_auth_context:
|
||||
# The auth credentials sent for the user are invalid.
|
||||
raise InvalidLogin(auth_result.error_message)
|
||||
|
||||
|
@ -109,9 +101,9 @@ def generate_registry_jwt(auth_result):
|
|||
'This repository is for managing %s resources ' + 'and not container images.') % repo.kind)
|
||||
|
||||
if 'push' in actions:
|
||||
# If there is no valid user or token, then the repository cannot be
|
||||
# Check if there is a valid user or token, as otherwise the repository cannot be
|
||||
# accessed.
|
||||
if user is not None or token is not None:
|
||||
if has_valid_auth_context:
|
||||
# Lookup the repository. If it exists, make sure the entity has modify
|
||||
# permission. Otherwise, make sure the entity has create permission.
|
||||
if repo:
|
||||
|
@ -123,6 +115,7 @@ def generate_registry_jwt(auth_result):
|
|||
else:
|
||||
logger.debug('No permission to modify repository %s/%s', namespace, reponame)
|
||||
else:
|
||||
user = get_authenticated_user()
|
||||
if CreateRepositoryPermission(namespace).can() and user is not None:
|
||||
logger.debug('Creating repository: %s/%s', namespace, reponame)
|
||||
model.create_repository(namespace, reponame, user)
|
||||
|
@ -172,19 +165,18 @@ def generate_registry_jwt(auth_result):
|
|||
}
|
||||
tuf_root = get_tuf_root(repo, namespace, reponame)
|
||||
|
||||
elif user is None and token is None:
|
||||
elif not has_valid_auth_context:
|
||||
# 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')
|
||||
raise Unauthorized()
|
||||
|
||||
# Send the user event.
|
||||
if user is not None:
|
||||
event = userevents.get_event(user.username)
|
||||
if get_authenticated_user() is not None:
|
||||
event = userevents.get_event(get_authenticated_user().username)
|
||||
event.publish_event_data('docker-cli', user_event_data)
|
||||
|
||||
# Build the signed JWT.
|
||||
context, subject = build_context_and_subject(user=user, token=token, oauthtoken=oauthtoken,
|
||||
appspecifictoken=appspecifictoken, tuf_root=tuf_root)
|
||||
# Build the signed JWT.
|
||||
context, subject = build_context_and_subject(get_authenticated_context(), tuf_root=tuf_root)
|
||||
token = generate_bearer_token(audience_param, subject, context, access,
|
||||
TOKEN_VALIDITY_LIFETIME_S, instance_keys)
|
||||
return jsonify({'token': token})
|
||||
|
|
|
@ -7,9 +7,7 @@ from flask import request
|
|||
|
||||
from app import analytics, userevents, ip_resolver
|
||||
from data import model
|
||||
from auth.registry_jwt_auth import get_granted_entity
|
||||
from auth.auth_context import (get_authenticated_user, get_validated_token,
|
||||
get_validated_oauth_token, get_validated_app_specific_token)
|
||||
from auth.auth_context import get_authenticated_context, get_authenticated_user
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -22,51 +20,16 @@ def track_and_log(event_name, repo_obj, analytics_name=None, analytics_sample=1,
|
|||
}
|
||||
metadata.update(kwargs)
|
||||
|
||||
# Add auth context metadata.
|
||||
analytics_id = 'anonymous'
|
||||
|
||||
authenticated_oauth_token = get_validated_oauth_token()
|
||||
authenticated_user = get_authenticated_user()
|
||||
authenticated_token = get_validated_token() if not authenticated_user else None
|
||||
app_specific_token = get_validated_app_specific_token()
|
||||
|
||||
if (not authenticated_user and not authenticated_token and not authenticated_oauth_token and
|
||||
not app_specific_token):
|
||||
entity = get_granted_entity()
|
||||
if entity:
|
||||
authenticated_user = entity.user
|
||||
authenticated_token = entity.token
|
||||
authenticated_oauth_token = entity.oauth
|
||||
app_specific_token = entity.app_specific_token
|
||||
|
||||
logger.debug('Logging the %s to Mixpanel and the log system', event_name)
|
||||
if authenticated_oauth_token:
|
||||
metadata['oauth_token_id'] = authenticated_oauth_token.id
|
||||
metadata['oauth_token_application_id'] = authenticated_oauth_token.application.client_id
|
||||
metadata['oauth_token_application'] = authenticated_oauth_token.application.name
|
||||
metadata['username'] = authenticated_user.username
|
||||
analytics_id = 'oauth:{0}'.format(authenticated_oauth_token.id)
|
||||
elif app_specific_token:
|
||||
metadata['app_specific_token'] = app_specific_token.uuid
|
||||
metadata['username'] = authenticated_user.username
|
||||
analytics_id = 'appspecifictoken:{0}'.format(app_specific_token.uuid)
|
||||
elif authenticated_user:
|
||||
metadata['username'] = authenticated_user.username
|
||||
analytics_id = authenticated_user.username
|
||||
elif authenticated_token:
|
||||
metadata['token'] = authenticated_token.friendly_name
|
||||
metadata['token_code'] = authenticated_token.code
|
||||
|
||||
if authenticated_token.kind:
|
||||
metadata['token_type'] = authenticated_token.kind.name
|
||||
|
||||
analytics_id = 'token:{0}'.format(authenticated_token.code)
|
||||
else:
|
||||
metadata['public'] = True
|
||||
analytics_id = 'anonymous'
|
||||
auth_context = get_authenticated_context()
|
||||
if auth_context is not None:
|
||||
analytics_id, context_metadata = auth_context.analytics_id_and_public_metadata()
|
||||
metadata.update(context_metadata)
|
||||
|
||||
# Publish the user event (if applicable)
|
||||
logger.debug('Checking publishing %s to the user events system', event_name)
|
||||
if authenticated_user and not authenticated_user.robot:
|
||||
if auth_context and auth_context.has_nonrobot_user:
|
||||
logger.debug('Publishing %s to the user events system', event_name)
|
||||
user_event_data = {
|
||||
'action': event_name,
|
||||
|
@ -74,7 +37,7 @@ def track_and_log(event_name, repo_obj, analytics_name=None, analytics_sample=1,
|
|||
'namespace': namespace_name,
|
||||
}
|
||||
|
||||
event = userevents.get_event(authenticated_user.username)
|
||||
event = userevents.get_event(auth_context.authed_user.username)
|
||||
event.publish_event_data('docker-cli', user_event_data)
|
||||
|
||||
# Save the action to mixpanel.
|
||||
|
@ -100,6 +63,6 @@ def track_and_log(event_name, repo_obj, analytics_name=None, analytics_sample=1,
|
|||
|
||||
# Log the action to the database.
|
||||
logger.debug('Logging the %s to logs system', event_name)
|
||||
model.log.log_action(event_name, namespace_name, performer=authenticated_user,
|
||||
model.log.log_action(event_name, namespace_name, performer=get_authenticated_user(),
|
||||
ip=request.remote_addr, metadata=metadata, repository=repo_obj)
|
||||
logger.debug('Track and log of %s complete', event_name)
|
||||
|
|
14
util/http.py
14
util/http.py
|
@ -5,7 +5,7 @@ from flask import request, make_response, current_app
|
|||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from app import analytics
|
||||
from auth.auth_context import get_authenticated_user, get_validated_token
|
||||
from auth.auth_context import get_authenticated_context
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -58,15 +58,9 @@ def abort(status_code, message=None, issue=None, headers=None, **kwargs):
|
|||
params['message'] = message
|
||||
|
||||
# Add the user information.
|
||||
auth_user = get_authenticated_user()
|
||||
auth_token = get_validated_token()
|
||||
if auth_user:
|
||||
analytics.track(auth_user.username, 'http_error', params)
|
||||
message = '%s (user: %s)' % (message, auth_user.username)
|
||||
elif auth_token:
|
||||
analytics.track(auth_token.code, 'http_error', params)
|
||||
message = '%s (token: %s)' % (message,
|
||||
auth_token.friendly_name or auth_token.code)
|
||||
auth_context = get_authenticated_context()
|
||||
if auth_context is not None:
|
||||
message = '%s (authorized: %s)' % (message, auth_context.description)
|
||||
|
||||
# Log the abort.
|
||||
logger.error('Error %s: %s; Arguments: %s' % (status_code, message, params))
|
||||
|
|
|
@ -106,50 +106,21 @@ 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=None, token=None, oauthtoken=None, appspecifictoken=None,
|
||||
tuf_root=None):
|
||||
def build_context_and_subject(auth_context=None, tuf_root=None):
|
||||
""" Builds the custom context field for the JWT signed token and returns it,
|
||||
along with the subject for the JWT signed token. """
|
||||
|
||||
# Serialize to a dictionary.
|
||||
context = auth_context.to_signed_dict() if auth_context else {}
|
||||
|
||||
# Default to quay root if not explicitly granted permission to see signer root
|
||||
if not tuf_root:
|
||||
tuf_root = QUAY_TUF_ROOT
|
||||
|
||||
context = {
|
||||
CLAIM_TUF_ROOT: tuf_root
|
||||
}
|
||||
|
||||
if oauthtoken:
|
||||
context.update({
|
||||
'kind': 'oauth',
|
||||
'user': user.username,
|
||||
'oauth': oauthtoken.uuid,
|
||||
})
|
||||
return (context, user.username)
|
||||
|
||||
if appspecifictoken:
|
||||
context.update({
|
||||
'kind': 'app_specific_token',
|
||||
'user': user.username,
|
||||
'ast': appspecifictoken.uuid,
|
||||
})
|
||||
return (context, user.username)
|
||||
|
||||
if user:
|
||||
context.update({
|
||||
'kind': 'user',
|
||||
'user': user.username,
|
||||
})
|
||||
return (context, user.username)
|
||||
|
||||
if token:
|
||||
context.update({
|
||||
'kind': 'token',
|
||||
'token': token.code,
|
||||
})
|
||||
return (context, None)
|
||||
|
||||
context.update({
|
||||
'kind': 'anonymous',
|
||||
CLAIM_TUF_ROOT: tuf_root
|
||||
})
|
||||
return (context, ANONYMOUS_SUB)
|
||||
|
||||
if not auth_context or auth_context.is_anonymous:
|
||||
return (context, ANONYMOUS_SUB)
|
||||
|
||||
return (context, auth_context.authed_user.username if auth_context.authed_user else None)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import logging
|
||||
|
||||
from urlparse import urljoin
|
||||
from posixpath import join
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from six import add_metaclass
|
||||
from urlparse import urljoin
|
||||
from posixpath import join
|
||||
|
||||
import requests
|
||||
|
||||
|
@ -11,7 +12,8 @@ from data.database import CloseForLongOperation
|
|||
from util.abchelpers import nooper
|
||||
from util.failover import failover, FailoverException
|
||||
from util.security.instancekeys import InstanceKeys
|
||||
from util.security.registry_jwt import build_context_and_subject, generate_bearer_token, QUAY_TUF_ROOT, SIGNER_TUF_ROOT
|
||||
from util.security.registry_jwt import (build_context_and_subject, generate_bearer_token,
|
||||
SIGNER_TUF_ROOT)
|
||||
|
||||
|
||||
DEFAULT_HTTP_HEADERS = {'Connection': 'close'}
|
||||
|
@ -223,7 +225,7 @@ class ImplementedTUFMetadataAPI(TUFMetadataAPIInterface):
|
|||
except Non200ResponseException as ex:
|
||||
logger.exception('Failed request for %s: %s', gun, str(ex))
|
||||
except InvalidMetadataException as ex:
|
||||
logger.exception('Failed to parse targets from metadata', str(ex))
|
||||
logger.exception('Failed to parse targets from metadata: %s', str(ex))
|
||||
return None
|
||||
|
||||
def _parse_signed(self, json_response):
|
||||
|
@ -240,7 +242,7 @@ class ImplementedTUFMetadataAPI(TUFMetadataAPIInterface):
|
|||
'name': gun,
|
||||
'actions': actions,
|
||||
}]
|
||||
context, subject = build_context_and_subject(user=None, token=None, oauthtoken=None, tuf_root=SIGNER_TUF_ROOT)
|
||||
context, subject = build_context_and_subject(auth_context=None, tuf_root=SIGNER_TUF_ROOT)
|
||||
token = generate_bearer_token(self._config["SERVER_HOSTNAME"], subject, context, access,
|
||||
TOKEN_VALIDITY_LIFETIME_S, self._instance_keys)
|
||||
return {'Authorization': 'Bearer %s' % token}
|
||||
|
|
Reference in a new issue