infrahub-server 1.4.0b0__py3-none-any.whl → 1.4.0rc0__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/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 +5 -1
- 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/m037_index_attr_vals.py +577 -0
- 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/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/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/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 +6 -6
- infrahub_sdk/ctl/repository.py +0 -51
- infrahub_sdk/ctl/schema.py +9 -9
- infrahub_sdk/protocols.py +6 -40
- infrahub_sdk/utils.py +9 -5
- {infrahub_server-1.4.0b0.dist-info → infrahub_server-1.4.0rc0.dist-info}/METADATA +5 -4
- {infrahub_server-1.4.0b0.dist-info → infrahub_server-1.4.0rc0.dist-info}/RECORD +52 -47
- {infrahub_server-1.4.0b0.dist-info → infrahub_server-1.4.0rc0.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.4.0b0.dist-info → infrahub_server-1.4.0rc0.dist-info}/WHEEL +0 -0
- {infrahub_server-1.4.0b0.dist-info → infrahub_server-1.4.0rc0.dist-info}/entry_points.txt +0 -0
|
@@ -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,
|
|
@@ -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/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
|
|
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
|
+
}
|
infrahub/permissions/globals.py
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
1
5
|
from infrahub.core.account import GlobalPermission
|
|
2
6
|
from infrahub.core.constants import GLOBAL_BRANCH_NAME, GlobalPermissions, PermissionDecision
|
|
7
|
+
from infrahub.core.manager import NodeManager
|
|
8
|
+
from infrahub.core.node import Node
|
|
9
|
+
from infrahub.core.protocols import CoreGlobalPermission
|
|
3
10
|
from infrahub.core.registry import registry
|
|
4
11
|
|
|
12
|
+
from .constants import GLOBAL_PERMISSION_DESCRIPTION
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from infrahub.database import InfrahubDatabase
|
|
16
|
+
|
|
5
17
|
|
|
6
18
|
def define_global_permission_from_branch(permission: GlobalPermissions, branch_name: str) -> GlobalPermission:
|
|
7
19
|
if branch_name in (GLOBAL_BRANCH_NAME, registry.default_branch):
|
|
@@ -10,3 +22,23 @@ def define_global_permission_from_branch(permission: GlobalPermissions, branch_n
|
|
|
10
22
|
decision = PermissionDecision.ALLOW_OTHER
|
|
11
23
|
|
|
12
24
|
return GlobalPermission(action=permission.value, decision=decision.value)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def get_or_create_global_permission(db: InfrahubDatabase, permission: GlobalPermissions) -> CoreGlobalPermission:
|
|
28
|
+
permissions = await NodeManager.query(
|
|
29
|
+
db=db, schema=CoreGlobalPermission, filters={"action__value": permission.value}, limit=1
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if permissions:
|
|
33
|
+
return permissions[0]
|
|
34
|
+
|
|
35
|
+
p = await Node.init(db=db, schema=CoreGlobalPermission)
|
|
36
|
+
await p.new(
|
|
37
|
+
db=db,
|
|
38
|
+
action=permission.value,
|
|
39
|
+
decision=PermissionDecision.ALLOW_ALL.value,
|
|
40
|
+
description=GLOBAL_PERMISSION_DESCRIPTION[permission],
|
|
41
|
+
)
|
|
42
|
+
await p.save(db=db)
|
|
43
|
+
|
|
44
|
+
return p
|
infrahub/task_manager/event.py
CHANGED
|
@@ -242,7 +242,11 @@ class PrefectEventData(PrefectEventModel):
|
|
|
242
242
|
**self._return_proposed_change_event(),
|
|
243
243
|
**self._return_proposed_change_reviewer_former_decision(),
|
|
244
244
|
}
|
|
245
|
-
case
|
|
245
|
+
case (
|
|
246
|
+
"infrahub.proposed_change.approvals_revoked"
|
|
247
|
+
| "infrahub.proposed_change.review_requested"
|
|
248
|
+
| "infrahub.proposed_change.merged"
|
|
249
|
+
):
|
|
246
250
|
event_specifics = self._return_proposed_change_event()
|
|
247
251
|
|
|
248
252
|
return event_specifics
|
infrahub_sdk/client.py
CHANGED
|
@@ -250,7 +250,7 @@ class BaseClient:
|
|
|
250
250
|
|
|
251
251
|
return Mutation(
|
|
252
252
|
name="AllocateIPAddress",
|
|
253
|
-
mutation="
|
|
253
|
+
mutation="IPAddressPoolGetResource",
|
|
254
254
|
query={"ok": None, "node": {"id": None, "kind": None, "identifier": None, "display_label": None}},
|
|
255
255
|
input_data={"data": input_data},
|
|
256
256
|
)
|
|
@@ -281,7 +281,7 @@ class BaseClient:
|
|
|
281
281
|
|
|
282
282
|
return Mutation(
|
|
283
283
|
name="AllocateIPPrefix",
|
|
284
|
-
mutation="
|
|
284
|
+
mutation="IPPrefixPoolGetResource",
|
|
285
285
|
query={"ok": None, "node": {"id": None, "kind": None, "identifier": None, "display_label": None}},
|
|
286
286
|
input_data={"data": input_data},
|
|
287
287
|
)
|
|
@@ -1300,7 +1300,7 @@ class InfrahubClient(BaseClient):
|
|
|
1300
1300
|
raise ValueError("resource_pool is not an IP address pool")
|
|
1301
1301
|
|
|
1302
1302
|
branch = branch or self.default_branch
|
|
1303
|
-
mutation_name = "
|
|
1303
|
+
mutation_name = "IPAddressPoolGetResource"
|
|
1304
1304
|
|
|
1305
1305
|
query = self._build_ip_address_allocation_query(
|
|
1306
1306
|
resource_pool_id=resource_pool.id,
|
|
@@ -1452,7 +1452,7 @@ class InfrahubClient(BaseClient):
|
|
|
1452
1452
|
raise ValueError("resource_pool is not an IP prefix pool")
|
|
1453
1453
|
|
|
1454
1454
|
branch = branch or self.default_branch
|
|
1455
|
-
mutation_name = "
|
|
1455
|
+
mutation_name = "IPPrefixPoolGetResource"
|
|
1456
1456
|
|
|
1457
1457
|
query = self._build_ip_prefix_allocation_query(
|
|
1458
1458
|
resource_pool_id=resource_pool.id,
|
|
@@ -2438,7 +2438,7 @@ class InfrahubClientSync(BaseClient):
|
|
|
2438
2438
|
raise ValueError("resource_pool is not an IP address pool")
|
|
2439
2439
|
|
|
2440
2440
|
branch = branch or self.default_branch
|
|
2441
|
-
mutation_name = "
|
|
2441
|
+
mutation_name = "IPAddressPoolGetResource"
|
|
2442
2442
|
|
|
2443
2443
|
query = self._build_ip_address_allocation_query(
|
|
2444
2444
|
resource_pool_id=resource_pool.id,
|
|
@@ -2586,7 +2586,7 @@ class InfrahubClientSync(BaseClient):
|
|
|
2586
2586
|
raise ValueError("resource_pool is not an IP prefix pool")
|
|
2587
2587
|
|
|
2588
2588
|
branch = branch or self.default_branch
|
|
2589
|
-
mutation_name = "
|
|
2589
|
+
mutation_name = "IPPrefixPoolGetResource"
|
|
2590
2590
|
|
|
2591
2591
|
query = self._build_ip_prefix_allocation_query(
|
|
2592
2592
|
resource_pool_id=resource_pool.id,
|
infrahub_sdk/ctl/repository.py
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
from typing import Optional
|
|
6
5
|
|
|
7
6
|
import typer
|
|
8
7
|
import yaml
|
|
9
|
-
from copier import run_copy
|
|
10
8
|
from pydantic import ValidationError
|
|
11
9
|
from rich.console import Console
|
|
12
10
|
from rich.table import Table
|
|
@@ -167,52 +165,3 @@ async def list(
|
|
|
167
165
|
)
|
|
168
166
|
|
|
169
167
|
console.print(table)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
@app.command()
|
|
173
|
-
async def init(
|
|
174
|
-
directory: Path = typer.Argument(help="Directory path for the new project."),
|
|
175
|
-
template: str = typer.Option(
|
|
176
|
-
default="https://github.com/opsmill/infrahub-template.git",
|
|
177
|
-
help="Template to use for the new repository. Can be a local path or a git repository URL.",
|
|
178
|
-
),
|
|
179
|
-
data: Optional[Path] = typer.Option(default=None, help="Path to YAML file containing answers to CLI prompt."),
|
|
180
|
-
vcs_ref: Optional[str] = typer.Option(
|
|
181
|
-
default="HEAD",
|
|
182
|
-
help="VCS reference to use for the template. Defaults to HEAD.",
|
|
183
|
-
),
|
|
184
|
-
trust: Optional[bool] = typer.Option(
|
|
185
|
-
default=False,
|
|
186
|
-
help="Trust the template repository. If set, the template will be cloned without verification.",
|
|
187
|
-
),
|
|
188
|
-
_: str = CONFIG_PARAM,
|
|
189
|
-
) -> None:
|
|
190
|
-
"""Initialize a new Infrahub repository."""
|
|
191
|
-
|
|
192
|
-
config_data = None
|
|
193
|
-
if data:
|
|
194
|
-
try:
|
|
195
|
-
with Path.open(data, encoding="utf-8") as file:
|
|
196
|
-
config_data = yaml.safe_load(file)
|
|
197
|
-
typer.echo(f"Loaded config: {config_data}")
|
|
198
|
-
except Exception as exc:
|
|
199
|
-
typer.echo(f"Error loading YAML file: {exc}", err=True)
|
|
200
|
-
raise typer.Exit(code=1)
|
|
201
|
-
|
|
202
|
-
# Allow template to be a local path or a URL
|
|
203
|
-
template_source = template or ""
|
|
204
|
-
if template and Path(template).exists():
|
|
205
|
-
template_source = str(Path(template).resolve())
|
|
206
|
-
|
|
207
|
-
try:
|
|
208
|
-
await asyncio.to_thread(
|
|
209
|
-
run_copy,
|
|
210
|
-
template_source,
|
|
211
|
-
str(directory),
|
|
212
|
-
data=config_data,
|
|
213
|
-
vcs_ref=vcs_ref,
|
|
214
|
-
unsafe=trust,
|
|
215
|
-
)
|
|
216
|
-
except Exception as e:
|
|
217
|
-
typer.echo(f"Error running copier: {e}", err=True)
|
|
218
|
-
raise typer.Exit(code=1)
|