infrahub-server 1.1.1__py3-none-any.whl → 1.1.3__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 (137) hide show
  1. infrahub/api/__init__.py +13 -5
  2. infrahub/api/artifact.py +9 -15
  3. infrahub/api/auth.py +7 -1
  4. infrahub/api/dependencies.py +15 -2
  5. infrahub/api/diff/diff.py +13 -7
  6. infrahub/api/file.py +5 -10
  7. infrahub/api/internal.py +19 -6
  8. infrahub/api/menu.py +8 -6
  9. infrahub/api/oauth2.py +25 -10
  10. infrahub/api/oidc.py +26 -10
  11. infrahub/api/query.py +2 -2
  12. infrahub/api/schema.py +48 -59
  13. infrahub/api/storage.py +8 -8
  14. infrahub/api/transformation.py +6 -5
  15. infrahub/auth.py +1 -26
  16. infrahub/cli/__init__.py +1 -1
  17. infrahub/cli/context.py +5 -8
  18. infrahub/cli/db.py +6 -6
  19. infrahub/cli/git_agent.py +1 -1
  20. infrahub/computed_attribute/models.py +1 -1
  21. infrahub/computed_attribute/tasks.py +1 -1
  22. infrahub/config.py +5 -5
  23. infrahub/core/account.py +2 -10
  24. infrahub/core/attribute.py +22 -0
  25. infrahub/core/branch/models.py +1 -1
  26. infrahub/core/branch/tasks.py +4 -3
  27. infrahub/core/diff/calculator.py +14 -0
  28. infrahub/core/diff/combiner.py +6 -2
  29. infrahub/core/diff/conflicts_enricher.py +2 -2
  30. infrahub/core/diff/coordinator.py +296 -87
  31. infrahub/core/diff/data_check_synchronizer.py +33 -4
  32. infrahub/core/diff/enricher/cardinality_one.py +3 -3
  33. infrahub/core/diff/enricher/hierarchy.py +4 -1
  34. infrahub/core/diff/merger/merger.py +11 -1
  35. infrahub/core/diff/merger/serializer.py +5 -29
  36. infrahub/core/diff/model/path.py +88 -4
  37. infrahub/core/diff/query/field_specifiers.py +35 -0
  38. infrahub/core/diff/query/roots_metadata.py +48 -0
  39. infrahub/core/diff/query/save.py +1 -0
  40. infrahub/core/diff/query_parser.py +27 -11
  41. infrahub/core/diff/repository/deserializer.py +7 -3
  42. infrahub/core/diff/repository/repository.py +100 -9
  43. infrahub/core/diff/tasks.py +1 -1
  44. infrahub/core/graph/__init__.py +1 -1
  45. infrahub/core/integrity/object_conflict/conflict_recorder.py +6 -1
  46. infrahub/core/ipam/utilization.py +6 -1
  47. infrahub/core/manager.py +8 -0
  48. infrahub/core/merge.py +6 -1
  49. infrahub/core/migrations/graph/__init__.py +2 -0
  50. infrahub/core/migrations/graph/m014_remove_index_attr_value.py +1 -1
  51. infrahub/core/migrations/graph/m015_diff_format_update.py +1 -1
  52. infrahub/core/migrations/graph/m016_diff_delete_bug_fix.py +1 -1
  53. infrahub/core/migrations/graph/m018_uniqueness_nulls.py +101 -0
  54. infrahub/core/migrations/query/attribute_add.py +5 -5
  55. infrahub/core/migrations/schema/tasks.py +2 -2
  56. infrahub/core/migrations/shared.py +3 -3
  57. infrahub/core/node/__init__.py +8 -2
  58. infrahub/core/node/constraints/grouped_uniqueness.py +9 -2
  59. infrahub/core/query/__init__.py +5 -2
  60. infrahub/core/query/diff.py +32 -19
  61. infrahub/core/query/ipam.py +30 -22
  62. infrahub/core/query/node.py +91 -40
  63. infrahub/core/schema/generated/attribute_schema.py +2 -2
  64. infrahub/core/schema/generated/base_node_schema.py +2 -2
  65. infrahub/core/schema/generated/relationship_schema.py +1 -1
  66. infrahub/core/schema/schema_branch_computed.py +1 -1
  67. infrahub/core/task/task_log.py +1 -1
  68. infrahub/core/validators/attribute/kind.py +1 -1
  69. infrahub/core/validators/interface.py +1 -2
  70. infrahub/core/validators/models/violation.py +1 -14
  71. infrahub/core/validators/shared.py +2 -2
  72. infrahub/core/validators/tasks.py +7 -4
  73. infrahub/core/validators/uniqueness/index.py +2 -4
  74. infrahub/database/index.py +1 -1
  75. infrahub/dependencies/builder/constraint/schema/aggregated.py +2 -0
  76. infrahub/dependencies/builder/constraint/schema/attribute_kind.py +8 -0
  77. infrahub/dependencies/builder/diff/data_check_synchronizer.py +2 -0
  78. infrahub/git/base.py +3 -3
  79. infrahub/git/integrator.py +1 -1
  80. infrahub/graphql/api/endpoints.py +12 -3
  81. infrahub/graphql/app.py +2 -2
  82. infrahub/graphql/auth/query_permission_checker/default_branch_checker.py +2 -17
  83. infrahub/graphql/auth/query_permission_checker/merge_operation_checker.py +1 -12
  84. infrahub/graphql/auth/query_permission_checker/object_permission_checker.py +6 -40
  85. infrahub/graphql/auth/query_permission_checker/super_admin_checker.py +5 -8
  86. infrahub/graphql/enums.py +2 -2
  87. infrahub/graphql/initialization.py +27 -8
  88. infrahub/graphql/manager.py +9 -3
  89. infrahub/graphql/models.py +6 -0
  90. infrahub/graphql/mutations/account.py +14 -10
  91. infrahub/graphql/mutations/computed_attribute.py +11 -22
  92. infrahub/graphql/mutations/diff.py +2 -0
  93. infrahub/graphql/mutations/main.py +5 -16
  94. infrahub/graphql/mutations/proposed_change.py +11 -20
  95. infrahub/graphql/mutations/resource_manager.py +6 -3
  96. infrahub/graphql/mutations/schema.py +8 -7
  97. infrahub/graphql/mutations/tasks.py +1 -1
  98. infrahub/graphql/permissions.py +3 -4
  99. infrahub/graphql/queries/account.py +2 -11
  100. infrahub/graphql/queries/resource_manager.py +21 -10
  101. infrahub/graphql/query.py +3 -1
  102. infrahub/graphql/resolvers/resolver.py +5 -1
  103. infrahub/graphql/types/task.py +14 -2
  104. infrahub/menu/generator.py +6 -18
  105. infrahub/message_bus/messages/event_node_mutated.py +2 -2
  106. infrahub/message_bus/operations/check/repository.py +2 -4
  107. infrahub/message_bus/operations/event/branch.py +2 -4
  108. infrahub/message_bus/operations/requests/proposed_change.py +1 -1
  109. infrahub/message_bus/operations/requests/repository.py +3 -5
  110. infrahub/message_bus/types.py +1 -1
  111. infrahub/permissions/__init__.py +12 -3
  112. infrahub/permissions/backend.py +2 -17
  113. infrahub/permissions/constants.py +12 -8
  114. infrahub/permissions/local_backend.py +5 -102
  115. infrahub/permissions/manager.py +135 -0
  116. infrahub/permissions/report.py +14 -25
  117. infrahub/permissions/types.py +6 -0
  118. infrahub/proposed_change/tasks.py +1 -1
  119. infrahub/task_manager/models.py +34 -5
  120. infrahub/task_manager/task.py +14 -6
  121. infrahub/visuals.py +1 -3
  122. infrahub_sdk/client.py +204 -43
  123. infrahub_sdk/ctl/cli_commands.py +106 -6
  124. infrahub_sdk/data.py +3 -2
  125. infrahub_sdk/graphql.py +5 -0
  126. infrahub_sdk/node.py +21 -2
  127. infrahub_sdk/queries.py +69 -0
  128. infrahub_sdk/schema/main.py +1 -0
  129. infrahub_sdk/testing/schemas/animal.py +1 -0
  130. infrahub_sdk/types.py +6 -0
  131. infrahub_sdk/utils.py +17 -0
  132. {infrahub_server-1.1.1.dist-info → infrahub_server-1.1.3.dist-info}/METADATA +1 -1
  133. {infrahub_server-1.1.1.dist-info → infrahub_server-1.1.3.dist-info}/RECORD +136 -131
  134. infrahub/core/diff/query/empty_roots.py +0 -33
  135. {infrahub_server-1.1.1.dist-info → infrahub_server-1.1.3.dist-info}/LICENSE.txt +0 -0
  136. {infrahub_server-1.1.1.dist-info → infrahub_server-1.1.3.dist-info}/WHEEL +0 -0
  137. {infrahub_server-1.1.1.dist-info → infrahub_server-1.1.3.dist-info}/entry_points.txt +0 -0
