lks-idprovider-fastapi 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.
- lks_idprovider_fastapi-0.1.0/PKG-INFO +37 -0
- lks_idprovider_fastapi-0.1.0/README.md +19 -0
- lks_idprovider_fastapi-0.1.0/pyproject.toml +34 -0
- lks_idprovider_fastapi-0.1.0/src/lks_idprovider_fastapi/__init__.py +17 -0
- lks_idprovider_fastapi-0.1.0/src/lks_idprovider_fastapi/dependencies.py +191 -0
- lks_idprovider_fastapi-0.1.0/src/lks_idprovider_fastapi/security.py +10 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: lks-idprovider-fastapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Provider-agnostic FastAPI integration for lks-idprovider API protocols.
|
|
5
|
+
License: LKSISL
|
|
6
|
+
Author: Raul Medeiros
|
|
7
|
+
Author-email: rmedeiros@lksnext.com
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Classifier: License :: Other/Proprietary License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: fastapi (>=0.116.1)
|
|
15
|
+
Requires-Dist: lks-idprovider-api (>=0.1.6,<0.2.0)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# lks-idprovider-fastapi
|
|
19
|
+
|
|
20
|
+
Provider-agnostic FastAPI integration for lks-idprovider API protocols.
|
|
21
|
+
|
|
22
|
+
This package provides reusable FastAPI dependencies, decorators, and middleware for REST API security, based on the lks-idprovider API protocols. It is not tied to any specific provider implementation (e.g., Keycloak) and can be used with any compatible backend.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
- Bearer token security scheme for OpenAPI
|
|
26
|
+
- AuthContext dependency for user/client authentication
|
|
27
|
+
- Role-based and route protection dependencies
|
|
28
|
+
- Easy integration with any provider implementing the API protocols
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
1. Install this package and your chosen provider implementation.
|
|
32
|
+
2. Configure your provider (e.g., Keycloak) in your FastAPI app.
|
|
33
|
+
3. Inject the provider into the dependencies from this package.
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
LKSISL
|
|
37
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# lks-idprovider-fastapi
|
|
2
|
+
|
|
3
|
+
Provider-agnostic FastAPI integration for lks-idprovider API protocols.
|
|
4
|
+
|
|
5
|
+
This package provides reusable FastAPI dependencies, decorators, and middleware for REST API security, based on the lks-idprovider API protocols. It is not tied to any specific provider implementation (e.g., Keycloak) and can be used with any compatible backend.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
- Bearer token security scheme for OpenAPI
|
|
9
|
+
- AuthContext dependency for user/client authentication
|
|
10
|
+
- Role-based and route protection dependencies
|
|
11
|
+
- Easy integration with any provider implementing the API protocols
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
1. Install this package and your chosen provider implementation.
|
|
15
|
+
2. Configure your provider (e.g., Keycloak) in your FastAPI app.
|
|
16
|
+
3. Inject the provider into the dependencies from this package.
|
|
17
|
+
|
|
18
|
+
## License
|
|
19
|
+
LKSISL
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core>=2.0"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lks-idprovider-fastapi"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Provider-agnostic FastAPI integration for lks-idprovider API protocols."
|
|
9
|
+
authors = [
|
|
10
|
+
{name = "Raul Medeiros", email = "rmedeiros@lksnext.com"}
|
|
11
|
+
]
|
|
12
|
+
license = "LKSISL"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.11"
|
|
15
|
+
|
|
16
|
+
dependencies = [
|
|
17
|
+
"fastapi>=0.116.1",
|
|
18
|
+
"lks-idprovider-api>=0.1.6,<0.2.0"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.poetry]
|
|
22
|
+
packages = [{include = "lks_idprovider_fastapi", from = "src"}]
|
|
23
|
+
|
|
24
|
+
[tool.poetry.group.dev.dependencies]
|
|
25
|
+
pytest = "^8.0.0"
|
|
26
|
+
pytest-cov = "^5.0.0"
|
|
27
|
+
pytest-asyncio = "^0.23.0"
|
|
28
|
+
pytest-dotenv = "^0.5.2"
|
|
29
|
+
lks-idprovider-keycloak = ">=0.1.1,<0.2.0"
|
|
30
|
+
lks-idprovider-entraid = ">=0.1.0,<0.2.0"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .security import get_bearer_token
|
|
2
|
+
from .dependencies import (
|
|
3
|
+
get_auth_context,
|
|
4
|
+
login_required,
|
|
5
|
+
requires_role,
|
|
6
|
+
requires_any_role,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
# Security
|
|
11
|
+
"get_bearer_token",
|
|
12
|
+
# Dependencies
|
|
13
|
+
"get_auth_context",
|
|
14
|
+
"login_required",
|
|
15
|
+
"requires_role",
|
|
16
|
+
"requires_any_role",
|
|
17
|
+
]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from typing import Union, Optional, Callable
|
|
2
|
+
from fastapi import Depends, HTTPException, status
|
|
3
|
+
from lks_idprovider_fastapi.security import get_bearer_token
|
|
4
|
+
from lks_idprovider.models.auth import AuthContext
|
|
5
|
+
from lks_idprovider.protocols import ClientCredentialsProvider, IdentityProvider
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Default provider factory - should be overridden in production
|
|
9
|
+
def get_default_provider() -> Union[ClientCredentialsProvider, IdentityProvider]:
|
|
10
|
+
"""Default provider factory that raises an error.
|
|
11
|
+
|
|
12
|
+
This should be overridden in production applications to return
|
|
13
|
+
the actual provider instance.
|
|
14
|
+
"""
|
|
15
|
+
raise HTTPException(
|
|
16
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
17
|
+
detail="No provider configured. Please override get_default_provider dependency.",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def get_auth_context(
|
|
22
|
+
token: str = Depends(get_bearer_token),
|
|
23
|
+
provider: Union[ClientCredentialsProvider, IdentityProvider] = Depends(
|
|
24
|
+
get_default_provider
|
|
25
|
+
),
|
|
26
|
+
) -> AuthContext:
|
|
27
|
+
"""
|
|
28
|
+
Dependency that resolves and returns an AuthContext for the current request.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
token (str): The bearer token extracted from the request, provided by the get_bearer_token dependency.
|
|
32
|
+
provider (Any): An instance of either IdentityProvider or ClientCredentialsProvider, injected as a dependency.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
AuthContext: The authentication context associated with the provided token.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
HTTPException: If authentication fails or the provider is not properly initialized.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
if isinstance(provider, IdentityProvider):
|
|
43
|
+
ctx = await provider.get_auth_context(token)
|
|
44
|
+
elif isinstance(provider, ClientCredentialsProvider):
|
|
45
|
+
ctx = await provider.get_client_auth_context(token)
|
|
46
|
+
return ctx
|
|
47
|
+
except Exception as e:
|
|
48
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def login_required(
|
|
52
|
+
token: str = Depends(get_bearer_token),
|
|
53
|
+
provider: Union[ClientCredentialsProvider, IdentityProvider] = Depends(
|
|
54
|
+
get_default_provider
|
|
55
|
+
),
|
|
56
|
+
) -> AuthContext:
|
|
57
|
+
"""
|
|
58
|
+
Dependency that validates the token and returns an AuthContext.
|
|
59
|
+
|
|
60
|
+
This is a convenience alias for get_auth_context that clearly indicates
|
|
61
|
+
the endpoint requires a valid login/authentication.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
token (str): The bearer token extracted from the request.
|
|
65
|
+
provider: An instance of either IdentityProvider or ClientCredentialsProvider.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
AuthContext: The authentication context for the validated token.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
HTTPException: If authentication fails.
|
|
72
|
+
"""
|
|
73
|
+
return await get_auth_context(token, provider)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _check_role_access(
|
|
77
|
+
auth_context: AuthContext, role_names: list[str], client: Optional[str] = None
|
|
78
|
+
) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Helper function to check if the user has any of the required roles.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
auth_context: The authentication context.
|
|
84
|
+
role_names: List of role names to check.
|
|
85
|
+
client: Optional client ID for client-specific roles.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
HTTPException: If the user doesn't have any of the required roles.
|
|
89
|
+
"""
|
|
90
|
+
# Check if user has any of the required roles
|
|
91
|
+
user_has_role = any(
|
|
92
|
+
role.name in role_names and role.client == client for role in auth_context.roles
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if not user_has_role:
|
|
96
|
+
# Create a more streamlined and descriptive error message
|
|
97
|
+
if len(role_names) == 1:
|
|
98
|
+
role_desc = f"'{role_names[0]}'"
|
|
99
|
+
message = f"Required role {role_desc}"
|
|
100
|
+
else:
|
|
101
|
+
roles_str = "', '".join(role_names)
|
|
102
|
+
role_desc = f"['{roles_str}']"
|
|
103
|
+
message = f"At least one of roles {role_desc}"
|
|
104
|
+
|
|
105
|
+
client_part = f" for client '{client}'" if client else ""
|
|
106
|
+
detail = f"Access denied. {message}{client_part} is required."
|
|
107
|
+
|
|
108
|
+
raise HTTPException(
|
|
109
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
110
|
+
detail=detail,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _create_role_dependency(
|
|
115
|
+
role_names: list[str], client: Optional[str] = None
|
|
116
|
+
) -> Callable[[AuthContext], AuthContext]:
|
|
117
|
+
"""
|
|
118
|
+
Internal helper to create a role dependency function.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def role_dependency(
|
|
122
|
+
auth_context: AuthContext = Depends(get_auth_context),
|
|
123
|
+
) -> AuthContext:
|
|
124
|
+
"""
|
|
125
|
+
Internal dependency that validates role access.
|
|
126
|
+
"""
|
|
127
|
+
_check_role_access(auth_context, role_names, client)
|
|
128
|
+
return auth_context
|
|
129
|
+
|
|
130
|
+
return role_dependency
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def requires_role(
|
|
134
|
+
role_name: str, client: Optional[str] = None
|
|
135
|
+
) -> Callable[[AuthContext], AuthContext]:
|
|
136
|
+
"""
|
|
137
|
+
Dependency factory that creates a role-based access control dependency.
|
|
138
|
+
|
|
139
|
+
This function returns a dependency that checks if the authenticated user/client
|
|
140
|
+
has the specified role. It builds on top of get_auth_context.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
role_name (str): The name of the required role.
|
|
144
|
+
client (Optional[str]): The client ID for client-specific roles. If None,
|
|
145
|
+
checks for realm/global roles.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Callable: A FastAPI dependency function that validates role access.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
```python
|
|
152
|
+
@app.get("/admin")
|
|
153
|
+
async def admin_endpoint(
|
|
154
|
+
auth_context: AuthContext = Depends(requires_role("admin"))
|
|
155
|
+
):
|
|
156
|
+
return {"message": "Admin access granted"}
|
|
157
|
+
|
|
158
|
+
@app.get("/client-specific")
|
|
159
|
+
async def client_endpoint(
|
|
160
|
+
auth_context: AuthContext = Depends(requires_role("manager", client="my-app"))
|
|
161
|
+
):
|
|
162
|
+
return {"message": "Client-specific role access granted"}
|
|
163
|
+
```
|
|
164
|
+
"""
|
|
165
|
+
return _create_role_dependency([role_name], client)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def requires_any_role(
|
|
169
|
+
*role_names: str, client: Optional[str] = None
|
|
170
|
+
) -> Callable[[AuthContext], AuthContext]:
|
|
171
|
+
"""
|
|
172
|
+
Dependency factory that creates a role-based access control dependency
|
|
173
|
+
that accepts any of the specified roles.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
*role_names: Variable number of role names. User must have at least one.
|
|
177
|
+
client (Optional[str]): The client ID for client-specific roles.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Callable: A FastAPI dependency function that validates role access.
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
```python
|
|
184
|
+
@app.get("/editor")
|
|
185
|
+
async def editor_endpoint(
|
|
186
|
+
auth_context: AuthContext = Depends(requires_any_role("editor", "admin"))
|
|
187
|
+
):
|
|
188
|
+
return {"message": "Editor or admin access granted"}
|
|
189
|
+
```
|
|
190
|
+
"""
|
|
191
|
+
return _create_role_dependency(list(role_names), client)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
2
|
+
from fastapi import Depends
|
|
3
|
+
|
|
4
|
+
bearer_scheme = HTTPBearer(auto_error=True)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_bearer_token(
|
|
8
|
+
credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
|
|
9
|
+
) -> str:
|
|
10
|
+
return credentials.credentials
|