fastapi-rbac-lite 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.
- fastapi_rbac_lite-0.1.0/PKG-INFO +32 -0
- fastapi_rbac_lite-0.1.0/README.md +0 -0
- fastapi_rbac_lite-0.1.0/pyproject.toml +54 -0
- fastapi_rbac_lite-0.1.0/setup.cfg +4 -0
- fastapi_rbac_lite-0.1.0/src/fastapi_rbac_lite/__init__.py +22 -0
- fastapi_rbac_lite-0.1.0/src/fastapi_rbac_lite/auth.py +57 -0
- fastapi_rbac_lite-0.1.0/src/fastapi_rbac_lite/interfaces.py +10 -0
- fastapi_rbac_lite-0.1.0/src/fastapi_rbac_lite/resolvers.py +46 -0
- fastapi_rbac_lite-0.1.0/src/fastapi_rbac_lite/verifiers.py +65 -0
- fastapi_rbac_lite-0.1.0/src/fastapi_rbac_lite.egg-info/PKG-INFO +32 -0
- fastapi_rbac_lite-0.1.0/src/fastapi_rbac_lite.egg-info/SOURCES.txt +14 -0
- fastapi_rbac_lite-0.1.0/src/fastapi_rbac_lite.egg-info/dependency_links.txt +1 -0
- fastapi_rbac_lite-0.1.0/src/fastapi_rbac_lite.egg-info/requires.txt +12 -0
- fastapi_rbac_lite-0.1.0/src/fastapi_rbac_lite.egg-info/top_level.txt +1 -0
- fastapi_rbac_lite-0.1.0/tests/test_rbac.py +73 -0
- fastapi_rbac_lite-0.1.0/tests/test_verifiers.py +32 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-rbac-lite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pluggable RBAC permission checking for FastAPI -- bring your own token verifier and permission store.
|
|
5
|
+
Author-email: Athul A <athulbabu.a@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/athul2346/fastapi-rbac-lite
|
|
8
|
+
Project-URL: Repository, https://github.com/athul2346/fastapi-rbac-lite
|
|
9
|
+
Project-URL: Issues, https://github.com/athul2346/fastapi-rbac-lite/issues
|
|
10
|
+
Keywords: fastapi,rbac,jwt,jwks,authorization,authentication,permissions
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Framework :: FastAPI
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: fastapi>=0.100
|
|
23
|
+
Requires-Dist: pyjwt[crypto]>=2.8
|
|
24
|
+
Requires-Dist: httpx>=0.24
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
28
|
+
Requires-Dist: httpx>=0.24; extra == "dev"
|
|
29
|
+
Requires-Dist: cryptography>=41.0; extra == "dev"
|
|
30
|
+
Requires-Dist: uvicorn>=0.23; extra == "dev"
|
|
31
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: twine>=4.0; extra == "dev"
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fastapi-rbac-lite"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pluggable RBAC permission checking for FastAPI -- bring your own token verifier and permission store."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Athul A", email = "athulbabu.a@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["fastapi", "rbac", "jwt", "jwks", "authorization", "authentication", "permissions"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Framework :: FastAPI",
|
|
25
|
+
"Topic :: Security",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"fastapi>=0.100",
|
|
29
|
+
"pyjwt[crypto]>=2.8",
|
|
30
|
+
"httpx>=0.24",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=7.4",
|
|
36
|
+
"pytest-asyncio>=0.21",
|
|
37
|
+
"httpx>=0.24",
|
|
38
|
+
"cryptography>=41.0",
|
|
39
|
+
"uvicorn>=0.23",
|
|
40
|
+
"build>=1.0",
|
|
41
|
+
"twine>=4.0",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/athul2346/fastapi-rbac-lite"
|
|
46
|
+
Repository = "https://github.com/athul2346/fastapi-rbac-lite"
|
|
47
|
+
Issues = "https://github.com/athul2346/fastapi-rbac-lite/issues"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.packages.find]
|
|
50
|
+
where = ["src"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
asyncio_mode = "auto"
|
|
54
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""fastapi-rbac-lite
|
|
2
|
+
|
|
3
|
+
Pluggable RBAC permission checking for FastAPI: bring your own token
|
|
4
|
+
verifier and your own permission store, get a clean require_any_permission
|
|
5
|
+
dependency for your routes.
|
|
6
|
+
"""
|
|
7
|
+
from .auth import RBAC
|
|
8
|
+
from .interfaces import PermissionResolver, TokenVerifier
|
|
9
|
+
from .resolvers import HTTPPermissionResolver, StaticPermissionResolver
|
|
10
|
+
from .verifiers import JWKSVerifier, default_jwks_url_resolver
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"RBAC",
|
|
16
|
+
"TokenVerifier",
|
|
17
|
+
"PermissionResolver",
|
|
18
|
+
"JWKSVerifier",
|
|
19
|
+
"default_jwks_url_resolver",
|
|
20
|
+
"HTTPPermissionResolver",
|
|
21
|
+
"StaticPermissionResolver",
|
|
22
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
from fastapi import Depends, HTTPException, status
|
|
4
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
5
|
+
|
|
6
|
+
from .interfaces import PermissionResolver, TokenVerifier
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RBAC:
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
verifier: TokenVerifier,
|
|
13
|
+
resolver: PermissionResolver,
|
|
14
|
+
groups_claim: str = "groups",
|
|
15
|
+
bypass: Callable[[dict], bool] | None = None,
|
|
16
|
+
):
|
|
17
|
+
self.verifier = verifier
|
|
18
|
+
self.resolver = resolver
|
|
19
|
+
self.groups_claim = groups_claim
|
|
20
|
+
self.bypass = bypass or (lambda payload: False)
|
|
21
|
+
self._bearer = HTTPBearer()
|
|
22
|
+
|
|
23
|
+
async def get_current_user(
|
|
24
|
+
self, credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer())
|
|
25
|
+
) -> dict:
|
|
26
|
+
"""Dependency: verify the bearer token only, no permission check."""
|
|
27
|
+
return await self.verifier.verify(credentials.credentials)
|
|
28
|
+
|
|
29
|
+
def require_any_permission(self, permissions: list[str] | None = None):
|
|
30
|
+
"""Return a FastAPI dependency that grants access if the caller has
|
|
31
|
+
ANY of the given permissions. Pass no permissions (or an empty list)
|
|
32
|
+
to require only a valid token, with no permission check.
|
|
33
|
+
"""
|
|
34
|
+
permissions = permissions or []
|
|
35
|
+
|
|
36
|
+
async def _check_permissions(
|
|
37
|
+
credentials: HTTPAuthorizationCredentials = Depends(self._bearer),
|
|
38
|
+
) -> dict:
|
|
39
|
+
payload = await self.verifier.verify(credentials.credentials)
|
|
40
|
+
groups = payload.get(self.groups_claim, [])
|
|
41
|
+
|
|
42
|
+
if self.bypass(payload):
|
|
43
|
+
return payload
|
|
44
|
+
|
|
45
|
+
if not permissions:
|
|
46
|
+
return payload
|
|
47
|
+
|
|
48
|
+
user_permissions = await self.resolver.resolve(groups)
|
|
49
|
+
if not any(p in user_permissions for p in permissions):
|
|
50
|
+
raise HTTPException(
|
|
51
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
52
|
+
detail=f"Permission denied: one of {permissions} required",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return payload
|
|
56
|
+
|
|
57
|
+
return _check_permissions
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from fastapi import HTTPException, status
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class StaticPermissionResolver:
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
mapping:dict[str, set[str]]
|
|
9
|
+
):
|
|
10
|
+
self.mapping = mapping
|
|
11
|
+
|
|
12
|
+
async def resolve(self, groups: list[str]) -> set[str]:
|
|
13
|
+
permissions = set()
|
|
14
|
+
for group in groups:
|
|
15
|
+
permissions.update(self.mapping.get(group, []))
|
|
16
|
+
return permissions
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class HTTPPermissionResolver:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
base_url: str,
|
|
23
|
+
endpoint: str = "/roles/permissions-bulk",
|
|
24
|
+
request_key: str = "role_names",
|
|
25
|
+
response_key: str = "permissions",
|
|
26
|
+
timeout: float = 5.0,
|
|
27
|
+
):
|
|
28
|
+
self.base_url = base_url
|
|
29
|
+
self.endpoint = endpoint
|
|
30
|
+
self.request_key = request_key
|
|
31
|
+
self.response_key = response_key
|
|
32
|
+
self.timeout = timeout
|
|
33
|
+
|
|
34
|
+
async def resolve(self, groups: list[str]) -> set[str]:
|
|
35
|
+
if not groups:
|
|
36
|
+
return set()
|
|
37
|
+
url = f"{self.base_url.rstrip('/')}{self.endpoint}"
|
|
38
|
+
try:
|
|
39
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
40
|
+
response = await client.post(url, json={self.request_key: groups})
|
|
41
|
+
response.raise_for_status()
|
|
42
|
+
data = response.json()
|
|
43
|
+
permissions = set(data.get(self.response_key, []))
|
|
44
|
+
return permissions
|
|
45
|
+
except httpx.HTTPError as e:
|
|
46
|
+
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"HTTP error: {e}")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
import jwt
|
|
3
|
+
from fastapi import HTTPException, status
|
|
4
|
+
|
|
5
|
+
def default_jwks_url_resolver(issuer: str) -> str:
|
|
6
|
+
return f"{issuer.rstrip('/')}/.well-known/jwks.json"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class JWKSVerifier:
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
trusted_issuers: list[str],
|
|
13
|
+
audiences: list[str],
|
|
14
|
+
jwks_url_resolver: Callable[[str], str] = default_jwks_url_resolver,
|
|
15
|
+
algorithms: list[str] | None = None,
|
|
16
|
+
):
|
|
17
|
+
self.trusted_issuers = trusted_issuers
|
|
18
|
+
self.audiences = audiences
|
|
19
|
+
self.jwks_url_resolver = jwks_url_resolver
|
|
20
|
+
self.algorithms = algorithms or ["RS256"]
|
|
21
|
+
self._jwks_clients: dict[str, jwt.PyJWKClient] = {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_jwks_client(self, issuer:str) -> jwt.PyJWKClient:
|
|
25
|
+
if issuer not in self._jwks_clients:
|
|
26
|
+
jwks_url = self.jwks_url_resolver(issuer)
|
|
27
|
+
self._jwks_clients[issuer] = jwt.PyJWKClient(jwks_url)
|
|
28
|
+
return self._jwks_clients[issuer]
|
|
29
|
+
|
|
30
|
+
async def verify(self, token: str) -> dict:
|
|
31
|
+
try:
|
|
32
|
+
unverified = jwt.decode(token, options={"verify_signature": False})
|
|
33
|
+
except jwt.InvalidTokenError as e:
|
|
34
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Malformed token: {e}")
|
|
35
|
+
iss = unverified.get("iss")
|
|
36
|
+
aud = unverified.get("aud")
|
|
37
|
+
if not iss or not aud:
|
|
38
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
|
39
|
+
if iss not in self.trusted_issuers:
|
|
40
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Untrusted token issuer")
|
|
41
|
+
try:
|
|
42
|
+
jwks_client = self._get_jwks_client(iss)
|
|
43
|
+
signing_key = jwks_client.get_signing_key_from_jwt(token)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=f"JWKS fetch failed: {e}")
|
|
46
|
+
try:
|
|
47
|
+
payload = jwt.decode(
|
|
48
|
+
token,
|
|
49
|
+
signing_key.key,
|
|
50
|
+
algorithms=self.algorithms,
|
|
51
|
+
audience=self.audiences,
|
|
52
|
+
issuer=iss,
|
|
53
|
+
)
|
|
54
|
+
return payload
|
|
55
|
+
except jwt.ExpiredSignatureError:
|
|
56
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired")
|
|
57
|
+
except jwt.InvalidAudienceError:
|
|
58
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid audience")
|
|
59
|
+
except jwt.InvalidIssuerError:
|
|
60
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid issuer")
|
|
61
|
+
except jwt.InvalidTokenError as e:
|
|
62
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Malformed token: {e}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi-rbac-lite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pluggable RBAC permission checking for FastAPI -- bring your own token verifier and permission store.
|
|
5
|
+
Author-email: Athul A <athulbabu.a@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/athul2346/fastapi-rbac-lite
|
|
8
|
+
Project-URL: Repository, https://github.com/athul2346/fastapi-rbac-lite
|
|
9
|
+
Project-URL: Issues, https://github.com/athul2346/fastapi-rbac-lite/issues
|
|
10
|
+
Keywords: fastapi,rbac,jwt,jwks,authorization,authentication,permissions
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Framework :: FastAPI
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: fastapi>=0.100
|
|
23
|
+
Requires-Dist: pyjwt[crypto]>=2.8
|
|
24
|
+
Requires-Dist: httpx>=0.24
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
28
|
+
Requires-Dist: httpx>=0.24; extra == "dev"
|
|
29
|
+
Requires-Dist: cryptography>=41.0; extra == "dev"
|
|
30
|
+
Requires-Dist: uvicorn>=0.23; extra == "dev"
|
|
31
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: twine>=4.0; extra == "dev"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/fastapi_rbac_lite/__init__.py
|
|
4
|
+
src/fastapi_rbac_lite/auth.py
|
|
5
|
+
src/fastapi_rbac_lite/interfaces.py
|
|
6
|
+
src/fastapi_rbac_lite/resolvers.py
|
|
7
|
+
src/fastapi_rbac_lite/verifiers.py
|
|
8
|
+
src/fastapi_rbac_lite.egg-info/PKG-INFO
|
|
9
|
+
src/fastapi_rbac_lite.egg-info/SOURCES.txt
|
|
10
|
+
src/fastapi_rbac_lite.egg-info/dependency_links.txt
|
|
11
|
+
src/fastapi_rbac_lite.egg-info/requires.txt
|
|
12
|
+
src/fastapi_rbac_lite.egg-info/top_level.txt
|
|
13
|
+
tests/test_rbac.py
|
|
14
|
+
tests/test_verifiers.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastapi_rbac_lite
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from fastapi import HTTPException
|
|
3
|
+
from src.fastapi_rbac_lite.auth import RBAC
|
|
4
|
+
from src.fastapi_rbac_lite.resolvers import StaticPermissionResolver
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FakeVerifier:
|
|
8
|
+
def __init__(self, tokens: dict[str, dict]):
|
|
9
|
+
self.tokens = tokens
|
|
10
|
+
|
|
11
|
+
async def verify(self, token: str) -> dict:
|
|
12
|
+
if token not in self.tokens:
|
|
13
|
+
raise HTTPException(401, detail="invalid token")
|
|
14
|
+
return self.tokens[token]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FakeCredentials:
|
|
18
|
+
def __init__(self, token: str):
|
|
19
|
+
self.credentials = token
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def rbac():
|
|
24
|
+
verifier = FakeVerifier({
|
|
25
|
+
"admin-token": {"sub": "u1", "groups": ["admins"]},
|
|
26
|
+
"viewer-token": {"sub": "u2", "groups": ["viewers"]},
|
|
27
|
+
"m2m-token": {"sub": "svc", "groups": [], "preferred_username": "internal-svc"},
|
|
28
|
+
})
|
|
29
|
+
resolver = StaticPermissionResolver({
|
|
30
|
+
"admins": {"invoice:read", "invoice:approve"},
|
|
31
|
+
"viewers": {"invoice:read"},
|
|
32
|
+
})
|
|
33
|
+
return RBAC(
|
|
34
|
+
verifier=verifier,
|
|
35
|
+
resolver=resolver,
|
|
36
|
+
bypass=lambda payload: payload.get("preferred_username") == "internal-svc",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_admin_has_permission(rbac):
|
|
42
|
+
dep = rbac.require_any_permission(["invoice:approve"])
|
|
43
|
+
payload = await dep(FakeCredentials("admin-token"))
|
|
44
|
+
assert payload["sub"] == "u1"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.mark.asyncio
|
|
48
|
+
async def test_viewer_denied_for_approve(rbac):
|
|
49
|
+
dep = rbac.require_any_permission(["invoice:approve"])
|
|
50
|
+
with pytest.raises(HTTPException) as exc_info:
|
|
51
|
+
await dep(FakeCredentials("viewer-token"))
|
|
52
|
+
assert exc_info.value.status_code == 403
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.asyncio
|
|
56
|
+
async def test_viewer_allowed_for_read(rbac):
|
|
57
|
+
dep = rbac.require_any_permission(["invoice:read"])
|
|
58
|
+
payload = await dep(FakeCredentials("viewer-token"))
|
|
59
|
+
assert payload["sub"] == "u2"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.asyncio
|
|
63
|
+
async def test_m2m_bypass_skips_permission_check(rbac):
|
|
64
|
+
dep = rbac.require_any_permission(["invoice:approve"])
|
|
65
|
+
payload = await dep(FakeCredentials("m2m-token"))
|
|
66
|
+
assert payload["sub"] == "svc"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.asyncio
|
|
70
|
+
async def test_no_permissions_required(rbac):
|
|
71
|
+
dep = rbac.require_any_permission([])
|
|
72
|
+
payload = await dep(FakeCredentials("viewer-token"))
|
|
73
|
+
assert payload["sub"] == "u2"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from fastapi import HTTPException
|
|
3
|
+
from src.fastapi_rbac_lite.verifiers import JWKSVerifier
|
|
4
|
+
from fixtures.sign_test_token import sign_test_token
|
|
5
|
+
|
|
6
|
+
@pytest.mark.asyncio
|
|
7
|
+
async def test_valid_token_verifies(fake_idp_url):
|
|
8
|
+
verifier = JWKSVerifier(trusted_issuers=[fake_idp_url], audiences=["test-audience"])
|
|
9
|
+
payload = {
|
|
10
|
+
"sub": "test-user",
|
|
11
|
+
"groups": ["test-user"],
|
|
12
|
+
"iss": fake_idp_url,
|
|
13
|
+
"aud": "test-audience"
|
|
14
|
+
}
|
|
15
|
+
token = sign_test_token(payload)
|
|
16
|
+
result = await verifier.verify(token)
|
|
17
|
+
assert result == payload
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.asyncio
|
|
21
|
+
async def test_wrong_audience_rejected(fake_idp_url):
|
|
22
|
+
verifier = JWKSVerifier(trusted_issuers=[fake_idp_url], audiences=["test-audience"])
|
|
23
|
+
payload = {
|
|
24
|
+
"sub": "test-user",
|
|
25
|
+
"groups": ["admins"],
|
|
26
|
+
"iss": fake_idp_url,
|
|
27
|
+
"aud": "fake-aud"
|
|
28
|
+
}
|
|
29
|
+
token = sign_test_token(payload)
|
|
30
|
+
with pytest.raises(HTTPException) as exc_info:
|
|
31
|
+
await verifier.verify(token)
|
|
32
|
+
assert exc_info.value.status_code == 401
|