platform-context 0.5.0__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.
- platform_context-0.5.0/.gitignore +50 -0
- platform_context-0.5.0/.trigger-ci +1 -0
- platform_context-0.5.0/CHANGELOG.md +56 -0
- platform_context-0.5.0/PKG-INFO +88 -0
- platform_context-0.5.0/README.md +56 -0
- platform_context-0.5.0/pyproject.toml +61 -0
- platform_context-0.5.0/src/platform_context/__init__.py +84 -0
- platform_context-0.5.0/src/platform_context/apps.py +11 -0
- platform_context-0.5.0/src/platform_context/audit.py +76 -0
- platform_context-0.5.0/src/platform_context/context.py +79 -0
- platform_context-0.5.0/src/platform_context/context_processors.py +76 -0
- platform_context-0.5.0/src/platform_context/db.py +51 -0
- platform_context-0.5.0/src/platform_context/exceptions.py +159 -0
- platform_context-0.5.0/src/platform_context/htmx.py +107 -0
- platform_context-0.5.0/src/platform_context/middleware.py +116 -0
- platform_context-0.5.0/src/platform_context/outbox.py +59 -0
- platform_context-0.5.0/src/platform_context/static/platform/css/pui-tokens.css +270 -0
- platform_context-0.5.0/src/platform_context/temporal_client.py +49 -0
- platform_context-0.5.0/src/platform_context/tenant_utils/__init__.py +25 -0
- platform_context-0.5.0/src/platform_context/tenant_utils/celery.py +87 -0
- platform_context-0.5.0/src/platform_context/tenant_utils/http_client.py +93 -0
- platform_context-0.5.0/src/platform_context/tenant_utils/middleware.py +57 -0
- platform_context-0.5.0/src/platform_context/tenant_utils/provisioning.py +171 -0
- platform_context-0.5.0/src/platform_context/tenant_utils/testing.py +96 -0
- platform_context-0.5.0/src/platform_context/testing/__init__.py +41 -0
- platform_context-0.5.0/src/platform_context/testing/assertions.py +300 -0
- platform_context-0.5.0/src/platform_context/testing/fixtures.py +149 -0
- platform_context-0.5.0/tests/__init__.py +0 -0
- platform_context-0.5.0/tests/settings.py +18 -0
- platform_context-0.5.0/tests/test_context.py +83 -0
- platform_context-0.5.0/tests/test_htmx.py +135 -0
- platform_context-0.5.0/tests/test_middleware.py +25 -0
- platform_context-0.5.0/tests/test_temporal_client.py +91 -0
- platform_context-0.5.0/tests/test_tenant_utils.py +223 -0
- platform_context-0.5.0/tests/test_testing.py +158 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*\.class
|
|
5
|
+
.venv/
|
|
6
|
+
venv/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
# Django
|
|
11
|
+
*.log
|
|
12
|
+
db.sqlite3
|
|
13
|
+
media/
|
|
14
|
+
staticfiles/
|
|
15
|
+
# Environment
|
|
16
|
+
.env
|
|
17
|
+
.env.local
|
|
18
|
+
.env.prod
|
|
19
|
+
|
|
20
|
+
# Secrets (ADR-045) — NEVER commit plaintext
|
|
21
|
+
secrets.env
|
|
22
|
+
!secrets.enc.env
|
|
23
|
+
# IDE
|
|
24
|
+
.idea/
|
|
25
|
+
.vscode/
|
|
26
|
+
*.swp
|
|
27
|
+
# OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
30
|
+
# Large files
|
|
31
|
+
*.sqlite3
|
|
32
|
+
*.log
|
|
33
|
+
output_batch/
|
|
34
|
+
app_backup/
|
|
35
|
+
docs_legacy/
|
|
36
|
+
# Sphinx build output
|
|
37
|
+
docs/_build/
|
|
38
|
+
*:Zone.Identifier
|
|
39
|
+
# Local Windsurf/MCP config — machine-specific, never commit
|
|
40
|
+
.windsurf/mcp_config.json
|
|
41
|
+
# Local infra plaintext secrets — NEVER commit
|
|
42
|
+
infra/*.env
|
|
43
|
+
infra/*.key
|
|
44
|
+
# Validate-ports CI draft (not yet integrated)
|
|
45
|
+
.github/workflows/validate-ports.yml
|
|
46
|
+
# Markdownlint local config override
|
|
47
|
+
.markdownlint.json
|
|
48
|
+
# Concept review drafts (local working copies, not for VCS)
|
|
49
|
+
docs/concepts/REVIEW-*
|
|
50
|
+
env_loader.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
triggered at 2026-02-20T18:25
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Changelog — platform-context
|
|
2
|
+
|
|
3
|
+
All notable changes to this package are documented here.
|
|
4
|
+
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## [0.5.0] — current
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `temporal_client.py` — Temporal workflow engine client integration (ADR-079)
|
|
12
|
+
- `tenant_utils/` — Shared tenant utility helpers
|
|
13
|
+
- `testing/` — Shared test fixtures and helpers for downstream packages (ADR-084)
|
|
14
|
+
- `outbox.py` — Transactional outbox pattern for reliable event publishing
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `middleware.py` — RequestContextMiddleware now sets tenant context per request
|
|
18
|
+
- `htmx.py` — HtmxErrorMiddleware hardened; handles non-HTMX requests gracefully
|
|
19
|
+
- `exceptions.py` — Platform-wide exception hierarchy extended
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## [0.4.x]
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- `audit.py` — Shared audit log base model and mixin
|
|
27
|
+
- `db.py` — Database utilities (connection helpers, vendor detection)
|
|
28
|
+
- `context_processors.py` — Django context processors for platform-wide template vars
|
|
29
|
+
- `context.py` — Request context holder (thread-local + async-safe)
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- `apps.py` — AppConfig registered as `platform_context`
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## [0.3.x]
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- Initial package structure with `src/` layout
|
|
40
|
+
- Django 5.x compatibility (requires `Django>=5.0,<6.0`)
|
|
41
|
+
- Optional extras: `tenants` (django-tenants), `temporal` (temporalio)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## [0.2.x]
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- First stable release as shared foundation package
|
|
49
|
+
- Extracted from bfagent monolith (ADR-028)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## [0.1.0]
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
- Initial extraction from bfagent-core (ADR-028: Platform Context Consolidation)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: platform-context
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Shared platform foundation for all Django projects (Context, Tenancy, Audit, Outbox)
|
|
5
|
+
Author-email: Achim Dehnert <achim@dehnert.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Framework :: Django :: 5.0
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: django<6.0,>=5.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: factory-boy>=3.3; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest-django>=4.8; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-mock>=3.12; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
20
|
+
Provides-Extra: temporal
|
|
21
|
+
Requires-Dist: temporalio>=1.7.0; extra == 'temporal'
|
|
22
|
+
Provides-Extra: tenants
|
|
23
|
+
Requires-Dist: django-tenants>=3.6; extra == 'tenants'
|
|
24
|
+
Requires-Dist: httpx>=0.27; extra == 'tenants'
|
|
25
|
+
Requires-Dist: tenant-schemas-celery>=2.0; extra == 'tenants'
|
|
26
|
+
Provides-Extra: testing
|
|
27
|
+
Requires-Dist: factory-boy>=3.3; extra == 'testing'
|
|
28
|
+
Requires-Dist: pytest-django>=4.8; extra == 'testing'
|
|
29
|
+
Requires-Dist: pytest-mock>=3.12; extra == 'testing'
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'testing'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# platform-context
|
|
34
|
+
|
|
35
|
+
Shared platform foundation for all Django projects in the iil.pet ecosystem.
|
|
36
|
+
|
|
37
|
+
## What it provides
|
|
38
|
+
|
|
39
|
+
- **Request Context** — Thread-safe context propagation (tenant_id, user_id, request_id)
|
|
40
|
+
- **Multi-Tenancy Middleware** — Subdomain-based tenant resolution + Postgres RLS
|
|
41
|
+
- **Audit Event Logging** — Structured audit trail (model-agnostic)
|
|
42
|
+
- **Outbox Pattern** — Reliable event publishing within transactions
|
|
43
|
+
- **Exception Hierarchy** — Structured platform exceptions
|
|
44
|
+
- **Template Context Processors** — Tenant/permission info in Django templates
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install -e packages/platform-context
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Add to `INSTALLED_APPS`:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
INSTALLED_APPS = [
|
|
56
|
+
...
|
|
57
|
+
"platform_context",
|
|
58
|
+
]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from platform_context import get_context, set_tenant, set_user_id
|
|
65
|
+
from platform_context.middleware import SubdomainTenantMiddleware
|
|
66
|
+
from platform_context.audit import emit_audit_event
|
|
67
|
+
from platform_context.exceptions import TenantNotFoundError
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Configuration
|
|
71
|
+
|
|
72
|
+
| Setting | Default | Description |
|
|
73
|
+
|---------|---------|-------------|
|
|
74
|
+
| `TENANT_BASE_DOMAIN` | `"localhost"` | Base domain for subdomain resolution |
|
|
75
|
+
| `TENANT_MODEL` | `"tenancy.Organization"` | Dotted path to tenant model |
|
|
76
|
+
| `TENANT_SLUG_FIELD` | `"slug"` | Field name for slug lookup |
|
|
77
|
+
| `TENANT_ID_FIELD` | `"tenant_id"` | Field name for tenant_id |
|
|
78
|
+
| `TENANT_ALLOW_LOCALHOST` | `False` | Allow admin access without tenant (dev) |
|
|
79
|
+
| `PLATFORM_AUDIT_MODEL` | `None` | Dotted path to AuditEvent model |
|
|
80
|
+
| `PLATFORM_OUTBOX_MODEL` | `None` | Dotted path to OutboxMessage model |
|
|
81
|
+
|
|
82
|
+
## Relation to bfagent-core
|
|
83
|
+
|
|
84
|
+
`platform-context` is the framework-agnostic foundation extracted from
|
|
85
|
+
`bfagent-core` (see ADR-028). `bfagent-core` now depends on
|
|
86
|
+
`platform-context` and re-exports its public API for backward compatibility.
|
|
87
|
+
|
|
88
|
+
New projects should depend on `platform-context` directly.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# platform-context
|
|
2
|
+
|
|
3
|
+
Shared platform foundation for all Django projects in the iil.pet ecosystem.
|
|
4
|
+
|
|
5
|
+
## What it provides
|
|
6
|
+
|
|
7
|
+
- **Request Context** — Thread-safe context propagation (tenant_id, user_id, request_id)
|
|
8
|
+
- **Multi-Tenancy Middleware** — Subdomain-based tenant resolution + Postgres RLS
|
|
9
|
+
- **Audit Event Logging** — Structured audit trail (model-agnostic)
|
|
10
|
+
- **Outbox Pattern** — Reliable event publishing within transactions
|
|
11
|
+
- **Exception Hierarchy** — Structured platform exceptions
|
|
12
|
+
- **Template Context Processors** — Tenant/permission info in Django templates
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install -e packages/platform-context
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Add to `INSTALLED_APPS`:
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
INSTALLED_APPS = [
|
|
24
|
+
...
|
|
25
|
+
"platform_context",
|
|
26
|
+
]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from platform_context import get_context, set_tenant, set_user_id
|
|
33
|
+
from platform_context.middleware import SubdomainTenantMiddleware
|
|
34
|
+
from platform_context.audit import emit_audit_event
|
|
35
|
+
from platform_context.exceptions import TenantNotFoundError
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
| Setting | Default | Description |
|
|
41
|
+
|---------|---------|-------------|
|
|
42
|
+
| `TENANT_BASE_DOMAIN` | `"localhost"` | Base domain for subdomain resolution |
|
|
43
|
+
| `TENANT_MODEL` | `"tenancy.Organization"` | Dotted path to tenant model |
|
|
44
|
+
| `TENANT_SLUG_FIELD` | `"slug"` | Field name for slug lookup |
|
|
45
|
+
| `TENANT_ID_FIELD` | `"tenant_id"` | Field name for tenant_id |
|
|
46
|
+
| `TENANT_ALLOW_LOCALHOST` | `False` | Allow admin access without tenant (dev) |
|
|
47
|
+
| `PLATFORM_AUDIT_MODEL` | `None` | Dotted path to AuditEvent model |
|
|
48
|
+
| `PLATFORM_OUTBOX_MODEL` | `None` | Dotted path to OutboxMessage model |
|
|
49
|
+
|
|
50
|
+
## Relation to bfagent-core
|
|
51
|
+
|
|
52
|
+
`platform-context` is the framework-agnostic foundation extracted from
|
|
53
|
+
`bfagent-core` (see ADR-028). `bfagent-core` now depends on
|
|
54
|
+
`platform-context` and re-exports its public API for backward compatibility.
|
|
55
|
+
|
|
56
|
+
New projects should depend on `platform-context` directly.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "platform-context"
|
|
3
|
+
version = "0.5.0"
|
|
4
|
+
description = "Shared platform foundation for all Django projects (Context, Tenancy, Audit, Outbox)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = {text = "MIT"}
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Achim Dehnert", email = "achim@dehnert.com"}
|
|
10
|
+
]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Framework :: Django :: 5.0",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
dependencies = [
|
|
21
|
+
"Django>=5.0,<6.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=8.0",
|
|
27
|
+
"pytest-django>=4.8",
|
|
28
|
+
"pytest-mock>=3.12",
|
|
29
|
+
"factory-boy>=3.3",
|
|
30
|
+
]
|
|
31
|
+
testing = [
|
|
32
|
+
"pytest>=8.0",
|
|
33
|
+
"pytest-django>=4.8",
|
|
34
|
+
"pytest-mock>=3.12",
|
|
35
|
+
"factory-boy>=3.3",
|
|
36
|
+
]
|
|
37
|
+
tenants = [
|
|
38
|
+
"django-tenants>=3.6",
|
|
39
|
+
"tenant-schemas-celery>=2.0",
|
|
40
|
+
"httpx>=0.27",
|
|
41
|
+
]
|
|
42
|
+
temporal = [
|
|
43
|
+
"temporalio>=1.7.0",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["hatchling"]
|
|
48
|
+
build-backend = "hatchling.build"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/platform_context"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
DJANGO_SETTINGS_MODULE = "tests.settings"
|
|
55
|
+
python_files = ["test_*.py"]
|
|
56
|
+
pythonpath = ["."]
|
|
57
|
+
testpaths = ["tests"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff]
|
|
60
|
+
line-length = 100
|
|
61
|
+
target-version = "py311"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
platform-context: Shared platform foundation for all Django projects.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- Request context management (tenant, user, request_id)
|
|
6
|
+
- Multi-tenancy middleware (subdomain-based)
|
|
7
|
+
- Postgres RLS helpers
|
|
8
|
+
- Audit event logging (model-agnostic)
|
|
9
|
+
- Outbox pattern for reliable events
|
|
10
|
+
- Exception hierarchy
|
|
11
|
+
- Django template context processors
|
|
12
|
+
- Shared test helpers (platform_context.testing)
|
|
13
|
+
- Multi-tenancy utilities (platform_context.tenant_utils) — ADR-056
|
|
14
|
+
- Temporal Client singleton (platform_context.temporal_client) — ADR-077
|
|
15
|
+
|
|
16
|
+
Usage::
|
|
17
|
+
|
|
18
|
+
from platform_context import get_context, set_tenant, set_user_id
|
|
19
|
+
from platform_context.middleware import SubdomainTenantMiddleware
|
|
20
|
+
from platform_context.audit import emit_audit_event
|
|
21
|
+
|
|
22
|
+
Test helpers (install with platform-context[testing])::
|
|
23
|
+
|
|
24
|
+
# conftest.py
|
|
25
|
+
from platform_context.testing.fixtures import user, admin_user, auth_client # noqa: F401
|
|
26
|
+
|
|
27
|
+
# tests
|
|
28
|
+
from platform_context.testing.assertions import assert_htmx_fragment, assert_login_required
|
|
29
|
+
|
|
30
|
+
Multi-tenancy utilities (ADR-056, requires django-tenants in consuming service)::
|
|
31
|
+
|
|
32
|
+
from platform_context.tenant_utils.http_client import TenantAwareHttpClient
|
|
33
|
+
from platform_context.tenant_utils.middleware import TenantPropagationMiddleware
|
|
34
|
+
from platform_context.tenant_utils.celery import TenantAwareTask, send_cross_service_task
|
|
35
|
+
from platform_context.tenant_utils.provisioning import provision_tenant
|
|
36
|
+
|
|
37
|
+
# In conftest.py:
|
|
38
|
+
from platform_context.tenant_utils.testing import tenant_a, tenant_b # noqa: F401
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from platform_context.context import (
|
|
42
|
+
RequestContext,
|
|
43
|
+
clear_context,
|
|
44
|
+
get_context,
|
|
45
|
+
set_request_id,
|
|
46
|
+
set_tenant,
|
|
47
|
+
set_user_id,
|
|
48
|
+
)
|
|
49
|
+
from platform_context.db import get_db_tenant, set_db_tenant
|
|
50
|
+
from platform_context.htmx import (
|
|
51
|
+
HtmxErrorMiddleware,
|
|
52
|
+
HtmxResponseMixin,
|
|
53
|
+
is_htmx_request,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
__version__ = "0.5.0"
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
# Context
|
|
60
|
+
"RequestContext",
|
|
61
|
+
"clear_context",
|
|
62
|
+
"get_context",
|
|
63
|
+
"set_request_id",
|
|
64
|
+
"set_tenant",
|
|
65
|
+
"set_user_id",
|
|
66
|
+
# DB
|
|
67
|
+
"get_db_tenant",
|
|
68
|
+
"set_db_tenant",
|
|
69
|
+
# HTMX (ADR-048)
|
|
70
|
+
"HtmxErrorMiddleware",
|
|
71
|
+
"HtmxResponseMixin",
|
|
72
|
+
"is_htmx_request",
|
|
73
|
+
# Testing helpers (platform-context[testing])
|
|
74
|
+
# Import via: from platform_context.testing.assertions import ...
|
|
75
|
+
# Import via: from platform_context.testing.fixtures import ...
|
|
76
|
+
# Multi-tenancy utilities (ADR-056, requires django-tenants)
|
|
77
|
+
# Import via: from platform_context.tenant_utils.http_client import TenantAwareHttpClient
|
|
78
|
+
# Import via: from platform_context.tenant_utils.middleware import TenantPropagationMiddleware
|
|
79
|
+
# Import via: from platform_context.tenant_utils.celery import TenantAwareTask
|
|
80
|
+
# Import via: from platform_context.tenant_utils.provisioning import provision_tenant
|
|
81
|
+
# Import via: from platform_context.tenant_utils.testing import tenant_a, tenant_b
|
|
82
|
+
# Temporal Client (ADR-077, requires platform-context[temporal])
|
|
83
|
+
# Import via: from platform_context.temporal_client import get_temporal_client
|
|
84
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Django app configuration for platform_context."""
|
|
2
|
+
|
|
3
|
+
from django.apps import AppConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PlatformContextConfig(AppConfig):
|
|
7
|
+
"""Django app config for platform-context."""
|
|
8
|
+
|
|
9
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
10
|
+
name = "platform_context"
|
|
11
|
+
verbose_name = "Platform Context"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Audit event logging for compliance.
|
|
3
|
+
|
|
4
|
+
Provides structured audit trail for all risk-relevant mutations.
|
|
5
|
+
Model-agnostic: configure PLATFORM_AUDIT_MODEL in Django settings.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from platform_context.context import get_context
|
|
13
|
+
|
|
14
|
+
_logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def emit_audit_event(
|
|
18
|
+
*,
|
|
19
|
+
tenant_id: UUID,
|
|
20
|
+
category: str,
|
|
21
|
+
action: str,
|
|
22
|
+
entity_type: str,
|
|
23
|
+
entity_id: UUID,
|
|
24
|
+
payload: dict[str, Any],
|
|
25
|
+
) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Emit an audit event for a mutation.
|
|
28
|
+
|
|
29
|
+
The actor_user_id and request_id are automatically populated
|
|
30
|
+
from the current request context.
|
|
31
|
+
|
|
32
|
+
Requires PLATFORM_AUDIT_MODEL to be set in Django settings
|
|
33
|
+
(e.g., "bfagent_core.AuditEvent").
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
tenant_id: The tenant this event belongs to
|
|
37
|
+
category: Event category (e.g., "risk.assessment")
|
|
38
|
+
action: Action performed (e.g., "created", "approved")
|
|
39
|
+
entity_type: Full entity type (e.g., "risk.Assessment")
|
|
40
|
+
entity_id: UUID of the affected entity
|
|
41
|
+
payload: Additional data to store
|
|
42
|
+
"""
|
|
43
|
+
from django.conf import settings
|
|
44
|
+
|
|
45
|
+
model_path = getattr(settings, "PLATFORM_AUDIT_MODEL", None)
|
|
46
|
+
if not model_path:
|
|
47
|
+
_logger.warning(
|
|
48
|
+
"PLATFORM_AUDIT_MODEL not configured, audit event dropped: "
|
|
49
|
+
"%s.%s on %s",
|
|
50
|
+
category,
|
|
51
|
+
action,
|
|
52
|
+
entity_type,
|
|
53
|
+
)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
from django.apps import apps
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
app_label, model_name = model_path.rsplit(".", 1)
|
|
60
|
+
AuditModel = apps.get_model(app_label, model_name)
|
|
61
|
+
except (LookupError, ValueError) as exc:
|
|
62
|
+
_logger.error("Cannot resolve PLATFORM_AUDIT_MODEL=%s: %s", model_path, exc)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
ctx = get_context()
|
|
66
|
+
|
|
67
|
+
AuditModel.objects.create(
|
|
68
|
+
tenant_id=tenant_id,
|
|
69
|
+
actor_user_id=ctx.user_id,
|
|
70
|
+
category=category,
|
|
71
|
+
action=action,
|
|
72
|
+
entity_type=entity_type,
|
|
73
|
+
entity_id=entity_id,
|
|
74
|
+
payload=payload,
|
|
75
|
+
request_id=ctx.request_id,
|
|
76
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Request context management using contextvars.
|
|
3
|
+
|
|
4
|
+
Thread-safe context propagation for:
|
|
5
|
+
- tenant_id / tenant_slug
|
|
6
|
+
- user_id
|
|
7
|
+
- request_id (correlation ID)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import contextvars
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
|
|
14
|
+
_request_id: contextvars.ContextVar[str | None] = contextvars.ContextVar(
|
|
15
|
+
"request_id", default=None
|
|
16
|
+
)
|
|
17
|
+
_current_tenant_id: contextvars.ContextVar[UUID | None] = contextvars.ContextVar(
|
|
18
|
+
"tenant_id", default=None
|
|
19
|
+
)
|
|
20
|
+
_current_tenant_slug: contextvars.ContextVar[str | None] = contextvars.ContextVar(
|
|
21
|
+
"tenant_slug", default=None
|
|
22
|
+
)
|
|
23
|
+
_current_user_id: contextvars.ContextVar[UUID | None] = contextvars.ContextVar(
|
|
24
|
+
"user_id", default=None
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class RequestContext:
|
|
30
|
+
"""Immutable snapshot of the current request context."""
|
|
31
|
+
|
|
32
|
+
request_id: str | None
|
|
33
|
+
tenant_id: UUID | None
|
|
34
|
+
tenant_slug: str | None
|
|
35
|
+
user_id: UUID | None
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_authenticated(self) -> bool:
|
|
39
|
+
"""Check if a user is set in context."""
|
|
40
|
+
return self.user_id is not None
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def has_tenant(self) -> bool:
|
|
44
|
+
"""Check if a tenant is set in context."""
|
|
45
|
+
return self.tenant_id is not None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def set_request_id(value: str | None) -> None:
|
|
49
|
+
"""Set the request/correlation ID for the current context."""
|
|
50
|
+
_request_id.set(value)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def set_tenant(tenant_id: UUID | None, tenant_slug: str | None) -> None:
|
|
54
|
+
"""Set the current tenant for the context."""
|
|
55
|
+
_current_tenant_id.set(tenant_id)
|
|
56
|
+
_current_tenant_slug.set(tenant_slug)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def set_user_id(value: UUID | None) -> None:
|
|
60
|
+
"""Set the current user ID for the context."""
|
|
61
|
+
_current_user_id.set(value)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_context() -> RequestContext:
|
|
65
|
+
"""Get the current request context snapshot."""
|
|
66
|
+
return RequestContext(
|
|
67
|
+
request_id=_request_id.get(),
|
|
68
|
+
tenant_id=_current_tenant_id.get(),
|
|
69
|
+
tenant_slug=_current_tenant_slug.get(),
|
|
70
|
+
user_id=_current_user_id.get(),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def clear_context() -> None:
|
|
75
|
+
"""Clear all context variables. Useful for testing."""
|
|
76
|
+
_request_id.set(None)
|
|
77
|
+
_current_tenant_id.set(None)
|
|
78
|
+
_current_tenant_slug.set(None)
|
|
79
|
+
_current_user_id.set(None)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Django template context processors."""
|
|
2
|
+
|
|
3
|
+
from django.http import HttpRequest
|
|
4
|
+
|
|
5
|
+
from platform_context.context import get_context
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def tenant_context(request: HttpRequest) -> dict:
|
|
9
|
+
"""
|
|
10
|
+
Add tenant information to template context.
|
|
11
|
+
|
|
12
|
+
Usage in settings.py:
|
|
13
|
+
TEMPLATES = [{
|
|
14
|
+
...
|
|
15
|
+
"OPTIONS": {
|
|
16
|
+
"context_processors": [
|
|
17
|
+
...
|
|
18
|
+
"platform_context.context_processors.tenant_context",
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
}]
|
|
22
|
+
|
|
23
|
+
Available in templates:
|
|
24
|
+
{{ tenant_id }}
|
|
25
|
+
{{ tenant_slug }}
|
|
26
|
+
{{ request_id }}
|
|
27
|
+
"""
|
|
28
|
+
ctx = get_context()
|
|
29
|
+
return {
|
|
30
|
+
"tenant_id": ctx.tenant_id,
|
|
31
|
+
"tenant_slug": ctx.tenant_slug,
|
|
32
|
+
"request_id": ctx.request_id,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def platform_context(request: HttpRequest) -> dict:
|
|
37
|
+
"""
|
|
38
|
+
Add platform context to templates (tenant, permissions, membership).
|
|
39
|
+
|
|
40
|
+
Usage in settings.py:
|
|
41
|
+
'platform_context.context_processors.platform_context',
|
|
42
|
+
|
|
43
|
+
Available in templates:
|
|
44
|
+
{{ tenant }} - Tenant object (if in tenant context)
|
|
45
|
+
{{ tenant_id }} - UUID
|
|
46
|
+
{{ tenant_slug }} - String
|
|
47
|
+
{{ membership }} - TenantMembership (if authenticated)
|
|
48
|
+
{{ permissions }} - FrozenSet of permission codes
|
|
49
|
+
{{ user_role }} - Role string (owner, admin, member, viewer)
|
|
50
|
+
{{ is_tenant_admin }} - Bool: owner or admin
|
|
51
|
+
{{ request_id }} - Correlation ID
|
|
52
|
+
|
|
53
|
+
Permission check in templates:
|
|
54
|
+
{% if 'stories.create' in permissions %}
|
|
55
|
+
<a href="...">Create Story</a>
|
|
56
|
+
{% endif %}
|
|
57
|
+
"""
|
|
58
|
+
ctx = get_context()
|
|
59
|
+
|
|
60
|
+
result = {
|
|
61
|
+
"tenant_id": ctx.tenant_id,
|
|
62
|
+
"tenant_slug": ctx.tenant_slug,
|
|
63
|
+
"request_id": ctx.request_id,
|
|
64
|
+
"tenant": getattr(request, "tenant", None),
|
|
65
|
+
"membership": getattr(request, "membership", None),
|
|
66
|
+
"permissions": getattr(request, "permissions", frozenset()),
|
|
67
|
+
"user_role": None,
|
|
68
|
+
"is_tenant_admin": False,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
membership = result["membership"]
|
|
72
|
+
if membership:
|
|
73
|
+
result["user_role"] = membership.role
|
|
74
|
+
result["is_tenant_admin"] = membership.role in ("owner", "admin")
|
|
75
|
+
|
|
76
|
+
return result
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database utilities for multi-tenancy.
|
|
3
|
+
|
|
4
|
+
Provides Postgres RLS (Row Level Security) session variable management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def set_db_tenant(tenant_id: UUID | None) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Set Postgres session variable for RLS policies.
|
|
13
|
+
|
|
14
|
+
This sets `app.current_tenant` which RLS policies use:
|
|
15
|
+
|
|
16
|
+
CREATE POLICY tenant_isolation ON my_table
|
|
17
|
+
USING (tenant_id = current_setting('app.current_tenant')::uuid);
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
tenant_id: The tenant UUID, or None to clear access
|
|
21
|
+
|
|
22
|
+
Note:
|
|
23
|
+
- Call this in middleware after resolving tenant
|
|
24
|
+
- Uses SET LOCAL so it's transaction-scoped
|
|
25
|
+
- Empty string = no tenant access (safe default when RLS enabled)
|
|
26
|
+
"""
|
|
27
|
+
from django.db import connection
|
|
28
|
+
|
|
29
|
+
value = "" if tenant_id is None else str(tenant_id)
|
|
30
|
+
with connection.cursor() as cursor:
|
|
31
|
+
cursor.execute("SELECT set_config('app.current_tenant', %s, true)", [value])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_db_tenant() -> UUID | None:
|
|
35
|
+
"""
|
|
36
|
+
Get the current tenant from Postgres session variable.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The tenant UUID if set, None otherwise
|
|
40
|
+
"""
|
|
41
|
+
from django.db import connection
|
|
42
|
+
|
|
43
|
+
with connection.cursor() as cursor:
|
|
44
|
+
cursor.execute("SELECT current_setting('app.current_tenant', true)")
|
|
45
|
+
result = cursor.fetchone()
|
|
46
|
+
if result and result[0]:
|
|
47
|
+
try:
|
|
48
|
+
return UUID(result[0])
|
|
49
|
+
except ValueError:
|
|
50
|
+
return None
|
|
51
|
+
return None
|