Inplement user Roles

This commit is contained in:
Sebastiaan
2025-05-26 00:35:30 +02:00
parent 46610c6cbd
commit 2ce051a5f3
13 changed files with 397 additions and 35 deletions

View File

@@ -1,7 +1,16 @@
from enum import IntFlag, Enum # Python 3.11 >= StrEnum
from enum import auto as auto_enum
from sqlmodel import SQLModel
from uuid import UUID as RowId
__all__ = [
'RowId',
'DocumentedStrEnum',
'DocumentedIntFlag',
'auto_enum',
'BaseSQLModel',
]
# region SQLModel base class ###################################################
@@ -12,6 +21,21 @@ class BaseSQLModel(SQLModel):
# endregion
# region enum # Fields #########################################################
class DocumentedStrEnum(str, Enum):
pass
class DocumentedIntFlag(IntFlag):
pass
# endregion
# region Generic message #######################################################

View File

@@ -6,12 +6,16 @@ from sqlmodel import Field
from .base import RowId as RowIdType
class Name(BaseModel):
name: str | None = Field(default=None, nullable=False, unique=True, max_length=255)
class FullName(BaseModel):
full_name: str | None = Field(default=None, nullable=True, max_length=255)
class IsActive(BaseModel):
is_active: bool | None = Field(default=False, nullable=False)
is_active: bool | None = Field(default=True, nullable=False)
class IsVerified(BaseModel):
@@ -56,3 +60,7 @@ class RowId(BaseModel):
class RowIdPublic(RowId):
id: RowIdType
class Description(BaseModel):
description: str | None = Field(default=None, nullable=True, max_length=512)

View File

