Inplement user Roles
This commit is contained in:
@@ -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 #######################################################
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user