alpha-python 0.3.2__tar.gz → 0.3.4__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 (131) hide show
  1. {alpha_python-0.3.2 → alpha_python-0.3.4}/PKG-INFO +3 -2
  2. {alpha_python-0.3.2 → alpha_python-0.3.4}/pyproject.toml +3 -2
  3. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/cli.py +13 -3
  4. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/domain/models/user.py +2 -2
  5. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/exceptions.py +21 -0
  6. alpha_python-0.3.4/src/alpha/factories/password_factory.py +130 -0
  7. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/api_generate_handler.py +2 -2
  8. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/Dockerfile.mustache +2 -0
  9. alpha_python-0.3.4/src/alpha/providers/database_provider.py +242 -0
  10. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/providers/models/identity.py +94 -46
  11. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/services/authentication_service.py +4 -4
  12. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/utils/is_pydantic.py +9 -1
  13. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha_python.egg-info/PKG-INFO +3 -2
  14. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha_python.egg-info/SOURCES.txt +1 -0
  15. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha_python.egg-info/requires.txt +2 -1
  16. alpha_python-0.3.2/src/alpha/providers/database_provider.py +0 -12
  17. {alpha_python-0.3.2 → alpha_python-0.3.4}/LICENSE +0 -0
  18. {alpha_python-0.3.2 → alpha_python-0.3.4}/README.md +0 -0
  19. {alpha_python-0.3.2 → alpha_python-0.3.4}/setup.cfg +0 -0
  20. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/__init__.py +0 -0
  21. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/adapters/__init__.py +0 -0
  22. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/adapters/sqla_unit_of_work.py +0 -0
  23. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/containers/__init__.py +0 -0
  24. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/containers/container.py +0 -0
  25. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/domain/__init__.py +0 -0
  26. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/domain/models/__init__.py +0 -0
  27. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/domain/models/base_model.py +0 -0
  28. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/domain/models/life_cycle_base.py +0 -0
  29. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/encoder.py +0 -0
  30. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/factories/__init__.py +0 -0
  31. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/factories/_type_conversion_matrix.py +0 -0
  32. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/factories/_type_mapping.py +0 -0
  33. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/factories/class_factories.py +0 -0
  34. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/factories/default_field_factory.py +0 -0
  35. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/factories/field_iterator.py +0 -0
  36. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/factories/jwt_factory.py +0 -0
  37. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/factories/logging_handler_factory.py +0 -0
  38. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/factories/model_class_factory.py +0 -0
  39. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/factories/models/__init__.py +0 -0
  40. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/factories/models/factory_classes.py +0 -0
  41. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/factories/request_factory.py +0 -0
  42. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/factories/response_factory.py +0 -0
  43. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/factories/type_factories.py +0 -0
  44. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/__init__.py +0 -0
  45. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/api_run_handler.py +0 -0
  46. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/base_handler.py +0 -0
  47. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/gen-code.sh +0 -0
  48. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/models/__init__.py +0 -0
  49. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/models/argument.py +0 -0
  50. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/models/command.py +0 -0
  51. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/models/section.py +0 -0
  52. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/models/subparser.py +0 -0
  53. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/run-api.sh +0 -0
  54. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/__init__.py +0 -0
  55. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/README.mustache +0 -0
  56. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/__init__model.mustache +0 -0
  57. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/__init__test.mustache +0 -0
  58. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/__main__.mustache +0 -0
  59. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/base_model.mustache +0 -0
  60. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/controller.mustache +0 -0
  61. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/controller_test.mustache +0 -0
  62. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/dockerignore.mustache +0 -0
  63. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/encoder.mustache +0 -0
  64. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/git_push.sh.mustache +0 -0
  65. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/gitignore.mustache +0 -0
  66. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/model.mustache +0 -0
  67. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/openapi.mustache +0 -0
  68. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/param_type.mustache +0 -0
  69. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/requirements.mustache +0 -0
  70. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/security_controller_.mustache +0 -0
  71. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/setup.mustache +0 -0
  72. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/test-requirements.mustache +0 -0
  73. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/tox.mustache +0 -0
  74. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/travis.mustache +0 -0
  75. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/typing_utils.mustache +0 -0
  76. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/handlers/templates/python-flask/util.mustache +0 -0
  77. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/infra/__init__.py +0 -0
  78. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/infra/connectors/__init__.py +0 -0
  79. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/infra/connectors/ldap_connector.py +0 -0
  80. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/infra/connectors/oidc_connector.py +0 -0
  81. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/infra/databases/__init__.py +0 -0
  82. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/infra/databases/sql_alchemy.py +0 -0
  83. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/infra/models/__init__.py +0 -0
  84. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/infra/models/filter_operators.py +0 -0
  85. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/infra/models/json_patch.py +0 -0
  86. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/infra/models/order_by.py +0 -0
  87. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/infra/models/query_clause.py +0 -0
  88. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/infra/models/search_filter.py +0 -0
  89. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/interfaces/__init__.py +0 -0
  90. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/interfaces/attrs_instance.py +0 -0
  91. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/interfaces/dataclass_instance.py +0 -0
  92. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/interfaces/factories.py +0 -0
  93. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/interfaces/handler.py +0 -0
  94. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/interfaces/openapi_model.py +0 -0
  95. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/interfaces/patchable.py +0 -0
  96. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/interfaces/providers.py +0 -0
  97. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/interfaces/pydantic_instance.py +0 -0
  98. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/interfaces/sql_database.py +0 -0
  99. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/interfaces/sql_mapper.py +0 -0
  100. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/interfaces/sql_repository.py +0 -0
  101. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/interfaces/token_factory.py +0 -0
  102. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/interfaces/unit_of_work.py +0 -0
  103. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/interfaces/updateable.py +0 -0
  104. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/mixins/__init__.py +0 -0
  105. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/mixins/jwt_provider.py +0 -0
  106. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/providers/__init__.py +0 -0
  107. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/providers/api_key_provider.py +0 -0
  108. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/providers/ldap_provider.py +0 -0
  109. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/providers/local_provider.py +0 -0
  110. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/providers/models/__init__.py +0 -0
  111. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/providers/models/credentials.py +0 -0
  112. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/providers/models/token.py +0 -0
  113. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/providers/oidc_provider.py +0 -0
  114. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/py.typed +0 -0
  115. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/repositories/__init__.py +0 -0
  116. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/repositories/models/__init__.py +0 -0
  117. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/repositories/models/repository_model.py +0 -0
  118. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/repositories/sql_alchemy_repository.py +0 -0
  119. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/services/__init__.py +0 -0
  120. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/utils/__init__.py +0 -0
  121. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/utils/_http_codes.py +0 -0
  122. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/utils/is_attrs.py +0 -0
  123. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/utils/logging_configurator.py +0 -0
  124. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/utils/logging_level_checker.py +0 -0
  125. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/utils/response_object.py +0 -0
  126. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/utils/verify_identity.py +0 -0
  127. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha/utils/version_checker.py +0 -0
  128. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha_python.egg-info/dependency_links.txt +0 -0
  129. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha_python.egg-info/entry_points.txt +0 -0
  130. {alpha_python-0.3.2 → alpha_python-0.3.4}/src/alpha_python.egg-info/top_level.txt +0 -0
  131. {alpha_python-0.3.2 → alpha_python-0.3.4}/tests/test_encoder.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpha-python
