Fix linting and typing errors

Document token model
Fix typo accessing incorrect crypto hash in passlib
Fix cyclical import around domain admin settings
Deprecate unused token usage enum
Document constants module
Fix typing errors in fields module
Fix creation order of database models
Add uuid default factory for uuid field
This commit is contained in:
Ethan Paul 2020-02-22 22:07:37 -05:00
parent 33325a344e
commit 71d6ed4e3c
10 changed files with 75 additions and 51 deletions

View File

@ -49,7 +49,7 @@ class SQLiteStorageConfigSerializer(msh.Schema):
:class:`KeyoskSQLiteStorageConfig` class. :class:`KeyoskSQLiteStorageConfig` class.
""" """
path = custom_fields.Path() path = custom_fields.PathString()
pragmas = msh.fields.Dict(keys=msh.fields.String(), values=msh.fields.Raw()) pragmas = msh.fields.Dict(keys=msh.fields.String(), values=msh.fields.Raw())
# pylint: disable=unused-argument,no-self-use # pylint: disable=unused-argument,no-self-use

View File

@ -1,3 +1,5 @@
"""Constant parameter definitions"""
DEFAULT_CONFIG_PATH = "/etc/keyosk/conf.toml" DEFAULT_CONFIG_PATH = "/etc/keyosk/conf.toml"
ENV_CONFIG_PATH = "KYSK_CONF_PATH" ENV_CONFIG_PATH = "KYSK_CONF_PATH"

View File

@ -28,21 +28,21 @@ from keyosk import datatypes
from keyosk.database._shared import INTERFACE as interface from keyosk.database._shared import INTERFACE as interface
from keyosk.database._shared import KeyoskBaseModel from keyosk.database._shared import KeyoskBaseModel
from keyosk.database.account import Account from keyosk.database.account import Account
from keyosk.database.account import AccountAssignment
from keyosk.database.account_acl import AccountACLEntry
from keyosk.database.domain import Domain from keyosk.database.domain import Domain
from keyosk.database.mappings import AccountACL from keyosk.database.domain import DomainAccessList
from keyosk.database.mappings import AccountAssignment from keyosk.database.domain import DomainPermission
from keyosk.database.mappings import DomainAccessList from keyosk.database.domain_admin import DomainAdmin
from keyosk.database.mappings import DomainAdmin
from keyosk.database.mappings import DomainPermission
MODELS: List[Type[KeyoskBaseModel]] = [ MODELS: List[Type[KeyoskBaseModel]] = [
Account, Account,
Domain,
DomainAccessList, DomainAccessList,
DomainPermission, DomainPermission,
DomainAdmin, DomainAdmin,
AccountACL, Domain,
AccountACLEntry,
AccountAssignment, AccountAssignment,
] ]

View File

@ -11,6 +11,7 @@ Thus the :func:`initialize` function and :class:`KeyoskBaseModel` class need to
separate modules, and for somewhat arbitrary reasons the base model was put here and the separate modules, and for somewhat arbitrary reasons the base model was put here and the
init function kept in init. init function kept in init.
""" """
import uuid
from typing import Any from typing import Any
from typing import Generator from typing import Generator
from typing import List from typing import List
@ -36,7 +37,9 @@ class KeyoskBaseModel(peewee.Model):
class Meta: # pylint: disable=missing-docstring,too-few-public-methods class Meta: # pylint: disable=missing-docstring,too-few-public-methods
database = INTERFACE database = INTERFACE
uuid = peewee.UUIDField(null=False, unique=True, primary_key=True) uuid = peewee.UUIDField(
null=False, unique=True, primary_key=True, default=uuid.uuid4
)
@staticmethod @staticmethod
def dict_keys() -> List[str]: def dict_keys() -> List[str]:

View File

@ -66,7 +66,7 @@ class Account(KeyoskBaseModel):
:returns: Boolean indicating whether the provided value matches the encrypted :returns: Boolean indicating whether the provided value matches the encrypted
secret secret
""" """
return passlib.hash.pdkdf2_sha512.verify( return passlib.hash.pbkdf2_sha512.verify(
value, self.encrypted_server_set_secret value, self.encrypted_server_set_secret
) )
@ -75,7 +75,7 @@ class Account(KeyoskBaseModel):
:param value: The string to set the encrypted client-set-secret to :param value: The string to set the encrypted client-set-secret to
""" """
self.encrypted_client_set_secret = passlib.hash.pdkdf2_sha512.hash(value) self.encrypted_client_set_secret = passlib.hash.pbkdf2_sha512.hash(value)
def update_server_set_secret(self, length: int = 42) -> str: def update_server_set_secret(self, length: int = 42) -> str:
"""Update the server set secret """Update the server set secret
@ -84,7 +84,7 @@ class Account(KeyoskBaseModel):
:returns: The new value of the server set secret :returns: The new value of the server set secret
""" """
value = secrets.token_urlsafe(length) value = secrets.token_urlsafe(length)
self.encrypted_server_set_secret = passlib.hash.pdkdf2_sha512.hash(value) self.encrypted_server_set_secret = passlib.hash.pbkdf2_sha512.hash(value)
return value return value
@staticmethod @staticmethod