@@ -8,6 +8,10 @@ from app.core.config import settings
from app.core.security import get_password_hash, verify_password
from .base import (
RowId,
DocumentedStrEnum,
DocumentedIntFlag,
auto_enum,
BaseSQLModel,
)
from . import mixin
@@ -16,6 +20,47 @@ from . import mixin
# region User ##################################################################
class PermissionModule(DocumentedStrEnum):
SYSTEM = auto_enum()
USER = auto_enum()
class PermissionPart(DocumentedStrEnum):
ADMIN = auto_enum()
HEALTHCHECK = auto_enum()
class PermissionRight(DocumentedIntFlag):
CREATE = auto_enum()
READ = auto_enum()
UPDATE = auto_enum()
DELETE = auto_enum()
ADMIN = CREATE | READ | UPDATE | DELETE
# #############################################################################
# link to User (many-to-many)
class UserRoleLink(BaseSQLModel, table=True):
user_id: RowId | None = Field(
default=None,
foreign_key="user.id",
primary_key=True,
nullable=False,
ondelete="CASCADE",
)
role_id: RowId | None = Field(
default=None,
foreign_key="role.id",
primary_key=True,
nullable=False,
ondelete="CASCADE",
)
# #############################################################################
# Shared properties
class UserBase(
mixin.UserName,
@@ -34,7 +79,7 @@ class UserCreate(mixin.Password, UserBase):
pass
class UserRegister(mixin.Password, BaseSQLModel):
class UserRegister(mixin.Password, mixin.FullName, BaseSQLModel):
email: EmailStr = Field(max_length=255)
@@ -60,6 +105,7 @@ class User(mixin.RowId, UserBase, table=True):
# --- back_populates links -------------------------------------------------
# --- many-to-many links ---------------------------------------------------
roles: list["Role"] = Relationship(back_populates="users", link_model=UserRoleLink)
# --- CRUD actions ---------------------------------------------------------
@classmethod
@@ -107,6 +153,64 @@ class User(mixin.RowId, UserBase, table=True):
return None
return db_obj
def add_role(self, *, name: str = None, id: RowId = None, db_obj: "Role" = None, session: Session) -> "User":
if db_obj:
pass
elif name:
db_obj = session.exec(select(Role).where(Role.name == name)).first()
elif id:
db_obj = session.exec(select(Role).where(Role.id == id)).first()
to_add = next((add for add in self.roles if add == db_obj), None)
if not to_add:
self.roles.append(db_obj)
session.commit()
return self
def remove_role(self, *, name: str = None, id: RowId = None, db_obj: "Role" = None, session: Session) -> "User":
if db_obj:
pass
elif name:
db_obj = session.exec(select(Role).where(Role.name == name)).first()
elif id:
db_obj = session.exec(select(Role).where(Role.id == id)).first()
to_remove = next((remove for remove in self.roles if remove == db_obj), None)
if to_remove:
statement = select(UserRoleLink).where(
UserRoleLink.user_id == self.id,
UserRoleLink.role_id == db_obj.id
)
link_to_remove = session.exec(statement).first()
if link_to_remove:
session.delete(link_to_remove)
session.commit()
return self
def has_permission(
self,
module: PermissionModule,
part: PermissionPart,
rights: PermissionRight | None = None,
) -> bool:
return any(
any(
(
link.permission.module == module
and link.permission.part == part
and link.permission.is_active
and (not rights or (link.rights & rights) == rights)
)
for link in role.permission_links
if role.is_active
)
for role in self.roles
)
# Properties to return via API, id is always required
class UserPublic(mixin.RowIdPublic, UserBase):
@@ -141,3 +245,116 @@ class NewPassword(BaseSQLModel):
# endregion
# region Permissions ###########################################################
# link to Roles (many-to-many)
class RolePermissionLink(BaseSQLModel, table=True):
role_id: RowId | None = Field(
default=None,
foreign_key="role.id",
primary_key=True,
nullable=False,
ondelete="CASCADE",
)
permission_id: RowId | None = Field(
default=None,
foreign_key="permission.id",
primary_key=True,
nullable=False,
ondelete="CASCADE",
)
rights: "PermissionRight | None" = Field(default=0, nullable=False)
role: "Role" = Relationship(back_populates="permission_links")
permission: "Permission" = Relationship(back_populates="role_links")
# #############################################################################
# TODO: if we want to mange roles add all crud classes
class Role(
mixin.RowId, mixin.Name, mixin.IsActive, mixin.Description, BaseSQLModel, table=True
):
# --- database only items --------------------------------------------------
# --- many-to-many links ---------------------------------------------------
permission_links: list["RolePermissionLink"] = Relationship(back_populates="role")
users: list["User"] = Relationship(back_populates="roles", link_model=UserRoleLink)
# --- CRUD actions ---------------------------------------------------------
@classmethod
def create(cls, *, session: Session, create_obj: "Role") -> "Role":
data_obj = create_obj.model_dump(exclude_unset=True)
db_obj = cls.model_validate(data_obj)
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
def add_permission(
self, add: "Permission", *, session: Session, right: PermissionRight = None
) -> "Role":
link = next(
(link for link in self.permission_links if link.permission == add),
None,
)
if link:
link.rights = right
else:
self.permission_links.append(
RolePermissionLink(
role=self,
permission=add,
rights=right,
)
)
session.add(self.permission_links[-1])
session.commit()
return self
def remove_permission(self, remove: "Permission", *, session: Session) -> "Role":
link = next(
(link for link in self.permission_links if link.permission == remove),
None,
)
if link:
session.delete(link)
session.commit()
return self
# #############################################################################
# All Permission will be generated during db init
class Permission(
mixin.RowId, mixin.IsActive, mixin.Description, BaseSQLModel, table=True
):
# --- database only items --------------------------------------------------
module: PermissionModule = Field(nullable=False)
part: PermissionPart = Field(nullable=False)
# --- many-to-many links ---------------------------------------------------
role_links: list["RolePermissionLink"] = Relationship(back_populates="permission")
# --- CRUD actions ---------------------------------------------------------
@classmethod
def create(cls, *, session: Session, create_obj: "Permission") -> "Permission":
data_obj = create_obj.model_dump(exclude_unset=True)
db_obj = cls.model_validate(data_obj)
session.add(db_obj)
session.commit()
session.refresh(db_obj)
return db_obj
# endregion