diff --git a/keyosk/database/__init__.py b/keyosk/database/__init__.py index c648750..92dd6ed 100644 --- a/keyosk/database/__init__.py +++ b/keyosk/database/__init__.py @@ -27,20 +27,20 @@ from keyosk import config 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_acl import AccountACLEntry -from keyosk.database.domain import Domain -from keyosk.database.domain import DomainAccessList -from keyosk.database.domain import DomainPermission +from keyosk.database.account import KeyoskAccount +from keyosk.database.account import KeyoskAccountScope +from keyosk.database.domain import KeyoskDomain +from keyosk.database.domain import KeyoskDomainAccessList +from keyosk.database.domain import KeyoskDomainPermission from keyosk.database.token import Token MODELS: List[Type[KeyoskBaseModel]] = [ - Account, - DomainAccessList, - DomainPermission, - Domain, - AccountACLEntry, + KeyoskAccount, + KeyoskDomain, + KeyoskDomainAccessList, + KeyoskDomainPermission, + KeyoskAccountScope, Token, ] diff --git a/keyosk/database/_shared.py b/keyosk/database/_shared.py index fc9b52c..57ac50f 100644 --- a/keyosk/database/_shared.py +++ b/keyosk/database/_shared.py @@ -12,10 +12,6 @@ separate modules, and for somewhat arbitrary reasons the base model was put here init function kept in init. """ import uuid -from typing import Any -from typing import Generator -from typing import List -from typing import Tuple import peewee @@ -41,41 +37,5 @@ class KeyoskBaseModel(peewee.Model): null=False, unique=True, primary_key=True, default=uuid.uuid4 ) - @staticmethod - def dict_keys() -> List[str]: - """Return tuple of attribute names that should be included in the dict form of the model - Inteneded to be used in a dictionary comprehension; see the :meth:`__iter__` method for - usage example. - """ - return [] - - @staticmethod - def foreign_ref() -> List[str]: - """Return tuple of attribute names that point to foreign key references on the model - Intended for usage when recursively converting models into dictionaries ahead of - serialization; see the :meth:`__iter__` method for usage example. - - .. warning:: Foreign keys should only be included here when their attribute appears in the - tuple returned from :meth:`dict_keys` - """ - return [] - - @staticmethod - def foreign_backref() -> List[str]: - """Return tuple of attribute names that point to foreign backreferences on the model - Inteneded for usage when recursively converting models into dictionaries ahead of - serialization; see the :meth:`__iter__` method for usage example. - - .. warning:: Foreign keys should only be included here when their attribute appears in the - tuple returned from :meth:`dict_keys` - """ - return [] - - def __iter__(self) -> Generator[Tuple[str, Any], None, None]: - for key in self.dict_keys(): - if key in self.foreign_ref(): - yield key, dict(getattr(self, key)) - elif key in self.foreign_backref(): - yield key, [dict(item) for item in getattr(self, key)] - else: - yield key, getattr(self, key) + def save_recursive(self, force_insert: bool = False): + pass diff --git a/keyosk/database/account.py b/keyosk/database/account.py index 98e738f..3f2999e 100644 --- a/keyosk/database/account.py +++ b/keyosk/database/account.py @@ -1,17 +1,16 @@ """Authentication account model definition""" import datetime import json -import secrets -from typing import List -import passlib.hash import peewee from keyosk.database._shared import KeyoskBaseModel +from keyosk.database.domain import KeyoskDomainAccessList +from keyosk.database.domain import KeyoskDomainPermission from keyosk.datatypes import Extras -class Account(KeyoskBaseModel): +class KeyoskAccount(KeyoskBaseModel): """Authentication account storage model :attribute created: Datetime indicating when the account was first created @@ -47,52 +46,44 @@ class Account(KeyoskBaseModel): """Set the extras dictionary""" self._extras = json.dumps(value) - def verify_client_set_secret(self, value: str) -> bool: - """Verify the client set secret matches a value - - :param value: The string to check matches the client-set-secret - :returns: Boolean indicating whether the provided value matches the encrypted - secret - """ - return passlib.hash.pbkdf2_sha512.verify( - value, self.encrypted_client_set_secret - ) - - def verify_server_set_secret(self, value: str) -> bool: - """Verify the server set secret matches a value - - :param value: The string to check matches the server-set-secret - :returns: Boolean indicating whether the provided value matches the encrypted - secret - """ - return passlib.hash.pbkdf2_sha512.verify( - value, self.encrypted_server_set_secret - ) - - def update_client_set_secret(self, value: str) -> None: - """Update the client set secret - - :param value: The string to set the encrypted client-set-secret to - """ - 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 - - :param length: Optional length of the generated token - :returns: The new value of the server set secret - """ - value = secrets.token_urlsafe(length) - self.encrypted_server_set_secret = passlib.hash.pbkdf2_sha512.hash(value) - return value - - @staticmethod - def dict_keys() -> List[str]: - return ["uuid", "created", "updated", "username", "enabled", "extras", "acls"] - - @staticmethod - def foreign_backref() -> List[str]: - return ["acls"] - def __str__(self) -> str: return f"Account '{self.username}' ({self.uuid})" + + +class KeyoskAccountScope(KeyoskBaseModel): + """Access control list entry model definition + + :attribute account: Account the ACL entry applies to + :attribute access_list: The access list the entry is for + :attribute permission: The permission the entry is for + :attribute with_server_secret: Whether the permission should be applied when the + account authenticates with the account's + server-set-secret + :attribute with_client_secret: Whether the permission should be applied when the + account authenticates with the account's + client-set-secret + + .. note:: Since permissions are by definition boolean, there is no need to store a + value parameter with an ACL entry: if an entry exists for a given account + for a given access list with a given permission, then that permission is + granted on that access list to that account; similarly, if one does not + exist then it is not granted. + """ + + class Meta: # pylint: disable=missing-docstring,too-few-public-methods + table_name = "account_scope" + + account = peewee.ForeignKeyField( + KeyoskAccount, null=False, on_delete="CASCADE", backref="scopes" + ) + access_list = peewee.ForeignKeyField( + KeyoskDomainAccessList, null=False, on_delete="CASCADE" + ) + permission = peewee.ForeignKeyField( + KeyoskDomainPermission, null=False, on_delete="CASCADE" + ) + with_server_secret = peewee.BooleanField(null=False) + with_client_secret = peewee.BooleanField(null=False) + + def __str__(self): + return f"ACL {self.permission.name}@{self.access_list.name} (scope:{'+'.join([item for item in ['server' if self.with_server_secret else '', 'client' if self.with_client_secret else ''] if item])})" diff --git a/keyosk/database/account_acl.py b/keyosk/database/account_acl.py deleted file mode 100644 index 126100c..0000000 --- a/keyosk/database/account_acl.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Account access control list entry model definition - -Access Control Lists (ACLs) are entities that can have permissions assigned to them -under certain conditions. Permissions are the possible permissions that can be granted- -or not granted- to an ACL. An entry in an ACL comprises the ACL identifier, the -permission to grant, and the identity that should be granted the permission. -""" -import peewee - -from keyosk.database._shared import KeyoskBaseModel -from keyosk.database.account import Account -from keyosk.database.domain import DomainAccessList -from keyosk.database.domain import DomainPermission - - -class AccountACLEntry(KeyoskBaseModel): - """Access control list entry model definition - - :attribute account: Account the ACL entry applies to - :attribute access_list: The access list the entry is for - :attribute permission: The permission the entry is for - :attribute with_server_secret: Whether the permission should be applied when the - account authenticates with the account's - server-set-secret - :attribute with_client_secret: Whether the permission should be applied when the - account authenticates with the account's - client-set-secret - - .. note:: Since permissions are by definition boolean, there is no need to store a - value parameter with an ACL entry: if an entry exists for a given account - for a given access list with a given permission, then that permission is - granted on that access list to that account; similarly, if one does not - exist then it is not granted. - """ - - class Meta: # pylint: disable=missing-docstring,too-few-public-methods - table_name = "account_acl" - - account = peewee.ForeignKeyField(Account, backref="acls") - access_list = peewee.ForeignKeyField(DomainAccessList) - permission = peewee.ForeignKeyField(DomainPermission) - with_server_secret = peewee.BooleanField(null=False) - with_client_secret = peewee.BooleanField(null=False) - - def __iter__(self): - yield "access_list", self.access_list.name - yield "permission", self.permission.name - yield "with_server_secret", self.with_server_secret - yield "with_client_secret", self.with_client_secret - - def __str__(self): - return f"ACL {self.permission.name}@{self.access_list.name} (scope:{'+'.join([item for item in ['server' if self.with_server_secret else '', 'client' if self.with_client_secret else ''] if item])})" diff --git a/keyosk/database/domain.py b/keyosk/database/domain.py index 1c29ddc..2937649 100644 --- a/keyosk/database/domain.py +++ b/keyosk/database/domain.py @@ -1,13 +1,12 @@ """Authentication domain model definition""" import datetime -from typing import List import peewee from keyosk.database._shared import KeyoskBaseModel -class Domain(KeyoskBaseModel): +class KeyoskDomain(KeyoskBaseModel): """Authentication domain storage model :attribute created: Datetime indicating when the domain was first created @@ -53,11 +52,6 @@ class Domain(KeyoskBaseModel): _lifespan_access = peewee.IntegerField(null=False) _lifespan_refresh = peewee.IntegerField(null=False) - @property - def access_list_names(self) -> List[str]: - """Return the list of access list names""" - return [item.name for item in self.access_lists] - @property def lifespan_access(self) -> datetime.timedelta: """Return the access lifespan as a timedelta""" @@ -78,75 +72,25 @@ class Domain(KeyoskBaseModel): """Set the refresh lifespan as an integer from a timedelta""" self._lifespan_refresh = int(value.total_seconds()) - @staticmethod - def dict_keys() -> List[str]: - return [ - "uuid", - "created", - "updated", - "name", - "audience", - "title", - "description", - "contact", - "enabled", - "enable_client_set_auth", - "enable_server_set_auth", - "enable_refresh", - "lifespan_access", - "lifespan_refresh", - "access_list_names", - "permissions", - ] - - @staticmethod - def foreign_backref() -> List[str]: - return ["permissions"] - def __str__(self) -> str: return f"Domain '{self.name}' ({self.uuid})" -class DomainAccessList(KeyoskBaseModel): - """Access list name model definition +class KeyoskDomainAccessList(KeyoskBaseModel): + class Meta: # pylint: disable=too-few-public-methods,missing-docstring + table_name = "domain_access_list" - :attribute name: Name of the access control list - :attribute domain: Authentication domain the ACL applies to - """ - - class Meta: # pylint: disable=missing-docstring,too-few-public-methods - table_name = "domain_acl" - - name = peewee.CharField(null=False, unique=True) - domain = peewee.ForeignKeyField(Domain, backref="access_lists") - - @staticmethod - def dict_keys() -> List[str]: - return ["name"] - - def __str__(self) -> str: - return self.name + domain = peewee.ForeignKeyField( + KeyoskDomain, null=False, on_delete="CASCADE", backref="access_lists" + ) + name = peewee.CharField(null=False) -class DomainPermission(KeyoskBaseModel): - """Permission name model definition - - :attribute name: Name of the permission - :attribute bitindex: Index in the generated bitmask that indicates this permission; - zero-indexed - :attribute domain: Authentication domain the permission should apply to the ACLs of - """ - - class Meta: # pylint: disable=missing-docstring,too-few-public-methods +class KeyoskDomainPermission(KeyoskBaseModel): + class Meta: # pylint: disable=too-few-public-methods,missing-docstring table_name = "domain_permission" + domain = peewee.ForeignKeyField( + KeyoskDomain, null=False, on_delete="CASCADE", backref="permissions" + ) name = peewee.CharField(null=False) - bitindex = peewee.IntegerField(null=False) - domain = peewee.ForeignKeyField(Domain, backref="permissions") - - @staticmethod - def dict_keys() -> List[str]: - return ["name", "bitindex"] - - def __str__(self) -> str: - return self.name diff --git a/keyosk/database/token.py b/keyosk/database/token.py index adb96b2..d935bcf 100644 --- a/keyosk/database/token.py +++ b/keyosk/database/token.py @@ -6,8 +6,8 @@ import peewee from keyosk import datatypes from keyosk.database._shared import KeyoskBaseModel -from keyosk.database.account import Account -from keyosk.database.domain import Domain +from keyosk.database.account import KeyoskAccount +from keyosk.database.domain import KeyoskDomain class Token(KeyoskBaseModel): @@ -33,22 +33,28 @@ class Token(KeyoskBaseModel): class Meta: # pylint: disable=missing-docstring,too-few-public-methods table_name = "token" - account = peewee.ForeignKeyField(Account, backref="tokens", null=True) - domain = peewee.ForeignKeyField(Domain, backref="tokens", null=True) + account = peewee.ForeignKeyField( + KeyoskAccount, backref="tokens", null=True, on_delete="SET NULL" + ) + domain = peewee.ForeignKeyField( + KeyoskDomain, backref="tokens", null=True, on_delete="SET NULL" + ) 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) refresh_expires = peewee.DateTimeField(null=True) - _claims = peewee.CharField(null=False) + hash_publickey = peewee.CharField(null=False) + hash_blacklist = peewee.CharField(null=False) + _scopes = peewee.CharField(null=False, default="[]") @property - def claims(self) -> datatypes.TokenClaims: + def scopes(self) -> datatypes.TokenClaims: """Return the claims dictionary""" - return json.loads(self._claims) + return json.loads(self._scopes) - @claims.setter - def claims(self, value: datatypes.TokenClaims): + @scopes.setter + def scopes(self, value: datatypes.TokenClaims): """Set the claims dictionary""" - self._claims = json.dumps(value) + self._scopes = json.dumps(value)