rbacx 0.1.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.
Files changed (73) hide show
  1. rbacx-0.1.0/LICENSE +22 -0
  2. rbacx-0.1.0/MANIFEST.in +14 -0
  3. rbacx-0.1.0/PKG-INFO +179 -0
  4. rbacx-0.1.0/README.md +93 -0
  5. rbacx-0.1.0/pyproject.toml +62 -0
  6. rbacx-0.1.0/setup.cfg +4 -0
  7. rbacx-0.1.0/src/rbacx/__init__.py +20 -0
  8. rbacx-0.1.0/src/rbacx/__pycache__/__init__.cpython-312.pyc +0 -0
  9. rbacx-0.1.0/src/rbacx/adapters/asgi.py +25 -0
  10. rbacx-0.1.0/src/rbacx/adapters/asgi_accesslog.py +36 -0
  11. rbacx-0.1.0/src/rbacx/adapters/asgi_logging.py +46 -0
  12. rbacx-0.1.0/src/rbacx/adapters/django/decorators.py +25 -0
  13. rbacx-0.1.0/src/rbacx/adapters/django/middleware.py +38 -0
  14. rbacx-0.1.0/src/rbacx/adapters/django/trace.py +25 -0
  15. rbacx-0.1.0/src/rbacx/adapters/drf.py +20 -0
  16. rbacx-0.1.0/src/rbacx/adapters/fastapi.py +55 -0
  17. rbacx-0.1.0/src/rbacx/adapters/fastapi_guard.py +19 -0
  18. rbacx-0.1.0/src/rbacx/adapters/flask.py +49 -0
  19. rbacx-0.1.0/src/rbacx/adapters/flask_guard.py +26 -0
  20. rbacx-0.1.0/src/rbacx/adapters/litestar.py +35 -0
  21. rbacx-0.1.0/src/rbacx/adapters/litestar_guard.py +36 -0
  22. rbacx-0.1.0/src/rbacx/adapters/starlette.py +53 -0
  23. rbacx-0.1.0/src/rbacx/cli.py +56 -0
  24. rbacx-0.1.0/src/rbacx/core/__pycache__/compiler.cpython-312.pyc +0 -0
  25. rbacx-0.1.0/src/rbacx/core/__pycache__/decision.cpython-312.pyc +0 -0
  26. rbacx-0.1.0/src/rbacx/core/__pycache__/engine.cpython-312.pyc +0 -0
  27. rbacx-0.1.0/src/rbacx/core/__pycache__/interpreter.cpython-312.pyc +0 -0
  28. rbacx-0.1.0/src/rbacx/core/__pycache__/model.cpython-312.pyc +0 -0
  29. rbacx-0.1.0/src/rbacx/core/__pycache__/obligations.cpython-312.pyc +0 -0
  30. rbacx-0.1.0/src/rbacx/core/__pycache__/policy.cpython-312.pyc +0 -0
  31. rbacx-0.1.0/src/rbacx/core/__pycache__/policyset.cpython-312.pyc +0 -0
  32. rbacx-0.1.0/src/rbacx/core/__pycache__/ports.cpython-312.pyc +0 -0
  33. rbacx-0.1.0/src/rbacx/core/__pycache__/roles.cpython-312.pyc +0 -0
  34. rbacx-0.1.0/src/rbacx/core/compiler.py +143 -0
  35. rbacx-0.1.0/src/rbacx/core/decision.py +16 -0
  36. rbacx-0.1.0/src/rbacx/core/engine.py +212 -0
  37. rbacx-0.1.0/src/rbacx/core/model.py +26 -0
  38. rbacx-0.1.0/src/rbacx/core/obligations.py +18 -0
  39. rbacx-0.1.0/src/rbacx/core/policy.py +367 -0
  40. rbacx-0.1.0/src/rbacx/core/policyset.py +119 -0
  41. rbacx-0.1.0/src/rbacx/core/ports.py +27 -0
  42. rbacx-0.1.0/src/rbacx/core/roles.py +28 -0
  43. rbacx-0.1.0/src/rbacx/dsl/__pycache__/lint.cpython-312.pyc +0 -0
  44. rbacx-0.1.0/src/rbacx/dsl/lint.py +201 -0
  45. rbacx-0.1.0/src/rbacx/dsl/policy.schema.json +388 -0
  46. rbacx-0.1.0/src/rbacx/dsl/validate.py +16 -0
  47. rbacx-0.1.0/src/rbacx/logging/context.py +29 -0
  48. rbacx-0.1.0/src/rbacx/logging/decision_logger.py +30 -0
  49. rbacx-0.1.0/src/rbacx/metrics/__pycache__/otel.cpython-312.pyc +0 -0
  50. rbacx-0.1.0/src/rbacx/metrics/__pycache__/prometheus.cpython-312.pyc +0 -0
  51. rbacx-0.1.0/src/rbacx/metrics/otel.py +46 -0
  52. rbacx-0.1.0/src/rbacx/metrics/prometheus.py +37 -0
  53. rbacx-0.1.0/src/rbacx/obligations/enforcer.py +49 -0
  54. rbacx-0.1.0/src/rbacx/policy/__pycache__/loader.cpython-312.pyc +0 -0
  55. rbacx-0.1.0/src/rbacx/policy/loader.py +53 -0
  56. rbacx-0.1.0/src/rbacx/py.typed +0 -0
  57. rbacx-0.1.0/src/rbacx/storage/__init__.py +122 -0
  58. rbacx-0.1.0/src/rbacx/storage/__pycache__/__init__.cpython-312.pyc +0 -0
  59. rbacx-0.1.0/src/rbacx/storage/s3.py +91 -0
  60. rbacx-0.1.0/src/rbacx/store/__pycache__/file_store.cpython-312.pyc +0 -0
  61. rbacx-0.1.0/src/rbacx/store/__pycache__/manager.cpython-312.pyc +0 -0
  62. rbacx-0.1.0/src/rbacx/store/file_store.py +23 -0
  63. rbacx-0.1.0/src/rbacx/store/http_store.py +33 -0
  64. rbacx-0.1.0/src/rbacx/store/manager.py +64 -0
  65. rbacx-0.1.0/src/rbacx/store/s3_store.py +30 -0
  66. rbacx-0.1.0/src/rbacx/telemetry/decision_log.py +18 -0
  67. rbacx-0.1.0/src/rbacx/telemetry/metrics_prom.py +22 -0
  68. rbacx-0.1.0/src/rbacx.egg-info/PKG-INFO +179 -0
  69. rbacx-0.1.0/src/rbacx.egg-info/SOURCES.txt +71 -0
  70. rbacx-0.1.0/src/rbacx.egg-info/dependency_links.txt +1 -0
  71. rbacx-0.1.0/src/rbacx.egg-info/entry_points.txt +2 -0
  72. rbacx-0.1.0/src/rbacx.egg-info/requires.txt +58 -0
  73. rbacx-0.1.0/src/rbacx.egg-info/top_level.txt +1 -0
