simple-module-sharing 0.0.39__tar.gz → 0.0.41__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.
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/PKG-INFO +5 -5
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/package.json +1 -1
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/pyproject.toml +5 -5
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/module.py +6 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/service.py +14 -7
- simple_module_sharing-0.0.41/sharing/user_cleanup.py +25 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/tests/test_sharing_service.py +21 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/.gitignore +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/LICENSE +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/README.md +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/__init__.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/audience.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/constants.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/contracts/__init__.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/contracts/schemas.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/deps.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/endpoints/__init__.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/endpoints/api.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/endpoints/views.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/locales/de.json +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/locales/en.json +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/models.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/pages/Admin.tsx +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/pages/components/ShareTable.tsx +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/pages/types.ts +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/policy.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/present.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/py.typed +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/services.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/settings.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/sharing/tenant_cleanup.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/tests/_sharing_support.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/tests/test_sharing_http.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/tests/test_sharing_tenant_audience.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/tests/test_sharing_tenant_cleanup.py +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/tests-js/sharing_Admin.test.tsx +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/tests-js/sharing_Admin_errors.test.tsx +0 -0
- {simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_sharing
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.41
|
|
4
4
|
Summary: Generic resource shares (owner / shared-with-user) for simple_module resource-scoped access
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -11,10 +11,10 @@ License-Expression: MIT
|
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Keywords: access-control,permissions,sharing,simple-module
|
|
13
13
|
Requires-Python: >=3.12
|
|
14
|
-
Requires-Dist: simple-module-core==0.0.
|
|
15
|
-
Requires-Dist: simple-module-db==0.0.
|
|
16
|
-
Requires-Dist: simple-module-hosting==0.0.
|
|
17
|
-
Requires-Dist: simple-module-inertia==0.0.
|
|
14
|
+
Requires-Dist: simple-module-core==0.0.41
|
|
15
|
+
Requires-Dist: simple-module-db==0.0.41
|
|
16
|
+
Requires-Dist: simple-module-hosting==0.0.41
|
|
17
|
+
Requires-Dist: simple-module-inertia==0.0.41
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
|
|
20
20
|
# simple_module_sharing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_sharing"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.41"
|
|
4
4
|
description = "Generic resource shares (owner / shared-with-user) for simple_module resource-scoped access"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -9,10 +9,10 @@ requires-python = ">=3.12"
|
|
|
9
9
|
authors = [{ name = "Anto Subash", email = "antosubash@live.com" }]
|
|
10
10
|
keywords = ["simple-module", "sharing", "permissions", "access-control"]
|
|
11
11
|
dependencies = [
|
|
12
|
-
"simple_module_core==0.0.
|
|
13
|
-
"simple_module_db==0.0.
|
|
14
|
-
"simple_module_hosting==0.0.
|
|
15
|
-
"simple_module_inertia==0.0.
|
|
12
|
+
"simple_module_core==0.0.41",
|
|
13
|
+
"simple_module_db==0.0.41",
|
|
14
|
+
"simple_module_hosting==0.0.41",
|
|
15
|
+
"simple_module_inertia==0.0.41",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[project.entry-points.simple_module]
|
|
@@ -35,6 +35,12 @@ class SharingModule(ModuleBase):
|
|
|
35
35
|
|
|
36
36
|
bus.subscribe_durable(TenantDeleted, purge_tenant_data)
|
|
37
37
|
|
|
38
|
+
from simple_module_core.user_events import UserDeleted
|
|
39
|
+
|
|
40
|
+
from sharing.user_cleanup import purge_user_shares
|
|
41
|
+
|
|
42
|
+
bus.subscribe_durable(UserDeleted, purge_user_shares)
|
|
43
|
+
|
|
38
44
|
def register_settings(self, app: FastAPI) -> None:
|
|
39
45
|
from sharing.services import SharingServices
|
|
40
46
|
from sharing.settings import SharingSettings
|
|
@@ -9,9 +9,10 @@ grantee is someone the user directory knows. Rows are tenant-scoped by
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import uuid
|
|
12
|
-
from datetime import UTC, datetime
|
|
13
12
|
from typing import Any
|
|
14
13
|
|
|
14
|
+
from simple_module_core.datetimes import aware as _aware
|
|
15
|
+
from simple_module_core.datetimes import utcnow
|
|
15
16
|
from simple_module_core.resources import (
|
|
16
17
|
ACTION_SHARE,
|
|
17
18
|
ResourcePrincipal,
|
|
@@ -20,6 +21,7 @@ from simple_module_core.resources import (
|
|
|
20
21
|
)
|
|
21
22
|
from simple_module_core.user_directory import UserDirectory
|
|
22
23
|
from simple_module_db.resource_policy import load_authorized
|
|
24
|
+
from sqlalchemy.exc import IntegrityError
|
|
23
25
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
24
26
|
from sqlmodel import select
|
|
25
27
|
|
|
@@ -49,10 +51,6 @@ class SharingError(Exception):
|
|
|
49
51
|
self.detail = detail
|
|
50
52
|
|
|
51
53
|
|
|
52
|
-
def _aware(value: datetime) -> datetime:
|
|
53
|
-
return value if value.tzinfo is not None else value.replace(tzinfo=UTC)
|
|
54
|
-
|
|
55
|
-
|
|
56
54
|
class SharingService:
|
|
57
55
|
def __init__(
|
|
58
56
|
self,
|
|
@@ -93,7 +91,7 @@ class SharingService:
|
|
|
93
91
|
resource_type = self._type(data.resource_type)
|
|
94
92
|
if data.role not in resource_type.shareable_actions:
|
|
95
93
|
raise SharingError(422, ERR_BAD_ROLE)
|
|
96
|
-
if data.expires_at is not None and _aware(data.expires_at) <=
|
|
94
|
+
if data.expires_at is not None and _aware(data.expires_at) <= utcnow():
|
|
97
95
|
raise SharingError(422, ERR_EXPIRED)
|
|
98
96
|
row = await self._authorize(principal, resource_type, data.resource_id)
|
|
99
97
|
grantee = (await self.directory.get_many(self.db, [data.subject_id])).get(data.subject_id)
|
|
@@ -112,8 +110,17 @@ class SharingService:
|
|
|
112
110
|
subject_id=grantee.id,
|
|
113
111
|
role=data.role,
|
|
114
112
|
granted_by=principal.user_id or "",
|
|
113
|
+
expires_at=data.expires_at,
|
|
115
114
|
)
|
|
116
|
-
|
|
115
|
+
try:
|
|
116
|
+
async with self.db.begin_nested():
|
|
117
|
+
self.db.add(share)
|
|
118
|
+
await self.db.flush()
|
|
119
|
+
except IntegrityError:
|
|
120
|
+
# Another grant may have won the unique-key race.
|
|
121
|
+
share = await self._existing(data.resource_type, resource_id, grantee.id)
|
|
122
|
+
if share is None:
|
|
123
|
+
raise
|
|
117
124
|
share.role = data.role
|
|
118
125
|
share.expires_at = data.expires_at
|
|
119
126
|
share.granted_by = principal.user_id or ""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Revoke shares addressed to a deleted principal across tenants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from simple_module_core.durable_events import DurableContext
|
|
8
|
+
from simple_module_db import all_tenants
|
|
9
|
+
from sqlmodel import col, delete
|
|
10
|
+
|
|
11
|
+
from sharing.constants import SUBJECT_USER
|
|
12
|
+
from sharing.models import ResourceShare
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from simple_module_core.user_events import UserDeleted
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def purge_user_shares(event: UserDeleted, ctx: DurableContext) -> None:
|
|
19
|
+
with all_tenants():
|
|
20
|
+
await ctx.db.execute(
|
|
21
|
+
delete(ResourceShare).where(
|
|
22
|
+
col(ResourceShare.subject_type) == SUBJECT_USER,
|
|
23
|
+
col(ResourceShare.subject_id) == str(event.user_id),
|
|
24
|
+
)
|
|
25
|
+
)
|
|
@@ -204,3 +204,24 @@ async def test_expired_share_can_be_renewed_without_duplicate_grants(env):
|
|
|
204
204
|
assert renewed.id == share.id
|
|
205
205
|
assert await _visible(service.db, FRIEND, ACTION_EDIT) == ["n"]
|
|
206
206
|
assert len(await service.for_resource(_who(OWNER), NOTE_TYPE, note_id)) == 1
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
async def test_duplicate_insert_race_reuses_winner_and_preserves_transaction(env, monkeypatch):
|
|
210
|
+
service, note_id = env
|
|
211
|
+
with tenant_context("t1"):
|
|
212
|
+
winner = await service.grant(_who(OWNER), _grant(note_id))
|
|
213
|
+
original = service._existing
|
|
214
|
+
calls = 0
|
|
215
|
+
|
|
216
|
+
async def miss_once(*args):
|
|
217
|
+
nonlocal calls
|
|
218
|
+
calls += 1
|
|
219
|
+
return None if calls == 1 else await original(*args)
|
|
220
|
+
|
|
221
|
+
# Simulate a concurrent winner becoming visible after our initial lookup.
|
|
222
|
+
monkeypatch.setattr(service, "_existing", miss_once)
|
|
223
|
+
result = await service.grant(_who(OWNER), _grant(note_id, role=ACTION_EDIT))
|
|
224
|
+
assert result.id == winner.id
|
|
225
|
+
assert result.role == ACTION_EDIT
|
|
226
|
+
assert len(await service.for_resource(_who(OWNER), NOTE_TYPE, note_id)) == 1
|
|
227
|
+
assert await _visible(service.db, FRIEND, ACTION_EDIT) == ["n"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/tests/test_sharing_tenant_audience.py
RENAMED
|
File without changes
|
{simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/tests/test_sharing_tenant_cleanup.py
RENAMED
|
File without changes
|
{simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/tests-js/sharing_Admin.test.tsx
RENAMED
|
File without changes
|
{simple_module_sharing-0.0.39 → simple_module_sharing-0.0.41}/tests-js/sharing_Admin_errors.test.tsx
RENAMED
|
File without changes
|
|
File without changes
|