devora-django 0.1.0__tar.gz → 0.1.2__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.
- devora_django-0.1.2/PKG-INFO +199 -0
- devora_django-0.1.2/README.md +170 -0
- {devora_django-0.1.0 → devora_django-0.1.2}/pyproject.toml +2 -2
- {devora_django-0.1.0 → devora_django-0.1.2}/src/devora_sdk_django/adapter.py +43 -0
- devora_django-0.1.0/PKG-INFO +0 -141
- devora_django-0.1.0/README.md +0 -112
- {devora_django-0.1.0 → devora_django-0.1.2}/.gitignore +0 -0
- {devora_django-0.1.0 → devora_django-0.1.2}/LICENSE +0 -0
- {devora_django-0.1.0 → devora_django-0.1.2}/src/devora_sdk_django/__init__.py +0 -0
- {devora_django-0.1.0 → devora_django-0.1.2}/src/devora_sdk_django/py.typed +0 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: devora-django
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Django adapter for the Devora Python backend SDK
|
|
5
|
+
Project-URL: Documentation, https://docs.devora.sh
|
|
6
|
+
Project-URL: Repository, https://github.com/getdevora/devora-sdks
|
|
7
|
+
Project-URL: Issues, https://github.com/getdevora/devora-sdks/issues
|
|
8
|
+
Author: Devora
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: devora,django,impersonation,sdk,security
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Framework :: Django
|
|
14
|
+
Classifier: Framework :: Django :: 5.2
|
|
15
|
+
Classifier: Framework :: Django :: 6.0
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: <4.0,>=3.10
|
|
25
|
+
Requires-Dist: devora-python<0.2.0,>=0.1.2
|
|
26
|
+
Requires-Dist: django!=6.0.0,!=6.0.1,!=6.0.2,!=6.0.3,!=6.0.4,!=6.0.5,!=6.0.6,!=6.0.7,<7.0,>=5.2.17
|
|
27
|
+
Requires-Dist: sqlparse>=0.6.0
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# devora-django
|
|
31
|
+
|
|
32
|
+
Recording, masking and capture policy are configured in the Devora dashboard and
|
|
33
|
+
authorized server-side for each session. This backend SDK takes no capture
|
|
34
|
+
settings, and `devora_sdk()` ignores keyword arguments it does not recognize,
|
|
35
|
+
so check option names carefully. See
|
|
36
|
+
[capture settings](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
|
|
37
|
+
|
|
38
|
+
Django adapter for the Devora Python backend SDK.
|
|
39
|
+
|
|
40
|
+
## Requirements
|
|
41
|
+
|
|
42
|
+
- Python `>=3.10,<4.0`
|
|
43
|
+
- Django `>=5.2.17,<7.0`, excluding `6.0.0` through `6.0.7` (on Django 6.0, use `6.0.8` or newer)
|
|
44
|
+
- Outbound HTTPS access from your backend to the Devora API
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install devora-python devora-django
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# devora_integration.py
|
|
56
|
+
import os
|
|
57
|
+
|
|
58
|
+
from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
|
|
59
|
+
|
|
60
|
+
sdk = devora_sdk(
|
|
61
|
+
api_key=os.environ["DEVORA_API_KEY"], # pk_server_live_...
|
|
62
|
+
secret_key=os.environ["DEVORA_SECRET_KEY"], # sk_server_live_...
|
|
63
|
+
org_id=os.environ["DEVORA_ORG_ID"],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
|
|
68
|
+
def search_users(req):
|
|
69
|
+
# Match name, email and the exact user ID with a parameterised query.
|
|
70
|
+
term = req.query.get("term", "")
|
|
71
|
+
limit = int(req.query.get("limit", 10))
|
|
72
|
+
users = search_customer_users(term, limit)
|
|
73
|
+
return {
|
|
74
|
+
"users": [
|
|
75
|
+
{
|
|
76
|
+
"id": u.id,
|
|
77
|
+
"name": u.name,
|
|
78
|
+
"email": u.email,
|
|
79
|
+
"attributes": {"company": u.company, "role": u.role, "plan": u.plan},
|
|
80
|
+
}
|
|
81
|
+
for u in users
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@sdk.register(DEVORA_ENDPOINTS.TERMINATE)
|
|
87
|
+
def terminate(req):
|
|
88
|
+
session_id = req.params["id"]
|
|
89
|
+
reason = (req.body or {}).get("reason")
|
|
90
|
+
# YOU IMPLEMENT: mark this Devora session revoked so every credential issued for it
|
|
91
|
+
# is rejected, including one issued after this call. Must be idempotent: Devora
|
|
92
|
+
# retries with a new request id (up to 5 attempts over 6 hours).
|
|
93
|
+
auth.revoke_impersonation_session(session_id=session_id, reason=reason)
|
|
94
|
+
return {"success": True}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
User search should match name, email and the exact user ID. `attributes` are optional display fields such as company, role or plan: up to 12 per user, lowercase keys like `last_login`, and string, number, boolean or `null` values. Devora drops invalid entries silently; see [Search results and templates](https://docs.devora.sh/guide/search-results) for the limits and how your team lays out results.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# urls.py
|
|
101
|
+
from django.urls import include, path
|
|
102
|
+
from devora_sdk_django import django_urlpatterns
|
|
103
|
+
from .devora_integration import sdk
|
|
104
|
+
|
|
105
|
+
urlpatterns = [
|
|
106
|
+
path("devora/", include(django_urlpatterns(sdk))),
|
|
107
|
+
]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Register every handler before building the URL patterns; routes registered
|
|
111
|
+
later are not mounted. If any handler is an `async def` function, use
|
|
112
|
+
`async_django_urlpatterns(sdk)` instead (it runs sync handlers in a worker
|
|
113
|
+
thread too); `django_urlpatterns` answers an async handler with
|
|
114
|
+
`400 HANDLER_ERROR`.
|
|
115
|
+
|
|
116
|
+
Each verified request is claimed once from Devora before your handler runs, so
|
|
117
|
+
replay protection needs no storage on your side; your backend only needs
|
|
118
|
+
outbound HTTPS access to the Devora API. See
|
|
119
|
+
[SIGNING.md](https://github.com/getdevora/devora-sdks/blob/main/SIGNING.md#request-claims).
|
|
120
|
+
|
|
121
|
+
The terminate body is `{"reason": ..., "terminatedBy": ...}`; Devora retries a
|
|
122
|
+
failed call (up to 5 attempts over 6 hours) with a new request id, so keep the
|
|
123
|
+
handler idempotent. Return a 2xx, or 429/503 with `Retry-After` when overloaded; any
|
|
124
|
+
other 4xx stops the retries. See
|
|
125
|
+
[Session lifecycle & cleanup](https://docs.devora.sh/guide/session-lifecycle).
|
|
126
|
+
|
|
127
|
+
For protected application routes, install the guard middleware and return the
|
|
128
|
+
context dict your `IMPERSONATE` handler stored, read back from your
|
|
129
|
+
authenticated request state.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
# devora_guard.py
|
|
133
|
+
from devora_sdk_django import create_impersonation_guard
|
|
134
|
+
from .devora_integration import sdk
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_impersonation_context(request):
|
|
138
|
+
# YOU IMPLEMENT: return the dict your IMPERSONATE handler stored, read from
|
|
139
|
+
# your verified session (never from headers or JSON the browser can set), or None.
|
|
140
|
+
return getattr(request.user, "devora", None)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
DevoraGuardMiddleware = create_impersonation_guard(
|
|
144
|
+
sdk=sdk,
|
|
145
|
+
get_impersonation_context=get_impersonation_context,
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Add `"yourapp.devora_guard.DevoraGuardMiddleware"` to `settings.MIDDLEWARE`,
|
|
150
|
+
after your own authentication middleware, so `request.user` is set when the
|
|
151
|
+
guard runs.
|
|
152
|
+
|
|
153
|
+
Blocked API calls get a JSON error. Blocked page loads and form posts (an
|
|
154
|
+
`Accept` header with `text/html` and without `application/json`, no JSON body,
|
|
155
|
+
not an XHR) get a small, uncached HTML page with the reason and a back link. To show your own template, pass `render_blocked`, a
|
|
156
|
+
synchronous function that receives the request, the status code and the error
|
|
157
|
+
body and returns a response:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from django.shortcuts import render
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def render_blocked(request, status_code, body):
|
|
164
|
+
return render(request, "impersonation_blocked.html", {"error": body.get("error")}, status=status_code)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
DevoraGuardMiddleware = create_impersonation_guard(
|
|
168
|
+
sdk=sdk,
|
|
169
|
+
get_impersonation_context=get_impersonation_context,
|
|
170
|
+
render_blocked=render_blocked,
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`expires_at` must be a Unix timestamp in milliseconds. If your JWT stores Unix
|
|
175
|
+
seconds, multiply by `1000` when building the impersonation context. `actor`,
|
|
176
|
+
`subject`, `auth_method`, `authorization_source`, and `recording_allowed` are
|
|
177
|
+
all required — the guard rejects the context as invalid without them, even
|
|
178
|
+
though the dataclass marks them optional for construction convenience.
|
|
179
|
+
|
|
180
|
+
## Cross-Origin-Opener-Policy
|
|
181
|
+
|
|
182
|
+
Django's `SecurityMiddleware` sends `Cross-Origin-Opener-Policy: same-origin`
|
|
183
|
+
by default. That cuts the page opened by a Devora impersonation link off from
|
|
184
|
+
the dashboard tab that opened it, so the link cannot be redeemed. Set
|
|
185
|
+
`SECURE_CROSS_ORIGIN_OPENER_POLICY = None`, or send `unsafe-none` on the
|
|
186
|
+
route your Devora links land on.
|
|
187
|
+
|
|
188
|
+
## Request body limits
|
|
189
|
+
|
|
190
|
+
SDK views read at most `max_body_size + 1` bytes before parsing (1 MiB by default)
|
|
191
|
+
and reject oversized declared lengths without reading. The browser-session view
|
|
192
|
+
uses a 4 KiB limit. Async SDK views perform this bounded read off the event loop.
|
|
193
|
+
Set `DATA_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024` and keep earlier middleware from
|
|
194
|
+
buffering larger SDK bodies. WSGI/ASGI servers and reverse proxies also need
|
|
195
|
+
request-size and read-timeout limits: Django's ASGI handler can spool a request
|
|
196
|
+
before a view runs, and the SDK cannot bound that prior allocation.
|
|
197
|
+
|
|
198
|
+
Options, error codes and the browser-session bridge are documented in the
|
|
199
|
+
[Python reference](https://docs.devora.sh/reference/python).
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# devora-django
|
|
2
|
+
|
|
3
|
+
Recording, masking and capture policy are configured in the Devora dashboard and
|
|
4
|
+
authorized server-side for each session. This backend SDK takes no capture
|
|
5
|
+
settings, and `devora_sdk()` ignores keyword arguments it does not recognize,
|
|
6
|
+
so check option names carefully. See
|
|
7
|
+
[capture settings](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
|
|
8
|
+
|
|
9
|
+
Django adapter for the Devora Python backend SDK.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Python `>=3.10,<4.0`
|
|
14
|
+
- Django `>=5.2.17,<7.0`, excluding `6.0.0` through `6.0.7` (on Django 6.0, use `6.0.8` or newer)
|
|
15
|
+
- Outbound HTTPS access from your backend to the Devora API
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install devora-python devora-django
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
# devora_integration.py
|
|
27
|
+
import os
|
|
28
|
+
|
|
29
|
+
from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
|
|
30
|
+
|
|
31
|
+
sdk = devora_sdk(
|
|
32
|
+
api_key=os.environ["DEVORA_API_KEY"], # pk_server_live_...
|
|
33
|
+
secret_key=os.environ["DEVORA_SECRET_KEY"], # sk_server_live_...
|
|
34
|
+
org_id=os.environ["DEVORA_ORG_ID"],
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
|
|
39
|
+
def search_users(req):
|
|
40
|
+
# Match name, email and the exact user ID with a parameterised query.
|
|
41
|
+
term = req.query.get("term", "")
|
|
42
|
+
limit = int(req.query.get("limit", 10))
|
|
43
|
+
users = search_customer_users(term, limit)
|
|
44
|
+
return {
|
|
45
|
+
"users": [
|
|
46
|
+
{
|
|
47
|
+
"id": u.id,
|
|
48
|
+
"name": u.name,
|
|
49
|
+
"email": u.email,
|
|
50
|
+
"attributes": {"company": u.company, "role": u.role, "plan": u.plan},
|
|
51
|
+
}
|
|
52
|
+
for u in users
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@sdk.register(DEVORA_ENDPOINTS.TERMINATE)
|
|
58
|
+
def terminate(req):
|
|
59
|
+
session_id = req.params["id"]
|
|
60
|
+
reason = (req.body or {}).get("reason")
|
|
61
|
+
# YOU IMPLEMENT: mark this Devora session revoked so every credential issued for it
|
|
62
|
+
# is rejected, including one issued after this call. Must be idempotent: Devora
|
|
63
|
+
# retries with a new request id (up to 5 attempts over 6 hours).
|
|
64
|
+
auth.revoke_impersonation_session(session_id=session_id, reason=reason)
|
|
65
|
+
return {"success": True}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
User search should match name, email and the exact user ID. `attributes` are optional display fields such as company, role or plan: up to 12 per user, lowercase keys like `last_login`, and string, number, boolean or `null` values. Devora drops invalid entries silently; see [Search results and templates](https://docs.devora.sh/guide/search-results) for the limits and how your team lays out results.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# urls.py
|
|
72
|
+
from django.urls import include, path
|
|
73
|
+
from devora_sdk_django import django_urlpatterns
|
|
74
|
+
from .devora_integration import sdk
|
|
75
|
+
|
|
76
|
+
urlpatterns = [
|
|
77
|
+
path("devora/", include(django_urlpatterns(sdk))),
|
|
78
|
+
]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Register every handler before building the URL patterns; routes registered
|
|
82
|
+
later are not mounted. If any handler is an `async def` function, use
|
|
83
|
+
`async_django_urlpatterns(sdk)` instead (it runs sync handlers in a worker
|
|
84
|
+
thread too); `django_urlpatterns` answers an async handler with
|
|
85
|
+
`400 HANDLER_ERROR`.
|
|
86
|
+
|
|
87
|
+
Each verified request is claimed once from Devora before your handler runs, so
|
|
88
|
+
replay protection needs no storage on your side; your backend only needs
|
|
89
|
+
outbound HTTPS access to the Devora API. See
|
|
90
|
+
[SIGNING.md](https://github.com/getdevora/devora-sdks/blob/main/SIGNING.md#request-claims).
|
|
91
|
+
|
|
92
|
+
The terminate body is `{"reason": ..., "terminatedBy": ...}`; Devora retries a
|
|
93
|
+
failed call (up to 5 attempts over 6 hours) with a new request id, so keep the
|
|
94
|
+
handler idempotent. Return a 2xx, or 429/503 with `Retry-After` when overloaded; any
|
|
95
|
+
other 4xx stops the retries. See
|
|
96
|
+
[Session lifecycle & cleanup](https://docs.devora.sh/guide/session-lifecycle).
|
|
97
|
+
|
|
98
|
+
For protected application routes, install the guard middleware and return the
|
|
99
|
+
context dict your `IMPERSONATE` handler stored, read back from your
|
|
100
|
+
authenticated request state.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
# devora_guard.py
|
|
104
|
+
from devora_sdk_django import create_impersonation_guard
|
|
105
|
+
from .devora_integration import sdk
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_impersonation_context(request):
|
|
109
|
+
# YOU IMPLEMENT: return the dict your IMPERSONATE handler stored, read from
|
|
110
|
+
# your verified session (never from headers or JSON the browser can set), or None.
|
|
111
|
+
return getattr(request.user, "devora", None)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
DevoraGuardMiddleware = create_impersonation_guard(
|
|
115
|
+
sdk=sdk,
|
|
116
|
+
get_impersonation_context=get_impersonation_context,
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Add `"yourapp.devora_guard.DevoraGuardMiddleware"` to `settings.MIDDLEWARE`,
|
|
121
|
+
after your own authentication middleware, so `request.user` is set when the
|
|
122
|
+
guard runs.
|
|
123
|
+
|
|
124
|
+
Blocked API calls get a JSON error. Blocked page loads and form posts (an
|
|
125
|
+
`Accept` header with `text/html` and without `application/json`, no JSON body,
|
|
126
|
+
not an XHR) get a small, uncached HTML page with the reason and a back link. To show your own template, pass `render_blocked`, a
|
|
127
|
+
synchronous function that receives the request, the status code and the error
|
|
128
|
+
body and returns a response:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from django.shortcuts import render
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def render_blocked(request, status_code, body):
|
|
135
|
+
return render(request, "impersonation_blocked.html", {"error": body.get("error")}, status=status_code)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
DevoraGuardMiddleware = create_impersonation_guard(
|
|
139
|
+
sdk=sdk,
|
|
140
|
+
get_impersonation_context=get_impersonation_context,
|
|
141
|
+
render_blocked=render_blocked,
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`expires_at` must be a Unix timestamp in milliseconds. If your JWT stores Unix
|
|
146
|
+
seconds, multiply by `1000` when building the impersonation context. `actor`,
|
|
147
|
+
`subject`, `auth_method`, `authorization_source`, and `recording_allowed` are
|
|
148
|
+
all required — the guard rejects the context as invalid without them, even
|
|
149
|
+
though the dataclass marks them optional for construction convenience.
|
|
150
|
+
|
|
151
|
+
## Cross-Origin-Opener-Policy
|
|
152
|
+
|
|
153
|
+
Django's `SecurityMiddleware` sends `Cross-Origin-Opener-Policy: same-origin`
|
|
154
|
+
by default. That cuts the page opened by a Devora impersonation link off from
|
|
155
|
+
the dashboard tab that opened it, so the link cannot be redeemed. Set
|
|
156
|
+
`SECURE_CROSS_ORIGIN_OPENER_POLICY = None`, or send `unsafe-none` on the
|
|
157
|
+
route your Devora links land on.
|
|
158
|
+
|
|
159
|
+
## Request body limits
|
|
160
|
+
|
|
161
|
+
SDK views read at most `max_body_size + 1` bytes before parsing (1 MiB by default)
|
|
162
|
+
and reject oversized declared lengths without reading. The browser-session view
|
|
163
|
+
uses a 4 KiB limit. Async SDK views perform this bounded read off the event loop.
|
|
164
|
+
Set `DATA_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024` and keep earlier middleware from
|
|
165
|
+
buffering larger SDK bodies. WSGI/ASGI servers and reverse proxies also need
|
|
166
|
+
request-size and read-timeout limits: Django's ASGI handler can spool a request
|
|
167
|
+
before a view runs, and the SDK cannot bound that prior allocation.
|
|
168
|
+
|
|
169
|
+
Options, error codes and the browser-session bridge are documented in the
|
|
170
|
+
[Python reference](https://docs.devora.sh/reference/python).
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devora-django"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Django adapter for the Devora Python backend SDK"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10,<4.0"
|
|
@@ -27,7 +27,7 @@ classifiers = [
|
|
|
27
27
|
"Typing :: Typed",
|
|
28
28
|
]
|
|
29
29
|
dependencies = [
|
|
30
|
-
"devora-python>=0.1.
|
|
30
|
+
"devora-python>=0.1.2,<0.2.0",
|
|
31
31
|
# Security floors: the lowest versions without known advisories (audited 2026-09-28).
|
|
32
32
|
"Django>=5.2.17,<7.0,!=6.0.0,!=6.0.1,!=6.0.2,!=6.0.3,!=6.0.4,!=6.0.5,!=6.0.6,!=6.0.7",
|
|
33
33
|
"sqlparse>=0.6.0",
|
|
@@ -59,6 +59,39 @@ class DjangoImpersonationGuardOptions:
|
|
|
59
59
|
on_liveness_unavailable: str = "deny"
|
|
60
60
|
is_impersonation_allowed: Optional[Callable[[Any, ImpersonationContext], bool]] = None
|
|
61
61
|
bridge_path: Optional[str] = None
|
|
62
|
+
render_blocked: Optional[Callable[[Any, int, Mapping[str, Any]], Any]] = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _wants_html(request: Any) -> bool:
|
|
66
|
+
"""A browser page load or form post, as opposed to a fetch/XHR/API call."""
|
|
67
|
+
headers = request.headers
|
|
68
|
+
if headers.get("X-Requested-With") == "XMLHttpRequest":
|
|
69
|
+
return False
|
|
70
|
+
if "application/json" in (headers.get("Content-Type") or ""):
|
|
71
|
+
return False
|
|
72
|
+
accept = headers.get("Accept") or ""
|
|
73
|
+
return "text/html" in accept and "application/json" not in accept
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _blocked_html_response(request: Any, status_code: int, body: Mapping[str, Any]) -> Any:
|
|
77
|
+
"""Default page for a blocked page load or form post: plain, escaped, never cached."""
|
|
78
|
+
from django.http import HttpResponse
|
|
79
|
+
from django.utils.html import escape
|
|
80
|
+
|
|
81
|
+
message = escape(str(body.get("error") or "This action is not allowed during impersonation"))
|
|
82
|
+
html = (
|
|
83
|
+
'<!doctype html><html lang="en"><head><meta charset="utf-8">'
|
|
84
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">'
|
|
85
|
+
"<title>Action not allowed</title></head>"
|
|
86
|
+
'<body style="font-family:system-ui,sans-serif;max-width:32rem;margin:4rem auto;padding:0 1rem">'
|
|
87
|
+
"<h1 style=\"font-size:1.25rem\">This action is not allowed in this session</h1>"
|
|
88
|
+
f"<p>{message}.</p>"
|
|
89
|
+
'<p><a href="javascript:history.back()">Go back</a></p>'
|
|
90
|
+
"</body></html>"
|
|
91
|
+
)
|
|
92
|
+
response = HttpResponse(html, status=status_code, content_type="text/html; charset=utf-8")
|
|
93
|
+
response["Cache-Control"] = "private, no-store"
|
|
94
|
+
return response
|
|
62
95
|
|
|
63
96
|
|
|
64
97
|
def django_urlpatterns(sdk: Any, options: Optional[DjangoAdapterOptions] = None) -> list[Any]:
|
|
@@ -234,6 +267,7 @@ def create_impersonation_guard(
|
|
|
234
267
|
on_liveness_unavailable: str = "deny",
|
|
235
268
|
is_impersonation_allowed: Optional[Callable[[Any, ImpersonationContext], bool]] = None,
|
|
236
269
|
bridge_path: Optional[str] = None,
|
|
270
|
+
render_blocked: Optional[Callable[[Any, int, Mapping[str, Any]], Any]] = None,
|
|
237
271
|
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
238
272
|
"""Django middleware factory (also usable as a view decorator).
|
|
239
273
|
|
|
@@ -245,6 +279,11 @@ def create_impersonation_guard(
|
|
|
245
279
|
including ``SCRIPT_NAME``). Deny rules also match ``path_info`` and the path
|
|
246
280
|
with an ``i18n_patterns`` language prefix removed. ``bridge_path`` enables the
|
|
247
281
|
read-scope allowance for a mounted browser-session bridge (off by default).
|
|
282
|
+
|
|
283
|
+
Blocked API calls (fetch/XHR, JSON) get a JSON error. Blocked page loads and
|
|
284
|
+
form posts get a small HTML page instead, or whatever
|
|
285
|
+
``render_blocked(request, status_code, body)`` returns, so a server-rendered
|
|
286
|
+
app can show its own template.
|
|
248
287
|
"""
|
|
249
288
|
options = DjangoImpersonationGuardOptions(
|
|
250
289
|
sdk=sdk,
|
|
@@ -257,6 +296,7 @@ def create_impersonation_guard(
|
|
|
257
296
|
on_liveness_unavailable=on_liveness_unavailable,
|
|
258
297
|
is_impersonation_allowed=is_impersonation_allowed,
|
|
259
298
|
bridge_path=bridge_path,
|
|
299
|
+
render_blocked=render_blocked,
|
|
260
300
|
)
|
|
261
301
|
liveness = (
|
|
262
302
|
SessionLivenessChecker(sdk, liveness_cache_ttl_ms, on_liveness_unavailable)
|
|
@@ -324,6 +364,9 @@ def create_impersonation_guard(
|
|
|
324
364
|
)
|
|
325
365
|
|
|
326
366
|
status_code, body = _guard_response(decision.status_code, decision.body, options.blocked_response)
|
|
367
|
+
if _wants_html(request):
|
|
368
|
+
render = options.render_blocked or _blocked_html_response
|
|
369
|
+
return render(request, status_code, body)
|
|
327
370
|
return _private_json_response(body, status=status_code)
|
|
328
371
|
|
|
329
372
|
def guard_failed() -> Any:
|
devora_django-0.1.0/PKG-INFO
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: devora-django
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Django adapter for the Devora Python backend SDK
|
|
5
|
-
Project-URL: Documentation, https://docs.devora.sh
|
|
6
|
-
Project-URL: Repository, https://github.com/getdevora/devora-sdks
|
|
7
|
-
Project-URL: Issues, https://github.com/getdevora/devora-sdks/issues
|
|
8
|
-
Author: Devora
|
|
9
|
-
License-Expression: MIT
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: devora,django,impersonation,sdk,security
|
|
12
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
-
Classifier: Framework :: Django
|
|
14
|
-
Classifier: Framework :: Django :: 5.2
|
|
15
|
-
Classifier: Framework :: Django :: 6.0
|
|
16
|
-
Classifier: Intended Audience :: Developers
|
|
17
|
-
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
-
Classifier: Typing :: Typed
|
|
24
|
-
Requires-Python: <4.0,>=3.10
|
|
25
|
-
Requires-Dist: devora-python<0.2.0,>=0.1.0
|
|
26
|
-
Requires-Dist: django!=6.0.0,!=6.0.1,!=6.0.2,!=6.0.3,!=6.0.4,!=6.0.5,!=6.0.6,!=6.0.7,<7.0,>=5.2.17
|
|
27
|
-
Requires-Dist: sqlparse>=0.6.0
|
|
28
|
-
Description-Content-Type: text/markdown
|
|
29
|
-
|
|
30
|
-
# devora-django
|
|
31
|
-
|
|
32
|
-
Recording, masking and activity preferences are configured in Devora Settings.
|
|
33
|
-
SDK initialization overrides are ignored. New sessions retain the server's policy
|
|
34
|
-
snapshot across exchange and resume. Developer privacy labels take effect only
|
|
35
|
-
when selected in Settings; sensitive-field protection remains mandatory.
|
|
36
|
-
See [migration details](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
|
|
37
|
-
|
|
38
|
-
Django adapter for the Devora Python backend SDK.
|
|
39
|
-
|
|
40
|
-
## Requirements
|
|
41
|
-
|
|
42
|
-
- Python `>=3.10,<4.0`
|
|
43
|
-
- Django `>=5.2,<7.0`
|
|
44
|
-
|
|
45
|
-
## Install
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
pip install devora-python devora-django
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Quick Start
|
|
52
|
-
|
|
53
|
-
```python
|
|
54
|
-
# devora_integration.py
|
|
55
|
-
from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
|
|
56
|
-
|
|
57
|
-
sdk = devora_sdk(
|
|
58
|
-
api_key="pk_server_live_...",
|
|
59
|
-
secret_key="sk_server_live_...",
|
|
60
|
-
org_id="org_...",
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
|
|
65
|
-
def search_users(req):
|
|
66
|
-
return {"users": search_customer_users(req.query.get("term", ""))}
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
```python
|
|
70
|
-
# urls.py
|
|
71
|
-
from django.urls import include, path
|
|
72
|
-
from devora_sdk_django import django_urlpatterns
|
|
73
|
-
from .devora_integration import sdk
|
|
74
|
-
|
|
75
|
-
urlpatterns = [
|
|
76
|
-
path("devora/", include(django_urlpatterns(sdk))),
|
|
77
|
-
]
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Use `async_django_urlpatterns(sdk)` instead when your exposed handlers are
|
|
81
|
-
async functions.
|
|
82
|
-
|
|
83
|
-
For protected application routes, install the guard middleware and extract a
|
|
84
|
-
trusted impersonation context from your authenticated request state.
|
|
85
|
-
|
|
86
|
-
```python
|
|
87
|
-
from devora_sdk import ImpersonationContext
|
|
88
|
-
from devora_sdk_django import create_impersonation_guard
|
|
89
|
-
from .devora_integration import sdk
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def get_impersonation_context(request):
|
|
93
|
-
devora = getattr(request.user, "devora", None)
|
|
94
|
-
if not devora:
|
|
95
|
-
return None
|
|
96
|
-
return ImpersonationContext(
|
|
97
|
-
is_impersonation=devora["isImpersonation"],
|
|
98
|
-
scope=devora["scope"],
|
|
99
|
-
session_id=devora["sessionId"],
|
|
100
|
-
expires_at=devora["expiresAt"],
|
|
101
|
-
actor=devora["actor"],
|
|
102
|
-
subject=devora["subject"],
|
|
103
|
-
auth_method=devora["authMethod"],
|
|
104
|
-
authorization_source=devora["authorizationSource"],
|
|
105
|
-
recording_allowed=devora["recordingAllowed"],
|
|
106
|
-
impersonator=devora.get("impersonator"),
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
DevoraGuardMiddleware = create_impersonation_guard(
|
|
111
|
-
sdk=sdk,
|
|
112
|
-
get_impersonation_context=get_impersonation_context,
|
|
113
|
-
)
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Add `"yourapp.devora_integration.DevoraGuardMiddleware"` to `settings.MIDDLEWARE`,
|
|
117
|
-
after your own authentication middleware.
|
|
118
|
-
|
|
119
|
-
`expires_at` must be a Unix timestamp in milliseconds. If your JWT stores Unix
|
|
120
|
-
seconds, multiply by `1000` when building the impersonation context. `actor`,
|
|
121
|
-
`subject`, `auth_method`, `authorization_source`, and `recording_allowed` are
|
|
122
|
-
all required — the guard rejects the context as invalid without them, even
|
|
123
|
-
though the dataclass marks them optional for construction convenience.
|
|
124
|
-
|
|
125
|
-
## Cross-Origin-Opener-Policy
|
|
126
|
-
|
|
127
|
-
Django's `SecurityMiddleware` sends `Cross-Origin-Opener-Policy: same-origin`
|
|
128
|
-
by default. That cuts the page opened by a Devora impersonation link off from
|
|
129
|
-
the dashboard tab that opened it, so the link cannot be redeemed. Set
|
|
130
|
-
`SECURE_CROSS_ORIGIN_OPENER_POLICY = None`, or send `unsafe-none` on the
|
|
131
|
-
route your Devora links land on.
|
|
132
|
-
|
|
133
|
-
## Request body limits
|
|
134
|
-
|
|
135
|
-
SDK views read at most `max_body_size + 1` bytes before parsing (1 MiB by default)
|
|
136
|
-
and reject oversized declared lengths without reading. The browser-session view
|
|
137
|
-
uses a 4 KiB limit. Async SDK views perform this bounded read off the event loop.
|
|
138
|
-
Set `DATA_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024` and keep earlier middleware from
|
|
139
|
-
buffering larger SDK bodies. WSGI/ASGI servers and reverse proxies also need
|
|
140
|
-
request-size and read-timeout limits: Django's ASGI handler can spool a request
|
|
141
|
-
before a view runs, and the SDK cannot bound that prior allocation.
|
devora_django-0.1.0/README.md
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
# devora-django
|
|
2
|
-
|
|
3
|
-
Recording, masking and activity preferences are configured in Devora Settings.
|
|
4
|
-
SDK initialization overrides are ignored. New sessions retain the server's policy
|
|
5
|
-
snapshot across exchange and resume. Developer privacy labels take effect only
|
|
6
|
-
when selected in Settings; sensitive-field protection remains mandatory.
|
|
7
|
-
See [migration details](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
|
|
8
|
-
|
|
9
|
-
Django adapter for the Devora Python backend SDK.
|
|
10
|
-
|
|
11
|
-
## Requirements
|
|
12
|
-
|
|
13
|
-
- Python `>=3.10,<4.0`
|
|
14
|
-
- Django `>=5.2,<7.0`
|
|
15
|
-
|
|
16
|
-
## Install
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
pip install devora-python devora-django
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Quick Start
|
|
23
|
-
|
|
24
|
-
```python
|
|
25
|
-
# devora_integration.py
|
|
26
|
-
from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
|
|
27
|
-
|
|
28
|
-
sdk = devora_sdk(
|
|
29
|
-
api_key="pk_server_live_...",
|
|
30
|
-
secret_key="sk_server_live_...",
|
|
31
|
-
org_id="org_...",
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
|
|
36
|
-
def search_users(req):
|
|
37
|
-
return {"users": search_customer_users(req.query.get("term", ""))}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
```python
|
|
41
|
-
# urls.py
|
|
42
|
-
from django.urls import include, path
|
|
43
|
-
from devora_sdk_django import django_urlpatterns
|
|
44
|
-
from .devora_integration import sdk
|
|
45
|
-
|
|
46
|
-
urlpatterns = [
|
|
47
|
-
path("devora/", include(django_urlpatterns(sdk))),
|
|
48
|
-
]
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Use `async_django_urlpatterns(sdk)` instead when your exposed handlers are
|
|
52
|
-
async functions.
|
|
53
|
-
|
|
54
|
-
For protected application routes, install the guard middleware and extract a
|
|
55
|
-
trusted impersonation context from your authenticated request state.
|
|
56
|
-
|
|
57
|
-
```python
|
|
58
|
-
from devora_sdk import ImpersonationContext
|
|
59
|
-
from devora_sdk_django import create_impersonation_guard
|
|
60
|
-
from .devora_integration import sdk
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def get_impersonation_context(request):
|
|
64
|
-
devora = getattr(request.user, "devora", None)
|
|
65
|
-
if not devora:
|
|
66
|
-
return None
|
|
67
|
-
return ImpersonationContext(
|
|
68
|
-
is_impersonation=devora["isImpersonation"],
|
|
69
|
-
scope=devora["scope"],
|
|
70
|
-
session_id=devora["sessionId"],
|
|
71
|
-
expires_at=devora["expiresAt"],
|
|
72
|
-
actor=devora["actor"],
|
|
73
|
-
subject=devora["subject"],
|
|
74
|
-
auth_method=devora["authMethod"],
|
|
75
|
-
authorization_source=devora["authorizationSource"],
|
|
76
|
-
recording_allowed=devora["recordingAllowed"],
|
|
77
|
-
impersonator=devora.get("impersonator"),
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
DevoraGuardMiddleware = create_impersonation_guard(
|
|
82
|
-
sdk=sdk,
|
|
83
|
-
get_impersonation_context=get_impersonation_context,
|
|
84
|
-
)
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Add `"yourapp.devora_integration.DevoraGuardMiddleware"` to `settings.MIDDLEWARE`,
|
|
88
|
-
after your own authentication middleware.
|
|
89
|
-
|
|
90
|
-
`expires_at` must be a Unix timestamp in milliseconds. If your JWT stores Unix
|
|
91
|
-
seconds, multiply by `1000` when building the impersonation context. `actor`,
|
|
92
|
-
`subject`, `auth_method`, `authorization_source`, and `recording_allowed` are
|
|
93
|
-
all required — the guard rejects the context as invalid without them, even
|
|
94
|
-
though the dataclass marks them optional for construction convenience.
|
|
95
|
-
|
|
96
|
-
## Cross-Origin-Opener-Policy
|
|
97
|
-
|
|
98
|
-
Django's `SecurityMiddleware` sends `Cross-Origin-Opener-Policy: same-origin`
|
|
99
|
-
by default. That cuts the page opened by a Devora impersonation link off from
|
|
100
|
-
the dashboard tab that opened it, so the link cannot be redeemed. Set
|
|
101
|
-
`SECURE_CROSS_ORIGIN_OPENER_POLICY = None`, or send `unsafe-none` on the
|
|
102
|
-
route your Devora links land on.
|
|
103
|
-
|
|
104
|
-
## Request body limits
|
|
105
|
-
|
|
106
|
-
SDK views read at most `max_body_size + 1` bytes before parsing (1 MiB by default)
|
|
107
|
-
and reject oversized declared lengths without reading. The browser-session view
|
|
108
|
-
uses a 4 KiB limit. Async SDK views perform this bounded read off the event loop.
|
|
109
|
-
Set `DATA_UPLOAD_MAX_MEMORY_SIZE = 1024 * 1024` and keep earlier middleware from
|
|
110
|
-
buffering larger SDK bodies. WSGI/ASGI servers and reverse proxies also need
|
|
111
|
-
request-size and read-timeout limits: Django's ASGI handler can spool a request
|
|
112
|
-
before a view runs, and the SDK cannot bound that prior allocation.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|