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:
josephschorr 2018-02-15 16:02:22 -05:00 committed by GitHub
commit 7cd2c00d4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 822 additions and 436 deletions

5
app.py
View file

@ -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

View file

@ -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
View 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

View file

@ -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
View 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,
}

View file

@ -0,0 +1,3 @@
ACCESS_TOKEN_USERNAME = '$token'
OAUTH_TOKEN_USERNAME = '$oauthtoken'
APP_SPECIFIC_TOKEN_USERNAME = '$app'

View file

@ -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'

View file

@ -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)

View file

@ -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:

View 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()

View file

@ -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'

View file

@ -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

View file

@ -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'

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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'])

View file

@ -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]

View file

@ -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())

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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})

View file

@ -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)

View file

@ -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))

View file

@ -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)

View file

@ -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}