pkg-auth 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pkg_auth/__init__.py +15 -0
- pkg_auth/admin/__init__.py +35 -0
- pkg_auth/admin/cli.py +87 -0
- pkg_auth/admin/client.py +401 -0
- pkg_auth/admin/env.py +74 -0
- pkg_auth/admin/helpers.py +113 -0
- pkg_auth/admin/provision_client.py +86 -0
- pkg_auth/admin/settings.py +33 -0
- pkg_auth/authentication/__init__.py +33 -0
- pkg_auth/authentication/adapters/__init__.py +1 -0
- pkg_auth/authentication/adapters/keycloak/__init__.py +6 -0
- pkg_auth/authentication/adapters/keycloak/jwt_decoder.py +105 -0
- pkg_auth/authentication/application/__init__.py +1 -0
- pkg_auth/authentication/application/use_cases/__init__.py +1 -0
- pkg_auth/authentication/application/use_cases/authenticate.py +91 -0
- pkg_auth/authentication/domain/__init__.py +1 -0
- pkg_auth/authentication/domain/entities.py +50 -0
- pkg_auth/authentication/domain/exceptions.py +18 -0
- pkg_auth/authentication/domain/ports.py +26 -0
- pkg_auth/authentication/domain/value_objects.py +42 -0
- pkg_auth/authorization/__init__.py +117 -0
- pkg_auth/authorization/adapters/__init__.py +1 -0
- pkg_auth/authorization/adapters/cache/__init__.py +32 -0
- pkg_auth/authorization/adapters/cache/decorators.py +181 -0
- pkg_auth/authorization/adapters/cache/memory.py +61 -0
- pkg_auth/authorization/adapters/cache/protocol.py +36 -0
- pkg_auth/authorization/adapters/cache/redis.py +60 -0
- pkg_auth/authorization/adapters/django_orm/__init__.py +37 -0
- pkg_auth/authorization/adapters/django_orm/apps.py +24 -0
- pkg_auth/authorization/adapters/django_orm/mixins.py +142 -0
- pkg_auth/authorization/adapters/django_orm/models.py +226 -0
- pkg_auth/authorization/adapters/django_orm/repositories/__init__.py +20 -0
- pkg_auth/authorization/adapters/django_orm/repositories/membership.py +118 -0
- pkg_auth/authorization/adapters/django_orm/repositories/organization.py +73 -0
- pkg_auth/authorization/adapters/django_orm/repositories/organization_service.py +71 -0
- pkg_auth/authorization/adapters/django_orm/repositories/permission_catalog.py +102 -0
- pkg_auth/authorization/adapters/django_orm/repositories/role.py +120 -0
- pkg_auth/authorization/adapters/django_orm/repositories/service.py +60 -0
- pkg_auth/authorization/adapters/django_orm/repositories/user.py +77 -0
- pkg_auth/authorization/adapters/sqlalchemy/__init__.py +90 -0
- pkg_auth/authorization/adapters/sqlalchemy/base.py +55 -0
- pkg_auth/authorization/adapters/sqlalchemy/migrations/__init__.py +1 -0
- pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260410_0001_initial_schema.py +293 -0
- pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260412_0002_add_permission_is_platform.py +39 -0
- pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0003_permission_visibility.py +65 -0
- pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0004_permission_description_jsonb.py +52 -0
- pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0005_services_tables.py +116 -0
- pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/__init__.py +1 -0
- pkg_auth/authorization/adapters/sqlalchemy/mixins.py +187 -0
- pkg_auth/authorization/adapters/sqlalchemy/models.py +268 -0
- pkg_auth/authorization/adapters/sqlalchemy/repositories/__init__.py +16 -0
- pkg_auth/authorization/adapters/sqlalchemy/repositories/membership.py +146 -0
- pkg_auth/authorization/adapters/sqlalchemy/repositories/organization.py +97 -0
- pkg_auth/authorization/adapters/sqlalchemy/repositories/organization_service.py +106 -0
- pkg_auth/authorization/adapters/sqlalchemy/repositories/permission_catalog.py +127 -0
- pkg_auth/authorization/adapters/sqlalchemy/repositories/role.py +171 -0
- pkg_auth/authorization/adapters/sqlalchemy/repositories/service.py +93 -0
- pkg_auth/authorization/adapters/sqlalchemy/repositories/user.py +74 -0
- pkg_auth/authorization/application/__init__.py +1 -0
- pkg_auth/authorization/application/use_cases/__init__.py +1 -0
- pkg_auth/authorization/application/use_cases/_helpers.py +82 -0
- pkg_auth/authorization/application/use_cases/check_permission.py +21 -0
- pkg_auth/authorization/application/use_cases/create_organization.py +41 -0
- pkg_auth/authorization/application/use_cases/create_role.py +69 -0
- pkg_auth/authorization/application/use_cases/delete_membership.py +21 -0
- pkg_auth/authorization/application/use_cases/delete_organization.py +21 -0
- pkg_auth/authorization/application/use_cases/delete_role.py +23 -0
- pkg_auth/authorization/application/use_cases/list_user_organizations.py +21 -0
- pkg_auth/authorization/application/use_cases/provision_default_services.py +38 -0
- pkg_auth/authorization/application/use_cases/register_permission_catalog.py +122 -0
- pkg_auth/authorization/application/use_cases/resolve_auth_context.py +70 -0
- pkg_auth/authorization/application/use_cases/resolve_user_from_jwt.py +34 -0
- pkg_auth/authorization/application/use_cases/set_organization_service.py +50 -0
- pkg_auth/authorization/application/use_cases/sync_permission_catalog.py +86 -0
- pkg_auth/authorization/application/use_cases/sync_service_catalog.py +91 -0
- pkg_auth/authorization/application/use_cases/sync_user_from_jwt.py +32 -0
- pkg_auth/authorization/application/use_cases/update_organization.py +31 -0
- pkg_auth/authorization/application/use_cases/update_role.py +61 -0
- pkg_auth/authorization/application/use_cases/upsert_membership.py +54 -0
- pkg_auth/authorization/cli/__init__.py +1 -0
- pkg_auth/authorization/cli/sync_catalog.py +180 -0
- pkg_auth/authorization/cli/sync_services.py +151 -0
- pkg_auth/authorization/config.py +21 -0
- pkg_auth/authorization/domain/__init__.py +1 -0
- pkg_auth/authorization/domain/entities.py +192 -0
- pkg_auth/authorization/domain/exceptions.py +68 -0
- pkg_auth/authorization/domain/ports.py +217 -0
- pkg_auth/authorization/domain/value_objects.py +208 -0
- pkg_auth/authorization/platform.py +47 -0
- pkg_auth/integrations/__init__.py +0 -0
- pkg_auth/integrations/django/__init__.py +32 -0
- pkg_auth/integrations/django/apps.py +10 -0
- pkg_auth/integrations/django/auth_context_middleware.py +105 -0
- pkg_auth/integrations/django/decorators.py +74 -0
- pkg_auth/integrations/django/install.py +136 -0
- pkg_auth/integrations/django/middleware.py +63 -0
- pkg_auth/integrations/fastapi/__init__.py +26 -0
- pkg_auth/integrations/fastapi/auth_context_dep.py +150 -0
- pkg_auth/integrations/fastapi/auth_factory.py +84 -0
- pkg_auth/integrations/fastapi/decorators.py +55 -0
- pkg_auth/integrations/fastapi/errors.py +72 -0
- pkg_auth/integrations/fastapi/identity_dep.py +41 -0
- pkg_auth/integrations/strawberry/__init__.py +20 -0
- pkg_auth/integrations/strawberry/auth.py +137 -0
- pkg_auth/integrations/strawberry/permissions.py +56 -0
- pkg_auth-3.0.0.dist-info/METADATA +147 -0
- pkg_auth-3.0.0.dist-info/RECORD +110 -0
- pkg_auth-3.0.0.dist-info/WHEEL +5 -0
- pkg_auth-3.0.0.dist-info/entry_points.txt +4 -0
- pkg_auth-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Add is_platform flag to permissions.
|
|
2
|
+
|
|
3
|
+
Revision ID: pkg_auth_acl_0002
|
|
4
|
+
Revises: pkg_auth_acl_0001
|
|
5
|
+
Create Date: 2026-04-12 00:00:00.000000
|
|
6
|
+
|
|
7
|
+
Adds the ``is_platform`` boolean column so the central ACL UI can
|
|
8
|
+
filter platform-only permissions out of org-scoped role builders.
|
|
9
|
+
Defaults to ``false`` so existing rows remain valid and backwards-
|
|
10
|
+
compatible 2-tuple registration calls keep working.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Sequence, Union
|
|
15
|
+
|
|
16
|
+
import sqlalchemy as sa
|
|
17
|
+
from alembic import op
|
|
18
|
+
|
|
19
|
+
# revision identifiers, used by Alembic.
|
|
20
|
+
revision: str = "pkg_auth_acl_0002"
|
|
21
|
+
down_revision: Union[str, None] = "pkg_auth_acl_0001"
|
|
22
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
23
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def upgrade() -> None:
|
|
27
|
+
op.add_column(
|
|
28
|
+
"permissions",
|
|
29
|
+
sa.Column(
|
|
30
|
+
"is_platform",
|
|
31
|
+
sa.Boolean(),
|
|
32
|
+
nullable=False,
|
|
33
|
+
server_default=sa.text("false"),
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def downgrade() -> None:
|
|
39
|
+
op.drop_column("permissions", "is_platform")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Replace permissions.is_platform with a tri-state visibility column.
|
|
2
|
+
|
|
3
|
+
Revision ID: pkg_auth_acl_0003
|
|
4
|
+
Revises: pkg_auth_acl_0002
|
|
5
|
+
Create Date: 2026-06-20 00:00:00.000000
|
|
6
|
+
|
|
7
|
+
``is_platform`` was a 2-state flag (platform-only vs everywhere). It is
|
|
8
|
+
replaced by a ``visibility`` enum-as-string:
|
|
9
|
+
|
|
10
|
+
- ``platform_only`` (was ``is_platform = true``)
|
|
11
|
+
- ``shared`` (was ``is_platform = false``, the default)
|
|
12
|
+
- ``tenant_only`` (new — hidden from the platform org)
|
|
13
|
+
|
|
14
|
+
Backfill maps the old boolean, then the old column is dropped.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Sequence, Union
|
|
19
|
+
|
|
20
|
+
import sqlalchemy as sa
|
|
21
|
+
from alembic import op
|
|
22
|
+
|
|
23
|
+
# revision identifiers, used by Alembic.
|
|
24
|
+
revision: str = "pkg_auth_acl_0003"
|
|
25
|
+
down_revision: Union[str, None] = "pkg_auth_acl_0002"
|
|
26
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
27
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def upgrade() -> None:
|
|
31
|
+
op.add_column(
|
|
32
|
+
"permissions",
|
|
33
|
+
sa.Column(
|
|
34
|
+
"visibility",
|
|
35
|
+
sa.String(32),
|
|
36
|
+
nullable=False,
|
|
37
|
+
server_default=sa.text("'shared'"),
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
op.execute(
|
|
41
|
+
"UPDATE permissions SET visibility = "
|
|
42
|
+
"CASE WHEN is_platform THEN 'platform_only' ELSE 'shared' END"
|
|
43
|
+
)
|
|
44
|
+
op.create_index(
|
|
45
|
+
"ix_permissions_visibility", "permissions", ["visibility"]
|
|
46
|
+
)
|
|
47
|
+
op.drop_column("permissions", "is_platform")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def downgrade() -> None:
|
|
51
|
+
op.add_column(
|
|
52
|
+
"permissions",
|
|
53
|
+
sa.Column(
|
|
54
|
+
"is_platform",
|
|
55
|
+
sa.Boolean(),
|
|
56
|
+
nullable=False,
|
|
57
|
+
server_default=sa.text("false"),
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
op.execute(
|
|
61
|
+
"UPDATE permissions SET is_platform = "
|
|
62
|
+
"(visibility = 'platform_only')"
|
|
63
|
+
)
|
|
64
|
+
op.drop_index("ix_permissions_visibility", table_name="permissions")
|
|
65
|
+
op.drop_column("permissions", "visibility")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Localize permissions.description: TEXT -> JSONB locale map.
|
|
2
|
+
|
|
3
|
+
Revision ID: pkg_auth_acl_0004
|
|
4
|
+
Revises: pkg_auth_acl_0003
|
|
5
|
+
Create Date: 2026-06-20 00:00:01.000000
|
|
6
|
+
|
|
7
|
+
``description`` becomes a JSONB ``{locale: text}`` map. Existing plain-text
|
|
8
|
+
descriptions are backfilled under the default locale, read from the
|
|
9
|
+
``ACL_DEFAULT_LOCALE`` env var at migration time (fallback ``"en"``).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
from typing import Sequence, Union
|
|
16
|
+
|
|
17
|
+
from alembic import op
|
|
18
|
+
|
|
19
|
+
# revision identifiers, used by Alembic.
|
|
20
|
+
revision: str = "pkg_auth_acl_0004"
|
|
21
|
+
down_revision: Union[str, None] = "pkg_auth_acl_0003"
|
|
22
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
23
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
24
|
+
|
|
25
|
+
_LOCALE_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_-]*$")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _default_locale() -> str:
|
|
29
|
+
loc = os.environ.get("ACL_DEFAULT_LOCALE") or "en"
|
|
30
|
+
if not _LOCALE_RE.match(loc):
|
|
31
|
+
raise ValueError(f"Invalid ACL_DEFAULT_LOCALE {loc!r}")
|
|
32
|
+
return loc
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def upgrade() -> None:
|
|
36
|
+
locale = _default_locale()
|
|
37
|
+
op.execute(
|
|
38
|
+
f"ALTER TABLE permissions "
|
|
39
|
+
f"ALTER COLUMN description TYPE JSONB USING "
|
|
40
|
+
f"CASE WHEN description IS NULL OR description = '' THEN NULL "
|
|
41
|
+
f"ELSE jsonb_build_object('{locale}', description) END"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def downgrade() -> None:
|
|
46
|
+
locale = _default_locale()
|
|
47
|
+
op.execute(
|
|
48
|
+
f"ALTER TABLE permissions "
|
|
49
|
+
f"ALTER COLUMN description TYPE TEXT USING "
|
|
50
|
+
f"COALESCE(description ->> '{locale}', "
|
|
51
|
+
f"(SELECT value FROM jsonb_each_text(description) LIMIT 1))"
|
|
52
|
+
)
|
pkg_auth/authorization/adapters/sqlalchemy/migrations/versions/20260620_0005_services_tables.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Create services and organization_services tables (service guard).
|
|
2
|
+
|
|
3
|
+
Revision ID: pkg_auth_acl_0005
|
|
4
|
+
Revises: pkg_auth_acl_0004
|
|
5
|
+
Create Date: 2026-06-20 00:00:02.000000
|
|
6
|
+
|
|
7
|
+
``services`` is the vendor-controlled service registry; ``auto_provision``
|
|
8
|
+
and ``saas_available`` are set only via ``pkg-auth-sync-services``.
|
|
9
|
+
``organization_services`` is the per-org entitlement that drives the
|
|
10
|
+
default-deny service guard in ``ResolveAuthContextUseCase``.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Sequence, Union
|
|
15
|
+
|
|
16
|
+
import sqlalchemy as sa
|
|
17
|
+
from alembic import op
|
|
18
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
19
|
+
|
|
20
|
+
# revision identifiers, used by Alembic.
|
|
21
|
+
revision: str = "pkg_auth_acl_0005"
|
|
22
|
+
down_revision: Union[str, None] = "pkg_auth_acl_0004"
|
|
23
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
24
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def upgrade() -> None:
|
|
28
|
+
op.create_table(
|
|
29
|
+
"services",
|
|
30
|
+
sa.Column(
|
|
31
|
+
"id",
|
|
32
|
+
sa.Uuid(as_uuid=True),
|
|
33
|
+
primary_key=True,
|
|
34
|
+
server_default=sa.text("gen_random_uuid()"),
|
|
35
|
+
),
|
|
36
|
+
sa.Column("name", sa.String(64), nullable=False),
|
|
37
|
+
sa.Column("display_label", JSONB, nullable=True),
|
|
38
|
+
sa.Column(
|
|
39
|
+
"auto_provision",
|
|
40
|
+
sa.Boolean(),
|
|
41
|
+
nullable=False,
|
|
42
|
+
server_default=sa.text("false"),
|
|
43
|
+
),
|
|
44
|
+
sa.Column(
|
|
45
|
+
"saas_available",
|
|
46
|
+
sa.Boolean(),
|
|
47
|
+
nullable=False,
|
|
48
|
+
server_default=sa.text("false"),
|
|
49
|
+
),
|
|
50
|
+
sa.Column(
|
|
51
|
+
"created_at",
|
|
52
|
+
sa.DateTime(timezone=True),
|
|
53
|
+
server_default=sa.text("now()"),
|
|
54
|
+
nullable=False,
|
|
55
|
+
),
|
|
56
|
+
sa.Column(
|
|
57
|
+
"updated_at",
|
|
58
|
+
sa.DateTime(timezone=True),
|
|
59
|
+
server_default=sa.text("now()"),
|
|
60
|
+
nullable=False,
|
|
61
|
+
),
|
|
62
|
+
sa.UniqueConstraint("name", name="uq_services_name"),
|
|
63
|
+
)
|
|
64
|
+
op.create_index("ix_services_name", "services", ["name"])
|
|
65
|
+
|
|
66
|
+
op.create_table(
|
|
67
|
+
"organization_services",
|
|
68
|
+
sa.Column(
|
|
69
|
+
"id",
|
|
70
|
+
sa.Uuid(as_uuid=True),
|
|
71
|
+
primary_key=True,
|
|
72
|
+
server_default=sa.text("gen_random_uuid()"),
|
|
73
|
+
),
|
|
74
|
+
sa.Column("organization_id", sa.Uuid(as_uuid=True), nullable=False),
|
|
75
|
+
sa.Column("service_name", sa.String(64), nullable=False),
|
|
76
|
+
sa.Column(
|
|
77
|
+
"enabled",
|
|
78
|
+
sa.Boolean(),
|
|
79
|
+
nullable=False,
|
|
80
|
+
server_default=sa.text("true"),
|
|
81
|
+
),
|
|
82
|
+
sa.Column(
|
|
83
|
+
"source",
|
|
84
|
+
sa.String(16),
|
|
85
|
+
nullable=False,
|
|
86
|
+
server_default=sa.text("'manual'"),
|
|
87
|
+
),
|
|
88
|
+
sa.Column(
|
|
89
|
+
"granted_at",
|
|
90
|
+
sa.DateTime(timezone=True),
|
|
91
|
+
server_default=sa.text("now()"),
|
|
92
|
+
nullable=False,
|
|
93
|
+
),
|
|
94
|
+
sa.ForeignKeyConstraint(
|
|
95
|
+
["organization_id"], ["organizations.id"], ondelete="CASCADE"
|
|
96
|
+
),
|
|
97
|
+
sa.UniqueConstraint(
|
|
98
|
+
"organization_id", "service_name",
|
|
99
|
+
name="uq_org_services_org_service",
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
op.create_index(
|
|
103
|
+
"ix_org_services_org_id",
|
|
104
|
+
"organization_services",
|
|
105
|
+
["organization_id"],
|
|
106
|
+
)
|
|
107
|
+
op.create_index(
|
|
108
|
+
"ix_org_services_service_name",
|
|
109
|
+
"organization_services",
|
|
110
|
+
["service_name"],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def downgrade() -> None:
|
|
115
|
+
op.drop_table("organization_services")
|
|
116
|
+
op.drop_table("services")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Alembic migration version files (branch_labels=('pkg_auth_acl',))."""
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Abstract column mixins for the ACL ORM models.
|
|
2
|
+
|
|
3
|
+
These mixins provide the ACL-essential columns without defining ``id``,
|
|
4
|
+
``__tablename__``, FK columns, or relationships. Consuming services
|
|
5
|
+
extend them by creating a concrete model class that inherits from both
|
|
6
|
+
their own ``DeclarativeBase`` and the appropriate mixin.
|
|
7
|
+
|
|
8
|
+
Example (service extends UserMixin)::
|
|
9
|
+
|
|
10
|
+
from pkg_auth.authorization.adapters.sqlalchemy.mixins import UserMixin
|
|
11
|
+
|
|
12
|
+
class OrmUser(Base, UserMixin):
|
|
13
|
+
__tablename__ = "users"
|
|
14
|
+
id: Mapped[UUID] = mapped_column(Uuid, primary_key=True)
|
|
15
|
+
# UserMixin columns inherited automatically
|
|
16
|
+
# Add service-specific columns:
|
|
17
|
+
username: Mapped[str] = mapped_column(String(255))
|
|
18
|
+
bio: Mapped[str | None] = mapped_column(Text)
|
|
19
|
+
|
|
20
|
+
Services that do NOT need to extend use the default concrete models in
|
|
21
|
+
``models.py`` which already inherit these mixins.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
|
|
27
|
+
from sqlalchemy import Boolean, DateTime, String, Text, func, text
|
|
28
|
+
from sqlalchemy.dialects.postgresql import JSONB
|
|
29
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UserMixin:
|
|
33
|
+
"""ACL columns for the users table."""
|
|
34
|
+
|
|
35
|
+
keycloak_sub: Mapped[str] = mapped_column(
|
|
36
|
+
String(64), unique=True, index=True
|
|
37
|
+
)
|
|
38
|
+
email: Mapped[str] = mapped_column(String(255), index=True)
|
|
39
|
+
full_name: Mapped[str | None] = mapped_column(String(255))
|
|
40
|
+
first_seen_at: Mapped[datetime] = mapped_column(
|
|
41
|
+
DateTime(timezone=True), server_default=func.now()
|
|
42
|
+
)
|
|
43
|
+
last_seen_at: Mapped[datetime] = mapped_column(
|
|
44
|
+
DateTime(timezone=True),
|
|
45
|
+
server_default=func.now(),
|
|
46
|
+
onupdate=func.now(),
|
|
47
|
+
)
|
|
48
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
49
|
+
DateTime(timezone=True), server_default=func.now()
|
|
50
|
+
)
|
|
51
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
52
|
+
DateTime(timezone=True),
|
|
53
|
+
server_default=func.now(),
|
|
54
|
+
onupdate=func.now(),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class OrganizationMixin:
|
|
59
|
+
"""ACL columns for the organizations table."""
|
|
60
|
+
|
|
61
|
+
slug: Mapped[str] = mapped_column(String(255), unique=True, index=True)
|
|
62
|
+
name: Mapped[str] = mapped_column(String(255))
|
|
63
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
64
|
+
DateTime(timezone=True), server_default=func.now()
|
|
65
|
+
)
|
|
66
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
67
|
+
DateTime(timezone=True),
|
|
68
|
+
server_default=func.now(),
|
|
69
|
+
onupdate=func.now(),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PermissionMixin:
|
|
74
|
+
"""ACL columns for the permissions (catalog) table.
|
|
75
|
+
|
|
76
|
+
``visibility`` controls which role builders may see/use the permission:
|
|
77
|
+
``platform_only`` (platform org only), ``shared`` (everywhere, default),
|
|
78
|
+
or ``tenant_only`` (normal orgs only — hidden from the platform org).
|
|
79
|
+
Consuming services declare it inline via :class:`CatalogEntry`; the
|
|
80
|
+
central ACL UI filters by it via the ``scope=`` argument on the catalog
|
|
81
|
+
repo. ``description`` is a localized JSONB map (``{locale: text}``).
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
key: Mapped[str] = mapped_column(String(255), unique=True, index=True)
|
|
85
|
+
service_name: Mapped[str] = mapped_column(String(64), index=True)
|
|
86
|
+
description: Mapped[dict | None] = mapped_column(JSONB)
|
|
87
|
+
visibility: Mapped[str] = mapped_column(
|
|
88
|
+
String(32),
|
|
89
|
+
nullable=False,
|
|
90
|
+
default="shared",
|
|
91
|
+
server_default=text("'shared'"),
|
|
92
|
+
index=True,
|
|
93
|
+
)
|
|
94
|
+
registered_at: Mapped[datetime] = mapped_column(
|
|
95
|
+
DateTime(timezone=True),
|
|
96
|
+
server_default=func.now(),
|
|
97
|
+
onupdate=func.now(),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ServiceMixin:
|
|
102
|
+
"""ACL columns for the ``services`` table (the service registry).
|
|
103
|
+
|
|
104
|
+
``auto_provision`` and ``saas_available`` are vendor-controlled and set
|
|
105
|
+
only via the ``pkg-auth-sync-services`` path. ``display_label`` is a
|
|
106
|
+
localized JSONB map.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
name: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
|
110
|
+
display_label: Mapped[dict | None] = mapped_column(JSONB)
|
|
111
|
+
auto_provision: Mapped[bool] = mapped_column(
|
|
112
|
+
Boolean,
|
|
113
|
+
nullable=False,
|
|
114
|
+
default=False,
|
|
115
|
+
server_default=text("false"),
|
|
116
|
+
)
|
|
117
|
+
saas_available: Mapped[bool] = mapped_column(
|
|
118
|
+
Boolean,
|
|
119
|
+
nullable=False,
|
|
120
|
+
default=False,
|
|
121
|
+
server_default=text("false"),
|
|
122
|
+
)
|
|
123
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
124
|
+
DateTime(timezone=True), server_default=func.now()
|
|
125
|
+
)
|
|
126
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
127
|
+
DateTime(timezone=True),
|
|
128
|
+
server_default=func.now(),
|
|
129
|
+
onupdate=func.now(),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class OrganizationServiceMixin:
|
|
134
|
+
"""ACL columns for the ``organization_services`` table (per-org service
|
|
135
|
+
entitlements). FK columns and the ``(organization_id, service_name)``
|
|
136
|
+
unique constraint live on the concrete model.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
service_name: Mapped[str] = mapped_column(String(64), index=True)
|
|
140
|
+
enabled: Mapped[bool] = mapped_column(
|
|
141
|
+
Boolean,
|
|
142
|
+
nullable=False,
|
|
143
|
+
default=True,
|
|
144
|
+
server_default=text("true"),
|
|
145
|
+
)
|
|
146
|
+
source: Mapped[str] = mapped_column(
|
|
147
|
+
String(16), nullable=False, server_default=text("'manual'")
|
|
148
|
+
)
|
|
149
|
+
granted_at: Mapped[datetime] = mapped_column(
|
|
150
|
+
DateTime(timezone=True), server_default=func.now()
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class RoleMixin:
|
|
155
|
+
"""ACL columns for the roles table."""
|
|
156
|
+
|
|
157
|
+
name: Mapped[str] = mapped_column(String(128))
|
|
158
|
+
description: Mapped[str | None] = mapped_column(Text)
|
|
159
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
160
|
+
DateTime(timezone=True), server_default=func.now()
|
|
161
|
+
)
|
|
162
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
163
|
+
DateTime(timezone=True),
|
|
164
|
+
server_default=func.now(),
|
|
165
|
+
onupdate=func.now(),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class MembershipMixin:
|
|
170
|
+
"""ACL columns for the memberships table.
|
|
171
|
+
|
|
172
|
+
Does NOT include ``status`` — services define their own status type
|
|
173
|
+
(string, enum, etc.). Does NOT include FK columns or relationships
|
|
174
|
+
— those depend on the concrete model's schema and table names.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
joined_at: Mapped[datetime] = mapped_column(
|
|
178
|
+
DateTime(timezone=True), server_default=func.now()
|
|
179
|
+
)
|
|
180
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
181
|
+
DateTime(timezone=True), server_default=func.now()
|
|
182
|
+
)
|
|
183
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
184
|
+
DateTime(timezone=True),
|
|
185
|
+
server_default=func.now(),
|
|
186
|
+
onupdate=func.now(),
|
|
187
|
+
)
|