infrahub-server 1.1.0b2__py3-none-any.whl → 1.1.2__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.
Files changed (244) hide show
  1. infrahub/api/artifact.py +8 -15
  2. infrahub/api/dependencies.py +15 -2
  3. infrahub/api/diff/diff.py +2 -2
  4. infrahub/api/file.py +2 -7
  5. infrahub/api/menu.py +9 -7
  6. infrahub/api/oauth2.py +3 -3
  7. infrahub/api/oidc.py +3 -3
  8. infrahub/api/query.py +4 -4
  9. infrahub/api/schema.py +24 -41
  10. infrahub/api/transformation.py +3 -3
  11. infrahub/auth.py +0 -2
  12. infrahub/cli/db.py +9 -11
  13. infrahub/cli/tasks.py +3 -1
  14. infrahub/computed_attribute/constants.py +2 -2
  15. infrahub/computed_attribute/models.py +20 -8
  16. infrahub/computed_attribute/tasks.py +136 -61
  17. infrahub/config.py +18 -4
  18. infrahub/core/account.py +8 -11
  19. infrahub/core/attribute.py +15 -0
  20. infrahub/core/branch/constants.py +2 -0
  21. infrahub/core/branch/models.py +3 -2
  22. infrahub/core/branch/tasks.py +150 -52
  23. infrahub/core/constraint/node/runner.py +11 -10
  24. infrahub/core/diff/calculator.py +4 -3
  25. infrahub/core/diff/combiner.py +1 -0
  26. infrahub/core/diff/coordinator.py +309 -105
  27. infrahub/core/diff/data_check_synchronizer.py +33 -4
  28. infrahub/core/diff/enricher/hierarchy.py +2 -0
  29. infrahub/core/diff/merger/merger.py +1 -1
  30. infrahub/core/diff/model/path.py +101 -3
  31. infrahub/core/diff/query/artifact.py +52 -28
  32. infrahub/core/diff/query/field_specifiers.py +35 -0
  33. infrahub/core/diff/query/roots_metadata.py +48 -0
  34. infrahub/core/diff/query/save.py +1 -0
  35. infrahub/core/diff/query_parser.py +52 -19
  36. infrahub/core/diff/repository/deserializer.py +7 -3
  37. infrahub/core/diff/repository/repository.py +98 -7
  38. infrahub/core/diff/tasks.py +29 -25
  39. infrahub/core/graph/__init__.py +1 -1
  40. infrahub/core/graph/constraints.py +4 -2
  41. infrahub/core/integrity/object_conflict/conflict_recorder.py +6 -1
  42. infrahub/core/manager.py +157 -43
  43. infrahub/core/merge.py +3 -2
  44. infrahub/core/migrations/graph/__init__.py +4 -0
  45. infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -6
  46. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +7 -5
  47. infrahub/core/migrations/graph/m014_remove_index_attr_value.py +1 -1
  48. infrahub/core/migrations/graph/m015_diff_format_update.py +1 -1
  49. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -1
  50. infrahub/core/migrations/graph/m017_add_core_profile.py +44 -0
  51. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +101 -0
  52. infrahub/core/migrations/query/attribute_add.py +2 -1
  53. infrahub/core/migrations/query/delete_element_in_schema.py +2 -1
  54. infrahub/core/migrations/query/node_duplicate.py +2 -1
  55. infrahub/core/migrations/query/relationship_duplicate.py +2 -1
  56. infrahub/core/migrations/query/schema_attribute_update.py +1 -1
  57. infrahub/core/migrations/schema/tasks.py +2 -2
  58. infrahub/core/migrations/shared.py +3 -3
  59. infrahub/core/node/__init__.py +34 -7
  60. infrahub/core/node/constraints/grouped_uniqueness.py +9 -2
  61. infrahub/core/query/__init__.py +12 -6
  62. infrahub/core/query/diff.py +23 -17
  63. infrahub/core/query/ipam.py +9 -4
  64. infrahub/core/query/node.py +161 -65
  65. infrahub/core/query/relationship.py +19 -17
  66. infrahub/core/query/resource_manager.py +21 -11
  67. infrahub/core/query/standard_node.py +13 -19
  68. infrahub/core/registry.py +0 -1
  69. infrahub/core/relationship/model.py +17 -5
  70. infrahub/core/schema/definitions/internal.py +1 -1
  71. infrahub/core/schema/generated/attribute_schema.py +2 -2
  72. infrahub/core/schema/generated/base_node_schema.py +2 -2
  73. infrahub/core/schema/generated/relationship_schema.py +1 -1
  74. infrahub/core/schema/schema_branch.py +16 -48
  75. infrahub/core/schema/schema_branch_computed.py +15 -4
  76. infrahub/core/task/task_log.py +1 -1
  77. infrahub/core/utils.py +5 -4
  78. infrahub/core/validators/interface.py +1 -2
  79. infrahub/core/validators/shared.py +2 -2
  80. infrahub/core/validators/tasks.py +8 -7
  81. infrahub/core/validators/uniqueness/index.py +2 -4
  82. infrahub/core/validators/uniqueness/query.py +2 -1
  83. infrahub/database/__init__.py +37 -20
  84. infrahub/database/index.py +1 -1
  85. infrahub/database/neo4j.py +3 -1
  86. infrahub/dependencies/builder/diff/data_check_synchronizer.py +2 -0
  87. infrahub/events/branch_action.py +13 -14
  88. infrahub/events/node_action.py +10 -10
  89. infrahub/events/schema_action.py +6 -5
  90. infrahub/exceptions.py +29 -16
  91. infrahub/generators/tasks.py +2 -1
  92. infrahub/git/base.py +106 -32
  93. infrahub/git/integrator.py +34 -3
  94. infrahub/git/models.py +5 -1
  95. infrahub/git/repository.py +19 -65
  96. infrahub/git/tasks.py +43 -22
  97. infrahub/graphql/app.py +2 -2
  98. infrahub/graphql/auth/query_permission_checker/default_branch_checker.py +2 -17
  99. infrahub/graphql/auth/query_permission_checker/merge_operation_checker.py +1 -12
  100. infrahub/graphql/auth/query_permission_checker/object_permission_checker.py +6 -40
  101. infrahub/graphql/auth/query_permission_checker/super_admin_checker.py +5 -8
  102. infrahub/graphql/enums.py +2 -2
  103. infrahub/graphql/initialization.py +30 -8
  104. infrahub/graphql/loaders/node.py +82 -0
  105. infrahub/graphql/manager.py +10 -4
  106. infrahub/graphql/models.py +6 -0
  107. infrahub/graphql/mutations/account.py +10 -6
  108. infrahub/graphql/mutations/branch.py +0 -2
  109. infrahub/graphql/mutations/computed_attribute.py +14 -23
  110. infrahub/graphql/mutations/diff.py +2 -0
  111. infrahub/graphql/mutations/ipam.py +13 -8
  112. infrahub/graphql/mutations/main.py +83 -8
  113. infrahub/graphql/mutations/proposed_change.py +11 -20
  114. infrahub/graphql/mutations/repository.py +3 -0
  115. infrahub/graphql/mutations/resource_manager.py +3 -0
  116. infrahub/graphql/mutations/schema.py +8 -7
  117. infrahub/graphql/mutations/tasks.py +36 -37
  118. infrahub/graphql/permissions.py +3 -4
  119. infrahub/graphql/queries/account.py +2 -11
  120. infrahub/graphql/queries/task.py +2 -12
  121. infrahub/graphql/query.py +3 -1
  122. infrahub/graphql/{resolver.py → resolvers/resolver.py} +14 -61
  123. infrahub/graphql/resolvers/single_relationship.py +169 -0
  124. infrahub/graphql/schema.py +0 -6
  125. infrahub/graphql/subscription/graphql_query.py +7 -1
  126. infrahub/graphql/types/task.py +14 -2
  127. infrahub/groups/tasks.py +9 -3
  128. infrahub/log.py +4 -0
  129. infrahub/menu/generator.py +6 -18
  130. infrahub/message_bus/messages/check_generator_run.py +1 -0
  131. infrahub/message_bus/messages/event_node_mutated.py +2 -2
  132. infrahub/message_bus/messages/refresh_git_fetch.py +1 -0
  133. infrahub/message_bus/messages/request_repository_userchecks.py +2 -1
  134. infrahub/message_bus/operations/check/artifact.py +3 -1
  135. infrahub/message_bus/operations/check/generator.py +20 -2
  136. infrahub/message_bus/operations/check/repository.py +31 -21
  137. infrahub/message_bus/operations/event/branch.py +2 -4
  138. infrahub/message_bus/operations/git/file.py +1 -3
  139. infrahub/message_bus/operations/git/repository.py +7 -2
  140. infrahub/message_bus/operations/requests/artifact_definition.py +6 -5
  141. infrahub/message_bus/operations/requests/generator_definition.py +10 -6
  142. infrahub/message_bus/operations/requests/proposed_change.py +31 -13
  143. infrahub/message_bus/operations/requests/repository.py +15 -13
  144. infrahub/message_bus/types.py +1 -1
  145. infrahub/permissions/__init__.py +12 -3
  146. infrahub/permissions/backend.py +2 -17
  147. infrahub/permissions/constants.py +12 -8
  148. infrahub/permissions/local_backend.py +5 -102
  149. infrahub/permissions/manager.py +135 -0
  150. infrahub/permissions/report.py +14 -25
  151. infrahub/permissions/types.py +6 -0
  152. infrahub/proposed_change/tasks.py +12 -9
  153. infrahub/server.py +3 -1
  154. infrahub/services/scheduler.py +1 -3
  155. infrahub/task_manager/models.py +34 -5
  156. infrahub/task_manager/task.py +10 -6
  157. infrahub/tasks/dummy.py +3 -0
  158. infrahub/tasks/telemetry.py +26 -24
  159. infrahub/transformations/tasks.py +2 -0
  160. infrahub/visuals.py +1 -3
  161. infrahub/webhook/models.py +6 -2
  162. infrahub/webhook/tasks.py +1 -1
  163. infrahub/workers/infrahub_async.py +14 -0
  164. infrahub/workflows/catalogue.py +24 -5
  165. infrahub/workflows/utils.py +11 -3
  166. infrahub_sdk/_importer.py +13 -4
  167. infrahub_sdk/analyzer.py +9 -7
  168. infrahub_sdk/batch.py +54 -8
  169. infrahub_sdk/branch.py +22 -20
  170. infrahub_sdk/checks.py +14 -41
  171. infrahub_sdk/client.py +769 -575
  172. infrahub_sdk/code_generator.py +21 -17
  173. infrahub_sdk/config.py +15 -13
  174. infrahub_sdk/ctl/branch.py +1 -1
  175. infrahub_sdk/ctl/check.py +15 -11
  176. infrahub_sdk/ctl/cli_commands.py +122 -21
  177. infrahub_sdk/ctl/client.py +18 -16
  178. infrahub_sdk/ctl/config.py +6 -7
  179. infrahub_sdk/ctl/exporter.py +2 -3
  180. infrahub_sdk/ctl/generator.py +11 -8
  181. infrahub_sdk/ctl/render.py +1 -1
  182. infrahub_sdk/ctl/repository.py +9 -7
  183. infrahub_sdk/ctl/schema.py +8 -4
  184. infrahub_sdk/ctl/transform.py +1 -1
  185. infrahub_sdk/ctl/utils.py +13 -9
  186. infrahub_sdk/ctl/validate.py +3 -2
  187. infrahub_sdk/data.py +5 -4
  188. infrahub_sdk/exceptions.py +14 -14
  189. infrahub_sdk/generator.py +9 -10
  190. infrahub_sdk/graphql.py +9 -4
  191. infrahub_sdk/groups.py +1 -3
  192. infrahub_sdk/node.py +188 -152
  193. infrahub_sdk/object_store.py +5 -5
  194. infrahub_sdk/playback.py +11 -7
  195. infrahub_sdk/protocols_base.py +30 -30
  196. infrahub_sdk/pytest_plugin/items/base.py +21 -7
  197. infrahub_sdk/pytest_plugin/items/check.py +11 -7
  198. infrahub_sdk/pytest_plugin/items/graphql_query.py +2 -2
  199. infrahub_sdk/pytest_plugin/items/jinja2_transform.py +5 -5
  200. infrahub_sdk/pytest_plugin/items/python_transform.py +12 -7
  201. infrahub_sdk/pytest_plugin/loader.py +2 -2
  202. infrahub_sdk/pytest_plugin/models.py +10 -10
  203. infrahub_sdk/pytest_plugin/plugin.py +3 -2
  204. infrahub_sdk/pytest_plugin/utils.py +1 -1
  205. infrahub_sdk/queries.py +69 -0
  206. infrahub_sdk/query_groups.py +17 -17
  207. infrahub_sdk/schema/__init__.py +687 -0
  208. infrahub_sdk/schema/main.py +343 -0
  209. infrahub_sdk/schema/repository.py +237 -0
  210. infrahub_sdk/spec/menu.py +2 -2
  211. infrahub_sdk/spec/object.py +14 -10
  212. infrahub_sdk/store.py +21 -23
  213. infrahub_sdk/task_report.py +20 -22
  214. infrahub_sdk/testing/__init__.py +0 -0
  215. infrahub_sdk/testing/docker.py +18 -0
  216. infrahub_sdk/testing/repository.py +107 -0
  217. infrahub_sdk/testing/schemas/__init__.py +0 -0
  218. infrahub_sdk/testing/schemas/animal.py +182 -0
  219. infrahub_sdk/testing/schemas/car_person.py +252 -0
  220. infrahub_sdk/timestamp.py +1 -2
  221. infrahub_sdk/transfer/exporter/interface.py +3 -2
  222. infrahub_sdk/transfer/exporter/json.py +13 -10
  223. infrahub_sdk/transfer/importer/json.py +9 -6
  224. infrahub_sdk/transfer/schema_sorter.py +8 -4
  225. infrahub_sdk/transforms.py +5 -46
  226. infrahub_sdk/types.py +19 -10
  227. infrahub_sdk/utils.py +22 -5
  228. infrahub_sdk/uuidt.py +5 -6
  229. infrahub_sdk/yaml.py +8 -6
  230. {infrahub_server-1.1.0b2.dist-info → infrahub_server-1.1.2.dist-info}/METADATA +3 -2
  231. {infrahub_server-1.1.0b2.dist-info → infrahub_server-1.1.2.dist-info}/RECORD +240 -225
  232. infrahub_testcontainers/__init__.py +6 -0
  233. {infrahub/testing → infrahub_testcontainers}/container.py +28 -22
  234. {infrahub/testing → infrahub_testcontainers}/docker-compose.test.yml +33 -40
  235. {infrahub/testing → infrahub_testcontainers}/helpers.py +31 -26
  236. infrahub/core/diff/query/empty_roots.py +0 -33
  237. infrahub/graphql/mutations/task.py +0 -147
  238. infrahub/testing/schemas/car_person.py +0 -125
  239. infrahub_sdk/schema.py +0 -1080
  240. /infrahub/{testing → graphql/loaders}/__init__.py +0 -0
  241. /infrahub/{testing/schemas → graphql/resolvers}/__init__.py +0 -0
  242. {infrahub_server-1.1.0b2.dist-info → infrahub_server-1.1.2.dist-info}/LICENSE.txt +0 -0
  243. {infrahub_server-1.1.0b2.dist-info → infrahub_server-1.1.2.dist-info}/WHEEL +0 -0
  244. {infrahub_server-1.1.0b2.dist-info → infrahub_server-1.1.2.dist-info}/entry_points.txt +0 -0
