ocmo-api 0.8.19__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.
- LICENSE +201 -0
- NOTICE +16 -0
- core/__init__.py +0 -0
- core/api/__init__.py +201 -0
- core/api/_common.py +37 -0
- core/api/audit.py +244 -0
- core/api/auth.py +50 -0
- core/api/config.py +96 -0
- core/api/errors.py +65 -0
- core/api/global_permissions.py +107 -0
- core/api/lock.py +99 -0
- core/api/namespace.py +86 -0
- core/api/propagation.py +29 -0
- core/api/resolve.py +229 -0
- core/api/resolver.py +96 -0
- core/api/secret.py +53 -0
- core/api/system.py +42 -0
- core/api/template.py +53 -0
- core/api/tree.py +274 -0
- core/apps.py +18 -0
- core/audit_operations.py +0 -0
- core/constants/__init__.py +5 -0
- core/constants/audit_operations.py +35 -0
- core/constants/permission_actions.py +59 -0
- core/constants/permissions.py +41 -0
- core/constants/resolve.py +3 -0
- core/constants/transformers.py +22 -0
- core/constants/webhook_events.py +31 -0
- core/data/builtin_schemas/_git_sync.schema.yaml +24 -0
- core/data/builtin_schemas/_permissions.schema.yaml +211 -0
- core/data/builtin_schemas/_webhooks.schema.yaml +145 -0
- core/decorators/__init__.py +14 -0
- core/decorators/audit.py +105 -0
- core/decorators/binding.py +49 -0
- core/decorators/permissions.py +129 -0
- core/decorators/webhook.py +87 -0
- core/exceptions.py +165 -0
- core/management/__init__.py +0 -0
- core/management/commands/__init__.py +0 -0
- core/management/commands/dump_openapi.py +40 -0
- core/managers/artifacts.py +346 -0
- core/managers/audit/__init__.py +1172 -0
- core/managers/audit/timeline.py +365 -0
- core/managers/auth.py +276 -0
- core/managers/cast.py +506 -0
- core/managers/config_validation.py +216 -0
- core/managers/crypto.py +78 -0
- core/managers/global_permissions.py +183 -0
- core/managers/health.py +56 -0
- core/managers/lock.py +229 -0
- core/managers/namespace.py +132 -0
- core/managers/permissions.py +168 -0
- core/managers/propagation.py +244 -0
- core/managers/resolution.py +938 -0
- core/managers/resolve_cache.py +266 -0
- core/managers/resolve_parameters.py +503 -0
- core/managers/resolver.py +56 -0
- core/managers/resolver_tokens.py +114 -0
- core/managers/resolving.py +673 -0
- core/managers/secret.py +147 -0
- core/managers/system.py +44 -0
- core/managers/tree/__init__.py +85 -0
- core/managers/tree/_common.py +85 -0
- core/managers/tree/base.py +203 -0
- core/managers/tree/config_ops.py +167 -0
- core/managers/tree/constants.py +11 -0
- core/managers/tree/create.py +75 -0
- core/managers/tree/diff.py +270 -0
- core/managers/tree/mutate.py +433 -0
- core/managers/tree/paths.py +49 -0
- core/managers/tree/propagation.py +200 -0
- core/managers/tree/read.py +263 -0
- core/managers/tree/resolver_ops.py +21 -0
- core/managers/tree/tags.py +167 -0
- core/managers/tree/update.py +137 -0
- core/managers/tree/versions.py +87 -0
- core/managers/tree_capabilities.py +189 -0
- core/managers/webhook/__init__.py +3 -0
- core/managers/webhook/cache.py +68 -0
- core/managers/webhook/main.py +357 -0
- core/middleware.py +32 -0
- core/migrations/0001_initial.py +157 -0
- core/migrations/0002_folder.py +33 -0
- core/migrations/0003_alter_namespace_name.py +17 -0
- core/migrations/0004_alter_namespace_name_and_more.py +25 -0
- core/migrations/0005_configversion_templateversion.py +85 -0
- core/migrations/0006_alter_templateversion_options_and_more.py +47 -0
- core/migrations/0007_configversion_deleted_at_templateversion_deleted_at.py +22 -0
- core/migrations/0008_namespace_tags_dek_secret_global_permissions.py +139 -0
- core/migrations/0009_tree_lock.py +58 -0
- core/migrations/0010_resolver_token_encryption.py +75 -0
- core/migrations/0011_audit_event.py +134 -0
- core/migrations/0012_audit_event_namespace_name.py +24 -0
- core/migrations/0013_audit_event_object_id.py +28 -0
- core/migrations/0014_sync_permissions_webhooks_builtin_schemas.py +26 -0
- core/migrations/0015_audit_event_operation_subresource.py +27 -0
- core/migrations/0016_sync_permissions_schema_audit_actions.py +26 -0
- core/migrations/0017_sync_permissions_schema_valid_actions.py +26 -0
- core/migrations/__init__.py +0 -0
- core/models/__init__.py +9 -0
- core/models/audit.py +97 -0
- core/models/config.py +132 -0
- core/models/folder.py +12 -0
- core/models/global_permissions.py +27 -0
- core/models/lock.py +35 -0
- core/models/namespace.py +45 -0
- core/models/resolver.py +25 -0
- core/models/secret.py +58 -0
- core/models/tree.py +29 -0
- core/notice.py +18 -0
- core/operation_ids.py +153 -0
- core/schemas/__init__.py +2 -0
- core/schemas/audit.py +75 -0
- core/schemas/cast_json_schema.py +57 -0
- core/schemas/cast_options.py +508 -0
- core/schemas/generic.py +233 -0
- core/schemas/metadata_json_schema.py +21 -0
- core/schemas/propagation.py +88 -0
- core/schemas/requests.py +773 -0
- core/schemas/resolver_json_schema.py +65 -0
- core/schemas/responses.py +690 -0
- core/schemas/webhooks.py +40 -0
- core/shortcuts.py +415 -0
- core/tests/__init__.py +0 -0
- core/tests/auth_helpers.py +23 -0
- core/tests/namespace_helpers.py +29 -0
- core/tests/test_api_cli.py +43 -0
- core/tests/test_api_errors.py +151 -0
- core/tests/test_artifacts.py +123 -0
- core/tests/test_audit.py +973 -0
- core/tests/test_audit_event_id.py +90 -0
- core/tests/test_audit_operations.py +222 -0
- core/tests/test_builtin_config_schemas.py +174 -0
- core/tests/test_can_i.py +510 -0
- core/tests/test_cast_format_options.py +57 -0
- core/tests/test_cast_options_schema.py +54 -0
- core/tests/test_cast_yaml.py +26 -0
- core/tests/test_config_schema_api.py +173 -0
- core/tests/test_config_validation.py +378 -0
- core/tests/test_document_parser.py +122 -0
- core/tests/test_dump_openapi.py +46 -0
- core/tests/test_extend_key.py +267 -0
- core/tests/test_folder_get_item.py +23 -0
- core/tests/test_folder_resolve_missing_tags.py +73 -0
- core/tests/test_get_item_version.py +31 -0
- core/tests/test_global_permission_create.py +171 -0
- core/tests/test_list_pagination.py +262 -0
- core/tests/test_locks.py +209 -0
- core/tests/test_multiline_transformer.py +132 -0
- core/tests/test_namespace_create.py +11 -0
- core/tests/test_ocmo_metadata.py +127 -0
- core/tests/test_oidc_jwt_audiences.py +64 -0
- core/tests/test_omit_transformer.py +78 -0
- core/tests/test_operation_ids.py +47 -0
- core/tests/test_permissions.py +1148 -0
- core/tests/test_propagation.py +441 -0
- core/tests/test_public_base_url.py +20 -0
- core/tests/test_resolve_draft.py +176 -0
- core/tests/test_resolve_parameters_version.py +181 -0
- core/tests/test_resolve_permissions.py +492 -0
- core/tests/test_resolve_resolver_response.py +107 -0
- core/tests/test_resolver_auth.py +33 -0
- core/tests/test_resolver_folder_filters.py +95 -0
- core/tests/test_resolver_get_item.py +23 -0
- core/tests/test_resolver_navigate_search_scope.py +98 -0
- core/tests/test_resolver_schema_api.py +91 -0
- core/tests/test_security_hardening.py +324 -0
- core/tests/test_selectors.py +65 -0
- core/tests/test_special_namespace_paths.py +409 -0
- core/tests/test_stable_promotion.py +124 -0
- core/tests/test_system.py +140 -0
- core/tests/test_tree_manager_for_item.py +192 -0
- core/tests/test_two_layer_cache.py +334 -0
- core/tests/test_url_trailing_slashes.py +53 -0
- core/tests/test_validation_error_responses.py +43 -0
- core/tests/test_webhooks.py +985 -0
- core/utils/__init__.py +23 -0
- core/utils/deep_merge.py +91 -0
- core/utils/namespace_special_configs.py +105 -0
- core/utils/permissions_compiler.py +627 -0
- core/utils/permissions_schema_document.py +52 -0
- core/validation_errors.py +105 -0
- core/version.py +27 -0
- core/views.py +1 -0
- ocmo_api-0.8.19.dist-info/METADATA +252 -0
- ocmo_api-0.8.19.dist-info/RECORD +203 -0
- ocmo_api-0.8.19.dist-info/WHEEL +4 -0
- ocmo_api-0.8.19.dist-info/entry_points.txt +2 -0
- ocmo_api-0.8.19.dist-info/licenses/LICENSE +201 -0
- ocmo_api-0.8.19.dist-info/licenses/NOTICE +16 -0
- ocmoapi/__init__.py +1 -0
- ocmoapi/__main__.py +6 -0
- ocmoapi/asgi.py +16 -0
- ocmoapi/auth.py +163 -0
- ocmoapi/cli.py +85 -0
- ocmoapi/oauth2_provider.py +23 -0
- ocmoapi/parser.py +120 -0
- ocmoapi/settings.py +275 -0
- ocmoapi/templates/ninja/oauth2-redirect.html +81 -0
- ocmoapi/templates/ninja/swagger.html +55 -0
- ocmoapi/testing_auth.py +64 -0
- ocmoapi/urls.py +40 -0
- ocmoapi/wsgi.py +16 -0
core/api/namespace.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from ninja import Query, Router
|
|
2
|
+
from ninja.pagination import paginate
|
|
3
|
+
|
|
4
|
+
from ..managers.audit import AuditManager
|
|
5
|
+
from ..managers.auth import AuthManager
|
|
6
|
+
from ..managers.namespace import NamespaceManager
|
|
7
|
+
from ..operation_ids import (
|
|
8
|
+
CREATE_NAMESPACE,
|
|
9
|
+
DELETE_NAMESPACE,
|
|
10
|
+
LIST_NAMESPACES,
|
|
11
|
+
SHOW_NAMESPACE,
|
|
12
|
+
UPDATE_NAMESPACE,
|
|
13
|
+
)
|
|
14
|
+
from ..schemas import (
|
|
15
|
+
ErrorSchema,
|
|
16
|
+
NamespaceCreateSchema,
|
|
17
|
+
NamespaceDeletedSchema,
|
|
18
|
+
NamespacePatchSchema,
|
|
19
|
+
NamespaceSchema,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
router = Router()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get("/ns/", response=list[NamespaceSchema], tags=["Namespace"], operation_id=LIST_NAMESPACES)
|
|
26
|
+
@paginate
|
|
27
|
+
def list_namespaces(request, name_filter: str = Query(None)):
|
|
28
|
+
"""List existing namespaces with optional name filter."""
|
|
29
|
+
auth = AuthManager.from_request(request)
|
|
30
|
+
AuditManager.bind(request, auth)
|
|
31
|
+
return NamespaceManager(None, auth=auth).list(name_filter)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@router.get(
|
|
35
|
+
"/ns/{namespace}",
|
|
36
|
+
response={200: NamespaceSchema, 404: ErrorSchema},
|
|
37
|
+
tags=["Namespace"],
|
|
38
|
+
operation_id=SHOW_NAMESPACE,
|
|
39
|
+
)
|
|
40
|
+
def show_namespace(request, namespace: str):
|
|
41
|
+
"""Get specific namespace details."""
|
|
42
|
+
auth = AuthManager.from_request(request)
|
|
43
|
+
mgr = NamespaceManager(namespace, auth=auth)
|
|
44
|
+
AuditManager.bind(request, auth, namespace=mgr.ns)
|
|
45
|
+
return mgr.get_or_raise()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@router.post(
|
|
49
|
+
"/ns/",
|
|
50
|
+
response={201: NamespaceSchema, 409: ErrorSchema},
|
|
51
|
+
tags=["Namespace"],
|
|
52
|
+
operation_id=CREATE_NAMESPACE,
|
|
53
|
+
)
|
|
54
|
+
def create_namespace(request, payload: NamespaceCreateSchema):
|
|
55
|
+
"""Create a new namespace."""
|
|
56
|
+
auth = AuthManager.from_request(request)
|
|
57
|
+
AuditManager.bind(request, auth)
|
|
58
|
+
return 201, NamespaceManager(payload.name, auth=auth).create(payload)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@router.patch(
|
|
62
|
+
"/ns/{namespace}",
|
|
63
|
+
response={200: NamespaceSchema, 404: ErrorSchema, 422: ErrorSchema},
|
|
64
|
+
tags=["Namespace"],
|
|
65
|
+
operation_id=UPDATE_NAMESPACE,
|
|
66
|
+
)
|
|
67
|
+
def update_namespace(request, namespace: str, payload: NamespacePatchSchema):
|
|
68
|
+
"""Update namespace metadata or active tag pointers."""
|
|
69
|
+
auth = AuthManager.from_request(request)
|
|
70
|
+
ns = NamespaceManager(namespace, auth=auth).get_or_raise()
|
|
71
|
+
AuditManager.bind(request, auth, namespace=ns)
|
|
72
|
+
return 200, NamespaceManager(namespace, auth=auth).update(payload)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@router.delete(
|
|
76
|
+
"/ns/{namespace}",
|
|
77
|
+
response={204: NamespaceDeletedSchema, 404: ErrorSchema},
|
|
78
|
+
tags=["Namespace"],
|
|
79
|
+
operation_id=DELETE_NAMESPACE,
|
|
80
|
+
)
|
|
81
|
+
def delete_namespace(request, namespace: str):
|
|
82
|
+
"""Delete namespace and all its contents."""
|
|
83
|
+
auth = AuthManager.from_request(request)
|
|
84
|
+
ns = NamespaceManager(namespace, auth=auth).get_or_raise()
|
|
85
|
+
AuditManager.bind(request, auth, namespace=ns)
|
|
86
|
+
return 204, NamespaceManager(namespace, auth=auth).delete()
|
core/api/propagation.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from ninja import Router
|
|
2
|
+
|
|
3
|
+
from ..managers.audit import AuditManager
|
|
4
|
+
from ..managers.auth import AuthManager
|
|
5
|
+
from ..managers.namespace import NamespaceManager
|
|
6
|
+
from ..managers.propagation import PropagationManager
|
|
7
|
+
from ..operation_ids import PROPAGATE_CONFIG
|
|
8
|
+
from ..schemas import ErrorSchema, PropagationResultSchema
|
|
9
|
+
|
|
10
|
+
router = Router()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.post(
|
|
14
|
+
"/ns/{namespace}/~propagate/{path:path}",
|
|
15
|
+
response={200: PropagationResultSchema, 404: ErrorSchema},
|
|
16
|
+
tags=["Propagation"],
|
|
17
|
+
operation_id=PROPAGATE_CONFIG,
|
|
18
|
+
)
|
|
19
|
+
def propagate_config(
|
|
20
|
+
request,
|
|
21
|
+
namespace: str,
|
|
22
|
+
path: str,
|
|
23
|
+
version: str = "latest",
|
|
24
|
+
):
|
|
25
|
+
"""Manually trigger propagation from the config at ``path``."""
|
|
26
|
+
auth = AuthManager.from_request(request)
|
|
27
|
+
ns = NamespaceManager(namespace, auth=auth).get_or_raise()
|
|
28
|
+
AuditManager.bind(request, auth, namespace=ns)
|
|
29
|
+
return PropagationManager(ns, path, auth=auth).propagate_manual(version)
|
core/api/resolve.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from django.http import HttpResponse
|
|
2
|
+
from ninja import Body, Query, Router
|
|
3
|
+
|
|
4
|
+
from ocmoapi.auth import resolver_auth
|
|
5
|
+
from ocmoapi.oauth2_provider import oauth2_auth
|
|
6
|
+
|
|
7
|
+
from ..managers.artifacts import ArtifactsManager
|
|
8
|
+
from ..managers.audit import AuditManager
|
|
9
|
+
from ..managers.auth import AuthManager
|
|
10
|
+
from ..managers.namespace import NamespaceManager
|
|
11
|
+
from ..managers.resolution import (
|
|
12
|
+
ResolutionManager,
|
|
13
|
+
decode_resolve_path,
|
|
14
|
+
parse_query_params,
|
|
15
|
+
scoped_resolve_path,
|
|
16
|
+
validate_cast_format,
|
|
17
|
+
)
|
|
18
|
+
from ..managers.resolve_parameters import ResolveParametersManager
|
|
19
|
+
from ..managers.tree import TreeManager
|
|
20
|
+
from ..operation_ids import (
|
|
21
|
+
DOWNLOAD_RESOLVED_ARTIFACT,
|
|
22
|
+
LIST_CAST_FORMATS,
|
|
23
|
+
RESOLVE_CONFIG,
|
|
24
|
+
RESOLVE_DRAFT_CONFIG,
|
|
25
|
+
RESOLVE_PARAMETERS,
|
|
26
|
+
)
|
|
27
|
+
from ..schemas import (
|
|
28
|
+
CastFormatsListSchema,
|
|
29
|
+
ResolveParametersResponseSchema,
|
|
30
|
+
ResolveResponseSchema,
|
|
31
|
+
)
|
|
32
|
+
from ..schemas.cast_options import cast_format_option_schemas, format_cast_options_json_schema
|
|
33
|
+
from ..schemas.requests import ConfigDocument
|
|
34
|
+
from ..shortcuts import public_base_url
|
|
35
|
+
from ._common import root_model_openapi_extension
|
|
36
|
+
|
|
37
|
+
router = Router()
|
|
38
|
+
|
|
39
|
+
_resolve_auth = [resolver_auth, oauth2_auth]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.get(
|
|
43
|
+
"/~cast-formats/",
|
|
44
|
+
response=CastFormatsListSchema,
|
|
45
|
+
tags=["Resolve"],
|
|
46
|
+
auth=oauth2_auth,
|
|
47
|
+
operation_id=LIST_CAST_FORMATS,
|
|
48
|
+
)
|
|
49
|
+
def list_cast_formats(request):
|
|
50
|
+
"""Return all REST-supported cast formats and their option schemas."""
|
|
51
|
+
return {
|
|
52
|
+
"formats": [
|
|
53
|
+
{
|
|
54
|
+
"format": name,
|
|
55
|
+
"options_schema": format_cast_options_json_schema(name),
|
|
56
|
+
}
|
|
57
|
+
for name in sorted(cast_format_option_schemas())
|
|
58
|
+
],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.get(
|
|
63
|
+
"/ns/{namespace}/~resolve/{path:path}/~download/{token}",
|
|
64
|
+
tags=["Resolve"],
|
|
65
|
+
auth=None,
|
|
66
|
+
operation_id=DOWNLOAD_RESOLVED_ARTIFACT,
|
|
67
|
+
)
|
|
68
|
+
def download_resolved_artifact(request, namespace: str, path: str, token: str):
|
|
69
|
+
"""Download a previously resolved artifact using a signed token.
|
|
70
|
+
|
|
71
|
+
Request authentication is not used; the short-lived token in the URL is the
|
|
72
|
+
sole credential (signed-URL semantics). Any Authorization header is ignored.
|
|
73
|
+
"""
|
|
74
|
+
ns = NamespaceManager(namespace, auth=None).get_or_raise()
|
|
75
|
+
mode, content = ArtifactsManager(ns).download_artifact(path, token)
|
|
76
|
+
if mode == "xaccel":
|
|
77
|
+
# X-Accel-Redirect mode: delegate to Nginx
|
|
78
|
+
response = HttpResponse(status=200)
|
|
79
|
+
response["X-Accel-Redirect"] = content
|
|
80
|
+
response["Content-Type"] = "application/octet-stream"
|
|
81
|
+
return response
|
|
82
|
+
elif mode == "direct":
|
|
83
|
+
return HttpResponse(content, content_type="application/octet-stream")
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError(f"Unknown download mode: {mode}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@router.get(
|
|
89
|
+
"/ns/{namespace}/~resolve/{path:path}",
|
|
90
|
+
response=ResolveResponseSchema,
|
|
91
|
+
tags=["Resolve"],
|
|
92
|
+
auth=_resolve_auth,
|
|
93
|
+
operation_id=RESOLVE_CONFIG,
|
|
94
|
+
)
|
|
95
|
+
def resolve_config(
|
|
96
|
+
request,
|
|
97
|
+
namespace: str,
|
|
98
|
+
path: str,
|
|
99
|
+
version: str = Query("latest"),
|
|
100
|
+
cast: str | None = Query(None),
|
|
101
|
+
trace_only: bool = Query(False),
|
|
102
|
+
mark_stable: bool = Query(False, alias="mark-stable"),
|
|
103
|
+
ignore_configs_with_missing_tags: bool = Query(False, alias="ignore-configs-with-missing-tags"),
|
|
104
|
+
no_creds: bool = Query(False, alias="no-creds"),
|
|
105
|
+
):
|
|
106
|
+
"""Resolve a Config (or every Config under a folder path) through the
|
|
107
|
+
parameters → extend → render → cast pipeline.
|
|
108
|
+
|
|
109
|
+
When ``mark-stable=true``, advance the reserved ``stable`` tag on resolved
|
|
110
|
+
Config root(s) to the version that was just resolved (Configs only).
|
|
111
|
+
|
|
112
|
+
When ``ignore-configs-with-missing-tags=true`` and ``path`` is a folder,
|
|
113
|
+
skip Configs that do not have the requested ``version`` tag or number
|
|
114
|
+
instead of failing the whole folder resolve.
|
|
115
|
+
|
|
116
|
+
When ``no-creds=true``, secret parameters are replaced with the dummy
|
|
117
|
+
value ``<secret-value-placeholder>`` without fetching or decrypting secrets
|
|
118
|
+
(``secret:resolve`` is not required).
|
|
119
|
+
|
|
120
|
+
When authenticated as a Resolver, ``path`` is relative to the resolver's
|
|
121
|
+
scope. Use ``'.'`` to resolve the scope root folder.
|
|
122
|
+
"""
|
|
123
|
+
auth = AuthManager.from_request(request)
|
|
124
|
+
ns = NamespaceManager(namespace, auth=auth).get_or_raise()
|
|
125
|
+
AuditManager.bind(request, auth, namespace=ns)
|
|
126
|
+
path = decode_resolve_path(path)
|
|
127
|
+
validate_cast_format(cast)
|
|
128
|
+
mgr = ResolutionManager(
|
|
129
|
+
ns,
|
|
130
|
+
path,
|
|
131
|
+
auth=auth,
|
|
132
|
+
query_params=request.GET,
|
|
133
|
+
base_url=public_base_url(request),
|
|
134
|
+
version=version,
|
|
135
|
+
cast=cast,
|
|
136
|
+
trace_only=trace_only,
|
|
137
|
+
promote_stable=mark_stable,
|
|
138
|
+
ignore_configs_with_missing_tags=ignore_configs_with_missing_tags,
|
|
139
|
+
no_creds=no_creds,
|
|
140
|
+
)
|
|
141
|
+
result = mgr.resolve()
|
|
142
|
+
request._resolve_cache_status = mgr.cache_status
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@router.get(
|
|
147
|
+
"/ns/{namespace}/~resolve-parameters/{path:path}",
|
|
148
|
+
response=ResolveParametersResponseSchema,
|
|
149
|
+
tags=["Resolve"],
|
|
150
|
+
auth=_resolve_auth,
|
|
151
|
+
operation_id=RESOLVE_PARAMETERS,
|
|
152
|
+
)
|
|
153
|
+
def resolve_parameters(
|
|
154
|
+
request,
|
|
155
|
+
namespace: str,
|
|
156
|
+
path: str,
|
|
157
|
+
version: str = Query("latest"),
|
|
158
|
+
no_creds: bool = Query(False, alias="no-creds"),
|
|
159
|
+
):
|
|
160
|
+
"""Return effective parameter values for a single Config (debug).
|
|
161
|
+
|
|
162
|
+
When ``no-creds=true``, secret parameters show the dummy value
|
|
163
|
+
``<secret-value-placeholder>`` without fetching secrets (``secret:resolve``
|
|
164
|
+
is not required).
|
|
165
|
+
|
|
166
|
+
When authenticated as a Resolver, ``path`` is relative to the resolver's
|
|
167
|
+
scope. Use ``'.'`` to refer to the scope root (must be a Config, not a
|
|
168
|
+
folder — a folder path is rejected by the underlying resolver).
|
|
169
|
+
"""
|
|
170
|
+
auth = AuthManager.from_request(request)
|
|
171
|
+
ns = NamespaceManager(namespace, auth=auth).get_or_raise()
|
|
172
|
+
AuditManager.bind(request, auth, namespace=ns)
|
|
173
|
+
path = decode_resolve_path(path)
|
|
174
|
+
effective_path, _ = scoped_resolve_path(ns, path, auth)
|
|
175
|
+
dynamic_params, _ = parse_query_params(request.GET)
|
|
176
|
+
config_obj = TreeManager(ns, effective_path, auth=auth).get_or_raise(["config"])
|
|
177
|
+
rpm = ResolveParametersManager(
|
|
178
|
+
ns,
|
|
179
|
+
config_obj,
|
|
180
|
+
base_folder="/".join(config_obj.path.split("/")[:-1]),
|
|
181
|
+
version_tag=version,
|
|
182
|
+
version_number=TreeManager.resolve_version(config_obj, version).version,
|
|
183
|
+
dynamic_params=dynamic_params,
|
|
184
|
+
auth=auth,
|
|
185
|
+
no_creds=no_creds,
|
|
186
|
+
)
|
|
187
|
+
return rpm.resolve_debug()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@router.post(
|
|
191
|
+
"/ns/{namespace}/~resolve-draft/{path:path}",
|
|
192
|
+
response=ResolveResponseSchema,
|
|
193
|
+
tags=["Resolve"],
|
|
194
|
+
auth=[oauth2_auth],
|
|
195
|
+
openapi_extra=root_model_openapi_extension(ConfigDocument),
|
|
196
|
+
operation_id=RESOLVE_DRAFT_CONFIG,
|
|
197
|
+
)
|
|
198
|
+
def resolve_draft_config(
|
|
199
|
+
request,
|
|
200
|
+
namespace: str,
|
|
201
|
+
path: str,
|
|
202
|
+
document: ConfigDocument = Body(...),
|
|
203
|
+
cast: str | None = Query(None),
|
|
204
|
+
trace_only: bool = Query(False),
|
|
205
|
+
no_creds: bool = Query(False, alias="no-creds"),
|
|
206
|
+
):
|
|
207
|
+
"""Resolve unsaved draft YAML at the given path without persisting it.
|
|
208
|
+
|
|
209
|
+
The full parameters → extend → render → cast pipeline runs against the
|
|
210
|
+
submitted content. ``config:resolve`` is required on the path and all
|
|
211
|
+
transitive participants. Results are returned as download URLs (same
|
|
212
|
+
shape as ``GET /~resolve/{path}``). Response items use ``version=0``.
|
|
213
|
+
"""
|
|
214
|
+
auth = AuthManager.from_request(request)
|
|
215
|
+
ns = NamespaceManager(namespace, auth=auth).get_or_raise()
|
|
216
|
+
AuditManager.bind(request, auth, namespace=ns)
|
|
217
|
+
validate_cast_format(cast)
|
|
218
|
+
mgr = ResolutionManager(
|
|
219
|
+
ns,
|
|
220
|
+
path,
|
|
221
|
+
auth=auth,
|
|
222
|
+
query_params=request.GET,
|
|
223
|
+
base_url=public_base_url(request),
|
|
224
|
+
cast=cast,
|
|
225
|
+
trace_only=trace_only,
|
|
226
|
+
no_creds=no_creds,
|
|
227
|
+
draft_content=document.root,
|
|
228
|
+
)
|
|
229
|
+
return mgr.resolve()
|
core/api/resolver.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from ninja import Body, Router
|
|
4
|
+
|
|
5
|
+
from ocmoapi.oauth2_provider import oauth2_auth
|
|
6
|
+
|
|
7
|
+
from ..managers.audit import AuditManager
|
|
8
|
+
from ..managers.auth import AuthManager
|
|
9
|
+
from ..managers.namespace import NamespaceManager
|
|
10
|
+
from ..managers.tree import TreeManager
|
|
11
|
+
from ..operation_ids import (
|
|
12
|
+
CREATE_RESOLVER,
|
|
13
|
+
GET_RESOLVER_CONFIGURATION_SCHEMA,
|
|
14
|
+
ROTATE_RESOLVER_TOKEN,
|
|
15
|
+
UPDATE_RESOLVER,
|
|
16
|
+
)
|
|
17
|
+
from ..schemas import ErrorSchema, ResolverSchema, ResolverTokenRotationResponseSchema
|
|
18
|
+
from ..schemas.requests import ResolverDocument, ResolverRotateTokenPayload
|
|
19
|
+
from ..schemas.resolver_json_schema import build_resolver_configuration_json_schema
|
|
20
|
+
from ._common import root_model_openapi_extension
|
|
21
|
+
|
|
22
|
+
router = Router()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get(
|
|
26
|
+
"/~resolver-configuration-schema",
|
|
27
|
+
response=dict[str, Any],
|
|
28
|
+
tags=["Resolver"],
|
|
29
|
+
auth=oauth2_auth,
|
|
30
|
+
summary="Resolver configuration JSON Schema",
|
|
31
|
+
operation_id=GET_RESOLVER_CONFIGURATION_SCHEMA,
|
|
32
|
+
)
|
|
33
|
+
def get_resolver_configuration_schema(request):
|
|
34
|
+
"""Return JSON Schema for resolver configuration YAML (editor autocomplete)."""
|
|
35
|
+
return build_resolver_configuration_json_schema()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.post(
|
|
39
|
+
"/ns/{namespace}/~resolver/~create/{path:path}",
|
|
40
|
+
response={201: ResolverSchema, 409: ErrorSchema, 422: ErrorSchema},
|
|
41
|
+
tags=["Resolver"],
|
|
42
|
+
openapi_extra=root_model_openapi_extension(ResolverDocument),
|
|
43
|
+
operation_id=CREATE_RESOLVER,
|
|
44
|
+
)
|
|
45
|
+
def create_resolver(
|
|
46
|
+
request,
|
|
47
|
+
namespace: str,
|
|
48
|
+
path: str,
|
|
49
|
+
document: ResolverDocument = Body(ResolverDocument("")),
|
|
50
|
+
):
|
|
51
|
+
"""Create a new Resolver. Returns token1 in full on creation."""
|
|
52
|
+
auth = AuthManager.from_request(request)
|
|
53
|
+
ns = NamespaceManager(namespace, auth=auth).get_or_raise()
|
|
54
|
+
AuditManager.bind(request, auth, namespace=ns)
|
|
55
|
+
item = TreeManager(ns, path, auth=auth).create_item(document.root, "resolver")
|
|
56
|
+
item._reveal_token1 = True
|
|
57
|
+
return 201, item
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@router.put(
|
|
61
|
+
"/ns/{namespace}/~resolver/~update/{path:path}",
|
|
62
|
+
response={200: ResolverSchema, 404: ErrorSchema, 422: ErrorSchema},
|
|
63
|
+
tags=["Resolver"],
|
|
64
|
+
openapi_extra=root_model_openapi_extension(ResolverDocument),
|
|
65
|
+
operation_id=UPDATE_RESOLVER,
|
|
66
|
+
)
|
|
67
|
+
def update_resolver(
|
|
68
|
+
request,
|
|
69
|
+
namespace: str,
|
|
70
|
+
path: str,
|
|
71
|
+
document: ResolverDocument = Body(ResolverDocument("")),
|
|
72
|
+
):
|
|
73
|
+
"""Update resolver configuration."""
|
|
74
|
+
auth = AuthManager.from_request(request)
|
|
75
|
+
ns = NamespaceManager(namespace, auth=auth).get_or_raise()
|
|
76
|
+
AuditManager.bind(request, auth, namespace=ns)
|
|
77
|
+
return TreeManager(ns, path, auth=auth).update_item(document.root)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@router.post(
|
|
81
|
+
"/ns/{namespace}/~resolver/~rotate-token/{path:path}",
|
|
82
|
+
response={200: ResolverTokenRotationResponseSchema, 404: ErrorSchema, 422: ErrorSchema},
|
|
83
|
+
tags=["Resolver"],
|
|
84
|
+
operation_id=ROTATE_RESOLVER_TOKEN,
|
|
85
|
+
)
|
|
86
|
+
def rotate_resolver_token(
|
|
87
|
+
request,
|
|
88
|
+
namespace: str,
|
|
89
|
+
path: str,
|
|
90
|
+
payload: ResolverRotateTokenPayload,
|
|
91
|
+
):
|
|
92
|
+
"""Rotate token1 or token2 for a Resolver (token_number must be 1 or 2)."""
|
|
93
|
+
auth = AuthManager.from_request(request)
|
|
94
|
+
ns = NamespaceManager(namespace, auth=auth).get_or_raise()
|
|
95
|
+
AuditManager.bind(request, auth, namespace=ns)
|
|
96
|
+
return TreeManager(ns, path, auth=auth).rotate_resolver_token(payload.token_number)
|
core/api/secret.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from ninja import Body, Router
|
|
2
|
+
|
|
3
|
+
from ..managers.audit import AuditManager
|
|
4
|
+
from ..managers.auth import AuthManager
|
|
5
|
+
from ..managers.namespace import NamespaceManager
|
|
6
|
+
from ..managers.secret import SecretManager
|
|
7
|
+
from ..operation_ids import CREATE_SECRET, UPDATE_SECRET
|
|
8
|
+
from ..schemas import AnyExtendedNodeSchema, ErrorSchema
|
|
9
|
+
from ..schemas.requests import SecretDocument
|
|
10
|
+
from ._common import root_model_openapi_extension
|
|
11
|
+
|
|
12
|
+
router = Router()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.post(
|
|
16
|
+
"/ns/{namespace}/~secret/~create/{path:path}",
|
|
17
|
+
response={201: AnyExtendedNodeSchema, 409: ErrorSchema, 413: ErrorSchema, 422: ErrorSchema},
|
|
18
|
+
tags=["Secret"],
|
|
19
|
+
openapi_extra=root_model_openapi_extension(SecretDocument),
|
|
20
|
+
operation_id=CREATE_SECRET,
|
|
21
|
+
)
|
|
22
|
+
def create_secret(
|
|
23
|
+
request,
|
|
24
|
+
namespace: str,
|
|
25
|
+
path: str,
|
|
26
|
+
document: SecretDocument = Body(...),
|
|
27
|
+
):
|
|
28
|
+
"""Create a new Secret. The document is validated as YAML then encrypted at rest."""
|
|
29
|
+
auth = AuthManager.from_request(request)
|
|
30
|
+
ns = NamespaceManager(namespace, auth=auth).get_or_raise()
|
|
31
|
+
AuditManager.bind(request, auth, namespace=ns)
|
|
32
|
+
item = SecretManager(ns, path, auth=auth).create(document.root)
|
|
33
|
+
return 201, item
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@router.put(
|
|
37
|
+
"/ns/{namespace}/~secret/~update/{path:path}",
|
|
38
|
+
response={200: AnyExtendedNodeSchema, 404: ErrorSchema, 413: ErrorSchema, 422: ErrorSchema},
|
|
39
|
+
tags=["Secret"],
|
|
40
|
+
openapi_extra=root_model_openapi_extension(SecretDocument),
|
|
41
|
+
operation_id=UPDATE_SECRET,
|
|
42
|
+
)
|
|
43
|
+
def update_secret(
|
|
44
|
+
request,
|
|
45
|
+
namespace: str,
|
|
46
|
+
path: str,
|
|
47
|
+
document: SecretDocument = Body(...),
|
|
48
|
+
):
|
|
49
|
+
"""Update an existing Secret. A new encrypted version is created only when content differs."""
|
|
50
|
+
auth = AuthManager.from_request(request)
|
|
51
|
+
ns = NamespaceManager(namespace, auth=auth).get_or_raise()
|
|
52
|
+
AuditManager.bind(request, auth, namespace=ns)
|
|
53
|
+
return SecretManager(ns, path, auth=auth).update(document.root)
|
core/api/system.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from django.http import HttpResponse
|
|
4
|
+
from ninja import Router
|
|
5
|
+
|
|
6
|
+
from ..managers.health import HealthManager
|
|
7
|
+
from ..managers.system import SystemManager
|
|
8
|
+
from ..operation_ids import HEALTH, VERSION
|
|
9
|
+
from ..schemas import HealthSchema, ProductVersionWithNoticeSchema
|
|
10
|
+
|
|
11
|
+
router = Router(tags=["System"])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@router.get(
|
|
15
|
+
"/health",
|
|
16
|
+
response={200: HealthSchema, 503: HealthSchema},
|
|
17
|
+
auth=None,
|
|
18
|
+
summary="Application health",
|
|
19
|
+
operation_id=HEALTH,
|
|
20
|
+
)
|
|
21
|
+
def health(request):
|
|
22
|
+
"""Validate core dependencies required for normal API operation."""
|
|
23
|
+
payload = HealthManager().check()
|
|
24
|
+
status_code = 200 if payload["status"] == "ok" else 503
|
|
25
|
+
return status_code, payload
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@router.get(
|
|
29
|
+
"/version",
|
|
30
|
+
response=ProductVersionWithNoticeSchema,
|
|
31
|
+
auth=None,
|
|
32
|
+
summary="Application version",
|
|
33
|
+
operation_id=VERSION,
|
|
34
|
+
)
|
|
35
|
+
def version(request, notice: bool = False):
|
|
36
|
+
"""Return the deployed OCMO product version and public auth configuration."""
|
|
37
|
+
payload = SystemManager().version_payload(include_notice=notice)
|
|
38
|
+
body = ProductVersionWithNoticeSchema.model_validate(payload).model_dump(
|
|
39
|
+
mode="json",
|
|
40
|
+
exclude_none=True,
|
|
41
|
+
)
|
|
42
|
+
return HttpResponse(json.dumps(body), content_type="application/json")
|
core/api/template.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from ninja import Body, Router
|
|
2
|
+
|
|
3
|
+
from ..managers.audit import AuditManager
|
|
4
|
+
from ..managers.auth import AuthManager
|
|
5
|
+
from ..managers.namespace import NamespaceManager
|
|
6
|
+
from ..managers.tree import TreeManager
|
|
7
|
+
from ..operation_ids import CREATE_TEMPLATE, UPDATE_TEMPLATE
|
|
8
|
+
from ..schemas import AnyExtendedNodeSchema, ErrorSchema
|
|
9
|
+
from ..schemas.requests import TemplateDocument
|
|
10
|
+
from ._common import root_model_openapi_extension
|
|
11
|
+
|
|
12
|
+
router = Router()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@router.post(
|
|
16
|
+
"/ns/{namespace}/~template/~create/{path:path}",
|
|
17
|
+
response={201: AnyExtendedNodeSchema, 409: ErrorSchema, 413: ErrorSchema, 422: ErrorSchema},
|
|
18
|
+
tags=["Template"],
|
|
19
|
+
openapi_extra=root_model_openapi_extension(TemplateDocument),
|
|
20
|
+
operation_id=CREATE_TEMPLATE,
|
|
21
|
+
)
|
|
22
|
+
def create_template(
|
|
23
|
+
request,
|
|
24
|
+
namespace: str,
|
|
25
|
+
path: str,
|
|
26
|
+
document: TemplateDocument = Body(...),
|
|
27
|
+
):
|
|
28
|
+
"""Create a new Template at the specified path."""
|
|
29
|
+
auth = AuthManager.from_request(request)
|
|
30
|
+
ns = NamespaceManager(namespace, auth=auth).get_or_raise()
|
|
31
|
+
AuditManager.bind(request, auth, namespace=ns)
|
|
32
|
+
item = TreeManager(ns, path, auth=auth).create_item(document.root, "template")
|
|
33
|
+
return 201, item
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@router.put(
|
|
37
|
+
"/ns/{namespace}/~template/~update/{path:path}",
|
|
38
|
+
response={200: AnyExtendedNodeSchema, 404: ErrorSchema, 413: ErrorSchema, 422: ErrorSchema},
|
|
39
|
+
tags=["Template"],
|
|
40
|
+
openapi_extra=root_model_openapi_extension(TemplateDocument),
|
|
41
|
+
operation_id=UPDATE_TEMPLATE,
|
|
42
|
+
)
|
|
43
|
+
def update_template(
|
|
44
|
+
request,
|
|
45
|
+
namespace: str,
|
|
46
|
+
path: str,
|
|
47
|
+
document: TemplateDocument = Body(...),
|
|
48
|
+
):
|
|
49
|
+
"""Update an existing Template. Creates a new version only when content changes."""
|
|
50
|
+
auth = AuthManager.from_request(request)
|
|
51
|
+
ns = NamespaceManager(namespace, auth=auth).get_or_raise()
|
|
52
|
+
AuditManager.bind(request, auth, namespace=ns)
|
|
53
|
+
return TreeManager(ns, path, auth=auth).update_item(document.root)
|