alpha-python 0.5.0__tar.gz → 0.5.1__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.
Files changed (139) hide show
  1. {alpha_python-0.5.0/src/alpha_python.egg-info → alpha_python-0.5.1}/PKG-INFO +1 -1
  2. {alpha_python-0.5.0 → alpha_python-0.5.1}/pyproject.toml +1 -1
  3. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/__init__.py +4 -0
  4. alpha_python-0.5.1/src/alpha/domain/models/user.py +130 -0
  5. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/factories/jwt_factory.py +71 -26
  6. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/factories/password_factory.py +1 -0
  7. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/controller.mustache +36 -28
  8. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/token_factory.py +6 -1
  9. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/providers/models/identity.py +1 -1
  10. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/repositories/models/repository_model.py +1 -1
  11. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/services/__init__.py +2 -0
  12. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/services/authentication_service.py +24 -13
  13. alpha_python-0.5.1/src/alpha/services/user_lifecycle_management.py +331 -0
  14. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/utils/__init__.py +2 -0
  15. alpha_python-0.5.1/src/alpha/utils/request_headers.py +108 -0
  16. {alpha_python-0.5.0 → alpha_python-0.5.1/src/alpha_python.egg-info}/PKG-INFO +1 -1
  17. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha_python.egg-info/SOURCES.txt +2 -0
  18. alpha_python-0.5.0/src/alpha/domain/models/user.py +0 -66
  19. {alpha_python-0.5.0 → alpha_python-0.5.1}/LICENSE +0 -0
  20. {alpha_python-0.5.0 → alpha_python-0.5.1}/README.md +0 -0
  21. {alpha_python-0.5.0 → alpha_python-0.5.1}/setup.cfg +0 -0
  22. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/adapters/__init__.py +0 -0
  23. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/adapters/rest_api_unit_of_work.py +0 -0
  24. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/adapters/sqla_unit_of_work.py +0 -0
  25. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/cli.py +0 -0
  26. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/containers/__init__.py +0 -0
  27. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/containers/container.py +0 -0
  28. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/domain/__init__.py +0 -0
  29. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/domain/models/__init__.py +0 -0
  30. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/domain/models/base_model.py +0 -0
  31. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/domain/models/group.py +0 -0
  32. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/domain/models/life_cycle_base.py +0 -0
  33. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/encoder.py +0 -0
  34. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/exceptions.py +0 -0
  35. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/factories/__init__.py +0 -0
  36. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/factories/_type_conversion_matrix.py +0 -0
  37. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/factories/_type_mapping.py +0 -0
  38. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/factories/class_factories.py +0 -0
  39. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/factories/default_field_factory.py +0 -0
  40. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/factories/field_iterator.py +0 -0
  41. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/factories/logging_handler_factory.py +0 -0
  42. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/factories/model_class_factory.py +0 -0
  43. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/factories/models/__init__.py +0 -0
  44. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/factories/models/factory_classes.py +0 -0
  45. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/factories/request_factory.py +0 -0
  46. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/factories/response_factory.py +0 -0
  47. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/factories/type_factories.py +0 -0
  48. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/__init__.py +0 -0
  49. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/api_generate_handler.py +0 -0
  50. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/api_run_handler.py +0 -0
  51. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/base_handler.py +0 -0
  52. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/gen-code.sh +0 -0
  53. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/models/__init__.py +0 -0
  54. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/models/argument.py +0 -0
  55. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/models/command.py +0 -0
  56. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/models/section.py +0 -0
  57. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/models/subparser.py +0 -0
  58. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/run-api.sh +0 -0
  59. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/__init__.py +0 -0
  60. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/Dockerfile.mustache +0 -0
  61. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/README.mustache +0 -0
  62. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/__init__model.mustache +0 -0
  63. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/__init__test.mustache +0 -0
  64. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/__main__.mustache +0 -0
  65. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/base_model.mustache +0 -0
  66. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/controller_test.mustache +0 -0
  67. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/dockerignore.mustache +0 -0
  68. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/encoder.mustache +0 -0
  69. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/git_push.sh.mustache +0 -0
  70. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/gitignore.mustache +0 -0
  71. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/model.mustache +0 -0
  72. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/openapi.mustache +0 -0
  73. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/param_type.mustache +0 -0
  74. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/requirements.mustache +0 -0
  75. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/security_controller_.mustache +0 -0
  76. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/setup.mustache +0 -0
  77. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/test-requirements.mustache +0 -0
  78. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/tox.mustache +0 -0
  79. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/travis.mustache +0 -0
  80. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/typing_utils.mustache +0 -0
  81. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/handlers/templates/python-flask/util.mustache +0 -0
  82. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/infra/__init__.py +0 -0
  83. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/infra/connectors/__init__.py +0 -0
  84. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/infra/connectors/ldap_connector.py +0 -0
  85. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/infra/connectors/oidc_connector.py +0 -0
  86. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/infra/databases/__init__.py +0 -0
  87. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/infra/databases/sql_alchemy.py +0 -0
  88. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/infra/models/__init__.py +0 -0
  89. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/infra/models/filter_operators.py +0 -0
  90. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/infra/models/json_patch.py +0 -0
  91. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/infra/models/order_by.py +0 -0
  92. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/infra/models/query_clause.py +0 -0
  93. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/infra/models/search_filter.py +0 -0
  94. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/__init__.py +0 -0
  95. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/api_repository.py +0 -0
  96. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/attrs_instance.py +0 -0
  97. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/dataclass_instance.py +0 -0
  98. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/factories.py +0 -0
  99. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/handler.py +0 -0
  100. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/openapi_model.py +0 -0
  101. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/patchable.py +0 -0
  102. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/providers.py +0 -0
  103. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/pydantic_instance.py +0 -0
  104. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/sql_database.py +0 -0
  105. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/sql_mapper.py +0 -0
  106. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/sql_repository.py +0 -0
  107. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/unit_of_work.py +0 -0
  108. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/interfaces/updateable.py +0 -0
  109. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/mixins/__init__.py +0 -0
  110. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/mixins/jwt_provider.py +0 -0
  111. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/providers/__init__.py +0 -0
  112. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/providers/api_key_provider.py +0 -0
  113. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/providers/database_provider.py +0 -0
  114. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/providers/ldap_provider.py +0 -0
  115. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/providers/models/__init__.py +0 -0
  116. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/providers/models/credentials.py +0 -0
  117. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/providers/models/token.py +0 -0
  118. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/providers/oidc_provider.py +0 -0
  119. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/py.typed +0 -0
  120. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/repositories/__init__.py +0 -0
  121. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/repositories/models/__init__.py +0 -0
  122. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/repositories/rest_api_repository.py +0 -0
  123. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/repositories/sql_alchemy_repository.py +0 -0
  124. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/services/models/__init__.py +0 -0
  125. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/services/models/cookie.py +0 -0
  126. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/utils/_http_codes.py +0 -0
  127. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/utils/is_attrs.py +0 -0
  128. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/utils/is_pydantic.py +0 -0
  129. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/utils/logging_configurator.py +0 -0
  130. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/utils/logging_level_checker.py +0 -0
  131. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/utils/response_object.py +0 -0
  132. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/utils/secret_generator.py +0 -0
  133. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/utils/verify_identity.py +0 -0
  134. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha/utils/version_checker.py +0 -0
  135. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha_python.egg-info/dependency_links.txt +0 -0
  136. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha_python.egg-info/entry_points.txt +0 -0
  137. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha_python.egg-info/requires.txt +0 -0
  138. {alpha_python-0.5.0 → alpha_python-0.5.1}/src/alpha_python.egg-info/top_level.txt +0 -0
  139. {alpha_python-0.5.0 → alpha_python-0.5.1}/tests/test_encoder.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpha-python