@@ -2,21 +2,23 @@ from typing import Optional
2
2
 
3
3
  from infrahub_sdk.uuidt import UUIDT
4
4
  from prefect import flow
5
+ from prefect.logging import get_run_logger
5
6
 
6
7
  from infrahub.core.constants import InfrahubKind, ValidatorConclusion, ValidatorState
7
8
  from infrahub.core.timestamp import Timestamp
8
9
  from infrahub.message_bus import InfrahubMessage, Meta, messages
9
10
  from infrahub.message_bus.types import KVTTL
10
11
  from infrahub.services import InfrahubServices
12
+ from infrahub.workflows.utils import add_tags
11
13
 
12
14
 
13
- @flow(name="generator-definition-check")
15
+ @flow(
16
+ name="generator-definition-check",
17
+ flow_run_name="Validate Generator selection for {message.generator_definition.definition_name}",
18
+ )
14
19
  async def check(message: messages.RequestGeneratorDefinitionCheck, service: InfrahubServices) -> None:
15
- service.log.info(
16
- "Validating Generator selection",
17
- generator_definition=message.generator_definition.definition_id,
18
- source_branch=message.source_branch,
19
- )
20
+ log = get_run_logger()
21
+ await add_tags(branches=[message.source_branch], nodes=[message.proposed_change])
20
22
  events: list[InfrahubMessage] = []
