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.
- infrahub/api/internal.py +2 -0
- infrahub/api/oauth2.py +13 -19
- infrahub/api/oidc.py +15 -21
- infrahub/api/schema.py +24 -3
- infrahub/artifacts/models.py +2 -1
- infrahub/auth.py +137 -3
- infrahub/cli/__init__.py +2 -0
- infrahub/cli/db.py +83 -102
- infrahub/cli/dev.py +118 -0
- infrahub/cli/tasks.py +46 -0
- infrahub/cli/upgrade.py +30 -3
- infrahub/computed_attribute/tasks.py +20 -8
- infrahub/core/attribute.py +10 -2
- infrahub/core/branch/enums.py +1 -1
- infrahub/core/branch/models.py +7 -3
- infrahub/core/branch/tasks.py +68 -7
- infrahub/core/constants/__init__.py +3 -0
- infrahub/core/diff/query/artifact.py +1 -0
- infrahub/core/diff/query/field_summary.py +1 -0
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/initialization.py +5 -2
- infrahub/core/migrations/__init__.py +3 -0
- infrahub/core/migrations/exceptions.py +4 -0
- infrahub/core/migrations/graph/__init__.py +10 -13
- infrahub/core/migrations/graph/load_schema_branch.py +21 -0
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +1 -1
- infrahub/core/migrations/graph/m040_duplicated_attributes.py +81 -0
- infrahub/core/migrations/graph/m041_profile_attrs_in_db.py +145 -0
- infrahub/core/migrations/graph/m042_create_hfid_display_label_in_db.py +164 -0
- infrahub/core/migrations/graph/m043_backfill_hfid_display_label_in_db.py +866 -0
- infrahub/core/migrations/query/__init__.py +7 -8
- infrahub/core/migrations/query/attribute_add.py +8 -6
- infrahub/core/migrations/query/attribute_remove.py +134 -0
- infrahub/core/migrations/runner.py +54 -0
- infrahub/core/migrations/schema/attribute_kind_update.py +9 -3
- infrahub/core/migrations/schema/attribute_supports_profile.py +90 -0
- infrahub/core/migrations/schema/node_attribute_add.py +30 -2
- infrahub/core/migrations/schema/node_attribute_remove.py +13 -109
- infrahub/core/migrations/schema/node_kind_update.py +2 -1
- infrahub/core/migrations/schema/node_remove.py +2 -1
- infrahub/core/migrations/schema/placeholder_dummy.py +3 -2
- infrahub/core/migrations/shared.py +48 -14
- infrahub/core/node/__init__.py +16 -11
- infrahub/core/node/create.py +46 -63
- infrahub/core/node/lock_utils.py +70 -44
- infrahub/core/node/resource_manager/ip_address_pool.py +2 -1
- infrahub/core/node/resource_manager/ip_prefix_pool.py +2 -1
- infrahub/core/node/resource_manager/number_pool.py +2 -1
- infrahub/core/query/attribute.py +55 -0
- infrahub/core/query/ipam.py +1 -0
- infrahub/core/query/node.py +9 -3
- infrahub/core/query/relationship.py +1 -0
- infrahub/core/schema/__init__.py +56 -0
- infrahub/core/schema/attribute_schema.py +4 -0
- infrahub/core/schema/definitions/internal.py +2 -2
- infrahub/core/schema/generated/attribute_schema.py +2 -2
- infrahub/core/schema/manager.py +22 -1
- infrahub/core/schema/schema_branch.py +180 -22
- infrahub/database/graph.py +21 -0
- infrahub/display_labels/tasks.py +13 -7
- infrahub/events/branch_action.py +27 -1
- infrahub/generators/tasks.py +3 -7
- infrahub/git/base.py +4 -1
- infrahub/git/integrator.py +1 -1
- infrahub/git/models.py +2 -1
- infrahub/git/repository.py +22 -5
- infrahub/git/tasks.py +66 -10
- infrahub/git/utils.py +123 -1
- infrahub/graphql/api/endpoints.py +14 -4
- infrahub/graphql/manager.py +4 -9
- infrahub/graphql/mutations/convert_object_type.py +11 -1
- infrahub/graphql/mutations/display_label.py +17 -10
- infrahub/graphql/mutations/hfid.py +17 -10
- infrahub/graphql/mutations/ipam.py +54 -35
- infrahub/graphql/mutations/main.py +27 -28
- infrahub/graphql/schema_sort.py +170 -0
- infrahub/graphql/types/branch.py +4 -1
- infrahub/graphql/types/enums.py +3 -0
- infrahub/hfid/tasks.py +13 -7
- infrahub/lock.py +52 -12
- infrahub/message_bus/types.py +2 -1
- infrahub/permissions/constants.py +2 -0
- infrahub/proposed_change/tasks.py +25 -16
- infrahub/server.py +6 -2
- infrahub/services/__init__.py +2 -2
- infrahub/services/adapters/http/__init__.py +5 -0
- infrahub/services/adapters/workflow/worker.py +14 -3
- infrahub/task_manager/event.py +5 -0
- infrahub/task_manager/models.py +7 -0
- infrahub/task_manager/task.py +73 -0
- infrahub/trigger/setup.py +13 -4
- infrahub/trigger/tasks.py +3 -0
- infrahub/workers/dependencies.py +10 -1
- infrahub/workers/infrahub_async.py +10 -2
- infrahub/workflows/catalogue.py +8 -0
- infrahub/workflows/initialization.py +5 -0
- infrahub/workflows/utils.py +2 -1
- infrahub_sdk/client.py +13 -10
- infrahub_sdk/config.py +29 -2
- infrahub_sdk/ctl/schema.py +22 -7
- infrahub_sdk/schema/__init__.py +32 -4
- infrahub_sdk/spec/models.py +7 -0
- infrahub_sdk/spec/object.py +37 -102
- infrahub_sdk/spec/processors/__init__.py +0 -0
- infrahub_sdk/spec/processors/data_processor.py +10 -0
- infrahub_sdk/spec/processors/factory.py +34 -0
- infrahub_sdk/spec/processors/range_expand_processor.py +56 -0
- {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.0b2.dist-info}/METADATA +3 -1
- {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.0b2.dist-info}/RECORD +115 -101
- infrahub_testcontainers/container.py +114 -2
- infrahub_testcontainers/docker-compose-cluster.test.yml +5 -0
- infrahub_testcontainers/docker-compose.test.yml +5 -0
- infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +0 -166
- infrahub/core/migrations/graph/m041_create_hfid_display_label_in_db.py +0 -97
- infrahub/core/migrations/graph/m042_backfill_hfid_display_label_in_db.py +0 -86
- {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.0b2.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.5.0b1.dist-info → infrahub_server-1.5.0b2.dist-info}/WHEEL +0 -0
- {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
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
+
)
|
infrahub/artifacts/models.py
CHANGED
|
@@ -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
|
|
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
|
|