infrahub-server 1.1.2__py3-none-any.whl → 1.1.3__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/__init__.py +13 -5
- infrahub/api/artifact.py +2 -1
- infrahub/api/auth.py +7 -1
- infrahub/api/diff/diff.py +13 -7
- infrahub/api/file.py +3 -3
- infrahub/api/internal.py +19 -6
- infrahub/api/oauth2.py +22 -7
- infrahub/api/oidc.py +23 -7
- infrahub/api/schema.py +24 -18
- infrahub/api/storage.py +8 -8
- infrahub/api/transformation.py +3 -2
- infrahub/auth.py +1 -24
- infrahub/cli/__init__.py +1 -1
- infrahub/cli/context.py +5 -8
- infrahub/cli/db.py +6 -6
- infrahub/cli/git_agent.py +1 -1
- infrahub/config.py +1 -1
- infrahub/core/attribute.py +22 -0
- infrahub/core/diff/calculator.py +14 -0
- infrahub/core/diff/combiner.py +5 -2
- infrahub/core/diff/conflicts_enricher.py +2 -2
- infrahub/core/diff/coordinator.py +11 -1
- infrahub/core/diff/enricher/cardinality_one.py +3 -3
- infrahub/core/diff/enricher/hierarchy.py +2 -1
- infrahub/core/diff/merger/merger.py +10 -0
- infrahub/core/diff/merger/serializer.py +5 -29
- infrahub/core/diff/model/path.py +3 -1
- infrahub/core/diff/query_parser.py +26 -11
- infrahub/core/diff/repository/repository.py +4 -4
- infrahub/core/ipam/utilization.py +6 -1
- infrahub/core/merge.py +5 -0
- infrahub/core/migrations/query/attribute_add.py +5 -5
- infrahub/core/query/diff.py +32 -19
- infrahub/core/query/ipam.py +30 -22
- infrahub/core/query/node.py +4 -0
- infrahub/core/validators/attribute/kind.py +1 -1
- infrahub/core/validators/models/violation.py +1 -14
- infrahub/core/validators/tasks.py +4 -1
- infrahub/dependencies/builder/constraint/schema/aggregated.py +2 -0
- infrahub/dependencies/builder/constraint/schema/attribute_kind.py +8 -0
- infrahub/graphql/api/endpoints.py +12 -3
- infrahub/graphql/mutations/account.py +4 -4
- infrahub/graphql/mutations/main.py +5 -16
- infrahub/graphql/mutations/resource_manager.py +3 -3
- infrahub/graphql/queries/resource_manager.py +21 -10
- infrahub/task_manager/task.py +5 -1
- {infrahub_server-1.1.2.dist-info → infrahub_server-1.1.3.dist-info}/METADATA +1 -1
- {infrahub_server-1.1.2.dist-info → infrahub_server-1.1.3.dist-info}/RECORD +51 -50
- {infrahub_server-1.1.2.dist-info → infrahub_server-1.1.3.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.1.2.dist-info → infrahub_server-1.1.3.dist-info}/WHEEL +0 -0
- {infrahub_server-1.1.2.dist-info → infrahub_server-1.1.3.dist-info}/entry_points.txt +0 -0
infrahub/api/__init__.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from typing import TYPE_CHECKING, NoReturn
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends
|
|
4
6
|
from fastapi.openapi.docs import (
|
|
5
7
|
get_redoc_html,
|
|
6
8
|
get_swagger_ui_html,
|
|
7
9
|
)
|
|
8
|
-
from starlette.responses import HTMLResponse
|
|
10
|
+
from starlette.responses import HTMLResponse # noqa: TC002
|
|
9
11
|
|
|
10
12
|
from infrahub.api import (
|
|
11
13
|
artifact,
|
|
@@ -21,8 +23,12 @@ from infrahub.api import (
|
|
|
21
23
|
storage,
|
|
22
24
|
transformation,
|
|
23
25
|
)
|
|
26
|
+
from infrahub.api.dependencies import get_current_user
|
|
24
27
|
from infrahub.exceptions import ResourceNotFoundError
|
|
25
28
|
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from infrahub.auth import AccountSession
|
|
31
|
+
|
|
26
32
|
router = APIRouter(prefix="/api")
|
|
27
33
|
|
|
28
34
|
router.include_router(artifact.router)
|
|
@@ -40,7 +46,9 @@ router.include_router(transformation.router)
|
|
|
40
46
|
|
|
41
47
|
|
|
42
48
|
@router.get("/docs", include_in_schema=False)
|
|
43
|
-
async def custom_swagger_ui_html(
|
|
49
|
+
async def custom_swagger_ui_html(
|
|
50
|
+
_: AccountSession = Depends(get_current_user),
|
|
51
|
+
) -> HTMLResponse:
|
|
44
52
|
return get_swagger_ui_html(
|
|
45
53
|
openapi_url="/api/openapi.json",
|
|
46
54
|
title="Infrahub - Swagger UI",
|
|
@@ -50,7 +58,7 @@ async def custom_swagger_ui_html() -> HTMLResponse:
|
|
|
50
58
|
|
|
51
59
|
|
|
52
60
|
@router.get("/redoc", include_in_schema=False)
|
|
53
|
-
async def redoc_html() -> HTMLResponse:
|
|
61
|
+
async def redoc_html(_: AccountSession = Depends(get_current_user)) -> HTMLResponse:
|
|
54
62
|
return get_redoc_html(
|
|
55
63
|
openapi_url="/api/openapi.json",
|
|
56
64
|
title="Infrahub - ReDoc",
|
infrahub/api/artifact.py
CHANGED
|
@@ -18,6 +18,7 @@ from infrahub.permissions.constants import PermissionDecisionFlag
|
|
|
18
18
|
from infrahub.workflows.catalogue import REQUEST_ARTIFACT_DEFINITION_GENERATE
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
|
+
from infrahub.auth import AccountSession
|
|
21
22
|
from infrahub.permissions import PermissionManager
|
|
22
23
|
|
|
23
24
|
log = get_logger()
|
|
@@ -37,7 +38,7 @@ async def get_artifact(
|
|
|
37
38
|
artifact_id: str,
|
|
38
39
|
db: InfrahubDatabase = Depends(get_db),
|
|
39
40
|
branch_params: BranchParams = Depends(get_branch_params),
|
|
40
|
-
_:
|
|
41
|
+
_: AccountSession = Depends(get_current_user),
|
|
41
42
|
) -> Response:
|
|
42
43
|
artifact = await registry.manager.get_one(db=db, id=artifact_id, branch=branch_params.branch, at=branch_params.at)
|
|
43
44
|
if not artifact:
|
infrahub/api/auth.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
1
5
|
from fastapi import APIRouter, Depends, Response
|
|
2
6
|
|
|
3
7
|
from infrahub import config, models
|
|
@@ -8,7 +12,9 @@ from infrahub.auth import (
|
|
|
8
12
|
create_fresh_access_token,
|
|
9
13
|
invalidate_refresh_token,
|
|
10
14
|
)
|
|
11
|
-
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from infrahub.database import InfrahubDatabase
|
|
12
18
|
|
|
13
19
|
router = APIRouter(prefix="/auth")
|
|
14
20
|
|
infrahub/api/diff/diff.py
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections import defaultdict
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
6
|
from fastapi import APIRouter, Depends, Request
|
|
7
7
|
|
|
8
8
|
from infrahub.api.dependencies import get_branch_dep, get_current_user, get_db
|
|
9
9
|
from infrahub.core import registry
|
|
10
|
-
from infrahub.core.branch import Branch # noqa: TC001
|
|
11
10
|
from infrahub.core.diff.artifacts.calculator import ArtifactDiffCalculator
|
|
12
11
|
from infrahub.core.diff.branch_differ import BranchDiffer
|
|
13
12
|
from infrahub.core.diff.model.diff import (
|
|
@@ -15,9 +14,11 @@ from infrahub.core.diff.model.diff import (
|
|
|
15
14
|
BranchDiffFile,
|
|
16
15
|
BranchDiffRepository,
|
|
17
16
|
)
|
|
18
|
-
from infrahub.database import InfrahubDatabase # noqa: TC001
|
|
19
17
|
|
|
20
18
|
if TYPE_CHECKING:
|
|
19
|
+
from infrahub.auth import AccountSession
|
|
20
|
+
from infrahub.core.branch import Branch
|
|
21
|
+
from infrahub.database import InfrahubDatabase
|
|
21
22
|
from infrahub.services import InfrahubServices
|
|
22
23
|
|
|
23
24
|
|
|
@@ -29,17 +30,22 @@ async def get_diff_files(
|
|
|
29
30
|
request: Request,
|
|
30
31
|
db: InfrahubDatabase = Depends(get_db),
|
|
31
32
|
branch: Branch = Depends(get_branch_dep),
|
|
32
|
-
time_from:
|
|
33
|
-
time_to:
|
|
33
|
+
time_from: str | None = None,
|
|
34
|
+
time_to: str | None = None,
|
|
34
35
|
branch_only: bool = True,
|
|
35
|
-
_:
|
|
36
|
+
_: AccountSession = Depends(get_current_user),
|
|
36
37
|
) -> dict[str, dict[str, BranchDiffRepository]]:
|
|
37
38
|
response: dict[str, dict[str, BranchDiffRepository]] = defaultdict(dict)
|
|
38
39
|
service: InfrahubServices = request.app.state.service
|
|
39
40
|
|
|
40
41
|
# Query the Diff for all files and repository from the database
|
|
41
42
|
diff = await BranchDiffer.init(
|
|
42
|
-
db=db,
|
|
43
|
+
db=db,
|
|
44
|
+
branch=branch,
|
|
45
|
+
diff_from=time_from,
|
|
46
|
+
diff_to=time_to,
|
|
47
|
+
branch_only=branch_only,
|
|
48
|
+
service=service,
|
|
43
49
|
)
|
|
44
50
|
diff_files = await diff.get_files()
|
|
45
51
|
|
infrahub/api/file.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from fastapi import APIRouter, Depends, Request
|
|
6
6
|
from starlette.responses import PlainTextResponse
|
|
@@ -27,13 +27,13 @@ async def get_file(
|
|
|
27
27
|
file_path: str,
|
|
28
28
|
branch_params: BranchParams = Depends(get_branch_params),
|
|
29
29
|
db: InfrahubDatabase = Depends(get_db),
|
|
30
|
-
commit:
|
|
30
|
+
commit: str | None = None,
|
|
31
31
|
_: str = Depends(get_current_user),
|
|
32
32
|
) -> PlainTextResponse:
|
|
33
33
|
"""Retrieve a file from a git repository."""
|
|
34
34
|
service: InfrahubServices = request.app.state.service
|
|
35
35
|
|
|
36
|
-
repo:
|
|
36
|
+
repo: CoreRepository | CoreReadOnlyRepository = await NodeManager.get_one_by_id_or_default_filter(
|
|
37
37
|
db=db,
|
|
38
38
|
id=repository_id,
|
|
39
39
|
kind=InfrahubKind.GENERICREPOSITORY,
|
infrahub/api/internal.py
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import re
|
|
2
|
-
from typing import
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
3
5
|
|
|
4
6
|
import ujson
|
|
5
|
-
from fastapi import APIRouter, Request
|
|
7
|
+
from fastapi import APIRouter, Depends, Request
|
|
6
8
|
from lunr.index import Index
|
|
7
9
|
from pydantic import BaseModel
|
|
8
10
|
|
|
9
11
|
from infrahub import config
|
|
10
|
-
from infrahub.
|
|
12
|
+
from infrahub.api.dependencies import get_current_user
|
|
13
|
+
from infrahub.config import ( # noqa: TC001
|
|
14
|
+
AnalyticsSettings,
|
|
15
|
+
ExperimentalFeaturesSettings,
|
|
16
|
+
LoggingSettings,
|
|
17
|
+
MainSettings,
|
|
18
|
+
)
|
|
11
19
|
from infrahub.core import registry
|
|
12
20
|
from infrahub.exceptions import NodeNotFoundError
|
|
13
21
|
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from infrahub.auth import AccountSession
|
|
24
|
+
|
|
14
25
|
router = APIRouter()
|
|
15
26
|
|
|
16
27
|
|
|
@@ -39,7 +50,7 @@ async def get_config() -> ConfigAPI:
|
|
|
39
50
|
|
|
40
51
|
|
|
41
52
|
@router.get("/info")
|
|
42
|
-
async def get_info(request: Request) -> InfoAPI:
|
|
53
|
+
async def get_info(request: Request, _: AccountSession = Depends(get_current_user)) -> InfoAPI:
|
|
43
54
|
return InfoAPI(deployment_id=str(registry.id), version=request.app.version)
|
|
44
55
|
|
|
45
56
|
|
|
@@ -47,7 +58,7 @@ class SearchDocs:
|
|
|
47
58
|
def __init__(self) -> None:
|
|
48
59
|
self._title_documents: list[dict] = []
|
|
49
60
|
self._heading_documents: list[dict] = []
|
|
50
|
-
self._heading_index:
|
|
61
|
+
self._heading_index: Index | None = None
|
|
51
62
|
|
|
52
63
|
def _load_json(self) -> None:
|
|
53
64
|
"""
|
|
@@ -142,7 +153,9 @@ class SearchResultAPI(BaseModel):
|
|
|
142
153
|
|
|
143
154
|
|
|
144
155
|
@router.get("/search/docs", include_in_schema=False)
|
|
145
|
-
async def search_docs(
|
|
156
|
+
async def search_docs(
|
|
157
|
+
query: str, limit: int | None = None, _: AccountSession = Depends(get_current_user)
|
|
158
|
+
) -> list[SearchResultAPI]:
|
|
146
159
|
smart_query = smart_queries(query)
|
|
147
160
|
search_results = search_docs_loader.heading_index.search(smart_query)
|
|
148
161
|
heading_results = [
|
infrahub/api/oauth2.py
CHANGED
|
@@ -3,9 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
from urllib.parse import urljoin
|
|
5
5
|
|
|
6
|
+
import ujson
|
|
6
7
|
from authlib.integrations.httpx_client import AsyncOAuth2Client
|
|
7
8
|
from fastapi import APIRouter, Depends, Request, Response
|
|
8
9
|
from fastapi.responses import JSONResponse, RedirectResponse
|
|
10
|
+
from opentelemetry import trace
|
|
9
11
|
|
|
10
12
|
from infrahub import config, models
|
|
11
13
|
from infrahub.api.dependencies import get_db
|
|
@@ -20,6 +22,8 @@ if TYPE_CHECKING:
|
|
|
20
22
|
from infrahub.database import InfrahubDatabase
|
|
21
23
|
from infrahub.services import InfrahubServices
|
|
22
24
|
|
|
25
|
+
# pylint: disable=R0801
|
|
26
|
+
|
|
23
27
|
log = get_logger()
|
|
24
28
|
router = APIRouter(prefix="/oauth2")
|
|
25
29
|
|
|
@@ -33,11 +37,16 @@ def _get_redirect_url(request: Request, provider_name: str) -> str:
|
|
|
33
37
|
@router.get("/{provider_name:str}/authorize")
|
|
34
38
|
async def authorize(request: Request, provider_name: str, final_url: str | None = None) -> Response:
|
|
35
39
|
provider = config.SETTINGS.security.get_oauth2_provider(provider=provider_name)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
|
|
41
|
+
with trace.get_tracer(__name__).start_as_current_span("sso_oauth2_client_configuration") as span:
|
|
42
|
+
span.set_attribute("provider_name", provider_name)
|
|
43
|
+
span.set_attribute("scopes", provider.scopes)
|
|
44
|
+
|
|
45
|
+
client = AsyncOAuth2Client(
|
|
46
|
+
client_id=provider.client_id,
|
|
47
|
+
client_secret=provider.client_secret,
|
|
48
|
+
scope=provider.scopes,
|
|
49
|
+
)
|
|
41
50
|
|
|
42
51
|
redirect_uri = _get_redirect_url(request=request, provider_name=provider_name)
|
|
43
52
|
final_url = final_url or config.SETTINGS.main.public_url or str(request.base_url)
|
|
@@ -88,7 +97,10 @@ async def token(
|
|
|
88
97
|
|
|
89
98
|
token_response = await service.http.post(provider.token_url, data=token_data)
|
|
90
99
|
_validate_response(response=token_response)
|
|
91
|
-
|
|
100
|
+
|
|
101
|
+
with trace.get_tracer(__name__).start_as_current_span("sso_token_request") as span:
|
|
102
|
+
span.set_attribute("token_request_data", ujson.dumps(token_response.json()))
|
|
103
|
+
payload = token_response.json()
|
|
92
104
|
|
|
93
105
|
headers = {"Authorization": f"{payload.get('token_type')} {payload.get('access_token')}"}
|
|
94
106
|
if provider.userinfo_method == config.UserInfoMethod.GET:
|
|
@@ -102,7 +114,10 @@ async def token(
|
|
|
102
114
|
if not sso_groups and config.SETTINGS.security.sso_user_default_group:
|
|
103
115
|
sso_groups = [config.SETTINGS.security.sso_user_default_group]
|
|
104
116
|
|
|
105
|
-
|
|
117
|
+
with trace.get_tracer(__name__).start_as_current_span("signin_sso_account") as span:
|
|
118
|
+
span.set_attribute("account_name", ujson.dumps(userinfo_response.json()))
|
|
119
|
+
span.set_attribute("sso_groups", sso_groups)
|
|
120
|
+
user_token = await signin_sso_account(db=db, account_name=user_info["name"], sso_groups=sso_groups)
|
|
106
121
|
|
|
107
122
|
response.set_cookie(
|
|
108
123
|
"access_token", user_token.access_token, httponly=True, max_age=config.SETTINGS.security.access_token_lifetime
|
infrahub/api/oidc.py
CHANGED
|
@@ -3,9 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
from urllib.parse import urljoin
|
|
5
5
|
|
|
6
|
+
import ujson
|
|
6
7
|
from authlib.integrations.httpx_client import AsyncOAuth2Client
|
|
7
8
|
from fastapi import APIRouter, Depends, Request, Response
|
|
8
9
|
from fastapi.responses import JSONResponse, RedirectResponse
|
|
10
|
+
from opentelemetry import trace
|
|
9
11
|
from pydantic import BaseModel, HttpUrl
|
|
10
12
|
|
|
11
13
|
from infrahub import config, models
|
|
@@ -21,6 +23,8 @@ if TYPE_CHECKING:
|
|
|
21
23
|
from infrahub.database import InfrahubDatabase
|
|
22
24
|
from infrahub.services import InfrahubServices
|
|
23
25
|
|
|
26
|
+
# pylint: disable=R0801
|
|
27
|
+
|
|
24
28
|
log = get_logger()
|
|
25
29
|
router = APIRouter(prefix="/oidc")
|
|
26
30
|
|
|
@@ -68,11 +72,16 @@ async def authorize(request: Request, provider_name: str, final_url: str | None
|
|
|
68
72
|
_validate_response(response=response)
|
|
69
73
|
oidc_config = OIDCDiscoveryConfig(**response.json())
|
|
70
74
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
with trace.get_tracer(__name__).start_as_current_span("sso_oauth2_client_configuration") as span:
|
|
76
|
+
span.set_attribute("provider_name", provider_name)
|
|
77
|
+
span.set_attribute("scopes", provider.scopes)
|
|
78
|
+
span.set_attribute("discovery_url", provider.discovery_url)
|
|
79
|
+
|
|
80
|
+
client = AsyncOAuth2Client(
|
|
81
|
+
client_id=provider.client_id,
|
|
82
|
+
client_secret=provider.client_secret,
|
|
83
|
+
scope=provider.scopes,
|
|
84
|
+
)
|
|
76
85
|
|
|
77
86
|
redirect_uri = _get_redirect_url(request=request, provider_name=provider_name)
|
|
78
87
|
final_url = final_url or config.SETTINGS.main.public_url or str(request.base_url)
|
|
@@ -126,7 +135,10 @@ async def token(
|
|
|
126
135
|
|
|
127
136
|
token_response = await service.http.post(str(oidc_config.token_endpoint), data=token_data)
|
|
128
137
|
_validate_response(response=token_response)
|
|
129
|
-
|
|
138
|
+
|
|
139
|
+
with trace.get_tracer(__name__).start_as_current_span("sso_token_request") as span:
|
|
140
|
+
span.set_attribute("token_request_data", ujson.dumps(token_response.json()))
|
|
141
|
+
payload = token_response.json()
|
|
130
142
|
|
|
131
143
|
headers = {"Authorization": f"{payload.get('token_type')} {payload.get('access_token')}"}
|
|
132
144
|
|
|
@@ -138,10 +150,14 @@ async def token(
|
|
|
138
150
|
_validate_response(response=userinfo_response)
|
|
139
151
|
user_info = userinfo_response.json()
|
|
140
152
|
sso_groups = user_info.get("groups", [])
|
|
153
|
+
|
|
141
154
|
if not sso_groups and config.SETTINGS.security.sso_user_default_group:
|
|
142
155
|
sso_groups = [config.SETTINGS.security.sso_user_default_group]
|
|
143
156
|
|
|
144
|
-
|
|
157
|
+
with trace.get_tracer(__name__).start_as_current_span("signin_sso_account") as span:
|
|
158
|
+
span.set_attribute("account_name", ujson.dumps(userinfo_response.json()))
|
|
159
|
+
span.set_attribute("sso_groups", sso_groups)
|
|
160
|
+
user_token = await signin_sso_account(db=db, account_name=user_info["name"], sso_groups=sso_groups)
|
|
145
161
|
|
|
146
162
|
response.set_cookie(
|
|
147
163
|
"access_token", user_token.access_token, httponly=True, max_age=config.SETTINGS.security.access_token_lifetime
|
infrahub/api/schema.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING, Any
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
5
|
from fastapi import APIRouter, Depends, Query, Request
|
|
6
6
|
from pydantic import (
|
|
@@ -72,17 +72,17 @@ class APISchemaMixin:
|
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
class APINodeSchema(NodeSchema, APISchemaMixin):
|
|
75
|
-
api_kind:
|
|
75
|
+
api_kind: str | None = Field(default=None, alias="kind", validate_default=True)
|
|
76
76
|
hash: str
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
class APIGenericSchema(GenericSchema, APISchemaMixin):
|
|
80
|
-
api_kind:
|
|
80
|
+
api_kind: str | None = Field(default=None, alias="kind", validate_default=True)
|
|
81
81
|
hash: str
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
class APIProfileSchema(ProfileSchema, APISchemaMixin):
|
|
85
|
-
api_kind:
|
|
85
|
+
api_kind: str | None = Field(default=None, alias="kind", validate_default=True)
|
|
86
86
|
hash: str
|
|
87
87
|
|
|
88
88
|
|
|
@@ -103,16 +103,16 @@ class SchemasLoadAPI(BaseModel):
|
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
class JSONSchema(BaseModel):
|
|
106
|
-
title:
|
|
107
|
-
description:
|
|
106
|
+
title: str | None = Field(None, description="Title of the schema")
|
|
107
|
+
description: str | None = Field(None, description="Description of the schema")
|
|
108
108
|
type: str = Field(..., description="Type of the schema element (e.g., 'object', 'array', 'string')")
|
|
109
|
-
properties:
|
|
110
|
-
items:
|
|
109
|
+
properties: dict[str, Any] | None = Field(None, description="Properties of the object if type is 'object'")
|
|
110
|
+
items: dict[str, Any] | list[dict[str, Any]] | None = Field(
|
|
111
111
|
None, description="Items of the array if type is 'array'"
|
|
112
112
|
)
|
|
113
|
-
required:
|
|
114
|
-
schema_spec:
|
|
115
|
-
additional_properties:
|
|
113
|
+
required: list[str] | None = Field(None, description="List of required properties if type is 'object'")
|
|
114
|
+
schema_spec: str | None = Field(None, alias="$schema", description="Schema version identifier")
|
|
115
|
+
additional_properties: bool | dict[str, Any] | None = Field(
|
|
116
116
|
None, description="Specifies whether additional properties are allowed", alias="additionalProperties"
|
|
117
117
|
)
|
|
118
118
|
|
|
@@ -152,7 +152,9 @@ def evaluate_candidate_schemas(
|
|
|
152
152
|
|
|
153
153
|
@router.get("")
|
|
154
154
|
async def get_schema(
|
|
155
|
-
branch: Branch = Depends(get_branch_dep),
|
|
155
|
+
branch: Branch = Depends(get_branch_dep),
|
|
156
|
+
namespaces: list[str] | None = Query(default=None),
|
|
157
|
+
_: AccountSession = Depends(get_current_user),
|
|
156
158
|
) -> SchemaReadAPI:
|
|
157
159
|
log.debug("schema_request", branch=branch.name)
|
|
158
160
|
schema_branch = registry.schema.get_schema_branch(name=branch.name)
|
|
@@ -180,7 +182,9 @@ async def get_schema(
|
|
|
180
182
|
|
|
181
183
|
|
|
182
184
|
@router.get("/summary")
|
|
183
|
-
async def get_schema_summary(
|
|
185
|
+
async def get_schema_summary(
|
|
186
|
+
branch: Branch = Depends(get_branch_dep), _: AccountSession = Depends(get_current_user)
|
|
187
|
+
) -> SchemaBranchHash:
|
|
184
188
|
log.debug("schema_summary_request", branch=branch.name)
|
|
185
189
|
schema_branch = registry.schema.get_schema_branch(name=branch.name)
|
|
186
190
|
return schema_branch.get_hash_full()
|
|
@@ -188,13 +192,13 @@ async def get_schema_summary(branch: Branch = Depends(get_branch_dep)) -> Schema
|
|
|
188
192
|
|
|
189
193
|
@router.get("/{schema_kind}")
|
|
190
194
|
async def get_schema_by_kind(
|
|
191
|
-
schema_kind: str, branch: Branch = Depends(get_branch_dep)
|
|
192
|
-
) ->
|
|
195
|
+
schema_kind: str, branch: Branch = Depends(get_branch_dep), _: AccountSession = Depends(get_current_user)
|
|
196
|
+
) -> APIProfileSchema | APINodeSchema | APIGenericSchema:
|
|
193
197
|
log.debug("schema_kind_request", branch=branch.name)
|
|
194
198
|
|
|
195
199
|
schema = registry.schema.get(name=schema_kind, branch=branch, duplicate=False)
|
|
196
200
|
|
|
197
|
-
api_schema: dict[str, type[
|
|
201
|
+
api_schema: dict[str, type[APIProfileSchema | APINodeSchema | APIGenericSchema]] = {
|
|
198
202
|
"profile": APIProfileSchema,
|
|
199
203
|
"node": APINodeSchema,
|
|
200
204
|
"generic": APIGenericSchema,
|
|
@@ -212,7 +216,9 @@ async def get_schema_by_kind(
|
|
|
212
216
|
|
|
213
217
|
|
|
214
218
|
@router.get("/json_schema/{schema_kind}")
|
|
215
|
-
async def get_json_schema_by_kind(
|
|
219
|
+
async def get_json_schema_by_kind(
|
|
220
|
+
schema_kind: str, branch: Branch = Depends(get_branch_dep), _: AccountSession = Depends(get_current_user)
|
|
221
|
+
) -> JSONSchema:
|
|
216
222
|
log.debug("json_schema_kind_request", branch=branch.name)
|
|
217
223
|
|
|
218
224
|
fields: dict[str, Any] = {}
|
|
@@ -368,7 +374,7 @@ async def check_schema(
|
|
|
368
374
|
request: Request,
|
|
369
375
|
schemas: SchemasLoadAPI,
|
|
370
376
|
branch: Branch = Depends(get_branch_dep),
|
|
371
|
-
_:
|
|
377
|
+
_: AccountSession = Depends(get_current_user),
|
|
372
378
|
) -> JSONResponse:
|
|
373
379
|
service: InfrahubServices = request.app.state.service
|
|
374
380
|
log.info("schema_check_request", branch=branch.name)
|
infrahub/api/storage.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import hashlib
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
2
5
|
|
|
3
6
|
from fastapi import APIRouter, Depends, File, Response, UploadFile
|
|
4
7
|
from infrahub_sdk.uuidt import UUIDT
|
|
@@ -8,6 +11,9 @@ from infrahub.api.dependencies import get_current_user
|
|
|
8
11
|
from infrahub.core import registry
|
|
9
12
|
from infrahub.log import get_logger
|
|
10
13
|
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from infrahub.auth import AccountSession
|
|
16
|
+
|
|
11
17
|
log = get_logger()
|
|
12
18
|
router = APIRouter(prefix="/storage")
|
|
13
19
|
|
|
@@ -22,10 +28,7 @@ class UploadContentPayload(BaseModel):
|
|
|
22
28
|
|
|
23
29
|
|
|
24
30
|
@router.get("/object/{identifier:str}")
|
|
25
|
-
def get_file(
|
|
26
|
-
identifier: str,
|
|
27
|
-
_: str = Depends(get_current_user),
|
|
28
|
-
) -> Response:
|
|
31
|
+
def get_file(identifier: str, _: AccountSession = Depends(get_current_user)) -> Response:
|
|
29
32
|
content = registry.storage.retrieve(identifier=identifier)
|
|
30
33
|
return Response(content=content)
|
|
31
34
|
|
|
@@ -48,10 +51,7 @@ def upload_content(
|
|
|
48
51
|
|
|
49
52
|
|
|
50
53
|
@router.post("/upload/file")
|
|
51
|
-
def upload_file(
|
|
52
|
-
file: UploadFile = File(...),
|
|
53
|
-
_: str = Depends(get_current_user),
|
|
54
|
-
) -> UploadResponse:
|
|
54
|
+
def upload_file(file: UploadFile = File(...), _: AccountSession = Depends(get_current_user)) -> UploadResponse:
|
|
55
55
|
# TODO need to optimized how we read the content of the file, especially if the file is really large
|
|
56
56
|
# Check this discussion for more details
|
|
57
57
|
# https://stackoverflow.com/questions/63048825/how-to-upload-file-using-fastapi
|
infrahub/api/transformation.py
CHANGED
|
@@ -27,6 +27,7 @@ from infrahub.transformations.models import TransformJinjaTemplateData, Transfor
|
|
|
27
27
|
from infrahub.workflows.catalogue import TRANSFORM_JINJA2_RENDER, TRANSFORM_PYTHON_RENDER
|
|
28
28
|
|
|
29
29
|
if TYPE_CHECKING:
|
|
30
|
+
from infrahub.auth import AccountSession
|
|
30
31
|
from infrahub.services import InfrahubServices
|
|
31
32
|
|
|
32
33
|
router = APIRouter()
|
|
@@ -38,7 +39,7 @@ async def transform_python(
|
|
|
38
39
|
transform_id: str,
|
|
39
40
|
db: InfrahubDatabase = Depends(get_db),
|
|
40
41
|
branch_params: BranchParams = Depends(get_branch_params),
|
|
41
|
-
_:
|
|
42
|
+
_: AccountSession = Depends(get_current_user),
|
|
42
43
|
) -> JSONResponse:
|
|
43
44
|
params = {key: value for key, value in request.query_params.items() if key not in ["branch", "at"]}
|
|
44
45
|
|
|
@@ -97,7 +98,7 @@ async def transform_jinja2(
|
|
|
97
98
|
transform_id: str = Path(description="ID or Name of the Jinja2 Transform to render"),
|
|
98
99
|
db: InfrahubDatabase = Depends(get_db),
|
|
99
100
|
branch_params: BranchParams = Depends(get_branch_params),
|
|
100
|
-
_:
|
|
101
|
+
_: AccountSession = Depends(get_current_user),
|
|
101
102
|
) -> PlainTextResponse:
|
|
102
103
|
params = {key: value for key, value in request.query_params.items() if key not in ["branch", "at"]}
|
|
103
104
|
|
infrahub/auth.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import uuid
|
|
4
4
|
from datetime import datetime, timedelta, timezone
|
|
5
5
|
from enum import Enum
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
8
|
import bcrypt
|
|
9
9
|
import jwt
|
|
@@ -233,29 +233,6 @@ async def validate_api_key(db: InfrahubDatabase, token: str) -> AccountSession:
|
|
|
233
233
|
return AccountSession(account_id=account_id, auth_type=AuthType.API)
|
|
234
234
|
|
|
235
235
|
|
|
236
|
-
def _validate_update_account(account_session: AccountSession, node_id: str, fields: list[str]) -> None:
|
|
237
|
-
if account_session.account_id != node_id:
|
|
238
|
-
# A regular account is not allowed to modify another account
|
|
239
|
-
raise PermissionError("You are not allowed to modify this account")
|
|
240
|
-
|
|
241
|
-
allowed_fields = ["description", "label", "password"]
|
|
242
|
-
for field in fields:
|
|
243
|
-
if field not in allowed_fields:
|
|
244
|
-
raise PermissionError(f"You are not allowed to modify '{field}'")
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
def validate_mutation_permissions_update_node(
|
|
248
|
-
operation: str, node_id: str, account_session: AccountSession, fields: list[str]
|
|
249
|
-
) -> None:
|
|
250
|
-
validation_map: dict[str, Callable[[AccountSession, str, list[str]], None]] = {
|
|
251
|
-
f"{InfrahubKind.ACCOUNT}Update": _validate_update_account,
|
|
252
|
-
f"{InfrahubKind.ACCOUNT}Upsert": _validate_update_account,
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if validator := validation_map.get(operation):
|
|
256
|
-
validator(account_session, node_id, fields)
|
|
257
|
-
|
|
258
|
-
|
|
259
236
|
async def invalidate_refresh_token(db: InfrahubDatabase, token_id: str) -> None:
|
|
260
237
|
refresh_token = await NodeManager.get_one(id=token_id, db=db)
|
|
261
238
|
if refresh_token:
|
infrahub/cli/__init__.py
CHANGED
|
@@ -20,7 +20,7 @@ app = typer.Typer(name="Infrahub CLI", pretty_exceptions_enable=False)
|
|
|
20
20
|
@app.callback()
|
|
21
21
|
def common(ctx: typer.Context) -> None:
|
|
22
22
|
"""Infrahub CLI"""
|
|
23
|
-
ctx.obj = CliContext(
|
|
23
|
+
ctx.obj = CliContext()
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
app.add_typer(server_app, name="server")
|
infrahub/cli/context.py
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
5
4
|
|
|
6
|
-
from infrahub.database import get_db
|
|
7
|
-
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
from infrahub.database import InfrahubDatabase
|
|
5
|
+
from infrahub.database import InfrahubDatabase, get_db
|
|
10
6
|
|
|
11
7
|
|
|
12
8
|
@dataclass
|
|
13
9
|
class CliContext:
|
|
14
|
-
database_class: type[InfrahubDatabase]
|
|
15
10
|
application: str = "infrahub.server:app"
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
# This method is inherited for Infrahub Enterprise.
|
|
13
|
+
@staticmethod
|
|
14
|
+
async def init_db(retry: int) -> InfrahubDatabase:
|
|
15
|
+
return InfrahubDatabase(driver=await get_db(retry=retry))
|
infrahub/cli/db.py
CHANGED
|
@@ -97,7 +97,7 @@ async def init(
|
|
|
97
97
|
config.load_and_exit(config_file_name=config_file)
|
|
98
98
|
|
|
99
99
|
context: CliContext = ctx.obj
|
|
100
|
-
dbdriver = await context.
|
|
100
|
+
dbdriver = await context.init_db(retry=1)
|
|
101
101
|
async with dbdriver.start_transaction() as db:
|
|
102
102
|
log.info("Delete All Nodes")
|
|
103
103
|
await delete_all_nodes(db=db)
|
|
@@ -120,7 +120,7 @@ async def load_test_data(
|
|
|
120
120
|
config.load_and_exit(config_file_name=config_file)
|
|
121
121
|
|
|
122
122
|
context: CliContext = ctx.obj
|
|
123
|
-
dbdriver = await context.
|
|
123
|
+
dbdriver = await context.init_db(retry=1)
|
|
124
124
|
async with dbdriver.start_session() as db:
|
|
125
125
|
await initialization(db=db)
|
|
126
126
|
|
|
@@ -152,7 +152,7 @@ async def migrate(
|
|
|
152
152
|
config.load_and_exit(config_file_name=config_file)
|
|
153
153
|
|
|
154
154
|
context: CliContext = ctx.obj
|
|
155
|
-
dbdriver = await context.
|
|
155
|
+
dbdriver = await context.init_db(retry=1)
|
|
156
156
|
async with dbdriver.start_session() as db:
|
|
157
157
|
rprint("Checking current state of the Database")
|
|
158
158
|
|
|
@@ -207,7 +207,7 @@ async def update_core_schema( # pylint: disable=too-many-statements
|
|
|
207
207
|
config.load_and_exit(config_file_name=config_file)
|
|
208
208
|
|
|
209
209
|
context: CliContext = ctx.obj
|
|
210
|
-
dbdriver = await context.
|
|
210
|
+
dbdriver = await context.init_db(retry=1)
|
|
211
211
|
|
|
212
212
|
error_badge = "[bold red]ERROR[/bold red]"
|
|
213
213
|
|
|
@@ -332,7 +332,7 @@ async def constraint(
|
|
|
332
332
|
config.load_and_exit(config_file_name=config_file)
|
|
333
333
|
|
|
334
334
|
context: CliContext = ctx.obj
|
|
335
|
-
dbdriver = await context.
|
|
335
|
+
dbdriver = await context.init_db(retry=1)
|
|
336
336
|
|
|
337
337
|
manager: Optional[ConstraintManagerBase] = None
|
|
338
338
|
if dbdriver.db_type == DatabaseType.NEO4J:
|
|
@@ -376,7 +376,7 @@ async def index(
|
|
|
376
376
|
config.load_and_exit(config_file_name=config_file)
|
|
377
377
|
|
|
378
378
|
context: CliContext = ctx.obj
|
|
379
|
-
dbdriver = await context.
|
|
379
|
+
dbdriver = await context.init_db(retry=1)
|
|
380
380
|
dbdriver.manager.index.init(nodes=node_indexes, rels=rel_indexes)
|
|
381
381
|
|
|
382
382
|
if action == IndexAction.ADD:
|