infrahub-server 1.5.0b1__py3-none-any.whl → 1.5.0b2__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 (118) hide show
  1. infrahub/api/internal.py +2 -0
  2. infrahub/api/oauth2.py +13 -19
  3. infrahub/api/oidc.py +15 -21
  4. infrahub/api/schema.py +24 -3
  5. infrahub/artifacts/models.py +2 -1
  6. infrahub/auth.py +137 -3
  7. infrahub/cli/__init__.py +2 -0
  8. infrahub/cli/db.py +83 -102
  9. infrahub/cli/dev.py +118 -0
  10. infrahub/cli/tasks.py +46 -0
  11. infrahub/cli/upgrade.py +30 -3
  12. infrahub/computed_attribute/tasks.py +20 -8
  13. infrahub/core/attribute.py +10 -2
  14. infrahub/core/branch/enums.py +1 -1
  15. infrahub/core/branch/models.py +7 -3
  16. infrahub/core/branch/tasks.py +68 -7
  17. infrahub/core/constants/__init__.py +3 -0
  18. infrahub/core/diff/query/artifact.py +1 -0
  19. infrahub/core/diff/query/field_summary.py +1 -0
  20. infrahub/core/graph/__init__.py +1 -1
  21. infrahub/core/initialization.py +5 -2
  22. infrahub/core/migrations/__init__.py +3 -0
  23. infrahub/core/migrations/exceptions.py +4 -0
  24. infrahub/core/migrations/graph/__init__.py +10 -13
  25. infrahub/core/migrations/graph/load_schema_branch.py +21 -0
  26. infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
  27. infrahub/core/migrations/graph/m040_duplicated_attributes.py +81 -0
  28. infrahub/core/migrations/graph/m041_profile_attrs_in_db.py +145 -0
  29. infrahub/core/migrations/graph/m042_create_hfid_display_label_in_db.py +164 -0
  30. infrahub/core/migrations/graph/m043_backfill_hfid_display_label_in_db.py +866 -0
  31. infrahub/core/migrations/query/__init__.py +7 -8
  32. infrahub/core/migrations/query/attribute_add.py +8 -6
  33. infrahub/core/migrations/query/attribute_remove.py +134 -0
  34. infrahub/core/migrations/runner.py +54 -0
  35. infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
  36. infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
  37. infrahub/core/migrations/schema/node_attribute_add.py +30 -2
  38. infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
  39. infrahub/core/migrations/schema/node_kind_update.py +2 -1
  40. infrahub/core/migrations/schema/node_remove.py +2 -1
  41. infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
  42. infrahub/core/migrations/shared.py +48 -14
  43. infrahub/core/node/__init__.py +16 -11
  44. infrahub/core/node/create.py +46 -63
  45. infrahub/core/node/lock_utils.py +70 -44
  46. infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
  47. infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
  48. infrahub/core/node/resource_manager/number_pool.py +2 -1
  49. infrahub/core/query/attribute.py +55 -0
  50. infrahub/core/query/ipam.py +1 -0
  51. infrahub/core/query/node.py +9 -3
  52. infrahub/core/query/relationship.py +1 -0
  53. infrahub/core/schema/__init__.py +56 -0
  54. infrahub/core/schema/attribute_schema.py +4 -0
  55. infrahub/core/schema/definitions/internal.py +2 -2
  56. infrahub/core/schema/generated/attribute_schema.py +2 -2
  57. infrahub/core/schema/manager.py +22 -1
  58. infrahub/core/schema/schema_branch.py +180 -22
  59. infrahub/database/graph.py +21 -0
  60. infrahub/display_labels/tasks.py +13 -7
  61. infrahub/events/branch_action.py +27 -1
  62. infrahub/generators/tasks.py +3 -7
  63. infrahub/git/base.py +4 -1
  64. infrahub/git/integrator.py +1 -1
  65. infrahub/git/models.py +2 -1
  66. infrahub/git/repository.py +22 -5
  67. infrahub/git/tasks.py +66 -10
  68. infrahub/git/utils.py +123 -1
  69. infrahub/graphql/api/endpoints.py +14 -4
  70. infrahub/graphql/manager.py +4 -9
  71. infrahub/graphql/mutations/convert_object_type.py +11 -1
  72. infrahub/graphql/mutations/display_label.py +17 -10
  73. infrahub/graphql/mutations/hfid.py +17 -10
  74. infrahub/graphql/mutations/ipam.py +54 -35
  75. infrahub/graphql/mutations/main.py +27 -28
  76. infrahub/graphql/schema_sort.py +170 -0
  77. infrahub/graphql/types/branch.py +4 -1
  78. infrahub/graphql/types/enums.py +3 -0
  79. infrahub/hfid/tasks.py +13 -7
  80. infrahub/lock.py +52 -12
  81. infrahub/message_bus/types.py +2 -1
  82. infrahub/permissions/constants.py +2 -0
  83. infrahub/proposed_change/tasks.py +25 -16
  84. infrahub/server.py +6 -2
  85. infrahub/services/__init__.py +2 -2
  86. infrahub/services/adapters/http/__init__.py +5 -0
  87. infrahub/services/adapters/workflow/worker.py +14 -3
  88. infrahub/task_manager/event.py +5 -0
  89. infrahub/task_manager/models.py +7 -0
  90. infrahub/task_manager/task.py +73 -0
  91. infrahub/trigger/setup.py +13 -4
  92. infrahub/trigger/tasks.py +3 -0
  93. infrahub/workers/dependencies.py +10 -1
  94. infrahub/workers/infrahub_async.py +10 -2
  95. infrahub/workflows/catalogue.py +8 -0
  96. infrahub/workflows/initialization.py +5 -0
  97. infrahub/workflows/utils.py +2 -1
  98. infrahub_sdk/client.py +13 -10
  99. infrahub_sdk/config.py +29 -2
  100. infrahub_sdk/ctl/schema.py +22 -7
  101. infrahub_sdk/schema/__init__.py +32 -4
  102. infrahub_sdk/spec/models.py +7 -0
  103. infrahub_sdk/spec/object.py +37 -102
  104. infrahub_sdk/spec/processors/__init__.py +0 -0
  105. infrahub_sdk/spec/processors/data_processor.py +10 -0
  106. infrahub_sdk/spec/processors/factory.py +34 -0
  107. infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
  108. {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.0b2.dist-info}/METADATA +3 -1
  109. {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.0b2.dist-info}/RECORD +115 -101
  110. infrahub_testcontainers/container.py +114 -2
  111. infrahub_testcontainers/docker-compose-cluster.test.yml +5 -0
  112. infrahub_testcontainers/docker-compose.test.yml +5 -0
  113. infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +0 -166
  114. infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py +0 -97
  115. infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py +0 -86
  116. {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.0b2.dist-info}/LICENSE.txt +0 -0
  117. {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.0b2.dist-info}/WHEEL +0 -0
  118. {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.0b2.dist-info}/entry_points.txt +0 -0