3
- Version: 0.5.0
3
+ Version: 0.5.1
4
4
  Summary: Alpha is intended to be the first dependency you need to add to your Python application. It is a Python library which contains standard building blocks that can be used in applications that are used as APIs and/or make use of database interaction.
5
5
  Author-email: Bart Reijling <bart@reijling.eu>
6
6
  Requires-Python: >=3.11
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "alpha-python"
3
- version = "0.5.0"
3
+ version = "0.5.1"
4
4
  description = "Alpha is intended to be the first dependency you need to add to your Python application. It is a Python library which contains standard building blocks that can be used in applications that are used as APIs and/or make use of database interaction."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -49,6 +49,7 @@ from alpha.repositories.models.repository_model import RepositoryModel
49
49
  from alpha.repositories.rest_api_repository import RestApiRepository
50
50
  from alpha.repositories.sql_alchemy_repository import SqlAlchemyRepository
51
51
  from alpha.services.authentication_service import AuthenticationService
52
+ from alpha.services.user_lifecycle_management import UserLifecycleManagement
52
53
  from alpha.utils.is_attrs import is_attrs
53
54
  from alpha.utils.is_pydantic import is_pydantic
54
55
  from alpha.utils.logging_configurator import (
@@ -56,6 +57,7 @@ from alpha.utils.logging_configurator import (
56
57
  GunicornLogger,
57
58
  )
58
59
  from alpha.utils.logging_level_checker import logging_level_checker
60
+ from alpha.utils.request_headers import Headers
59
61
  from alpha.utils.response_object import create_response_object
60
62
  from alpha.utils.verify_identity import verify_identity
61
63
  from alpha.utils.version_checker import minor_version_gte
@@ -126,11 +128,13 @@ __all__ = [
126
128
  "RestApiRepository",
127
129
  "SqlAlchemyRepository",
128
130
  "AuthenticationService",
131
+ "UserLifecycleManagement",
129
132
  "is_attrs",
130
133
  "is_pydantic",
131
134
  "LoggingConfigurator",
132
135
  "GunicornLogger",
133
136
  "logging_level_checker",
137
+ "Headers",
134
138
  "create_response_object",
135
139
  "verify_identity",
136
140
  "minor_version_gte",
@@ -0,0 +1,130 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime, timezone
3
+ from enum import Enum, auto
4
+ from typing import Self, Sequence, cast
5
+ from uuid import UUID
6
+
7
+ from alpha.domain.models.base_model import BaseDomainModel, DomainModel
8
+ from alpha.domain.models.group import Group
9
+ from alpha.domain.models.life_cycle_base import LifeCycleBase
10
+
11
+ from alpha.providers.models.identity import Identity
12
+
13
+
14
+ class Role(Enum):
15
+ """Defines user roles with varying levels of permissions. The roles are
16
+ ordered from highest to lowest permissions. The comparison methods allow
17
+ for easy comparison of roles based on their hierarchy. The roles are
18
+ ordered on a scale from highest to lowest permissions.
19
+
20
+ Typical permissions are as follows:
21
+ - CREATE: Permission to create new content or data, but not modify existing
22
+ content.
23
+ - READ: Permission to read content or data.
24
+ - UPDATE: Permission to modify existing content or data, but not create new
25
+ content.
26
+ - DELETE: Permission to delete content or data.
27
+ - MANAGE_USERS: Permission to manage user accounts and permissions.
28
+ - MANAGE_SETTINGS: Permission to manage system settings and configurations.
29
+ - ALL: Permission to perform all actions, including user management and
30
+ system settings.
31
+
32
+ Roles:
33
+ - ADMIN: Role with permissions to manage users, content, and system
34
+ settings. Typically has the ALL permissions.
35
+ - SUPERUSER: Role with all permissions, including system settings and user
36
+ management. Typically has the ALL permissions, but may be used to denote a
37
+ special type of admin user with additional privileges or responsibilities.
38
+ - OWNER: Role with permissions to manage their own resources and users, but
39
+ not system settings. Typically has permissions similar to ADMIN, but
40
+ limited to their own scope of resources.
41
+ - MODERATOR: Role with permissions to manage content and users, but not
42
+ system settings. Typically has permissions to UPDATE and DELETE content,
43
+ and MANAGE_USERS, but not MANAGE_SETTINGS.
44
+ - EDITOR: Role with permissions to create and edit content, but not manage
45
+ users or settings. Typically has permissions to CREATE, READ, UPDATE, and
46
+ DELETE content, but not MANAGE_USERS or MANAGE_SETTINGS.
47
+ - USER: Default role with standard permissions. Typically has permissions
48
+ to CREATE, READ, and UPDATE their own content, but not DELETE content or
49
+ manage users or settings.
50
+ - VIEWER: Typical read-only role with limited permissions. Typically has
51
+ permission to READ content, but not CREATE, UPDATE, DELETE, or manage users
52
+ or settings.
53
+ """
54
+
55
+ ADMIN = auto()
56
+ SUPERUSER = auto()
57
+ OWNER = auto()
58
+ MODERATOR = auto()
59
+ EDITOR = auto()
60
+ USER = auto()
61
+ VIEWER = auto()
62
+
63
+ def __lt__(self, obj: Self) -> bool:
64
+ return self.value < obj.value
65
+
66
+ def __le__(self, obj: Self) -> bool:
67
+ return self.value <= obj.value
68
+
69
+ def __gt__(self, obj: Self) -> bool:
70
+ return self.value > obj.value
71
+
72
+ def __ge__(self, obj: Self) -> bool:
73
+ return self.value >= obj.value
74
+
75
+
76
+ @dataclass(kw_only=True)
77
+ class User(LifeCycleBase, BaseDomainModel):
78
+ id: UUID | int | str | None = None
79
+ username: str | None = None
80
+ password: str | None = None
81
+ role: str | Role | None = None
82
+ email: str | None = None
83
+ phone: str | None = None
84
+ display_name: str | None = None
85
+ permissions: Sequence[str] | None = None
86
+ groups: Sequence[str | Group] | None = None
87
+ is_active: bool = True
88
+ admin: bool = False
89
+
90
+ @classmethod
91
+ def from_identity(cls, identity: Identity) -> Self:
92
+ """Create a User instance from an Identity instance.
93
+
94
+ Parameters
95
+ ----------
96
+ identity
97
+ Identity object to convert.
98
+
99
+ Returns
100
+ -------
101
+ User instance created from the Identity.
102
+ """
103
+ return cls(
104
+ id=identity.subject,
105
+ username=identity.username,
106
+ email=identity.email,
107
+ display_name=identity.display_name,
108
+ )
109
+
110
+ def update(self, obj: DomainModel) -> DomainModel:
111
+ """Update the User instance with data from another User instance.
112
+
113
+ Parameters
114
+ ----------
115
+ obj
116
+ User object to update from.
117
+ """
118
+ if not isinstance(obj, User):
119
+ raise TypeError("User.update expects a User instance.")
120
+
121
+ self.username = obj.username
122
+ self.email = obj.email
123
+ self.phone = obj.phone
124
+ self.display_name = obj.display_name
125
+ self.permissions = obj.permissions
126
+ self.groups = obj.groups
127
+ self.modified_at = datetime.now(tz=timezone.utc)
128
+ self.is_active = obj.is_active
129
+ self.admin = obj.admin
130
+ return cast(DomainModel, self)
@@ -18,8 +18,12 @@ class JWTFactory:
18
18
  lifetime_hours: str | None = "12",
19
19
  issuer: str = "http://localhost",
20
20
  jwt_algorithm: str = "HS256",
21
+ options: dict[str, Any] | None = None,
21
22
  ) -> None:
22
- """Initialize the JWTFactory.
23
+ """Initialize the JWTFactory. This method sets up the necessary
24
+ configuration for creating and validating JWT tokens. It requires a
25
+ secret key for signing the tokens and allows optional configuration
26
+ for token lifetime, issuer, algorithm, and decoding options.
23
27
 
24
28
  Parameters
25
29
  ----------
@@ -31,6 +35,10 @@ class JWTFactory:
31
35
  The issuer of the JWT, by default "http://localhost"
32
36
  jwt_algorithm, optional
33
37
  The algorithm used to sign the JWT, by default "HS256"
38
+ options, optional
39
+ A dictionary of options to customize the decoding behavior, by
40
+ default None. If not provided, it defaults to requiring standard
41
+ claims and verifying the signature.
34
42
 
35
43
  Raises
36
44
  ------
@@ -46,6 +54,10 @@ class JWTFactory:
46
54
  self.JWT_ISSUER = issuer
47
55
  self.JWT_ALGORITHM = jwt_algorithm
48
56
  self.JWT_LIFETIME_SECONDS = 3600 * int(lifetime_hours)
57
+ self.JWT_OPTIONS = options or {
58
+ "require": ["exp", "iat", "nbf", "iss", "sub"],
59
+ "verify_signature": True,
60
+ }
49
61
 
50
62
  def create(
51
63
  self,
@@ -93,13 +105,19 @@ class JWTFactory:
93
105
  )
94
106
  return token
95
107
 
96
- def validate(self, token: str) -> bool:
97
- """Validate a JWT token.
108
+ def validate(
109
+ self, token: str, options: dict[str, Any] | None = None
110
+ ) -> bool:
111
+ """Validate a JWT token. This method checks the token's signature,
112
+ expiration, and issuer. If the token is invalid, it raises an
113
+ appropriate exception. If the token is valid, it returns True.
98
114
 
99
115
  Parameters
100
116
  ----------
101
117
  token
102
118
  The JWT token to be validated.
119
+ options
120
+ A dictionary of options to customize the decoding behavior.
103
121
 
104
122
  Returns
105
123
  -------
@@ -112,15 +130,62 @@ class JWTFactory:
112
130
  InvalidSignatureException
113
131
  If the token signature is invalid.
114
132
  """
133
+ if self._decode(token, options):
134
+ return True
135
+ return False
136
+
137
+ def get_payload(
138
+ self, token: str, options: dict[str, Any] | None = None
139
+ ) -> dict[str, Any]:
140
+ """Retrieve the payload from a JWT token. This method does not perform
141
+ validation of the token by default. It simply decodes the token and
142
+ extracts the payload.
143
+
144
+ If the `options` parameter is provided, it will be passed to the
145
+ `jwt.decode` function. This allows customization of the decoding
146
+ behavior, such as enabling or disabling signature verification.
147
+
148
+ Parameters
149
+ ----------
150
+ token
151
+ The JWT token from which to extract the payload.
152
+ options
153
+ A dictionary of options to customize the decoding behavior.
154
+
155
+ Returns
156
+ -------
157
+ A dictionary containing the payload data extracted from the token.
158
+ """
159
+ decoded = self._decode(token, options)
160
+ return decoded.get("payload", {})
161
+
162
+ def _decode(
163
+ self, token: str, options: dict[str, Any] | None = None
164
+ ) -> dict[str, Any]:
165
+ """Decode a JWT token without performing validation. This method is
166
+ intended for internal use and should not be exposed as part of the
167
+ public API.
168
+
169
+ Parameters
170
+ ----------
171
+ token
172
+ The JWT token to be decoded.
173
+ options
174
+ A dictionary of options to customize the decoding behavior.
175
+
176
+ Returns
177
+ -------
178
+ A dictionary containing the decoded token data.
179
+ """
115
180
  try:
116
- jwt.decode(
181
+ decoded: dict[str, Any] = jwt.decode(
117
182
  jwt=token,
118
183
  key=self.JWT_SECRET,
119
184
  algorithms=[self.JWT_ALGORITHM],
120
185
  issuer=self.JWT_ISSUER,
121
- verify=True,
186
+ options=options or self.JWT_OPTIONS,
122
187
  )
123
- return True
188
+ return decoded
124
189
  except jwt.ExpiredSignatureError as e:
125
190
  raise exceptions.TokenExpiredException(str(e)) from e
126
191
  except jwt.InvalidSignatureError as e:
@@ -129,23 +194,3 @@ class JWTFactory:
129
194
  raise exceptions.InvalidTokenException(
130
195
  f"Token is invalid: {str(e)}"
131
196
  ) from e
132
-
133
- def get_payload(self, token: str) -> dict[str, Any]:
134
- """Retrieve the payload from a JWT token.
135
-
136
- Parameters
137
- ----------
138
- token
139
- The JWT token from which to extract the payload.
140
-
141
- Returns
142
- -------
143
- A dictionary containing the payload data extracted from the token.
144
- """
145
- decoded: dict[str, Any] = jwt.decode(
146
- jwt=token,
147
- key=self.JWT_SECRET,
148
- algorithms=[self.JWT_ALGORITHM],
149
- issuer=self.JWT_ISSUER,
150
- )
151
- return decoded.get("payload", {})
@@ -11,6 +11,7 @@ from alpha import exceptions
11
11
  class PasswordFactory:
12
12
  """This class provides methods for hashing and verifying passwords using
13
13
  the argon2 library. It includes the following methods:
14
+
14
15
  - hash_password: Hashes a given password and returns the hashed value as a
15
16
  hexadecimal string.
16
17
  - verify_password: Verifies a given password against a provided hash and
@@ -11,6 +11,7 @@ from typing import List
11
11
  from alpha.factories.request_factory import RequestFactory
12
12
  from alpha.factories.response_factory import ResponseFactory
13
13
  from alpha.utils.response_object import create_response_object
14
+ from alpha.utils.request_headers import Headers
14
15
  from alpha.utils.verify_identity import verify_identity
15
16
  from alpha.utils._http_codes import http_codes_{{#languageCode}}{{languageCode}}{{/languageCode}}{{^languageCode}}en{{/languageCode}} as http_codes
16
17
  from alpha.exceptions import (
@@ -144,28 +145,36 @@ def {{operationId}}(
144
145
  {{response}}
145
146
  {{/isContainer}}
146
147
  {{/allParams}}
148
+ headers = Headers.from_headers(connexion.request.headers)
149
+
150
+ auth_token = headers.auth_token if headers.has_auth_token else None
151
+ refresh_token = headers.refresh_token
152
+ api_key = headers.api_key
147
153
 
148
- {{#authMethods}}{{#isBasicBearer}}
149
- if "Authorization" in connexion.request.headers:
150
- token = connexion.request.headers["Authorization"].split(" ").pop()
151
- else:
152
- token = None
153
- {{/isBasicBearer}}{{/authMethods}}
154
154
  try:
155
155
  # Objects used for authorization
156
156
  roles=[{{#vendorExtensions.x-alpha-verify-roles}}"{{.}}",{{/vendorExtensions.x-alpha-verify-roles}}]
157
157
  groups=[{{#vendorExtensions.x-alpha-verify-groups}}"{{.}}",{{/vendorExtensions.x-alpha-verify-groups}}]
158
158
  permissions=[{{#vendorExtensions.x-alpha-verify-permissions}}"{{.}}",{{/vendorExtensions.x-alpha-verify-permissions}}]
159
- {{#authMethods}}{{#isBasicBearer}}
160
- # validate token
161
- if token_factory.validate(token):
162
- identity = token_factory.get_payload(token)
163
- verify_identity(identity,
164
- roles=roles,
165
- groups=groups,
166
- permissions=permissions,
167
- )
168
- {{/isBasicBearer}}{{/authMethods}}
159
+ {{#authMethods}}
160
+ {{#isBasicBearer}}
161
+ # Validate authentication token
162
+ if auth_token is None:
163
+ raise UnauthorizedException('Missing authentication token')
164
+
165
+ token_factory.validate(auth_token)
166
+
167
+ identity = token_factory.get_payload(auth_token)
168
+ verify_identity(identity,
169
+ roles=roles,
170
+ groups=groups,
171
+ permissions=permissions,
172
+ )
173
+ {{/isBasicBearer}}
174
+ {{#isApiKey}}
175
+ # This needs to be replaced by code to authenticate with an API key
176
+ {{/isApiKey}}
177
+ {{/authMethods}}
169
178
  # Call method or use variable
170
179
  {{#vendorExtensions.x-alpha-request-factory}}
171
180
  {{#vendorExtensions.x-alpha-service-name}}
@@ -180,35 +189,34 @@ def {{operationId}}(
180
189
  {{/vendorExtensions.x-alpha-service-method}}
181
190
  {{/vendorExtensions.x-alpha-service-name}}
182
191
  {{^vendorExtensions.x-alpha-service-name}}
183
- {{#vendorExtensions.x-alpha-method}}
184
- result = {{vendorExtensions.x-alpha-method}}
185
- {{/vendorExtensions.x-alpha-method}}
192
+ {{#vendorExtensions.x-alpha-custom-response}}
193
+ result = {{vendorExtensions.x-alpha-custom-response}}
194
+ {{/vendorExtensions.x-alpha-custom-response}}
186
195
  {{/vendorExtensions.x-alpha-service-name}}
187
196
  {{/vendorExtensions.x-alpha-request-factory}}
188
197
  {{^vendorExtensions.x-alpha-request-factory}}
189
198
  {{^vendorExtensions.x-alpha-service-name}}
190
- {{^vendorExtensions.x-alpha-method}}
199
+ {{^vendorExtensions.x-alpha-custom-response}}
191
200
  raise ServerErrorException('Missing x-alpha-service-name in API specification')
192
- {{/vendorExtensions.x-alpha-method}}
201
+ {{/vendorExtensions.x-alpha-custom-response}}
193
202
  {{^vendorExtensions.x-alpha-service-method}}
194
- {{^vendorExtensions.x-alpha-method}}
203
+ {{^vendorExtensions.x-alpha-custom-response}}
195
204
  raise ServerErrorException('Missing x-alpha-service-method in API specification')
196
- {{/vendorExtensions.x-alpha-method}}
205
+ {{/vendorExtensions.x-alpha-custom-response}}
197
206
  {{/vendorExtensions.x-alpha-service-method}}
198
207
  {{/vendorExtensions.x-alpha-service-name}}
199
208
  {{#vendorExtensions.x-alpha-service-name}}
200
209
  {{#vendorExtensions.x-alpha-service-method}}
201
210
  result = {{vendorExtensions.x-alpha-service-name}}.{{vendorExtensions.x-alpha-service-method}}(
202
211
  {{#allParams}}{{paramName}}={{paramName}},{{/allParams}}
203
- {{#vendorExtensions.x-alpha-service-additional-parameters}}{{.}}={{.}},
204
- {{/vendorExtensions.x-alpha-service-additional-parameters}}
212
+ {{#vendorExtensions.x-alpha-service-additional-parameters}}{{.}}={{.}},{{/vendorExtensions.x-alpha-service-additional-parameters}}
205
213
  )
206
214
  {{/vendorExtensions.x-alpha-service-method}}
207
215
  {{/vendorExtensions.x-alpha-service-name}}
208
216
  {{^vendorExtensions.x-alpha-service-name}}
209
- {{#vendorExtensions.x-alpha-method}}
210
- result = {{vendorExtensions.x-alpha-method}}
211
- {{/vendorExtensions.x-alpha-method}}
217
+ {{#vendorExtensions.x-alpha-custom-response}}
218
+ result = {{vendorExtensions.x-alpha-custom-response}}
219
+ {{/vendorExtensions.x-alpha-custom-response}}
212
220
  {{/vendorExtensions.x-alpha-service-name}}
213
221
  {{/vendorExtensions.x-alpha-request-factory}}
214
222
 
@@ -48,13 +48,18 @@ class TokenFactory(Protocol):
48
48
  """
49
49
  ...
50
50
 
51
- def get_payload(self, token: str) -> dict[str, str]:
51
+ def get_payload(
52
+ self, token: str, options: dict[str, bool] | None
53
+ ) -> dict[str, str]:
52
54
  """Retrieve the payload from an authentication token.
53
55
 
54
56
  Parameters
55
57
  ----------
56
58
  token
57
59
  The authentication token from which to extract the payload.
60
+ options
61
+ A dictionary of options to customize the decoding behavior, such as
62
+ enabling or disabling signature verification.
58
63
 
59
64
  Returns
60
65
  -------
@@ -123,7 +123,7 @@ class Identity:
123
123
  username: str | None
124
124
  email: str | None
125
125
  display_name: str | None
126
- groups: Sequence[str]
126
+ groups: Sequence[str | Group]
127
127
  permissions: Sequence[str]
128
128
  claims: Mapping[str, Any]
129
129
  issued_at: datetime
@@ -30,5 +30,5 @@ class RepositoryModel(Generic[DomainModel]):
30
30
  name: str
31
31
  repository: Callable[..., object]
32
32
  default_model: type[DomainModel]
33
- interface: object | None
33
+ interface: object | None = None
34
34
  additional_config: dict[str, object] | None = None
@@ -1,5 +1,7 @@
1
1
  from alpha.services.authentication_service import AuthenticationService
2
+ from alpha.services.user_lifecycle_management import UserLifecycleManagement
2
3
 
3
4
  __all__ = [
4
5
  "AuthenticationService",
6
+ "UserLifecycleManagement",
5
7
  ]
@@ -43,8 +43,8 @@ class AuthenticationService:
43
43
  refresh_token_max_age: int = 3600 * 24 * 7,
44
44
  merge_with_database_users: bool = False,
45
45
  merge_with_database_groups: bool = False,
46
- user_id_attribute: str = "username",
47
- group_id_attribute: str = "name",
46
+ user_username_attribute: str = "username",
47
+ group_name_attribute: str = "name",
48
48
  uow: UnitOfWork | None = None,
49
49
  users_repository_name: str = "users",
50
50
  groups_repository_name: str = "groups",
@@ -106,10 +106,10 @@ class AuthenticationService:
106
106
  merge_with_database_groups, optional
107
107
  Whether to merge identity data with database group data,
108
108
  by default False
109
- user_id_attribute, optional
109
+ user_username_attribute, optional
110
110
  Attribute name in the user database to use as the unique
111
111
  identifier, by default "username"
112
- group_id_attribute, optional
112
+ group_name_attribute, optional
113
113
  Attribute name in the group database to use as the unique
114
114
  identifier, by default "name"
115
115
  uow, optional
@@ -176,8 +176,8 @@ class AuthenticationService:
176
176
  self._refresh_token_max_age = refresh_token_max_age
177
177
  self._merge_with_database_users = merge_with_database_users
178
178
  self._merge_with_database_groups = merge_with_database_groups
179
- self._user_id_attribute = user_id_attribute
180
- self._group_id_attribute = group_id_attribute
179
+ self._user_username_attribute = user_username_attribute
180
+ self._group_name_attribute = group_name_attribute
181
181
  self.uow = uow
182
182
  self._users_repository_name = users_repository_name
183
183
  self._groups_repository_name = groups_repository_name
@@ -368,8 +368,14 @@ class AuthenticationService:
368
368
  "configured, cannot retrieve identity from auth token."
369
369
  )
370
370
  try:
371
+ # Attempt to retrieve the identity from the auth token without
372
+ # validating the token, since it may be expired. The payload
373
+ # should still be retrievable if the token is expired, as long
374
+ # as the signature is valid. If the signature is invalid, an
375
+ # exception will be raised and caught, resulting in the
376
+ # identity remaining None.
371
377
  payload = self._identity_provider.token_factory.get_payload(
372
- token=auth_token
378
+ token=auth_token, options={"verify_exp": False}
373
379
  )
374
380
  identity = Identity.from_dict(payload)
375
381
  except Exception:
@@ -469,8 +475,8 @@ class AuthenticationService:
469
475
  )
470
476
 
471
477
  user = users.get_by_id(
472
- value=getattr(identity, self._user_id_attribute),
473
- attr=self._user_id_attribute,
478
+ value=getattr(identity, self._user_username_attribute),
479
+ attr=self._user_username_attribute,
474
480
  )
475
481
  if user:
476
482
  identity.update_from_user(user)
@@ -502,18 +508,23 @@ class AuthenticationService:
502
508
  self._raise_no_uow()
503
509
 
504
510
  with self.uow:
505
- groups: SqlRepository[Group] = getattr(
511
+ groups_repo: SqlRepository[Group] = getattr(
506
512
  self.uow, self._groups_repository_name
507
513
  )
508
514
 
515
+ groups = list(identity.groups)
516
+ for i, group in enumerate(groups):
517
+ if isinstance(group, Group):
518
+ groups[i] = getattr(group, self._group_name_attribute)
519
+
509
520
  filters = [
510
521
  SearchFilter(
511
- field=self._group_id_attribute,
522
+ field=self._group_name_attribute,
512
523
  op=Operator.IN,
513
- value=identity.groups,
524
+ value=groups,
514
525
  )
515
526
  ]
516
- user_groups = groups.select(filters=filters)
527
+ user_groups = groups_repo.select(filters=filters)
517
528
  identity.update_from_groups(user_groups)
518
529
 
519
530
  return identity