View File

@ -5,7 +5,6 @@ from typing import List
import peewee import peewee
from keyosk.database._shared import KeyoskBaseModel from keyosk.database._shared import KeyoskBaseModel
from keyosk.database.domain_admin import DomainAdmin
class Domain(KeyoskBaseModel): class Domain(KeyoskBaseModel):
@ -29,8 +28,8 @@ class Domain(KeyoskBaseModel):
be valid for be valid for
:attribute lifespan_refresh: Number of seconds an an issued JWT refresh token should :attribute lifespan_refresh: Number of seconds an an issued JWT refresh token should
be valid for be valid for
:attribute administration: Container of additional settings related to the :property administration: Container of additional settings related to the
administration of the domain itself administration of the domain itself
:property access_list_names: List of Access Control Lists under the domain that accounts :property access_list_names: List of Access Control Lists under the domain that accounts
can have permission entries on can have permission entries on
:property permission_names: List of permissions that can be assigned to an account's ACL :property permission_names: List of permissions that can be assigned to an account's ACL
@ -53,7 +52,6 @@ class Domain(KeyoskBaseModel):
enable_refresh = peewee.BooleanField(null=False) enable_refresh = peewee.BooleanField(null=False)
lifespan_access = peewee.IntegerField(null=False) lifespan_access = peewee.IntegerField(null=False)
lifespan_refresh = peewee.IntegerField(null=False) lifespan_refresh = peewee.IntegerField(null=False)
administration = peewee.ForeignKeyField(DomainAdmin, null=False, unique=True)
@property @property
def access_list_names(self) -> List[str]: def access_list_names(self) -> List[str]:
@ -65,6 +63,11 @@ class Domain(KeyoskBaseModel):
"""Return the list of permission names from the backref""" """Return the list of permission names from the backref"""
return [item.name for item in self._permissions] return [item.name for item in self._permissions]
@property
def administration(self):
"""Return administration settings container"""
return self._administration[0]
@staticmethod @staticmethod
def dict_keys() -> List[str]: def dict_keys() -> List[str]:
return [ return [

View File

@ -16,8 +16,9 @@ from typing import Tuple
import peewee import peewee
from keyosk.database._shared import KeyoskBaseModel from keyosk.database._shared import KeyoskBaseModel
from keyosk.database.mappings import DomainAccessList from keyosk.database.domain import Domain
from keyosk.database.mappings import DomainPermission from keyosk.database.domain import DomainAccessList
from keyosk.database.domain import DomainPermission
class DomainAdmin(KeyoskBaseModel): class DomainAdmin(KeyoskBaseModel):
@ -55,6 +56,9 @@ class DomainAdmin(KeyoskBaseModel):
class Meta: # pylint: disable=missing-docstring,too-few-public-methods class Meta: # pylint: disable=missing-docstring,too-few-public-methods
table_name = "domain_admin" table_name = "domain_admin"
domain = peewee.ForeignKeyField(
Domain, unique=True, null=False, backref="_administration"
)
access_list = peewee.ForeignKeyField(DomainAccessList, null=True) access_list = peewee.ForeignKeyField(DomainAccessList, null=True)
domain_read = peewee.ForeignKeyField(DomainPermission, null=True) domain_read = peewee.ForeignKeyField(DomainPermission, null=True)
domain_update = peewee.ForeignKeyField(DomainPermission, null=True) domain_update = peewee.ForeignKeyField(DomainPermission, null=True)

View File

@ -1,5 +1,7 @@
"""Access token model definition"""
import datetime import datetime
import json import json
import secrets
from collections import OrderedDict from collections import OrderedDict
from typing import Sequence from typing import Sequence
@ -13,42 +15,54 @@ from keyosk.database.domain import Domain
class Token(KeyoskBaseModel): class Token(KeyoskBaseModel):
class Meta: """Issued access token storage model
:attribute account: Account the token was issued to
:attribute domain: Domain the token was issued for
:attribute issuer: Value of the issuer parameter at generation time
:attribute issued: Datetime indicating when the token was issued
:attribute expires: Datetime indicating when the token expires
:attribute revoked: Whether the token has been revoked
:attribute refresh: Refresh token attached to the issued access token; can be
``None`` if refresh tokens are disabled for the domain
:property claims: Claims generated for the token
.. note:: Settings and parameters may be changed on linked records. However, the
``claims`` property will always contain the set of claims as assigned at
issuance time.
"""
class Meta: # pylint: disable=missing-docstring,too-few-public-methods
table_name = "token" table_name = "token"
account = peewee.ForeignKeyField(Account, backref="tokens") account = peewee.ForeignKeyField(Account, backref="tokens", null=True)
domain = peewee.ForeignKeyField(Domain, backref="tokens") domain = peewee.ForeignKeyField(Domain, backref="tokens", null=True)
issuer = peewee.CharField(null=False) issuer = peewee.CharField(null=False)
issued = peewee.DateTimeField(null=False, default=datetime.datetime.utcnow) issued = peewee.DateTimeField(null=False, default=datetime.datetime.utcnow)
expires = peewee.DateTimeField(null=False) expires = peewee.DateTimeField(null=False)
revoked = peewee.BooleanField(null=False) revoked = peewee.BooleanField(null=False)
refresh = peewee.CharField(null=True)
_claims = peewee.CharField(null=False) _claims = peewee.CharField(null=False)
_usage = peewee.CharField(null=False)
@property @property
def claims(self): def claims(self) -> datatypes.TokenClaims:
"""Return the claims dictionary"""
return json.loads(self._claims) return json.loads(self._claims)
@claims.setter @claims.setter
def claims(self, value): def claims(self, value: datatypes.TokenClaims):
"""Set the claims dictionary"""
self._claims = json.dumps(value) self._claims = json.dumps(value)
@property
def usage(self) -> datatypes.TokenUsage:
return datatypes.TokenUsage[self._usage]
@usage.setter
def usage(self, value: datatypes.TokenUsage):
self._usage = value.name
def make_public_claims(self): def make_public_claims(self):
"""Generate the public JWT claims from current state data"""
return { return {
"jti": self.uuid, "jti": self.uuid,
"sub": self.account.username, "sub": self.account.username,
"aud": self.domain.audience, "aud": self.domain.audience,
"iss": self.issuer, "iss": self.issuer,
"exp": int(self.expires.timestamp()), "exp": int(self.expires.timestamp()), # pylint: disable=no-member
"iat": int(self.issued.timestamp()), "iat": int(self.issued.timestamp()), # pylint: disable=no-member
} }
@classmethod @classmethod
@ -58,16 +72,21 @@ class Token(KeyoskBaseModel):
domain: Domain, domain: Domain,
issuer: str, issuer: str,
lifespan: datetime.timedelta, lifespan: datetime.timedelta,
usage: datatypes.TokenUsage,
permissions: Sequence[AccountACLEntry], permissions: Sequence[AccountACLEntry],
generate_refresh: bool,
): ):
"""Create a new token using provided data
This function is intentionally not documented, as I expect it will not survive
first contact with a practical implementation
"""
new = cls( new = cls(
account=account, account=account,
domain=domain, domain=domain,
issuer=issuer, issuer=issuer,
expires=(datetime.datetime.utcnow() + lifespan), expires=(datetime.datetime.utcnow() + lifespan),
usage=usage,
revoked=False, revoked=False,
refresh=secrets.token_urlsafe(42) if generate_refresh else None,
) )
acls = {} acls = {}
@ -96,7 +115,7 @@ class Token(KeyoskBaseModel):
} }
claims = new.make_public_claims() claims = new.make_public_claims()
claims.update({"ksk-usg": new.usage.value, "ksk-pem": bitmasks}) claims.update({"ksk-pem": bitmasks})
new.claims = claims new.claims = claims