infrahub/api/internal.py CHANGED
@@ -161,6 +161,8 @@ class SearchResultAPI(BaseModel):
161
161
  async def search_docs(
162
162
  query: str, limit: int | None = None, _: AccountSession = Depends(get_current_user)
163
163
  ) -> list[SearchResultAPI]:
164
+ if not query:
165
+ return []
164
166
  smart_query = smart_queries(query)
165
167
  search_results = search_docs_loader.heading_index.search(smart_query)
166
168
  heading_results = [
infrahub/api/oauth2.py CHANGED
@@ -11,14 +11,16 @@ from opentelemetry import trace
11
11
 
12
12
  from infrahub import config, models
13
13
  from infrahub.api.dependencies import get_db
14
- from infrahub.auth import get_groups_from_provider, signin_sso_account
15
- from infrahub.exceptions import GatewayError, ProcessingError
14
+ from infrahub.auth import (
15
+ get_groups_from_provider,
16
+ signin_sso_account,
17
+ validate_auth_response,
18
+ )
19
+ from infrahub.exceptions import ProcessingError
16
20
  from infrahub.log import get_logger
17
21
  from infrahub.message_bus.types import KVTTL
18
22
 
19
23
  if TYPE_CHECKING:
20
- import httpx
21
-
22
24
  from infrahub.database import InfrahubDatabase
23
25
  from infrahub.services import InfrahubServices
24
26
 
@@ -95,7 +97,7 @@ async def token(
95
97
  }
96
98
 
97
99
  token_response = await service.http.post(provider.token_url, data=token_data)
98
- _validate_response(response=token_response)
100
+ validate_auth_response(response=token_response, provider_type="OAuth 2.0")
99
101
 
100
102
  with trace.get_tracer(__name__).start_as_current_span("sso_token_request") as span:
101
103
  span.set_attribute("token_request_data", ujson.dumps(token_response.json()))
@@ -107,12 +109,17 @@ async def token(
107
109
  else:
108
110
  userinfo_response = await service.http.post(provider.userinfo_url, headers=headers)
109
111
 
110
- _validate_response(response=userinfo_response)
112
+ validate_auth_response(response=userinfo_response, provider_type="OAuth 2.0")
111
113
  user_info = userinfo_response.json()
112
114
  sso_groups = user_info.get("groups", []) or await get_groups_from_provider(
113
115
  provider=provider, service=service, payload=payload, user_info=user_info
114
116
  )
115
117
 
118
+ log.info(
119
+ "SSO user authenticated",
120
+ body={"user_name": user_info.get("name"), "groups": sso_groups},
121
+ )
122
+
116
123
  if not sso_groups and config.SETTINGS.security.sso_user_default_group:
117
124
  sso_groups = [config.SETTINGS.security.sso_user_default_group]
118
125
 
@@ -134,16 +141,3 @@ async def token(
134
141
  return models.UserTokenWithUrl(
135
142
  access_token=user_token.access_token, refresh_token=user_token.refresh_token, final_url=stored_final_url
136
143
  )
137
-
138
-
139
- def _validate_response(response: httpx.Response) -> None:
140
- if 200 <= response.status_code <= 299:
141
- return
142
-
143
- log.error(
144
- "Invalid response from the OAuth provider",
145
- url=response.url,
146
- status_code=response.status_code,
147
- body=response.json(),
148
- )
149
- raise GatewayError(message="Invalid response from Authentication provider")
infrahub/api/oidc.py CHANGED
@@ -13,14 +13,16 @@ from pydantic import BaseModel, HttpUrl
13
13
 
14
14
  from infrahub import config, models
15
15
  from infrahub.api.dependencies import get_db
16
- from infrahub.auth import get_groups_from_provider, signin_sso_account
17
- from infrahub.exceptions import GatewayError, ProcessingError
16
+ from infrahub.auth import (
17
+ get_groups_from_provider,
18
+ signin_sso_account,
19
+ validate_auth_response,
20
+ )
21
+ from infrahub.exceptions import ProcessingError
18
22
  from infrahub.log import get_logger
19
23
  from infrahub.message_bus.types import KVTTL
20
24
 
21
25
  if TYPE_CHECKING:
22
- import httpx
23
-
24
26
  from infrahub.database import InfrahubDatabase
25
27
  from infrahub.services import InfrahubServices
26
28
 
@@ -69,7 +71,7 @@ async def authorize(request: Request, provider_name: str, final_url: str | None
69
71
  service: InfrahubServices = request.app.state.service
70
72
 
71
73
  response = await service.http.get(url=provider.discovery_url)
72
- _validate_response(response=response)
74
+ validate_auth_response(response=response, provider_type="OIDC")
73
75
  oidc_config = OIDCDiscoveryConfig(**response.json())
74
76
 
75
77
  with trace.get_tracer(__name__).start_as_current_span("sso_oauth2_client_configuration") as span:
@@ -129,12 +131,12 @@ async def token(
129
131
  }
130
132
 
131
133
  discovery_response = await service.http.get(url=provider.discovery_url)
132
- _validate_response(response=discovery_response)
134
+ validate_auth_response(response=discovery_response, provider_type="OIDC")
133
135
 
134
136
  oidc_config = OIDCDiscoveryConfig(**discovery_response.json())
135
137
 
136
138
  token_response = await service.http.post(str(oidc_config.token_endpoint), data=token_data)
137
- _validate_response(response=token_response)
139
+ validate_auth_response(response=token_response, provider_type="OIDC")
138
140
 
139
141
  with trace.get_tracer(__name__).start_as_current_span("sso_token_request") as span:
140
142
  span.set_attribute("token_request_data", ujson.dumps(token_response.json()))
@@ -147,7 +149,7 @@ async def token(
147
149
  else:
148
150
  userinfo_response = await service.http.post(str(oidc_config.userinfo_endpoint), headers=headers)
149
151
 
150
- _validate_response(response=userinfo_response)
152
+ validate_auth_response(response=userinfo_response, provider_type="OIDC")
151
153
  user_info: dict[str, Any] = userinfo_response.json()
152
154
  sso_groups = (
153
155
  user_info.get("groups")
@@ -157,6 +159,11 @@ async def token(
157
159
  or await get_groups_from_provider(provider=provider, service=service, payload=payload, user_info=user_info)
158
160
  )
159
161
 
162
+ log.info(
163
+ "SSO user authenticated",
164
+ body={"user_name": user_info.get("name"), "groups": sso_groups},
165
+ )
166
+
160
167
  if not sso_groups and config.SETTINGS.security.sso_user_default_group:
161
168
  sso_groups = [config.SETTINGS.security.sso_user_default_group]
162
169
 
@@ -180,19 +187,6 @@ async def token(
180
187
  )
181
188
 
182
189
 
183
- def _validate_response(response: httpx.Response) -> None:
184
- if 200 <= response.status_code <= 299:
185
- return
186
-
187
- log.error(
188
- "Invalid response from the OIDC provider",
189
- url=response.url,
190
- status_code=response.status_code,
191
- body=response.json(),
192
- )
193
- raise GatewayError(message="Invalid response from Authentication provider")
194
-
195
-
196
190
  async def _get_id_token_groups(
197
191
  oidc_config: OIDCDiscoveryConfig, service: InfrahubServices, payload: dict[str, Any], client_id: str
198
192
  ) -> list[str]:
infrahub/api/schema.py CHANGED
@@ -26,7 +26,15 @@ from infrahub.core.models import ( # noqa: TC001
26
26
  SchemaDiff,
27
27
  SchemaUpdateValidationResult,
28
28
  )
29
- from infrahub.core.schema import GenericSchema, MainSchemaTypes, NodeSchema, ProfileSchema, SchemaRoot, TemplateSchema
29
+ from infrahub.core.schema import (
30
+ GenericSchema,
31
+ MainSchemaTypes,
32
+ NodeSchema,
33
+ ProfileSchema,
34
+ SchemaRoot,
35
+ SchemaWarning,
36
+ TemplateSchema,
37
+ )
30
38
  from infrahub.core.schema.constants import SchemaNamespace # noqa: TC001
31
39
  from infrahub.core.validators.models.validate_migration import (
32
40
  SchemaValidateMigrationData,
@@ -130,6 +138,9 @@ class SchemaUpdate(BaseModel):
130
138
  hash: str = Field(..., description="The new hash for the entire schema")
131
139
  previous_hash: str = Field(..., description="The previous hash for the entire schema")
132
140
  diff: SchemaDiff = Field(..., description="The modifications to the schema")
141
+ warnings: list[SchemaWarning] = Field(
142
+ default_factory=list, description="Warnings encountered while loading the schema"
143
+ )
133
144
 
134
145
  @computed_field
135
146
  def schema_updated(self) -> bool:
@@ -307,8 +318,10 @@ async def load_schema(
307
318
  log.info("schema_load_request", branch=branch.name)
308
319
 
309
320
  errors: list[str] = []
321
+ warnings: list[SchemaWarning] = []
310
322
  for schema in schemas.schemas:
311
323
  errors += schema.validate_namespaces()
324
+ warnings += schema.gather_warnings()
312
325
 
313
326
  if errors:
314
327
  raise SchemaNotValidError(message=", ".join(errors))
@@ -402,7 +415,7 @@ async def load_schema(
402
415
  )
403
416
  await service.event.send(event=event)
404
417
 
405
- return SchemaUpdate(hash=updated_hash, previous_hash=original_hash, diff=result.diff)
418
+ return SchemaUpdate(hash=updated_hash, previous_hash=original_hash, diff=result.diff, warnings=warnings)
406
419
 
407
420
 
408
421
  @router.post("/check")
@@ -417,8 +430,10 @@ async def check_schema(
417
430
  log.info("schema_check_request", branch=branch.name)
418
431
 
419
432
  errors: list[str] = []
433
+ warnings: list[SchemaWarning] = []
420
434
  for schema in schemas.schemas:
421
435
  errors += schema.validate_namespaces()
436
+ warnings += schema.gather_warnings()
422
437
 
423
438
  if errors:
424
439
  raise SchemaNotValidError(message=", ".join(errors))
@@ -445,4 +460,10 @@ async def check_schema(
445
460
  if error_messages:
446
461
  raise SchemaNotValidError(message=",\n".join(error_messages))
447
462
 
448
- return JSONResponse(status_code=202, content={"diff": result.diff.model_dump()})
463
+ return JSONResponse(
464
+ status_code=202,
465
+ content={
466
+ "diff": result.diff.model_dump(),
467
+ "warnings": [warning.model_dump(mode="json") for warning in warnings],
468
+ },
469
+ )
@@ -25,7 +25,8 @@ class CheckArtifactCreate(BaseModel):
25
25
  target_kind: str = Field(..., description="The kind of the target object for this artifact")
26
26
  target_name: str = Field(..., description="Name of the artifact target")
27
27
  artifact_id: str | None = Field(default=None, description="The id of the artifact if it previously existed")
28
- query: str = Field(..., description="The name of the query to use when collecting data")
28
+ query: str = Field(..., description="The name of the query to use when collecting data") # Deprecated
29
+ query_id: str = Field(..., description="The id of the query to use when collecting data")
29
30
  timeout: int = Field(..., description="Timeout for requests used to generate this artifact")
30
31
  variables: dict = Field(..., description="Input variables when generating the artifact")
31
32
  validator_id: str = Field(..., description="The ID of the validator")
infrahub/auth.py CHANGED
@@ -3,27 +3,37 @@ from __future__ import annotations
3
3
  import uuid
4
4
  from datetime import UTC, datetime, timedelta
5
5
  from enum import Enum
6
- from typing import TYPE_CHECKING
6
+ from typing import TYPE_CHECKING, Any
7
7
 
8
8
  import bcrypt
9
9
  import jwt
10
10
  from pydantic import BaseModel
11
11
 
12
12
  from infrahub import config, models
13
- from infrahub.config import SecurityOAuth2Google, SecurityOAuth2Settings, SecurityOIDCGoogle, SecurityOIDCSettings
13
+ from infrahub.config import (
14
+ SecurityOAuth2Google,
15
+ SecurityOAuth2Settings,
16
+ SecurityOIDCGoogle,
17
+ SecurityOIDCSettings,
18
+ )
14
19
  from infrahub.core.account import validate_token
15
20
  from infrahub.core.constants import AccountStatus, InfrahubKind
16
21
  from infrahub.core.manager import NodeManager
17
22
  from infrahub.core.node import Node
18
23
  from infrahub.core.protocols import CoreAccount, CoreAccountGroup
19
24
  from infrahub.core.registry import registry
20
- from infrahub.exceptions import AuthorizationError, NodeNotFoundError
25
+ from infrahub.exceptions import AuthorizationError, GatewayError, NodeNotFoundError
26
+ from infrahub.log import get_logger
21
27
 
22
28
  if TYPE_CHECKING:
29
+ import httpx
30
+
23
31
  from infrahub.core.protocols import CoreGenericAccount
24
32
  from infrahub.database import InfrahubDatabase
25
33
  from infrahub.services import InfrahubServices
26
34
 
35
+ log = get_logger()
36
+
27
37
 
28
38
  class AuthType(str, Enum):
29
39
  NONE = "none"
@@ -256,3 +266,127 @@ async def get_groups_from_provider(
256
266
  return [membership["groupKey"]["id"] for membership in group_memberships["memberships"]]
257
267
 
258
268
  return []
269
+
270
+
271
+ def safe_get_response_body(response: httpx.Response, raise_error_on_empty_body: bool = True) -> str | dict[str, Any]:
272
+ """Safely extract response body from HTTP response. If the response body cannot be JSON parsed or is empty,
273
+ it raises a GatewayError.
274
+
275
+ Args:
276
+ response: The HTTP response object
277
+ raise_error_on_empty_body: Whether to raise an error if the response body is empty
278
+
279
+ Returns:
280
+ The response body as JSON dict if possible, otherwise as text
281
+
282
+ Raises:
283
+ GatewayError: When the response body cannot be parsed or is empty
284
+ """
285
+ # Try to parse as JSON first
286
+ try:
287
+ return response.json()
288
+ except Exception as json_error:
289
+ try:
290
+ # Try to get as text
291
+ text_body = response.text
292
+ if not text_body.strip() and raise_error_on_empty_body: # Check for empty or whitespace-only response
293
+ log.error(
294
+ "Empty response body from authentication provider",
295
+ url=str(response.url),
296
+ status_code=response.status_code,
297
+ )
298
+ raise GatewayError(message="Authentication provider returned an empty response") from json_error
299
+ except Exception:
300
+ log.error(
301
+ "Unable to read response body from authentication provider",
302
+ url=str(response.url),
303
+ status_code=response.status_code,
304
+ )
305
+ raise GatewayError(message="Unable to read response from authentication provider") from json_error
306
+
307
+ # Here it means we got a text response but not JSON
308
+ return text_body
309
+
310
+
311
+ def extract_auth_error_message(response_body: str | dict[str, Any], base_message: str) -> str:
312
+ """Extract error message from OAuth 2.0/OIDC provider response following RFC 6749.
313
+
314
+ Args:
315
+ response_body: The response body from the authentication provider
316
+ base_message: Base error message to use if no specific error is found
317
+
318
+ Returns:
319
+ Formatted error message with provider details if available
320
+ """
321
+ if not isinstance(response_body, dict):
322
+ return base_message
323
+
324
+ # RFC 6749 standard error response format
325
+ error_description = response_body.get("error_description")
326
+ error_code = response_body.get("error")
327
+
328
+ if error_description:
329
+ return f"{base_message}: {error_description}"
330
+ if error_code:
331
+ return f"{base_message}: {error_code}"
332
+
333
+ return base_message
334
+
335
+
336
+ def validate_auth_response(response: httpx.Response, provider_type: str = "authentication") -> None:
337
+ """Validate HTTP response from OAuth 2.0/OIDC provider and raise appropriate errors.
338
+
339
+ Args:
340
+ response: The HTTP response from the authentication provider
341
+ provider_type: Type of provider for logging (e.g., "OAuth 2.0", "OIDC")
342
+
343
+ Raises:
344
+ GatewayError: When the response indicates an error or invalid state
345
+ """
346
+ # If the status code is successful, simply return
347
+ if 200 <= response.status_code <= 299:
348
+ # Verify that we can read the response body safely and it is not empty
349
+ safe_get_response_body(response)
350
+ return
351
+
352
+ # Prepare variables with default values for logging
353
+ response_body = safe_get_response_body(response, raise_error_on_empty_body=False)
354
+ log_message: str = f"Unexpected response from {provider_type} provider"
355
+ base_msg: str = "Unexpected response from authentication provider."
356
+
357
+ # Handle specific HTTP status codes with appropriate error messages
358
+ match response.status_code:
359
+ case 400:
360
+ log_message = f"Bad request to {provider_type} provider"
361
+ base_msg = "Bad request to authentication provider. Please try again later or contact your administrator."
362
+
363
+ case 401:
364
+ log_message = f"Unauthorized request to {provider_type} provider"
365
+ base_msg = (
366
+ "Unauthorized request to authentication provider. Please try again later or contact your administrator."
367
+ )
368
+
369
+ case 403:
370
+ log_message = f"Forbidden request to {provider_type} provider"
371
+ base_msg = (
372
+ "Access forbidden by authentication provider. Please try again later or contact your administrator."
373
+ )
374
+
375
+ case 404:
376
+ log_message = f"Resource not found for {provider_type} provider"
377
+ base_msg = (
378
+ "Authentication provider endpoint not found. Please try again later or contact your administrator."
379
+ )
380
+
381
+ case 429:
382
+ log_message = f"Rate limited by {provider_type} provider"
383
+ base_msg = "Rate limited by authentication provider. Please try again later."
384
+
385
+ case status_code if 500 <= status_code <= 599:
386
+ log_message = f"Server error from {provider_type} provider"
387
+ base_msg = "Authentication provider is experiencing server issues. Please try again later or contact your administrator."
388
+
389
+ # Print proper log and raise gateway error
390
+ log.error(log_message, url=str(response.url), status_code=response.status_code, body=response_body)
391
+ error_msg = extract_auth_error_message(response_body, base_msg)
392
+ raise GatewayError(message=error_msg)
infrahub/cli/__init__.py CHANGED
@@ -7,6 +7,7 @@ from infrahub.core.initialization import initialization
7
7
  from ..workers.dependencies import get_database
8
8
  from .context import CliContext
9
9
  from .db import app as db_app
10
+ from .dev import app as dev_app
10
11
  from .events import app as events_app
11
12
  from .git_agent import app as git_app
12
13
  from .server import app as server_app
@@ -27,6 +28,7 @@ app.add_typer(git_app, name="git-agent", hidden=True)
27
28
  app.add_typer(db_app, name="db")
28
29
  app.add_typer(events_app, name="events", help="Interact with the events system.", hidden=True)
29
30
  app.add_typer(tasks_app, name="tasks", hidden=True)
31
+ app.add_typer(dev_app, name="dev", help="Internal development commands.")
30
32
  app.command(name="upgrade")(upgrade_cmd)
31
33
 
32
34