21
23
 
22
24
  proposed_change = await service.client.get(kind=InfrahubKind.PROPOSEDCHANGE, id=message.proposed_change)
@@ -87,6 +89,7 @@ async def check(message: messages.RequestGeneratorDefinitionCheck, service: Infr
87
89
  check_execution_id = str(UUIDT())
88
90
  check_execution_ids.append(check_execution_id)
89
91
  requested_instances += 1
92
+ log.info(f"Trigger execution of {message.generator_definition.definition_name} for {member.display_label}")
90
93
  events.append(
91
94
  messages.CheckGeneratorRun(
92
95
  generator_definition=message.generator_definition,
@@ -101,6 +104,7 @@ async def check(message: messages.RequestGeneratorDefinitionCheck, service: Infr
101
104
  target_id=member.id,
102
105
  target_name=member.display_label,
103
106
  validator_id=validator.id,
107
+ proposed_change=message.proposed_change,
104
108
  meta=Meta(validator_execution_id=validator_execution_id, check_execution_id=check_execution_id),
105
109
  )
106
110
  )
@@ -2,7 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  from enum import IntFlag
4
4
 
5
- from prefect import flow
5
+ from prefect import flow, task
6
+ from prefect.logging import get_run_logger
6
7
  from pydantic import BaseModel
7
8
 
8
9
  from infrahub import lock
@@ -11,7 +12,6 @@ from infrahub.core.diff.coordinator import DiffCoordinator
11
12
  from infrahub.core.registry import registry
12
13
  from infrahub.dependencies.registry import get_component_registry
13
14
  from infrahub.git.repository import InfrahubRepository
14
- from infrahub.log import get_logger
15
15
  from infrahub.message_bus import InfrahubMessage, messages
16
16
  from infrahub.message_bus.types import (
17
17
  ProposedChangeArtifactDefinition,
@@ -26,7 +26,7 @@ from infrahub.proposed_change.models import (
26
26
  RequestProposedChangeSchemaIntegrity,
27
27
  RequestProposedChangeUserTests,
28
28
  )
29
- from infrahub.services import InfrahubServices # noqa: TCH001
29
+ from infrahub.services import InfrahubServices # noqa: TC001
30
30
  from infrahub.workflows.catalogue import (
31
31
  REQUEST_PROPOSED_CHANGE_DATA_INTEGRITY,
32
32
  REQUEST_PROPOSED_CHANGE_REPOSITORY_CHECKS,
@@ -34,8 +34,7 @@ from infrahub.workflows.catalogue import (
34
34
  REQUEST_PROPOSED_CHANGE_SCHEMA_INTEGRITY,
35
35
  REQUEST_PROPOSED_CHANGE_USER_TESTS,
36
36
  )
37
-
38
- log = get_logger()
37
+ from infrahub.workflows.utils import add_tags
39
38
 
40
39
 
41
40
  class DefinitionSelect(IntFlag):
@@ -64,13 +63,15 @@ class DefinitionSelect(IntFlag):
64
63
  return "Doesn't require changes due to no relevant modified kinds or file changes in Git"
65
64
 
66
65
 
67
- @flow(name="proposed-changed-pipeline")
66
+ @flow(name="proposed-changed-pipeline", flow_run_name="Execute Pipeline")
68
67
  async def pipeline(message: messages.RequestProposedChangePipeline, service: InfrahubServices) -> None:
69
68
  events: list[InfrahubMessage] = []
70
69
 
71
70
  repositories = await _get_proposed_change_repositories(message=message, service=service)
72
71
 
73
- if message.source_branch_sync_with_git and await _validate_repository_merge_conflicts(repositories=repositories):
72
+ if message.source_branch_sync_with_git and await _validate_repository_merge_conflicts(
73
+ repositories=repositories, service=service
74
+ ):
74
75
  for repo in repositories:
75
76
  if not repo.read_only and repo.internal_status == RepositoryInternalStatus.ACTIVE.value:
76
77
  events.append(
@@ -86,7 +87,7 @@ async def pipeline(message: messages.RequestProposedChangePipeline, service: Inf
86
87
  await service.send(message=event)
87
88
  return
88
89
 
89
- await _gather_repository_repository_diffs(repositories=repositories)
90
+ await _gather_repository_repository_diffs(repositories=repositories, service=service)
90
91
 
91
92
  async with service.database.start_transaction() as dbt:
92
93
  destination_branch = await registry.get_branch(db=dbt, branch=message.destination_branch)
@@ -187,9 +188,12 @@ async def pipeline(message: messages.RequestProposedChangePipeline, service: Inf
187
188
 
188
189
  @flow(
189
190
  name="proposed-changed-refresh-artifact",
190
- flow_run_name="Refreshing artifacts for change_proposal={message.proposed_change}",
191
+ flow_run_name="Trigger artifacts refresh",
191
192
  )
192
193
  async def refresh_artifacts(message: messages.RequestProposedChangeRefreshArtifacts, service: InfrahubServices) -> None:
194
+ await add_tags(branches=[message.source_branch], nodes=[message.proposed_change])
195
+ log = get_run_logger()
196
+
193
197
  definition_information = await service.client.execute_graphql(
194
198
  query=GATHER_ARTIFACT_DEFINITIONS,
195
199
  branch_name=message.source_branch,
@@ -227,6 +231,7 @@ async def refresh_artifacts(message: messages.RequestProposedChangeRefreshArtifa
227
231
  )
228
232
 
229
233
  if select:
234
+ log.info(f"Trigger processing of {artifact_definition.definition_name}")
230
235
  msg = messages.RequestArtifactDefinitionCheck(
231
236
  artifact_definition=artifact_definition,
232
237
  branch_diff=message.branch_diff,
@@ -511,26 +516,39 @@ async def _get_proposed_change_repositories(
511
516
  return _parse_proposed_change_repositories(message=message, source=source_all, destination=destination_all)
512
517
 
513
518
 
514
- async def _validate_repository_merge_conflicts(repositories: list[ProposedChangeRepository]) -> bool:
519
+ @task(name="proposed-change-validate-repository-conflicts", task_run_name="Validate conflicts on repository")
520
+ async def _validate_repository_merge_conflicts(
521
+ repositories: list[ProposedChangeRepository], service: InfrahubServices
522
+ ) -> bool:
523
+ log = get_run_logger()
515
524
  conflicts = False
516
525
  for repo in repositories:
517
526
  if repo.has_diff and not repo.is_staging:
518
- git_repo = await InfrahubRepository.init(id=repo.repository_id, name=repo.repository_name)
527
+ git_repo = await InfrahubRepository.init(
528
+ id=repo.repository_id, name=repo.repository_name, client=service.client
529
+ )
519
530
  async with lock.registry.get(name=repo.repository_name, namespace="repository"):
520
531
  repo.conflicts = await git_repo.get_conflicts(
521
532
  source_branch=repo.source_branch, dest_branch=repo.destination_branch
522
533
  )
523
534
  if repo.conflicts:
535
+ log.info(f"{len(repo.conflicts)} conflict(s) identified on {repo.repository_name}")
524
536
  conflicts = True
537
+ else:
538
+ log.info(f"no conflict identified for {repo.repository_name}")
525
539
 
526
540
  return conflicts
527
541
 
528
542
 
529
- async def _gather_repository_repository_diffs(repositories: list[ProposedChangeRepository]) -> None:
543
+ async def _gather_repository_repository_diffs(
544
+ repositories: list[ProposedChangeRepository], service: InfrahubServices
545
+ ) -> None:
530
546
  for repo in repositories:
531
547
  if repo.has_diff and repo.source_commit and repo.destination_commit:
532
548
  # TODO we need to find a way to return all files in the repo if the repo is new
533
- git_repo = await InfrahubRepository.init(id=repo.repository_id, name=repo.repository_name)
549
+ git_repo = await InfrahubRepository.init(
550
+ id=repo.repository_id, name=repo.repository_name, client=service.client
551
+ )
534
552
 
535
553
  files_changed: list[str] = []
536
554
  files_added: list[str] = []
@@ -1,24 +1,22 @@
1
- from typing import List
2
-
3
1
  from infrahub_sdk.uuidt import UUIDT
4
2
  from prefect import flow
3
+ from prefect.logging import get_run_logger
5
4
 
6
5
  from infrahub.core.constants import InfrahubKind
7
6
  from infrahub.core.timestamp import Timestamp
8
- from infrahub.log import get_logger
9
7
  from infrahub.message_bus import InfrahubMessage, messages
10
8
  from infrahub.message_bus.types import KVTTL
11
9
  from infrahub.services import InfrahubServices
12
-
13
- log = get_logger()
10
+ from infrahub.workflows.utils import add_tags
14
11
 
15
12
 
16
- @flow(name="repository-check")
13
+ @flow(name="repository-check", flow_run_name="Running repository checks for repository {message.repository}")
17
14
  async def checks(message: messages.RequestRepositoryChecks, service: InfrahubServices) -> None:
18
15
  """Request to start validation checks on a specific repository."""
19
- log.info("Running repository checks", repository_id=message.repository, proposed_change_id=message.proposed_change)
16
+ await add_tags(branches=[message.source_branch], nodes=[message.proposed_change])
17
+ log = get_run_logger()
20
18
 
21
- events: List[InfrahubMessage] = []
19
+ events: list[InfrahubMessage] = []
22
20
 
23
21
  repository = await service.client.get(
24
22
  kind=InfrahubKind.GENERICREPOSITORY, id=message.repository, branch=message.source_branch
@@ -26,7 +24,7 @@ async def checks(message: messages.RequestRepositoryChecks, service: InfrahubSer
26
24
  proposed_change = await service.client.get(kind=InfrahubKind.PROPOSEDCHANGE, id=message.proposed_change)
27
25
 
28
26
  validator_execution_id = str(UUIDT())
29
- check_execution_ids: List[str] = []
27
+ check_execution_ids: list[str] = []
30
28
  await proposed_change.validations.fetch()
31
29
  await repository.checks.fetch()
32
30
 
@@ -76,7 +74,7 @@ async def checks(message: messages.RequestRepositoryChecks, service: InfrahubSer
76
74
  )
77
75
 
78
76
  checks_in_execution = ",".join(check_execution_ids)
79
- log.info("Checks in execution", checks=checks_in_execution)
77
+ log.info(f"Checks in execution {checks_in_execution}")
80
78
  await service.cache.set(
81
79
  key=f"validator_execution_id:{validator_execution_id}:checks",
82
80
  value=checks_in_execution,
@@ -98,14 +96,18 @@ async def checks(message: messages.RequestRepositoryChecks, service: InfrahubSer
98
96
 
99
97
  @flow(
100
98
  name="repository-users-check",
101
- flow_run_name="Evaluating user-defined checks on repository {message.repository} and proposed change {message.proposed_change}",
99
+ flow_run_name="Evaluating user-defined checks on repository {message.repository_name}",
102
100
  )
103
101
  async def user_checks(message: messages.RequestRepositoryUserChecks, service: InfrahubServices) -> None:
104
102
  """Request to start validation checks on a specific repository for User-defined checks."""
105
- events: List[InfrahubMessage] = []
103
+
104
+ await add_tags(branches=[message.source_branch], nodes=[message.proposed_change])
105
+ log = get_run_logger()
106
+
107
+ events: list[InfrahubMessage] = []
106
108
 
107
109
  repository = await service.client.get(
108
- kind=InfrahubKind.GENERICREPOSITORY, id=message.repository, branch=message.source_branch, fragment=True
110
+ kind=InfrahubKind.GENERICREPOSITORY, id=message.repository_id, branch=message.source_branch, fragment=True
109
111
  )
110
112
  await repository.checks.fetch()
111
113
 
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import re
4
4
  from enum import Enum
5
5
 
6
- from infrahub_sdk.diff import NodeDiff # noqa: TCH002
6
+ from infrahub_sdk.diff import NodeDiff # noqa: TC002
7
7
  from pydantic import BaseModel, Field
8
8
 
9
9
  from infrahub.core.constants import InfrahubKind, RepositoryInternalStatus
@@ -1,4 +1,13 @@
1
- from .backend import PermissionBackend
2
- from .local_backend import LocalPermissionBackend
1
+ from infrahub.permissions.backend import PermissionBackend
2
+ from infrahub.permissions.local_backend import LocalPermissionBackend
3
+ from infrahub.permissions.manager import PermissionManager
4
+ from infrahub.permissions.report import report_schema_permissions
5
+ from infrahub.permissions.types import AssignedPermissions
3
6
 
4
- __all__ = ["LocalPermissionBackend", "PermissionBackend"]
7
+ __all__ = [
8
+ "AssignedPermissions",
9
+ "LocalPermissionBackend",
10
+ "PermissionBackend",
11
+ "PermissionManager",
12
+ "report_schema_permissions",
13
+ ]
@@ -5,28 +5,13 @@ from typing import TYPE_CHECKING
5
5
 
6
6
  if TYPE_CHECKING:
7
7
  from infrahub.auth import AccountSession
8
- from infrahub.core.account import GlobalPermission, ObjectPermission
9
8
  from infrahub.core.branch import Branch
10
9
  from infrahub.database import InfrahubDatabase
11
- from infrahub.permissions.constants import AssignedPermissions, PermissionDecisionFlag
10
+ from infrahub.permissions.types import AssignedPermissions
12
11
 
13
12
 
14
13
  class PermissionBackend(ABC):
15
14
  @abstractmethod
16
15
  async def load_permissions(
17
- self, db: InfrahubDatabase, account_session: AccountSession, branch: Branch
16
+ self, db: InfrahubDatabase, branch: Branch, account_session: AccountSession
18
17
  ) -> AssignedPermissions: ...
19
-
20
- @abstractmethod
21
- def report_object_permission(
22
- self, permissions: list[ObjectPermission], namespace: str, name: str, action: str
23
- ) -> PermissionDecisionFlag: ...
24
-
25
- @abstractmethod
26
- async def has_permission(
27
- self,
28
- db: InfrahubDatabase,
29
- account_session: AccountSession,
30
- permission: GlobalPermission | ObjectPermission,
31
- branch: Branch,
32
- ) -> bool: ...
@@ -1,15 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from enum import IntFlag, StrEnum, auto
4
- from typing import TYPE_CHECKING, TypedDict
5
4
 
6
- if TYPE_CHECKING:
7
- from infrahub.core.account import GlobalPermission, ObjectPermission
8
-
9
-
10
- class AssignedPermissions(TypedDict):
11
- global_permissions: list[GlobalPermission]
12
- object_permissions: list[ObjectPermission]
5
+ from infrahub.core.constants import GlobalPermissions
13
6
 
14
7
 
15
8
  class PermissionDecisionFlag(IntFlag):
@@ -26,3 +19,14 @@ class BranchRelativePermissionDecision(StrEnum):
26
19
  ALLOW = auto()
27
20
  ALLOW_DEFAULT = auto()
28
21
  ALLOW_OTHER = auto()
22
+
23
+
24
+ GLOBAL_PERMISSION_DENIAL_MESSAGE = {
25
+ GlobalPermissions.EDIT_DEFAULT_BRANCH.value: "You are not allowed to change data in the default branch",
26
+ GlobalPermissions.MERGE_BRANCH.value: "You are not allowed to merge a branch",
27
+ GlobalPermissions.MERGE_PROPOSED_CHANGE.value: "You are not allowed to merge proposed changes",
28
+ GlobalPermissions.MANAGE_SCHEMA.value: "You are not allowed to manage the schema",
29
+ GlobalPermissions.MANAGE_ACCOUNTS.value: "You are not allowed to manage user accounts, groups or roles",
30
+ GlobalPermissions.MANAGE_PERMISSIONS.value: "You are not allowed to manage permissions",
31
+ GlobalPermissions.MANAGE_REPOSITORIES.value: "You are not allowed to manage repositories",
32
+ }
@@ -3,11 +3,9 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING
4
4
 
5
5
  from infrahub import config
6
- from infrahub.core.account import GlobalPermission, ObjectPermission, fetch_permissions, fetch_role_permissions
7
- from infrahub.core.constants import GlobalPermissions, PermissionDecision
6
+ from infrahub.core.account import fetch_permissions, fetch_role_permissions
8
7
  from infrahub.core.manager import NodeManager
9
8
  from infrahub.core.protocols import CoreAccountRole
10
- from infrahub.permissions.constants import PermissionDecisionFlag
11
9
 
12
10
  from .backend import PermissionBackend
13
11
 
@@ -15,79 +13,14 @@ if TYPE_CHECKING:
15
13
  from infrahub.auth import AccountSession
16
14
  from infrahub.core.branch import Branch
17
15
  from infrahub.database import InfrahubDatabase
18
- from infrahub.permissions.constants import AssignedPermissions
16
+ from infrahub.permissions.types import AssignedPermissions
19
17
 
18
+ __all__ = ["LocalPermissionBackend"]
20
19
 
21
- class LocalPermissionBackend(PermissionBackend):
22
- wildcard_values = ["*"]
23
- wildcard_actions = ["any"]
24
-
25
- def _compute_specificity(self, permission: ObjectPermission) -> int:
26
- specificity = 0
27
- if permission.namespace not in self.wildcard_values:
28
- specificity += 1
29
- if permission.name not in self.wildcard_values:
30
- specificity += 1
31
- if permission.action not in self.wildcard_actions:
32
- specificity += 1
33
- if not permission.decision & PermissionDecisionFlag.ALLOW_ALL:
34
- specificity += 1
35
- return specificity
36
-
37
- def report_object_permission(
38
- self, permissions: list[ObjectPermission], namespace: str, name: str, action: str
39
- ) -> PermissionDecisionFlag:
40
- """Given a set of permissions, return the permission decision for a given kind and action."""
41
- highest_specificity: int = -1
42
- combined_decision = PermissionDecisionFlag.DENY
43
-
44
- for permission in permissions:
45
- if (
46
- permission.namespace in [namespace, *self.wildcard_values]
47
- and permission.name in [name, *self.wildcard_values]
48
- and permission.action in [action, *self.wildcard_actions]
49
- ):
50
- permission_decision = PermissionDecisionFlag(value=permission.decision)
51
- # Compute the specifity of a permission to keep the decision of the most specific if two or more permissions overlap
52
- specificity = self._compute_specificity(permission=permission)
53
- if specificity > highest_specificity:
54
- combined_decision = permission_decision
55
- highest_specificity = specificity
56
- elif specificity == highest_specificity and permission_decision != PermissionDecisionFlag.DENY:
57
- combined_decision |= permission_decision
58
-
59
- return combined_decision
60
-
61
- def resolve_object_permission(
62
- self, permissions: list[ObjectPermission], permission_to_check: ObjectPermission
63
- ) -> bool:
64
- """Compute the permissions and check if the one provided is allowed."""
65
- required_decision = PermissionDecisionFlag(value=permission_to_check.decision)
66
- combined_decision = self.report_object_permission(
67
- permissions=permissions,
68
- namespace=permission_to_check.namespace,
69
- name=permission_to_check.name,
70
- action=permission_to_check.action,
71
- )
72
-
73
- return combined_decision & required_decision == required_decision
74
-
75
- def resolve_global_permission(
76
- self, permissions: list[GlobalPermission], permission_to_check: GlobalPermission
77
- ) -> bool:
78
- grant_permission = False
79
-
80
- for permission in permissions:
81
- if permission.action == permission_to_check.action:
82
- # Early exit on deny as deny preempt allow
83
- if permission.decision == PermissionDecisionFlag.DENY:
84
- return False
85
- grant_permission = True
86
-
87
- return grant_permission
88
20
 
21
+ class LocalPermissionBackend(PermissionBackend):
89
22
  async def load_permissions(
90
- self, db: InfrahubDatabase, account_session: AccountSession, branch: Branch
23
+ self, db: InfrahubDatabase, branch: Branch, account_session: AccountSession
91
24
  ) -> AssignedPermissions:
92
25
  if not account_session.authenticated:
93
26
  anonymous_permissions: AssignedPermissions = {"global_permissions": [], "object_permissions": []}
@@ -103,33 +36,3 @@ class LocalPermissionBackend(PermissionBackend):
103
36
  return anonymous_permissions
104
37
 
105
38
  return await fetch_permissions(db=db, account_id=account_session.account_id, branch=branch)
106
-
107
- async def has_permission(
108
- self,
109
- db: InfrahubDatabase,
110
- account_session: AccountSession,
111
- permission: GlobalPermission | ObjectPermission,
112
- branch: Branch,
113
- ) -> bool:
114
- granted_permissions = await self.load_permissions(db=db, account_session=account_session, branch=branch)
115
- is_super_admin = self.resolve_global_permission(
116
- permissions=granted_permissions["global_permissions"],
117
- permission_to_check=GlobalPermission(
118
- action=GlobalPermissions.SUPER_ADMIN, decision=PermissionDecision.ALLOW_ALL
119
- ),
120
- )
121
-
122
- if isinstance(permission, GlobalPermission):
123
- return (
124
- self.resolve_global_permission(
125
- permissions=granted_permissions["global_permissions"], permission_to_check=permission
126
- )
127
- or is_super_admin
128
- )
129
-
130
- return (
131
- self.resolve_object_permission(
132
- permissions=granted_permissions["object_permissions"], permission_to_check=permission
133
- )
134
- or is_super_admin
135
- )
@@ -0,0 +1,135 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Sequence
4
+
5
+ from infrahub.core import registry
6
+ from infrahub.core.account import GlobalPermission
7
+ from infrahub.core.constants import GlobalPermissions, PermissionDecision
8
+ from infrahub.exceptions import PermissionDeniedError
9
+ from infrahub.permissions.constants import GLOBAL_PERMISSION_DENIAL_MESSAGE, PermissionDecisionFlag
10
+
11
+ if TYPE_CHECKING:
12
+ from infrahub.auth import AccountSession
13
+ from infrahub.core.account import ObjectPermission
14
+ from infrahub.core.branch import Branch
15
+ from infrahub.database import InfrahubDatabase
16
+ from infrahub.permissions.types import AssignedPermissions
17
+
18
+ __all__ = ["PermissionManager"]
19
+
20
+
21
+ class PermissionManager:
22
+ wildcard_values = ["*"]
23
+ wildcard_actions = ["any"]
24
+
25
+ def __init__(self, account_session: AccountSession) -> None:
26
+ self.account_session = account_session
27
+ self.permissions: AssignedPermissions = {"global_permissions": [], "object_permissions": []}
28
+
29
+ async def load_permissions(self, db: InfrahubDatabase, branch: Branch) -> None:
30
+ """Load permissions from the configured backends into memory."""
31
+ for permission_backend in registry.permission_backends:
32
+ backend_permissions = await permission_backend.load_permissions(
33
+ db=db, branch=branch, account_session=self.account_session
34
+ )
35
+ self.permissions["global_permissions"].extend(backend_permissions["global_permissions"])
36
+ self.permissions["object_permissions"].extend(backend_permissions["object_permissions"])
37
+
38
+ def _compute_specificity(self, permission: ObjectPermission) -> int:
39
+ """Return how specific a permission is."""
40
+ specificity = 0
41
+ if permission.namespace not in self.wildcard_values:
42
+ specificity += 1
43
+ if permission.name not in self.wildcard_values:
44
+ specificity += 1
45
+ if permission.action not in self.wildcard_actions:
46
+ specificity += 1
47
+ if not permission.decision & PermissionDecisionFlag.ALLOW_ALL:
48
+ specificity += 1
49
+ return specificity
50
+
51
+ def report_object_permission(self, namespace: str, name: str, action: str) -> PermissionDecisionFlag:
52
+ """Given a set of permissions, return the permission decision for a given kind and action."""
53
+ highest_specificity: int = -1
54
+ combined_decision = PermissionDecisionFlag.DENY
55
+
56
+ for permission in self.permissions["object_permissions"]:
57
+ if (
58
+ permission.namespace in [namespace, *self.wildcard_values]
59
+ and permission.name in [name, *self.wildcard_values]
60
+ and permission.action in [action, *self.wildcard_actions]
61
+ ):
62
+ permission_decision = PermissionDecisionFlag(value=permission.decision)
63
+ # Compute the specifity of a permission to keep the decision of the most specific if two or more permissions overlap
64
+ specificity = self._compute_specificity(permission=permission)
65
+ if specificity > highest_specificity:
66
+ combined_decision = permission_decision
67
+ highest_specificity = specificity
68
+ elif specificity == highest_specificity and permission_decision != PermissionDecisionFlag.DENY:
69
+ combined_decision |= permission_decision
70
+
71
+ return combined_decision
72
+
73
+ def resolve_object_permission(self, permission_to_check: ObjectPermission) -> bool:
74
+ """Compute the permissions and check if the one provided is granted."""
75
+ required_decision = PermissionDecisionFlag(value=permission_to_check.decision)
76
+ combined_decision = self.report_object_permission(
77
+ namespace=permission_to_check.namespace, name=permission_to_check.name, action=permission_to_check.action
78
+ )
79
+
80
+ return combined_decision & required_decision == required_decision
81
+
82
+ def resolve_global_permission(self, permission_to_check: GlobalPermission) -> bool:
83
+ """Tell if a global permission is granted."""
84
+ grant_permission = False
85
+
86
+ for permission in self.permissions["global_permissions"]:
87
+ if permission.action == permission_to_check.action:
88
+ # Early exit on deny as deny preempt allow
89
+ if permission.decision == PermissionDecisionFlag.DENY:
90
+ return False
91
+ grant_permission = True
92
+
93
+ return grant_permission
94
+
95
+ def has_permission(self, permission: GlobalPermission | ObjectPermission) -> bool:
96
+ """Tell if a permission is granted given the permissions loaded in memory."""
97
+ is_super_admin = self.resolve_global_permission(
98
+ permission_to_check=GlobalPermission(
99
+ action=GlobalPermissions.SUPER_ADMIN, decision=PermissionDecision.ALLOW_ALL
100
+ ),
101
+ )
102
+
103
+ if isinstance(permission, GlobalPermission):
104
+ return self.resolve_global_permission(permission_to_check=permission) or is_super_admin
105
+
106
+ return self.resolve_object_permission(permission_to_check=permission) or is_super_admin
107
+
108
+ def has_permissions(self, permissions: Sequence[GlobalPermission | ObjectPermission]) -> bool:
109
+ """Same as `has_permission` but for multiple permissions, return `True` only if all permissions are granted."""
110
+ return all(self.has_permission(permission=permission) for permission in permissions)
111
+
112
+ def raise_for_permission(self, permission: GlobalPermission | ObjectPermission, message: str = "") -> None:
113
+ """Same as `has_permission` but raise a `PermissionDeniedError` if the permission is not granted."""
114
+ if self.has_permission(permission=permission):
115
+ return
116
+
117
+ if not message:
118
+ if isinstance(permission, GlobalPermission) and permission.action in GLOBAL_PERMISSION_DENIAL_MESSAGE:
119
+ message = GLOBAL_PERMISSION_DENIAL_MESSAGE[permission.action]
120
+ else:
121
+ message = f"You do not have the following permission: {permission!s}"
122
+
123
+ raise PermissionDeniedError(message=message)
124
+
125
+ def raise_for_permissions(
126
+ self, permissions: Sequence[GlobalPermission | ObjectPermission], message: str = ""
127
+ ) -> None:
128
+ """Same as `has_permissions` but raise a `PermissionDeniedError` if any of the permissions is not granted."""
129
+ if self.has_permissions(permissions=permissions):
130
+ return
131
+
132
+ if not message:
133
+ message = f"You do not have one of the following permissions: {' | '.join([str(p) for p in permissions])}"
134
+
135
+ raise PermissionDeniedError(message=message)