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.
@@ -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