@@ -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)
@@ -6,21 +6,20 @@ from infrahub.core import registry
6
6
  from infrahub.core.account import GlobalPermission
7
7
  from infrahub.core.constants import GLOBAL_BRANCH_NAME, GlobalPermissions, InfrahubKind, PermissionDecision
8
8
  from infrahub.core.schema.node_schema import NodeSchema
9
- from infrahub.permissions.constants import AssignedPermissions, BranchRelativePermissionDecision, PermissionDecisionFlag
10
- from infrahub.permissions.local_backend import LocalPermissionBackend
9
+ from infrahub.permissions.constants import BranchRelativePermissionDecision, PermissionDecisionFlag
11
10
 
12
11
  if TYPE_CHECKING:
13
- from infrahub.auth import AccountSession
14
12
  from infrahub.core.branch import Branch
15
13
  from infrahub.core.schema import MainSchemaTypes
16
- from infrahub.database import InfrahubDatabase
17
- from infrahub.permissions.backend import PermissionBackend
14
+ from infrahub.permissions.manager import PermissionManager
18
15
  from infrahub.permissions.types import KindPermissions
19
16
 
20
17
 
18
+ __all__ = ["report_schema_permissions"]
19
+
20
+
21
21
  def get_permission_report( # noqa: PLR0911
22
- backend: PermissionBackend,
23
- permissions: AssignedPermissions,
22
+ permission_manager: PermissionManager,
24
23
  branch: Branch,
25
24
  node: MainSchemaTypes,
26
25
  action: str,
@@ -56,9 +55,7 @@ def get_permission_report( # noqa: PLR0911
56
55
  )
57
56
 
58
57
  is_default_branch = branch.name in (GLOBAL_BRANCH_NAME, registry.default_branch)
59
- decision = backend.report_object_permission(
60
- permissions=permissions["object_permissions"], namespace=node.namespace, name=node.name, action=action
61
- )
58
+ decision = permission_manager.report_object_permission(namespace=node.namespace, name=node.name, action=action)
62
59
 
63
60
  if (
64
61
  decision == PermissionDecisionFlag.ALLOW_ALL
@@ -75,16 +72,12 @@ def get_permission_report( # noqa: PLR0911
75
72
 
76
73
 
77
74
  async def report_schema_permissions(
78
- db: InfrahubDatabase, schemas: list[MainSchemaTypes], account_session: AccountSession, branch: Branch
75
+ branch: Branch, permission_manager: PermissionManager, schemas: list[MainSchemaTypes]
79
76
  ) -> list[KindPermissions]:
80
- perm_backend = LocalPermissionBackend()
81
- permissions = await perm_backend.load_permissions(db=db, account_session=account_session, branch=branch)
82
-
83
77
  global_permission_report: dict[GlobalPermissions, bool] = {}
84
78
  for perm in GlobalPermissions:
85
- global_permission_report[perm] = perm_backend.resolve_global_permission(
86
- permissions=permissions["global_permissions"],
87
- permission_to_check=GlobalPermission(action=perm.value, decision=PermissionDecision.ALLOW_ALL.value),
79
+ global_permission_report[perm] = permission_manager.resolve_global_permission(
80
+ permission_to_check=GlobalPermission(action=perm.value, decision=PermissionDecision.ALLOW_ALL.value)
88
81
  )
89
82
 
90
83
  permission_objects: list[KindPermissions] = []
@@ -93,32 +86,28 @@ async def report_schema_permissions(
93
86
  {
94
87
  "kind": node.kind,
95
88
  "create": get_permission_report(
96
- backend=perm_backend,
97
- permissions=permissions,
89
+ permission_manager=permission_manager,
98
90
  branch=branch,
99
91
  node=node,
100
92
  action="create",
101
93
  global_permission_report=global_permission_report,
102
94
  ),
103
95
  "delete": get_permission_report(
104
- backend=perm_backend,
105
- permissions=permissions,
96
+ permission_manager=permission_manager,
106
97
  branch=branch,
107
98
  node=node,
108
99
  action="delete",
109
100
  global_permission_report=global_permission_report,
110
101
  ),
111
102
  "update": get_permission_report(
112
- backend=perm_backend,
113
- permissions=permissions,
103
+ permission_manager=permission_manager,
114
104
  branch=branch,
115
105
  node=node,
116
106
  action="update",
117
107
  global_permission_report=global_permission_report,
118
108
  ),
119
109
  "view": get_permission_report(
120
- backend=perm_backend,
121
- permissions=permissions,
110
+ permission_manager=permission_manager,
122
111
  branch=branch,
123
112
  node=node,
124
113
  action="view",
@@ -3,9 +3,15 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING, TypedDict
4
4
 
5
5
  if TYPE_CHECKING:
6
+ from infrahub.core.account import GlobalPermission, ObjectPermission
6
7
  from infrahub.permissions.constants import BranchRelativePermissionDecision
7
8
 
8
9
 
10
+ class AssignedPermissions(TypedDict):
11
+ global_permissions: list[GlobalPermission]
12
+ object_permissions: list[ObjectPermission]
13
+
14
+
9
15
  class KindPermissions(TypedDict):
10
16
  kind: str
11
17
  create: BranchRelativePermissionDecision
@@ -11,7 +11,7 @@ from infrahub_sdk.protocols import CoreGeneratorDefinition, CoreProposedChange
11
11
  from prefect import flow, task
12
12
  from prefect.cache_policies import NONE
13
13
  from prefect.client.schemas.objects import (
14
- State, # noqa: TCH002
14
+ State, # noqa: TC002
15
15
  )
16
16
  from prefect.logging import get_run_logger
17
17
  from prefect.states import Completed, Failed
@@ -1,5 +1,4 @@
1
1
  from collections import defaultdict
2
- from typing import DefaultDict
3
2
  from uuid import UUID
4
3
 
5
4
  from prefect.client.schemas.objects import Log as PrefectLog
@@ -8,16 +7,46 @@ from pydantic import BaseModel, Field
8
7
  from .constants import LOG_LEVEL_MAPPING
9
8
 
10
9
 
10
+ class RelatedNodeInfo(BaseModel):
11
+ id: str
12
+ kind: str | None = None
13
+
14
+
11
15
  class RelatedNodesInfo(BaseModel):
12
- id: dict[UUID, str] = Field(default_factory=dict)
13
- kind: dict[UUID, str | None] = Field(default_factory=dict)
16
+ flows: dict[UUID, dict[str, RelatedNodeInfo]] = Field(default_factory=lambda: defaultdict(dict))
17
+ nodes: dict[str, RelatedNodeInfo] = Field(default_factory=dict)
18
+
19
+ def add_nodes(self, flow_id: UUID, node_ids: list[str]) -> None:
20
+ for node_id in node_ids:
21
+ self.add_node(flow_id=flow_id, node_id=node_id)
22
+
23
+ def add_node(self, flow_id: UUID, node_id: str) -> None:
24
+ if node_id not in self.nodes:
25
+ node = RelatedNodeInfo(id=node_id)
26
+ self.nodes[node_id] = node
27
+ self.flows[flow_id][node_id] = self.nodes[node_id]
28
+
29
+ def get_related_nodes(self, flow_id: UUID) -> list[RelatedNodeInfo]:
30
+ if flow_id not in self.flows or len(self.flows[flow_id].keys()) == 0:
31
+ return []
32
+ return list(self.flows[flow_id].values())
33
+
34
+ def get_related_nodes_as_dict(self, flow_id: UUID) -> list[dict[str, str | None]]:
35
+ if flow_id not in self.flows or len(self.flows[flow_id].keys()) == 0:
36
+ return []
37
+ return [item.model_dump() for item in list(self.flows[flow_id].values())]
38
+
39
+ def get_first_related_node(self, flow_id: UUID) -> RelatedNodeInfo | None:
40
+ if nodes := self.get_related_nodes(flow_id=flow_id):
41
+ return nodes[0]
42
+ return None
14
43
 
15
44
  def get_unique_related_node_ids(self) -> list[str]:
16
- return list(set(list(self.id.values())))
45
+ return list(self.nodes.keys())
17
46
 
18
47
 
19
48
  class FlowLogs(BaseModel):
20
- logs: DefaultDict[UUID, list[PrefectLog]] = Field(default_factory=lambda: defaultdict(list))
49
+ logs: defaultdict[UUID, list[PrefectLog]] = Field(default_factory=lambda: defaultdict(list))
21
50
 
22
51
  def to_graphql(self, flow_id: UUID) -> list[dict]:
23
52
  return [
@@ -69,15 +69,16 @@ class PrefectTask:
69
69
  ]
70
70
  if not related_node_ids:
71
71
  continue
72
- related_nodes.id[flow.id] = related_node_ids[0]
72
+ related_nodes.add_nodes(flow_id=flow.id, node_ids=related_node_ids)
73
73
 
74
74
  if unique_related_node_ids := related_nodes.get_unique_related_node_ids():
75
75
  query = await NodeGetKindQuery.init(db=db, ids=unique_related_node_ids)
76
76
  await query.execute(db=db)
77
77
  unique_related_node_ids_kind = await query.get_node_kind_map()
78
78
 
79
- for flow_id, node_id in related_nodes.id.items():
80
- related_nodes.kind[flow_id] = unique_related_node_ids_kind.get(node_id, None)
79
+ for node_id, node_kind in unique_related_node_ids_kind.items():
80
+ if node_id in related_nodes.nodes:
81
+ related_nodes.nodes[node_id].kind = node_kind
81
82
 
82
83
  return related_nodes
83
84
 
@@ -223,7 +224,11 @@ class PrefectTask:
223
224
  if "progress" in node_fields:
224
225
  progress_flow = await cls._get_progress(client=client, flow_ids=[flow.id for flow in flows])
225
226
 
226
- if "related_node" in node_fields or "related_node_kind" in node_fields:
227
+ if (
228
+ "related_nodes" in node_fields
229
+ or "related_node" in node_fields
230
+ or "related_node_kind" in node_fields
231
+ ):
227
232
  related_nodes_info = await cls._get_related_nodes(db=db, flows=flows)
228
233
 
229
234
  if "workflow" in node_fields:
@@ -238,6 +243,8 @@ class PrefectTask:
238
243
  if log_fields:
239
244
  logs = logs_flow.to_graphql(flow_id=flow.id)
240
245
 
246
+ related_node = related_nodes_info.get_first_related_node(flow_id=flow.id)
247
+
241
248
  nodes.append(
242
249
  {
243
250
  "node": {
@@ -251,8 +258,9 @@ class PrefectTask:
251
258
  "branch": await cls._extract_branch_name(flow=flow),
252
259
  "tags": flow.tags,
253
260
  "workflow": workflow_names.get(flow.flow_id, None),
254
- "related_node": related_nodes_info.id.get(flow.id, None),
255
- "related_node_kind": related_nodes_info.kind.get(flow.id, None),
261
+ "related_node": related_node.id if related_node else None,
262
+ "related_node_kind": related_node.kind if related_node else None,
263
+ "related_nodes": related_nodes_info.get_related_nodes_as_dict(flow_id=flow.id),
256
264
  "created_at": flow.created.to_iso8601_string(), # type: ignore
257
265
  "updated_at": flow.updated.to_iso8601_string(), # type: ignore
258
266
  "start_time": flow.start_time.to_iso8601_string() if flow.start_time else None,
infrahub/visuals.py CHANGED
@@ -1,5 +1,3 @@
1
- from typing import List
2
-
3
1
  COLOR_SELECTION = [
4
2
  "#ed6a5a",
5
3
  "#f4f1bb",
@@ -24,7 +22,7 @@ COLOR_SELECTION = [
24
22
  ]
25
23
 
26
24
 
27
- def select_color(existing: List[str]) -> str:
25
+ def select_color(existing: list[str]) -> str:
28
26
  """Select a color from a predefined list without including anything from a list of existing colors."""
29
27
  for color in COLOR_SELECTION:
30
28
  if color not in existing: