kdex-entitlements 0.1.19__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.
@@ -0,0 +1,21 @@
1
+ VENV = .venv
2
+ VENV_BIN = $(VENV)/bin
3
+
4
+ .PHONY: all
5
+ all: test
6
+
7
+ .PHONY: test
8
+ test: $(VENV) ## Run Python tests.
9
+ $(VENV_BIN)/pytest
10
+
11
+ .PHONY: coverage
12
+ coverage: $(VENV) ## Run Python tests with coverage.
13
+ $(VENV_BIN)/pytest --cov=entitlements --cov-report=term-missing
14
+
15
+ .PHONY: lint
16
+ lint: $(VENV) ## Run Python linter (ruff).
17
+ $(VENV_BIN)/ruff check .
18
+
19
+ $(VENV):
20
+ python3 -m venv $(VENV)
21
+ $(VENV_BIN)/pip install pytest pytest-cov
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: kdex-entitlements
3
+ Version: 0.1.19
4
+ Summary: A simple entitlements checking library
5
+ Author: KDEX Tech
6
+ Provides-Extra: test
7
+ Requires-Dist: pytest>=8.0.0; extra == "test"
8
+ Requires-Dist: pytest-cov>=4.1.0; extra == "test"
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "kdex-entitlements"
3
+ dynamic = ["version"]
4
+ description = "A simple entitlements checking library"
5
+ authors = [
6
+ {name = "KDEX Tech"}
7
+ ]
8
+ dependencies = []
9
+
10
+ [build-system]
11
+ requires = ["setuptools>=61.0", "setuptools_scm[toml]>=6.2"]
12
+ build-backend = "setuptools.build_meta"
13
+
14
+ [tool.setuptools_scm]
15
+ fallback_version = "0.0.0"
16
+
17
+
18
+ [project.optional-dependencies]
19
+ test = [
20
+ "pytest>=8.0.0",
21
+ "pytest-cov>=4.1.0",
22
+ ]
23
+
24
+ [tool.pytest.ini_options]
25
+ testpaths = ["tests"]
26
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,115 @@
1
+ from typing import Dict, List, Optional, Union
2
+ import dataclasses
3
+
4
+ # Types
5
+ SecurityScheme = str
6
+ Entitlements = Dict[SecurityScheme, List[str]]
7
+ RequirementSet = Dict[SecurityScheme, List[str]]
8
+ Requirements = List[RequirementSet]
9
+
10
+ @dataclasses.dataclass(frozen=True)
11
+ class Pattern:
12
+ """Represents a parsed entitlement or requirement pattern."""
13
+ resource: Optional[str] = None
14
+ name: Optional[str] = None
15
+ verb: Optional[str] = None
16
+ opaque: Optional[str] = None
17
+
18
+ @classmethod
19
+ def parse(cls, s: str) -> "Pattern":
20
+ parts = s.split(":")
21
+ if len(parts) == 3:
22
+ return cls(resource=parts[0], name=parts[1], verb=parts[2])
23
+ elif len(parts) == 2:
24
+ return cls(resource=parts[0], name="*", verb=parts[1])
25
+ else:
26
+ return cls(opaque=s)
27
+
28
+ def satisfies(self, required: "Pattern") -> bool:
29
+ # Both opaque: must match exactly
30
+ if self.opaque is not None and required.opaque is not None:
31
+ return self.opaque == required.opaque
32
+
33
+ # Mixed: no match unless strings are identical (unlikely given parse)
34
+ if (self.opaque is not None) != (required.opaque is not None):
35
+ return False
36
+
37
+ # Structured:
38
+ # Resource must match
39
+ if self.resource != required.resource:
40
+ return False
41
+
42
+ # Verb must match exactly or entitlement is "all"
43
+ if self.verb != required.verb and self.verb != "all":
44
+ return False
45
+
46
+ # Name must match exactly, or either is a wildcard
47
+ if self.name != required.name and self.name not in ("*", "") and required.name not in ("*", ""):
48
+ return False
49
+
50
+ return True
51
+
52
+ class EntitlementsChecker:
53
+ def __init__(self, anonymous_entitlements: Optional[List[str]] = None, default_scheme: str = "bearer"):
54
+ self.anonymous_entitlements = anonymous_entitlements or []
55
+ self.default_scheme = default_scheme
56
+
57
+ def verify(self, user_entitlements: Entitlements, requirements: Requirements) -> bool:
58
+ if not requirements:
59
+ return True
60
+
61
+ # Merge with anonymous
62
+ merged = {k: list(v) for k, v in user_entitlements.items()}
63
+ if self.anonymous_entitlements:
64
+ scheme_list = merged.setdefault(self.default_scheme, [])
65
+ for anon in self.anonymous_entitlements:
66
+ if anon not in scheme_list:
67
+ scheme_list.append(anon)
68
+
69
+ # Pre-parse entitlements
70
+ parsed_entitlements: Dict[SecurityScheme, List[Pattern]] = {
71
+ scheme: [Pattern.parse(e) for e in entries]
72
+ for scheme, entries in merged.items()
73
+ }
74
+
75
+ # OR logic for requirements list
76
+ for req_set in requirements:
77
+ if self._verify_set(parsed_entitlements, req_set):
78
+ return True
79
+
80
+ return False
81
+
82
+ def _verify_set(self, user_patterns: Dict[SecurityScheme, List[Pattern]], req_set: RequirementSet) -> bool:
83
+ # AND logic for a requirement set
84
+ for scheme, required_patterns in req_set.items():
85
+ user_list = user_patterns.get(scheme)
86
+ if user_list is None:
87
+ return False
88
+
89
+ for req_str in required_patterns:
90
+ req_p = Pattern.parse(req_str)
91
+ if not any(user_p.satisfies(req_p) for user_p in user_list):
92
+ return False
93
+ return True
94
+
95
+ def verify_resource(
96
+ self,
97
+ user_entitlements: Entitlements,
98
+ resource: str,
99
+ name: str,
100
+ verb: str,
101
+ additional_requirements: Optional[Requirements] = None
102
+ ) -> bool:
103
+ identity_req = f"{resource}:{name}:{verb}"
104
+
105
+ if not additional_requirements:
106
+ return self.verify(user_entitlements, [{self.default_scheme: [identity_req]}])
107
+
108
+ # Identity AND (additional requirements OR...)
109
+ combined: Requirements = []
110
+ for req_set in additional_requirements:
111
+ new_set = dict(req_set)
112
+ new_set.setdefault(self.default_scheme, []).append(identity_req)
113
+ combined.append(new_set)
114
+
115
+ return self.verify(user_entitlements, combined)
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: kdex-entitlements
3
+ Version: 0.1.19
4
+ Summary: A simple entitlements checking library
5
+ Author: KDEX Tech
6
+ Provides-Extra: test
7
+ Requires-Dist: pytest>=8.0.0; extra == "test"
8
+ Requires-Dist: pytest-cov>=4.1.0; extra == "test"
@@ -0,0 +1,9 @@
1
+ Makefile
2
+ pyproject.toml
3
+ src/entitlements/__init__.py
4
+ src/kdex_entitlements.egg-info/PKG-INFO
5
+ src/kdex_entitlements.egg-info/SOURCES.txt
6
+ src/kdex_entitlements.egg-info/dependency_links.txt
7
+ src/kdex_entitlements.egg-info/requires.txt
8
+ src/kdex_entitlements.egg-info/top_level.txt
9
+ tests/test_entitlements.py
@@ -0,0 +1,4 @@
1
+
2
+ [test]
3
+ pytest>=8.0.0
4
+ pytest-cov>=4.1.0
@@ -0,0 +1,67 @@
1
+ import pytest
2
+ from entitlements import EntitlementsChecker, Pattern
3
+
4
+ def test_pattern_parse():
5
+ assert Pattern.parse("pages:/foo:read") == Pattern(resource="pages", name="/foo", verb="read")
6
+ assert Pattern.parse("pages:read") == Pattern(resource="pages", name="*", verb="read")
7
+ assert Pattern.parse("admin") == Pattern(opaque="admin")
8
+
9
+ def test_pattern_satisfies():
10
+ ent = Pattern.parse("pages:*:read")
11
+ req = Pattern.parse("pages:/foo:read")
12
+ assert ent.satisfies(req)
13
+
14
+ ent = Pattern.parse("pages:all")
15
+ req = Pattern.parse("pages:/foo:read")
16
+ assert ent.satisfies(req)
17
+
18
+ ent = Pattern.parse("pages:/bar:read")
19
+ req = Pattern.parse("pages:/foo:read")
20
+ assert not ent.satisfies(req)
21
+
22
+ ent = Pattern.parse("admin")
23
+ req = Pattern.parse("admin")
24
+ assert ent.satisfies(req)
25
+
26
+ def test_verify():
27
+ checker = EntitlementsChecker(anonymous_entitlements=["anonymous:read"], default_scheme="bearer")
28
+
29
+ user_entitlements = {"bearer": ["pages:foo:read"]}
30
+
31
+ # Simple match
32
+ requirements = [{"bearer": ["pages:foo:read"]}]
33
+ assert checker.verify(user_entitlements, requirements)
34
+
35
+ # Wildcard match
36
+ requirements = [{"bearer": ["pages:*:read"]}]
37
+ assert checker.verify(user_entitlements, requirements)
38
+
39
+ # Anonymous match
40
+ requirements = [{"bearer": ["anonymous:*:read"]}]
41
+ assert checker.verify(user_entitlements, requirements)
42
+
43
+ # Failing match
44
+ requirements = [{"bearer": ["pages:foo:write"]}]
45
+ assert not checker.verify(user_entitlements, requirements)
46
+
47
+ # OR match
48
+ requirements = [
49
+ {"bearer": ["pages:foo:write"]},
50
+ {"bearer": ["pages:foo:read"]}
51
+ ]
52
+ assert checker.verify(user_entitlements, requirements)
53
+
54
+ def test_verify_resource():
55
+ checker = EntitlementsChecker(default_scheme="bearer")
56
+ user_entitlements = {"bearer": ["pages:foo:read", "admin"]}
57
+
58
+ # Identity check
59
+ assert checker.verify_resource(user_entitlements, "pages", "foo", "read")
60
+ assert not checker.verify_resource(user_entitlements, "pages", "bar", "read")
61
+
62
+ # Identity + additional requirements
63
+ additional = [{"bearer": ["admin"]}]
64
+ assert checker.verify_resource(user_entitlements, "pages", "foo", "read", additional)
65
+
66
+ additional = [{"bearer": ["superadmin"]}]
67
+ assert not checker.verify_resource(user_entitlements, "pages", "foo", "read", additional)