Dry run publish 1C access profiles
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-21 19:22:02 +03:00
parent 6051f59e08
commit 29bbe1dca6
4 changed files with 92 additions and 4 deletions
+1 -1
View File
@@ -29,6 +29,7 @@ http://server/base/hs/sfera/v1/metadata/apply
- `data.read` - чтение данных через ограниченный запрос или менеджер объекта.
- `data.write` - изменение данных только при явном `allow_mutation`.
- `metadata.apply` - изменение структуры не выполняется из HTTP runtime. Возвращает план установки `.cfe`; применение делает Windows Agent через Designer.
- `access.profile.apply` - dry-run проверки плана профиля доступа через `/v1/metadata/apply`. Универсальный мост подтверждает профиль, роли и операции, но реальную запись профилей доступа выполняет только отдельный адаптер под конкретную конфигурацию/БСП или Windows Agent.
## Безопасность
@@ -41,4 +42,3 @@ http://server/base/hs/sfera/v1/metadata/apply
- `dry_run=false`.
Без этого операции изменения возвращают блокировку.
@@ -49,7 +49,8 @@
КонецЕсли;
Возврат ОтветJSON(BridgeMetadataApply(
ПолучитьПоле(Контекст, "payload", Новый Структура),
ПолучитьПоле(Контекст, "dry_run", Истина)));
ПолучитьПоле(Контекст, "dry_run", Истина),
ПолучитьПоле(Контекст, "allow_mutation", Ложь)));
КонецФункции
#КонецОбласти
@@ -64,6 +65,7 @@
Результат.Вставить("timestamp", ТекущаяДата());
Результат.Вставить("mutation_supported", Истина);
Результат.Вставить("metadata_apply_supported", Ложь);
Результат.Вставить("access_profile_apply_supported", Истина);
Возврат Результат;
КонецФункции
@@ -153,7 +155,11 @@
Возврат ОшибкаSFERA("Generic write adapter is intentionally not enabled yet. Implement object-specific handlers first.");
КонецФункции
Функция BridgeMetadataApply(Параметры, DryRun)
Функция BridgeMetadataApply(Параметры, DryRun, AllowMutation)
Операция = Строка(ПолучитьПоле(Параметры, "operation", ""));
Если Операция = "access.profile.apply" Тогда
Возврат BridgeAccessProfileApply(Параметры, DryRun, AllowMutation);
КонецЕсли;
Результат = Новый Структура;
Результат.Вставить("status", "planned");
Результат.Вставить("message", "Changing configuration structure is performed by SFERA Windows Agent through Designer and .cfe update, not by runtime HTTP.");
@@ -162,6 +168,27 @@
Возврат Результат;
КонецФункции
Функция BridgeAccessProfileApply(Параметры, DryRun, AllowMutation)
Профиль = ПолучитьПоле(Параметры, "profile", Новый Структура);
Операции = ПолучитьПоле(Параметры, "operations", Новый Массив);
ИмяПрофиля = Строка(ПолучитьПоле(Профиль, "qualified_name", ПолучитьПоле(Профиль, "name", "")));
Если ПустаяСтрока(ИмяПрофиля) Тогда
Возврат ОшибкаSFERA("profile.name or profile.qualified_name is required for access.profile.apply");
КонецЕсли;
Если Не DryRun И Не AllowMutation Тогда
Возврат ОшибкаSFERA("Access profile mutation is blocked. Use dry_run=true or allow_mutation=true with project mutation guard enabled.");
КонецЕсли;
Результат = Новый Структура;
Результат.Вставить("status", ?(DryRun, "dry_run", "planned"));
Результат.Вставить("operation", "access.profile.apply");
Результат.Вставить("profile", ИмяПрофиля);
Результат.Вставить("operations_count", КоличествоЭлементовSFERA(Операции));
Результат.Вставить("operations", Операции);
Результат.Вставить("message", "Access profile plan was accepted by SFERA extension. Runtime mutation is not executed by the generic bridge; apply through a configuration-specific adapter or Windows Agent.");
Результат.Вставить("dry_run", DryRun);
Возврат Результат;
КонецФункции
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
@@ -210,6 +237,16 @@
Возврат ЗначениеПоУмолчанию;
КонецФункции
Функция КоличествоЭлементовSFERA(Значение)
Если Значение = Неопределено Тогда
Возврат 0;
КонецЕсли;
Если ТипЗнч(Значение) = Тип("Массив") Или ТипЗнч(Значение) = Тип("Структура") Или ТипЗнч(Значение) = Тип("Соответствие") Тогда
Возврат Значение.Количество();
КонецЕсли;
Возврат 0;
КонецФункции
Процедура ДобавитьКоллекциюМетаданных(Коллекции, ИмяКоллекции, КоллекцияМетаданных)
Объекты = Новый Массив;
Для Каждого ОбъектМетаданных Из КоллекцияМетаданных Цикл
@@ -3015,6 +3015,30 @@ async def get_project_access_profile_publish_plan(project_id: str, profile_name:
return _build_access_profile_publish_plan(normalized, profile)
@app.post("/projects/{project_id}/access/profiles/{profile_name}/publish-dry-run", response_model=SferaExtensionCallResponse)
async def dry_run_project_access_profile_publish(project_id: str, profile_name: str) -> SferaExtensionCallResponse:
settings = _project_settings_or_404(project_id)
normalized = _load_normalized_project(project_id)
if normalized is None:
raise HTTPException(status_code=404, detail="NormalizedProject not found")
profile = _access_profile_by_name(normalized, profile_name)
if profile is None:
raise HTTPException(status_code=404, detail="Access profile not found")
plan = _build_access_profile_publish_plan(normalized, profile)
if not plan.ready_for_extension:
raise HTTPException(status_code=409, detail="Access profile publish plan is not ready for extension")
return _call_sfera_extension(
project_id,
settings,
SferaExtensionCallRequest(
operation="access.profile.apply",
payload=plan.extension_payload,
dry_run=True,
allow_mutation=False,
),
)
@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)
@@ -6568,6 +6592,7 @@ _SFERA_EXTENSION_MUTATION_OPERATIONS = {
"data.write",
"data.delete",
"metadata.apply",
"access.profile.apply",
"admin.command",
}
@@ -6601,6 +6626,7 @@ def _sfera_extension_operation_path(operation: str) -> str:
"data.write": "v1/data/write",
"query.execute": "v1/query",
"metadata.apply": "v1/metadata/apply",
"access.profile.apply": "v1/metadata/apply",
"admin.command": "v1/admin/command",
}
return mapping.get(operation, "v1/call")
+26 -1
View File
@@ -1466,7 +1466,7 @@ def test_runtime_required_import_does_not_index_cf_file_directly(monkeypatch, tm
assert payload["normalized_summary"]["object_count"] >= 1
def test_import_supports_structure_only_indexing(tmp_path: Path):
def test_import_supports_structure_only_indexing(monkeypatch, tmp_path: Path):
(tmp_path / "metadata.xml").write_text(
"""
<Configuration>
@@ -1585,6 +1585,31 @@ def test_import_supports_structure_only_indexing(tmp_path: Path):
assert any(item["action"] == "ADD_ROLE_TO_PROFILE" and item["role"] == "Роль.Менеджер" for item in plan_payload["operations"])
assert plan_payload["extension_payload"]["operation"] == "access.profile.apply"
captured_extension_call = {}
def fake_extension_call(project_id_arg, settings_arg, request_arg):
captured_extension_call["project_id"] = project_id_arg
captured_extension_call["request"] = request_arg
return main.SferaExtensionCallResponse(
project_id=project_id_arg,
operation=request_arg.operation,
status="READY",
ready=True,
dry_run=request_arg.dry_run,
extension_url="http://example.test/hs/sfera/v1/metadata/apply",
result={"status": "dry_run", "operation": request_arg.operation},
)
monkeypatch.setattr(main, "_call_sfera_extension", fake_extension_call)
publish_dry_run = client.post(
f"/projects/{project_id}/access/profiles/{quote('НовыйПрофильHTTP')}/publish-dry-run"
)
assert publish_dry_run.status_code == 200
assert publish_dry_run.json()["operation"] == "access.profile.apply"
assert captured_extension_call["project_id"] == project_id
assert captured_extension_call["request"].dry_run is True
assert captured_extension_call["request"].payload["profile"]["roles"] == ["Роль.Менеджер"]
tree = client.get(f"/projects/{project_id}/metadata/tree")
assert tree.status_code == 200
root = tree.json()["root"]