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.
Files changed (52) hide show
  1. infrahub/api/schema.py +3 -7
  2. infrahub/cli/db.py +25 -0
  3. infrahub/cli/db_commands/__init__.py +0 -0
  4. infrahub/cli/db_commands/check_inheritance.py +284 -0
  5. infrahub/cli/upgrade.py +3 -0
  6. infrahub/config.py +4 -4
  7. infrahub/core/attribute.py +6 -0
  8. infrahub/core/constants/__init__.py +1 -0
  9. infrahub/core/graph/__init__.py +1 -1
  10. infrahub/core/initialization.py +26 -21
  11. infrahub/core/manager.py +2 -2
  12. infrahub/core/migrations/__init__.py +2 -0
  13. infrahub/core/migrations/graph/__init__.py +5 -1
  14. infrahub/core/migrations/graph/m033_deduplicate_relationship_vertices.py +1 -1
  15. infrahub/core/migrations/graph/m035_orphan_relationships.py +43 -0
  16. infrahub/core/migrations/graph/{m035_drop_attr_value_index.py → m036_drop_attr_value_index.py} +3 -3
  17. infrahub/core/migrations/graph/m037_index_attr_vals.py +577 -0
  18. infrahub/core/migrations/query/node_duplicate.py +26 -3
  19. infrahub/core/migrations/schema/attribute_kind_update.py +156 -0
  20. infrahub/core/models.py +5 -1
  21. infrahub/core/node/resource_manager/ip_address_pool.py +50 -48
  22. infrahub/core/node/resource_manager/ip_prefix_pool.py +55 -53
  23. infrahub/core/node/resource_manager/number_pool.py +20 -18
  24. infrahub/core/query/branch.py +37 -20
  25. infrahub/core/query/node.py +15 -0
  26. infrahub/core/relationship/model.py +13 -13
  27. infrahub/core/schema/definitions/internal.py +1 -1
  28. infrahub/core/schema/generated/attribute_schema.py +1 -1
  29. infrahub/core/validators/attribute/kind.py +5 -1
  30. infrahub/core/validators/determiner.py +22 -2
  31. infrahub/events/__init__.py +2 -0
  32. infrahub/events/proposed_change_action.py +22 -0
  33. infrahub/graphql/context.py +1 -1
  34. infrahub/graphql/mutations/proposed_change.py +5 -0
  35. infrahub/graphql/mutations/relationship.py +1 -1
  36. infrahub/graphql/mutations/schema.py +14 -1
  37. infrahub/graphql/schema.py +3 -14
  38. infrahub/graphql/types/event.py +8 -0
  39. infrahub/permissions/__init__.py +3 -0
  40. infrahub/permissions/constants.py +13 -0
  41. infrahub/permissions/globals.py +32 -0
  42. infrahub/task_manager/event.py +5 -1
  43. infrahub_sdk/client.py +6 -6
  44. infrahub_sdk/ctl/repository.py +0 -51
  45. infrahub_sdk/ctl/schema.py +9 -9
  46. infrahub_sdk/protocols.py +6 -40
  47. infrahub_sdk/utils.py +9 -5
  48. {infrahub_server-1.4.0b0.dist-info → infrahub_server-1.4.0rc0.dist-info}/METADATA +5 -4
  49. {infrahub_server-1.4.0b0.dist-info → infrahub_server-1.4.0rc0.dist-info}/RECORD +52 -47
  50. {infrahub_server-1.4.0b0.dist-info → infrahub_server-1.4.0rc0.dist-info}/LICENSE.txt +0 -0
  51. {infrahub_server-1.4.0b0.dist-info → infrahub_server-1.4.0rc0.dist-info}/WHEEL +0 -0
  52. {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
- async def _process_data(self, data: dict | RelationshipPeerData | str) -> None:
169
+ def _process_data(self, data: dict | RelationshipPeerData | str) -> None:
170
170
  self.data = data
171
171
 
172
172
  if isinstance(data, RelationshipPeerData):
173
- await self.set_peer(value=str(data.peer_id))
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
- await self.set_peer(value=data.get(key, None))
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
- await self.set_peer(value=data)
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
- await self._process_data(data=data)
209
+ self._process_data(data=data)
210
210
 
211
211
  return self
212
212
 
213
- async def load(
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
- await self._process_data(data=data)
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
- async def set_peer(self, value: str | Node) -> None:
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
- await self.set_peer(value=peer)
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
- await self.set_peer(value=peer)
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
- await self.set_peer(value=assigned_peer)
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
- await Relationship(
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
- await rel.load(data=item, db=db)
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.VALIDATE_CONSTRAINT},
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": "validate_constraint"}
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=result.get("attribute_value"), name=self.attribute_schema.name, schema=self.attribute_schema
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 != UpdateSupport.VALIDATE_CONSTRAINT.value:
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 != UpdateSupport.VALIDATE_CONSTRAINT.value:
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,
@@ -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")
@@ -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.globals import define_global_permission_from_branch
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
- await rel.load(db=db, data=existing_peers[node_data.get("id")])
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
 
@@ -26,21 +26,10 @@ from .mutations.proposed_change import (
26
26
  ProposedChangeRequestRunCheck,
27
27
  ProposedChangeReview,
28
28
  )
29
- from .mutations.relationship import (
30
- RelationshipAdd,
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,
@@ -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,
@@ -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
+ }
@@ -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
@@ -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 "infrahub.proposed_change.review_requested" | "infrahub.proposed_change.merged":
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="InfrahubIPAddressPoolGetResource",
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="InfrahubIPPrefixPoolGetResource",
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 = "InfrahubIPAddressPoolGetResource"
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 = "InfrahubIPPrefixPoolGetResource"
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 = "InfrahubIPAddressPoolGetResource"
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 = "InfrahubIPPrefixPoolGetResource"
2589
+ mutation_name = "IPPrefixPoolGetResource"
2590
2590
 
2591
2591
  query = self._build_ip_prefix_allocation_query(
2592
2592
  resource_pool_id=resource_pool.id,
@@ -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)