Refactor auth code to be cleaner and more extensible
We move all the auth handling, serialization and deserialization into a new AuthContext interface, and then standardize a registration model for handling of specific auth context types (user, robot, token, etc).
This commit is contained in:
parent
8ba2e71fb1
commit
e220b50543
31 changed files with 822 additions and 436 deletions
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
|
||||
|
|
Reference in a new issue