alpha-python 0.6.2__py3-none-any.whl → 0.7.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.
- alpha/__init__.py +16 -0
- alpha/encoder.py +11 -5
- alpha/factories/response_factory.py +11 -4
- alpha/handlers/templates/python-flask/controller.mustache +12 -6
- alpha/infra/connectors/ldap_connector.py +25 -2
- alpha/interfaces/__init__.py +2 -0
- alpha/interfaces/http_client.py +26 -0
- alpha/interfaces/providers.py +6 -2
- alpha/interfaces/refresh_repository.py +60 -0
- alpha/providers/models/token.py +47 -9
- alpha/repositories/__init__.py +16 -0
- alpha/repositories/refresh/__init__.py +19 -0
- alpha/repositories/refresh/cache_repository.py +54 -0
- alpha/repositories/refresh/database_repository.py +145 -0
- alpha/repositories/refresh/file_repository.py +167 -0
- alpha/repositories/refresh/memory_repository.py +122 -0
- alpha/repositories/rest_api_repository.py +33 -19
- alpha/services/authentication_service.py +98 -405
- alpha/utils/openapi_test/container.py +10 -2
- alpha/utils/openapi_test/models.py +48 -10
- alpha/utils/response_object.py +90 -23
- {alpha_python-0.6.2.dist-info → alpha_python-0.7.0.dist-info}/METADATA +1 -1
- {alpha_python-0.6.2.dist-info → alpha_python-0.7.0.dist-info}/RECORD +27 -20
- {alpha_python-0.6.2.dist-info → alpha_python-0.7.0.dist-info}/WHEEL +0 -0
- {alpha_python-0.6.2.dist-info → alpha_python-0.7.0.dist-info}/entry_points.txt +0 -0
- {alpha_python-0.6.2.dist-info → alpha_python-0.7.0.dist-info}/licenses/LICENSE +0 -0
- {alpha_python-0.6.2.dist-info → alpha_python-0.7.0.dist-info}/top_level.txt +0 -0
alpha/__init__.py
CHANGED
|
@@ -64,6 +64,18 @@ from alpha.providers.oidc_provider import (
|
|
|
64
64
|
from alpha.repositories.models.repository_model import RepositoryModel
|
|
65
65
|
from alpha.repositories.rest_api_repository import RestApiRepository
|
|
66
66
|
from alpha.repositories.sql_alchemy_repository import SqlAlchemyRepository
|
|
67
|
+
from alpha.repositories.refresh.cache_repository import (
|
|
68
|
+
CacheRefreshRepository,
|
|
69
|
+
)
|
|
70
|
+
from alpha.repositories.refresh.database_repository import (
|
|
71
|
+
DatabaseRefreshRepository,
|
|
72
|
+
)
|
|
73
|
+
from alpha.repositories.refresh.file_repository import (
|
|
74
|
+
FileRefreshRepository,
|
|
75
|
+
)
|
|
76
|
+
from alpha.repositories.refresh.memory_repository import (
|
|
77
|
+
MemoryRefreshRepository,
|
|
78
|
+
)
|
|
67
79
|
from alpha.services.authentication_service import AuthenticationService
|
|
68
80
|
from alpha.services.user_lifecycle_management import UserLifecycleManagement
|
|
69
81
|
from alpha.utils.is_attrs import is_attrs
|
|
@@ -156,6 +168,10 @@ __all__ = [
|
|
|
156
168
|
"RepositoryModel",
|
|
157
169
|
"RestApiRepository",
|
|
158
170
|
"SqlAlchemyRepository",
|
|
171
|
+
"CacheRefreshRepository",
|
|
172
|
+
"DatabaseRefreshRepository",
|
|
173
|
+
"FileRefreshRepository",
|
|
174
|
+
"MemoryRefreshRepository",
|
|
159
175
|
"AuthenticationService",
|
|
160
176
|
"UserLifecycleManagement",
|
|
161
177
|
"is_attrs",
|
alpha/encoder.py
CHANGED
|
@@ -4,6 +4,7 @@ complex types into JSON format.
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
from dataclasses import asdict, is_dataclass
|
|
7
|
+
from attrs import asdict as attrs_asdict
|
|
7
8
|
from datetime import date, datetime, time
|
|
8
9
|
from enum import Enum
|
|
9
10
|
from json import encoder
|
|
@@ -14,8 +15,9 @@ import numpy as np # type: ignore[import-untyped]
|
|
|
14
15
|
import pandas as pd # type: ignore[import-untyped]
|
|
15
16
|
import six # type: ignore[import-untyped]
|
|
16
17
|
|
|
17
|
-
|
|
18
18
|
from alpha.interfaces.openapi_model import OpenAPIModel
|
|
19
|
+
from alpha.utils.is_attrs import is_attrs
|
|
20
|
+
from alpha.utils.is_pydantic import is_pydantic
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
class JSONEncoder(encoder.JSONEncoder):
|
|
@@ -72,11 +74,15 @@ class JSONEncoder(encoder.JSONEncoder):
|
|
|
72
74
|
return o.isoformat()
|
|
73
75
|
if isinstance(o, time):
|
|
74
76
|
return o.isoformat()
|
|
77
|
+
if isinstance(o, type):
|
|
78
|
+
cls = getattr(o, "__name__", None)
|
|
79
|
+
return cls if cls is not None else str(o)
|
|
75
80
|
if is_dataclass(o):
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
return asdict(o) # type: ignore
|
|
82
|
+
if is_attrs(o):
|
|
83
|
+
return attrs_asdict(o)
|
|
84
|
+
if is_pydantic(o):
|
|
85
|
+
return o.model_dump()
|
|
80
86
|
|
|
81
87
|
try:
|
|
82
88
|
return json.JSONEncoder.default(self, o)
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
import json
|
|
4
4
|
from dataclasses import MISSING, is_dataclass
|
|
5
5
|
from enum import Enum
|
|
6
|
-
from typing import Any,
|
|
6
|
+
from typing import Any, Sequence, get_args, get_origin
|
|
7
7
|
|
|
8
8
|
from alpha import exceptions
|
|
9
9
|
from alpha.encoder import JSONEncoder
|
|
10
|
+
from alpha.interfaces.attrs_instance import AttrsInstance
|
|
10
11
|
from alpha.interfaces.dataclass_instance import DataclassInstance
|
|
11
12
|
from alpha.interfaces.openapi_model import OpenAPIModel
|
|
13
|
+
from alpha.interfaces.pydantic_instance import PydanticInstance
|
|
12
14
|
from alpha.utils.is_attrs import is_attrs
|
|
13
15
|
from alpha.utils.is_pydantic import is_pydantic
|
|
14
16
|
|
|
@@ -18,8 +20,13 @@ class ResponseFactory:
|
|
|
18
20
|
|
|
19
21
|
def process(
|
|
20
22
|
self,
|
|
21
|
-
response: DataclassInstance
|
|
22
|
-
|
|
23
|
+
response: DataclassInstance
|
|
24
|
+
| Sequence[DataclassInstance]
|
|
25
|
+
| AttrsInstance
|
|
26
|
+
| Sequence[AttrsInstance]
|
|
27
|
+
| PydanticInstance
|
|
28
|
+
| Sequence[PydanticInstance],
|
|
29
|
+
cls: OpenAPIModel | Sequence[OpenAPIModel],
|
|
23
30
|
) -> object:
|
|
24
31
|
"""Mapping a dataclass instance or a collection of instances to an
|
|
25
32
|
OpenAPI model
|
|
@@ -54,7 +61,7 @@ class ResponseFactory:
|
|
|
54
61
|
|
|
55
62
|
# When the source instance and target class are of an iterable type
|
|
56
63
|
if cls_origin in [list, tuple, set]:
|
|
57
|
-
if isinstance(response,
|
|
64
|
+
if isinstance(response, Sequence):
|
|
58
65
|
arg = get_args(cls)[0]
|
|
59
66
|
return [
|
|
60
67
|
self.process(response=obj, cls=arg) for obj in response
|
|
@@ -12,6 +12,7 @@ from typing import List
|
|
|
12
12
|
from alpha.factories.request_factory import RequestFactory
|
|
13
13
|
from alpha.factories.response_factory import ResponseFactory
|
|
14
14
|
from alpha.encoder import JSONEncoder
|
|
15
|
+
from alpha.providers.models.identity import Identity
|
|
15
16
|
from alpha.utils.logging_level_checker import logging_level_checker
|
|
16
17
|
from alpha.utils.response_object import create_response_object
|
|
17
18
|
from alpha.utils.request_headers import Headers
|
|
@@ -227,7 +228,8 @@ def {{operationId}}(
|
|
|
227
228
|
token_factory.validate(auth_token)
|
|
228
229
|
|
|
229
230
|
# Get auth token payload and verify identity
|
|
230
|
-
|
|
231
|
+
payload = token_factory.get_payload(auth_token)
|
|
232
|
+
identity = Identity.from_dict(payload)
|
|
231
233
|
verify_identity(identity,
|
|
232
234
|
roles=roles,
|
|
233
235
|
groups=groups,
|
|
@@ -307,8 +309,9 @@ def {{operationId}}(
|
|
|
307
309
|
status_code={{code}},
|
|
308
310
|
status_message='{{message}}',
|
|
309
311
|
data=result,
|
|
310
|
-
{{#vendorExtensions.x-content-type}}
|
|
311
|
-
{{#vendorExtensions.x-
|
|
312
|
+
accept_header={{#vendorExtensions.x-content-type}}'{{vendorExtensions.x-content-type}}'{{/vendorExtensions.x-content-type}}{{^vendorExtensions.x-content-type}}connexion.request.headers.get("Accept", None){{/vendorExtensions.x-content-type}},
|
|
313
|
+
supported_accept_headers=[{{#produces}}'{{mediaType}}',{{/produces}}{{^produces}}{{#vendorExtensions.x-preferred-produce}}'{{mediaType}}',{{/vendorExtensions.x-preferred-produce}}{{/produces}}],
|
|
314
|
+
{{#vendorExtensions.x-alpha-cookie-support}}response_format='flask'{{/vendorExtensions.x-alpha-cookie-support}}
|
|
312
315
|
)
|
|
313
316
|
{{/returnType}}
|
|
314
317
|
{{^returnType}}
|
|
@@ -316,19 +319,20 @@ def {{operationId}}(
|
|
|
316
319
|
http_codes=http_codes,
|
|
317
320
|
status_code=204,
|
|
318
321
|
status_message='{{message}}',
|
|
319
|
-
{{#vendorExtensions.x-alpha-cookie-support}}data=result,
|
|
322
|
+
{{#vendorExtensions.x-alpha-cookie-support}}data=result, response_format='flask'{{/vendorExtensions.x-alpha-cookie-support}}
|
|
320
323
|
)
|
|
321
324
|
{{/returnType}}
|
|
322
325
|
{{/vendorExtensions.x-alpha-raw-response}}
|
|
323
326
|
{{/is2xx}}
|
|
324
|
-
|
|
325
327
|
{{#is4xx}}
|
|
326
328
|
{{#vendorExtensions.x-alpha-exception}}
|
|
327
329
|
except {{vendorExtensions.x-alpha-exception}} as exc:
|
|
328
330
|
return response_object_function(
|
|
329
331
|
http_codes=http_codes,
|
|
330
332
|
status_code={{code}},
|
|
331
|
-
status_message=f'{exc}'
|
|
333
|
+
status_message=f'{exc}',
|
|
334
|
+
accept_header={{#vendorExtensions.x-content-type}}'{{vendorExtensions.x-content-type}}'{{/vendorExtensions.x-content-type}}{{^vendorExtensions.x-content-type}}connexion.request.headers.get("Accept", None){{/vendorExtensions.x-content-type}},
|
|
335
|
+
supported_accept_headers=[{{#produces}}'{{mediaType}}',{{/produces}}{{^produces}}{{#vendorExtensions.x-preferred-produce}}'{{mediaType}}',{{/vendorExtensions.x-preferred-produce}}{{/produces}}],
|
|
332
336
|
)
|
|
333
337
|
{{/vendorExtensions.x-alpha-exception}}
|
|
334
338
|
{{/is4xx}}
|
|
@@ -339,6 +343,8 @@ def {{operationId}}(
|
|
|
339
343
|
http_codes=http_codes,
|
|
340
344
|
status_code={{code}},
|
|
341
345
|
status_message=f'{exc}'
|
|
346
|
+
accept_header={{#vendorExtensions.x-content-type}}'{{vendorExtensions.x-content-type}}'{{/vendorExtensions.x-content-type}}{{^vendorExtensions.x-content-type}}connexion.request.headers.get("Accept", None){{/vendorExtensions.x-content-type}},
|
|
347
|
+
supported_accept_headers=[{{#produces}}'{{mediaType}}',{{/produces}}{{^produces}}{{#vendorExtensions.x-preferred-produce}}'{{mediaType}}',{{/vendorExtensions.x-preferred-produce}}{{/produces}}],
|
|
342
348
|
)
|
|
343
349
|
{{/vendorExtensions.x-alpha-exception}}
|
|
344
350
|
{{/is5xx}}
|
|
@@ -31,6 +31,9 @@ class LDAPConnector:
|
|
|
31
31
|
server_port: int = 636,
|
|
32
32
|
use_tls: bool = True,
|
|
33
33
|
client_strategy: ClientStrategyType = SYNC,
|
|
34
|
+
connect_timeout: float | None = 5.0,
|
|
35
|
+
additional_connector_params: dict[str, Any] | None = None,
|
|
36
|
+
additional_server_params: dict[str, Any] | None = None,
|
|
34
37
|
) -> None:
|
|
35
38
|
"""
|
|
36
39
|
Parameters
|
|
@@ -59,12 +62,29 @@ class LDAPConnector:
|
|
|
59
62
|
- 'MOCK_SYNC': Mock synchronous strategy.
|
|
60
63
|
- 'MOCK_ASYNC': Mock asynchronous strategy.
|
|
61
64
|
- 'ASYNC_STREAM': Asynchronous stream strategy.
|
|
65
|
+
connect_timeout
|
|
66
|
+
Maximum number of seconds to wait while opening the socket.
|
|
67
|
+
additional_connector_params
|
|
68
|
+
Additional parameters to pass to the LDAP connection, by default
|
|
69
|
+
{"receive_timeout": 5}
|
|
70
|
+
additional_server_params
|
|
71
|
+
Additional parameters to pass to the LDAP server, by default None
|
|
62
72
|
"""
|
|
63
73
|
self._server_url = server_url
|
|
64
74
|
self._bind_dn = bind_dn
|
|
65
75
|
self._bind_password = bind_password
|
|
66
76
|
self._client_strategy = client_strategy
|
|
67
|
-
|
|
77
|
+
self._connect_timeout = connect_timeout
|
|
78
|
+
self._additional_connector_params: dict[str, Any] = (
|
|
79
|
+
{"receive_timeout": 5}
|
|
80
|
+
if additional_connector_params is None
|
|
81
|
+
else dict(additional_connector_params)
|
|
82
|
+
)
|
|
83
|
+
self._additional_server_params: dict[str, Any] = (
|
|
84
|
+
{}
|
|
85
|
+
if additional_server_params is None
|
|
86
|
+
else dict(additional_server_params)
|
|
87
|
+
)
|
|
68
88
|
tls = None
|
|
69
89
|
if use_tls:
|
|
70
90
|
tls = Tls(
|
|
@@ -78,6 +98,8 @@ class LDAPConnector:
|
|
|
78
98
|
use_ssl=use_tls,
|
|
79
99
|
tls=tls,
|
|
80
100
|
get_info=ALL,
|
|
101
|
+
connect_timeout=self._connect_timeout,
|
|
102
|
+
**self._additional_server_params,
|
|
81
103
|
)
|
|
82
104
|
self._connection: Connection | None = None
|
|
83
105
|
|
|
@@ -97,8 +119,9 @@ class LDAPConnector:
|
|
|
97
119
|
self._server,
|
|
98
120
|
user=self._bind_dn,
|
|
99
121
|
password=self._bind_password,
|
|
100
|
-
auto_bind=True,
|
|
101
122
|
client_strategy=self._client_strategy, # type: ignore
|
|
123
|
+
auto_bind=True,
|
|
124
|
+
**self._additional_connector_params,
|
|
102
125
|
)
|
|
103
126
|
|
|
104
127
|
def disconnect(self) -> None:
|
alpha/interfaces/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from alpha.interfaces.patchable import Patchable
|
|
|
9
9
|
# import all repository related interfaces
|
|
10
10
|
from alpha.interfaces.api_repository import ApiRepository
|
|
11
11
|
from alpha.interfaces.sql_repository import SqlRepository
|
|
12
|
+
from alpha.interfaces.refresh_repository import RefreshRepository
|
|
12
13
|
|
|
13
14
|
# import all database related interfaces
|
|
14
15
|
from alpha.interfaces.sql_mapper import SqlMapper
|
|
@@ -35,6 +36,7 @@ __all__ = [
|
|
|
35
36
|
"Patchable",
|
|
36
37
|
"ApiRepository",
|
|
37
38
|
"SqlRepository",
|
|
39
|
+
"RefreshRepository",
|
|
38
40
|
"SqlMapper",
|
|
39
41
|
"SqlDatabase",
|
|
40
42
|
"UnitOfWork",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Protocol, Any, runtime_checkable
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@runtime_checkable
|
|
5
|
+
class HTTPClient(Protocol):
|
|
6
|
+
"""Interface for HTTP clients like requests, httpx or a custom
|
|
7
|
+
implementation.
|
|
8
|
+
|
|
9
|
+
This interface is compatible with the popular synchronous HTTP client
|
|
10
|
+
libraries, for example, the `requests` library, `httpx` library or any
|
|
11
|
+
custom implementation that follows the same method signatures.
|
|
12
|
+
|
|
13
|
+
This interface defines the methods that an HTTP client should implement to
|
|
14
|
+
be compatible with the REST API repository. It includes methods for making
|
|
15
|
+
HTTP requests (POST, GET, DELETE, PUT, PATCH) and allows for additional
|
|
16
|
+
parameters to be passed as needed.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
cookies: Any
|
|
20
|
+
headers: Any
|
|
21
|
+
|
|
22
|
+
def post(self, url: str, json: Any = None, **kwargs: Any) -> Any: ...
|
|
23
|
+
def get(self, url: str, **kwargs: Any) -> Any: ...
|
|
24
|
+
def delete(self, url: str, **kwargs: Any) -> Any: ...
|
|
25
|
+
def put(self, url: str, json: Any = None, **kwargs: Any) -> Any: ...
|
|
26
|
+
def patch(self, url: str, json: Any = None, **kwargs: Any) -> Any: ...
|
alpha/interfaces/providers.py
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
"""This module contains interfaces for various types of identity providers."""
|
|
2
2
|
|
|
3
|
-
from typing import ClassVar, Protocol, runtime_checkable
|
|
3
|
+
from typing import TYPE_CHECKING, ClassVar, Protocol, runtime_checkable, Any
|
|
4
4
|
|
|
5
5
|
from alpha.interfaces.token_factory import TokenFactory
|
|
6
6
|
from alpha.providers.models.credentials import PasswordCredentials
|
|
7
7
|
from alpha.providers.models.identity import Identity
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from alpha.providers.models.token import Token
|
|
11
|
+
else:
|
|
12
|
+
Token = Any
|
|
9
13
|
|
|
10
14
|
|
|
11
15
|
@runtime_checkable
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Protocol, Any
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from alpha.providers.models.token import Token
|
|
5
|
+
else:
|
|
6
|
+
Token = Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RefreshRepository(Protocol):
|
|
10
|
+
"""Repository interface for managing refresh tokens."""
|
|
11
|
+
|
|
12
|
+
def get(self, token: str) -> Token:
|
|
13
|
+
"""Get a token by its value.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
token
|
|
18
|
+
Token value.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
Token
|
|
23
|
+
Token object with the given token value.
|
|
24
|
+
"""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
def create(self, subject: str) -> Token:
|
|
28
|
+
"""Create a new token for a given subject.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
subject
|
|
33
|
+
Subject identifier
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
Token
|
|
38
|
+
Newly created token object.
|
|
39
|
+
"""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
def delete(self, token: str) -> None:
|
|
43
|
+
"""Delete a token by its value.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
token
|
|
48
|
+
Token value.
|
|
49
|
+
"""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
def delete_all(self, subject: str) -> None:
|
|
53
|
+
"""Delete all tokens for a given subject.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
subject
|
|
58
|
+
Subject identifier.
|
|
59
|
+
"""
|
|
60
|
+
...
|
alpha/providers/models/token.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from uuid import UUID
|
|
2
|
-
from datetime import datetime
|
|
2
|
+
from datetime import datetime, timedelta, timezone
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from typing import Any, Literal
|
|
5
5
|
|
|
6
6
|
from alpha.domain.models.base_model import BaseDomainModel
|
|
7
|
+
from alpha.utils.secret_generator import generate_secret
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
@dataclass
|
|
@@ -79,6 +80,40 @@ class Token(BaseDomainModel):
|
|
|
79
80
|
),
|
|
80
81
|
)
|
|
81
82
|
|
|
83
|
+
@classmethod
|
|
84
|
+
def create_refresh(
|
|
85
|
+
cls,
|
|
86
|
+
subject: str,
|
|
87
|
+
max_age_seconds: int = 7 * 24 * 3600,
|
|
88
|
+
token_length: int = 32,
|
|
89
|
+
) -> "Token":
|
|
90
|
+
"""Factory method to create a new Refresh Token instance.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
subject
|
|
95
|
+
The subject or user associated with the token.
|
|
96
|
+
max_age_seconds, optional
|
|
97
|
+
Optional maximum age of the token in seconds. Defaults to 7 days.
|
|
98
|
+
If provided, the expires_at will be set to created_at +
|
|
99
|
+
max_age_seconds.
|
|
100
|
+
token_length, optional
|
|
101
|
+
Optional length of the token value. Defaults to 32.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
Token
|
|
106
|
+
A new Refresh Token instance with the provided attributes and generated id and created_at.
|
|
107
|
+
"""
|
|
108
|
+
return cls(
|
|
109
|
+
value=generate_secret(token_length),
|
|
110
|
+
token_type="Refresh",
|
|
111
|
+
subject=subject,
|
|
112
|
+
created_at=datetime.now(tz=timezone.utc),
|
|
113
|
+
expires_at=datetime.now(tz=timezone.utc)
|
|
114
|
+
+ timedelta(seconds=max_age_seconds),
|
|
115
|
+
)
|
|
116
|
+
|
|
82
117
|
def to_dict(self) -> dict[str, str | None]:
|
|
83
118
|
"""Converts the Token instance to a dictionary.
|
|
84
119
|
|
|
@@ -97,15 +132,18 @@ class Token(BaseDomainModel):
|
|
|
97
132
|
- "expires_at": ISO format datetime string for when the token
|
|
98
133
|
expires or None.
|
|
99
134
|
"""
|
|
100
|
-
|
|
101
|
-
|
|
135
|
+
|
|
136
|
+
obj: dict[str, str | None] = {
|
|
102
137
|
"value": self.value,
|
|
103
138
|
"subject": self.subject,
|
|
104
139
|
"token_type": self.token_type,
|
|
105
|
-
"created_at": (
|
|
106
|
-
self.created_at.isoformat() if self.created_at else None
|
|
107
|
-
),
|
|
108
|
-
"expires_at": (
|
|
109
|
-
self.expires_at.isoformat() if self.expires_at else None
|
|
110
|
-
),
|
|
111
140
|
}
|
|
141
|
+
|
|
142
|
+
if self.id:
|
|
143
|
+
obj["id"] = str(self.id)
|
|
144
|
+
if self.created_at:
|
|
145
|
+
obj["created_at"] = self.created_at.isoformat()
|
|
146
|
+
if self.expires_at:
|
|
147
|
+
obj["expires_at"] = self.expires_at.isoformat()
|
|
148
|
+
|
|
149
|
+
return obj
|
alpha/repositories/__init__.py
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
from alpha.repositories.models.repository_model import RepositoryModel
|
|
2
|
+
from alpha.repositories.refresh.cache_repository import (
|
|
3
|
+
CacheRefreshRepository,
|
|
4
|
+
)
|
|
5
|
+
from alpha.repositories.refresh.database_repository import (
|
|
6
|
+
DatabaseRefreshRepository,
|
|
7
|
+
)
|
|
8
|
+
from alpha.repositories.refresh.file_repository import (
|
|
9
|
+
FileRefreshRepository,
|
|
10
|
+
)
|
|
11
|
+
from alpha.repositories.refresh.memory_repository import (
|
|
12
|
+
MemoryRefreshRepository,
|
|
13
|
+
)
|
|
2
14
|
from alpha.repositories.rest_api_repository import RestApiRepository
|
|
3
15
|
from alpha.repositories.sql_alchemy_repository import SqlAlchemyRepository
|
|
4
16
|
|
|
@@ -6,4 +18,8 @@ __all__ = [
|
|
|
6
18
|
"RepositoryModel",
|
|
7
19
|
"RestApiRepository",
|
|
8
20
|
"SqlAlchemyRepository",
|
|
21
|
+
"CacheRefreshRepository",
|
|
22
|
+
"DatabaseRefreshRepository",
|
|
23
|
+
"FileRefreshRepository",
|
|
24
|
+
"MemoryRefreshRepository",
|
|
9
25
|
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from alpha.repositories.refresh.cache_repository import (
|
|
2
|
+
CacheRefreshRepository,
|
|
3
|
+
)
|
|
4
|
+
from alpha.repositories.refresh.database_repository import (
|
|
5
|
+
DatabaseRefreshRepository,
|
|
6
|
+
)
|
|
7
|
+
from alpha.repositories.refresh.file_repository import (
|
|
8
|
+
FileRefreshRepository,
|
|
9
|
+
)
|
|
10
|
+
from alpha.repositories.refresh.memory_repository import (
|
|
11
|
+
MemoryRefreshRepository,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"CacheRefreshRepository",
|
|
16
|
+
"DatabaseRefreshRepository",
|
|
17
|
+
"FileRefreshRepository",
|
|
18
|
+
"MemoryRefreshRepository",
|
|
19
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from alpha.providers.models.token import Token
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CacheRefreshRepository:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
cache_connector: Any,
|
|
10
|
+
token_model: type[Token] = Token,
|
|
11
|
+
token_max_age_seconds: int = 7 * 24 * 3600,
|
|
12
|
+
token_length: int = 32,
|
|
13
|
+
):
|
|
14
|
+
"""Initialize the CacheRefreshRepository with the given cache connector.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
cache_connector
|
|
19
|
+
The cache connector instance to use for cache operations.
|
|
20
|
+
token_model, optional
|
|
21
|
+
The model class for tokens, by default Token. The model class
|
|
22
|
+
should have a `from_dict` class method that takes a dictionary and
|
|
23
|
+
returns an instance of the model. The dictionary will have the same
|
|
24
|
+
structure as the token data in the JSON file. The model class
|
|
25
|
+
should also have a `to_dict` method that converts an instance of
|
|
26
|
+
the model to a dictionary with the same structure as the token data
|
|
27
|
+
in the JSON file. The model class should also have a
|
|
28
|
+
`create_refresh` class method that creates a new refresh token.
|
|
29
|
+
token_max_age_seconds, optional
|
|
30
|
+
The maximum age of a token in seconds, by default the equivalent of
|
|
31
|
+
7 days in seconds
|
|
32
|
+
token_length, optional
|
|
33
|
+
The length of the generated token string, by default 32 characters
|
|
34
|
+
"""
|
|
35
|
+
self._cache_connector = cache_connector
|
|
36
|
+
self._token_model = token_model
|
|
37
|
+
self._token_max_age_seconds = token_max_age_seconds
|
|
38
|
+
self._token_length = token_length
|
|
39
|
+
|
|
40
|
+
def get(self, token: str) -> Token:
|
|
41
|
+
"""Get a token by its value."""
|
|
42
|
+
raise NotImplementedError("Method not implemented yet.")
|
|
43
|
+
|
|
44
|
+
def create(self, subject: str) -> Token:
|
|
45
|
+
"""Create a new token for a given subject."""
|
|
46
|
+
raise NotImplementedError("Method not implemented yet.")
|
|
47
|
+
|
|
48
|
+
def delete(self, token: str) -> None:
|
|
49
|
+
"""Delete a token by its value."""
|
|
50
|
+
raise NotImplementedError("Method not implemented yet.")
|
|
51
|
+
|
|
52
|
+
def delete_all(self, subject: str) -> None:
|
|
53
|
+
"""Delete all tokens for a given subject."""
|
|
54
|
+
raise NotImplementedError("Method not implemented yet.")
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from alpha import exceptions
|
|
2
|
+
from alpha.infra.connectors.sql_alchemy import SqlAlchemyDatabase
|
|
3
|
+
from alpha.providers.models.token import Token
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DatabaseRefreshRepository:
|
|
7
|
+
"""Implementation of the RefreshRepository interface for database
|
|
8
|
+
operations.
|
|
9
|
+
|
|
10
|
+
This repository uses a SQLAlchemy database connector to manage refresh
|
|
11
|
+
tokens in a database. It provides methods to get, create, delete, and
|
|
12
|
+
delete all refresh tokens for a given subject. The tokens are stored in a
|
|
13
|
+
database table which is mapped to the token model.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
database_connector: SqlAlchemyDatabase,
|
|
19
|
+
token_model: type[Token] = Token,
|
|
20
|
+
token_max_age_seconds: int = 7 * 24 * 3600,
|
|
21
|
+
token_length: int = 32,
|
|
22
|
+
):
|
|
23
|
+
"""Initialize the DatabaseRefreshRepository with the given database
|
|
24
|
+
connector and token model.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
database_connector
|
|
29
|
+
The database connector instance to use for database operations.
|
|
30
|
+
token_model, optional
|
|
31
|
+
The model class for tokens, by default Token. The model class
|
|
32
|
+
should have a `from_dict` class method that takes a dictionary and
|
|
33
|
+
returns an instance of the model. The dictionary will have the same
|
|
34
|
+
structure as the token data in the JSON file. The model class
|
|
35
|
+
should also have a `to_dict` method that converts an instance of
|
|
36
|
+
the model to a dictionary with the same structure as the token data
|
|
37
|
+
in the JSON file. The model class should also have a
|
|
38
|
+
`create_refresh` class method that creates a new refresh token.
|
|
39
|
+
token_max_age_seconds, optional
|
|
40
|
+
The maximum age of a token in seconds, by default the equivalent of
|
|
41
|
+
7 days in seconds
|
|
42
|
+
token_length, optional
|
|
43
|
+
The length of the generated token string, by default 32 characters
|
|
44
|
+
"""
|
|
45
|
+
self._database_connector = database_connector
|
|
46
|
+
self._token_model = token_model
|
|
47
|
+
self._token_max_age_seconds = token_max_age_seconds
|
|
48
|
+
self._token_length = token_length
|
|
49
|
+
|
|
50
|
+
def get(self, token: str) -> Token:
|
|
51
|
+
"""Get a token by its value.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
token
|
|
56
|
+
The value of the token to retrieve.
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
Token
|
|
61
|
+
The token object corresponding to the given value.
|
|
62
|
+
|
|
63
|
+
Raises
|
|
64
|
+
------
|
|
65
|
+
NotFoundException
|
|
66
|
+
If the token is not found in the database.
|
|
67
|
+
"""
|
|
68
|
+
with self._database_connector.get_session() as session:
|
|
69
|
+
result = (
|
|
70
|
+
session.query(self._token_model)
|
|
71
|
+
.filter_by(value=token)
|
|
72
|
+
.one_or_none()
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if result is None:
|
|
76
|
+
raise exceptions.NotFoundException("Refresh token not found")
|
|
77
|
+
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
def create(self, subject: str) -> Token:
|
|
81
|
+
"""Create a new token for a given subject.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
subject
|
|
86
|
+
The subject for which to create the token.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
Token
|
|
91
|
+
The newly created token object.
|
|
92
|
+
"""
|
|
93
|
+
token = self._token_model.create_refresh(
|
|
94
|
+
subject=subject,
|
|
95
|
+
max_age_seconds=self._token_max_age_seconds,
|
|
96
|
+
token_length=self._token_length,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
with self._database_connector.get_session() as session:
|
|
100
|
+
session.add(token)
|
|
101
|
+
session.commit()
|
|
102
|
+
session.refresh(token)
|
|
103
|
+
return token
|
|
104
|
+
|
|
105
|
+
def delete(self, token: str) -> None:
|
|
106
|
+
"""Delete a token by its value.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
token
|
|
111
|
+
The value of the token to delete.
|
|
112
|
+
|
|
113
|
+
Raises
|
|
114
|
+
------
|
|
115
|
+
NotFoundException
|
|
116
|
+
If the token is not found in the database.
|
|
117
|
+
"""
|
|
118
|
+
with self._database_connector.get_session() as session:
|
|
119
|
+
token_obj = (
|
|
120
|
+
session.query(self._token_model)
|
|
121
|
+
.filter_by(value=token)
|
|
122
|
+
.one_or_none()
|
|
123
|
+
)
|
|
124
|
+
if token_obj is None:
|
|
125
|
+
raise exceptions.NotFoundException("Refresh token not found")
|
|
126
|
+
session.delete(token_obj)
|
|
127
|
+
session.commit()
|
|
128
|
+
|
|
129
|
+
def delete_all(self, subject: str) -> None:
|
|
130
|
+
"""Delete all tokens for a given subject.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
subject
|
|
135
|
+
The subject for which to delete all tokens.
|
|
136
|
+
"""
|
|
137
|
+
with self._database_connector.get_session() as session:
|
|
138
|
+
tokens = (
|
|
139
|
+
session.query(self._token_model)
|
|
140
|
+
.filter_by(subject=subject)
|
|
141
|
+
.all()
|
|
142
|
+
)
|
|
143
|
+
for token in tokens:
|
|
144
|
+
session.delete(token)
|
|
145
|
+
session.commit()
|