Release v0.1.5

This commit is contained in:
2026-07-07 15:49:06 +02:00
parent f37c6668e5
commit efb69b3d2d
43 changed files with 6464 additions and 192 deletions

View File

@@ -4,10 +4,11 @@ import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, JSON, String, Text, UniqueConstraint, text
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, String, Text, UniqueConstraint, JSON, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from govoplan_access.backend.db.base import AccessBase, TimestampMixin
from govoplan_tenancy.backend.db.models import Tenant
def new_uuid() -> str:
@@ -15,11 +16,13 @@ def new_uuid() -> str:
class Account(AccessBase, TimestampMixin):
__tablename__ = "access_accounts"
"""Global login identity shared by one or more tenant memberships."""
__tablename__ = "accounts"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
email: Mapped[str] = mapped_column(String(320), nullable=False)
normalized_email: Mapped[str] = mapped_column(String(320), nullable=False, unique=True, index=True)
normalized_email: Mapped[str] = mapped_column(String(320), unique=True, nullable=False, index=True)
display_name: Mapped[str | None] = mapped_column(String(255))
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
auth_provider: Mapped[str] = mapped_column(String(50), default="local", nullable=False)
@@ -27,72 +30,61 @@ class Account(AccessBase, TimestampMixin):
password_reset_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
memberships: Mapped[list[Membership]] = relationship(back_populates="account", cascade="all, delete-orphan")
memberships: Mapped[list[User]] = relationship(back_populates="account")
auth_sessions: Mapped[list[AuthSession]] = relationship(back_populates="account", cascade="all, delete-orphan")
system_role_assignments: Mapped[list[SystemRoleAssignment]] = relationship(
back_populates="account", cascade="all, delete-orphan"
)
class Tenant(AccessBase, TimestampMixin):
__tablename__ = "access_tenants"
class User(AccessBase, TimestampMixin):
__tablename__ = "users"
__table_args__ = (
UniqueConstraint("tenant_id", "email", name="uq_users_tenant_email"),
UniqueConstraint("tenant_id", "account_id", name="uq_users_tenant_account"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
slug: Mapped[str] = mapped_column(String(100), nullable=False, unique=True, index=True)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
default_locale: Mapped[str] = mapped_column(String(20), default="en", nullable=False)
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
memberships: Mapped[list[Membership]] = relationship(back_populates="tenant", cascade="all, delete-orphan")
class Membership(AccessBase, TimestampMixin):
__tablename__ = "access_memberships"
__table_args__ = (UniqueConstraint("tenant_id", "account_id", name="uq_access_memberships_tenant_account"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("access_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
account_id: Mapped[str] = mapped_column(ForeignKey("access_accounts.id", ondelete="CASCADE"), nullable=False, index=True)
email_snapshot: Mapped[str | None] = mapped_column(String(320), index=True)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
account_id: Mapped[str] = mapped_column(ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True)
email: Mapped[str] = mapped_column(String(320), nullable=False, index=True)
display_name: Mapped[str | None] = mapped_column(String(255))
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
is_tenant_admin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
auth_provider: Mapped[str] = mapped_column(String(50), default="local", nullable=False)
password_hash: Mapped[str | None] = mapped_column(String(500))
last_login_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
mail_profile_policy: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
tenant: Mapped[Tenant] = relationship(back_populates="users")
account: Mapped[Account] = relationship(back_populates="memberships")
tenant: Mapped[Tenant] = relationship(back_populates="memberships")
api_keys: Mapped[list[ApiKey]] = relationship(back_populates="user", cascade="all, delete-orphan")
auth_sessions: Mapped[list[AuthSession]] = relationship(back_populates="user", cascade="all, delete-orphan")
class Group(AccessBase, TimestampMixin):
__tablename__ = "access_groups"
__table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_access_groups_tenant_slug"),)
__tablename__ = "groups"
__table_args__ = (UniqueConstraint("tenant_id", "slug", name="uq_groups_tenant_slug"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("access_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
slug: Mapped[str] = mapped_column(String(100), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
template_id: Mapped[str | None] = mapped_column(String(100), index=True)
is_protected: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
system_template_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
system_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
class GroupMembership(AccessBase, TimestampMixin):
__tablename__ = "access_group_memberships"
__table_args__ = (
UniqueConstraint("tenant_id", "membership_id", "group_id", name="uq_access_group_memberships_member_group"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("access_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
membership_id: Mapped[str] = mapped_column(ForeignKey("access_memberships.id", ondelete="CASCADE"), nullable=False, index=True)
group_id: Mapped[str] = mapped_column(ForeignKey("access_groups.id", ondelete="CASCADE"), nullable=False, index=True)
mail_profile_policy: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
class Role(AccessBase, TimestampMixin):
__tablename__ = "access_roles"
__tablename__ = "roles"
__table_args__ = (
UniqueConstraint("tenant_id", "slug", name="uq_access_roles_tenant_slug"),
UniqueConstraint("tenant_id", "slug", name="uq_roles_tenant_slug"),
Index(
"uq_access_roles_system_slug",
"uq_roles_system_slug",
"slug",
unique=True,
sqlite_where=text("tenant_id IS NULL"),
@@ -101,136 +93,106 @@ class Role(AccessBase, TimestampMixin):
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str | None] = mapped_column(ForeignKey("access_tenants.id", ondelete="CASCADE"), nullable=True, index=True)
tenant_id: Mapped[str | None] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True, index=True)
slug: Mapped[str] = mapped_column(String(100), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
permissions: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
level: Mapped[str] = mapped_column(String(20), default="tenant", nullable=False, index=True)
permissions: Mapped[list[str]] = mapped_column(JSON, default=list)
is_builtin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
is_assignable: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
template_id: Mapped[str | None] = mapped_column(String(100), index=True)
is_protected: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
system_template_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
system_required: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
class RoleBinding(AccessBase, TimestampMixin):
__tablename__ = "access_role_bindings"
__table_args__ = (
Index("ix_access_role_bindings_subject", "subject_type", "subject_id"),
Index("ix_access_role_bindings_tenant_subject", "tenant_id", "subject_type", "subject_id"),
)
class SystemRoleAssignment(AccessBase, TimestampMixin):
__tablename__ = "system_role_assignments"
__table_args__ = (UniqueConstraint("account_id", "role_id", name="uq_system_role_assignments"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str | None] = mapped_column(ForeignKey("access_tenants.id", ondelete="CASCADE"), nullable=True, index=True)
role_id: Mapped[str] = mapped_column(ForeignKey("access_roles.id", ondelete="CASCADE"), nullable=False, index=True)
subject_type: Mapped[str] = mapped_column(String(50), nullable=False)
subject_id: Mapped[str] = mapped_column(String(36), nullable=False)
created_by_account_id: Mapped[str | None] = mapped_column(String(36), index=True)
created_by_membership_id: Mapped[str | None] = mapped_column(String(36), index=True)
account_id: Mapped[str] = mapped_column(ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True)
role_id: Mapped[str] = mapped_column(ForeignKey("roles.id", ondelete="CASCADE"), nullable=False, index=True)
account: Mapped[Account] = relationship(back_populates="system_role_assignments")
role: Mapped[Role] = relationship()
class PermissionCatalogEntry(AccessBase, TimestampMixin):
__tablename__ = "access_permission_catalog"
scope: Mapped[str] = mapped_column(String(255), primary_key=True)
module_id: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
resource: Mapped[str] = mapped_column(String(100), nullable=False)
action: Mapped[str] = mapped_column(String(100), nullable=False)
label: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str] = mapped_column(Text, default="", nullable=False)
category: Mapped[str] = mapped_column(String(100), nullable=False)
level: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
is_deprecated: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
class RoleTemplateModel(AccessBase, TimestampMixin):
__tablename__ = "access_role_templates"
__table_args__ = (UniqueConstraint("module_id", "level", "slug", name="uq_access_role_templates_module_level_slug"),)
class UserGroupMembership(AccessBase, TimestampMixin):
__tablename__ = "user_group_memberships"
__table_args__ = (UniqueConstraint("tenant_id", "user_id", "group_id", name="uq_user_group_memberships"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
module_id: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
slug: Mapped[str] = mapped_column(String(100), nullable=False)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
group_id: Mapped[str] = mapped_column(ForeignKey("groups.id", ondelete="CASCADE"), nullable=False, index=True)
class UserRoleAssignment(AccessBase, TimestampMixin):
__tablename__ = "user_role_assignments"
__table_args__ = (UniqueConstraint("tenant_id", "user_id", "role_id", name="uq_user_role_assignments"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
role_id: Mapped[str] = mapped_column(ForeignKey("roles.id", ondelete="CASCADE"), nullable=False, index=True)
class GroupRoleAssignment(AccessBase, TimestampMixin):
__tablename__ = "group_role_assignments"
__table_args__ = (UniqueConstraint("tenant_id", "group_id", "role_id", name="uq_group_role_assignments"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
group_id: Mapped[str] = mapped_column(ForeignKey("groups.id", ondelete="CASCADE"), nullable=False, index=True)
role_id: Mapped[str] = mapped_column(ForeignKey("roles.id", ondelete="CASCADE"), nullable=False, index=True)
class ApiKey(AccessBase, TimestampMixin):
__tablename__ = "api_keys"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text)
permissions: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
level: Mapped[str] = mapped_column(String(20), default="tenant", nullable=False)
managed: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
protected: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
prefix: Mapped[str] = mapped_column(String(16), nullable=False, index=True)
key_hash: Mapped[str] = mapped_column(String(128), nullable=False)
scopes: Mapped[list[str]] = mapped_column(JSON, default=list)
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
class ModuleInstallation(AccessBase, TimestampMixin):
__tablename__ = "access_module_installations"
module_id: Mapped[str] = mapped_column(String(100), primary_key=True)
version: Mapped[str] = mapped_column(String(50), nullable=False)
enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
user: Mapped[User] = relationship(back_populates="api_keys")
class AuthSession(AccessBase, TimestampMixin):
__tablename__ = "access_auth_sessions"
__tablename__ = "auth_sessions"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str | None] = mapped_column(ForeignKey("access_tenants.id", ondelete="CASCADE"), nullable=True, index=True)
membership_id: Mapped[str | None] = mapped_column(ForeignKey("access_memberships.id", ondelete="CASCADE"), nullable=True, index=True)
account_id: Mapped[str] = mapped_column(ForeignKey("access_accounts.id", ondelete="CASCADE"), nullable=False, index=True)
token_hash: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True)
csrf_token_hash: Mapped[str | None] = mapped_column(String(255))
tenant_id: Mapped[str] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True)
user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
account_id: Mapped[str] = mapped_column(ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True)
token_hash: Mapped[str] = mapped_column(String(128), nullable=False, unique=True, index=True)
csrf_token_hash: Mapped[str | None] = mapped_column(String(128), nullable=True)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True)
user_agent: Mapped[str | None] = mapped_column(String(500))
ip_address: Mapped[str | None] = mapped_column(String(100))
class ApiKey(AccessBase, TimestampMixin):
__tablename__ = "access_api_keys"
__table_args__ = (Index("ix_access_api_keys_owner", "owner_subject_type", "owner_subject_id"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("access_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
owner_subject_type: Mapped[str] = mapped_column(String(50), nullable=False)
owner_subject_id: Mapped[str] = mapped_column(String(36), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
prefix: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
key_hash: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
scopes: Mapped[list[str]] = mapped_column(JSON, default=list, nullable=False)
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True)
user: Mapped[User] = relationship(back_populates="auth_sessions")
account: Mapped[Account] = relationship(back_populates="auth_sessions")
class TenantDatastore(AccessBase, TimestampMixin):
__tablename__ = "access_tenant_datastores"
__table_args__ = (UniqueConstraint("tenant_id", "module_id", name="uq_access_tenant_datastores_tenant_module"),)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(ForeignKey("access_tenants.id", ondelete="CASCADE"), nullable=False, index=True)
module_id: Mapped[str] = mapped_column(String(100), default="*", nullable=False)
mode: Mapped[str] = mapped_column(String(20), default="shared", nullable=False)
database_url_secret_ref: Mapped[str | None] = mapped_column(String(255))
schema_name: Mapped[str | None] = mapped_column(String(100))
storage_bucket: Mapped[str | None] = mapped_column(String(255))
region: Mapped[str | None] = mapped_column(String(100))
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
class SystemSettings(AccessBase, TimestampMixin):
__tablename__ = "access_system_settings"
id: Mapped[str] = mapped_column(String(36), primary_key=True, default="global")
default_locale: Mapped[str] = mapped_column(String(20), default="en", nullable=False)
allow_tenant_custom_groups: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
allow_tenant_custom_roles: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
allow_tenant_api_keys: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
class TenantSettings(AccessBase, TimestampMixin):
__tablename__ = "access_tenant_settings"
tenant_id: Mapped[str] = mapped_column(ForeignKey("access_tenants.id", ondelete="CASCADE"), primary_key=True)
allow_custom_groups: Mapped[bool | None] = mapped_column(Boolean)
allow_custom_roles: Mapped[bool | None] = mapped_column(Boolean)
allow_api_keys: Mapped[bool | None] = mapped_column(Boolean)
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
__all__ = [
"Account",
"ApiKey",
"AuthSession",
"Group",
"GroupRoleAssignment",
"Role",
"SystemRoleAssignment",
"Tenant",
"User",
"UserGroupMembership",
"UserRoleAssignment",
"new_uuid",
]