Add an AppSpecificAuthToken data model for app-specific auth tokens. These will be used for the Docker CLI in place of username+password
This commit is contained in:
parent
53b762a875
commit
524d77f527
50 changed files with 943 additions and 289 deletions
|
@ -126,6 +126,7 @@ config = Config()
|
|||
# There MUST NOT be any circular dependencies between these subsections. If there are fix it by
|
||||
# moving the minimal number of things to _basequery
|
||||
from data.model import (
|
||||
appspecifictoken,
|
||||
blob,
|
||||
build,
|
||||
image,
|
||||
|
|
108
data/model/appspecifictoken.py
Normal file
108
data/model/appspecifictoken.py
Normal file
|
@ -0,0 +1,108 @@
|
|||
import logging
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from cachetools import lru_cache
|
||||
from peewee import PeeweeException
|
||||
|
||||
from data.database import AppSpecificAuthToken, User, db_transaction
|
||||
from data.model import config
|
||||
from util.timedeltastring import convert_to_timedelta
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _default_expiration():
|
||||
expiration_str = config.app_config.get('APP_SPECIFIC_TOKEN_EXPIRATION')
|
||||
return datetime.now() + convert_to_timedelta(expiration_str) if expiration_str else expiration_str
|
||||
|
||||
|
||||
_default_expiration_opt = 'deo'
|
||||
def create_token(user, title, expiration=_default_expiration_opt):
|
||||
""" Creates and returns an app specific token for the given user. If no expiration is specified
|
||||
(including `None`), then the default from config is used. """
|
||||
expiration = expiration if expiration != _default_expiration_opt else _default_expiration()
|
||||
return AppSpecificAuthToken.create(user=user, title=title, expiration=expiration)
|
||||
|
||||
|
||||
def list_tokens(user):
|
||||
""" Lists all tokens for the given user. """
|
||||
return AppSpecificAuthToken.select().where(AppSpecificAuthToken.user == user)
|
||||
|
||||
|
||||
def revoke_token(token):
|
||||
""" Revokes an app specific token by deleting it. """
|
||||
token.delete_instance()
|
||||
|
||||
|
||||
def get_expiring_tokens(user, soon):
|
||||
""" Returns all tokens owned by the given user that will be expiring "soon", where soon is defined
|
||||
by the soon parameter (a timedelta from now).
|
||||
"""
|
||||
soon_datetime = datetime.now() + soon
|
||||
return (AppSpecificAuthToken
|
||||
.select()
|
||||
.where(AppSpecificAuthToken.user == user,
|
||||
AppSpecificAuthToken.expiration <= soon_datetime))
|
||||
|
||||
|
||||
def gc_expired_tokens(user):
|
||||
""" Deletes all expired tokens owned by the given user. """
|
||||
(AppSpecificAuthToken
|
||||
.delete()
|
||||
.where(AppSpecificAuthToken.user == user, AppSpecificAuthToken.expiration < datetime.now())
|
||||
.execute())
|
||||
|
||||
|
||||
def get_token_by_uuid(uuid, owner=None):
|
||||
""" Looks up an unexpired app specific token with the given uuid. Returns it if found or
|
||||
None if none. If owner is specified, only tokens owned by the owner user will be
|
||||
returned.
|
||||
"""
|
||||
try:
|
||||
query = (AppSpecificAuthToken
|
||||
.select()
|
||||
.where(AppSpecificAuthToken.uuid == uuid,
|
||||
((AppSpecificAuthToken.expiration > datetime.now()) |
|
||||
(AppSpecificAuthToken.expiration >> None))))
|
||||
if owner is not None:
|
||||
query = query.where(AppSpecificAuthToken.user == owner)
|
||||
|
||||
return query.get()
|
||||
except AppSpecificAuthToken.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
def access_valid_token(token_code):
|
||||
""" Looks up an unexpired app specific token with the given token code. If found, the token's
|
||||
last_accessed field is set to now and the token is returned. If not found, returns None.
|
||||
"""
|
||||
with db_transaction():
|
||||
try:
|
||||
token = (AppSpecificAuthToken
|
||||
.select(AppSpecificAuthToken, User)
|
||||
.join(User)
|
||||
.where(AppSpecificAuthToken.token_code == token_code,
|
||||
((AppSpecificAuthToken.expiration > datetime.now()) |
|
||||
(AppSpecificAuthToken.expiration >> None)))
|
||||
.get())
|
||||
except AppSpecificAuthToken.DoesNotExist:
|
||||
return None
|
||||
|
||||
token.last_accessed = datetime.now()
|
||||
|
||||
try:
|
||||
token.save()
|
||||
except PeeweeException as ex:
|
||||
strict_logging_disabled = config.app_config.get('ALLOW_PULLS_WITHOUT_STRICT_LOGGING')
|
||||
if strict_logging_disabled:
|
||||
data = {
|
||||
'exception': ex,
|
||||
'token': token.id,
|
||||
}
|
||||
|
||||
logger.exception('update last_accessed for token failed', extra=data)
|
||||
else:
|
||||
raise
|
||||
|
||||
return token
|
77
data/model/test/test_appspecifictoken.py
Normal file
77
data/model/test/test_appspecifictoken.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from data import model
|
||||
from data.model.appspecifictoken import create_token, revoke_token, access_valid_token
|
||||
from data.model.appspecifictoken import gc_expired_tokens, get_expiring_tokens
|
||||
from util.timedeltastring import convert_to_timedelta
|
||||
|
||||
from test.fixtures import *
|
||||
|
||||
@pytest.mark.parametrize('expiration', [
|
||||
(None),
|
||||
('-1m'),
|
||||
('-1d'),
|
||||
('-1w'),
|
||||
('10m'),
|
||||
('10d'),
|
||||
('10w'),
|
||||
])
|
||||
def test_gc(expiration, initialized_db):
|
||||
user = model.user.get_user('devtable')
|
||||
|
||||
expiration_date = None
|
||||
is_expired = False
|
||||
if expiration:
|
||||
if expiration[0] == '-':
|
||||
is_expired = True
|
||||
expiration_date = datetime.now() - convert_to_timedelta(expiration[1:])
|
||||
else:
|
||||
expiration_date = datetime.now() + convert_to_timedelta(expiration)
|
||||
|
||||
# Create a token.
|
||||
token = create_token(user, 'Some token', expiration=expiration_date)
|
||||
|
||||
# GC tokens.
|
||||
gc_expired_tokens(user)
|
||||
|
||||
# Ensure the token was GCed if expired and not if it wasn't.
|
||||
assert (access_valid_token(token.token_code) is None) == is_expired
|
||||
|
||||
|
||||
def test_access_token(initialized_db):
|
||||
user = model.user.get_user('devtable')
|
||||
|
||||
# Create a token.
|
||||
token = create_token(user, 'Some token')
|
||||
assert token.last_accessed is None
|
||||
|
||||
# Lookup the token.
|
||||
token = access_valid_token(token.token_code)
|
||||
assert token.last_accessed is not None
|
||||
|
||||
# Revoke the token.
|
||||
revoke_token(token)
|
||||
|
||||
# Ensure it cannot be accessed
|
||||
assert access_valid_token(token.token_code) is None
|
||||
|
||||
|
||||
def test_expiring_soon(initialized_db):
|
||||
user = model.user.get_user('devtable')
|
||||
|
||||
# Create some tokens.
|
||||
create_token(user, 'Some token')
|
||||
exp_token = create_token(user, 'Some expiring token', datetime.now() + convert_to_timedelta('1d'))
|
||||
create_token(user, 'Some other token', expiration=datetime.now() + convert_to_timedelta('2d'))
|
||||
|
||||
# Get the token expiring soon.
|
||||
expiring_soon = get_expiring_tokens(user, convert_to_timedelta('25h'))
|
||||
assert expiring_soon
|
||||
assert len(expiring_soon) == 1
|
||||
assert expiring_soon[0].id == exp_token.id
|
||||
|
||||
expiring_soon = get_expiring_tokens(user, convert_to_timedelta('49h'))
|
||||
assert expiring_soon
|
||||
assert len(expiring_soon) == 2
|
Reference in a new issue