simple-module-sharing 0.0.38__tar.gz

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.
Files changed (33) hide show
  1. simple_module_sharing-0.0.38/.gitignore +17 -0
  2. simple_module_sharing-0.0.38/LICENSE +21 -0
  3. simple_module_sharing-0.0.38/PKG-INFO +73 -0
  4. simple_module_sharing-0.0.38/README.md +54 -0
  5. simple_module_sharing-0.0.38/package.json +18 -0
  6. simple_module_sharing-0.0.38/pyproject.toml +46 -0
  7. simple_module_sharing-0.0.38/sharing/__init__.py +0 -0
  8. simple_module_sharing-0.0.38/sharing/audience.py +44 -0
  9. simple_module_sharing-0.0.38/sharing/constants.py +65 -0
  10. simple_module_sharing-0.0.38/sharing/contracts/__init__.py +5 -0
  11. simple_module_sharing-0.0.38/sharing/contracts/schemas.py +36 -0
  12. simple_module_sharing-0.0.38/sharing/deps.py +31 -0
  13. simple_module_sharing-0.0.38/sharing/endpoints/__init__.py +0 -0
  14. simple_module_sharing-0.0.38/sharing/endpoints/api.py +69 -0
  15. simple_module_sharing-0.0.38/sharing/endpoints/views.py +75 -0
  16. simple_module_sharing-0.0.38/sharing/locales/de.json +33 -0
  17. simple_module_sharing-0.0.38/sharing/locales/en.json +33 -0
  18. simple_module_sharing-0.0.38/sharing/models.py +51 -0
  19. simple_module_sharing-0.0.38/sharing/module.py +86 -0
  20. simple_module_sharing-0.0.38/sharing/pages/Admin.tsx +100 -0
  21. simple_module_sharing-0.0.38/sharing/pages/components/ShareTable.tsx +86 -0
  22. simple_module_sharing-0.0.38/sharing/pages/types.ts +14 -0
  23. simple_module_sharing-0.0.38/sharing/policy.py +109 -0
  24. simple_module_sharing-0.0.38/sharing/present.py +49 -0
  25. simple_module_sharing-0.0.38/sharing/py.typed +0 -0
  26. simple_module_sharing-0.0.38/sharing/service.py +165 -0
  27. simple_module_sharing-0.0.38/sharing/services.py +22 -0
  28. simple_module_sharing-0.0.38/sharing/settings.py +17 -0
  29. simple_module_sharing-0.0.38/tests/_sharing_support.py +67 -0
  30. simple_module_sharing-0.0.38/tests/test_sharing_http.py +103 -0
  31. simple_module_sharing-0.0.38/tests/test_sharing_service.py +145 -0
  32. simple_module_sharing-0.0.38/tests/test_sharing_tenant_audience.py +87 -0
  33. simple_module_sharing-0.0.38/tsconfig.json +10 -0
