Refactor our auth handling code to be cleaner
Breaks out the validation code from the auth context modification calls, makes decorators easier to define and adds testing for each individual piece. Will be the basis of better error messaging in the following change.
This commit is contained in:
parent
1bd4422da9
commit
651666b60b
18 changed files with 830 additions and 455 deletions
69
auth/test/test_basic.py
Normal file
69
auth/test/test_basic.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
import pytest
|
||||
|
||||
from base64 import b64encode
|
||||
|
||||
from auth.basic import validate_basic_auth, ACCESS_TOKEN_USERNAME, OAUTH_TOKEN_USERNAME
|
||||
from auth.validateresult import AuthKind, ValidateResult
|
||||
from data import model
|
||||
from test.fixtures import app, appconfig, database_uri, init_db_path, sqlitedb_file
|
||||
|
||||
def _token(username, password):
|
||||
return 'basic ' + b64encode('%s:%s' % (username, password))
|
||||
|
||||
@pytest.mark.parametrize('token, expected_result', [
|
||||
('', ValidateResult(AuthKind.basic, missing=True)),
|
||||
('someinvalidtoken', ValidateResult(AuthKind.basic, missing=True)),
|
||||
('somefoobartoken', ValidateResult(AuthKind.basic, missing=True)),
|
||||
('basic ', ValidateResult(AuthKind.basic, missing=True)),
|
||||
('basic some token', ValidateResult(AuthKind.basic, missing=True)),
|
||||
('basic sometoken', ValidateResult(AuthKind.basic, missing=True)),
|
||||
|
||||
(_token(ACCESS_TOKEN_USERNAME, 'invalid'),
|
||||
ValidateResult(AuthKind.basic, error_message='Invalid access token')),
|
||||
|
||||
(_token(OAUTH_TOKEN_USERNAME, 'invalid'),
|
||||
ValidateResult(AuthKind.oauth, error_message='OAuth access token could not be validated')),
|
||||
|
||||
(_token('devtable', 'invalid'),
|
||||
ValidateResult(AuthKind.basic, error_message='Invalid Username or Password')),
|
||||
|
||||
(_token('devtable+somebot', 'invalid'),
|
||||
ValidateResult(AuthKind.basic,
|
||||
error_message='Could not find robot with username: devtable+somebot ' +
|
||||
'and supplied password.')),
|
||||
|
||||
(_token('disabled', 'password'),
|
||||
ValidateResult(AuthKind.basic,
|
||||
error_message='This user has been disabled. Please contact your administrator.')),
|
||||
])
|
||||
def test_validate_basic_auth_token(token, expected_result, app):
|
||||
result = validate_basic_auth(token)
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
def test_valid_user(app):
|
||||
token = _token('devtable', 'password')
|
||||
result = validate_basic_auth(token)
|
||||
assert result == ValidateResult(AuthKind.basic, user=model.user.get_user('devtable'))
|
||||
|
||||
|
||||
def test_valid_robot(app):
|
||||
robot, password = model.user.create_robot('somerobot', model.user.get_user('devtable'))
|
||||
token = _token(robot.username, password)
|
||||
result = validate_basic_auth(token)
|
||||
assert result == ValidateResult(AuthKind.basic, robot=robot)
|
||||
|
||||
|
||||
def test_valid_token(app):
|
||||
access_token = model.token.create_delegate_token('devtable', 'simple', 'sometoken')
|
||||
token = _token(ACCESS_TOKEN_USERNAME, access_token.code)
|
||||
result = validate_basic_auth(token)
|
||||
assert result == ValidateResult(AuthKind.basic, token=access_token)
|
||||
|
||||
|
||||
def test_valid_oauth(app):
|
||||
user = model.user.get_user('devtable')
|
||||
oauth_token = list(model.oauth.list_access_tokens_for_user(user))[0]
|
||||
token = _token(OAUTH_TOKEN_USERNAME, oauth_token.access_token)
|
||||
result = validate_basic_auth(token)
|
||||
assert result == ValidateResult(AuthKind.oauth, oauthtoken=oauth_token)
|
61
auth/test/test_cookie.py
Normal file
61
auth/test/test_cookie.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
import uuid
|
||||
|
||||
from flask_login import login_user
|
||||
|
||||
from app import LoginWrappedDBUser
|
||||
from data import model
|
||||
from auth.cookie import validate_session_cookie
|
||||
from test.fixtures import app, appconfig, database_uri, init_db_path, sqlitedb_file
|
||||
|
||||
def test_anonymous_cookie(app):
|
||||
assert validate_session_cookie().missing
|
||||
|
||||
def test_invalidformatted_cookie(app):
|
||||
# "Login" with a non-UUID reference.
|
||||
someuser = model.user.get_user('devtable')
|
||||
login_user(LoginWrappedDBUser('somenonuuid', someuser))
|
||||
|
||||
# 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.error_message == 'Invalid session cookie format'
|
||||
|
||||
def test_disabled_user(app):
|
||||
# "Login" with a disabled user.
|
||||
someuser = model.user.get_user('disabled')
|
||||
login_user(LoginWrappedDBUser(someuser.uuid, someuser))
|
||||
|
||||
# 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.error_message == 'User account is disabled'
|
||||
|
||||
def test_valid_user(app):
|
||||
# Login with a valid user.
|
||||
someuser = model.user.get_user('devtable')
|
||||
login_user(LoginWrappedDBUser(someuser.uuid, someuser))
|
||||
|
||||
result = validate_session_cookie()
|
||||
assert result.authed_user == someuser
|
||||
assert result.identity is not None
|
||||
assert result.has_user
|
||||
assert result.error_message is None
|
||||
|
||||
def test_valid_organization(app):
|
||||
# "Login" with a valid organization.
|
||||
someorg = model.user.get_namespace_user('buynlarge')
|
||||
someorg.uuid = str(uuid.uuid4())
|
||||
someorg.verified = True
|
||||
someorg.save()
|
||||
|
||||
login_user(LoginWrappedDBUser(someorg.uuid, someorg))
|
||||
|
||||
result = validate_session_cookie()
|
||||
assert result.authed_user is None
|
||||
assert result.identity is None
|
||||
assert not result.has_user
|
||||
assert result.error_message == 'Cannot login to organization'
|
105
auth/test/test_decorators.py
Normal file
105
auth/test/test_decorators.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
import pytest
|
||||
|
||||
from flask import session
|
||||
from flask_login import login_user
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from app import LoginWrappedDBUser
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth.decorators import (extract_namespace_repo_from_session, require_session_login,
|
||||
process_auth_or_cookie)
|
||||
from data import model
|
||||
from test.fixtures import app, appconfig, database_uri, init_db_path, sqlitedb_file
|
||||
|
||||
def test_extract_namespace_repo_from_session_missing(app):
|
||||
def emptyfunc():
|
||||
pass
|
||||
|
||||
session.clear()
|
||||
with pytest.raises(HTTPException):
|
||||
extract_namespace_repo_from_session(emptyfunc)()
|
||||
|
||||
|
||||
def test_extract_namespace_repo_from_session_present(app):
|
||||
encountered = []
|
||||
|
||||
def somefunc(namespace, repository):
|
||||
encountered.append(namespace)
|
||||
encountered.append(repository)
|
||||
|
||||
# Add the namespace and repository to the session.
|
||||
session.clear()
|
||||
session['namespace'] = 'foo'
|
||||
session['repository'] = 'bar'
|
||||
|
||||
# Call the decorated method.
|
||||
extract_namespace_repo_from_session(somefunc)()
|
||||
|
||||
assert encountered[0] == 'foo'
|
||||
assert encountered[1] == 'bar'
|
||||
|
||||
|
||||
def test_require_session_login_missing(app):
|
||||
def emptyfunc():
|
||||
pass
|
||||
|
||||
with pytest.raises(HTTPException):
|
||||
require_session_login(emptyfunc)()
|
||||
|
||||
|
||||
def test_require_session_login_valid_user(app):
|
||||
def emptyfunc():
|
||||
pass
|
||||
|
||||
# Login as a valid user.
|
||||
someuser = model.user.get_user('devtable')
|
||||
login_user(LoginWrappedDBUser(someuser.uuid, someuser))
|
||||
|
||||
# Call the function.
|
||||
require_session_login(emptyfunc)()
|
||||
|
||||
# Ensure the authenticated user was updated.
|
||||
assert get_authenticated_user() == someuser
|
||||
|
||||
|
||||
def test_require_session_login_invalid_user(app):
|
||||
def emptyfunc():
|
||||
pass
|
||||
|
||||
# "Login" as a disabled user.
|
||||
someuser = model.user.get_user('disabled')
|
||||
login_user(LoginWrappedDBUser(someuser.uuid, someuser))
|
||||
|
||||
# Call the function.
|
||||
with pytest.raises(HTTPException):
|
||||
require_session_login(emptyfunc)()
|
||||
|
||||
# Ensure the authenticated user was not updated.
|
||||
assert get_authenticated_user() is None
|
||||
|
||||
|
||||
def test_process_auth_or_cookie_invalid_user(app):
|
||||
def emptyfunc():
|
||||
pass
|
||||
|
||||
# Call the function.
|
||||
process_auth_or_cookie(emptyfunc)()
|
||||
|
||||
# Ensure the authenticated user was not updated.
|
||||
assert get_authenticated_user() is None
|
||||
|
||||
|
||||
def test_process_auth_or_cookie_valid_user(app):
|
||||
def emptyfunc():
|
||||
pass
|
||||
|
||||
# Login as a valid user.
|
||||
someuser = model.user.get_user('devtable')
|
||||
login_user(LoginWrappedDBUser(someuser.uuid, someuser))
|
||||
|
||||
# Call the function.
|
||||
process_auth_or_cookie(emptyfunc)()
|
||||
|
||||
# Ensure the authenticated user was updated.
|
||||
assert get_authenticated_user() == someuser
|
||||
|
48
auth/test/test_oauth.py
Normal file
48
auth/test/test_oauth.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
import pytest
|
||||
|
||||
from auth.oauth import validate_bearer_auth, validate_oauth_token
|
||||
from auth.validateresult import AuthKind, ValidateResult
|
||||
from data import model
|
||||
from test.fixtures import app, appconfig, database_uri, init_db_path, sqlitedb_file
|
||||
|
||||
@pytest.mark.parametrize('header, expected_result', [
|
||||
('', ValidateResult(AuthKind.oauth, missing=True)),
|
||||
('somerandomtoken', ValidateResult(AuthKind.oauth, missing=True)),
|
||||
('bearer some random token', ValidateResult(AuthKind.oauth, missing=True)),
|
||||
|
||||
('bearer invalidtoken',
|
||||
ValidateResult(AuthKind.oauth, error_message='OAuth access token could not be validated')),
|
||||
])
|
||||
def test_bearer(header, expected_result, app):
|
||||
assert validate_bearer_auth(header) == expected_result
|
||||
|
||||
def test_valid_oauth(app):
|
||||
user = model.user.get_user('devtable')
|
||||
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.authed_user == user
|
||||
assert result.auth_valid
|
||||
|
||||
def test_disabled_user_oauth(app):
|
||||
user = model.user.get_user('disabled')
|
||||
token = model.oauth.create_access_token_for_testing(user, 'deadbeef', 'repo:admin',
|
||||
access_token='foo')
|
||||
|
||||
result = validate_bearer_auth('bearer ' + token.access_token)
|
||||
assert result.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'
|
||||
|
||||
def test_expired_token(app):
|
||||
user = model.user.get_user('devtable')
|
||||
token = model.oauth.create_access_token_for_testing(user, 'deadbeef', 'repo:admin',
|
||||
access_token='bar', expires_in=-1000)
|
||||
|
||||
result = validate_bearer_auth('bearer ' + token.access_token)
|
||||
assert result.oauthtoken is None
|
||||
assert result.authed_user is None
|
||||
assert not result.auth_valid
|
||||
assert result.error_message == 'OAuth access token has expired'
|
50
auth/test/test_scopes.py
Normal file
50
auth/test/test_scopes.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
import pytest
|
||||
|
||||
from auth.scopes import (scopes_from_scope_string, validate_scope_string, ALL_SCOPES,
|
||||
is_subset_string)
|
||||
|
||||
@pytest.mark.parametrize('scopes_string, expected', [
|
||||
# Valid single scopes.
|
||||
('repo:read', ['repo:read']),
|
||||
('repo:admin', ['repo:admin']),
|
||||
|
||||
# Invalid scopes.
|
||||
('not:valid', []),
|
||||
('repo:admins', []),
|
||||
|
||||
# Valid scope strings.
|
||||
('repo:read repo:admin', ['repo:read', 'repo:admin']),
|
||||
('repo:read,repo:admin', ['repo:read', 'repo:admin']),
|
||||
('repo:read,repo:admin repo:write', ['repo:read', 'repo:admin', 'repo:write']),
|
||||
|
||||
# Partially invalid scopes.
|
||||
('repo:read,not:valid', []),
|
||||
('repo:read repo:admins', []),
|
||||
|
||||
# Invalid scope strings.
|
||||
('repo:read|repo:admin', []),
|
||||
|
||||
# Mixture of delimiters.
|
||||
('repo:read, repo:admin', []),
|
||||
])
|
||||
def test_parsing(scopes_string, expected):
|
||||
expected_scope_set = {ALL_SCOPES[scope_name] for scope_name in expected}
|
||||
parsed_scope_set = scopes_from_scope_string(scopes_string)
|
||||
assert parsed_scope_set == expected_scope_set
|
||||
assert validate_scope_string(scopes_string) == bool(expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('superset, subset, result', [
|
||||
('repo:read', 'repo:read', True),
|
||||
('repo:read repo:admin', 'repo:read', True),
|
||||
('repo:read,repo:admin', 'repo:read', True),
|
||||
('repo:read,repo:admin', 'repo:admin', True),
|
||||
('repo:read,repo:admin', 'repo:admin repo:read', True),
|
||||
|
||||
('', 'repo:read', False),
|
||||
('unknown:tag', 'repo:read', False),
|
||||
('repo:read unknown:tag', 'repo:read', False),
|
||||
('repo:read,unknown:tag', 'repo:read', False),
|
||||
])
|
||||
def test_subset_string(superset, subset, result):
|
||||
assert is_subset_string(superset, subset) == result
|
18
auth/test/test_signedgrant.py
Normal file
18
auth/test/test_signedgrant.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
import pytest
|
||||
|
||||
from auth.signedgrant import validate_signed_grant, generate_signed_token, SIGNATURE_PREFIX
|
||||
from auth.validateresult import AuthKind, ValidateResult
|
||||
|
||||
@pytest.mark.parametrize('header, expected_result', [
|
||||
('', ValidateResult(AuthKind.signed_grant, missing=True)),
|
||||
('somerandomtoken', ValidateResult(AuthKind.signed_grant, missing=True)),
|
||||
('token somerandomtoken', ValidateResult(AuthKind.signed_grant, missing=True)),
|
||||
|
||||
('token ' + SIGNATURE_PREFIX + 'foo',
|
||||
ValidateResult(AuthKind.signed_grant, error_message='Signed grant could not be validated')),
|
||||
|
||||
('token ' + generate_signed_token({'a': 'b'}, {'c': 'd'}),
|
||||
ValidateResult(AuthKind.signed_grant, signed_data={'grants': {'a': 'b'}, 'user_context': {'c': 'd'}})),
|
||||
])
|
||||
def test_token(header, expected_result):
|
||||
assert validate_signed_grant(header) == expected_result
|
63
auth/test/test_validateresult.py
Normal file
63
auth/test/test_validateresult.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
import pytest
|
||||
|
||||
from auth.auth_context import (get_authenticated_user, get_grant_context, get_validated_token,
|
||||
get_validated_oauth_token)
|
||||
from auth.validateresult import AuthKind, ValidateResult
|
||||
from data import model
|
||||
from test.fixtures import app, appconfig, database_uri, init_db_path, sqlitedb_file
|
||||
|
||||
|
||||
def get_user():
|
||||
return model.user.get_user('devtable')
|
||||
|
||||
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'),
|
||||
])
|
||||
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
|
||||
|
||||
entity = get_entity()
|
||||
args = {}
|
||||
args[entity_kind] = entity
|
||||
|
||||
result = ValidateResult(AuthKind.basic, **args)
|
||||
result.apply_to_context()
|
||||
|
||||
expected_user = entity if entity_kind == 'user' or entity_kind == 'robot' else None
|
||||
if entity_kind == 'oauthtoken':
|
||||
expected_user = entity.authorized_user
|
||||
|
||||
expected_token = entity if entity_kind == 'token' else None
|
||||
expected_oauth = entity if entity_kind == 'oauthtoken' 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
|
Reference in a new issue