diff --git a/services/api-server/src/api_server/main.py b/services/api-server/src/api_server/main.py index 23c9d53..a4b5435 100644 --- a/services/api-server/src/api_server/main.py +++ b/services/api-server/src/api_server/main.py @@ -194,6 +194,8 @@ from operations_core import ( build_project_report, ) from one_c_normalizer import ( + AccessProfile, + AccessRoleAssignment, COMMON_BRANCH_CHILDREN, METADATA_CHILD_OBJECT_SPECS, METADATA_TYPE_CONTEXT_ACTIONS, @@ -853,6 +855,19 @@ class AccessProfileDraftResponse(BaseModel): proposed_profile: dict = Field(default_factory=dict) +class AccessProfileApplyRequest(AccessProfileDraftRequest): + allow_incomplete: bool = False + replace_existing: bool = False + author: str | None = None + + +class AccessProfileApplyResponse(BaseModel): + applied: bool + profile: dict + draft: AccessProfileDraftResponse + message: str + + class ProjectSetupResponse(BaseModel): project_id: str status: ProjectSetupStatus @@ -2948,6 +2963,39 @@ async def preview_project_access_profile(project_id: str, request: AccessProfile return _build_access_profile_draft(normalized, request) +@app.post("/projects/{project_id}/access/profiles", response_model=AccessProfileApplyResponse) +async def apply_project_access_profile(project_id: str, request: AccessProfileApplyRequest) -> AccessProfileApplyResponse: + normalized = _load_normalized_project(project_id) + if normalized is None: + raise HTTPException(status_code=404, detail="NormalizedProject not found") + draft = _build_access_profile_draft(normalized, request) + if draft.missing_objects and not request.allow_incomplete: + raise HTTPException(status_code=409, detail="Access profile draft has missing objects") + profile = _access_profile_from_draft(draft, request) + existing_index = next( + ( + index + for index, item in enumerate(normalized.access.profiles) + if item.name.casefold() == profile.name.casefold() + or str(item.qualified_name or "").casefold() == str(profile.qualified_name or "").casefold() + ), + None, + ) + if existing_index is not None and not request.replace_existing: + raise HTTPException(status_code=409, detail="Access profile already exists") + if existing_index is None: + normalized.access.profiles.append(profile) + else: + normalized.access.profiles[existing_index] = profile + _save_normalized_project(project_id, normalized) + return AccessProfileApplyResponse( + applied=True, + profile=profile.model_dump(mode="json"), + draft=draft, + message="Access profile saved in SFERA workspace model", + ) + + @app.get("/projects/{project_id}/imports/quality", response_model=ImportQualityResponse) async def get_import_quality(project_id: str) -> ImportQualityResponse: return _import_quality_response(project_id) @@ -7374,6 +7422,38 @@ def _build_access_profile_draft( ) +def _access_profile_from_draft( + draft: AccessProfileDraftResponse, + request: AccessProfileApplyRequest, +) -> AccessProfile: + now = _current_timestamp() + attributes = { + "status": "workspace_draft", + "created_by": request.author or "", + "created_at": now, + "target_objects": list(draft.proposed_profile.get("target_objects", [])), + "permissions": list(draft.proposed_profile.get("permissions", [])), + "missing_objects": list(draft.missing_objects), + } + return AccessProfile( + name=draft.name, + qualified_name=str(draft.proposed_profile.get("qualified_name") or f"ПрофильГруппыДоступа.{draft.name}"), + attributes=attributes, + roles=[ + AccessRoleAssignment( + role=role.role, + role_qualified_name=role.role_qualified_name, + source=role.source, + attributes={ + "matched_objects": role.matched_objects, + "permissions": role.permissions, + }, + ) + for role in draft.roles + ], + ) + + def _access_roles_for_targets( normalized: NormalizedProject, target_objects: list[str], diff --git a/services/api-server/tests/test_api.py b/services/api-server/tests/test_api.py index 665ab3e..226d17c 100644 --- a/services/api-server/tests/test_api.py +++ b/services/api-server/tests/test_api.py @@ -1558,6 +1558,22 @@ def test_import_supports_structure_only_indexing(tmp_path: Path): assert "Роль.Менеджер" in preview_payload["proposed_profile"]["roles"] assert preview_payload["missing_objects"] == [] + apply_profile = client.post( + f"/projects/{project_id}/access/profiles", + json={ + "name": "НовыйПрофильHTTP", + "target_objects": ["HTTPСервис.ПубличныйAPI"], + "permissions": ["read"], + "author": "dev.ivan", + }, + ) + assert apply_profile.status_code == 200 + assert apply_profile.json()["profile"]["attributes"]["status"] == "workspace_draft" + + updated_access = client.get(f"/projects/{project_id}/access") + assert updated_access.status_code == 200 + assert any(item["name"] == "НовыйПрофильHTTP" for item in updated_access.json()["profiles"]) + tree = client.get(f"/projects/{project_id}/metadata/tree") assert tree.status_code == 200 root = tree.json()["root"]