2022-03-28 20:58:40 +00:00
|
|
|
"""
|
2022-03-28 21:54:39 +00:00
|
|
|
Python representation of `user` table.
|
2022-03-28 20:58:40 +00:00
|
|
|
"""
|
|
|
|
|
2022-03-27 01:17:48 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-03-30 01:51:43 +00:00
|
|
|
from typing import Any, Iterable, Sequence
|
2022-03-27 01:17:48 +00:00
|
|
|
|
|
|
|
from pydantic import root_validator
|
|
|
|
from sqlalchemy.exc import IntegrityError
|
2022-03-27 13:47:38 +00:00
|
|
|
from sqlmodel import Field, Relationship, SQLModel
|
2022-03-27 01:17:48 +00:00
|
|
|
|
|
|
|
from ..config import Config
|
|
|
|
from .connection import Connection
|
2022-03-28 00:43:28 +00:00
|
|
|
from .device import Device
|
2022-03-29 19:57:33 +00:00
|
|
|
from .tag import Tag, TagValue
|
2022-03-27 01:17:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
class UserBase(SQLModel):
|
2022-03-28 20:58:40 +00:00
|
|
|
"""
|
|
|
|
Common to all representations of users
|
|
|
|
"""
|
|
|
|
|
2022-03-27 01:17:48 +00:00
|
|
|
name: str = Field(primary_key=True)
|
2022-03-31 16:34:36 +00:00
|
|
|
email: str
|
2022-03-27 01:17:48 +00:00
|
|
|
|
2022-03-30 08:30:20 +00:00
|
|
|
country: str | None = Field(default=None, max_length=2)
|
2022-03-27 01:17:48 +00:00
|
|
|
state: str | None = Field(default=None)
|
|
|
|
city: str | None = Field(default=None)
|
|
|
|
organization: str | None = Field(default=None)
|
|
|
|
organizational_unit: str | None = Field(default=None)
|
|
|
|
|
|
|
|
|
2022-03-28 20:58:40 +00:00
|
|
|
class UserCreate(UserBase):
|
|
|
|
"""
|
|
|
|
Representation of a newly created user
|
|
|
|
"""
|
|
|
|
|
|
|
|
password: str | None = Field(default=None)
|
|
|
|
password_clear: str | None = Field(default=None)
|
|
|
|
|
|
|
|
@root_validator
|
|
|
|
@classmethod
|
|
|
|
def hash_password(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
"""
|
|
|
|
Ensure the `password` value of this user gets set.
|
|
|
|
"""
|
|
|
|
|
|
|
|
if (values.get("password")) is not None:
|
|
|
|
# password is set
|
|
|
|
return values
|
|
|
|
|
|
|
|
if (password_clear := values.get("password_clear")) is None:
|
|
|
|
raise ValueError("No password to hash")
|
|
|
|
|
2022-03-31 16:32:07 +00:00
|
|
|
if (current_config := Config.load()) is None:
|
2022-03-28 20:58:40 +00:00
|
|
|
raise ValueError("Not configured")
|
|
|
|
|
2022-03-30 10:15:24 +00:00
|
|
|
values["password"] = current_config.crypto.context.hash(
|
2022-03-28 20:58:40 +00:00
|
|
|
password_clear)
|
|
|
|
|
|
|
|
return values
|
|
|
|
|
|
|
|
|
|
|
|
class UserRead(UserBase):
|
|
|
|
"""
|
|
|
|
Representation of a user read via the API
|
|
|
|
"""
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2022-03-27 01:17:48 +00:00
|
|
|
class User(UserBase, table=True):
|
2022-03-28 20:58:40 +00:00
|
|
|
"""
|
2022-03-28 21:54:39 +00:00
|
|
|
Representation of `user` table
|
2022-03-28 20:58:40 +00:00
|
|
|
"""
|
|
|
|
|
2022-03-27 01:17:48 +00:00
|
|
|
password: str
|
|
|
|
|
2022-03-29 19:57:33 +00:00
|
|
|
tags: list[Tag] = Relationship(
|
2022-03-27 13:47:38 +00:00
|
|
|
back_populates="user",
|
|
|
|
sa_relationship_kwargs={
|
|
|
|
"lazy": "joined",
|
|
|
|
"cascade": "all, delete-orphan",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
2022-03-28 00:43:28 +00:00
|
|
|
devices: list[Device] = Relationship(
|
|
|
|
back_populates="owner",
|
|
|
|
)
|
|
|
|
|
2022-03-27 01:17:48 +00:00
|
|
|
@classmethod
|
2022-03-29 00:01:28 +00:00
|
|
|
def create(
|
|
|
|
cls,
|
|
|
|
*,
|
|
|
|
user: UserCreate,
|
|
|
|
) -> User | None:
|
2022-03-27 01:17:48 +00:00
|
|
|
"""
|
|
|
|
Create a new user in the database.
|
|
|
|
"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
with Connection.session as db:
|
2022-03-29 00:01:28 +00:00
|
|
|
new_user = cls.from_orm(user)
|
2022-03-27 01:17:48 +00:00
|
|
|
|
2022-03-29 00:01:28 +00:00
|
|
|
db.add(new_user)
|
2022-03-27 01:17:48 +00:00
|
|
|
db.commit()
|
2022-03-29 00:01:28 +00:00
|
|
|
db.refresh(new_user)
|
2022-03-27 01:17:48 +00:00
|
|
|
|
2022-03-29 00:01:28 +00:00
|
|
|
return new_user
|
2022-03-27 01:17:48 +00:00
|
|
|
|
|
|
|
except IntegrityError:
|
|
|
|
# user already existed
|
|
|
|
return None
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get(cls, name: str) -> User | None:
|
2022-03-27 13:47:18 +00:00
|
|
|
"""
|
|
|
|
Load user from database by name.
|
|
|
|
"""
|
|
|
|
|
2022-03-27 01:17:48 +00:00
|
|
|
with Connection.session as db:
|
|
|
|
return db.get(cls, name)
|
|
|
|
|
2022-03-27 13:47:18 +00:00
|
|
|
@classmethod
|
|
|
|
def authenticate(
|
|
|
|
cls,
|
|
|
|
name: str,
|
|
|
|
password: str,
|
|
|
|
) -> User | None:
|
|
|
|
"""
|
|
|
|
Authenticate with name/password against users in database.
|
|
|
|
"""
|
|
|
|
|
2022-03-30 10:15:24 +00:00
|
|
|
crypt_context = Config._.crypto.context
|
2022-03-27 13:47:18 +00:00
|
|
|
|
|
|
|
if (user := cls.get(name)) is None:
|
|
|
|
# nonexistent user, fake doing password verification
|
|
|
|
crypt_context.dummy_verify()
|
|
|
|
return None
|
|
|
|
|
|
|
|
if not crypt_context.verify(password, user.password):
|
|
|
|
# password hash mismatch
|
|
|
|
return None
|
2022-03-27 01:17:48 +00:00
|
|
|
|
2022-04-01 06:20:20 +00:00
|
|
|
if not (user.has_tag(TagValue.login) or user.is_admin):
|
2022-03-29 23:36:23 +00:00
|
|
|
# no login permission
|
|
|
|
return None
|
|
|
|
|
2022-03-27 13:47:18 +00:00
|
|
|
return user
|
|
|
|
|
|
|
|
def update(self) -> None:
|
|
|
|
"""
|
|
|
|
Update this user in the database.
|
|
|
|
"""
|
|
|
|
|
|
|
|
with Connection.session as db:
|
|
|
|
db.add(self)
|
|
|
|
db.commit()
|
|
|
|
db.refresh(self)
|
|
|
|
|
2022-03-28 20:18:00 +00:00
|
|
|
def delete(self) -> None:
|
2022-03-27 13:47:18 +00:00
|
|
|
"""
|
|
|
|
Delete this user from the database.
|
|
|
|
"""
|
|
|
|
|
|
|
|
with Connection.session as db:
|
|
|
|
db.delete(self)
|
|
|
|
db.commit()
|
2022-03-27 13:47:38 +00:00
|
|
|
|
2022-04-01 06:20:20 +00:00
|
|
|
@property
|
|
|
|
def __tags(self) -> Iterable[TagValue]:
|
2022-03-28 20:58:40 +00:00
|
|
|
"""
|
2022-03-29 19:57:33 +00:00
|
|
|
Return the tags of this user.
|
2022-03-28 20:58:40 +00:00
|
|
|
"""
|
|
|
|
|
2022-03-30 01:51:43 +00:00
|
|
|
return (
|
2022-03-29 19:57:33 +00:00
|
|
|
tag._
|
|
|
|
for tag in self.tags
|
2022-03-28 00:48:44 +00:00
|
|
|
)
|
2022-03-27 13:47:38 +00:00
|
|
|
|
2022-03-30 01:51:43 +00:00
|
|
|
def has_tag(self, tag: TagValue) -> bool:
|
|
|
|
"""
|
|
|
|
Check if this user has a tag.
|
|
|
|
"""
|
|
|
|
|
2022-04-01 06:20:20 +00:00
|
|
|
return tag in self.__tags
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_admin(self) -> bool:
|
|
|
|
"""
|
|
|
|
Shorthand for checking if this user has the `admin` tag.
|
|
|
|
"""
|
|
|
|
|
|
|
|
return TagValue.admin in self.__tags
|
2022-03-30 01:51:43 +00:00
|
|
|
|
|
|
|
def add_tags(
|
|
|
|
self,
|
|
|
|
tags: Sequence[TagValue],
|
|
|
|
) -> None:
|
|
|
|
"""
|
|
|
|
Add tags to this user.
|
|
|
|
"""
|
|
|
|
|
|
|
|
self.tags = [
|
|
|
|
tag._(self)
|
2022-04-01 06:20:20 +00:00
|
|
|
for tag in (set(self.__tags) | set(tags))
|
2022-03-30 01:51:43 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
def remove_tags(
|
2022-03-28 22:17:31 +00:00
|
|
|
self,
|
2022-03-29 19:57:33 +00:00
|
|
|
tags: Sequence[TagValue],
|
2022-03-28 22:17:31 +00:00
|
|
|
) -> None:
|
2022-03-28 20:58:40 +00:00
|
|
|
"""
|
2022-03-30 01:51:43 +00:00
|
|
|
remove tags from this user.
|
2022-03-28 20:58:40 +00:00
|
|
|
"""
|
|
|
|
|
2022-03-29 19:57:33 +00:00
|
|
|
self.tags = [
|
2022-03-30 01:51:43 +00:00
|
|
|
tag._(self)
|
2022-04-01 06:20:20 +00:00
|
|
|
for tag in (set(self.__tags) - set(tags))
|
2022-03-28 00:48:44 +00:00
|
|
|
]
|
2022-03-29 15:56:12 +00:00
|
|
|
|
2022-04-07 08:53:54 +00:00
|
|
|
def check_edit(
|
2022-03-29 16:35:41 +00:00
|
|
|
self,
|
2022-03-29 23:36:23 +00:00
|
|
|
target: User | Device,
|
2022-04-07 08:53:54 +00:00
|
|
|
) -> None:
|
2022-03-29 16:35:41 +00:00
|
|
|
"""
|
2022-03-29 23:36:23 +00:00
|
|
|
Check if this user can edit another user or a device.
|
2022-03-29 16:35:41 +00:00
|
|
|
"""
|
|
|
|
|
2022-03-29 23:36:23 +00:00
|
|
|
# admin can "edit" everything
|
2022-04-01 06:20:20 +00:00
|
|
|
if self.is_admin:
|
2022-04-07 08:53:54 +00:00
|
|
|
return None
|
2022-03-29 16:35:41 +00:00
|
|
|
|
2022-04-07 08:53:54 +00:00
|
|
|
# user can only "edit" itself
|
|
|
|
if isinstance(target, User) and target == self:
|
|
|
|
return None
|
2022-03-29 16:12:55 +00:00
|
|
|
|
2022-03-29 23:36:23 +00:00
|
|
|
# user can edit its owned devices
|
2022-04-07 08:53:54 +00:00
|
|
|
if isinstance(target, Device) and target.owner == self:
|
|
|
|
return None
|
2022-03-29 16:35:41 +00:00
|
|
|
|
2022-04-07 08:53:54 +00:00
|
|
|
# deny by default
|
|
|
|
raise PermissionError()
|
|
|
|
|
|
|
|
def check_admin(
|
2022-03-29 16:35:41 +00:00
|
|
|
self,
|
2022-03-29 23:36:23 +00:00
|
|
|
target: User | Device,
|
2022-04-07 08:53:54 +00:00
|
|
|
) -> None:
|
2022-03-29 16:35:41 +00:00
|
|
|
"""
|
2022-03-29 23:36:23 +00:00
|
|
|
Check if this user can administer another user or a device.
|
2022-03-29 16:35:41 +00:00
|
|
|
"""
|
|
|
|
|
2022-03-29 23:36:23 +00:00
|
|
|
# only admin can "admin" anything
|
2022-04-01 06:20:20 +00:00
|
|
|
if not self.is_admin:
|
2022-04-07 08:53:54 +00:00
|
|
|
raise PermissionError("Must be admin")
|
2022-03-29 16:35:41 +00:00
|
|
|
|
2022-04-07 08:53:54 +00:00
|
|
|
# admin cannot "admin" itself!
|
2022-03-29 23:36:23 +00:00
|
|
|
if isinstance(target, User) and target == self:
|
2022-04-07 08:53:54 +00:00
|
|
|
raise PermissionError("Can't administer self")
|
2022-03-29 16:35:41 +00:00
|
|
|
|
2022-03-29 23:36:23 +00:00
|
|
|
# admin can "admin" everything else
|
2022-04-07 08:53:54 +00:00
|
|
|
return None
|
2022-03-29 16:35:41 +00:00
|
|
|
|
2022-04-07 08:53:54 +00:00
|
|
|
def check_create(
|
2022-03-29 16:35:41 +00:00
|
|
|
self,
|
2022-03-29 23:36:23 +00:00
|
|
|
target: type,
|
|
|
|
owner: User | None = None,
|
2022-04-07 08:53:54 +00:00
|
|
|
) -> None:
|
2022-03-29 16:35:41 +00:00
|
|
|
"""
|
2022-03-29 23:36:23 +00:00
|
|
|
Check if this user can create another user or a device.
|
2022-03-29 16:35:41 +00:00
|
|
|
"""
|
|
|
|
|
2022-03-29 23:36:23 +00:00
|
|
|
# can never create anything but users or devices
|
|
|
|
if not issubclass(target, (User, Device)):
|
2022-04-07 08:53:54 +00:00
|
|
|
raise PermissionError(f"Cannot create target type {target}")
|
2022-03-29 16:35:41 +00:00
|
|
|
|
2022-03-29 23:36:23 +00:00
|
|
|
# admin can "create" everything
|
2022-04-01 06:20:20 +00:00
|
|
|
if self.is_admin:
|
2022-04-07 08:53:54 +00:00
|
|
|
return None
|
2022-03-29 16:12:55 +00:00
|
|
|
|
2022-03-29 23:36:23 +00:00
|
|
|
# user can only create devices for itself
|
|
|
|
if target is Device and owner == self:
|
2022-04-07 08:53:54 +00:00
|
|
|
return None
|
2022-03-29 15:56:12 +00:00
|
|
|
|
2022-04-07 08:53:54 +00:00
|
|
|
# deny by default
|
|
|
|
raise PermissionError()
|
2022-04-02 21:24:44 +00:00
|
|
|
|
2022-04-05 01:52:58 +00:00
|
|
|
@property
|
|
|
|
def can_issue(self) -> bool:
|
2022-04-02 21:24:44 +00:00
|
|
|
"""
|
|
|
|
Check if this user can issue a certificate without approval.
|
|
|
|
"""
|
|
|
|
|
|
|
|
return (
|
2022-04-05 01:52:58 +00:00
|
|
|
self.is_admin
|
|
|
|
or self.has_tag(TagValue.issue)
|
2022-04-02 21:24:44 +00:00
|
|
|
)
|
|
|
|
|
2022-04-05 01:52:58 +00:00
|
|
|
@property
|
|
|
|
def can_renew(self) -> bool:
|
2022-04-02 21:24:44 +00:00
|
|
|
"""
|
|
|
|
Check if this user can renew a certificate without approval.
|
|
|
|
"""
|
|
|
|
|
|
|
|
return (
|
2022-04-05 01:52:58 +00:00
|
|
|
self.is_admin
|
|
|
|
or self.has_tag(TagValue.renew)
|
2022-04-02 21:24:44 +00:00
|
|
|
)
|