fastapi-factory-utilities 0.2.11__py3-none-any.whl → 0.3.0__py3-none-any.whl
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.
Potentially problematic release.
This version of fastapi-factory-utilities might be problematic. Click here for more details.
- fastapi_factory_utilities/core/app/config.py +20 -1
- fastapi_factory_utilities/core/plugins/odm_plugin/repositories.py +2 -3
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/builder.py +65 -16
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/configs.py +13 -0
- fastapi_factory_utilities/core/security/jwt.py +158 -0
- fastapi_factory_utilities/core/security/kratos.py +78 -0
- fastapi_factory_utilities/core/services/kratos/__init__.py +13 -0
- fastapi_factory_utilities/core/services/kratos/enums.py +11 -0
- fastapi_factory_utilities/core/services/kratos/exceptions.py +15 -0
- fastapi_factory_utilities/core/services/kratos/objects.py +42 -0
- fastapi_factory_utilities/core/services/kratos/services.py +86 -0
- {fastapi_factory_utilities-0.2.11.dist-info → fastapi_factory_utilities-0.3.0.dist-info}/METADATA +3 -1
- {fastapi_factory_utilities-0.2.11.dist-info → fastapi_factory_utilities-0.3.0.dist-info}/RECORD +16 -9
- {fastapi_factory_utilities-0.2.11.dist-info → fastapi_factory_utilities-0.3.0.dist-info}/LICENSE +0 -0
- {fastapi_factory_utilities-0.2.11.dist-info → fastapi_factory_utilities-0.3.0.dist-info}/WHEEL +0 -0
- {fastapi_factory_utilities-0.2.11.dist-info → fastapi_factory_utilities-0.3.0.dist-info}/entry_points.txt +0 -0
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any, ClassVar, Generic, TypeVar, get_args
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from fastapi import Request
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, HttpUrl
|
|
6
7
|
|
|
7
8
|
from fastapi_factory_utilities.core.app.exceptions import ConfigBuilderError
|
|
8
9
|
from fastapi_factory_utilities.core.utils.configs import (
|
|
@@ -71,6 +72,18 @@ class BaseApplicationConfig(BaseModel):
|
|
|
71
72
|
root_path: str = Field(default="", description="Root path")
|
|
72
73
|
|
|
73
74
|
|
|
75
|
+
class HttpServiceDependencyConfig(BaseModel):
|
|
76
|
+
"""Http service dependency config."""
|
|
77
|
+
|
|
78
|
+
url: HttpUrl
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class DependencyConfig(BaseModel):
|
|
82
|
+
"""Dependency config."""
|
|
83
|
+
|
|
84
|
+
kratos: HttpServiceDependencyConfig | None = Field(default=None, description="Kratos dependency config")
|
|
85
|
+
|
|
86
|
+
|
|
74
87
|
class RootConfig(BaseModel):
|
|
75
88
|
"""Root configuration."""
|
|
76
89
|
|
|
@@ -84,6 +97,7 @@ class RootConfig(BaseModel):
|
|
|
84
97
|
cors: CorsConfig = Field(description="CORS configuration", default_factory=CorsConfig)
|
|
85
98
|
development: DevelopmentConfig = Field(description="Development configuration", default_factory=DevelopmentConfig)
|
|
86
99
|
logging: list[LoggingConfig] = Field(description="Logging configuration", default_factory=list)
|
|
100
|
+
dependencies: DependencyConfig = Field(description="Dependencies configuration", default_factory=DependencyConfig)
|
|
87
101
|
|
|
88
102
|
|
|
89
103
|
GenericConfig = TypeVar("GenericConfig", bound=BaseModel)
|
|
@@ -162,3 +176,8 @@ class GenericConfigBuilder(Generic[GenericConfig]):
|
|
|
162
176
|
) from exception
|
|
163
177
|
|
|
164
178
|
return config
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def depends_dependency_config(request: Request) -> DependencyConfig:
|
|
182
|
+
"""Get the dependency config."""
|
|
183
|
+
return request.app.state.config.dependencies
|
|
@@ -6,7 +6,6 @@ from collections.abc import AsyncGenerator, Callable
|
|
|
6
6
|
from contextlib import asynccontextmanager
|
|
7
7
|
from typing import Any, Generic, TypeVar, get_args
|
|
8
8
|
from uuid import UUID
|
|
9
|
-
from venv import create
|
|
10
9
|
|
|
11
10
|
from motor.motor_asyncio import AsyncIOMotorClientSession, AsyncIOMotorDatabase
|
|
12
11
|
from pydantic import BaseModel
|
|
@@ -79,7 +78,7 @@ class AbstractRepository(ABC, Generic[DocumentGenericType, EntityGenericType]):
|
|
|
79
78
|
UnableToCreateEntityDueToDuplicateKeyError: If the entity cannot be created due to a duplicate key error.
|
|
80
79
|
OperationError: If the operation fails.
|
|
81
80
|
"""
|
|
82
|
-
insert_time: datetime.datetime = datetime.datetime.now(tz=datetime.
|
|
81
|
+
insert_time: datetime.datetime = datetime.datetime.now(tz=datetime.UTC)
|
|
83
82
|
try:
|
|
84
83
|
entity_dump: dict[str, Any] = entity.model_dump()
|
|
85
84
|
entity_dump["created_at"] = insert_time
|
|
@@ -120,7 +119,7 @@ class AbstractRepository(ABC, Generic[DocumentGenericType, EntityGenericType]):
|
|
|
120
119
|
ValueError: If the entity cannot be created from the document.
|
|
121
120
|
OperationError: If the operation fails.
|
|
122
121
|
"""
|
|
123
|
-
update_time: datetime.datetime = datetime.datetime.now(tz=datetime.
|
|
122
|
+
update_time: datetime.datetime = datetime.datetime.now(tz=datetime.UTC)
|
|
124
123
|
try:
|
|
125
124
|
entity_dump: dict[str, Any] = entity.model_dump()
|
|
126
125
|
entity_dump["updated_at"] = update_time
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
"""Provides a factory function to build a objets for OpenTelemetry."""
|
|
2
2
|
|
|
3
3
|
from typing import Any, Self
|
|
4
|
+
from urllib.parse import ParseResult, urlparse
|
|
4
5
|
|
|
5
|
-
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import
|
|
6
|
-
|
|
6
|
+
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
|
|
7
|
+
OTLPMetricExporter as OTLPMetricExporterGRPC,
|
|
8
|
+
)
|
|
9
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
|
10
|
+
OTLPSpanExporter as OTLPSpanExporterGRPC,
|
|
11
|
+
)
|
|
12
|
+
from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
|
|
13
|
+
OTLPMetricExporter as OTLPMetricExporterHTTP,
|
|
14
|
+
)
|
|
15
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
16
|
+
OTLPSpanExporter as OTLPSpanExporterHTTP,
|
|
17
|
+
)
|
|
7
18
|
from opentelemetry.metrics import set_meter_provider
|
|
8
19
|
from opentelemetry.propagate import set_global_textmap
|
|
9
20
|
from opentelemetry.propagators.b3 import B3MultiFormat
|
|
@@ -31,24 +42,30 @@ from fastapi_factory_utilities.core.utils.yaml_reader import (
|
|
|
31
42
|
YamlFileReader,
|
|
32
43
|
)
|
|
33
44
|
|
|
34
|
-
from .configs import OpenTelemetryConfig
|
|
45
|
+
from .configs import OpenTelemetryConfig, ProtocolEnum
|
|
35
46
|
from .exceptions import OpenTelemetryPluginConfigError
|
|
36
47
|
|
|
48
|
+
GRPC_PORT: int = 4317
|
|
49
|
+
HTTP_PORT: int = 4318
|
|
50
|
+
|
|
37
51
|
|
|
38
52
|
class OpenTelemetryPluginBuilder:
|
|
39
53
|
"""Configure the injection bindings for OpenTelemetryPlugin."""
|
|
40
54
|
|
|
41
|
-
def __init__(self, application: ApplicationAbstractProtocol) -> None:
|
|
55
|
+
def __init__(self, application: ApplicationAbstractProtocol, settings: OpenTelemetryConfig | None = None) -> None:
|
|
42
56
|
"""Instantiate the OpenTelemetryPluginFactory.
|
|
43
57
|
|
|
44
58
|
Args:
|
|
45
59
|
application (BaseApplicationProtocol): The application object.
|
|
60
|
+
settings (OpenTelemetryConfig | None): The OpenTelemetry configuration object.
|
|
46
61
|
"""
|
|
47
62
|
self._application: ApplicationAbstractProtocol = application
|
|
48
63
|
self._resource: Resource | None = None
|
|
49
|
-
self._config: OpenTelemetryConfig | None =
|
|
64
|
+
self._config: OpenTelemetryConfig | None = settings
|
|
50
65
|
self._meter_provider: MeterProvider | None = None
|
|
51
66
|
self._tracer_provider: TracerProvider | None = None
|
|
67
|
+
self._trace_exporter: OTLPSpanExporterGRPC | OTLPSpanExporterHTTP | None = None
|
|
68
|
+
self._metric_exporter: OTLPMetricExporterGRPC | OTLPMetricExporterHTTP | None = None
|
|
52
69
|
|
|
53
70
|
@property
|
|
54
71
|
def resource(self) -> Resource | None:
|
|
@@ -161,12 +178,27 @@ class OpenTelemetryPluginBuilder:
|
|
|
161
178
|
|
|
162
179
|
# TODO: Extract to a dedicated method for the exporter and period reader setup
|
|
163
180
|
|
|
181
|
+
url_parsed: ParseResult = urlparse(self._config.endpoint.unicode_string())
|
|
182
|
+
|
|
183
|
+
exporter: OTLPMetricExporterGRPC | OTLPMetricExporterHTTP
|
|
164
184
|
# Setup the Exporter
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
185
|
+
if url_parsed.port == GRPC_PORT or self._config.protocol == ProtocolEnum.OTLP_GRPC:
|
|
186
|
+
exporter = OTLPMetricExporterGRPC(
|
|
187
|
+
endpoint=f"{self._config.endpoint.unicode_string()}",
|
|
188
|
+
timeout=self._config.timeout,
|
|
189
|
+
insecure=True if str(self._config.endpoint).startswith("http") else False,
|
|
190
|
+
)
|
|
191
|
+
elif url_parsed.port == HTTP_PORT or self._config.protocol == ProtocolEnum.OTLP_HTTP:
|
|
192
|
+
exporter = OTLPMetricExporterHTTP(
|
|
193
|
+
endpoint=f"{self._config.endpoint.unicode_string()}/v1/metrics",
|
|
194
|
+
timeout=self._config.timeout,
|
|
195
|
+
)
|
|
196
|
+
else:
|
|
197
|
+
raise OpenTelemetryPluginConfigError(
|
|
198
|
+
"The endpoint port is not supported. Use 4317 for gRPC or 4318 for HTTP or set the protocol."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
self._metric_exporter = exporter
|
|
170
202
|
|
|
171
203
|
# Setup the Metric Reader
|
|
172
204
|
meter_config: OpenTelemetryMeterConfig = self._config.meter_config
|
|
@@ -214,12 +246,28 @@ class OpenTelemetryPluginBuilder:
|
|
|
214
246
|
if self._config.tracer_config is None:
|
|
215
247
|
raise OpenTelemetryPluginConfigError("The tracer configuration is missing.")
|
|
216
248
|
|
|
249
|
+
exporter: OTLPSpanExporterGRPC | OTLPSpanExporterHTTP
|
|
250
|
+
url_parsed: ParseResult = urlparse(self._config.endpoint.unicode_string())
|
|
217
251
|
# Setup the Exporter
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
252
|
+
if url_parsed.port == GRPC_PORT or self._config.protocol == ProtocolEnum.OTLP_GRPC:
|
|
253
|
+
insecure: bool = False if str(self._config.endpoint).startswith("https") else True
|
|
254
|
+
endpoint: str = f"{self._config.endpoint.unicode_string()}"
|
|
255
|
+
exporter = OTLPSpanExporterGRPC(
|
|
256
|
+
endpoint=endpoint,
|
|
257
|
+
# timeout=self._config.timeout,
|
|
258
|
+
insecure=insecure,
|
|
259
|
+
)
|
|
260
|
+
elif url_parsed.port == HTTP_PORT or self._config.protocol == ProtocolEnum.OTLP_HTTP:
|
|
261
|
+
exporter = OTLPSpanExporterHTTP(
|
|
262
|
+
endpoint=f"{self._config.endpoint.unicode_string()}/v1/traces",
|
|
263
|
+
# timeout=self._config.timeout,
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
raise OpenTelemetryPluginConfigError(
|
|
267
|
+
"The endpoint port is not supported. Use 4317 for gRPC or 4318 for HTTP or set the protocol."
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
self._trace_exporter = exporter
|
|
223
271
|
|
|
224
272
|
# Setup the Span Processor
|
|
225
273
|
tracer_config: OpenTelemetryTracerConfig = self._config.tracer_config
|
|
@@ -261,7 +309,8 @@ class OpenTelemetryPluginBuilder:
|
|
|
261
309
|
Self: The OpenTelemetryPluginFactory object.
|
|
262
310
|
"""
|
|
263
311
|
self.build_resource()
|
|
264
|
-
self.
|
|
312
|
+
if self._config is None:
|
|
313
|
+
self.build_config()
|
|
265
314
|
self.build_meter_provider()
|
|
266
315
|
self.build_tracer_provider()
|
|
267
316
|
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
"""Provides the configuration model for the OpenTelemetry plugin."""
|
|
2
2
|
|
|
3
|
+
from enum import StrEnum
|
|
3
4
|
from typing import Annotated
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel, ConfigDict, Field, UrlConstraints
|
|
6
7
|
from pydantic_core import Url
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
class ProtocolEnum(StrEnum):
|
|
11
|
+
"""Defines the protocol enum for OpenTelemetry."""
|
|
12
|
+
|
|
13
|
+
OTLP_GRPC = "otlp_grpc"
|
|
14
|
+
OTLP_HTTP = "otlp_http"
|
|
15
|
+
|
|
16
|
+
|
|
9
17
|
class OpenTelemetryMeterConfig(BaseModel):
|
|
10
18
|
"""Provides the configuration model for the OpenTelemetry meter as sub-model."""
|
|
11
19
|
|
|
@@ -77,6 +85,11 @@ class OpenTelemetryConfig(BaseModel):
|
|
|
77
85
|
description="The collector endpoint.",
|
|
78
86
|
)
|
|
79
87
|
|
|
88
|
+
protocol: ProtocolEnum | None = Field(
|
|
89
|
+
default=None,
|
|
90
|
+
description="The protocol to use for the collector.",
|
|
91
|
+
)
|
|
92
|
+
|
|
80
93
|
timeout: int = Field(
|
|
81
94
|
default=TEN_SECONDS_IN_SECONDS,
|
|
82
95
|
description="The timeout in seconds for the collector.",
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Provides security-related functions for the API."""
|
|
2
|
+
|
|
3
|
+
from asyncio import Task, TaskGroup
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
from typing import Any, ClassVar, NewType, cast
|
|
6
|
+
|
|
7
|
+
import jwt
|
|
8
|
+
import pydantic
|
|
9
|
+
from fastapi import Request
|
|
10
|
+
from fastapi.exceptions import HTTPException
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
Scope = NewType("Scope", str)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class JWTBearerDecoded(BaseModel):
|
|
17
|
+
"""JWT bearer token decoded."""
|
|
18
|
+
|
|
19
|
+
model_config: ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(
|
|
20
|
+
arbitrary_types_allowed=True,
|
|
21
|
+
extra="forbid",
|
|
22
|
+
frozen=True,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
scopes: list[str] | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class JWTBearerAuthentication:
|
|
29
|
+
"""JWT Bearer Authentication.
|
|
30
|
+
|
|
31
|
+
This class is used to authenticate users using JWT tokens.
|
|
32
|
+
It extracts the token from the request, decodes it, and verifies its validity.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, scopes: list[Scope] | None = None, jwt_raw: str | None = None) -> None:
|
|
36
|
+
"""Initialize the OAuth2 class.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
scopes (SecurityScopes): Security scopes for the OAuth2.
|
|
40
|
+
jwt_raw (str): JWT token to be used for authentication.
|
|
41
|
+
"""
|
|
42
|
+
self.jwt_raw: str | None = jwt_raw
|
|
43
|
+
self.scopes: list[Scope] | None = scopes
|
|
44
|
+
|
|
45
|
+
def _extract_raw_token(self, request: Request) -> str:
|
|
46
|
+
"""Extract the raw token from the request.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
request (Request): FastAPI request object.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
str: Raw token.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
HTTPException: If the token is missing or invalid.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
authorization_header: str | None = request.headers.get("Authorization")
|
|
59
|
+
except (AttributeError, KeyError) as e:
|
|
60
|
+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Missing Credentials") from e
|
|
61
|
+
|
|
62
|
+
if not authorization_header:
|
|
63
|
+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Missing Credentials")
|
|
64
|
+
|
|
65
|
+
if not authorization_header.startswith("Bearer "):
|
|
66
|
+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Invalid Credentials")
|
|
67
|
+
|
|
68
|
+
return authorization_header.split(sep=" ")[1]
|
|
69
|
+
|
|
70
|
+
async def _decode_jwt(self, jwt_raw: str) -> JWTBearerDecoded:
|
|
71
|
+
"""Decode the JWT token.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
jwt_raw (str): Raw JWT token.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
JWTBearerDecoded: Decoded JWT token.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
HTTPException: If the token is invalid or expired.
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
jwt_decoded: dict[str, Any] = cast(
|
|
84
|
+
dict[str, Any],
|
|
85
|
+
jwt.decode(
|
|
86
|
+
jwt=jwt_raw,
|
|
87
|
+
algorithms=["HS256", "RS256"],
|
|
88
|
+
options={"verify_signature": True},
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
return JWTBearerDecoded(**jwt_decoded)
|
|
92
|
+
except jwt.ExpiredSignatureError as e:
|
|
93
|
+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Token expired") from e
|
|
94
|
+
except jwt.InvalidTokenError as e:
|
|
95
|
+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Invalid token") from e
|
|
96
|
+
except pydantic.ValidationError as e:
|
|
97
|
+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=f"Invalid token: {e.json()}") from e
|
|
98
|
+
|
|
99
|
+
async def _verify(self, jwt_raw: str) -> None:
|
|
100
|
+
"""Verify the JWT token.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
jwt_raw (str): Raw JWT token.
|
|
104
|
+
"""
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
def _has_scope(self, jwt_decoded: JWTBearerDecoded) -> None:
|
|
108
|
+
"""Check if the JWT token has the required scope.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
jwt_decoded (JWTBearerDecoded): Decoded JWT token.
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
# Just Authentication (no scopes, no authorization)
|
|
115
|
+
if not self.scopes:
|
|
116
|
+
return
|
|
117
|
+
# JWT without scopes (no authorization)
|
|
118
|
+
if not jwt_decoded.scopes:
|
|
119
|
+
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Unauthorized")
|
|
120
|
+
# Check if all required scopes are present
|
|
121
|
+
if not all(scope in jwt_decoded.scopes for scope in (self.scopes or [])):
|
|
122
|
+
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Unauthorized")
|
|
123
|
+
|
|
124
|
+
# All scopes are valid (authorization)
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
async def __call__(self, request: Request | None = None) -> JWTBearerDecoded:
|
|
128
|
+
"""Call the JWT bearer authentication.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
request (Request): FastAPI request object.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
JWTBearerDecoded: Decoded JWT token.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
HTTPException: If the token is missing or invalid.
|
|
138
|
+
"""
|
|
139
|
+
# Ensure that the jwt will be provided
|
|
140
|
+
# by the request or by the jwt parameter
|
|
141
|
+
if self.jwt_raw is None and request is None:
|
|
142
|
+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="Missing Credentials")
|
|
143
|
+
jwt_raw: str = (
|
|
144
|
+
self.jwt_raw
|
|
145
|
+
if self.jwt_raw is not None
|
|
146
|
+
else self._extract_raw_token(request=request) # type: ignore[arg-type]
|
|
147
|
+
)
|
|
148
|
+
# Execute the io bound and cpu bound tasks in parallel
|
|
149
|
+
async with TaskGroup() as tg:
|
|
150
|
+
# TODO: Can be disabled by configuration (for operation purposes)
|
|
151
|
+
# Ensure that the jwt is not revoked or expired
|
|
152
|
+
tg.create_task(self._verify(jwt_raw=jwt_raw), name="verify_jwt")
|
|
153
|
+
# Ensure that the jwt is not altered or expired
|
|
154
|
+
task_decode: Task[Any] = tg.create_task(self._decode_jwt(jwt_raw=jwt_raw), name="decode_jwt")
|
|
155
|
+
# Scope Validation
|
|
156
|
+
jwt_decoded: JWTBearerDecoded = task_decode.result()
|
|
157
|
+
self._has_scope(jwt_decoded=jwt_decoded)
|
|
158
|
+
return jwt_decoded
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Provide Kratos Session and Identity classes."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from fastapi import Depends, HTTPException, Request
|
|
6
|
+
|
|
7
|
+
from fastapi_factory_utilities.core.services.kratos import (
|
|
8
|
+
KratosOperationError,
|
|
9
|
+
KratosService,
|
|
10
|
+
KratosSessionInvalidError,
|
|
11
|
+
KratosSessionObject,
|
|
12
|
+
depends_kratos_service,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class KratosSessionAuthentication:
|
|
17
|
+
"""Kratos Session class."""
|
|
18
|
+
|
|
19
|
+
DEFAULT_COOKIE_NAME: str = "ory_kratos_session"
|
|
20
|
+
|
|
21
|
+
def __init__(self, cookie_name: str = DEFAULT_COOKIE_NAME) -> None:
|
|
22
|
+
"""Initialize the KratosSessionAuthentication class.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
cookie_name (str): Name of the cookie to extract the session
|
|
26
|
+
"""
|
|
27
|
+
self._cookie_name: str = cookie_name
|
|
28
|
+
|
|
29
|
+
def _extract_cookie(self, request: Request) -> str:
|
|
30
|
+
"""Extract the cookie from the request.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
request (Request): FastAPI request object.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
str | None: Cookie value or None if not found.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
HTTPException: If the cookie is missing.
|
|
40
|
+
"""
|
|
41
|
+
cookie: str | None = request.cookies.get(self._cookie_name)
|
|
42
|
+
if not cookie:
|
|
43
|
+
raise HTTPException(
|
|
44
|
+
status_code=401,
|
|
45
|
+
detail="Missing Credentials",
|
|
46
|
+
)
|
|
47
|
+
return cookie
|
|
48
|
+
|
|
49
|
+
async def __call__(
|
|
50
|
+
self, request: Request, kratos_service: Annotated[KratosService, Depends(depends_kratos_service)]
|
|
51
|
+
) -> KratosSessionObject:
|
|
52
|
+
"""Extract the Kratos session from the request.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
request (Request): FastAPI request object.
|
|
56
|
+
kratos_service (KratosService): Kratos service object.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
KratosSessionObject: Kratos session object.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
HTTPException: If the session is invalid.
|
|
63
|
+
"""
|
|
64
|
+
cookie: str = self._extract_cookie(request)
|
|
65
|
+
try:
|
|
66
|
+
session: KratosSessionObject = await kratos_service.whoami(cookie_value=cookie)
|
|
67
|
+
except KratosSessionInvalidError as e:
|
|
68
|
+
raise HTTPException(
|
|
69
|
+
status_code=401,
|
|
70
|
+
detail="Invalid Credentials",
|
|
71
|
+
) from e
|
|
72
|
+
except KratosOperationError as e:
|
|
73
|
+
raise HTTPException(
|
|
74
|
+
status_code=500,
|
|
75
|
+
detail="Internal Server Error",
|
|
76
|
+
) from e
|
|
77
|
+
|
|
78
|
+
return session
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Kratos service module."""
|
|
2
|
+
|
|
3
|
+
from .exceptions import KratosOperationError, KratosSessionInvalidError
|
|
4
|
+
from .objects import KratosSessionObject
|
|
5
|
+
from .services import KratosService, depends_kratos_service
|
|
6
|
+
|
|
7
|
+
__all__: list[str] = [
|
|
8
|
+
"KratosOperationError",
|
|
9
|
+
"KratosService",
|
|
10
|
+
"KratosSessionInvalidError",
|
|
11
|
+
"KratosSessionObject",
|
|
12
|
+
"depends_kratos_service",
|
|
13
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Python exceptions for the Kratos service."""
|
|
2
|
+
|
|
3
|
+
from fastapi_factory_utilities.core.exceptions import FastAPIFactoryUtilitiesError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class KratosError(FastAPIFactoryUtilitiesError):
|
|
7
|
+
"""Base class for all exceptions raised by the Kratos service."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class KratosOperationError(KratosError):
|
|
11
|
+
"""Exception raised when a Kratos operation fails."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class KratosSessionInvalidError(KratosOperationError):
|
|
15
|
+
"""Exception raised when a Kratos session is invalid."""
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Provides the Kratos Objects."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import ClassVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
|
|
9
|
+
from .enums import AuthenticatorAssuranceLevelEnum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class KratosTraitsObject(BaseModel):
|
|
13
|
+
"""Traits for Kratos."""
|
|
14
|
+
|
|
15
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(extra="ignore")
|
|
16
|
+
|
|
17
|
+
email: str
|
|
18
|
+
username: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class KratosIdentityObject(BaseModel):
|
|
22
|
+
"""Identity for Kratos."""
|
|
23
|
+
|
|
24
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(extra="ignore")
|
|
25
|
+
|
|
26
|
+
id: uuid.UUID
|
|
27
|
+
state: str
|
|
28
|
+
traits: KratosTraitsObject
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class KratosSessionObject(BaseModel):
|
|
32
|
+
"""Session object for Kratos."""
|
|
33
|
+
|
|
34
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(extra="ignore")
|
|
35
|
+
|
|
36
|
+
id: uuid.UUID
|
|
37
|
+
active: bool
|
|
38
|
+
issued_at: datetime.datetime
|
|
39
|
+
expires_at: datetime.datetime
|
|
40
|
+
authenticated_at: datetime.datetime
|
|
41
|
+
authenticator_assurance_level: AuthenticatorAssuranceLevelEnum
|
|
42
|
+
identity: KratosIdentityObject
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Provides the KratosService class for handling Kratos operations."""
|
|
2
|
+
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
from fastapi import Depends
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
from fastapi_factory_utilities.core.app import (
|
|
11
|
+
DependencyConfig,
|
|
12
|
+
HttpServiceDependencyConfig,
|
|
13
|
+
depends_dependency_config,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .exceptions import KratosOperationError, KratosSessionInvalidError
|
|
17
|
+
from .objects import KratosSessionObject
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class KratosService:
|
|
21
|
+
"""Service class for handling Kratos operations."""
|
|
22
|
+
|
|
23
|
+
COOKIE_NAME: str = "ory_kratos_session"
|
|
24
|
+
|
|
25
|
+
def __init__(self, kratos_http_config: HttpServiceDependencyConfig) -> None:
|
|
26
|
+
"""Initialize the KratosService class.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
kratos_http_config (HttpServiceDependencyConfig): Kratos HTTP configuration.
|
|
30
|
+
"""
|
|
31
|
+
self._http_config: HttpServiceDependencyConfig = kratos_http_config
|
|
32
|
+
|
|
33
|
+
async def whoami(self, cookie_value: str) -> KratosSessionObject:
|
|
34
|
+
"""Get the current user session.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
cookie_value (str): Cookie value.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
KratosSessionObject: Kratos session object.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
KratosOperationError: If the Kratos service returns an error.
|
|
44
|
+
KratosSessionInvalidError: If the Kratos session is invalid.
|
|
45
|
+
"""
|
|
46
|
+
cookies: dict[str, str] = {self.COOKIE_NAME: cookie_value}
|
|
47
|
+
async with aiohttp.ClientSession(base_url=str(self._http_config.url), cookies=cookies) as session:
|
|
48
|
+
async with session.get(
|
|
49
|
+
url="/sessions/whoami",
|
|
50
|
+
) as response:
|
|
51
|
+
if response.status >= HTTPStatus.INTERNAL_SERVER_ERROR.value:
|
|
52
|
+
raise KratosOperationError(message=f"Kratos service error: {response.status} - {response.reason}")
|
|
53
|
+
if response.status == HTTPStatus.UNAUTHORIZED:
|
|
54
|
+
raise KratosSessionInvalidError(
|
|
55
|
+
message=f"Kratos session invalid: {response.status} - {response.reason}"
|
|
56
|
+
)
|
|
57
|
+
if response.status != HTTPStatus.OK:
|
|
58
|
+
raise KratosOperationError(message=f"Kratos service error: {response.status} - {response.reason}")
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
kratos_session: KratosSessionObject = KratosSessionObject(**await response.json())
|
|
62
|
+
except ValidationError as e:
|
|
63
|
+
raise KratosOperationError(message=f"Kratos service error: {e}") from e
|
|
64
|
+
|
|
65
|
+
return kratos_session
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def depends_kratos_service(
|
|
69
|
+
dependency_config: Annotated[DependencyConfig, Depends(depends_dependency_config)],
|
|
70
|
+
) -> KratosService:
|
|
71
|
+
"""Dependency function to get the Kratos service instance.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
dependency_config (DependencyConfig): Dependency configuration.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
KratosService: Kratos service instance.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
KratosOperationError: If the Kratos dependency is not configured.
|
|
81
|
+
"""
|
|
82
|
+
if dependency_config.kratos is None:
|
|
83
|
+
raise KratosOperationError(message="Kratos dependency not configured")
|
|
84
|
+
return KratosService(
|
|
85
|
+
kratos_http_config=dependency_config.kratos,
|
|
86
|
+
)
|
{fastapi_factory_utilities-0.2.11.dist-info → fastapi_factory_utilities-0.3.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: fastapi_factory_utilities
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Consolidate libraries and utilities to create microservices in Python with FastAPI, Beanie, Httpx, AioPika and OpenTelemetry.
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: python,fastapi,beanie,httpx,opentelemetry,microservices
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Dist: aiohttp[speedups] (>=3.11.16,<4.0.0)
|
|
21
22
|
Requires-Dist: beanie (>=1.27.0,<2.0.0)
|
|
22
23
|
Requires-Dist: fastapi (>=0.115.4,<0.116.0)
|
|
23
24
|
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
@@ -29,6 +30,7 @@ Requires-Dist: opentelemetry-propagator-b3 (>=1.26.0,<2.0.0)
|
|
|
29
30
|
Requires-Dist: opentelemetry-sdk (>=1.26.0,<2.0.0)
|
|
30
31
|
Requires-Dist: pyaml (>=25.1.0,<26.0.0)
|
|
31
32
|
Requires-Dist: pydantic (>=2.8.2,<3.0.0)
|
|
33
|
+
Requires-Dist: pyjwt (>=2.10.1,<3.0.0)
|
|
32
34
|
Requires-Dist: pymongo (>=4.9.2,<4.10.0)
|
|
33
35
|
Requires-Dist: reactivex (>=4.0.4,<5.0.0)
|
|
34
36
|
Requires-Dist: structlog (>=24.1,<26.0)
|
{fastapi_factory_utilities-0.2.11.dist-info → fastapi_factory_utilities-0.3.0.dist-info}/RECORD
RENAMED
|
@@ -8,7 +8,7 @@ fastapi_factory_utilities/core/api/v1/sys/readiness.py,sha256=xIY8pQLShU7KWRtlOU
|
|
|
8
8
|
fastapi_factory_utilities/core/app/__init__.py,sha256=I04abOkkWiY9ChgkUNeiwnuWq8kkBoRAtMLBUKB7J7Y,405
|
|
9
9
|
fastapi_factory_utilities/core/app/application.py,sha256=WrDXh00r_jzQTtZGeFO43lIzvDraplitejTaSJj_uFE,5091
|
|
10
10
|
fastapi_factory_utilities/core/app/builder.py,sha256=VbThqoI1qWnADwPQ61D774oNZ5d6OMxW0tyXr_Yz5E4,4503
|
|
11
|
-
fastapi_factory_utilities/core/app/config.py,sha256=
|
|
11
|
+
fastapi_factory_utilities/core/app/config.py,sha256=81KYoxB14TX05jMWujPNjLI1BXkvMnUyw2VgF7nJIE0,7063
|
|
12
12
|
fastapi_factory_utilities/core/app/enums.py,sha256=X1upnaehYU0eHExXTde5xsH-pI9q7HZDNsOEF5PApdg,226
|
|
13
13
|
fastapi_factory_utilities/core/app/exceptions.py,sha256=tQDf0_4j5xgCbku7TL7JaZGs3_bjsWG2YLBCydQJpPw,664
|
|
14
14
|
fastapi_factory_utilities/core/app/fastapi_builder.py,sha256=DgIqiCnJK6cqsG-sg4H7Pi0lkhaxOhSLQt_ksHjpjW0,2835
|
|
@@ -25,13 +25,20 @@ fastapi_factory_utilities/core/plugins/odm_plugin/configs.py,sha256=zQoJC1wLNyq2
|
|
|
25
25
|
fastapi_factory_utilities/core/plugins/odm_plugin/depends.py,sha256=OcLsfTLzMBk_xFV6qsMy_-qFkiphEbbEuaHUooagxg8,730
|
|
26
26
|
fastapi_factory_utilities/core/plugins/odm_plugin/documents.py,sha256=BFQYHxHBmTacJRfhZi2OffvT_RAFvAAiDVQAa_d6Y7w,1141
|
|
27
27
|
fastapi_factory_utilities/core/plugins/odm_plugin/exceptions.py,sha256=acnKJB0lGAzDs-7-LjBap8shjP3iV1a7dw7ouPVF27o,551
|
|
28
|
-
fastapi_factory_utilities/core/plugins/odm_plugin/repositories.py,sha256=
|
|
28
|
+
fastapi_factory_utilities/core/plugins/odm_plugin/repositories.py,sha256=r880o--iVj3oK3yTenM5kgJwfFWKSUZa0bfnEwOukFE,8705
|
|
29
29
|
fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py,sha256=UsXPjiAASn5GIHW8vrF32mklxGNq8ajILV-ty4K1Tbs,4371
|
|
30
|
-
fastapi_factory_utilities/core/plugins/opentelemetry_plugin/builder.py,sha256=
|
|
31
|
-
fastapi_factory_utilities/core/plugins/opentelemetry_plugin/configs.py,sha256=
|
|
30
|
+
fastapi_factory_utilities/core/plugins/opentelemetry_plugin/builder.py,sha256=9npQImifYAbEg0lFG7KwZ8V78SNrPoaINgd8vKitdMw,12509
|
|
31
|
+
fastapi_factory_utilities/core/plugins/opentelemetry_plugin/configs.py,sha256=pMG9leMB7rtdkdGFLIxXflV7bf9epGrrYPt2N97KZcM,3750
|
|
32
32
|
fastapi_factory_utilities/core/plugins/opentelemetry_plugin/exceptions.py,sha256=CpsHayfQpP0zghN8y5PP6TBy-cXhHoNxBR--I86gAdE,327
|
|
33
33
|
fastapi_factory_utilities/core/plugins/opentelemetry_plugin/helpers.py,sha256=qpTIzX67orJz7vy6SBIwRs24omMBoToJkhpurZRjPuk,1533
|
|
34
34
|
fastapi_factory_utilities/core/protocols.py,sha256=TzZKr_KfmTphk2LL-TD2XzxNlLbihbGM2DxWMhc5lEQ,2428
|
|
35
|
+
fastapi_factory_utilities/core/security/jwt.py,sha256=LuNVmTlONrmoKl7ghNv5JHV4qzMNOwuxJQlWgGSvBoo,5631
|
|
36
|
+
fastapi_factory_utilities/core/security/kratos.py,sha256=o6fkl2KfSFT7W_uDAwlLiVFl53r-neL3LQ45H3VqEIM,2350
|
|
37
|
+
fastapi_factory_utilities/core/services/kratos/__init__.py,sha256=DaC29-Ol0WR5vX56IHLGDXP9UrhISq0Juhg_sJTasw4,368
|
|
38
|
+
fastapi_factory_utilities/core/services/kratos/enums.py,sha256=ULJppowlZbOjdnUIXQyI4_nHmHZoNnv7-M1CYQBYXFY,220
|
|
39
|
+
fastapi_factory_utilities/core/services/kratos/exceptions.py,sha256=xAX01-lQvPpADgcwhB5YWSy1UqAxG38s2rlU9AJBJd8,472
|
|
40
|
+
fastapi_factory_utilities/core/services/kratos/objects.py,sha256=Dq9aEuwgwz21oLqJ4VWxrBB6ASJ7Xp_MEdyA3UbzWVE,956
|
|
41
|
+
fastapi_factory_utilities/core/services/kratos/services.py,sha256=RFKrME6M6omWiHORMBCSyvIG0BsimQaCfnmtsOdFfsg,3158
|
|
35
42
|
fastapi_factory_utilities/core/services/status/__init__.py,sha256=N5H0cCN9ZFu_0YZar4RLdCDEjKMICrIhNtfKgB0LI78,370
|
|
36
43
|
fastapi_factory_utilities/core/services/status/enums.py,sha256=IUxWAd0Ecknri4BqzaqoDRRhT_8LdcgtQcNqgNVDXGE,599
|
|
37
44
|
fastapi_factory_utilities/core/services/status/exceptions.py,sha256=_fQFGqHKnG54Hs-ZtC4gs0xwzSH246_WwQOonraoGKw,856
|
|
@@ -64,8 +71,8 @@ fastapi_factory_utilities/example/models/books/repository.py,sha256=7K63uAsSEGZ2
|
|
|
64
71
|
fastapi_factory_utilities/example/services/books/__init__.py,sha256=Z06yNRoA7Zg3TGN-Q9rrvJg6Bbx-qJw661MVwukV6vQ,148
|
|
65
72
|
fastapi_factory_utilities/example/services/books/services.py,sha256=-x7d4hotUWLzWo5uImMjFmtNcSTHwWv2bfttIbYYKbA,5380
|
|
66
73
|
fastapi_factory_utilities/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
67
|
-
fastapi_factory_utilities-0.
|
|
68
|
-
fastapi_factory_utilities-0.
|
|
69
|
-
fastapi_factory_utilities-0.
|
|
70
|
-
fastapi_factory_utilities-0.
|
|
71
|
-
fastapi_factory_utilities-0.
|
|
74
|
+
fastapi_factory_utilities-0.3.0.dist-info/LICENSE,sha256=iO1nLzMMst6vEiqgSUrfrbetM7b0bvdzXhbed5tqG8o,1074
|
|
75
|
+
fastapi_factory_utilities-0.3.0.dist-info/METADATA,sha256=Leqepw4j2B1UqEphj5J1M-w_miwtf0RX7KQql4Q_WN4,3477
|
|
76
|
+
fastapi_factory_utilities-0.3.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
77
|
+
fastapi_factory_utilities-0.3.0.dist-info/entry_points.txt,sha256=IK0VcBexXo4uXQmTrbfhhnnfq4GmXPRn0GBB8hzlsq4,101
|
|
78
|
+
fastapi_factory_utilities-0.3.0.dist-info/RECORD,,
|
{fastapi_factory_utilities-0.2.11.dist-info → fastapi_factory_utilities-0.3.0.dist-info}/LICENSE
RENAMED
|
File without changes
|
{fastapi_factory_utilities-0.2.11.dist-info → fastapi_factory_utilities-0.3.0.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|