rbacx-0.1.0/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+
2
+ MIT License
3
+
4
+ Copyright (c) 2025 RBACX (Cheater121)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,14 @@
1
+
2
+ include LICENSE
3
+ include README.md
4
+ include pyproject.toml
5
+
6
+ recursive-include src *
7
+
8
+ prune .github
9
+ prune docs
10
+ prune tests
11
+ prune examples
12
+ prune site
13
+ prune build
14
+ prune dist
rbacx-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: rbacx
3
+ Version: 0.1.0
4
+ Summary: RBAC/ABAC policy engine for Python with policy sets, condition DSL, and hot reload
5
+ Author-email: Cheater121 <cheater1211@gmail.com>
6
+ License:
7
+ MIT License
8
+
9
+ Copyright (c) 2025 RBACX (Cheater121)
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Project-URL: Homepage, https://github.com/Cheater121/rbacx
30
+ Project-URL: Repository, https://github.com/Cheater121/rbacx
31
+ Project-URL: Issues, https://github.com/Cheater121/rbacx/issues
32
+ Project-URL: Changelog, https://github.com/Cheater121/rbacx/blob/main/CHANGELOG.md
33
+ Keywords: rbac,abac,policy,authorization
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Typing :: Typed
37
+ Requires-Python: >=3.8
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Provides-Extra: adapters-fastapi
41
+ Requires-Dist: fastapi>=0.110; extra == "adapters-fastapi"
42
+ Requires-Dist: starlette>=0.37; extra == "adapters-fastapi"
43
+ Provides-Extra: adapters-flask
44
+ Requires-Dist: flask>=2.3; extra == "adapters-flask"
45
+ Provides-Extra: adapters-drf
46
+ Requires-Dist: django>=4.2; extra == "adapters-drf"
47
+ Requires-Dist: djangorestframework>=3.14; extra == "adapters-drf"
48
+ Provides-Extra: metrics
49
+ Requires-Dist: prometheus-client>=0.14; extra == "metrics"
50
+ Requires-Dist: opentelemetry-api>=1.25; extra == "metrics"
51
+ Provides-Extra: dates
52
+ Requires-Dist: python-dateutil>=2.8; extra == "dates"
53
+ Provides-Extra: http
54
+ Requires-Dist: requests>=2.28; extra == "http"
55
+ Provides-Extra: s3
56
+ Requires-Dist: boto3>=1.26; extra == "s3"
57
+ Provides-Extra: otel
58
+ Requires-Dist: opentelemetry-api>=1.19; extra == "otel"
59
+ Requires-Dist: opentelemetry-sdk>=1.19; extra == "otel"
60
+ Provides-Extra: dev
61
+ Requires-Dist: pytest; extra == "dev"
62
+ Requires-Dist: pytest-cov; extra == "dev"
63
+ Requires-Dist: pre-commit>=3.7; extra == "dev"
64
+ Requires-Dist: build>=1.2; extra == "dev"
65
+ Requires-Dist: ruff>=0.6; extra == "dev"
66
+ Requires-Dist: twine; extra == "dev"
67
+ Requires-Dist: mypy>=1.8; extra == "dev"
68
+ Provides-Extra: tests
69
+ Requires-Dist: pytest>=8; extra == "tests"
70
+ Requires-Dist: pytest-asyncio>=0.23; extra == "tests"
71
+ Requires-Dist: coverage>=7; extra == "tests"
72
+ Provides-Extra: docs
73
+ Requires-Dist: mkdocs>=1.6; extra == "docs"
74
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
75
+ Provides-Extra: examples
76
+ Requires-Dist: uvicorn>=0.30; extra == "examples"
77
+ Requires-Dist: fastapi>=0.110; extra == "examples"
78
+ Requires-Dist: starlette>=0.37; extra == "examples"
79
+ Requires-Dist: flask>=2.3; extra == "examples"
80
+ Requires-Dist: django>=4.2; extra == "examples"
81
+ Requires-Dist: djangorestframework>=3.14; extra == "examples"
82
+ Requires-Dist: litestar>=2.8; extra == "examples"
83
+ Provides-Extra: validate
84
+ Requires-Dist: jsonschema>=4.18; extra == "validate"
85
+ Dynamic: license-file
86
+
87
+ # RBACX
88
+
89
+ <!--
90
+ [![CI](https://github.com/Cheater121/rbacx/actions/workflows/ci.yml/badge.svg)](https://github.com/Cheater121/rbacx/actions/workflows/ci.yml)
91
+ [![Docs](https://img.shields.io/badge/docs-website-blue)](https://cheater121.github.io/rbacx/)
92
+ -->
93
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
94
+ <!-- After publishing to PyPI:
95
+ [![PyPI](https://img.shields.io/pypi/v/rbacx)](https://pypi.org/project/rbacx/)
96
+ [![Python](https://img.shields.io/pypi/pyversions/rbacx)](https://pypi.org/project/rbacx/)
97
+ -->
98
+
99
+ Universal **RBAC/ABAC** policy engine for Python with a clean core, policy sets, a compact condition DSL (including time ops), and adapters for common web frameworks.
100
+
101
+ ## Features
102
+ - Algorithms: `deny-overrides` (default), `permit-overrides`, `first-applicable`
103
+ - Conditions: `==`, `!=`, `<`, `<=`, `>`, `>=`, `contains`, `in`, `hasAll`, `hasAny`, `startsWith`, `endsWith`, `before`, `after`, `between`
104
+ - Explainability: `decision`, `reason`, `rule_id`/`last_rule_id`, `obligations`
105
+ - Policy sets: combine multiple policies with the same algorithms
106
+ - Hot reload: file/HTTP/S3 sources with ETag and a polling manager
107
+ - Types & lint: mypy-friendly core, Ruff-ready
108
+
109
+ ## Installation
110
+ ```bash
111
+ pip install rbacx
112
+ ```
113
+
114
+ ## Quickstart
115
+ ```python
116
+ from rbacx.core.engine import Guard
117
+ from rbacx.core.model import Subject, Action, Resource, Context
118
+
119
+ policy = {
120
+ "algorithm": "deny-overrides",
121
+ "rules": [
122
+ {
123
+ "id": "doc_read",
124
+ "effect": "permit",
125
+ "actions": ["read"],
126
+ "resource": {"type": "doc", "attrs": {"visibility": ["public", "internal"]}},
127
+ "condition": {"hasAny": [ {"attr": "subject.roles"}, ["reader", "admin"] ]},
128
+ "obligations": [{"mfa": True}]
129
+ },
130
+ {"id": "doc_deny_archived", "effect": "deny", "actions": ["*"], "resource": {"type": "doc", "attrs": {"archived": True}}},
131
+ ],
132
+ }
133
+
134
+ g = Guard(policy)
135
+
136
+ d = g.evaluate_sync(
137
+ subject=Subject(id="u1", roles=["reader"]),
138
+ action=Action("read"),
139
+ resource=Resource(type="doc", id="42", attrs={"visibility": "public"}),
140
+ context=Context(attrs={"mfa": True}),
141
+ )
142
+
143
+ assert d.allowed is True
144
+ assert d.effect == "permit"
145
+ print(d.reason, d.rule_id) # "matched", "doc_read"
146
+ ```
147
+
148
+ ### Decision schema
149
+ - `decision`: `"permit"` or `"deny"`
150
+ - `reason`: `"matched"`, `"explicit_deny"`, `"action_mismatch"`, `"resource_mismatch"`, `"condition_mismatch"`, `"condition_type_mismatch"`, `"no_match"`
151
+ - `rule_id` / `last_rule_id`
152
+ - `obligations`: list passed to the obligation checker
153
+
154
+ ### Policy sets
155
+ ```python
156
+ from rbacx.core.policyset import decide as decide_policyset
157
+
158
+ policyset = {"algorithm":"deny-overrides", "policies":[ policy, {"rules":[...]} ]}
159
+ result = decide_policyset(policyset, {"subject":..., "action":"read", "resource":...})
160
+ ```
161
+
162
+ ## Hot reloading
163
+ ```python
164
+ from rbacx.core.engine import Guard
165
+ from rbacx.storage import FilePolicySource # from rbacx.storage import HotReloader if you prefer
166
+ from rbacx.store.manager import PolicyManager
167
+
168
+ guard = Guard(policy={})
169
+ mgr = PolicyManager(guard, FilePolicySource("policy.json"))
170
+ mgr.poll_once() # initial load
171
+ mgr.start_polling(10) # background polling thread
172
+ ```
173
+
174
+ ## Packaging
175
+ - We ship `py.typed` so type checkers pick up annotations.
176
+ - Standard PyPA flow: `python -m build`, then `twine upload` to (Test)PyPI.
177
+
178
+ ## License
179
+ MIT
rbacx-0.1.0/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # RBACX
2
+
3
+ <!--
4
+ [![CI](https://github.com/Cheater121/rbacx/actions/workflows/ci.yml/badge.svg)](https://github.com/Cheater121/rbacx/actions/workflows/ci.yml)
5
+ [![Docs](https://img.shields.io/badge/docs-website-blue)](https://cheater121.github.io/rbacx/)
6
+ -->
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
8
+ <!-- After publishing to PyPI:
9
+ [![PyPI](https://img.shields.io/pypi/v/rbacx)](https://pypi.org/project/rbacx/)
10
+ [![Python](https://img.shields.io/pypi/pyversions/rbacx)](https://pypi.org/project/rbacx/)
11
+ -->
12
+
13
+ Universal **RBAC/ABAC** policy engine for Python with a clean core, policy sets, a compact condition DSL (including time ops), and adapters for common web frameworks.
14
+
15
+ ## Features
16
+ - Algorithms: `deny-overrides` (default), `permit-overrides`, `first-applicable`
17
+ - Conditions: `==`, `!=`, `<`, `<=`, `>`, `>=`, `contains`, `in`, `hasAll`, `hasAny`, `startsWith`, `endsWith`, `before`, `after`, `between`
18
+ - Explainability: `decision`, `reason`, `rule_id`/`last_rule_id`, `obligations`
19
+ - Policy sets: combine multiple policies with the same algorithms
20
+ - Hot reload: file/HTTP/S3 sources with ETag and a polling manager
21
+ - Types & lint: mypy-friendly core, Ruff-ready
22
+
23
+ ## Installation
24
+ ```bash
25
+ pip install rbacx
26
+ ```
27
+
28
+ ## Quickstart
29
+ ```python
30
+ from rbacx.core.engine import Guard
31
+ from rbacx.core.model import Subject, Action, Resource, Context
32
+
33
+ policy = {
34
+ "algorithm": "deny-overrides",
35
+ "rules": [
36
+ {
37
+ "id": "doc_read",
38
+ "effect": "permit",
39
+ "actions": ["read"],
40
+ "resource": {"type": "doc", "attrs": {"visibility": ["public", "internal"]}},
41
+ "condition": {"hasAny": [ {"attr": "subject.roles"}, ["reader", "admin"] ]},
42
+ "obligations": [{"mfa": True}]
43
+ },
44
+ {"id": "doc_deny_archived", "effect": "deny", "actions": ["*"], "resource": {"type": "doc", "attrs": {"archived": True}}},
45
+ ],
46
+ }
47
+
48
+ g = Guard(policy)
49
+
50
+ d = g.evaluate_sync(
51
+ subject=Subject(id="u1", roles=["reader"]),
52
+ action=Action("read"),
53
+ resource=Resource(type="doc", id="42", attrs={"visibility": "public"}),
54
+ context=Context(attrs={"mfa": True}),
55
+ )
56
+
57
+ assert d.allowed is True
58
+ assert d.effect == "permit"
59
+ print(d.reason, d.rule_id) # "matched", "doc_read"
60
+ ```
61
+
62
+ ### Decision schema
63
+ - `decision`: `"permit"` or `"deny"`
64
+ - `reason`: `"matched"`, `"explicit_deny"`, `"action_mismatch"`, `"resource_mismatch"`, `"condition_mismatch"`, `"condition_type_mismatch"`, `"no_match"`
65
+ - `rule_id` / `last_rule_id`
66
+ - `obligations`: list passed to the obligation checker
67
+
68
+ ### Policy sets
69
+ ```python
70
+ from rbacx.core.policyset import decide as decide_policyset
71
+
72
+ policyset = {"algorithm":"deny-overrides", "policies":[ policy, {"rules":[...]} ]}
73
+ result = decide_policyset(policyset, {"subject":..., "action":"read", "resource":...})
74
+ ```
75
+
76
+ ## Hot reloading
77
+ ```python
78
+ from rbacx.core.engine import Guard
79
+ from rbacx.storage import FilePolicySource # from rbacx.storage import HotReloader if you prefer
80
+ from rbacx.store.manager import PolicyManager
81
+
82
+ guard = Guard(policy={})
83
+ mgr = PolicyManager(guard, FilePolicySource("policy.json"))
84
+ mgr.poll_once() # initial load
85
+ mgr.start_polling(10) # background polling thread
86
+ ```
87
+
88
+ ## Packaging
89
+ - We ship `py.typed` so type checkers pick up annotations.
90
+ - Standard PyPA flow: `python -m build`, then `twine upload` to (Test)PyPI.
91
+
92
+ ## License
93
+ MIT
@@ -0,0 +1,62 @@
1
+ [project]
2
+ name = "rbacx"
3
+ version = "0.1.0"
4
+ description = "RBAC/ABAC policy engine for Python with policy sets, condition DSL, and hot reload"
5
+ readme = "README.md"
6
+ requires-python = ">=3.8"
7
+ license = { file = "LICENSE" }
8
+ authors = [{ name = "Cheater121", email = "cheater1211@gmail.com" }]
9
+ keywords = ["rbac", "abac", "policy", "authorization"]
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Typing :: Typed",
14
+ ]
15
+
16
+ [project.urls]
17
+ Homepage = "https://github.com/Cheater121/rbacx"
18
+ Repository = "https://github.com/Cheater121/rbacx"
19
+ Issues = "https://github.com/Cheater121/rbacx/issues"
20
+ Changelog = "https://github.com/Cheater121/rbacx/blob/main/CHANGELOG.md"
21
+ # Documentation = "https://cheater121.github.io/rbacx/"
22
+
23
+ [project.optional-dependencies]
24
+ adapters-fastapi = ["fastapi>=0.110","starlette>=0.37"]
25
+ adapters-flask = ["flask>=2.3"]
26
+ adapters-drf = ["django>=4.2","djangorestframework>=3.14"]
27
+ metrics = ["prometheus-client>=0.14", "opentelemetry-api>=1.25"]
28
+ dates = ["python-dateutil>=2.8"]
29
+ http = ["requests>=2.28"]
30
+ s3 = ["boto3>=1.26"]
31
+ otel = ["opentelemetry-api>=1.19", "opentelemetry-sdk>=1.19"]
32
+ dev = ["pytest", "pytest-cov", "pre-commit>=3.7", "build>=1.2", "ruff>=0.6", "twine", "mypy>=1.8"]
33
+ tests = ["pytest>=8", "pytest-asyncio>=0.23", "coverage>=7"]
34
+ docs = ["mkdocs>=1.6", "mkdocs-material>=9.5"]
35
+ examples = ["uvicorn>=0.30", "fastapi>=0.110", "starlette>=0.37", "flask>=2.3", "django>=4.2", "djangorestframework>=3.14", "litestar>=2.8"]
36
+ validate = ["jsonschema>=4.18"]
37
+
38
+ [project.scripts]
39
+ rbacx = "rbacx.cli:main"
40
+
41
+ [tool.ruff]
42
+ line-length = 100
43
+
44
+ [tool.ruff.lint]
45
+ extend-select = ["I", "B"]
46
+
47
+ [tool.coverage.run]
48
+ source = ["rbacx"]
49
+ branch = true
50
+
51
+ [tool.coverage.report]
52
+ show_missing = true
53
+
54
+ [tool.setuptools]
55
+ package-dir = {"" = "src"}
56
+ include-package-data = true
57
+
58
+ [tool.setuptools.packages.find]
59
+ where = ["src"]
60
+
61
+ [tool.setuptools.package-data]
62
+ rbacx = ["py.typed", "dsl/policy.schema.json"]
rbacx-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,20 @@
1
+
2
+ from __future__ import annotations
3
+
4
+ try:
5
+ from importlib.metadata import PackageNotFoundError, version # py3.8+
6
+ except Exception: # pragma: no cover
7
+ version = None # type: ignore
8
+ PackageNotFoundError = Exception # type: ignore
9
+
10
+ __all__ = ["core", "adapters", "storage", "obligations", "__version__"]
11
+
12
+ def _detect_version() -> str:
13
+ try:
14
+ if version is None:
15
+ raise PackageNotFoundError # type: ignore
16
+ return version("rbacx")
17
+ except Exception:
18
+ return "0.1.0"
19
+
20
+ __version__ = _detect_version()
@@ -0,0 +1,25 @@
1
+
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from typing import Any
6
+
7
+ from ..core.engine import Guard
8
+
9
+ logger = logging.getLogger("rbacx.adapters.asgi")
10
+
11
+ class RbacxMiddleware:
12
+ def __init__(self, app: Any, *, guard: Guard, mode: str = "enforce", policy_reloader: Any | None = None) -> None:
13
+ self.app = app
14
+ self.guard = guard
15
+ self.mode = mode
16
+ self.reloader = policy_reloader
17
+
18
+ async def __call__(self, scope, receive, send):
19
+ if self.reloader:
20
+ try:
21
+ self.reloader.check_and_reload()
22
+ except Exception as e:
23
+ logger.exception("RBACX: policy reload failed", exc_info=e)
24
+ scope["rbacx_guard"] = self.guard
25
+ await self.app(scope, receive, send)
@@ -0,0 +1,36 @@
1
+
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import time
6
+ from typing import Any
7
+
8
+ from ..logging.context import get_current_trace_id
9
+
10
+ logger = logging.getLogger("rbacx.adapters.asgi.access")
11
+
12
+ class AccessLogMiddleware:
13
+ def __init__(self, app: Any) -> None:
14
+ self.app = app
15
+
16
+ async def __call__(self, scope, receive, send):
17
+ if scope.get("type") != "http":
18
+ await self.app(scope, receive, send)
19
+ return
20
+ method = scope.get("method")
21
+ path = (scope.get("path") or "") + (("?" + scope.get("query_string", b"").decode("latin1")) if scope.get("query_string") else "")
22
+ start = time.time()
23
+ status = 0
24
+
25
+ async def send_wrapper(message):
26
+ nonlocal status
27
+ if message.get("type") == "http.response.start":
28
+ status = int(message.get("status", 0))
29
+ await send(message)
30
+
31
+ try:
32
+ await self.app(scope, receive, send_wrapper)
33
+ finally:
34
+ dur_ms = int((time.time() - start) * 1000)
35
+ rid = get_current_trace_id() or "-"
36
+ logger.info("access %s %s %s %sms trace_id=%s", method, path, status, dur_ms, rid)
@@ -0,0 +1,46 @@
1
+
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from typing import Any, Iterable, Tuple
6
+
7
+ from ..logging.context import clear_current_trace_id, gen_trace_id, set_current_trace_id
8
+
9
+ logger = logging.getLogger("rbacx.adapters.asgi")
10
+
11
+ class TraceIdMiddleware:
12
+ def __init__(self, app: Any, header_name: bytes = b"x-request-id") -> None:
13
+ self.app = app
14
+ self.header_name = header_name
15
+
16
+ async def __call__(self, scope, receive, send):
17
+ if scope.get("type") != "http":
18
+ await self.app(scope, receive, send)
19
+ return
20
+
21
+ req_headers: Iterable[Tuple[bytes, bytes]] = scope.get("headers", []) or []
22
+ rid = None
23
+ for k, v in req_headers:
24
+ if k.lower() == self.header_name:
25
+ rid = v.decode("latin1")
26
+ break
27
+ if not rid:
28
+ rid = gen_trace_id()
29
+
30
+ token = set_current_trace_id(rid)
31
+
32
+ async def send_wrapper(message):
33
+ if message.get("type") == "http.response.start":
34
+ headers = message.setdefault("headers", [])
35
+ try:
36
+ # remove any existing header to avoid duplicates
37
+ headers[:] = [h for h in headers if h[0].lower() != self.header_name]
38
+ headers.append([self.header_name, rid.encode("latin1")])
39
+ except Exception:
40
+ pass
41
+ await send(message)
42
+
43
+ try:
44
+ await self.app(scope, receive, send_wrapper)
45
+ finally:
46
+ clear_current_trace_id(token)
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import wraps
4
+
5
+ from django.http import HttpRequest, HttpResponseForbidden # type: ignore[import-untyped]
6
+
7
+ from ...core.model import Action, Context, Resource, Subject
8
+
9
+
10
+ def require(action: str, resource_type: str, *, audit: bool = False):
11
+ def deco(view_func):
12
+ @wraps(view_func)
13
+ def _wrapped(request: HttpRequest, *args, **kwargs):
14
+ guard = getattr(request, "rbacx_guard", None)
15
+ if guard is None:
16
+ return view_func(request, *args, **kwargs)
17
+ sub = Subject(id=str(getattr(getattr(request, 'user', None), 'id', 'anonymous')))
18
+ res = Resource(type=resource_type)
19
+ ctx = Context(attrs={})
20
+ ok = guard.is_allowed_sync(sub, Action(action), res, ctx)
21
+ if not ok and not audit:
22
+ return HttpResponseForbidden("Forbidden")
23
+ return view_func(request, *args, **kwargs)
24
+ return _wrapped
25
+ return deco
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from importlib import import_module
5
+ from typing import Any, Callable
6
+
7
+ from django.conf import settings # type: ignore[import-untyped]
8
+
9
+ logger = logging.getLogger("rbacx.adapters.django")
10
+
11
+
12
+ def _load_dotted(path: str) -> Callable[[], Any]:
13
+ mod, _, attr = path.rpartition(".")
14
+ if not mod:
15
+ raise ImportError(f"Invalid dotted path: {path}")
16
+ m = import_module(mod)
17
+ obj = getattr(m, attr, None)
18
+ if obj is None:
19
+ raise ImportError(f"Attribute '{attr}' not found in module '{mod}'")
20
+ if not callable(obj):
21
+ raise TypeError(f"Object at '{path}' is not callable")
22
+ return obj
23
+
24
+
25
+ class RbacxDjangoMiddleware:
26
+ def __init__(self, get_response: Callable) -> None:
27
+ self.get_response = get_response
28
+ self._guard: Any | None = None
29
+ factory_path = getattr(settings, "RBACX_GUARD_FACTORY", None)
30
+ if factory_path:
31
+ factory = _load_dotted(factory_path)
32
+ self._guard = factory()
33
+
34
+ def __call__(self, request):
35
+ if self._guard is not None:
36
+ request.rbacx_guard = self._guard
37
+ response = self.get_response(request)
38
+ return response
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
4
+
5
+ from ...logging.context import (
6
+ clear_current_trace_id,
7
+ gen_trace_id,
8
+ get_current_trace_id,
9
+ set_current_trace_id,
10
+ )
11
+
12
+
13
+ class TraceIdMiddleware:
14
+ def __init__(self, get_response: Callable) -> None:
15
+ self.get_response = get_response
16
+
17
+ def __call__(self, request):
18
+ rid = request.META.get("HTTP_X_REQUEST_ID") or gen_trace_id()
19
+ token = set_current_trace_id(rid)
20
+ response = self.get_response(request)
21
+ try:
22
+ response["X-Request-ID"] = get_current_trace_id() or rid
23
+ finally:
24
+ clear_current_trace_id(token)
25
+ return response
@@ -0,0 +1,20 @@
1
+
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Callable
5
+
6
+ from rest_framework.permissions import BasePermission # type: ignore
7
+
8
+ from ..core.engine import Guard
9
+ from ..core.model import Action, Context, Resource, Subject
10
+
11
+
12
+ def make_permission(guard: Guard, build_env: Callable[[Any], tuple[Subject, Action, Resource, Context]]):
13
+ class RBACXPermission(BasePermission):
14
+ message = "forbidden"
15
+ def has_permission(self, request, view):
16
+ subject, action, resource, context = build_env(request)
17
+ dec = guard.evaluate_sync(subject, action, resource, context)
18
+ self.message = f"forbidden: {dec.reason}"
19
+ return bool(dec.allowed)
20
+ return RBACXPermission