View File

@ -7,14 +7,7 @@ from typing import Union
Extras = Dict[str, Union[int, float, bool, str, None]] Extras = Dict[str, Union[int, float, bool, str, None]]
class TokenUsage(enum.Enum): TokenClaims = Dict[str, Union[str, int, bool, Dict[str, int]]]
"""Possible usage values for an issued JWT
Values will be the value of the ``ksk-usg`` claim in the issued token
"""
REFRESH = "ref"
ACCESS = "acc"
@enum.unique @enum.unique

View File

@ -59,7 +59,7 @@ class EnumItem(msh.fields.Field):
return self.enum[self._from_pretty_name(value)] return self.enum[self._from_pretty_name(value)]
return self.enum[value] return self.enum[value]
except ValueError as err: except ValueError as err:
raise msh.ValidationError(err) raise msh.ValidationError(str(err))
except (KeyError, AttributeError) as err: except (KeyError, AttributeError) as err:
raise msh.ValidationError(f"No {self.enum} named {err}") raise msh.ValidationError(f"No {self.enum} named {err}")
@ -72,11 +72,11 @@ class EnumItem(msh.fields.Field):
return value.replace("-", "_").upper() return value.replace("-", "_").upper()
class Path(msh.fields.String): class PathString(msh.fields.String):
"""Translate between a string and a path object""" """Translate between a string and a path object"""
def _serialize(self, value: Union[str, Path], *args, **kwargs) -> str: def _serialize(self, value: Union[str, Path], attr, obj, **kwargs) -> str:
return super()._serialize(str(value), *args, **kwargs) return super()._serialize(str(value), attr, obj, **kwargs)
def _deserialize(self, value: str, *args, **kwargs) -> Path: def _deserialize(self, value: str, attr, data, **kwargs) -> Path:
return Path(super()._deserialize(value, *args, **kwargs)) return Path(super()._deserialize(value, attr, data, **kwargs))