caronte-sdk 0.2.4__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.
- caronte_sdk-0.2.4/.gitignore +41 -0
- caronte_sdk-0.2.4/LICENSE +26 -0
- caronte_sdk-0.2.4/PKG-INFO +189 -0
- caronte_sdk-0.2.4/README.md +118 -0
- caronte_sdk-0.2.4/argos/__init__.py +27 -0
- caronte_sdk-0.2.4/argos/adapters/__init__.py +1 -0
- caronte_sdk-0.2.4/argos/adapters/django.py +145 -0
- caronte_sdk-0.2.4/argos/adapters/fastapi.py +181 -0
- caronte_sdk-0.2.4/argos/adapters/flask.py +128 -0
- caronte_sdk-0.2.4/argos/adapters/strawberry.py +181 -0
- caronte_sdk-0.2.4/argos/client.py +327 -0
- caronte_sdk-0.2.4/argos/decorators.py +81 -0
- caronte_sdk-0.2.4/argos/exceptions.py +25 -0
- caronte_sdk-0.2.4/argos/middleware.py +75 -0
- caronte_sdk-0.2.4/argos/models.py +49 -0
- caronte_sdk-0.2.4/pyproject.toml +70 -0
- caronte_sdk-0.2.4/tests/__init__.py +0 -0
- caronte_sdk-0.2.4/tests/test_client.py +227 -0
- caronte_sdk-0.2.4/tests/test_decorators.py +127 -0
- caronte_sdk-0.2.4/tests/test_django_adapter.py +144 -0
- caronte_sdk-0.2.4/tests/test_flask_adapter.py +178 -0
- caronte_sdk-0.2.4/tests/test_middleware.py +157 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Env files
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
!.env.example
|
|
5
|
+
|
|
6
|
+
# Node
|
|
7
|
+
node_modules/
|
|
8
|
+
dist/
|
|
9
|
+
*.tgz
|
|
10
|
+
*.tsbuildinfo
|
|
11
|
+
npm-debug.log*
|
|
12
|
+
|
|
13
|
+
# Angular
|
|
14
|
+
.angular/
|
|
15
|
+
|
|
16
|
+
# Python
|
|
17
|
+
__pycache__/
|
|
18
|
+
*.pyc
|
|
19
|
+
*.pyo
|
|
20
|
+
*.egg-info/
|
|
21
|
+
.pytest_cache/
|
|
22
|
+
.venv/
|
|
23
|
+
venv/
|
|
24
|
+
|
|
25
|
+
# OS
|
|
26
|
+
.DS_Store
|
|
27
|
+
Thumbs.db
|
|
28
|
+
|
|
29
|
+
# IDE
|
|
30
|
+
.vscode/
|
|
31
|
+
.idea/
|
|
32
|
+
|
|
33
|
+
hermes/docs/demo/
|
|
34
|
+
hermes/docs/storybook/
|
|
35
|
+
hermes/docs/figma/
|
|
36
|
+
hermes/docs/documentation.json
|
|
37
|
+
hermes/docs/website/src/compiled-content/*
|
|
38
|
+
!hermes/docs/website/src/compiled-content/.gitkeep
|
|
39
|
+
|
|
40
|
+
# Clone de referência do shadcn-ui/ui upstream — não é nosso repo, não deve virar gitlink.
|
|
41
|
+
/shadcn/
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
CC0 1.0 Universal
|
|
2
|
+
|
|
3
|
+
Statement of Purpose
|
|
4
|
+
|
|
5
|
+
The laws of most jurisdictions throughout the world automatically confer
|
|
6
|
+
exclusive Copyright and Related Rights (defined below) upon the creator and
|
|
7
|
+
subsequent owner(s) of an original work of authorship and/or a database
|
|
8
|
+
(each, a "Work").
|
|
9
|
+
|
|
10
|
+
Certain owners wish to permanently relinquish those rights to a Work for the
|
|
11
|
+
purpose of contributing to a commons of creative, cultural and scientific works
|
|
12
|
+
that the public can reliably and without fear of infringement build upon,
|
|
13
|
+
modify, incorporate in other works, cite, and distribute, as freely as
|
|
14
|
+
possible, without legal restriction.
|
|
15
|
+
|
|
16
|
+
To the greatest extent permitted by, but not in contravention of, applicable
|
|
17
|
+
law, Affirmer hereby overtly, fully, permanently, irrevocably and
|
|
18
|
+
unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
|
|
19
|
+
and Related Rights and associated claims and causes of action, in the Work.
|
|
20
|
+
|
|
21
|
+
Should any part of this dedication be judged legally invalid or ineffective
|
|
22
|
+
under applicable law, the dedication shall be preserved to the maximum extent
|
|
23
|
+
permitted by law.
|
|
24
|
+
|
|
25
|
+
For more information, please see:
|
|
26
|
+
https://creativecommons.org/publicdomain/zero/1.0/
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: caronte-sdk
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: Python client for the Argos authorizer (OIDC + RBAC)
|
|
5
|
+
Project-URL: Homepage, https://github.com/xlucvvs/argos-python
|
|
6
|
+
Project-URL: Repository, https://github.com/xlucvvs/argos-python
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/xlucvvs/argos-python/issues
|
|
8
|
+
Author-email: Lucas Ribeiro <lucasribeiro.sec@gmail.com>
|
|
9
|
+
License: CC0 1.0 Universal
|
|
10
|
+
|
|
11
|
+
Statement of Purpose
|
|
12
|
+
|
|
13
|
+
The laws of most jurisdictions throughout the world automatically confer
|
|
14
|
+
exclusive Copyright and Related Rights (defined below) upon the creator and
|
|
15
|
+
subsequent owner(s) of an original work of authorship and/or a database
|
|
16
|
+
(each, a "Work").
|
|
17
|
+
|
|
18
|
+
Certain owners wish to permanently relinquish those rights to a Work for the
|
|
19
|
+
purpose of contributing to a commons of creative, cultural and scientific works
|
|
20
|
+
that the public can reliably and without fear of infringement build upon,
|
|
21
|
+
modify, incorporate in other works, cite, and distribute, as freely as
|
|
22
|
+
possible, without legal restriction.
|
|
23
|
+
|
|
24
|
+
To the greatest extent permitted by, but not in contravention of, applicable
|
|
25
|
+
law, Affirmer hereby overtly, fully, permanently, irrevocably and
|
|
26
|
+
unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
|
|
27
|
+
and Related Rights and associated claims and causes of action, in the Work.
|
|
28
|
+
|
|
29
|
+
Should any part of this dedication be judged legally invalid or ineffective
|
|
30
|
+
under applicable law, the dedication shall be preserved to the maximum extent
|
|
31
|
+
permitted by law.
|
|
32
|
+
|
|
33
|
+
For more information, please see:
|
|
34
|
+
https://creativecommons.org/publicdomain/zero/1.0/
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Keywords: argos,auth,authorization,jwt,oidc,rbac
|
|
37
|
+
Classifier: Development Status :: 4 - Beta
|
|
38
|
+
Classifier: Framework :: Django
|
|
39
|
+
Classifier: Framework :: FastAPI
|
|
40
|
+
Classifier: Framework :: Flask
|
|
41
|
+
Classifier: Intended Audience :: Developers
|
|
42
|
+
Classifier: License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
|
|
43
|
+
Classifier: Programming Language :: Python :: 3
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
46
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
47
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
48
|
+
Classifier: Topic :: Security
|
|
49
|
+
Requires-Python: >=3.11
|
|
50
|
+
Requires-Dist: httpx>=0.27
|
|
51
|
+
Requires-Dist: pyjwt[crypto]>=2.8
|
|
52
|
+
Requires-Dist: python-jose[cryptography]>=3.3
|
|
53
|
+
Provides-Extra: dev
|
|
54
|
+
Requires-Dist: cryptography>=42; extra == 'dev'
|
|
55
|
+
Requires-Dist: django>=4.0; extra == 'dev'
|
|
56
|
+
Requires-Dist: flask>=2.0; extra == 'dev'
|
|
57
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
58
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
59
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
60
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
61
|
+
Provides-Extra: django
|
|
62
|
+
Requires-Dist: django>=4.0; extra == 'django'
|
|
63
|
+
Provides-Extra: fastapi
|
|
64
|
+
Requires-Dist: fastapi>=0.111; extra == 'fastapi'
|
|
65
|
+
Requires-Dist: starlette>=0.37; extra == 'fastapi'
|
|
66
|
+
Provides-Extra: flask
|
|
67
|
+
Requires-Dist: flask>=2.0; extra == 'flask'
|
|
68
|
+
Provides-Extra: strawberry
|
|
69
|
+
Requires-Dist: strawberry-graphql>=0.235; extra == 'strawberry'
|
|
70
|
+
Description-Content-Type: text/markdown
|
|
71
|
+
|
|
72
|
+
# argos
|
|
73
|
+
|
|
74
|
+
Python client for the **Argos** authorizer — handles app authentication,
|
|
75
|
+
JWT validation, permission checking and automatic operation sync.
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install argos
|
|
81
|
+
# With FastAPI support
|
|
82
|
+
pip install "argos[fastapi]"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Quick start with FastAPI
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from fastapi import FastAPI, Request
|
|
89
|
+
from argos.adapters.fastapi import Argos, operation
|
|
90
|
+
|
|
91
|
+
app = FastAPI()
|
|
92
|
+
|
|
93
|
+
argos = Argos(
|
|
94
|
+
app,
|
|
95
|
+
authorizer_url="http://localhost:4000/api",
|
|
96
|
+
app_id="your-app-uuid",
|
|
97
|
+
secret="your-app-secret",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.get("/public")
|
|
102
|
+
@operation(id="public.info", level="public")
|
|
103
|
+
async def public_info():
|
|
104
|
+
"""Anyone can call this — no token needed."""
|
|
105
|
+
return {"message": "Hello, world!"}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.get("/items")
|
|
109
|
+
@operation(id="items.list", level="private")
|
|
110
|
+
async def list_items(request: Request):
|
|
111
|
+
"""Requires any authenticated user (token with non-empty groups)."""
|
|
112
|
+
user = request.state.user # TokenClaims
|
|
113
|
+
return {"user": user.sub, "items": []}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@app.delete("/items/{item_id}")
|
|
117
|
+
@operation(id="items.delete", level="protected")
|
|
118
|
+
async def delete_item(item_id: str, request: Request):
|
|
119
|
+
"""Requires a user whose group has explicit permission for items.delete."""
|
|
120
|
+
return {"deleted": item_id}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Standalone client
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import asyncio
|
|
127
|
+
from argos import ArgosClient, OperationDescriptor
|
|
128
|
+
|
|
129
|
+
client = ArgosClient(
|
|
130
|
+
authorizer_url="http://localhost:4000/api",
|
|
131
|
+
app_id="your-app-uuid",
|
|
132
|
+
secret="your-app-secret",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
client.register_operation(
|
|
136
|
+
OperationDescriptor(identifier="items.list", method="read", level="private")
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def main():
|
|
140
|
+
await client.startup()
|
|
141
|
+
|
|
142
|
+
# Validate a token from an incoming request
|
|
143
|
+
claims = await client.validate_token("<bearer-token>")
|
|
144
|
+
|
|
145
|
+
# Check whether the token has permission
|
|
146
|
+
allowed = client.check_permission(claims, "items.list", "read")
|
|
147
|
+
print(f"Allowed: {allowed}")
|
|
148
|
+
|
|
149
|
+
asyncio.run(main())
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Operation levels
|
|
153
|
+
|
|
154
|
+
| Level | Who can access |
|
|
155
|
+
|-------------|----------------|
|
|
156
|
+
| `public` | Everyone — no token required |
|
|
157
|
+
| `private` | Any authenticated user (token with at least one group) |
|
|
158
|
+
| `protected` | Only users whose groups intersect the operation's `allowed_groups` |
|
|
159
|
+
|
|
160
|
+
## Method auto-detection
|
|
161
|
+
|
|
162
|
+
The `@operation` decorator infers the method from the function name:
|
|
163
|
+
|
|
164
|
+
| Function name prefix/keyword | Detected method |
|
|
165
|
+
|------------------------------|-----------------|
|
|
166
|
+
| `get_`, `list_`, `fetch_`, `read_` | `read` |
|
|
167
|
+
| `delete_`, `remove_`, `destroy_` | `delete` |
|
|
168
|
+
| contains `stream`, `subscribe`, `watch` | `stream` |
|
|
169
|
+
| anything else | `write` |
|
|
170
|
+
|
|
171
|
+
## Configuration
|
|
172
|
+
|
|
173
|
+
| Parameter | Description |
|
|
174
|
+
|-----------|-------------|
|
|
175
|
+
| `authorizer_url` | Base URL of Argos, including the API prefix (e.g. `http://host/api`) |
|
|
176
|
+
| `app_id` | UUID of the app registered in `auth.apps` |
|
|
177
|
+
| `secret` | Plain-text app secret (never committed — use env vars) |
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
import os
|
|
181
|
+
from argos.adapters.fastapi import Argos
|
|
182
|
+
|
|
183
|
+
argos = Argos(
|
|
184
|
+
app,
|
|
185
|
+
authorizer_url=os.environ["AUTHORIZER_URL"],
|
|
186
|
+
app_id=os.environ["APP_ID"],
|
|
187
|
+
secret=os.environ["APP_SECRET"],
|
|
188
|
+
)
|
|
189
|
+
```
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# argos
|
|
2
|
+
|
|
3
|
+
Python client for the **Argos** authorizer — handles app authentication,
|
|
4
|
+
JWT validation, permission checking and automatic operation sync.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install argos
|
|
10
|
+
# With FastAPI support
|
|
11
|
+
pip install "argos[fastapi]"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick start with FastAPI
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from fastapi import FastAPI, Request
|
|
18
|
+
from argos.adapters.fastapi import Argos, operation
|
|
19
|
+
|
|
20
|
+
app = FastAPI()
|
|
21
|
+
|
|
22
|
+
argos = Argos(
|
|
23
|
+
app,
|
|
24
|
+
authorizer_url="http://localhost:4000/api",
|
|
25
|
+
app_id="your-app-uuid",
|
|
26
|
+
secret="your-app-secret",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.get("/public")
|
|
31
|
+
@operation(id="public.info", level="public")
|
|
32
|
+
async def public_info():
|
|
33
|
+
"""Anyone can call this — no token needed."""
|
|
34
|
+
return {"message": "Hello, world!"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.get("/items")
|
|
38
|
+
@operation(id="items.list", level="private")
|
|
39
|
+
async def list_items(request: Request):
|
|
40
|
+
"""Requires any authenticated user (token with non-empty groups)."""
|
|
41
|
+
user = request.state.user # TokenClaims
|
|
42
|
+
return {"user": user.sub, "items": []}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.delete("/items/{item_id}")
|
|
46
|
+
@operation(id="items.delete", level="protected")
|
|
47
|
+
async def delete_item(item_id: str, request: Request):
|
|
48
|
+
"""Requires a user whose group has explicit permission for items.delete."""
|
|
49
|
+
return {"deleted": item_id}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Standalone client
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import asyncio
|
|
56
|
+
from argos import ArgosClient, OperationDescriptor
|
|
57
|
+
|
|
58
|
+
client = ArgosClient(
|
|
59
|
+
authorizer_url="http://localhost:4000/api",
|
|
60
|
+
app_id="your-app-uuid",
|
|
61
|
+
secret="your-app-secret",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
client.register_operation(
|
|
65
|
+
OperationDescriptor(identifier="items.list", method="read", level="private")
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
async def main():
|
|
69
|
+
await client.startup()
|
|
70
|
+
|
|
71
|
+
# Validate a token from an incoming request
|
|
72
|
+
claims = await client.validate_token("<bearer-token>")
|
|
73
|
+
|
|
74
|
+
# Check whether the token has permission
|
|
75
|
+
allowed = client.check_permission(claims, "items.list", "read")
|
|
76
|
+
print(f"Allowed: {allowed}")
|
|
77
|
+
|
|
78
|
+
asyncio.run(main())
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Operation levels
|
|
82
|
+
|
|
83
|
+
| Level | Who can access |
|
|
84
|
+
|-------------|----------------|
|
|
85
|
+
| `public` | Everyone — no token required |
|
|
86
|
+
| `private` | Any authenticated user (token with at least one group) |
|
|
87
|
+
| `protected` | Only users whose groups intersect the operation's `allowed_groups` |
|
|
88
|
+
|
|
89
|
+
## Method auto-detection
|
|
90
|
+
|
|
91
|
+
The `@operation` decorator infers the method from the function name:
|
|
92
|
+
|
|
93
|
+
| Function name prefix/keyword | Detected method |
|
|
94
|
+
|------------------------------|-----------------|
|
|
95
|
+
| `get_`, `list_`, `fetch_`, `read_` | `read` |
|
|
96
|
+
| `delete_`, `remove_`, `destroy_` | `delete` |
|
|
97
|
+
| contains `stream`, `subscribe`, `watch` | `stream` |
|
|
98
|
+
| anything else | `write` |
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
| Parameter | Description |
|
|
103
|
+
|-----------|-------------|
|
|
104
|
+
| `authorizer_url` | Base URL of Argos, including the API prefix (e.g. `http://host/api`) |
|
|
105
|
+
| `app_id` | UUID of the app registered in `auth.apps` |
|
|
106
|
+
| `secret` | Plain-text app secret (never committed — use env vars) |
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import os
|
|
110
|
+
from argos.adapters.fastapi import Argos
|
|
111
|
+
|
|
112
|
+
argos = Argos(
|
|
113
|
+
app,
|
|
114
|
+
authorizer_url=os.environ["AUTHORIZER_URL"],
|
|
115
|
+
app_id=os.environ["APP_ID"],
|
|
116
|
+
secret=os.environ["APP_SECRET"],
|
|
117
|
+
)
|
|
118
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""argos — Python client for the Argos authorizer."""
|
|
2
|
+
|
|
3
|
+
from .client import ArgosClient
|
|
4
|
+
from .decorators import operation
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
ArgosAuthError,
|
|
7
|
+
ArgosConfigError,
|
|
8
|
+
ArgosError,
|
|
9
|
+
ArgosForbiddenError,
|
|
10
|
+
ArgosSyncError,
|
|
11
|
+
ArgosTokenError,
|
|
12
|
+
)
|
|
13
|
+
from .models import OperationDescriptor, OperationWithGroups, TokenClaims
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ArgosClient",
|
|
17
|
+
"OperationDescriptor",
|
|
18
|
+
"OperationWithGroups",
|
|
19
|
+
"TokenClaims",
|
|
20
|
+
"ArgosError",
|
|
21
|
+
"ArgosAuthError",
|
|
22
|
+
"ArgosTokenError",
|
|
23
|
+
"ArgosForbiddenError",
|
|
24
|
+
"ArgosSyncError",
|
|
25
|
+
"ArgosConfigError",
|
|
26
|
+
"operation",
|
|
27
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# argos adapters
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Django adapter for argos."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
from ..client import ArgosClient
|
|
9
|
+
from ..decorators import get_registry, operation # re-export
|
|
10
|
+
from ..exceptions import ArgosTokenError
|
|
11
|
+
from ..models import TokenClaims
|
|
12
|
+
|
|
13
|
+
__all__ = ["ArgosMiddleware", "operation"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _extract_token(request: Any) -> str:
|
|
17
|
+
"""Extract Bearer token from HTTP_AUTHORIZATION."""
|
|
18
|
+
auth: str = request.META.get("HTTP_AUTHORIZATION", "")
|
|
19
|
+
scheme, _, token_str = auth.partition(" ")
|
|
20
|
+
return token_str.strip() if scheme.lower() == "bearer" else ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _operation_info(view_func: Any) -> tuple[str, str, str] | None:
|
|
24
|
+
"""Return ``(identifier, op_method, level)`` from view metadata, or ``None``."""
|
|
25
|
+
# Try the function itself, then __wrapped__ (functools.wraps)
|
|
26
|
+
for candidate in (view_func, getattr(view_func, "__wrapped__", None)):
|
|
27
|
+
if candidate is None:
|
|
28
|
+
continue
|
|
29
|
+
identifier = getattr(candidate, "__argos_id__", None)
|
|
30
|
+
op_method = getattr(candidate, "__argos_method__", None)
|
|
31
|
+
level = getattr(candidate, "__argos_level__", None)
|
|
32
|
+
if identifier and op_method and level:
|
|
33
|
+
return (identifier, op_method, level)
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ArgosMiddleware:
|
|
38
|
+
"""Django WSGI middleware for argos.
|
|
39
|
+
|
|
40
|
+
Uses Django's ``process_view`` hook — which runs *after* URL resolution
|
|
41
|
+
and receives the matched view function directly — so ``@operation``
|
|
42
|
+
metadata is always available regardless of middleware order.
|
|
43
|
+
|
|
44
|
+
Add to ``MIDDLEWARE`` in ``settings.py``::
|
|
45
|
+
|
|
46
|
+
MIDDLEWARE = [
|
|
47
|
+
...
|
|
48
|
+
"argos.adapters.django.ArgosMiddleware",
|
|
49
|
+
...
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
Required Django settings::
|
|
53
|
+
|
|
54
|
+
ARGOS_AUTHORIZER_URL = "http://localhost:4000/api"
|
|
55
|
+
ARGOS_APP_ID = "<uuid>"
|
|
56
|
+
ARGOS_SECRET = "<secret>"
|
|
57
|
+
|
|
58
|
+
Mark views with :func:`argos.decorators.operation`::
|
|
59
|
+
|
|
60
|
+
from django.http import JsonResponse
|
|
61
|
+
from argos.adapters.django import operation
|
|
62
|
+
|
|
63
|
+
@operation(id="items.list", level="private")
|
|
64
|
+
def list_items(request):
|
|
65
|
+
user = request.argos_user # TokenClaims
|
|
66
|
+
return JsonResponse({"user": user.sub})
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
get_response: The next middleware or view callable (injected by Django).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, get_response: Callable[[Any], Any]) -> None:
|
|
73
|
+
from django.conf import settings
|
|
74
|
+
|
|
75
|
+
authorizer_url: str = getattr(settings, "ARGOS_AUTHORIZER_URL", "")
|
|
76
|
+
realm_id: str = getattr(settings, "ARGOS_REALM_ID", "")
|
|
77
|
+
app_id: str = getattr(settings, "ARGOS_APP_ID", "")
|
|
78
|
+
secret: str = getattr(settings, "ARGOS_SECRET", "")
|
|
79
|
+
|
|
80
|
+
if not (authorizer_url and realm_id and app_id and secret):
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
"argos requires ARGOS_AUTHORIZER_URL, ARGOS_REALM_ID, "
|
|
83
|
+
"ARGOS_APP_ID and ARGOS_SECRET in Django settings."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
self.get_response = get_response
|
|
87
|
+
self.client = ArgosClient(authorizer_url, realm_id, app_id, secret)
|
|
88
|
+
|
|
89
|
+
for op in get_registry():
|
|
90
|
+
self.client.register_operation(op)
|
|
91
|
+
asyncio.run(self.client.startup())
|
|
92
|
+
|
|
93
|
+
def __call__(self, request: Any) -> Any:
|
|
94
|
+
"""Pass the request through — auth is enforced in process_view."""
|
|
95
|
+
return self.get_response(request)
|
|
96
|
+
|
|
97
|
+
def process_view(
|
|
98
|
+
self,
|
|
99
|
+
request: Any,
|
|
100
|
+
view_func: Any,
|
|
101
|
+
view_args: Any,
|
|
102
|
+
view_kwargs: Any,
|
|
103
|
+
) -> Any | None:
|
|
104
|
+
"""Enforce authentication and permissions before the view runs.
|
|
105
|
+
|
|
106
|
+
Called by Django after URL resolution. Returns ``None`` to let the
|
|
107
|
+
view proceed, or an :class:`~django.http.HttpResponse` to
|
|
108
|
+
short-circuit the request.
|
|
109
|
+
"""
|
|
110
|
+
from django.http import JsonResponse
|
|
111
|
+
|
|
112
|
+
op_info = _operation_info(view_func)
|
|
113
|
+
|
|
114
|
+
# No argos annotation — pass through
|
|
115
|
+
if op_info is None:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
identifier, op_method, level = op_info
|
|
119
|
+
|
|
120
|
+
# Public — no token required
|
|
121
|
+
if level == "public":
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
token_str = _extract_token(request)
|
|
125
|
+
|
|
126
|
+
if not token_str:
|
|
127
|
+
return JsonResponse(
|
|
128
|
+
{"error": "unauthorized", "detail": "Bearer token required."}, status=401
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Validate token (sync wrapper)
|
|
132
|
+
try:
|
|
133
|
+
claims: TokenClaims = asyncio.run(self.client.validate_token(token_str))
|
|
134
|
+
except ArgosTokenError as exc:
|
|
135
|
+
return JsonResponse({"error": "unauthorized", "detail": str(exc)}, status=401)
|
|
136
|
+
|
|
137
|
+
# Check permission
|
|
138
|
+
if not self.client.check_permission(claims, identifier, op_method):
|
|
139
|
+
return JsonResponse(
|
|
140
|
+
{"error": "forbidden", "detail": "Insufficient permissions."}, status=403
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Inject claims — available in views as request.argos_user
|
|
144
|
+
request.argos_user = claims
|
|
145
|
+
return None
|