3
- Version: 0.3.2
3
+ Version: 0.3.4
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
@@ -16,7 +16,8 @@ Requires-Dist: pyjwt>=2.10.1
16
16
  Requires-Dist: six>=1.17.0
17
17
  Requires-Dist: sqlalchemy>=2.0.44
18
18
  Requires-Dist: requests>=2.28.1
19
- Requires-Dist: dependency-injector[yaml]<5.0.0,>=4.48.3
19
+ Requires-Dist: dependency-injector[yaml]!=4.48.3,<5.0.0,>=4.42.0
20
+ Requires-Dist: argon2-cffi>=25.1.0
20
21
  Provides-Extra: api-generator
21
22
  Requires-Dist: openapi-generator-cli==7.14.0; (python_version >= "3.11" and python_version < "4.0") and extra == "api-generator"
22
23
  Requires-Dist: jdk4py; extra == "api-generator"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "alpha-python"
3
- version = "0.3.2"
3
+ version = "0.3.4"
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 = [
@@ -18,7 +18,8 @@ dependencies = [
18
18
  "six>=1.17.0",
19
19
  "sqlalchemy>=2.0.44",
20
20
  "requests>=2.28.1",
21
- "dependency-injector[yaml]>=4.48.3,<5.0.0",
21
+ "dependency-injector[yaml]>=4.42.0,<5.0.0,!=4.48.3",
22
+ "argon2-cffi>=25.1.0",
22
23
  ]
23
24
 
24
25
  [project.scripts]
@@ -134,15 +134,25 @@ def _guess_current_package_name() -> str:
134
134
  try:
135
135
  with open(pyproject_path, 'rb') as f:
136
136
  pyproject_data = tomllib.load(f)
137
- return pyproject_data['project']['name'].replace('-', '_')
137
+ name = None
138
+ try:
139
+ if 'project' in pyproject_data:
140
+ name = pyproject_data['project']['name']
141
+ elif 'tool' in pyproject_data and 'poetry' in pyproject_data['tool']:
142
+ name = pyproject_data['tool']['poetry']['name']
143
+ if name is not None:
144
+ return name.replace('-', '_')
145
+ except KeyError:
146
+ print('Could not find project name in pyproject.toml')
138
147
  except Exception:
139
148
  pass
149
+ else:
150
+ print('Could not find pyproject.toml')
140
151
 
141
152
  # Fallback to use the current folder name
142
- print('Could not find pyproject.toml, guessing package name from folder')
153
+ print('Guessing package name from folder')
143
154
  return os.path.basename(cwd)
144
155
 
145
-
146
156
  def init() -> None:
147
157
  """Init the container and wire it to the main function."""
148
158
  container = Container()
@@ -48,7 +48,7 @@ class User(LifeCycleBase, BaseDomainModel):
48
48
 
49
49
  Parameters
50
50
  ----------
51
- user
51
+ obj
52
52
  User object to update from.
53
53
  """
54
54
  if not isinstance(obj, User):
@@ -60,7 +60,7 @@ class User(LifeCycleBase, BaseDomainModel):
60
60
  self.display_name = obj.display_name
61
61
  self.permissions = obj.permissions
62
62
  self.groups = obj.groups
63
- self.updated_at = datetime.now(tz=timezone.utc)
63
+ self.modified_at = datetime.now(tz=timezone.utc)
64
64
  self.is_active = obj.is_active
65
65
  self.admin = obj.admin
66
66
  return cast(DomainModel, self)
@@ -177,6 +177,27 @@ class TokenCreationException(Exception):
177
177
  """Raised when there is an error during token creation."""
178
178
 
179
179
 
180
+ # User exceptions
181
+ class UnknownUserException(BadRequestException):
182
+ """Raised when a referenced user cannot be found."""
183
+
184
+
185
+ class EmptyValueException(BadRequestException):
186
+ """Raised when a required value is empty."""
187
+
188
+
189
+ class UnknownTokenException(UnauthorizedException):
190
+ """Raised when a token is not recognized."""
191
+
192
+
193
+ class WrongPasswordException(BadRequestException):
194
+ """Raised when an incorrect password is provided."""
195
+
196
+
197
+ class MissingPasswordException(InternalServerErrorException):
198
+ """Raised when a required password is missing from the request or configuration."""
199
+
200
+
180
201
  # Cli Exceptions
181
202
  class InvalidArgumentsException(Exception):
182
203
  """Raised when invalid arguments are provided to a CLI command."""
@@ -0,0 +1,130 @@
1
+ """Contains PasswordFactory class with password hashing methods
2
+ hash_password & verify_password
3
+ """
4
+
5
+ from argon2 import PasswordHasher
6
+ from argon2.exceptions import VerifyMismatchError
7
+
8
+ from alpha import exceptions
9
+
10
+
11
+ class PasswordFactory:
12
+ """This class provides methods for hashing and verifying passwords using
13
+ the argon2 library. It includes the following methods:
14
+ - hash_password: Hashes a given password and returns the hashed value as a
15
+ hexadecimal string.
16
+ - verify_password: Verifies a given password against a provided hash and
17
+ returns True if the password matches the hash, False otherwise.
18
+
19
+ Parameters
20
+ ----------
21
+ password_hasher
22
+ An optional password hasher instance. If not provided, a default
23
+ PasswordHasher with a salt length of 16 will be used.
24
+ """
25
+
26
+ def __init__(self, password_hasher: PasswordHasher | None = None) -> None:
27
+ self._password_hasher = password_hasher or PasswordHasher(salt_len=16)
28
+
29
+ def hash_password(
30
+ self, password: str | None, convert_to_hex: bool = True
31
+ ) -> str:
32
+ """Hashes the provided password using the password hasher.
33
+
34
+ Parameters
35
+ ----------
36
+ password
37
+ The password to be hashed.
38
+ convert_to_hex
39
+ A boolean flag indicating whether to convert the hashed password to
40
+ hexadecimal format. Defaults to True.
41
+
42
+ Returns
43
+ -------
44
+ The hashed password as a hexadecimal string.
45
+
46
+ Raises
47
+ ------
48
+ exceptions.WrongPasswordException
49
+ Raised when the provided password is None or empty.
50
+ """
51
+ if not password:
52
+ raise exceptions.WrongPasswordException("Password value is empty")
53
+ hashed_password = self._password_hasher.hash(password=password)
54
+ return (
55
+ self._to_hex(hashed_password)
56
+ if convert_to_hex
57
+ else hashed_password
58
+ )
59
+
60
+ def verify_password(
61
+ self,
62
+ password: str | None,
63
+ hash: str | None,
64
+ convert_from_hex: bool = True,
65
+ ) -> bool:
66
+ """Verifies the provided password against the given hash using the
67
+ password hasher.
68
+
69
+ Parameters
70
+ ----------
71
+ password
72
+ The password to be verified.
73
+ hash
74
+ The hash to verify the password against.
75
+ convert_from_hex
76
+ A boolean flag indicating whether to convert the hash from
77
+ hexadecimal format before verification. Defaults to True.
78
+
79
+ Returns
80
+ -------
81
+ True if the password matches the hash, False otherwise.
82
+ Raises
83
+ ------
84
+ exceptions.WrongPasswordException
85
+ Raised when the provided password is None or empty.
86
+ exceptions.MissingPasswordException
87
+ Raised when the provided hash is None or empty.
88
+ """
89
+ if not password:
90
+ raise exceptions.WrongPasswordException("Password value is empty")
91
+ if not hash:
92
+ raise exceptions.MissingPasswordException(
93
+ "No password value to compare with"
94
+ )
95
+
96
+ try:
97
+ return self._password_hasher.verify(
98
+ hash=self._from_hex(hash) if convert_from_hex else hash,
99
+ password=password,
100
+ )
101
+ except VerifyMismatchError:
102
+ return False
103
+
104
+ def _to_hex(self, value: str) -> str:
105
+ """Converts a string value to its hexadecimal representation.
106
+
107
+ Parameters
108
+ ----------
109
+ value
110
+ The string value to be converted to hexadecimal.
111
+
112
+ Returns
113
+ -------
114
+ The hexadecimal representation of the string value.
115
+ """
116
+ return value.encode("utf-8").hex()
117
+
118
+ def _from_hex(self, hex: str) -> str:
119
+ """Converts a hexadecimal string back to its original string
120
+ representation.
121
+
122
+ Parameters
123
+ ----------
124
+ hex
125
+ The hexadecimal string to be converted back to the original string.
126
+ Returns
127
+ -------
128
+ The original string representation of the hexadecimal input.
129
+ """
130
+ return bytes.fromhex(hex).decode("utf-8")
@@ -23,8 +23,8 @@ class ApiGenerateHandler(BaseHandler):
23
23
  "the required packages first. \nThis can be done by installing "
24
24
  "the \'api-generator\' extra: \n"
25
25
  "- pip install alpha-python[api-generator]\n"
26
- "- poetry add alpha-python --extras api-generator\n"
27
- "- uv add alpha-python --extra api-generator"
26
+ "- poetry add --dev alpha-python --extras api-generator\n"
27
+ "- uv add --dev alpha-python --extra api-generator"
28
28
  )
29
29
  exit(1)
30
30
 
@@ -19,5 +19,7 @@ COPY . /usr/src/app
19
19
 
20
20
  EXPOSE {{serverPort}}
21
21
 
22
+ ENV FLASK_ENV=production
23
+
22
24
  CMD ["gunicorn", "--bind", ":{{serverPort}}", "--workers", "4", "--logger-class", "alpha.utils.logging_configurator.GunicornLogger", "{{packageName}}.__main__:app"]
23
25
 
@@ -0,0 +1,242 @@
1
+ """Database Identity Provider implementation"""
2
+
3
+ import logging
4
+
5
+ from alpha.domain.models.user import User
6
+ from alpha.factories.password_factory import PasswordFactory
7
+ from alpha.interfaces.sql_repository import SqlRepository
8
+ from alpha.interfaces.token_factory import TokenFactory
9
+ from alpha.interfaces.unit_of_work import UnitOfWork
10
+ from alpha.mixins.jwt_provider import JWTProviderMixin
11
+ from alpha.providers.models.credentials import PasswordCredentials
12
+ from alpha.providers.models.identity import Identity
13
+ from alpha import exceptions
14
+
15
+
16
+ class DatabaseProvider(JWTProviderMixin):
17
+ """Database Identity Provider implementation."""
18
+
19
+ protocol = "database"
20
+ _token_factory: TokenFactory | None = None
21
+
22
+ def __init__(
23
+ self,
24
+ uow: UnitOfWork,
25
+ token_factory: TokenFactory | None = None,
26
+ password_factory: PasswordFactory | None = None,
27
+ user_name_attribute: str = "username",
28
+ users_repository_name: str = "users",
29
+ ) -> None:
30
+ """Database Identity Provider implementation for user authentication
31
+ and management. This provider uses a database to store user information
32
+ and credentials, and provides methods for authenticating users,
33
+ retrieving user information, and changing passwords.
34
+
35
+ Parameters
36
+ ----------
37
+ uow
38
+ Unit of work instance to manage database transactions
39
+ token_factory
40
+ Token factory instance to generate and validate tokens
41
+ password_factory, optional
42
+ Password factory instance to handle password hashing and
43
+ verification, by default None
44
+ user_name_attribute, optional
45
+ Attribute name to identify the user, by default "username"
46
+ users_repository_name, optional
47
+ Repository name for user entities, by default "users"
48
+ """
49
+ self.uow = uow
50
+ self._token_factory = token_factory
51
+ self._password_factory = password_factory or PasswordFactory()
52
+ self._user_name_attribute = user_name_attribute
53
+ self._users_repository_name = users_repository_name
54
+
55
+ def authenticate(self, credentials: PasswordCredentials) -> Identity:
56
+ """Authenticate a user using their credentials.
57
+
58
+ Parameters
59
+ ----------
60
+ credentials
61
+ Password credentials for the user
62
+
63
+ Returns
64
+ -------
65
+ Identity instance representing the authenticated user
66
+ """
67
+ with self.uow:
68
+ users: SqlRepository[User] = getattr(
69
+ self.uow, self._users_repository_name
70
+ )
71
+ user = self._verify_password(
72
+ credentials=credentials, user_repository=users
73
+ )
74
+
75
+ return Identity.from_user(user)
76
+
77
+ def get_user(self, subject: str) -> Identity:
78
+ """Retrieve a user by their subject identifier.
79
+
80
+ Parameters
81
+ ----------
82
+ subject
83
+ The subject identifier of the user
84
+
85
+ Returns
86
+ -------
87
+ Identity instance representing the user
88
+ """
89
+ with self.uow:
90
+ users: SqlRepository[User] = getattr(
91
+ self.uow, self._users_repository_name
92
+ )
93
+ user = self._get_user(
94
+ username=subject, user_repository=users, attribute_name="id"
95
+ )
96
+
97
+ return Identity.from_user(user)
98
+
99
+ def change_password(
100
+ self, credentials: PasswordCredentials, new_password: str
101
+ ) -> None:
102
+ """Change the password for a user.
103
+
104
+ Parameters
105
+ ----------
106
+ credentials
107
+ Password credentials for the user
108
+ new_password
109
+ The new password to set for the user
110
+ """
111
+ with self.uow:
112
+ users: SqlRepository[User] = getattr(
113
+ self.uow, self._users_repository_name
114
+ )
115
+ user = self._verify_password(
116
+ credentials=credentials, user_repository=users
117
+ )
118
+
119
+ self._update_user_password(user, new_password)
120
+ self.uow.commit()
121
+
122
+ def _get_user(
123
+ self,
124
+ username: str,
125
+ user_repository: SqlRepository[User],
126
+ attribute_name: str | None = None,
127
+ ) -> User:
128
+ """Retrieve a user by their username.
129
+
130
+ Parameters
131
+ ----------
132
+ username
133
+ The username of the user
134
+ user_repository
135
+ The repository to query for the user
136
+ attribute_name
137
+ The attribute name to use for querying the user, by default None.
138
+ If None, the provider's configured user_name_attribute will be
139
+ used.
140
+
141
+ Returns
142
+ -------
143
+ User instance representing the retrieved user
144
+
145
+ Raises
146
+ ------
147
+ exceptions.UserNotFoundException
148
+ If the user does not exist
149
+ """
150
+ user = user_repository.get_one_or_none(
151
+ attr=attribute_name or self._user_name_attribute,
152
+ value=username,
153
+ )
154
+
155
+ if not user:
156
+ msg = (
157
+ f"User with '{attribute_name or self._user_name_attribute}'="
158
+ f"'{username}' does not exist"
159
+ )
160
+ logging.debug(msg)
161
+ # Disabled lines below for future implementation of logging and
162
+ # unit of work commit
163
+ # self.logger(msg=msg, level=LogLevel.DEBUG)
164
+ # self.uow.commit()
165
+ raise exceptions.UserNotFoundException(msg)
166
+
167
+ return user
168
+
169
+ def _verify_password(
170
+ self,
171
+ credentials: PasswordCredentials,
172
+ user_repository: SqlRepository[User],
173
+ ) -> User:
174
+ """Verify the password for a user.
175
+
176
+ Parameters
177
+ ----------
178
+ credentials
179
+ Password credentials for the user
180
+ user_repository
181
+ The repository to query for the user
182
+
183
+ Returns
184
+ -------
185
+ User instance representing the authenticated user
186
+
187
+ Raises
188
+ ------
189
+ exceptions.InvalidCredentialsException
190
+ If the provided credentials are invalid
191
+ exceptions.MissingPasswordException
192
+ If the user does not have a password set
193
+ """
194
+
195
+ user = self._get_user(credentials.username, user_repository)
196
+
197
+ try:
198
+ if not self._password_factory.verify_password(
199
+ credentials.password, user.password
200
+ ):
201
+ msg = (
202
+ f"The provided password for user "
203
+ f"'{getattr(user, self._user_name_attribute)}' is "
204
+ "incorrect"
205
+ )
206
+ logging.debug(msg)
207
+ # Disabled lines below for future implementation of logging and
208
+ # unit of work commit
209
+ # self.logger(msg=msg, level=LogLevel.DEBUG)
210
+ # self.uow.commit()
211
+ raise exceptions.InvalidCredentialsException(msg)
212
+ except exceptions.MissingPasswordException as exc:
213
+ msg = (
214
+ f"No password value to compare for "
215
+ f"'{getattr(user, self._user_name_attribute)}'"
216
+ )
217
+ logging.error(msg)
218
+ # Disabled lines below for future implementation of logging and
219
+ # unit of work commit
220
+ # self.logger(msg=msg, level=LogLevel.ERROR)
221
+ # self.uow.commit()
222
+ raise exceptions.MissingPasswordException(msg) from exc
223
+
224
+ return user
225
+
226
+ def _update_user_password(
227
+ self,
228
+ user: User,
229
+ new_password: str,
230
+ ) -> None:
231
+ """Change the password for a user.
232
+
233
+ Parameters
234
+ ----------
235
+ user
236
+ User instance representing the user to update the password for
237
+ new_password
238
+ The new password to set for the user
239
+ user_repository
240
+ The repository to query for the user
241
+ """
242
+ user.password = self._password_factory.hash_password(new_password)
@@ -27,43 +27,43 @@ DEFAULT_AD_MAPPINGS = {
27
27
  }
28
28
 
29
29
  AD_SEARCH_ATTRIBUTES = [
30
- 'cn',
31
- 'generationQualifier',
32
- 'name',
33
- 'postalAddress',
34
- 'lastLogonTimestamp',
35
- 'mobile',
36
- 'postalCode',
37
- 'countryCode',
38
- 'company',
39
- 'displayName',
40
- 'o',
41
- 'st',
42
- 'ou',
43
- 'givenName',
44
- 'msExchUserCulture',
45
- 'l',
46
- 'initials',
47
- 'msTSLicenseVersion',
48
- 'memberOf',
49
- 'whenChanged',
50
- 'mailNickname',
51
- 'sn',
52
- 'street',
53
- 'accountExpires',
54
- 'uSNChanged',
55
- 'distinguishedName',
56
- 'whenCreated',
57
- 'sAMAccountName',
58
- 'c',
59
- 'employeeID',
60
- 'streetAddress',
61
- 'description',
62
- 'mail',
63
- 'title',
64
- 'department',
65
- 'co',
66
- 'personalTitle',
30
+ "cn",
31
+ "generationQualifier",
32
+ "name",
33
+ "postalAddress",
34
+ "lastLogonTimestamp",
35
+ "mobile",
36
+ "postalCode",
37
+ "countryCode",
38
+ "company",
39
+ "displayName",
40
+ "o",
41
+ "st",
42
+ "ou",
43
+ "givenName",
44
+ "msExchUserCulture",
45
+ "l",
46
+ "initials",
47
+ "msTSLicenseVersion",
48
+ "memberOf",
49
+ "whenChanged",
50
+ "mailNickname",
51
+ "sn",
52
+ "street",
53
+ "accountExpires",
54
+ "uSNChanged",
55
+ "distinguishedName",
56
+ "whenCreated",
57
+ "sAMAccountName",
58
+ "c",
59
+ "employeeID",
60
+ "streetAddress",
61
+ "description",
62
+ "mail",
63
+ "title",
64
+ "department",
65
+ "co",
66
+ "personalTitle",
67
67
  ]
68
68
 
69
69
 
@@ -230,6 +230,32 @@ class Identity:
230
230
  ),
231
231
  )
232
232
 
233
+ @classmethod
234
+ def from_user(cls, user: User) -> "Identity":
235
+ """Instantiate an Identity from a User instance.
236
+
237
+ Parameters
238
+ ----------
239
+ user
240
+ User object to create the Identity from.
241
+
242
+ Returns
243
+ -------
244
+ An Identity instance populated with data from the User object.
245
+ """
246
+ return cls(
247
+ subject=str(user.id),
248
+ username=user.username,
249
+ email=user.email,
250
+ display_name=user.display_name,
251
+ groups=user.groups or [],
252
+ permissions=user.permissions or [],
253
+ claims={},
254
+ issued_at=datetime.now(tz=timezone.utc),
255
+ role=user.role,
256
+ admin=user.admin,
257
+ )
258
+
233
259
  def update_from_user(self, user: User) -> None:
234
260
  """Update the Identity instance with data from a User instance.
235
261
 
@@ -244,11 +270,11 @@ class Identity:
244
270
  if not self.display_name:
245
271
  self.display_name = user.display_name
246
272
  for permission in user.permissions or []:
247
- if permission not in self.permissions:
248
- self.permissions.append(permission) # type: ignore
273
+ self.permissions = self._append_on_sequence(
274
+ self.permissions, permission
275
+ )
249
276
  for group in user.groups or []:
250
- if group not in self.groups:
251
- self.groups.append(group) # type: ignore
277
+ self.groups = self._append_on_sequence(self.groups, group)
252
278
  self.role = user.role
253
279
  self.admin = user.admin
254
280
 
@@ -379,11 +405,33 @@ class Identity:
379
405
  groups: list[str] = []
380
406
  for item in entry.get("memberOf", []):
381
407
  group = (
382
- item.replace('\\,', ';')
383
- .split(',')[0]
384
- .replace(';', ',')
385
- .replace('CN=', '')
386
- .replace('cn=', '')
408
+ item.replace("\\,", ";")
409
+ .split(",")[0]
410
+ .replace(";", ",")
411
+ .replace("CN=", "")
412
+ .replace("cn=", "")
387
413
  )
388
414
  groups.append(group)
389
415
  return groups
416
+
417
+ def _append_on_sequence(
418
+ self, sequence: Sequence[str], item: str
419
+ ) -> Sequence[str]:
420
+ """Helper method to append an item to a sequence if it's not already
421
+ present.
422
+
423
+ Parameters
424
+ ----------
425
+ sequence
426
+ The original sequence to append to.
427
+ item
428
+ The item to append if not already in the sequence.
429
+
430
+ Returns
431
+ -------
432
+ A new sequence with the item appended if it was not already
433
+ present.
434
+ """
435
+ if item not in sequence:
436
+ return list(sequence) + [item]
437
+ return sequence