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.
"""
path = custom_fields.Path()
path = custom_fields.PathString()
pragmas = msh.fields.Dict(keys=msh.fields.String(), values=msh.fields.Raw())
# pylint: disable=unused-argument,no-self-use

View File

@ -1,3 +1,5 @@
"""Constant parameter definitions"""
DEFAULT_CONFIG_PATH = "/etc/keyosk/conf.toml"
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 KeyoskBaseModel
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.mappings import AccountACL
from keyosk.database.mappings import AccountAssignment
from keyosk.database.mappings import DomainAccessList
from keyosk.database.mappings import DomainAdmin
from keyosk.database.mappings import DomainPermission
from keyosk.database.domain import DomainAccessList
from keyosk.database.domain import DomainPermission
from keyosk.database.domain_admin import DomainAdmin
MODELS: List[Type[KeyoskBaseModel]] = [
Account,
Domain,
DomainAccessList,
DomainPermission,
DomainAdmin,
AccountACL,
Domain,
AccountACLEntry,
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
init function kept in init.
"""
import uuid
from typing import Any
from typing import Generator
from typing import List
@ -36,7 +37,9 @@ class KeyoskBaseModel(peewee.Model):
class Meta: # pylint: disable=missing-docstring,too-few-public-methods
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
def dict_keys() -> List[str]:

View File

@ -66,7 +66,7 @@ class Account(KeyoskBaseModel):
:returns: Boolean indicating whether the provided value matches the encrypted
secret
"""
return passlib.hash.pdkdf2_sha512.verify(
return passlib.hash.pbkdf2_sha512.verify(
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
"""
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:
"""Update the server set secret
@ -84,7 +84,7 @@ class Account(KeyoskBaseModel):
:returns: The new value of the server set secret
"""
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
@staticmethod

View File

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

View File

@ -16,8 +16,9 @@ from typing import Tuple
import peewee
from keyosk.database._shared import KeyoskBaseModel
from keyosk.database.mappings import DomainAccessList
from keyosk.database.mappings import DomainPermission
from keyosk.database.domain import Domain
from keyosk.database.domain import DomainAccessList
from keyosk.database.domain import DomainPermission
class DomainAdmin(KeyoskBaseModel):
@ -55,6 +56,9 @@ class DomainAdmin(KeyoskBaseModel):
class Meta: # pylint: disable=missing-docstring,too-few-public-methods
table_name = "domain_admin"
domain = peewee.ForeignKeyField(
Domain, unique=True, null=False, backref="_administration"
)
access_list = peewee.ForeignKeyField(DomainAccessList, null=True)
domain_read = 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 json
import secrets
from collections import OrderedDict
from typing import Sequence
@ -13,42 +15,54 @@ from keyosk.database.domain import Domain
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"
account = peewee.ForeignKeyField(Account, backref="tokens")
domain = peewee.ForeignKeyField(Domain, backref="tokens")
account = peewee.ForeignKeyField(Account, backref="tokens", null=True)
domain = peewee.ForeignKeyField(Domain, backref="tokens", null=True)
issuer = peewee.CharField(null=False)
issued = peewee.DateTimeField(null=False, default=datetime.datetime.utcnow)
expires = peewee.DateTimeField(null=False)
revoked = peewee.BooleanField(null=False)
refresh = peewee.CharField(null=True)
_claims = peewee.CharField(null=False)
_usage = peewee.CharField(null=False)
@property
def claims(self):
def claims(self) -> datatypes.TokenClaims:
"""Return the claims dictionary"""
return json.loads(self._claims)
@claims.setter
def claims(self, value):
def claims(self, value: datatypes.TokenClaims):
"""Set the claims dictionary"""
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):
"""Generate the public JWT claims from current state data"""
return {
"jti": self.uuid,
"sub": self.account.username,
"aud": self.domain.audience,
"iss": self.issuer,
"exp": int(self.expires.timestamp()),
"iat": int(self.issued.timestamp()),
"exp": int(self.expires.timestamp()), # pylint: disable=no-member
"iat": int(self.issued.timestamp()), # pylint: disable=no-member
}
@classmethod
@ -58,16 +72,21 @@ class Token(KeyoskBaseModel):
domain: Domain,
issuer: str,
lifespan: datetime.timedelta,
usage: datatypes.TokenUsage,
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(
account=account,
domain=domain,
issuer=issuer,
expires=(datetime.datetime.utcnow() + lifespan),
usage=usage,
revoked=False,
refresh=secrets.token_urlsafe(42) if generate_refresh else None,
)
acls = {}
@ -96,7 +115,7 @@ class Token(KeyoskBaseModel):
}
claims = new.make_public_claims()
claims.update({"ksk-usg": new.usage.value, "ksk-pem": bitmasks})
claims.update({"ksk-pem": bitmasks})
new.claims = claims

View File

@ -7,14 +7,7 @@ from typing import Union
Extras = Dict[str, Union[int, float, bool, str, None]]
class TokenUsage(enum.Enum):
"""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"
TokenClaims = Dict[str, Union[str, int, bool, Dict[str, int]]]
@enum.unique

View File

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