@@ -0,0 +1,17 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ dist/
6
+ build/
7
+ *.db
8
+ *.sqlite3
9
+ .env
10
+
11
+ # Built frontend assets — shipped inside the wheel via force-include but
12
+ # NOT source-controlled. Re-run `smpy module build` before `uv build` to
13
+ # repopulate.
14
+ */static/dist/
15
+
16
+ # smpy module verify/build cache (throwaway scaffolded host)
17
+ .smpy/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anto Subash
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.5
2
+ Name: simple_module_sharing
3
+ Version: 0.0.38
4
+ Summary: Generic resource shares (owner / shared-with-user) for simple_module resource-scoped access
5
+ Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
+ Project-URL: Repository, https://github.com/antosubash/simple_module_python
7
+ Project-URL: Issues, https://github.com/antosubash/simple_module_python/issues
8
+ Project-URL: Changelog, https://github.com/antosubash/simple_module_python/blob/main/CHANGELOG.md
9
+ Author-email: Anto Subash <antosubash@live.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: access-control,permissions,sharing,simple-module
13
+ Requires-Python: >=3.12
14
+ Requires-Dist: simple-module-core==0.0.38
15
+ Requires-Dist: simple-module-db==0.0.38
16
+ Requires-Dist: simple-module-hosting==0.0.38
17
+ Requires-Dist: simple-module-inertia==0.0.38
18
+ Description-Content-Type: text/markdown
19
+
20
+ # simple_module_sharing
21
+
22
+ A generic share table for
23
+ [simple_module](https://github.com/antosubash/simple_module_python) apps:
24
+ give one user `view` or `edit` access to one row of another module, with an
25
+ optional expiry, without that module shipping a bespoke share model. It plugs
26
+ into the framework's resource-scoped access (`ResourcePolicy`,
27
+ `OwnedResourcePolicy`, `RequiresResource`), so detail routes, lists and counts
28
+ all honour a share in SQL.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install simple_module_sharing
34
+ ```
35
+
36
+ Add `simple_module_sharing` to your host's dependencies; the
37
+ `[project.entry-points.simple_module]` entry point is discovered automatically
38
+ at boot. Run `alembic upgrade heads` to create `sharing_resource_share`.
39
+
40
+ ## Usage
41
+
42
+ Declare the resource type in the owning module and hand the policy a
43
+ `share_exists` factory:
44
+
45
+ ```python
46
+ from sharing.policy import share_exists_for
47
+
48
+ NOTE_POLICY = OwnedResourcePolicy(
49
+ Note.owner_id,
50
+ bypass_perm=PERM_NOTES_MANAGE,
51
+ share_exists=share_exists_for("notes.note", Note.id),
52
+ )
53
+
54
+
55
+ def register_resource_types(self, registry):
56
+ registry.register(
57
+ ResourceType(name="notes.note", policy=NOTE_POLICY, select_by_id=select_by_id(Note))
58
+ )
59
+ ```
60
+
61
+ | Route | Purpose | Permission |
62
+ |---|---|---|
63
+ | `POST /api/sharing/shares` | Grant or update a share | `sharing.share` + policy `can("share")` |
64
+ | `GET /api/sharing/shares?resource_type=&resource_id=` | Shares on one row | `sharing.share` + policy `can("share")` |
65
+ | `DELETE /api/sharing/shares/{id}` | Revoke (hard delete) | `sharing.share`; granter, grantee or `sharing.manage` |
66
+ | `GET /admin/sharing/` | Admin list of every share in the tenant | `sharing.view` |
67
+
68
+ Shares are tenant-scoped, expired shares never match, and grantees are
69
+ validated through the user directory. See `docs/modules/sharing.md`.
70
+
71
+ ## License
72
+
73
+ MIT
@@ -0,0 +1,54 @@
1
+ # simple_module_sharing
2
+
3
+ A generic share table for
4
+ [simple_module](https://github.com/antosubash/simple_module_python) apps:
5
+ give one user `view` or `edit` access to one row of another module, with an
6
+ optional expiry, without that module shipping a bespoke share model. It plugs
7
+ into the framework's resource-scoped access (`ResourcePolicy`,
8
+ `OwnedResourcePolicy`, `RequiresResource`), so detail routes, lists and counts
9
+ all honour a share in SQL.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install simple_module_sharing
15
+ ```
16
+
17
+ Add `simple_module_sharing` to your host's dependencies; the
18
+ `[project.entry-points.simple_module]` entry point is discovered automatically
19
+ at boot. Run `alembic upgrade heads` to create `sharing_resource_share`.
20
+
21
+ ## Usage
22
+
23
+ Declare the resource type in the owning module and hand the policy a
24
+ `share_exists` factory:
25
+
26
+ ```python
27
+ from sharing.policy import share_exists_for
28
+
29
+ NOTE_POLICY = OwnedResourcePolicy(
30
+ Note.owner_id,
31
+ bypass_perm=PERM_NOTES_MANAGE,
32
+ share_exists=share_exists_for("notes.note", Note.id),
33
+ )
34
+
35
+
36
+ def register_resource_types(self, registry):
37
+ registry.register(
38
+ ResourceType(name="notes.note", policy=NOTE_POLICY, select_by_id=select_by_id(Note))
39
+ )
40
+ ```
41
+
42
+ | Route | Purpose | Permission |
43
+ |---|---|---|
44
+ | `POST /api/sharing/shares` | Grant or update a share | `sharing.share` + policy `can("share")` |
45
+ | `GET /api/sharing/shares?resource_type=&resource_id=` | Shares on one row | `sharing.share` + policy `can("share")` |
46
+ | `DELETE /api/sharing/shares/{id}` | Revoke (hard delete) | `sharing.share`; granter, grantee or `sharing.manage` |
47
+ | `GET /admin/sharing/` | Admin list of every share in the tenant | `sharing.view` |
48
+
49
+ Shares are tenant-scoped, expired shares never match, and grantees are
50
+ validated through the user directory. See `docs/modules/sharing.md`.
51
+
52
+ ## License
53
+
54
+ MIT
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@simple-module-py/sharing",
3
+ "version": "0.0.38",
4
+ "private": true,
5
+ "description": "Frontend assets for the Sharing module",
6
+ "peerDependencies": {
7
+ "react": "^19.0.0",
8
+ "react-dom": "^19.0.0",
9
+ "@inertiajs/react": "^3.7.0",
10
+ "@simple-module-py/ui": "*"
11
+ },
12
+ "devDependencies": {
13
+ "@simple-module-py/tsconfig": "*"
14
+ },
15
+ "dependencies": {
16
+ "lucide-react": "^1.43.0"
17
+ }
18
+ }
@@ -0,0 +1,46 @@
1
+ [project]
2
+ name = "simple_module_sharing"
3
+ version = "0.0.38"
4
+ description = "Generic resource shares (owner / shared-with-user) for simple_module resource-scoped access"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ authors = [{ name = "Anto Subash", email = "antosubash@live.com" }]
10
+ keywords = ["simple-module", "sharing", "permissions", "access-control"]
11
+ dependencies = [
12
+ "simple_module_core==0.0.38",
13
+ "simple_module_db==0.0.38",
14
+ "simple_module_hosting==0.0.38",
15
+ "simple_module_inertia==0.0.38",
16
+ ]
17
+
18
+ [project.entry-points.simple_module]
19
+ sharing = "sharing.module:SharingModule"
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/antosubash/simple_module_python"
23
+ Repository = "https://github.com/antosubash/simple_module_python"
24
+ Issues = "https://github.com/antosubash/simple_module_python/issues"
25
+ Changelog = "https://github.com/antosubash/simple_module_python/blob/main/CHANGELOG.md"
26
+
27
+ [build-system]
28
+ requires = ["hatchling"]
29
+ build-backend = "hatchling.build"
30
+
31
+ # The distribution name (simple_module_sharing) doesn't match the package
32
+ # directory (sharing), so hatchling can't infer what to ship — without
33
+ # this the wheel builds empty and the entry point fails to import.
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["sharing"]
36
+
37
+ # Ship the module-root package.json inside the wheel so the host can
38
+ # discover JS deps via importlib.resources after a pip install.
39
+ [tool.hatch.build.targets.wheel.force-include]
40
+ "package.json" = "sharing/package.json"
41
+
42
+ [tool.uv.sources]
43
+ simple_module_core = { workspace = true }
44
+ simple_module_db = { workspace = true }
45
+ simple_module_hosting = { workspace = true }
46
+ simple_module_inertia = { workspace = true }
File without changes
@@ -0,0 +1,44 @@
1
+ """Which user ids a request may act on or name: the active tenant's members.
2
+
3
+ On a multi-tenant app a grantee, and any user label shown, must belong to the
4
+ active tenant; otherwise ``sharing`` would let a caller share with (and probe
5
+ for) accounts in other tenants and read their directory labels. Membership is
6
+ asked through ``simple_module_core.tenancy.tenant_member_ids`` (the
7
+ ``tenants`` module publishes it), so this module never imports ``tenants``.
8
+ With no active tenant, or no membership store installed, nobody is in the
9
+ audience (fail closed). A single-tenant app has no such boundary.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Collection
15
+ from typing import Any
16
+
17
+ from simple_module_core.tenancy import TenancyMode, tenant_member_ids
18
+ from simple_module_db.tenancy import current_tenant_id
19
+ from simple_module_hosting.tenancy import tenancy_mode
20
+
21
+
22
+ class Audience:
23
+ """Filters user ids down to those the current request may see."""
24
+
25
+ def __init__(self, app: Any, db: Any) -> None:
26
+ self.app = app
27
+ self.db = db
28
+
29
+ async def visible(self, user_ids: Collection[str]) -> set[str]:
30
+ wanted = {uid for uid in user_ids if uid}
31
+ if not wanted or tenancy_mode(self.app) is not TenancyMode.MULTI:
32
+ return wanted
33
+ tenant_id = current_tenant_id.get()
34
+ if tenant_id is None:
35
+ return set()
36
+ members = await tenant_member_ids(self.app, self.db, tenant_id, wanted)
37
+ return members or set()
38
+
39
+
40
+ class OpenAudience:
41
+ """Everyone is visible (single-tenant use, or tests without an app)."""
42
+
43
+ async def visible(self, user_ids: Collection[str]) -> set[str]:
44
+ return {uid for uid in user_ids if uid}
@@ -0,0 +1,65 @@
1
+ """Stable identifiers for the sharing module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+ from simple_module_core.resources import ACTION_EDIT, ACTION_VIEW
8
+
9
+ MODULE_NAME: Final = "Sharing"
10
+ MODULE_PACKAGE: Final = "sharing"
11
+ LOCALE_NAMESPACE: Final = "sharing"
12
+
13
+ PERM_GROUP: Final = "Sharing"
14
+ PERM_SHARE: Final = "sharing.share"
15
+ """Use the grant/revoke API at all; the resource's own policy still decides."""
16
+ PERM_VIEW: Final = "sharing.view"
17
+ """See every share in the tenant (the admin page)."""
18
+ PERM_MANAGE: Final = "sharing.manage"
19
+ """Revoke any share in the tenant."""
20
+ USER_ROLE: Final = "user"
21
+
22
+ PAGE_ADMIN: Final = "Sharing/Admin"
23
+
24
+ API_PREFIX: Final = "/api/sharing"
25
+ VIEW_PREFIX: Final = "/admin/sharing"
26
+ # Trailing slash: the index route is registered at "/" under the prefix.
27
+ ADMIN_URL: Final = f"{VIEW_PREFIX}/"
28
+ AUDIT_URL_TEMPLATE: Final = f"{VIEW_PREFIX}/?share={{id}}"
29
+
30
+ MENU_LABEL: Final = "Sharing"
31
+ MENU_LABEL_KEY: Final = "sharing.nav.title"
32
+ MENU_ICON: Final = "share"
33
+ MENU_ORDER: Final = 60
34
+ MENU_GROUP: Final = "Access"
35
+ MENU_GROUP_KEY: Final = "ui.nav_groups.access"
36
+ AUDIT_LABEL: Final = "Share"
37
+ AUDIT_LABEL_KEY: Final = "sharing.audit.share"
38
+
39
+ # Prefix is load-bearing: all modules share one schema.
40
+ TABLE_SHARE: Final = "sharing_resource_share"
41
+ UQ_SHARE: Final = "uq_sharing_resource_share_subject"
42
+ IX_SHARE_RESOURCE: Final = "ix_sharing_resource_share_resource"
43
+
44
+ RESOURCE_TYPE_MAX: Final = 100
45
+ RESOURCE_ID_MAX: Final = 100
46
+ SUBJECT_TYPE_MAX: Final = 16
47
+ SUBJECT_ID_MAX: Final = 255
48
+ ROLE_MAX: Final = 32
49
+
50
+ SUBJECT_USER: Final = "user"
51
+ """The only subject kind so far; the column leaves room for groups or tenants."""
52
+
53
+ # A role is the action it grants plus everything it implies.
54
+ ROLE_IMPLIES: Final[dict[str, tuple[str, ...]]] = {
55
+ ACTION_VIEW: (ACTION_VIEW,),
56
+ ACTION_EDIT: (ACTION_VIEW, ACTION_EDIT),
57
+ }
58
+
59
+ ERR_UNKNOWN_TYPE: Final = "Unknown resource type"
60
+ ERR_NOT_FOUND: Final = "Not found"
61
+ ERR_FORBIDDEN: Final = "You may not share this resource"
62
+ ERR_UNKNOWN_GRANTEE: Final = "Unknown user"
63
+ ERR_BAD_ROLE: Final = "This role cannot be granted on this resource type"
64
+ ERR_EXPIRED: Final = "expires_at must be in the future"
65
+ ERR_SELF: Final = "You cannot share a resource with yourself"
@@ -0,0 +1,5 @@
1
+ """Sharing contracts — public interface for other modules."""
2
+
3
+ from sharing.contracts.schemas import ShareGrant, ShareOut
4
+
5
+ __all__ = ["ShareGrant", "ShareOut"]
@@ -0,0 +1,36 @@
1
+ """Sharing DTOs — the module's public surface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from datetime import datetime
7
+
8
+ from simple_module_core.resources import ACTION_VIEW
9
+ from sqlmodel import Field, SQLModel
10
+
11
+ from sharing.constants import RESOURCE_ID_MAX, RESOURCE_TYPE_MAX, ROLE_MAX, SUBJECT_ID_MAX
12
+
13
+
14
+ class ShareGrant(SQLModel):
15
+ """Grant (or update) one user's access to one resource."""
16
+
17
+ resource_type: str = Field(min_length=1, max_length=RESOURCE_TYPE_MAX)
18
+ resource_id: str = Field(min_length=1, max_length=RESOURCE_ID_MAX)
19
+ subject_id: str = Field(min_length=1, max_length=SUBJECT_ID_MAX)
20
+ role: str = Field(default=ACTION_VIEW, max_length=ROLE_MAX)
21
+ expires_at: datetime | None = None
22
+
23
+
24
+ class ShareOut(SQLModel):
25
+ id: uuid.UUID
26
+ resource_type: str
27
+ resource_id: str
28
+ subject_type: str
29
+ subject_id: str
30
+ subject_label: str | None = None
31
+ role: str
32
+ granted_by: str
33
+ granted_by_label: str | None = None
34
+ expires_at: datetime | None = None
35
+ expired: bool = False
36
+ created_at: datetime | None = None
@@ -0,0 +1,31 @@
1
+ """FastAPI dependencies for the sharing module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ from fastapi import Depends, HTTPException, Request
8
+ from simple_module_core.user_directory import get_user_directory
9
+ from simple_module_db.deps import get_db
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+
12
+ from sharing.audience import Audience
13
+ from sharing.service import SharingError, SharingService
14
+
15
+
16
+ async def get_sharing_service(
17
+ request: Request, db: AsyncSession = Depends(get_db)
18
+ ) -> SharingService:
19
+ return SharingService(
20
+ db,
21
+ request.app.state.sm.resource_types,
22
+ get_user_directory(request.app),
23
+ Audience(request.app, db),
24
+ )
25
+
26
+
27
+ SharingServiceDep = Annotated[SharingService, Depends(get_sharing_service)]
28
+
29
+
30
+ def http_error(exc: SharingError) -> HTTPException:
31
+ return HTTPException(status_code=exc.status_code, detail=exc.detail)
@@ -0,0 +1,69 @@
1
+ """REST API for shares — mounted under ``/api/sharing``.
2
+
3
+ Gated on ``sharing.share`` (so an API token scoped to it may call these);
4
+ the resource's own policy then decides per row.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import uuid
10
+
11
+ from fastapi import APIRouter, Depends, Request
12
+ from simple_module_core.resources import ResourcePrincipal
13
+ from simple_module_core.user_directory import get_user_directory
14
+ from simple_module_hosting.permissions import RequiresPermission
15
+ from simple_module_hosting.resource_deps import resource_principal
16
+
17
+ from sharing.constants import PERM_SHARE
18
+ from sharing.contracts.schemas import ShareGrant, ShareOut
19
+ from sharing.deps import SharingServiceDep, http_error
20
+ from sharing.present import present_shares
21
+ from sharing.service import SharingError
22
+
23
+ router = APIRouter(prefix="/shares", dependencies=[Depends(RequiresPermission(PERM_SHARE))])
24
+
25
+
26
+ @router.get("", response_model=list[ShareOut])
27
+ async def list_shares(
28
+ request: Request,
29
+ resource_type: str,
30
+ resource_id: str,
31
+ service: SharingServiceDep,
32
+ who: ResourcePrincipal = Depends(resource_principal),
33
+ ) -> list[ShareOut]:
34
+ try:
35
+ shares = await service.for_resource(who, resource_type, resource_id)
36
+ except SharingError as exc:
37
+ raise http_error(exc) from None
38
+ return await present_shares(
39
+ get_user_directory(request.app), service.db, shares, service.audience
40
+ )
41
+
42
+
43
+ @router.post("", response_model=ShareOut, status_code=201)
44
+ async def grant_share(
45
+ request: Request,
46
+ data: ShareGrant,
47
+ service: SharingServiceDep,
48
+ who: ResourcePrincipal = Depends(resource_principal),
49
+ ) -> ShareOut:
50
+ try:
51
+ share = await service.grant(who, data)
52
+ except SharingError as exc:
53
+ raise http_error(exc) from None
54
+ (out,) = await present_shares(
55
+ get_user_directory(request.app), service.db, [share], service.audience
56
+ )
57
+ return out
58
+
59
+
60
+ @router.delete("/{share_id}", status_code=204)
61
+ async def revoke_share(
62
+ share_id: uuid.UUID,
63
+ service: SharingServiceDep,
64
+ who: ResourcePrincipal = Depends(resource_principal),
65
+ ) -> None:
66
+ try:
67
+ await service.revoke(who, share_id)
68
+ except SharingError as exc:
69
+ raise http_error(exc) from None
@@ -0,0 +1,75 @@
1
+ """The admin page — mounted under ``/admin/sharing``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+
7
+ from fastapi import APIRouter, Depends, Request
8
+ from simple_module_core.resources import ResourcePrincipal
9
+ from simple_module_core.user_directory import get_user_directory
10
+ from simple_module_db import TenantIsolationError, paginate
11
+ from simple_module_hosting.inertia_deps import InertiaDep
12
+ from simple_module_hosting.pagination import LenientPageParamsDep
13
+ from simple_module_hosting.permissions import RequiresPermission
14
+ from simple_module_hosting.resource_deps import resource_principal
15
+ from simple_module_inertia import InertiaResponse
16
+ from sqlmodel import col, select
17
+ from starlette.responses import RedirectResponse
18
+
19
+ from sharing.constants import ADMIN_URL, PAGE_ADMIN, PERM_MANAGE, PERM_VIEW
20
+ from sharing.deps import SharingServiceDep, http_error
21
+ from sharing.models import ResourceShare
22
+ from sharing.present import present_shares
23
+ from sharing.service import SharingError
24
+
25
+ router = APIRouter()
26
+
27
+
28
+ @router.get("/", response_model=None, dependencies=[Depends(RequiresPermission(PERM_VIEW))])
29
+ async def admin_index(
30
+ request: Request,
31
+ inertia: InertiaDep,
32
+ service: SharingServiceDep,
33
+ params: LenientPageParamsDep,
34
+ who: ResourcePrincipal = Depends(resource_principal),
35
+ ) -> InertiaResponse:
36
+ stmt = select(ResourceShare).order_by(
37
+ col(ResourceShare.created_at).desc(), col(ResourceShare.id)
38
+ )
39
+ try:
40
+ page = await paginate(service.db, stmt, params)
41
+ shares = await present_shares(
42
+ get_user_directory(request.app), service.db, page.items, service.audience
43
+ )
44
+ total, served, tenant_required = page.total, page.page, False
45
+ except TenantIsolationError:
46
+ # Strict tenancy with no active tenant: nothing to list.
47
+ shares, total, served, tenant_required = [], 0, 1, True
48
+ return await inertia.render(
49
+ PAGE_ADMIN,
50
+ {
51
+ "shares": [s.model_dump(mode="json") for s in shares],
52
+ "total": total,
53
+ "page": served,
54
+ "per_page": params.per_page,
55
+ "can_revoke": who.has(PERM_MANAGE),
56
+ "tenant_required": tenant_required,
57
+ },
58
+ )
59
+
60
+
61
+ @router.post(
62
+ "/{share_id}/revoke",
63
+ response_model=None,
64
+ dependencies=[Depends(RequiresPermission(PERM_MANAGE))],
65
+ )
66
+ async def admin_revoke(
67
+ share_id: uuid.UUID,
68
+ service: SharingServiceDep,
69
+ who: ResourcePrincipal = Depends(resource_principal),
70
+ ) -> RedirectResponse:
71
+ try:
72
+ await service.revoke(who, share_id)
73
+ except SharingError as exc:
74
+ raise http_error(exc) from None
75
+ return RedirectResponse(ADMIN_URL, status_code=303)
@@ -0,0 +1,33 @@
1
+ {
2
+ "nav": {
3
+ "title": "Freigaben"
4
+ },
5
+ "permission_group": "Freigaben",
6
+ "audit": {
7
+ "share": "Freigabe"
8
+ },
9
+ "admin": {
10
+ "title": "Freigaben",
11
+ "description": "Wer in diesem Arbeitsbereich Zugriff auf welche Ressource erhalten hat.",
12
+ "tenant_required": "Wählen Sie einen Arbeitsbereich, um seine Freigaben zu sehen."
13
+ },
14
+ "list": {
15
+ "resource": "Ressource",
16
+ "subject": "Freigegeben für",
17
+ "role": "Rolle",
18
+ "granted_by": "Freigegeben von",
19
+ "expires": "Läuft ab",
20
+ "actions": "Aktionen",
21
+ "never": "Nie",
22
+ "expired": "Abgelaufen",
23
+ "revoke": "Widerrufen",
24
+ "empty_title": "Noch nichts freigegeben",
25
+ "empty_description": "Freigaben erscheinen hier, sobald jemand einer anderen Person Zugriff auf eine Ressource gibt."
26
+ },
27
+ "revoke": {
28
+ "title": "Diese Freigabe widerrufen?",
29
+ "description": "Die Person verliert sofort den Zugriff auf die Ressource.",
30
+ "confirm": "Widerrufen",
31
+ "cancel": "Abbrechen"
32
+ }
33
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "nav": {
3
+ "title": "Sharing"
4
+ },
5
+ "permission_group": "Sharing",
6
+ "audit": {
7
+ "share": "Share"
8
+ },
9
+ "admin": {
10
+ "title": "Shares",
11
+ "description": "Who has been given access to which resource in this workspace.",
12
+ "tenant_required": "Select a workspace to see its shares."
13
+ },
14
+ "list": {
15
+ "resource": "Resource",
16
+ "subject": "Shared with",
17
+ "role": "Role",
18
+ "granted_by": "Granted by",
19
+ "expires": "Expires",
20
+ "actions": "Actions",
21
+ "never": "Never",
22
+ "expired": "Expired",
23
+ "revoke": "Revoke",
24
+ "empty_title": "Nothing shared yet",
25
+ "empty_description": "Shares appear here when someone gives another user access to a resource."
26
+ },
27
+ "revoke": {
28
+ "title": "Revoke this share?",
29
+ "description": "The user loses access to the resource immediately.",
30
+ "confirm": "Revoke",
31
+ "cancel": "Cancel"
32
+ }
33
+ }