infrahub-server 1.4.0b1__py3-none-any.whl → 1.4.1__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.
- infrahub/api/schema.py +3 -7
- infrahub/cli/db.py +25 -0
- infrahub/cli/db_commands/__init__.py +0 -0
- infrahub/cli/db_commands/check_inheritance.py +284 -0
- infrahub/cli/upgrade.py +3 -0
- infrahub/config.py +4 -4
- infrahub/core/attribute.py +6 -0
- infrahub/core/constants/__init__.py +1 -0
- infrahub/core/diff/model/path.py +0 -39
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/initialization.py +26 -21
- infrahub/core/manager.py +2 -2
- infrahub/core/migrations/__init__.py +2 -0
- infrahub/core/migrations/graph/__init__.py +4 -2
- infrahub/core/migrations/graph/m033_deduplicate_relationship_vertices.py +1 -1
- infrahub/core/migrations/graph/m035_orphan_relationships.py +43 -0
- infrahub/core/migrations/graph/{m035_drop_attr_value_index.py → m036_drop_attr_value_index.py} +3 -3
- infrahub/core/migrations/graph/{m036_index_attr_vals.py → m037_index_attr_vals.py} +3 -3
- infrahub/core/migrations/query/node_duplicate.py +26 -3
- infrahub/core/migrations/schema/attribute_kind_update.py +156 -0
- infrahub/core/models.py +5 -1
- infrahub/core/node/resource_manager/ip_address_pool.py +50 -48
- infrahub/core/node/resource_manager/ip_prefix_pool.py +55 -53
- infrahub/core/node/resource_manager/number_pool.py +20 -18
- infrahub/core/query/branch.py +37 -20
- infrahub/core/query/node.py +15 -0
- infrahub/core/relationship/model.py +13 -13
- infrahub/core/schema/definitions/internal.py +1 -1
- infrahub/core/schema/generated/attribute_schema.py +1 -1
- infrahub/core/schema/node_schema.py +0 -5
- infrahub/core/schema/schema_branch.py +2 -2
- infrahub/core/validators/attribute/choices.py +28 -3
- infrahub/core/validators/attribute/kind.py +5 -1
- infrahub/core/validators/determiner.py +22 -2
- infrahub/events/__init__.py +2 -0
- infrahub/events/proposed_change_action.py +22 -0
- infrahub/graphql/app.py +2 -1
- infrahub/graphql/context.py +1 -1
- infrahub/graphql/mutations/proposed_change.py +5 -0
- infrahub/graphql/mutations/relationship.py +1 -1
- infrahub/graphql/mutations/schema.py +14 -1
- infrahub/graphql/queries/diff/tree.py +53 -2
- infrahub/graphql/schema.py +3 -14
- infrahub/graphql/types/event.py +8 -0
- infrahub/permissions/__init__.py +3 -0
- infrahub/permissions/constants.py +13 -0
- infrahub/permissions/globals.py +32 -0
- infrahub/task_manager/event.py +5 -1
- infrahub_sdk/client.py +8 -8
- infrahub_sdk/node/node.py +2 -2
- infrahub_sdk/protocols.py +6 -40
- infrahub_sdk/pytest_plugin/items/graphql_query.py +1 -1
- infrahub_sdk/schema/repository.py +1 -1
- infrahub_sdk/testing/docker.py +1 -1
- infrahub_sdk/utils.py +11 -7
- {infrahub_server-1.4.0b1.dist-info → infrahub_server-1.4.1.dist-info}/METADATA +4 -4
- {infrahub_server-1.4.0b1.dist-info → infrahub_server-1.4.1.dist-info}/RECORD +60 -56
- {infrahub_server-1.4.0b1.dist-info → infrahub_server-1.4.1.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.4.0b1.dist-info → infrahub_server-1.4.1.dist-info}/WHEEL +0 -0
- {infrahub_server-1.4.0b1.dist-info → infrahub_server-1.4.1.dist-info}/entry_points.txt +0 -0
infrahub/core/query/node.py
CHANGED
|
@@ -161,7 +161,22 @@ class NodeCreateAllQuery(NodeQuery):
|
|
|
161
161
|
relationships: list[RelationshipCreateData] = []
|
|
162
162
|
for rel_name in self.node._relationships:
|
|
163
163
|
rel_manager: RelationshipManager = getattr(self.node, rel_name)
|
|
164
|
+
if rel_manager.schema.cardinality == "many":
|
|
165
|
+
# Fetch all relationship peers through a single database call for performances.
|
|
166
|
+
peers = await rel_manager.get_peers(db=db, branch_agnostic=self.branch_agnostic)
|
|
167
|
+
|
|
164
168
|
for rel in rel_manager._relationships:
|
|
169
|
+
if rel_manager.schema.cardinality == "many":
|
|
170
|
+
try:
|
|
171
|
+
rel.set_peer(value=peers[rel.get_peer_id()])
|
|
172
|
+
except KeyError:
|
|
173
|
+
pass
|
|
174
|
+
except ValueError:
|
|
175
|
+
# Relationship has not been initialized yet, it means the peer does not exist in db yet
|
|
176
|
+
# typically because it will be allocated from a ressource pool. In that case, the peer
|
|
177
|
+
# will be fetched using `rel.resolve` later.
|
|
178
|
+
pass
|
|
179
|
+
|
|
165
180
|
rel_create_data = await rel.get_create_data(db=db, at=at)
|
|
166
181
|
if rel_create_data.peer_branch_level > deepest_branch_level or (
|
|
167
182
|
deepest_branch_name == GLOBAL_BRANCH_NAME and rel_create_data.peer_branch == registry.default_branch
|
|
@@ -166,11 +166,11 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
|
|
|
166
166
|
return registry.get_global_branch()
|
|
167
167
|
return self.branch
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
def _process_data(self, data: dict | RelationshipPeerData | str) -> None:
|
|
170
170
|
self.data = data
|
|
171
171
|
|
|
172
172
|
if isinstance(data, RelationshipPeerData):
|
|
173
|
-
|
|
173
|
+
self.set_peer(value=str(data.peer_id))
|
|
174
174
|
|
|
175
175
|
if not self.id and data.rel_node_id:
|
|
176
176
|
self.id = data.rel_node_id
|
|
@@ -187,7 +187,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
|
|
|
187
187
|
elif isinstance(data, dict):
|
|
188
188
|
for key, value in data.items():
|
|
189
189
|
if key in ["peer", "id"]:
|
|
190
|
-
|
|
190
|
+
self.set_peer(value=data.get(key, None))
|
|
191
191
|
elif key == "hfid" and self.peer_id is None:
|
|
192
192
|
self.peer_hfid = value
|
|
193
193
|
elif key.startswith(PREFIX_PROPERTY) and key.replace(PREFIX_PROPERTY, "") in self._flag_properties:
|
|
@@ -198,7 +198,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
|
|
|
198
198
|
self.from_pool = value
|
|
199
199
|
|
|
200
200
|
else:
|
|
201
|
-
|
|
201
|
+
self.set_peer(value=data)
|
|
202
202
|
|
|
203
203
|
async def new(
|
|
204
204
|
self,
|
|
@@ -206,11 +206,11 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
|
|
|
206
206
|
data: dict | RelationshipPeerData | Any = None,
|
|
207
207
|
**kwargs: Any, # noqa: ARG002
|
|
208
208
|
) -> Relationship:
|
|
209
|
-
|
|
209
|
+
self._process_data(data=data)
|
|
210
210
|
|
|
211
211
|
return self
|
|
212
212
|
|
|
213
|
-
|
|
213
|
+
def load(
|
|
214
214
|
self,
|
|
215
215
|
db: InfrahubDatabase, # noqa: ARG002
|
|
216
216
|
id: UUID | None = None,
|
|
@@ -223,7 +223,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
|
|
|
223
223
|
self.id = id or self.id
|
|
224
224
|
self.db_id = db_id or self.db_id
|
|
225
225
|
|
|
226
|
-
|
|
226
|
+
self._process_data(data=data)
|
|
227
227
|
|
|
228
228
|
if updated_at and hash(self) != hash_before:
|
|
229
229
|
self.updated_at = Timestamp(updated_at)
|
|
@@ -252,7 +252,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
|
|
|
252
252
|
self._node_id = self._node.id
|
|
253
253
|
return node
|
|
254
254
|
|
|
255
|
-
|
|
255
|
+
def set_peer(self, value: str | Node) -> None:
|
|
256
256
|
if isinstance(value, str):
|
|
257
257
|
self.peer_id = value
|
|
258
258
|
else:
|
|
@@ -433,7 +433,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
|
|
|
433
433
|
db=db, id=self.peer_id, branch=self.branch, kind=self.schema.peer, fields={"display_label": None}
|
|
434
434
|
)
|
|
435
435
|
if peer:
|
|
436
|
-
|
|
436
|
+
self.set_peer(value=peer)
|
|
437
437
|
|
|
438
438
|
if not self.peer_id and self.peer_hfid:
|
|
439
439
|
peer_schema = db.schema.get(name=self.schema.peer, branch=self.branch)
|
|
@@ -450,7 +450,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
|
|
|
450
450
|
fields={"display_label": None},
|
|
451
451
|
raise_on_error=True,
|
|
452
452
|
)
|
|
453
|
-
|
|
453
|
+
self.set_peer(value=peer)
|
|
454
454
|
|
|
455
455
|
if not self.peer_id and self.from_pool and "id" in self.from_pool:
|
|
456
456
|
pool_id = str(self.from_pool.get("id"))
|
|
@@ -473,7 +473,7 @@ class Relationship(FlagPropertyMixin, NodePropertyMixin):
|
|
|
473
473
|
data_from_pool["identifier"] = f"hfid={hfid_str} rel={self.name}"
|
|
474
474
|
|
|
475
475
|
assigned_peer: Node = await pool.get_resource(db=db, branch=self.branch, at=at, **data_from_pool) # type: ignore[attr-defined]
|
|
476
|
-
|
|
476
|
+
self.set_peer(value=assigned_peer)
|
|
477
477
|
self.set_source(value=pool.id)
|
|
478
478
|
|
|
479
479
|
async def save(self, db: InfrahubDatabase, at: Timestamp | None = None) -> Self:
|
|
@@ -962,7 +962,7 @@ class RelationshipManager:
|
|
|
962
962
|
|
|
963
963
|
for peer_id in details.peer_ids_present_database_only:
|
|
964
964
|
self._relationships.append(
|
|
965
|
-
|
|
965
|
+
Relationship(
|
|
966
966
|
schema=self.schema,
|
|
967
967
|
branch=self.branch,
|
|
968
968
|
at=at or self.at,
|
|
@@ -1050,7 +1050,7 @@ class RelationshipManager:
|
|
|
1050
1050
|
if isinstance(item, dict) and item.get("id", None) in previous_relationships:
|
|
1051
1051
|
rel = previous_relationships[item["id"]]
|
|
1052
1052
|
hash_before = hash(rel)
|
|
1053
|
-
|
|
1053
|
+
rel.load(data=item, db=db)
|
|
1054
1054
|
if hash(rel) != hash_before:
|
|
1055
1055
|
changed = True
|
|
1056
1056
|
self._relationships.append(rel)
|
|
@@ -487,7 +487,7 @@ attribute_schema = SchemaNode(
|
|
|
487
487
|
kind="Text",
|
|
488
488
|
description="Defines the type of the attribute.",
|
|
489
489
|
enum=ATTRIBUTE_KIND_LABELS,
|
|
490
|
-
extra={"update": UpdateSupport.
|
|
490
|
+
extra={"update": UpdateSupport.MIGRATION_REQUIRED},
|
|
491
491
|
),
|
|
492
492
|
SchemaAttribute(
|
|
493
493
|
name="enum",
|
|
@@ -31,7 +31,7 @@ class GeneratedAttributeSchema(HashableModel):
|
|
|
31
31
|
json_schema_extra={"update": "migration_required"},
|
|
32
32
|
)
|
|
33
33
|
kind: str = Field(
|
|
34
|
-
..., description="Defines the type of the attribute.", json_schema_extra={"update": "
|
|
34
|
+
..., description="Defines the type of the attribute.", json_schema_extra={"update": "migration_required"}
|
|
35
35
|
)
|
|
36
36
|
enum: list | None = Field(
|
|
37
37
|
default=None,
|
|
@@ -63,11 +63,6 @@ class NodeSchema(GeneratedNodeSchema):
|
|
|
63
63
|
f"{self.kind}.{attribute.name} inherited from {interface.namespace}{interface.name} must be the same kind "
|
|
64
64
|
f'["{interface_attr.kind}", "{attribute.kind}"]'
|
|
65
65
|
)
|
|
66
|
-
if attribute.optional != interface_attr.optional:
|
|
67
|
-
raise ValueError(
|
|
68
|
-
f"{self.kind}.{attribute.name} inherited from {interface.namespace}{interface.name} must have the same value for property "
|
|
69
|
-
f'"optional" ["{interface_attr.optional}", "{attribute.optional}"]'
|
|
70
|
-
)
|
|
71
66
|
|
|
72
67
|
for relationship in self.relationships:
|
|
73
68
|
if relationship.name in interface.relationship_names and not relationship.inherited:
|
|
@@ -226,8 +226,8 @@ class SchemaBranch:
|
|
|
226
226
|
def update(self, schema: SchemaBranch) -> None:
|
|
227
227
|
"""Update another SchemaBranch into this one."""
|
|
228
228
|
|
|
229
|
-
local_kinds =
|
|
230
|
-
other_kinds =
|
|
229
|
+
local_kinds = self.all_names
|
|
230
|
+
other_kinds = schema.all_names
|
|
231
231
|
|
|
232
232
|
in_both, _, other_only = compare_lists(list1=local_kinds, list2=other_kinds)
|
|
233
233
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING, Any
|
|
3
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
4
4
|
|
|
5
5
|
from infrahub.core.constants import NULL_VALUE, PathType
|
|
6
6
|
from infrahub.core.path import DataPath, GroupedDataPaths
|
|
7
|
+
from infrahub.core.schema.generic_schema import GenericSchema
|
|
7
8
|
|
|
8
9
|
from ..interface import ConstraintCheckerInterface
|
|
9
10
|
from ..shared import AttributeSchemaValidatorQuery
|
|
@@ -18,6 +19,14 @@ if TYPE_CHECKING:
|
|
|
18
19
|
class AttributeChoicesUpdateValidatorQuery(AttributeSchemaValidatorQuery):
|
|
19
20
|
name: str = "attribute_constraints_choices_validator"
|
|
20
21
|
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
excluded_kinds: list[str] | None = None,
|
|
25
|
+
**kwargs: Any,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.excluded_kinds: list[str] = excluded_kinds or []
|
|
28
|
+
super().__init__(**kwargs)
|
|
29
|
+
|
|
21
30
|
async def query_init(self, db: InfrahubDatabase, **kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
22
31
|
if self.attribute_schema.choices is None:
|
|
23
32
|
return
|
|
@@ -28,9 +37,11 @@ class AttributeChoicesUpdateValidatorQuery(AttributeSchemaValidatorQuery):
|
|
|
28
37
|
self.params["attr_name"] = self.attribute_schema.name
|
|
29
38
|
self.params["allowed_values"] = [choice.name for choice in self.attribute_schema.choices]
|
|
30
39
|
self.params["null_value"] = NULL_VALUE
|
|
40
|
+
self.params["excluded_kinds"] = self.excluded_kinds
|
|
31
41
|
|
|
32
42
|
query = """
|
|
33
|
-
MATCH
|
|
43
|
+
MATCH (n:%(node_kind)s)
|
|
44
|
+
WHERE size($excluded_kinds) = 0 OR NOT n.kind IN $excluded_kinds
|
|
34
45
|
CALL (n) {
|
|
35
46
|
MATCH path = (root:Root)<-[rr:IS_PART_OF]-(n)-[ra:HAS_ATTRIBUTE]-(:Attribute { name: $attr_name } )-[rv:HAS_VALUE]-(av:AttributeValue)
|
|
36
47
|
WHERE all(
|
|
@@ -92,10 +103,24 @@ class AttributeChoicesChecker(ConstraintCheckerInterface):
|
|
|
92
103
|
if attribute_schema.choices is None:
|
|
93
104
|
return grouped_data_paths_list
|
|
94
105
|
|
|
106
|
+
# skip inheriting schemas that override the attribute being checked
|
|
107
|
+
excluded_kinds: list[str] = []
|
|
108
|
+
if request.node_schema.is_generic_schema:
|
|
109
|
+
request.node_schema = cast(GenericSchema, request.node_schema)
|
|
110
|
+
for inheriting_kind in request.node_schema.used_by:
|
|
111
|
+
inheriting_schema = request.schema_branch.get_node(name=inheriting_kind, duplicate=False)
|
|
112
|
+
inheriting_schema_attribute = inheriting_schema.get_attribute(name=request.schema_path.field_name)
|
|
113
|
+
if not inheriting_schema_attribute.inherited:
|
|
114
|
+
excluded_kinds.append(inheriting_kind)
|
|
115
|
+
|
|
95
116
|
for query_class in self.query_classes:
|
|
96
117
|
# TODO add exception handling
|
|
97
118
|
query = await query_class.init(
|
|
98
|
-
db=self.db,
|
|
119
|
+
db=self.db,
|
|
120
|
+
branch=self.branch,
|
|
121
|
+
node_schema=request.node_schema,
|
|
122
|
+
schema_path=request.schema_path,
|
|
123
|
+
excluded_kinds=excluded_kinds,
|
|
99
124
|
)
|
|
100
125
|
await query.execute(db=self.db)
|
|
101
126
|
grouped_data_paths_list.append(await query.get_paths())
|
|
@@ -65,8 +65,12 @@ class AttributeKindUpdateValidatorQuery(AttributeSchemaValidatorQuery):
|
|
|
65
65
|
if value in (None, NULL_VALUE):
|
|
66
66
|
continue
|
|
67
67
|
try:
|
|
68
|
+
attr_value = result.get("attribute_value")
|
|
68
69
|
infrahub_attribute_class.validate_format(
|
|
69
|
-
value=
|
|
70
|
+
value=attr_value, name=self.attribute_schema.name, schema=self.attribute_schema
|
|
71
|
+
)
|
|
72
|
+
infrahub_attribute_class.validate_content(
|
|
73
|
+
value=attr_value, name=self.attribute_schema.name, schema=self.attribute_schema
|
|
70
74
|
)
|
|
71
75
|
except ValidationError:
|
|
72
76
|
grouped_data_paths.add_data_path(
|
|
@@ -98,7 +98,10 @@ class ConstraintValidatorDeterminer:
|
|
|
98
98
|
continue
|
|
99
99
|
|
|
100
100
|
prop_field_update = prop_field_info.json_schema_extra.get("update")
|
|
101
|
-
if prop_field_update
|
|
101
|
+
if prop_field_update not in (
|
|
102
|
+
UpdateSupport.VALIDATE_CONSTRAINT.value,
|
|
103
|
+
UpdateSupport.MIGRATION_REQUIRED.value,
|
|
104
|
+
):
|
|
102
105
|
continue
|
|
103
106
|
|
|
104
107
|
if getattr(schema, prop_name) is None:
|
|
@@ -112,6 +115,13 @@ class ConstraintValidatorDeterminer:
|
|
|
112
115
|
)
|
|
113
116
|
constraint_name = f"node.{prop_name}.update"
|
|
114
117
|
|
|
118
|
+
do_constraint_validation = prop_field_update == UpdateSupport.VALIDATE_CONSTRAINT.value or (
|
|
119
|
+
prop_field_update == UpdateSupport.MIGRATION_REQUIRED.value
|
|
120
|
+
and CONSTRAINT_VALIDATOR_MAP.get(constraint_name)
|
|
121
|
+
)
|
|
122
|
+
if not do_constraint_validation:
|
|
123
|
+
continue
|
|
124
|
+
|
|
115
125
|
constraints.append(SchemaUpdateConstraintInfo(constraint_name=constraint_name, path=schema_path))
|
|
116
126
|
return constraints
|
|
117
127
|
|
|
@@ -154,7 +164,10 @@ class ConstraintValidatorDeterminer:
|
|
|
154
164
|
continue
|
|
155
165
|
|
|
156
166
|
prop_field_update = prop_field_info.json_schema_extra.get("update")
|
|
157
|
-
if prop_field_update
|
|
167
|
+
if prop_field_update not in (
|
|
168
|
+
UpdateSupport.VALIDATE_CONSTRAINT.value,
|
|
169
|
+
UpdateSupport.MIGRATION_REQUIRED.value,
|
|
170
|
+
):
|
|
158
171
|
continue
|
|
159
172
|
|
|
160
173
|
if prop_value is None:
|
|
@@ -168,6 +181,13 @@ class ConstraintValidatorDeterminer:
|
|
|
168
181
|
path_type = SchemaPathType.RELATIONSHIP
|
|
169
182
|
constraint_name = f"relationship.{prop_name}.update"
|
|
170
183
|
|
|
184
|
+
do_constraint_validation = prop_field_update == UpdateSupport.VALIDATE_CONSTRAINT.value or (
|
|
185
|
+
prop_field_update == UpdateSupport.MIGRATION_REQUIRED.value
|
|
186
|
+
and CONSTRAINT_VALIDATOR_MAP.get(constraint_name)
|
|
187
|
+
)
|
|
188
|
+
if not do_constraint_validation:
|
|
189
|
+
continue
|
|
190
|
+
|
|
171
191
|
schema_path = SchemaPath(
|
|
172
192
|
schema_kind=schema.kind,
|
|
173
193
|
path_type=path_type,
|
infrahub/events/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ from .models import EventMeta, InfrahubEvent
|
|
|
5
5
|
from .node_action import NodeCreatedEvent, NodeDeletedEvent, NodeUpdatedEvent
|
|
6
6
|
from .proposed_change_action import (
|
|
7
7
|
ProposedChangeApprovalRevokedEvent,
|
|
8
|
+
ProposedChangeApprovalsRevokedEvent,
|
|
8
9
|
ProposedChangeApprovedEvent,
|
|
9
10
|
ProposedChangeMergedEvent,
|
|
10
11
|
ProposedChangeRejectedEvent,
|
|
@@ -32,6 +33,7 @@ __all__ = [
|
|
|
32
33
|
"NodeDeletedEvent",
|
|
33
34
|
"NodeUpdatedEvent",
|
|
34
35
|
"ProposedChangeApprovalRevokedEvent",
|
|
36
|
+
"ProposedChangeApprovalsRevokedEvent",
|
|
35
37
|
"ProposedChangeApprovedEvent",
|
|
36
38
|
"ProposedChangeMergedEvent",
|
|
37
39
|
"ProposedChangeRejectedEvent",
|
|
@@ -150,6 +150,28 @@ class ProposedChangeRejectionRevokedEvent(ProposedChangeReviewRevokedEvent):
|
|
|
150
150
|
event_name: ClassVar[str] = f"{EVENT_NAMESPACE}.proposed_change.rejection_revoked"
|
|
151
151
|
|
|
152
152
|
|
|
153
|
+
class ProposedChangeApprovalsRevokedEvent(ProposedChangeEvent):
|
|
154
|
+
reviewer_accounts: dict[str, str] = Field(
|
|
155
|
+
default_factory=dict, description="ID to name map of accounts whose approval was revoked"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
event_name: ClassVar[str] = f"{EVENT_NAMESPACE}.proposed_change.approvals_revoked"
|
|
159
|
+
|
|
160
|
+
def get_related(self) -> list[dict[str, str]]:
|
|
161
|
+
related = super().get_related()
|
|
162
|
+
for account_id, account_name in self.reviewer_accounts.items():
|
|
163
|
+
related.append(
|
|
164
|
+
{
|
|
165
|
+
"prefect.resource.id": account_id,
|
|
166
|
+
"prefect.resource.role": "infrahub.related.node",
|
|
167
|
+
"infrahub.node.kind": InfrahubKind.GENERICACCOUNT,
|
|
168
|
+
"infrahub.node.id": account_id,
|
|
169
|
+
"infrahub.reviewer.account.name": account_name,
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
return related
|
|
173
|
+
|
|
174
|
+
|
|
153
175
|
class ProposedChangeThreadEvent(ProposedChangeEvent):
|
|
154
176
|
thread_id: str = Field(..., description="The ID of the thread that was created or updated")
|
|
155
177
|
thread_kind: str = Field(..., description="The name of the thread that was created or updated")
|
infrahub/graphql/app.py
CHANGED
|
@@ -231,6 +231,7 @@ class InfrahubGraphQLApp:
|
|
|
231
231
|
operation_name=operation_name,
|
|
232
232
|
branch=branch,
|
|
233
233
|
)
|
|
234
|
+
impacted_models = analyzed_query.query_report.impacted_models
|
|
234
235
|
|
|
235
236
|
await self._evaluate_permissions(
|
|
236
237
|
db=db,
|
|
@@ -282,7 +283,7 @@ class InfrahubGraphQLApp:
|
|
|
282
283
|
GRAPHQL_QUERY_HEIGHT_METRICS.labels(**labels).observe(await analyzed_query.calculate_height())
|
|
283
284
|
# GRAPHQL_QUERY_VARS_METRICS.labels(**labels).observe(len(analyzed_query.variables))
|
|
284
285
|
GRAPHQL_TOP_LEVEL_QUERIES_METRICS.labels(**labels).observe(analyzed_query.nbr_queries)
|
|
285
|
-
GRAPHQL_QUERY_OBJECTS_METRICS.labels(**labels).observe(len(
|
|
286
|
+
GRAPHQL_QUERY_OBJECTS_METRICS.labels(**labels).observe(len(impacted_models))
|
|
286
287
|
|
|
287
288
|
_, errors = analyzed_query.is_valid
|
|
288
289
|
if errors:
|
infrahub/graphql/context.py
CHANGED
|
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
|
|
5
5
|
from infrahub.core.constants import GlobalPermissions, InfrahubKind
|
|
6
6
|
from infrahub.core.manager import NodeManager
|
|
7
7
|
from infrahub.exceptions import NodeNotFoundError, ValidationError
|
|
8
|
-
from infrahub.permissions
|
|
8
|
+
from infrahub.permissions import define_global_permission_from_branch
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
from .initialization import GraphqlContext
|
|
@@ -137,6 +137,9 @@ class InfrahubProposedChangeMutation(InfrahubMutationMixin, Mutation):
|
|
|
137
137
|
updated_state = ProposedChangeState(state_update)
|
|
138
138
|
state.validate_state_transition(updated_state)
|
|
139
139
|
|
|
140
|
+
# Check if the draft state will change (defaults to current draft state)
|
|
141
|
+
will_be_draft = data.get("is_draft", {}).get("value", obj.is_draft.value)
|
|
142
|
+
|
|
140
143
|
# Check before starting a transaction, stopping in the middle of the transaction seems to break with memgraph
|
|
141
144
|
if updated_state == ProposedChangeState.MERGED and graphql_context.account_session:
|
|
142
145
|
try:
|
|
@@ -150,6 +153,8 @@ class InfrahubProposedChangeMutation(InfrahubMutationMixin, Mutation):
|
|
|
150
153
|
raise ValidationError(str(exc)) from exc
|
|
151
154
|
|
|
152
155
|
if updated_state == ProposedChangeState.MERGED:
|
|
156
|
+
if will_be_draft:
|
|
157
|
+
raise ValidationError("A draft proposed change is not allowed to be merged")
|
|
153
158
|
data["state"]["value"] = ProposedChangeState.MERGING.value
|
|
154
159
|
|
|
155
160
|
proposed_change, result = await super().mutate_update(
|
|
@@ -233,7 +233,7 @@ class RelationshipRemove(Mutation):
|
|
|
233
233
|
# we should use RelationshipDataDeleteQuery to delete the relationship
|
|
234
234
|
# it would be more query efficient
|
|
235
235
|
rel = Relationship(schema=rel_schema, branch=graphql_context.branch, node=source)
|
|
236
|
-
|
|
236
|
+
rel.load(db=db, data=existing_peers[node_data.get("id")])
|
|
237
237
|
if group_event_type != GroupUpdateType.NONE:
|
|
238
238
|
peers.append(EventNode(id=rel.get_peer_id(), kind=nodes[rel.get_peer_id()].get_kind()))
|
|
239
239
|
node_changelog.delete_relationship(relationship=rel)
|
|
@@ -6,7 +6,7 @@ from graphene import Boolean, Field, InputObjectType, Mutation, String
|
|
|
6
6
|
|
|
7
7
|
from infrahub import lock
|
|
8
8
|
from infrahub.core import registry
|
|
9
|
-
from infrahub.core.constants import RESTRICTED_NAMESPACES
|
|
9
|
+
from infrahub.core.constants import RESTRICTED_NAMESPACES, GlobalPermissions
|
|
10
10
|
from infrahub.core.manager import NodeManager
|
|
11
11
|
from infrahub.core.schema import DropdownChoice, GenericSchema, NodeSchema
|
|
12
12
|
from infrahub.database import InfrahubDatabase, retry_db_transaction
|
|
@@ -16,6 +16,7 @@ from infrahub.exceptions import ValidationError
|
|
|
16
16
|
from infrahub.graphql.context import apply_external_context
|
|
17
17
|
from infrahub.graphql.types.context import ContextInput
|
|
18
18
|
from infrahub.log import get_log_data, get_logger
|
|
19
|
+
from infrahub.permissions import define_global_permission_from_branch
|
|
19
20
|
from infrahub.worker import WORKER_IDENTITY
|
|
20
21
|
|
|
21
22
|
from ..types import DropdownFields
|
|
@@ -32,6 +33,14 @@ if TYPE_CHECKING:
|
|
|
32
33
|
log = get_logger()
|
|
33
34
|
|
|
34
35
|
|
|
36
|
+
def _validate_schema_permission(graphql_context: GraphqlContext) -> None:
|
|
37
|
+
graphql_context.active_permissions.raise_for_permission(
|
|
38
|
+
permission=define_global_permission_from_branch(
|
|
39
|
+
permission=GlobalPermissions.MANAGE_SCHEMA, branch_name=graphql_context.branch.name
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
35
44
|
class SchemaEnumInput(InputObjectType):
|
|
36
45
|
kind = String(required=True)
|
|
37
46
|
attribute = String(required=True)
|
|
@@ -69,6 +78,7 @@ class SchemaDropdownAdd(Mutation):
|
|
|
69
78
|
) -> Self:
|
|
70
79
|
graphql_context: GraphqlContext = info.context
|
|
71
80
|
|
|
81
|
+
_validate_schema_permission(graphql_context=graphql_context)
|
|
72
82
|
await apply_external_context(graphql_context=graphql_context, context_input=context)
|
|
73
83
|
|
|
74
84
|
kind = graphql_context.db.schema.get(name=str(data.kind), branch=graphql_context.branch.name)
|
|
@@ -130,6 +140,7 @@ class SchemaDropdownRemove(Mutation):
|
|
|
130
140
|
) -> dict[str, bool]:
|
|
131
141
|
graphql_context: GraphqlContext = info.context
|
|
132
142
|
|
|
143
|
+
_validate_schema_permission(graphql_context=graphql_context)
|
|
133
144
|
kind = graphql_context.db.schema.get(name=str(data.kind), branch=graphql_context.branch.name)
|
|
134
145
|
await apply_external_context(graphql_context=graphql_context, context_input=context)
|
|
135
146
|
|
|
@@ -185,6 +196,7 @@ class SchemaEnumAdd(Mutation):
|
|
|
185
196
|
) -> dict[str, bool]:
|
|
186
197
|
graphql_context: GraphqlContext = info.context
|
|
187
198
|
|
|
199
|
+
_validate_schema_permission(graphql_context=graphql_context)
|
|
188
200
|
kind = graphql_context.db.schema.get(name=str(data.kind), branch=graphql_context.branch.name)
|
|
189
201
|
await apply_external_context(graphql_context=graphql_context, context_input=context)
|
|
190
202
|
|
|
@@ -230,6 +242,7 @@ class SchemaEnumRemove(Mutation):
|
|
|
230
242
|
) -> dict[str, bool]:
|
|
231
243
|
graphql_context: GraphqlContext = info.context
|
|
232
244
|
|
|
245
|
+
_validate_schema_permission(graphql_context=graphql_context)
|
|
233
246
|
kind = graphql_context.db.schema.get(name=str(data.kind), branch=graphql_context.branch.name)
|
|
234
247
|
await apply_external_context(graphql_context=graphql_context, context_input=context)
|
|
235
248
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from dataclasses import dataclass
|
|
3
4
|
from typing import TYPE_CHECKING, Any
|
|
4
5
|
|
|
5
6
|
from graphene import Argument, Boolean, DateTime, Field, InputObjectType, Int, List, NonNull, ObjectType, String
|
|
6
7
|
from graphene import Enum as GrapheneEnum
|
|
7
8
|
|
|
8
9
|
from infrahub.core import registry
|
|
9
|
-
from infrahub.core.constants import DiffAction, RelationshipCardinality
|
|
10
|
+
from infrahub.core.constants import DiffAction, RelationshipCardinality, RelationshipDirection
|
|
10
11
|
from infrahub.core.constants.database import DatabaseEdgeType
|
|
11
12
|
from infrahub.core.diff.model.path import NameTrackingId
|
|
12
13
|
from infrahub.core.diff.query.filters import EnrichedDiffQueryFilters
|
|
@@ -38,6 +39,12 @@ GrapheneDiffActionEnum = GrapheneEnum.from_enum(DiffAction)
|
|
|
38
39
|
GrapheneCardinalityEnum = GrapheneEnum.from_enum(RelationshipCardinality)
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
@dataclass
|
|
43
|
+
class ParentNodeInfo:
|
|
44
|
+
node: EnrichedDiffNode
|
|
45
|
+
relationship_name: str
|
|
46
|
+
|
|
47
|
+
|
|
41
48
|
class ConflictDetails(ObjectType):
|
|
42
49
|
uuid = String(required=True)
|
|
43
50
|
base_branch_action = Field(GrapheneDiffActionEnum, required=True)
|
|
@@ -145,9 +152,16 @@ class DiffTreeSummary(DiffSummaryCounts):
|
|
|
145
152
|
|
|
146
153
|
|
|
147
154
|
class DiffTreeResolver:
|
|
155
|
+
def __init__(self) -> None:
|
|
156
|
+
self.source_branch_name: str | None = None
|
|
157
|
+
|
|
158
|
+
def initialize(self, enriched_diff_root: EnrichedDiffRoot) -> None:
|
|
159
|
+
self.source_branch_name = enriched_diff_root.diff_branch_name
|
|
160
|
+
|
|
148
161
|
async def to_diff_tree(
|
|
149
162
|
self, enriched_diff_root: EnrichedDiffRoot, graphql_context: GraphqlContext | None = None
|
|
150
163
|
) -> DiffTree:
|
|
164
|
+
self.initialize(enriched_diff_root=enriched_diff_root)
|
|
151
165
|
all_nodes = list(enriched_diff_root.nodes)
|
|
152
166
|
tree_nodes = [self.to_diff_node(enriched_node=e_node, graphql_context=graphql_context) for e_node in all_nodes]
|
|
153
167
|
name = None
|
|
@@ -166,6 +180,43 @@ class DiffTreeResolver:
|
|
|
166
180
|
num_conflicts=enriched_diff_root.num_conflicts,
|
|
167
181
|
)
|
|
168
182
|
|
|
183
|
+
def _get_parent_info(
|
|
184
|
+
self, diff_node: EnrichedDiffNode, graphql_context: GraphqlContext | None = None
|
|
185
|
+
) -> ParentNodeInfo | None:
|
|
186
|
+
for r in diff_node.relationships:
|
|
187
|
+
for n in r.nodes:
|
|
188
|
+
relationship_name: str = "undefined"
|
|
189
|
+
|
|
190
|
+
if not graphql_context or not self.source_branch_name:
|
|
191
|
+
return ParentNodeInfo(node=n, relationship_name=relationship_name)
|
|
192
|
+
|
|
193
|
+
node_schema = graphql_context.db.schema.get(
|
|
194
|
+
name=diff_node.kind, branch=self.source_branch_name, duplicate=False
|
|
195
|
+
)
|
|
196
|
+
rel_schema = node_schema.get_relationship(name=r.name)
|
|
197
|
+
|
|
198
|
+
parent_schema = graphql_context.db.schema.get(
|
|
199
|
+
name=n.kind, branch=self.source_branch_name, duplicate=False
|
|
200
|
+
)
|
|
201
|
+
rels_parent = parent_schema.get_relationships_by_identifier(id=rel_schema.get_identifier())
|
|
202
|
+
|
|
203
|
+
if rels_parent and len(rels_parent) == 1:
|
|
204
|
+
relationship_name = rels_parent[0].name
|
|
205
|
+
elif rels_parent and len(rels_parent) > 1:
|
|
206
|
+
for rel_parent in rels_parent:
|
|
207
|
+
if (
|
|
208
|
+
rel_schema.direction == RelationshipDirection.INBOUND
|
|
209
|
+
and rel_parent.direction == RelationshipDirection.OUTBOUND
|
|
210
|
+
) or (
|
|
211
|
+
rel_schema.direction == RelationshipDirection.OUTBOUND
|
|
212
|
+
and rel_parent.direction == RelationshipDirection.INBOUND
|
|
213
|
+
):
|
|
214
|
+
relationship_name = rel_parent.name
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
return ParentNodeInfo(node=n, relationship_name=relationship_name)
|
|
218
|
+
return None
|
|
219
|
+
|
|
169
220
|
def to_diff_node(self, enriched_node: EnrichedDiffNode, graphql_context: GraphqlContext | None = None) -> DiffNode:
|
|
170
221
|
diff_attributes = [
|
|
171
222
|
self.to_diff_attribute(enriched_attribute=e_attr, graphql_context=graphql_context)
|
|
@@ -181,7 +232,7 @@ class DiffTreeResolver:
|
|
|
181
232
|
conflict = self.to_diff_conflict(enriched_conflict=enriched_node.conflict, graphql_context=graphql_context)
|
|
182
233
|
|
|
183
234
|
parent = None
|
|
184
|
-
if parent_info :=
|
|
235
|
+
if parent_info := self._get_parent_info(diff_node=enriched_node, graphql_context=graphql_context):
|
|
185
236
|
parent = DiffNodeParent(
|
|
186
237
|
uuid=parent_info.node.uuid,
|
|
187
238
|
kind=parent_info.node.kind,
|
infrahub/graphql/schema.py
CHANGED
|
@@ -26,21 +26,10 @@ from .mutations.proposed_change import (
|
|
|
26
26
|
ProposedChangeRequestRunCheck,
|
|
27
27
|
ProposedChangeReview,
|
|
28
28
|
)
|
|
29
|
-
from .mutations.relationship import
|
|
30
|
-
|
|
31
|
-
RelationshipRemove,
|
|
32
|
-
)
|
|
33
|
-
from .mutations.repository import (
|
|
34
|
-
ProcessRepository,
|
|
35
|
-
ValidateRepositoryConnectivity,
|
|
36
|
-
)
|
|
29
|
+
from .mutations.relationship import RelationshipAdd, RelationshipRemove
|
|
30
|
+
from .mutations.repository import ProcessRepository, ValidateRepositoryConnectivity
|
|
37
31
|
from .mutations.resource_manager import IPAddressPoolGetResource, IPPrefixPoolGetResource
|
|
38
|
-
from .mutations.schema import
|
|
39
|
-
SchemaDropdownAdd,
|
|
40
|
-
SchemaDropdownRemove,
|
|
41
|
-
SchemaEnumAdd,
|
|
42
|
-
SchemaEnumRemove,
|
|
43
|
-
)
|
|
32
|
+
from .mutations.schema import SchemaDropdownAdd, SchemaDropdownRemove, SchemaEnumAdd, SchemaEnumRemove
|
|
44
33
|
from .queries import (
|
|
45
34
|
AccountPermissions,
|
|
46
35
|
AccountToken,
|
infrahub/graphql/types/event.py
CHANGED
|
@@ -136,6 +136,13 @@ class ProposedChangeReviewRevokedEvent(ObjectType):
|
|
|
136
136
|
payload = Field(GenericScalar, required=True)
|
|
137
137
|
|
|
138
138
|
|
|
139
|
+
class ProposedChangeApprovalsRevokedEvent(ObjectType):
|
|
140
|
+
class Meta:
|
|
141
|
+
interfaces = (EventNodeInterface,)
|
|
142
|
+
|
|
143
|
+
payload = Field(GenericScalar, required=True)
|
|
144
|
+
|
|
145
|
+
|
|
139
146
|
class ProposedChangeReviewRequestedEvent(ObjectType):
|
|
140
147
|
class Meta:
|
|
141
148
|
interfaces = (EventNodeInterface,)
|
|
@@ -220,6 +227,7 @@ EVENT_TYPES: dict[str, type[ObjectType]] = {
|
|
|
220
227
|
events.ProposedChangeRejectedEvent.event_name: ProposedChangeReviewEvent,
|
|
221
228
|
events.ProposedChangeRejectionRevokedEvent.event_name: ProposedChangeReviewRevokedEvent,
|
|
222
229
|
events.ProposedChangeReviewRequestedEvent.event_name: ProposedChangeReviewRequestedEvent,
|
|
230
|
+
events.ProposedChangeApprovalsRevokedEvent.event_name: ProposedChangeApprovalsRevokedEvent,
|
|
223
231
|
events.ProposedChangeMergedEvent.event_name: ProposedChangeMergedEvent,
|
|
224
232
|
events.ProposedChangeThreadCreatedEvent.event_name: ProposedChangeThreadEvent,
|
|
225
233
|
events.ProposedChangeThreadUpdatedEvent.event_name: ProposedChangeThreadEvent,
|
infrahub/permissions/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from infrahub.permissions.backend import PermissionBackend
|
|
2
|
+
from infrahub.permissions.globals import define_global_permission_from_branch, get_or_create_global_permission
|
|
2
3
|
from infrahub.permissions.local_backend import LocalPermissionBackend
|
|
3
4
|
from infrahub.permissions.manager import PermissionManager
|
|
4
5
|
from infrahub.permissions.report import report_schema_permissions
|
|
@@ -9,6 +10,8 @@ __all__ = [
|
|
|
9
10
|
"LocalPermissionBackend",
|
|
10
11
|
"PermissionBackend",
|
|
11
12
|
"PermissionManager",
|
|
13
|
+
"define_global_permission_from_branch",
|
|
12
14
|
"get_global_permission_for_kind",
|
|
15
|
+
"get_or_create_global_permission",
|
|
13
16
|
"report_schema_permissions",
|
|
14
17
|
]
|
|
@@ -25,8 +25,21 @@ GLOBAL_PERMISSION_DENIAL_MESSAGE = {
|
|
|
25
25
|
GlobalPermissions.EDIT_DEFAULT_BRANCH.value: "You are not allowed to change data in the default branch",
|
|
26
26
|
GlobalPermissions.MERGE_BRANCH.value: "You are not allowed to merge a branch",
|
|
27
27
|
GlobalPermissions.MERGE_PROPOSED_CHANGE.value: "You are not allowed to merge proposed changes",
|
|
28
|
+
GlobalPermissions.REVIEW_PROPOSED_CHANGE.value: "You are not allowed to review proposed changes",
|
|
28
29
|
GlobalPermissions.MANAGE_SCHEMA.value: "You are not allowed to manage the schema",
|
|
29
30
|
GlobalPermissions.MANAGE_ACCOUNTS.value: "You are not allowed to manage user accounts, groups or roles",
|
|
30
31
|
GlobalPermissions.MANAGE_PERMISSIONS.value: "You are not allowed to manage permissions",
|
|
31
32
|
GlobalPermissions.MANAGE_REPOSITORIES.value: "You are not allowed to manage repositories",
|
|
32
33
|
}
|
|
34
|
+
|
|
35
|
+
GLOBAL_PERMISSION_DESCRIPTION = {
|
|
36
|
+
GlobalPermissions.EDIT_DEFAULT_BRANCH: "Allow a user to change data in the default branch",
|
|
37
|
+
GlobalPermissions.MERGE_BRANCH: "Allow a user to merge branches",
|
|
38
|
+
GlobalPermissions.MERGE_PROPOSED_CHANGE: "Allow a user to merge proposed changes",
|
|
39
|
+
GlobalPermissions.REVIEW_PROPOSED_CHANGE: "Allow a user to approve or reject proposed changes",
|
|
40
|
+
GlobalPermissions.MANAGE_SCHEMA: "Allow a user to manage the schema",
|
|
41
|
+
GlobalPermissions.MANAGE_ACCOUNTS: "Allow a user to manage accounts, account roles and account groups",
|
|
42
|
+
GlobalPermissions.MANAGE_PERMISSIONS: "Allow a user to manage permissions",
|
|
43
|
+
GlobalPermissions.MANAGE_REPOSITORIES: "Allow a user to manage repositories",
|
|
44
|
+
GlobalPermissions.SUPER_ADMIN: "Allow a user to do anything",
|
|
45
|
+
}
|