From 9b0bff1386f31302543049b226911ded0ffe1682 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 13 Aug 2026 20:12:15 +0300 Subject: [PATCH] =?UTF-8?q?fix(role-compile,role-validate):=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=BA=D1=80=D1=8B=D1=82=D1=8B=D0=B9=20=D0=B1=D0=B5=D0=BB=D1=8B?= =?UTF-8?q?=D0=B9=20=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20=D1=82=D0=B8?= =?UTF-8?q?=D0=BF=D0=BE=D0=B2=20=D0=B8=20=D0=BF=D1=80=D0=B0=D0=B2=20=D0=B2?= =?UTF-8?q?=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20=D0=BF=D1=80=D0=B5=D0=B4=D1=83?= =?UTF-8?q?=D0=BF=D1=80=D0=B5=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Неизвестный тип объекта обрабатывался веткой «warning» и всё равно попадал в Rights.xml: и генератор, и валидатор рапортовали успех. Блок прав на тип, который прав не имеет, платформа не отвергает — конфигурация с правами на перечисление не загружается в информационную базу, конфигуратор зависает без сообщений. Белый список — дерево редактора ролей: 27 типов (добавлен ExternalDataSource, которого не было ни в одной таблице) плюс таблица видов вложенности с наборами прав и привязкой вида к типу-родителю. Значения сняты с корпуса (acc/erp/ut/unf, ~2750 ролей) и с выгрузок роли со всеми проставленными правами — для внешнего источника данных и перерасчёта регистра расчёта, которых в корпусе нет. - role-compile: отказ ДО записи файлов, все причины разом, exit 1; ни файлов роли, ни записи в Configuration.xml. Русские алиасы для типов без прав — ради внятного отказа, а не ради генерации. - role-validate: те же случаи — ошибка вместо предупреждения. - Побочно снято 424 ложных предупреждения на корпусе: права Use у операций веб-сервисов и методов HTTP-сервисов считались недопустимыми для вложенных объектов. - Проверка имён прав и вложенных путей больше не пропускает мусор: неизвестное право и путь вида Enum.Х.Attribute.Y раньше не проверялись вовсе. - docs/1c-role-spec.md: ExternalDataSource ошибочно числился типом без прав. - dsl-reference.md переписан под «что можно написать» (тип → права, виды вложенности) вместо таблиц прощающего ввода. Тесты: expect.filesAbsent в раннере; новые кейсы на отказ и на вложенные объекты. Починены кейсы, которые ничего не проверяли: три задавали права одной строкой "Read View" (навык писал в эталон несуществующее право), valid-role собирал роль неизвестным ключом и валидировал пустую, bad-root проходил на «файл не найден». Co-Authored-By: Claude Opus 5 (1M context) --- .claude/skills/role-compile/SKILL.md | 10 +- .claude/skills/role-compile/dsl-reference.md | 298 ++++++------------ .../role-compile/scripts/role-compile.ps1 | 192 +++++++++-- .../role-compile/scripts/role-compile.py | 181 +++++++++-- .claude/skills/role-validate/SKILL.md | 11 + .../role-validate/scripts/role-validate.ps1 | 140 ++++++-- .../role-validate/scripts/role-validate.py | 132 ++++++-- docs/1c-role-spec.md | 53 +++- .../skills/cases/role-compile/basic-role.json | 2 +- .../role-compile/error-forbidden-type.json | 19 ++ .../role-compile/error-nested-alien-kind.json | 12 + .../role-compile/error-unknown-right.json | 12 + .../role-compile/error-unknown-type.json | 13 + .../role-compile/nested-and-services.json | 25 ++ .../basic-role/Roles/Кладовщик/Ext/Rights.xml | 6 +- .../nested-and-services/Configuration.xml | 252 +++++++++++++++ .../Ext/ClientApplicationInterface.xml | 18 ++ .../nested-and-services/Languages/Русский.xml | 16 + .../nested-and-services/Roles/Интеграция.xml | 15 + .../Roles/Интеграция/Ext/Rights.xml | 96 ++++++ .../Roles/Кладовщик/Ext/Rights.xml | 6 +- .../cases/role-compile/synonym-rights.json | 2 +- .../role-validate/error-forbidden-type.json | 13 + .../role-validate/external-data-source.json | 10 + .../{ => Roles}/BadRole/Ext/Rights.xml | 0 .../ПраваНаВнешнийИсточник/Ext/Rights.xml | 205 ++++++++++++ .../Roles/ПраваНаПерерасчет/Ext/Rights.xml | 64 ++++ .../Roles/РольВсеТипы/Ext/Rights.xml | 83 +++++ .../cases/role-validate/recalculation.json | 10 + .../valid-role/Roles/Тест/Ext/Rights.xml | 7 + .../cases/role-validate/valid-role.json | 9 +- tests/skills/check-type-maps.mjs | 6 +- .../skills/integration/build-config.test.mjs | 8 +- .../integration/build-webtest-config.test.mjs | 28 +- tests/skills/runner.mjs | 22 +- 35 files changed, 1646 insertions(+), 330 deletions(-) create mode 100644 tests/skills/cases/role-compile/error-forbidden-type.json create mode 100644 tests/skills/cases/role-compile/error-nested-alien-kind.json create mode 100644 tests/skills/cases/role-compile/error-unknown-right.json create mode 100644 tests/skills/cases/role-compile/error-unknown-type.json create mode 100644 tests/skills/cases/role-compile/nested-and-services.json create mode 100644 tests/skills/cases/role-compile/snapshots/nested-and-services/Configuration.xml create mode 100644 tests/skills/cases/role-compile/snapshots/nested-and-services/Ext/ClientApplicationInterface.xml create mode 100644 tests/skills/cases/role-compile/snapshots/nested-and-services/Languages/Русский.xml create mode 100644 tests/skills/cases/role-compile/snapshots/nested-and-services/Roles/Интеграция.xml create mode 100644 tests/skills/cases/role-compile/snapshots/nested-and-services/Roles/Интеграция/Ext/Rights.xml create mode 100644 tests/skills/cases/role-validate/error-forbidden-type.json create mode 100644 tests/skills/cases/role-validate/external-data-source.json rename tests/skills/cases/role-validate/fixtures/bad-root/{ => Roles}/BadRole/Ext/Rights.xml (100%) create mode 100644 tests/skills/cases/role-validate/fixtures/external-data-source/Roles/ПраваНаВнешнийИсточник/Ext/Rights.xml create mode 100644 tests/skills/cases/role-validate/fixtures/external-data-source/Roles/ПраваНаПерерасчет/Ext/Rights.xml create mode 100644 tests/skills/cases/role-validate/fixtures/no-rights-types/Roles/РольВсеТипы/Ext/Rights.xml create mode 100644 tests/skills/cases/role-validate/recalculation.json diff --git a/.claude/skills/role-compile/SKILL.md b/.claude/skills/role-compile/SKILL.md index d998e1e9..8e7e8758 100644 --- a/.claude/skills/role-compile/SKILL.md +++ b/.claude/skills/role-compile/SKILL.md @@ -64,7 +64,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-compile.ps1" - ### Русские синонимы -Поддерживаются русские типы (`Справочник`→Catalog, `Документ`→Document) и права (`Чтение`→Read, `Просмотр`→View). Смешивание допустимо: `"Справочник.Контрагенты: Чтение, View"`. +Поддерживаются русские типы (`Справочник`→Catalog, `Документ`→Document) и права (`Чтение`→Read, `Просмотр`→View). Каноничная форма — английская. ### Шаблоны RLS @@ -99,7 +99,13 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-compile.ps1" - } ``` -Подробные таблицы пресетов, русских синонимов и дополнительные примеры — в `dsl-reference.md`. +## Что можно писать в `objects` + +Права имеют 27 типов объектов; тип или имя права вне списка — ошибка: роль не создаётся, файлы не пишутся, `Configuration.xml` не меняется. Права нельзя назначить на `Enum`, `CommonModule`, `DefinedType`, `CommonPicture`, `CommonTemplate`, `Language`, `FunctionalOption`, `EventSubscription`, `ScheduledJob`, `StyleItem`, `SettingsStorage` и подобные — в дереве редактора ролей их нет. + +Права на части объекта задаются точечным путём: `Catalog.Контрагенты.Attribute.ИНН: View, Edit`, `WebService.Обмен.Operation.Загрузить: Use`. + +Полные таблицы «тип → права», виды вложенности (включая внешние источники данных), список типов без прав, таблицы пресетов и дополнительные примеры — в `dsl-reference.md`. ## Верификация diff --git a/.claude/skills/role-compile/dsl-reference.md b/.claude/skills/role-compile/dsl-reference.md index 0e23a589..fd6f8f83 100644 --- a/.claude/skills/role-compile/dsl-reference.md +++ b/.claude/skills/role-compile/dsl-reference.md @@ -2,49 +2,86 @@ Подробная справка по JSON DSL для `/role-compile`. Компактное описание — в [SKILL.md](SKILL.md). -## Структура верхнего уровня +## Какие объекты можно писать в `objects` -```json -{ - "name": "ИмяРоли", - "synonym": "Отображаемое имя роли", - "comment": "", - "setForNewObjects": false, - "setForAttributesByDefault": true, - "independentRightsOfChildObjects": false, - "objects": [ ... ], - "templates": [ ... ] -} -``` +Права в роли имеют ровно 27 типов. Тип вне этого списка — ошибка компиляции: роль не +создаётся, файлы не пишутся. -- `name` — программное имя роли (обязательно) -- `synonym` — отображаемое имя (по умолчанию = name) -- `comment` — комментарий (по умолчанию пусто) -- Глобальные флаги — по умолчанию `false`, `true`, `false` +| Тип | Права | +|-----|-------| +| `Configuration` | Administration, DataAdministration, UpdateDataBaseConfiguration, ConfigurationExtensionsAdministration, ActiveUsers, EventLog, ExclusiveMode, ThinClient, ThickClient, WebClient, MobileClient, ExternalConnection, Automation, Output, SaveUserData, TechnicalSpecialistMode, InteractiveOpenExtDataProcessors, InteractiveOpenExtReports, AnalyticsSystemClient, CollaborationSystemInfoBaseRegistration, MainWindowModeNormal, MainWindowModeWorkplace, MainWindowModeEmbeddedWorkplace, MainWindowModeFullscreenWorkplace, MainWindowModeKiosk | +| `Catalog`, `ChartOfCharacteristicTypes` | Read, Insert, Update, Delete, View, Edit, InputByString, Interactive* (Insert, SetDeletionMark, ClearDeletionMark, Delete, DeleteMarked), *PredefinedData, *DataHistory* | +| `Document` | то же + Posting, UndoPosting, InteractivePosting, InteractivePostingRegular, InteractiveUndoPosting, InteractiveChangeOfPosted | +| `ChartOfAccounts`, `ChartOfCalculationTypes`, `ExchangePlan` | Read, Insert, Update, Delete, View, Edit, InputByString, Interactive*, *DataHistory* (без InteractiveDeleteMarked у ChartOfAccounts) | +| `BusinessProcess` | Read, Insert, Update, Delete, View, Edit, InputByString, Start, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractiveDelete, InteractiveActivate, InteractiveStart | +| `Task` | то же, но вместо Start/InteractiveStart — Execute, InteractiveExecute | +| `InformationRegister` | Read, Update, View, Edit, TotalsControl, *DataHistory* | +| `AccumulationRegister`, `AccountingRegister` | Read, Update, View, Edit, TotalsControl | +| `CalculationRegister` | Read, View | +| `Constant` | Read, Update, View, Edit, *DataHistory* | +| `DocumentJournal` | Read, View | +| `Sequence` | Read, Update | +| `DataProcessor`, `Report` | Use, View | +| `WebService`, `HTTPService`, `IntegrationService` | Use | +| `CommonForm`, `CommonCommand`, `Subsystem`, `FilterCriterion` | View | +| `CommonAttribute` | View, Edit | +| `SessionParameter` | Get, Set | +| `ExternalDataSource` | Use, Administration, StandardAuthenticationChange, SessionStandardAuthenticationChange, SessionOSAuthenticationChange | -## Объекты: два формата +Полные списки прав по каждому типу — в коде навыка (`scripts/role-compile.ps1`, +таблица `$knownRights`); навык проверяет каждое имя права и отвергает несуществующее. + +### Вложенные объекты + +Путь длиннее двух сегментов задаёт права на часть объекта. Вид вложенности — +предпоследний сегмент, он определяет набор прав. + +| Вид | Права | +|-----|-------| +| `Attribute`, `StandardAttribute`, `TabularSection`, `StandardTabularSection`, `Dimension`, `Resource`, `AccountingFlag`, `ExtDimensionAccountingFlag`, `AddressingAttribute` | View, Edit | +| `Command`, вложенная `Subsystem` | View | +| `Operation` (веб-сервис), `Method` (HTTP-сервис, под `URLTemplate`), `IntegrationServiceChannel` | Use | +| `Recalculation` (регистр расчёта) | Read, Update | + +Виды `Operation`, `Method`, `IntegrationServiceChannel`, `Recalculation` и виды внешнего +источника допустимы только у своего типа-родителя. Графы журнала документов +(`DocumentJournal.Ж.Column.Г`) собственных прав не имеют. + +Примеры: `Catalog.Контрагенты.Attribute.ИНН: View, Edit`, +`Document.Реализация.TabularSection.Товары.Attribute.Цена: View`, +`WebService.Обмен.Operation.Загрузить: Use`, +`HTTPService.API.URLTemplate.Файлы.Method.get: Use`. + +### Вложенные объекты внешнего источника данных + +Виды `Table`, `Cube`, `Function`, `Field`, `DimensionTable` бывают **только** под +`ExternalDataSource`. + +| Путь | Права | +|------|-------| +| `ExternalDataSource.И.Table.Т` | Read, Insert, Update, Delete, View, Edit, InputByString, InteractiveInsert, InteractiveDelete | +| `ExternalDataSource.И.Table.Т.Field.П` | View, Edit | +| `ExternalDataSource.И.Cube.К` | Read, View | +| `ExternalDataSource.И.Cube.К.Dimension.И`, `…Resource.Р` | View | +| `ExternalDataSource.И.Cube.К.DimensionTable.Т` | Read, View | +| `ExternalDataSource.И.Cube.К.DimensionTable.Т.Field.П` | View, Edit | +| `ExternalDataSource.И.Function.Ф` | Use, View | +| `…Table.Т.Command.К`, `…Cube.К.Command.К`, `…DimensionTable.Т.Command.К` | View | + +### Типы объектов без прав в ролях + +Права нельзя назначить на `Enum`, `CommonModule`, `DefinedType`, `CommonPicture`, +`CommonTemplate`, `Language`, `FunctionalOption`, `FunctionalOptionsParameter`, +`EventSubscription`, `ScheduledJob`, `StyleItem`, `Style`, `SettingsStorage`, +`XDTOPackage`, `WSReference`, `DocumentNumerator` — в дереве редактора ролей этих типов нет. + +Попытка — ошибка компиляции. Права на перечисления особенно опасны: конфигурация с таким +блоком не загружается в информационную базу, конфигуратор зависает без сообщений. + +## Формат записи объектов Массив `objects` принимает строки (shorthand) и объекты (полная форма). -### Строковый shorthand - -``` -"ОбъектМетаданных: @пресет" -"ОбъектМетаданных: Право1, Право2" -``` - -Примеры: -```json -"objects": [ - "Catalog.Номенклатура: @view", - "Document.Реализация: @edit", - "InformationRegister.Цены: Read, Update", - "DataProcessor.Загрузка: @view" -] -``` - -### Объектная форма (для RLS и переопределений) - ```json { "name": "Document.Реализация", @@ -58,6 +95,9 @@ - `rights` — переопределения: dict `{"Right": true/false}` или массив `["Right1", "Right2"]` - `rls` — RLS-ограничения: `{"ИмяПрава": "текст условия"}` +Русские имена типов и прав тоже принимаются (`Справочник.Контрагенты: Чтение`), каноничная +форма — английская. + ## Пресеты — подробные таблицы Пресеты обозначаются `@` в строковом формате. В объектной форме ключ `preset` без `@`. @@ -88,127 +128,8 @@ | SessionParameter | Get, Set | | CommonAttribute | View, Edit | -Для сервисов (WebService, HTTPService, IntegrationService) пресеты не определены — используй явные права: `"WebService.Имя: Use"`. - -Если пресет не определён для типа объекта — предупреждение с подсказкой доступных. - -## Русские синонимы - -Скрипт автоматически транслирует русские имена в английские. Можно смешивать: `"Справочник.Контрагенты: Чтение, View"` — работает. - -### Типы объектов - -| Русский | English | -|---------|---------| -| `Справочник` | Catalog | -| `Документ` | Document | -| `РегистрСведений` | InformationRegister | -| `РегистрНакопления` | AccumulationRegister | -| `РегистрБухгалтерии` | AccountingRegister | -| `РегистрРасчета` | CalculationRegister | -| `Константа` | Constant | -| `ПланСчетов` | ChartOfAccounts | -| `ПланВидовХарактеристик` | ChartOfCharacteristicTypes | -| `ПланВидовРасчета` | ChartOfCalculationTypes | -| `ПланОбмена` | ExchangePlan | -| `БизнесПроцесс` | BusinessProcess | -| `Задача` | Task | -| `Обработка` | DataProcessor | -| `Отчет` | Report | -| `ОбщаяФорма` | CommonForm | -| `ОбщаяКоманда` | CommonCommand | -| `Подсистема` | Subsystem | -| `КритерийОтбора` | FilterCriterion | -| `ЖурналДокументов` | DocumentJournal | -| `Последовательность` | Sequence | -| `ВебСервис` | WebService | -| `HTTPСервис` | HTTPService | -| `СервисИнтеграции` | IntegrationService | -| `ПараметрСеанса` | SessionParameter | -| `ОбщийРеквизит` | CommonAttribute | -| `Конфигурация` | Configuration | -| `Перечисление` | Enum | - -### Вложенные типы - -| Русский | English | -|---------|---------| -| `Реквизит` | Attribute | -| `СтандартныйРеквизит` | StandardAttribute | -| `ТабличнаяЧасть` | TabularSection | -| `Измерение` | Dimension | -| `Ресурс` | Resource | -| `Команда` | Command | -| `РеквизитАдресации` | AddressingAttribute | - -### Права (основные) - -| Русский | English | -|---------|---------| -| `Чтение` | Read | -| `Добавление` | Insert | -| `Изменение` | Update | -| `Удаление` | Delete | -| `Просмотр` | View | -| `Редактирование` | Edit | -| `ВводПоСтроке` | InputByString | -| `Проведение` | Posting | -| `ОтменаПроведения` | UndoPosting | -| `Использование` | Use | -| `Получение` | Get | -| `Установка` | Set | -| `Старт` | Start | -| `Выполнение` | Execute | -| `УправлениеИтогами` | TotalsControl | - -### Права (интерактивные) - -| Русский | English | -|---------|---------| -| `ИнтерактивноеДобавление` | InteractiveInsert | -| `ИнтерактивнаяПометкаУдаления` | InteractiveSetDeletionMark | -| `ИнтерактивноеСнятиеПометкиУдаления` | InteractiveClearDeletionMark | -| `ИнтерактивноеУдаление` | InteractiveDelete | -| `ИнтерактивноеУдалениеПомеченных` | InteractiveDeleteMarked | -| `ИнтерактивноеПроведение` | InteractivePosting | -| `ИнтерактивноеПроведениеНеоперативное` | InteractivePostingRegular | -| `ИнтерактивнаяОтменаПроведения` | InteractiveUndoPosting | -| `ИнтерактивноеИзменениеПроведенных` | InteractiveChangeOfPosted | -| `ИнтерактивныйСтарт` | InteractiveStart | -| `ИнтерактивнаяАктивация` | InteractiveActivate | -| `ИнтерактивноеВыполнение` | InteractiveExecute | - -### Права (конфигурация) - -| Русский | English | -|---------|---------| -| `Администрирование` | Administration | -| `АдминистрированиеДанных` | DataAdministration | -| `ТонкийКлиент` | ThinClient | -| `ТолстыйКлиент` | ThickClient | -| `ВебКлиент` | WebClient | -| `МобильныйКлиент` | MobileClient | -| `ВнешнееСоединение` | ExternalConnection | -| `Вывод` | Output | -| `СохранениеДанныхПользователя` | SaveUserData | - -## Типы объектов без прав в ролях - -Следующие типы 1С **не могут** иметь права в ролях (не добавляются в `objects`): - -| Тип | Причина | -|-----|---------| -| Enum (Перечисление) | Права наследуются от конфигурации, явное назначение невозможно | -| CommonModule (ОбщийМодуль) | Не имеет собственных прав в роли | -| DefinedType (ОпределяемыйТип) | Тип данных, не объект прав | -| CommonPicture (ОбщаяКартинка) | Ресурс, не объект прав | -| CommonTemplate (ОбщийМакет) | Ресурс, не объект прав | -| Language (Язык) | Конфигурационный элемент | -| FunctionalOption (ФункциональнаяОпция) | Не объект прав | -| FunctionalOptionsParameter | Не объект прав | -| EventSubscription (ПодпискаНаСобытие) | Не объект прав | -| ScheduledJob (РегламентноеЗадание) | Не объект прав | -| StyleItem (ЭлементСтиля) | Ресурс оформления | +Для сервисов (WebService, HTTPService, IntegrationService) и внешних источников данных +пресеты не определены — задавай права явно: `"WebService.Имя: Use"`. ## Шаблоны ограничений (RLS templates) @@ -227,36 +148,7 @@ ## Примеры -### 1. Простая роль (только пресеты) - -```json -{ - "name": "ЧтениеНоменклатуры", - "synonym": "Чтение номенклатуры", - "objects": [ - "Catalog.Номенклатура: @view", - "Catalog.Контрагенты: @view", - "DataProcessor.Загрузка: @view" - ] -} -``` - -### 2. Роль для регламентного задания - -```json -{ - "name": "ОбновлениеЦен", - "synonym": "Обновление цен номенклатуры", - "objects": [ - "Catalog.Номенклатура: Read", - "Catalog.Валюты: Read", - "InformationRegister.ЦеныНоменклатуры: Read, Update", - "Constant.ОсновнаяВалюта: Read" - ] -} -``` - -### 3. Роль с RLS +### Роль с RLS ```json { @@ -281,22 +173,7 @@ } ``` -### 4. Роль с русскими синонимами - -```json -{ - "name": "ПросмотрДанных", - "synonym": "Просмотр данных", - "objects": [ - "Справочник.Контрагенты: @view", - "Документ.Реализация: Чтение, Просмотр", - "РегистрСведений.Цены: @edit", - "Обработка.ЗагрузкаДанных: @view" - ] -} -``` - -### 5. Роль с переопределением прав из пресета +### Роль с переопределением прав из пресета ```json { @@ -311,3 +188,16 @@ ] } ``` + +### Роль с правами на части объекта + +```json +{ + "name": "МенеджерБезЦен", + "synonym": "Менеджер без доступа к ценам", + "objects": [ + "Catalog.Номенклатура: @view", + { "name": "Catalog.Номенклатура.Attribute.Цена", "rights": { "View": false, "Edit": false } } + ] +} +``` diff --git a/.claude/skills/role-compile/scripts/role-compile.ps1 b/.claude/skills/role-compile/scripts/role-compile.ps1 index 189853ac..3aab0568 100644 --- a/.claude/skills/role-compile/scripts/role-compile.ps1 +++ b/.claude/skills/role-compile/scripts/role-compile.ps1 @@ -1,4 +1,4 @@ -# role-compile v1.24 — Compile 1C role from JSON (+русские алиасы типов: формы с ё и без) +# role-compile v1.25 — Compile 1C role from JSON (+русские алиасы типов: формы с ё и без) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -217,7 +217,24 @@ $script:typeAliases = @{ "ПараметрСеанса" = "SessionParameter" "ОбщийРеквизит" = "CommonAttribute" "Конфигурация" = "Configuration" + "ВнешнийИсточникДанных" = "ExternalDataSource" + # Типы без прав в ролях: алиасы нужны не ради генерации, а ради отказа по делу — + # иначе на русскую запись навык ответит «неизвестный тип 'ОбщийМодуль'». "Перечисление" = "Enum" + "ОбщийМодуль" = "CommonModule" + "ОпределяемыйТип" = "DefinedType" + "ОбщаяКартинка" = "CommonPicture" + "ОбщийМакет" = "CommonTemplate" + "Язык" = "Language" + "ФункциональнаяОпция" = "FunctionalOption" + "ПараметрФункциональныхОпций" = "FunctionalOptionsParameter" + "ПодпискаНаСобытие" = "EventSubscription" + "РегламентноеЗадание" = "ScheduledJob" + "ЭлементСтиля" = "StyleItem" + "ХранилищеНастроек" = "SettingsStorage" + "ПакетXDTO" = "XDTOPackage" + "WSСсылка" = "WSReference" + "Нумератор" = "DocumentNumerator" # Nested "Реквизит" = "Attribute" "СтандартныйРеквизит" = "StandardAttribute" @@ -403,11 +420,72 @@ $script:knownRights = @{ "IntegrationService" = @("Use") "SessionParameter" = @("Get","Set") "CommonAttribute" = @("View","Edit") + "ExternalDataSource" = @( + "Use","Administration","StandardAuthenticationChange", + "SessionStandardAuthenticationChange","SessionOSAuthenticationChange" + ) } -# Nested objects: Attribute, StandardAttribute, TabularSection, Dimension, Resource, AddressingAttribute -$script:nestedRights = @("View","Edit") -$script:commandRights = @("View") +# Виды вложенности (предпоследний сегмент пути) → допустимые права. Списки сняты с корпуса +# типовых конфигураций и с выгрузки роли, где права проставлены по всему дереву редактора: +# догадкам тут не место — закрытый список превращает промах в ложный отказ. +$script:nestedKindRights = @{ + "Attribute" = @("View","Edit") + "StandardAttribute" = @("View","Edit") + "TabularSection" = @("View","Edit") + "StandardTabularSection" = @("View","Edit") + "Dimension" = @("View","Edit") + "Resource" = @("View","Edit") + "AccountingFlag" = @("View","Edit") + "ExtDimensionAccountingFlag" = @("View","Edit") + "AddressingAttribute" = @("View","Edit") + "Field" = @("View","Edit") + "Command" = @("View") + "Subsystem" = @("View") + "Operation" = @("Use") + "Method" = @("Use") + "IntegrationServiceChannel" = @("Use") + "Recalculation" = @("Read","Update") + "Cube" = @("Read","View") + "DimensionTable" = @("Read","View") + "Function" = @("Use","View") + "Table" = @( + "Read","Insert","Update","Delete","View","Edit","InputByString", + "InteractiveInsert","InteractiveDelete" + ) +} + +# Виды, существующие только у одного типа-родителя: без этой привязки +# `Catalog.Товары.Field.Цена` прошёл бы как валидный вложенный объект. +$script:kindOwners = @{ + "Table" = "ExternalDataSource" + "Cube" = "ExternalDataSource" + "Function" = "ExternalDataSource" + "Field" = "ExternalDataSource" + "DimensionTable" = "ExternalDataSource" + "Recalculation" = "CalculationRegister" + "Operation" = "WebService" + "Method" = "HTTPService" + "IntegrationServiceChannel" = "IntegrationService" +} + +# Один и тот же вид под разными родителями имеет разный набор: измерение регистра — +# View + Edit, измерение куба внешнего источника — только View. Объединять нельзя, +# объединение молча разрешило бы Edit там, где платформа его не даёт. +$script:nestedKindRightsByType = @{ + "ExternalDataSource" = @{ + "Dimension" = @("View") + "Resource" = @("View") + } +} + +# Типы без прав в ролях (в дереве редактора ролей их нет). Таблица НЕ управляет поведением — +# отказ даёт отсутствие типа в $knownRights; здесь только причина для сообщения. +$script:noRightsTypes = @( + "Enum","CommonModule","DefinedType","CommonPicture","CommonTemplate","Language", + "FunctionalOption","FunctionalOptionsParameter","EventSubscription","ScheduledJob", + "StyleItem","Style","SettingsStorage","XDTOPackage","WSReference","DocumentNumerator" +) # --- 4. Presets (@view, @edit) --- @@ -472,6 +550,66 @@ function Is-NestedObject { return ($objectName.Split(".").Count -ge 3) } +# Вид вложенности — предпоследний сегмент: путь бывает и восьмисегментным +# (ExternalDataSource.И.Cube.К.DimensionTable.Т.Field.П), считать от конца. +function Get-NestedKind { + param([string]$objectName) + $parts = $objectName.Split(".") + if ($parts.Count -lt 3) { return $null } + return $parts[$parts.Count - 2] +} + +function Get-NestedRights { + param([string]$objectType, [string]$kind) + if ($script:nestedKindRightsByType.ContainsKey($objectType) -and + $script:nestedKindRightsByType[$objectType].ContainsKey($kind)) { + return @($script:nestedKindRightsByType[$objectType][$kind]) + } + if ($script:nestedKindRights.ContainsKey($kind)) { return @($script:nestedKindRights[$kind]) } + return $null +} + +# Отказ копится, а не печатается сразу: роль пишется целиком, поэтому единственный +# безопасный момент отказа — до первой записи, и показать надо все причины сразу. +$script:validationErrors = @() + +function Add-ValidationError { + param([string]$message) + $script:validationErrors += $message +} + +# Проверка имени объекта: тип по белому списку (всегда, включая вложенные пути) и вид +# вложенности. Запрещённый и незнакомый тип — разные диагнозы. +function Validate-ObjectName { + param([string]$objectName) + + $objectType = Get-ObjectType $objectName + if (-not $script:knownRights.ContainsKey($objectType)) { + if ($script:noRightsTypes -contains $objectType) { + Add-ValidationError "${objectName}: тип '$objectType' не имеет прав в роли — уберите объект из списка" + } else { + $similar = @($script:knownRights.Keys | Where-Object { $_ -like "*$objectType*" -or $objectType -like "*$_*" }) + $sug = if ($similar.Count -gt 0) { " Возможно: $(($similar | Select-Object -First 3) -join ', ')?" } else { "" } + Add-ValidationError "${objectName}: неизвестный тип объекта '$objectType'.$sug" + } + return $false + } + + if (Is-NestedObject $objectName) { + $kind = Get-NestedKind $objectName + if ($script:kindOwners.ContainsKey($kind) -and $objectType -ne $script:kindOwners[$kind]) { + Add-ValidationError "${objectName}: вид '$kind' бывает только у $($script:kindOwners[$kind])" + return $false + } + if ($null -eq (Get-NestedRights $objectType $kind)) { + Add-ValidationError "${objectName}: неизвестный вид вложенности '$kind'" + return $false + } + } + + return $true +} + function Resolve-Preset { param([string]$objectType, [string]$presetName) @@ -503,23 +641,18 @@ function Validate-RightName { $objectType = Get-ObjectType $objectName - if (Is-NestedObject $objectName) { - if ($objectName -match '\.Command\.') { - if ($rightName -notin $script:commandRights) { - Write-Warning "${objectName}: '$rightName' not valid for commands (only: View)" - return $false - } - } else { - if ($rightName -notin $script:nestedRights) { - Write-Warning "${objectName}: '$rightName' not valid for nested objects (only: View, Edit)" - return $false - } - } - return $true - } + # Тип уже проверен Validate-ObjectName — здесь только права, иначе про один + # запрещённый тип напечатается столько строк, сколько у него перечислено прав. + if (-not $script:knownRights.ContainsKey($objectType)) { return $false } - if (-not $script:knownRights.ContainsKey($objectType)) { - Write-Warning "${objectName}: unknown object type '$objectType'" + if (Is-NestedObject $objectName) { + $kind = Get-NestedKind $objectName + $validNested = Get-NestedRights $objectType $kind + if ($null -eq $validNested) { return $false } + if ($rightName -notin $validNested) { + Add-ValidationError "${objectName}: право '$rightName' недопустимо для вида '$kind' (допустимо: $($validNested -join ', '))" + return $false + } return $true } @@ -528,8 +661,8 @@ function Validate-RightName { $suggestions = @($validRights | Where-Object { $_ -like "*$rightName*" -or $rightName -like "*$_*" }) - $sugStr = if ($suggestions.Count -gt 0) { " Did you mean: $($suggestions -join ', ')?" } else { "" } - Write-Warning "${objectName}: unknown right '$rightName'.$sugStr" + $sugStr = if ($suggestions.Count -gt 0) { " Возможно: $(($suggestions | Select-Object -First 3) -join ', ')?" } else { "" } + Add-ValidationError "${objectName}: право '$rightName' не существует у типа '$objectType'.$sugStr" return $false } @@ -550,6 +683,9 @@ function Parse-ObjectEntry { } $objName = Translate-ObjectName ($entry.Substring(0, $colonIdx).Trim()) $rightsStr = $entry.Substring($colonIdx + 1).Trim() + # Объект с непроходным именем дальше не разбираем: пресет для несуществующего типа + # добавил бы к отказу ещё и бессмысленное предупреждение. + if (-not (Validate-ObjectName $objName)) { return $null } $objectType = Get-ObjectType $objName if ($rightsStr.StartsWith('@')) { @@ -575,6 +711,8 @@ function Parse-ObjectEntry { return $null } + if (-not (Validate-ObjectName $objName)) { return $null } + $objectType = Get-ObjectType $objName $rightsMap = [ordered]@{} @@ -648,6 +786,16 @@ if ($def.objects) { } } +# Отказ ДО записи: роль пишется тремя файлами (метаданные, права, регистрация в +# Configuration.xml), и частично записанная роль хуже отсутствующей. Печатаем все причины +# разом — иначе пользователь чинит их по одной. Console.Error, а не Write-Error: под +# ErrorActionPreference=Stop последний бросает исключение (см. комментарий в support-guard). +if ($script:validationErrors.Count -gt 0) { + [Console]::Error.WriteLine("[role-compile] Роль не создана: $($script:validationErrors.Count) ошибок в описании прав.") + foreach ($e in $script:validationErrors) { [Console]::Error.WriteLine(" ERROR: $e") } + exit 1 +} + # --- Detect format version --- function Detect-FormatVersion([string]$dir) { diff --git a/.claude/skills/role-compile/scripts/role-compile.py b/.claude/skills/role-compile/scripts/role-compile.py index c769e54b..fed2b41b 100644 --- a/.claude/skills/role-compile/scripts/role-compile.py +++ b/.claude/skills/role-compile/scripts/role-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# role-compile v1.24 — Compile 1C role from JSON (+русские алиасы типов: формы с ё и без) +# role-compile v1.25 — Compile 1C role from JSON (+русские алиасы типов: формы с ё и без) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -354,7 +354,24 @@ TYPE_ALIASES = { "ПараметрСеанса": "SessionParameter", "ОбщийРеквизит": "CommonAttribute", "Конфигурация": "Configuration", + "ВнешнийИсточникДанных": "ExternalDataSource", + # Типы без прав в ролях: алиасы нужны не ради генерации, а ради отказа по делу — + # иначе на русскую запись навык ответит «неизвестный тип 'ОбщийМодуль'». "Перечисление": "Enum", + "ОбщийМодуль": "CommonModule", + "ОпределяемыйТип": "DefinedType", + "ОбщаяКартинка": "CommonPicture", + "ОбщийМакет": "CommonTemplate", + "Язык": "Language", + "ФункциональнаяОпция": "FunctionalOption", + "ПараметрФункциональныхОпций": "FunctionalOptionsParameter", + "ПодпискаНаСобытие": "EventSubscription", + "РегламентноеЗадание": "ScheduledJob", + "ЭлементСтиля": "StyleItem", + "ХранилищеНастроек": "SettingsStorage", + "ПакетXDTO": "XDTOPackage", + "WSСсылка": "WSReference", + "Нумератор": "DocumentNumerator", # Nested "Реквизит": "Attribute", "СтандартныйРеквизит": "StandardAttribute", @@ -516,10 +533,72 @@ KNOWN_RIGHTS = { "IntegrationService": ["Use"], "SessionParameter": ["Get", "Set"], "CommonAttribute": ["View", "Edit"], + "ExternalDataSource": [ + "Use", "Administration", "StandardAuthenticationChange", + "SessionStandardAuthenticationChange", "SessionOSAuthenticationChange", + ], } -NESTED_RIGHTS = ["View", "Edit"] -COMMAND_RIGHTS = ["View"] +# Виды вложенности (предпоследний сегмент пути) → допустимые права. Списки сняты с корпуса +# типовых конфигураций и с выгрузки роли, где права проставлены по всему дереву редактора: +# догадкам тут не место — закрытый список превращает промах в ложный отказ. +NESTED_KIND_RIGHTS = { + "Attribute": ["View", "Edit"], + "StandardAttribute": ["View", "Edit"], + "TabularSection": ["View", "Edit"], + "StandardTabularSection": ["View", "Edit"], + "Dimension": ["View", "Edit"], + "Resource": ["View", "Edit"], + "AccountingFlag": ["View", "Edit"], + "ExtDimensionAccountingFlag": ["View", "Edit"], + "AddressingAttribute": ["View", "Edit"], + "Field": ["View", "Edit"], + "Command": ["View"], + "Subsystem": ["View"], + "Operation": ["Use"], + "Method": ["Use"], + "IntegrationServiceChannel": ["Use"], + "Recalculation": ["Read", "Update"], + "Cube": ["Read", "View"], + "DimensionTable": ["Read", "View"], + "Function": ["Use", "View"], + "Table": [ + "Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString", + "InteractiveInsert", "InteractiveDelete", + ], +} + +# Виды, существующие только у одного типа-родителя: без этой привязки +# `Catalog.Товары.Field.Цена` прошёл бы как валидный вложенный объект. +KIND_OWNERS = { + 'Table': 'ExternalDataSource', + 'Cube': 'ExternalDataSource', + 'Function': 'ExternalDataSource', + 'Field': 'ExternalDataSource', + 'DimensionTable': 'ExternalDataSource', + 'Recalculation': 'CalculationRegister', + 'Operation': 'WebService', + 'Method': 'HTTPService', + 'IntegrationServiceChannel': 'IntegrationService', +} + +# Один и тот же вид под разными родителями имеет разный набор: измерение регистра — +# View + Edit, измерение куба внешнего источника — только View. Объединять нельзя, +# объединение молча разрешило бы Edit там, где платформа его не даёт. +NESTED_KIND_RIGHTS_BY_TYPE = { + "ExternalDataSource": { + "Dimension": ["View"], + "Resource": ["View"], + }, +} + +# Типы без прав в ролях (в дереве редактора ролей их нет). Список НЕ управляет поведением — +# отказ даёт отсутствие типа в KNOWN_RIGHTS; здесь только выбор формулировки. +NO_RIGHTS_TYPES = [ + "Enum", "CommonModule", "DefinedType", "CommonPicture", "CommonTemplate", "Language", + "FunctionalOption", "FunctionalOptionsParameter", "EventSubscription", "ScheduledJob", + "StyleItem", "Style", "SettingsStorage", "XDTOPackage", "WSReference", "DocumentNumerator", +] # --- Presets --- @@ -594,6 +673,56 @@ def is_nested_object(object_name): return len(object_name.split('.')) >= 3 +def get_nested_kind(object_name): + """Вид вложенности — предпоследний сегмент: путь бывает и восьмисегментным + (ExternalDataSource.И.Cube.К.DimensionTable.Т.Field.П), считать от конца.""" + parts = object_name.split('.') + if len(parts) < 3: + return None + return parts[-2] + + +def get_nested_rights(object_type, kind): + by_type = NESTED_KIND_RIGHTS_BY_TYPE.get(object_type) + if by_type and kind in by_type: + return by_type[kind] + return NESTED_KIND_RIGHTS.get(kind) + + +# Отказ копится, а не печатается сразу: роль пишется целиком, поэтому единственный +# безопасный момент отказа — до первой записи, и показать надо все причины сразу. +VALIDATION_ERRORS = [] + + +def add_validation_error(message): + VALIDATION_ERRORS.append(message) + + +def validate_object_name(object_name): + """Тип по белому списку (всегда, включая вложенные пути) и вид вложенности. + Запрещённый и незнакомый тип — разные диагнозы.""" + object_type = get_object_type(object_name) + if object_type not in KNOWN_RIGHTS: + if object_type in NO_RIGHTS_TYPES: + add_validation_error(f"{object_name}: тип '{object_type}' не имеет прав в роли — уберите объект из списка") + else: + similar = [t for t in KNOWN_RIGHTS if object_type in t or t in object_type][:3] + sug = f" Возможно: {', '.join(similar)}?" if similar else '' + add_validation_error(f"{object_name}: неизвестный тип объекта '{object_type}'.{sug}") + return False + + if is_nested_object(object_name): + kind = get_nested_kind(object_name) + if kind in KIND_OWNERS and object_type != KIND_OWNERS[kind]: + add_validation_error(f"{object_name}: вид '{kind}' бывает только у {KIND_OWNERS[kind]}") + return False + if get_nested_rights(object_type, kind) is None: + add_validation_error(f"{object_name}: неизвестный вид вложенности '{kind}'") + return False + + return True + + def resolve_preset(object_type, preset_name): preset = preset_name.lstrip('@') if preset not in PRESETS: @@ -614,26 +743,26 @@ def resolve_preset(object_type, preset_name): def validate_right_name(object_name, right_name): object_type = get_object_type(object_name) - if is_nested_object(object_name): - if '.Command.' in object_name: - if right_name not in COMMAND_RIGHTS: - print(f"WARNING: {object_name}: '{right_name}' not valid for commands (only: View)", file=sys.stderr) - return False - else: - if right_name not in NESTED_RIGHTS: - print(f"WARNING: {object_name}: '{right_name}' not valid for nested objects (only: View, Edit)", file=sys.stderr) - return False - return True - + # Тип уже проверен validate_object_name — здесь только права, иначе про один + # запрещённый тип напечатается столько строк, сколько у него перечислено прав. if object_type not in KNOWN_RIGHTS: - print(f"WARNING: {object_name}: unknown object type '{object_type}'", file=sys.stderr) + return False + + if is_nested_object(object_name): + kind = get_nested_kind(object_name) + valid_nested = get_nested_rights(object_type, kind) + if valid_nested is None: + return False + if right_name not in valid_nested: + add_validation_error(f"{object_name}: право '{right_name}' недопустимо для вида '{kind}' (допустимо: {', '.join(valid_nested)})") + return False return True valid_rights = KNOWN_RIGHTS[object_type] if right_name not in valid_rights: - suggestions = [r for r in valid_rights if right_name in r or r in right_name] - sug_str = f" Did you mean: {', '.join(suggestions)}?" if suggestions else "" - print(f"WARNING: {object_name}: unknown right '{right_name}'.{sug_str}", file=sys.stderr) + suggestions = [r for r in valid_rights if right_name in r or r in right_name][:3] + sug_str = f" Возможно: {', '.join(suggestions)}?" if suggestions else "" + add_validation_error(f"{object_name}: право '{right_name}' не существует у типа '{object_type}'.{sug_str}") return False return True @@ -648,6 +777,10 @@ def parse_object_entry(entry): return None obj_name = translate_object_name(entry[:colon_idx].strip()) rights_str = entry[colon_idx + 1:].strip() + # Объект с непроходным именем дальше не разбираем: пресет для несуществующего типа + # добавил бы к отказу ещё и бессмысленное предупреждение. + if not validate_object_name(obj_name): + return None object_type = get_object_type(obj_name) if rights_str.startswith('@'): @@ -668,6 +801,9 @@ def parse_object_entry(entry): print("WARNING: Object entry missing 'name' field", file=sys.stderr) return None + if not validate_object_name(obj_name): + return None + object_type = get_object_type(obj_name) # Use a list of tuples to preserve insertion order rights_map = {} # name -> {Value, Condition} @@ -761,6 +897,15 @@ def main(): if parsed: parsed_objects.append(parsed) + # Отказ ДО записи: роль пишется тремя файлами (метаданные, права, регистрация в + # Configuration.xml), и частично записанная роль хуже отсутствующей. Печатаем все + # причины разом — иначе пользователь чинит их по одной. + if VALIDATION_ERRORS: + print(f"[role-compile] Роль не создана: {len(VALIDATION_ERRORS)} ошибок в описании прав.", file=sys.stderr) + for e in VALIDATION_ERRORS: + print(f" ERROR: {e}", file=sys.stderr) + sys.exit(1) + # --- 3. Generate UUID --- uid = new_uuid() diff --git a/.claude/skills/role-validate/SKILL.md b/.claude/skills/role-validate/SKILL.md index f3dab146..166b8cc0 100644 --- a/.claude/skills/role-validate/SKILL.md +++ b/.claude/skills/role-validate/SKILL.md @@ -25,3 +25,14 @@ allowed-tools: ```powershell powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-validate.ps1" -RightsPath "Roles/МояРоль" ``` + +## Что считается ошибкой + +Ошибка (код возврата 1), а не предупреждение: + +- тип объекта, не имеющий прав в ролях (`Enum`, `CommonModule`, `CommonPicture`, `ScheduledJob` и т.п.) — конфигурация с блоком прав на перечисление не загружается в базу, конфигуратор зависает без сообщений; +- незнакомый тип объекта (опечатка); +- имя права, которого у этого типа нет; +- вид вложенности, невозможный у данного типа, и недопустимое право у вложенного объекта. + +Полные списки «тип → права» и виды вложенности — в `role-compile/dsl-reference.md`. diff --git a/.claude/skills/role-validate/scripts/role-validate.ps1 b/.claude/skills/role-validate/scripts/role-validate.ps1 index fdac6ebe..c7515bda 100644 --- a/.claude/skills/role-validate/scripts/role-validate.ps1 +++ b/.claude/skills/role-validate/scripts/role-validate.ps1 @@ -1,4 +1,4 @@ -# role-validate v1.2 — Validate 1C role structure +# role-validate v1.3 — Validate 1C role structure # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -127,11 +127,72 @@ $script:knownRights = @{ "IntegrationService" = @("Use") "SessionParameter" = @("Get","Set") "CommonAttribute" = @("View","Edit") + "ExternalDataSource" = @( + "Use","Administration","StandardAuthenticationChange", + "SessionStandardAuthenticationChange","SessionOSAuthenticationChange" + ) } -$script:nestedRights = @("View","Edit") -$script:channelRights = @("Use") -$script:commandRights = @("View") +# Виды вложенности (предпоследний сегмент пути) → допустимые права. Списки сняты с корпуса +# типовых конфигураций и с выгрузки роли, где права проставлены по всему дереву редактора: +# догадкам тут не место — закрытый список превращает промах в ложный отказ. +$script:nestedKindRights = @{ + "Attribute" = @("View","Edit") + "StandardAttribute" = @("View","Edit") + "TabularSection" = @("View","Edit") + "StandardTabularSection" = @("View","Edit") + "Dimension" = @("View","Edit") + "Resource" = @("View","Edit") + "AccountingFlag" = @("View","Edit") + "ExtDimensionAccountingFlag" = @("View","Edit") + "AddressingAttribute" = @("View","Edit") + "Field" = @("View","Edit") + "Command" = @("View") + "Subsystem" = @("View") + "Operation" = @("Use") + "Method" = @("Use") + "IntegrationServiceChannel" = @("Use") + "Recalculation" = @("Read","Update") + "Cube" = @("Read","View") + "DimensionTable" = @("Read","View") + "Function" = @("Use","View") + "Table" = @( + "Read","Insert","Update","Delete","View","Edit","InputByString", + "InteractiveInsert","InteractiveDelete" + ) +} + +# Виды, существующие только у одного типа-родителя: без этой привязки +# `Catalog.Товары.Field.Цена` прошёл бы как валидный вложенный объект. +$script:kindOwners = @{ + "Table" = "ExternalDataSource" + "Cube" = "ExternalDataSource" + "Function" = "ExternalDataSource" + "Field" = "ExternalDataSource" + "DimensionTable" = "ExternalDataSource" + "Recalculation" = "CalculationRegister" + "Operation" = "WebService" + "Method" = "HTTPService" + "IntegrationServiceChannel" = "IntegrationService" +} + +# Один и тот же вид под разными родителями имеет разный набор: измерение регистра — +# View + Edit, измерение куба внешнего источника — только View. Объединять нельзя, +# объединение молча разрешило бы Edit там, где платформа его не даёт. +$script:nestedKindRightsByType = @{ + "ExternalDataSource" = @{ + "Dimension" = @("View") + "Resource" = @("View") + } +} + +# Типы без прав в ролях (в дереве редактора ролей их нет). Таблица НЕ управляет поведением — +# отказ даёт отсутствие типа в $knownRights; здесь только причина для сообщения. +$script:noRightsTypes = @( + "Enum","CommonModule","DefinedType","CommonPicture","CommonTemplate","Language", + "FunctionalOption","FunctionalOptionsParameter","EventSubscription","ScheduledJob", + "StyleItem","Style","SettingsStorage","XDTOPackage","WSReference","DocumentNumerator" +) # --- 2. Output helpers --- @@ -179,6 +240,37 @@ function Is-NestedObject { return ($name.Split(".").Count -ge 3) } +# Вид вложенности — предпоследний сегмент: путь бывает и восьмисегментным +# (ExternalDataSource.И.Cube.К.DimensionTable.Т.Field.П), считать от конца. +function Get-NestedKind { + param([string]$name) + $parts = $name.Split(".") + if ($parts.Count -lt 3) { return $null } + return $parts[$parts.Count - 2] +} + +function Get-NestedRights { + param([string]$objectType, [string]$kind) + if ($script:nestedKindRightsByType.ContainsKey($objectType) -and + $script:nestedKindRightsByType[$objectType].ContainsKey($kind)) { + return @($script:nestedKindRightsByType[$objectType][$kind]) + } + if ($script:nestedKindRights.ContainsKey($kind)) { return @($script:nestedKindRights[$kind]) } + return $null +} + +# Запрещённый тип и незнакомый тип — разные диагнозы: первый документирован, второй +# скорее опечатка или пробел в таблице. +function Format-TypeError { + param([string]$objName, [string]$objectType) + if ($script:noRightsTypes -contains $objectType) { + return "${objName}: тип '$objectType' не имеет прав в роли — блок нужно удалить" + } + $similar = Find-Similar -needle $objectType -haystack @($script:knownRights.Keys) + $sug = if ($similar.Count -gt 0) { " Возможно: $($similar -join ', ')?" } else { "" } + return "${objName}: неизвестный тип объекта '$objectType'.$sug" +} + function Find-Similar { param([string]$needle, [string[]]$haystack) $result = @($haystack | Where-Object { @@ -303,9 +395,16 @@ foreach ($obj in $objects) { $objectType = Get-ObjectType $objName $isNested = Is-NestedObject $objName - # Check object type is known - if (-not $isNested -and -not $script:knownRights.ContainsKey($objectType)) { - Report-Warn "${objName}: unknown object type '$objectType'" + # Тип проверяется ВСЕГДА, включая вложенные пути: раньше `Enum.Х.Attribute.Y` не проверялся + # вообще, потому что ветка «unknown» стояла под `-not $isNested`. + $nestedKind = if ($isNested) { Get-NestedKind $objName } else { $null } + $typeKnown = $script:knownRights.ContainsKey($objectType) + if (-not $typeKnown) { + Report-Error (Format-TypeError $objName $objectType) + } elseif ($isNested -and $script:kindOwners.ContainsKey($nestedKind) -and $objectType -ne $script:kindOwners[$nestedKind]) { + Report-Error "${objName}: вид '$nestedKind' бывает только у $($script:kindOwners[$nestedKind])" + } elseif ($isNested -and $null -eq (Get-NestedRights $objectType $nestedKind)) { + Report-Error "${objName}: неизвестный вид вложенности '$nestedKind'" } # Check rights @@ -345,27 +444,20 @@ foreach ($obj in $objects) { $rightCount++ - # Validate right name - if ($isNested) { - if ($objName -match '\.Command\.') { - if ($rName -notin $script:commandRights) { - Report-Warn "${objName}: '$rName' not valid for commands (only: View)" - } - } elseif ($objName -match '\.IntegrationServiceChannel\.') { - if ($rName -notin $script:channelRights) { - Report-Warn "${objName}: '$rName' not valid for channels (only: Use)" - } - } else { - if ($rName -notin $script:nestedRights) { - Report-Warn "${objName}: '$rName' not valid for nested objects (only: View, Edit)" - } + # Validate right name. Тип уже отвергнут выше — второй раз про него не пишем. + if (-not $typeKnown) { + # уже сообщено + } elseif ($isNested) { + $validNested = Get-NestedRights $objectType $nestedKind + if ($null -ne $validNested -and $rName -notin $validNested) { + Report-Error "${objName}: право '$rName' недопустимо для вида '$nestedKind' (допустимо: $($validNested -join ', '))" } - } elseif ($script:knownRights.ContainsKey($objectType)) { + } else { $validRights = $script:knownRights[$objectType] if ($rName -notin $validRights) { $similar = Find-Similar -needle $rName -haystack $validRights - $sugStr = if ($similar.Count -gt 0) { " Did you mean: $($similar -join ', ')?" } else { "" } - Report-Warn "${objName}: unknown right '$rName'.$sugStr" + $sugStr = if ($similar.Count -gt 0) { " Возможно: $($similar -join ', ')?" } else { "" } + Report-Error "${objName}: право '$rName' не существует у типа '$objectType'.$sugStr" } } } diff --git a/.claude/skills/role-validate/scripts/role-validate.py b/.claude/skills/role-validate/scripts/role-validate.py index 2836998a..93fe1915 100644 --- a/.claude/skills/role-validate/scripts/role-validate.py +++ b/.claude/skills/role-validate/scripts/role-validate.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# role-validate v1.2 — Validate 1C role Rights.xml structure +# role-validate v1.3 — Validate 1C role Rights.xml structure # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """Validates role Rights.xml: root element, global flags, objects, rights, RLS, templates.""" import sys, os, argparse, re @@ -145,11 +145,72 @@ KNOWN_RIGHTS = { 'IntegrationService': ['Use'], 'SessionParameter': ['Get', 'Set'], 'CommonAttribute': ['View', 'Edit'], + 'ExternalDataSource': [ + 'Use', 'Administration', 'StandardAuthenticationChange', + 'SessionStandardAuthenticationChange', 'SessionOSAuthenticationChange', + ], } -NESTED_RIGHTS = ['View', 'Edit'] -CHANNEL_RIGHTS = ['Use'] -COMMAND_RIGHTS = ['View'] +# Виды вложенности (предпоследний сегмент пути) → допустимые права. Списки сняты с корпуса +# типовых конфигураций и с выгрузки роли, где права проставлены по всему дереву редактора: +# догадкам тут не место — закрытый список превращает промах в ложный отказ. +NESTED_KIND_RIGHTS = { + 'Attribute': ['View', 'Edit'], + 'StandardAttribute': ['View', 'Edit'], + 'TabularSection': ['View', 'Edit'], + 'StandardTabularSection': ['View', 'Edit'], + 'Dimension': ['View', 'Edit'], + 'Resource': ['View', 'Edit'], + 'AccountingFlag': ['View', 'Edit'], + 'ExtDimensionAccountingFlag': ['View', 'Edit'], + 'AddressingAttribute': ['View', 'Edit'], + 'Field': ['View', 'Edit'], + 'Command': ['View'], + 'Subsystem': ['View'], + 'Operation': ['Use'], + 'Method': ['Use'], + 'IntegrationServiceChannel': ['Use'], + 'Recalculation': ['Read', 'Update'], + 'Cube': ['Read', 'View'], + 'DimensionTable': ['Read', 'View'], + 'Function': ['Use', 'View'], + 'Table': [ + 'Read', 'Insert', 'Update', 'Delete', 'View', 'Edit', 'InputByString', + 'InteractiveInsert', 'InteractiveDelete', + ], +} + +# Виды, существующие только у одного типа-родителя: без этой привязки +# `Catalog.Товары.Field.Цена` прошёл бы как валидный вложенный объект. +KIND_OWNERS = { + 'Table': 'ExternalDataSource', + 'Cube': 'ExternalDataSource', + 'Function': 'ExternalDataSource', + 'Field': 'ExternalDataSource', + 'DimensionTable': 'ExternalDataSource', + 'Recalculation': 'CalculationRegister', + 'Operation': 'WebService', + 'Method': 'HTTPService', + 'IntegrationServiceChannel': 'IntegrationService', +} + +# Один и тот же вид под разными родителями имеет разный набор: измерение регистра — +# View + Edit, измерение куба внешнего источника — только View. Объединять нельзя, +# объединение молча разрешило бы Edit там, где платформа его не даёт. +NESTED_KIND_RIGHTS_BY_TYPE = { + 'ExternalDataSource': { + 'Dimension': ['View'], + 'Resource': ['View'], + }, +} + +# Типы без прав в ролях (в дереве редактора ролей их нет). Таблица НЕ управляет поведением — +# отказ даёт отсутствие типа в KNOWN_RIGHTS; здесь только причина для сообщения. +NO_RIGHTS_TYPES = [ + 'Enum', 'CommonModule', 'DefinedType', 'CommonPicture', 'CommonTemplate', 'Language', + 'FunctionalOption', 'FunctionalOptionsParameter', 'EventSubscription', 'ScheduledJob', + 'StyleItem', 'Style', 'SettingsStorage', 'XDTOPackage', 'WSReference', 'DocumentNumerator', +] def get_object_type(name): @@ -163,6 +224,22 @@ def is_nested_object(name): return name.count('.') >= 2 +def get_nested_kind(name): + """Вид вложенности — предпоследний сегмент: путь бывает и восьмисегментным + (ExternalDataSource.И.Cube.К.DimensionTable.Т.Field.П), считать от конца.""" + parts = name.split('.') + if len(parts) < 3: + return None + return parts[-2] + + +def get_nested_rights(object_type, kind): + by_type = NESTED_KIND_RIGHTS_BY_TYPE.get(object_type) + if by_type and kind in by_type: + return by_type[kind] + return NESTED_KIND_RIGHTS.get(kind) + + def find_similar(needle, haystack): result = [] needle_lower = needle.lower() @@ -175,6 +252,16 @@ def find_similar(needle, haystack): return result +def format_type_error(obj_name, object_type): + """Запрещённый тип и незнакомый тип — разные диагнозы: первый документирован, + второй скорее опечатка или пробел в таблице.""" + if object_type in NO_RIGHTS_TYPES: + return f"{obj_name}: тип '{object_type}' не имеет прав в роли — блок нужно удалить" + similar = find_similar(object_type, list(KNOWN_RIGHTS.keys())) + sug = f" Возможно: {', '.join(similar)}?" if similar else '' + return f"{obj_name}: неизвестный тип объекта '{object_type}'.{sug}" + + def get_child_text(parent, local_name, ns): """Get text of first child element with given local name in namespace.""" for child in parent: @@ -339,9 +426,16 @@ def main(): object_type = get_object_type(obj_name) is_nested = is_nested_object(obj_name) - # Check object type is known - if not is_nested and object_type not in KNOWN_RIGHTS: - report_warn(f"{obj_name}: unknown object type '{object_type}'") + # Тип проверяется ВСЕГДА, включая вложенные пути: раньше `Enum.Х.Attribute.Y` + # не проверялся вообще — ветка «unknown» стояла под `not is_nested`. + nested_kind = get_nested_kind(obj_name) if is_nested else None + type_known = object_type in KNOWN_RIGHTS + if not type_known: + report_error(format_type_error(obj_name, object_type)) + elif is_nested and nested_kind in KIND_OWNERS and object_type != KIND_OWNERS[nested_kind]: + report_error(f"{obj_name}: вид '{nested_kind}' бывает только у {KIND_OWNERS[nested_kind]}") + elif is_nested and get_nested_rights(object_type, nested_kind) is None: + report_error(f"{obj_name}: неизвестный вид вложенности '{nested_kind}'") # Check rights for child in obj: @@ -384,23 +478,19 @@ def main(): right_count += 1 - # Validate right name - if is_nested: - if '.Command.' in obj_name: - if r_name not in COMMAND_RIGHTS: - report_warn(f"{obj_name}: '{r_name}' not valid for commands (only: View)") - elif '.IntegrationServiceChannel.' in obj_name: - if r_name not in CHANNEL_RIGHTS: - report_warn(f"{obj_name}: '{r_name}' not valid for channels (only: Use)") - else: - if r_name not in NESTED_RIGHTS: - report_warn(f"{obj_name}: '{r_name}' not valid for nested objects (only: View, Edit)") - elif object_type in KNOWN_RIGHTS: + # Validate right name. Тип уже отвергнут выше — второй раз про него не пишем. + if not type_known: + pass + elif is_nested: + valid_nested = get_nested_rights(object_type, nested_kind) + if valid_nested is not None and r_name not in valid_nested: + report_error(f"{obj_name}: право '{r_name}' недопустимо для вида '{nested_kind}' (допустимо: {', '.join(valid_nested)})") + else: valid_rights = KNOWN_RIGHTS[object_type] if r_name not in valid_rights: similar = find_similar(r_name, valid_rights) - sug_str = f' Did you mean: {", ".join(similar)}?' if similar else '' - report_warn(f"{obj_name}: unknown right '{r_name}'.{sug_str}") + sug_str = f" Возможно: {', '.join(similar)}?" if similar else '' + report_error(f"{obj_name}: право '{r_name}' не существует у типа '{object_type}'.{sug_str}") report_ok(f'{obj_count} objects, {right_count} rights') if rls_count > 0: diff --git a/docs/1c-role-spec.md b/docs/1c-role-spec.md index 0efd7a6e..309b3bb6 100644 --- a/docs/1c-role-spec.md +++ b/docs/1c-role-spec.md @@ -542,18 +542,44 @@ Subsystem.Администрирование.Subsystem.Пользователи | `SessionParameter` | Get, Set | | `CommonAttribute` | View, Edit | +#### ExternalDataSource + +Источник ошибки в прежних редакциях спецификации: внешние источники данных права **имеют** — +узел «Внешние источники данных» есть в дереве редактора ролей. Значения ниже сняты с выгрузки +роли со всеми проставленными правами (8.3.25). + +| Объект | Права | +|--------|-------| +| `ExternalDataSource.И` | Use, Administration, StandardAuthenticationChange, SessionStandardAuthenticationChange, SessionOSAuthenticationChange | +| `ExternalDataSource.И.Table.Т` | Read, Insert, Update, Delete, View, Edit, InputByString, InteractiveInsert, InteractiveDelete | +| `ExternalDataSource.И.Table.Т.Field.П` | View, Edit | +| `ExternalDataSource.И.Cube.К` | Read, View | +| `ExternalDataSource.И.Cube.К.Dimension.И` | View | +| `ExternalDataSource.И.Cube.К.Resource.Р` | View | +| `ExternalDataSource.И.Cube.К.DimensionTable.Т` | Read, View | +| `ExternalDataSource.И.Cube.К.DimensionTable.Т.Field.П` | View, Edit | +| `ExternalDataSource.И.Function.Ф` | Use, View | +| `…Table.Т.Command.К`, `…Cube.К.Command.К`, `…DimensionTable.Т.Command.К` | View | + #### Типы объектов БЕЗ прав в ролях -Следующие типы не фигурируют в Rights.xml (права не применимы или управляются иначе): +Следующие типы не фигурируют в Rights.xml — в дереве редактора ролей их нет: -- `Enum` (перечисления) -- `FunctionalOption` -- `DefinedType` +- `Enum` (перечисления) — блок прав на перечисление приводит к зависанию загрузки + конфигурации в информационную базу: конфигуратор не завершается и не выдаёт сообщений - `CommonModule` +- `DefinedType` - `CommonPicture` - `CommonTemplate` +- `Language` +- `FunctionalOption`, `FunctionalOptionsParameter` +- `EventSubscription` +- `ScheduledJob` +- `StyleItem`, `Style` - `SettingsStorage` -- `ExternalDataSource` +- `XDTOPackage` +- `WSReference` +- `DocumentNumerator` --- @@ -646,8 +672,21 @@ Subsystem.Администрирование.Subsystem.Пользователи | `TabularSection.*.Attribute` | (все с TabularSection) | View, Edit | | `Dimension` | InformationRegister, AccumulationRegister, AccountingRegister | View, Edit | | `Resource` | InformationRegister, AccumulationRegister, AccountingRegister | View, Edit | -| `Command` | Catalog, Document, DataProcessor, Report, *Register, DocumentJournal, ExchangePlan, BusinessProcess, Task | View | -| `AddressingAttribute` | Task | View, Edit | +| `Command` | Catalog, Document, DataProcessor, Report, *Register, DocumentJournal, ExchangePlan, BusinessProcess, Task, ExternalDataSource (Table/Cube/DimensionTable) | View | +| `AddressingAttribute` | Task, BusinessProcess | View, Edit | +| `StandardTabularSection` | Catalog, Document, ChartOf* | View, Edit | +| `AccountingFlag`, `ExtDimensionAccountingFlag` | ChartOfAccounts | View, Edit | +| `Subsystem` | Subsystem (вложенная) | View | +| `Operation` | WebService | Use | +| `URLTemplate.*.Method` | HTTPService | Use | +| `IntegrationServiceChannel` | IntegrationService | Use | +| `Recalculation` | CalculationRegister | Read, Update | +| `Column` (графа журнала) | — | прав не имеет | +| `Table`, `Cube`, `Function`, `Field`, `DimensionTable` | только ExternalDataSource | см. раздел ExternalDataSource | + +Виды, встречающиеся в выгрузках типовых конфигураций, сверены с корпусом +(`acc`, `erp`, `ut`, `unf` — ~2750 ролей); внешние источники данных — с выгрузкой роли, +где права проставлены по всему дереву. --- diff --git a/tests/skills/cases/role-compile/basic-role.json b/tests/skills/cases/role-compile/basic-role.json index 88463da6..31b39621 100644 --- a/tests/skills/cases/role-compile/basic-role.json +++ b/tests/skills/cases/role-compile/basic-role.json @@ -16,7 +16,7 @@ "input": { "name": "Кладовщик", "objects": [ - "Catalog.Товары: Read View" + "Catalog.Товары: Read, View" ] }, "validatePath": "Roles/Кладовщик", diff --git a/tests/skills/cases/role-compile/error-forbidden-type.json b/tests/skills/cases/role-compile/error-forbidden-type.json new file mode 100644 index 00000000..72a127ae --- /dev/null +++ b/tests/skills/cases/role-compile/error-forbidden-type.json @@ -0,0 +1,19 @@ +{ + "name": "Отказ: тип без прав в ролях (Enum), файлы не созданы", + "input": { + "name": "Кладовщик", + "objects": [ + "Enum.ЮрФизЛицо: Read", + "Перечисление.ЮрФизЛицо: Чтение" + ] + }, + "expectError": "не имеет прав в роли", + "noSnapshot": "негативный кейс: навык не должен создать ни одного файла", + "expect": { + "filesAbsent": [ + "Roles/Кладовщик.xml", + "Roles/Кладовщик/Ext/Rights.xml" + ], + "fileNotContains": { "file": "Configuration.xml", "text": "Кладовщик" } + } +} diff --git a/tests/skills/cases/role-compile/error-nested-alien-kind.json b/tests/skills/cases/role-compile/error-nested-alien-kind.json new file mode 100644 index 00000000..59d971dc --- /dev/null +++ b/tests/skills/cases/role-compile/error-nested-alien-kind.json @@ -0,0 +1,12 @@ +{ + "name": "Отказ: вид вложенности не от того типа", + "input": { + "name": "Кладовщик", + "objects": ["Catalog.Товары.Field.Цена: View"] + }, + "expectError": "бывает только у ExternalDataSource", + "noSnapshot": "негативный кейс: навык не должен создать ни одного файла", + "expect": { + "filesAbsent": ["Roles/Кладовщик/Ext/Rights.xml"] + } +} diff --git a/tests/skills/cases/role-compile/error-unknown-right.json b/tests/skills/cases/role-compile/error-unknown-right.json new file mode 100644 index 00000000..c5a3bc59 --- /dev/null +++ b/tests/skills/cases/role-compile/error-unknown-right.json @@ -0,0 +1,12 @@ +{ + "name": "Отказ: несуществующее право у известного типа", + "input": { + "name": "Кладовщик", + "objects": ["Catalog.Товары: Reed"] + }, + "expectError": "право 'Reed' не существует у типа 'Catalog'", + "noSnapshot": "негативный кейс: навык не должен создать ни одного файла", + "expect": { + "filesAbsent": ["Roles/Кладовщик/Ext/Rights.xml"] + } +} diff --git a/tests/skills/cases/role-compile/error-unknown-type.json b/tests/skills/cases/role-compile/error-unknown-type.json new file mode 100644 index 00000000..f54ca766 --- /dev/null +++ b/tests/skills/cases/role-compile/error-unknown-type.json @@ -0,0 +1,13 @@ +{ + "name": "Отказ: незнакомый тип объекта с подсказкой", + "input": { + "name": "Кладовщик", + "objects": ["Catalogg.Товары: Read"] + }, + "expectError": "неизвестный тип объекта 'Catalogg'", + "noSnapshot": "негативный кейс: навык не должен создать ни одного файла", + "expect": { + "stderrContains": "Возможно: Catalog?", + "filesAbsent": ["Roles/Кладовщик/Ext/Rights.xml"] + } +} diff --git a/tests/skills/cases/role-compile/nested-and-services.json b/tests/skills/cases/role-compile/nested-and-services.json new file mode 100644 index 00000000..340a083b --- /dev/null +++ b/tests/skills/cases/role-compile/nested-and-services.json @@ -0,0 +1,25 @@ +{ + "name": "Позитив: операции сервисов, вложенные объекты и внешний источник данных", + "input": { + "name": "Интеграция", + "synonym": "Интеграция", + "objects": [ + "WebService.Обмен.Operation.Загрузить: Use", + "HTTPService.ПередачаДанных.URLTemplate.Файлы.Method.get: Use", + "IntegrationService.Канал.IntegrationServiceChannel.Основной: Use", + "Catalog.Товары.Attribute.Цена: View, Edit", + "Subsystem.Продажи.Subsystem.Опт: View", + "CalculationRegister.Начисления.Recalculation.Перерасчет: Read, Update", + "ExternalDataSource.Источник: Use, Administration", + "ExternalDataSource.Источник.Table.Заказы: Read, View", + "ExternalDataSource.Источник.Table.Заказы.Field.Сумма: View, Edit", + "ExternalDataSource.Источник.Cube.Продажи.Dimension.Период: View" + ] + }, + "expect": { + "files": [ + "Roles/Интеграция/Ext/Rights.xml" + ] + }, + "skipValidation": true +} diff --git a/tests/skills/cases/role-compile/snapshots/basic-role/Roles/Кладовщик/Ext/Rights.xml b/tests/skills/cases/role-compile/snapshots/basic-role/Roles/Кладовщик/Ext/Rights.xml index ce904f5f..f0b9463c 100644 --- a/tests/skills/cases/role-compile/snapshots/basic-role/Roles/Кладовщик/Ext/Rights.xml +++ b/tests/skills/cases/role-compile/snapshots/basic-role/Roles/Кладовщик/Ext/Rights.xml @@ -6,7 +6,11 @@ Catalog.Товары - Read View + Read + true + + + View true diff --git a/tests/skills/cases/role-compile/snapshots/nested-and-services/Configuration.xml b/tests/skills/cases/role-compile/snapshots/nested-and-services/Configuration.xml new file mode 100644 index 00000000..b243f6c7 --- /dev/null +++ b/tests/skills/cases/role-compile/snapshots/nested-and-services/Configuration.xml @@ -0,0 +1,252 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + UUID-008 + UUID-009 + + + UUID-010 + UUID-011 + + + UUID-012 + UUID-013 + + + UUID-014 + UUID-015 + + + + TestConfig + + + ru + TestConfig + + + + + Version8_3_24 + ManagedApplication + + PlatformApplication + + Russian + + + + + false + false + false + + + + + + + + + + + + + + + + + + + + + + Biometrics + true + + + Location + false + + + BackgroundLocation + false + + + BluetoothPrinters + false + + + WiFiPrinters + false + + + Contacts + false + + + Calendars + false + + + PushNotifications + false + + + LocalNotifications + false + + + InAppPurchases + false + + + PersonalComputerFileExchange + false + + + Ads + false + + + NumberDialing + false + + + CallProcessing + false + + + CallLog + false + + + AutoSendSMS + false + + + ReceiveSMS + false + + + SMSLog + false + + + Camera + false + + + Microphone + false + + + MusicLibrary + false + + + PictureAndVideoLibraries + false + + + AudioPlaybackAndVibration + false + + + BackgroundAudioPlaybackAndVibration + false + + + InstallPackages + false + + + OSBackup + true + + + ApplicationUsageStatistics + false + + + BarcodeScanning + false + + + BackgroundAudioRecording + false + + + AllFilesAccess + false + + + Videoconferences + false + + + NFC + false + + + DocumentScanning + false + + + SpeechToText + false + + + Geofences + false + + + IncomingShareRequests + false + + + AllIncomingShareRequestsTypesProcessing + false + + + + + + Normal + + + Language.Русский + + + + + + Managed + NotAutoFree + DontUse + DontUse + TaxiEnableVersion8_2 + DontUse + Version8_3_24 + + + + Русский + Интеграция + + + \ No newline at end of file diff --git a/tests/skills/cases/role-compile/snapshots/nested-and-services/Ext/ClientApplicationInterface.xml b/tests/skills/cases/role-compile/snapshots/nested-and-services/Ext/ClientApplicationInterface.xml new file mode 100644 index 00000000..3c1161b2 --- /dev/null +++ b/tests/skills/cases/role-compile/snapshots/nested-and-services/Ext/ClientApplicationInterface.xml @@ -0,0 +1,18 @@ + + + + + UUID-002 + + + + + UUID-004 + + + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/role-compile/snapshots/nested-and-services/Languages/Русский.xml b/tests/skills/cases/role-compile/snapshots/nested-and-services/Languages/Русский.xml new file mode 100644 index 00000000..37c60d78 --- /dev/null +++ b/tests/skills/cases/role-compile/snapshots/nested-and-services/Languages/Русский.xml @@ -0,0 +1,16 @@ + + + + + Русский + + + ru + Русский + + + + ru + + + \ No newline at end of file diff --git a/tests/skills/cases/role-compile/snapshots/nested-and-services/Roles/Интеграция.xml b/tests/skills/cases/role-compile/snapshots/nested-and-services/Roles/Интеграция.xml new file mode 100644 index 00000000..2df0275d --- /dev/null +++ b/tests/skills/cases/role-compile/snapshots/nested-and-services/Roles/Интеграция.xml @@ -0,0 +1,15 @@ + + + + + Интеграция + + + ru + Интеграция + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/role-compile/snapshots/nested-and-services/Roles/Интеграция/Ext/Rights.xml b/tests/skills/cases/role-compile/snapshots/nested-and-services/Roles/Интеграция/Ext/Rights.xml new file mode 100644 index 00000000..c0b3e410 --- /dev/null +++ b/tests/skills/cases/role-compile/snapshots/nested-and-services/Roles/Интеграция/Ext/Rights.xml @@ -0,0 +1,96 @@ + + + false + true + false + + WebService.Обмен.Operation.Загрузить + + Use + true + + + + HTTPService.ПередачаДанных.URLTemplate.Файлы.Method.get + + Use + true + + + + IntegrationService.Канал.IntegrationServiceChannel.Основной + + Use + true + + + + Catalog.Товары.Attribute.Цена + + View + true + + + Edit + true + + + + Subsystem.Продажи.Subsystem.Опт + + View + true + + + + CalculationRegister.Начисления.Recalculation.Перерасчет + + Read + true + + + Update + true + + + + ExternalDataSource.Источник + + Use + true + + + Administration + true + + + + ExternalDataSource.Источник.Table.Заказы + + Read + true + + + View + true + + + + ExternalDataSource.Источник.Table.Заказы.Field.Сумма + + View + true + + + Edit + true + + + + ExternalDataSource.Источник.Cube.Продажи.Dimension.Период + + View + true + + + \ No newline at end of file diff --git a/tests/skills/cases/role-compile/snapshots/synonym-rights/Roles/Кладовщик/Ext/Rights.xml b/tests/skills/cases/role-compile/snapshots/synonym-rights/Roles/Кладовщик/Ext/Rights.xml index ce904f5f..f0b9463c 100644 --- a/tests/skills/cases/role-compile/snapshots/synonym-rights/Roles/Кладовщик/Ext/Rights.xml +++ b/tests/skills/cases/role-compile/snapshots/synonym-rights/Roles/Кладовщик/Ext/Rights.xml @@ -6,7 +6,11 @@ Catalog.Товары - Read View + Read + true + + + View true diff --git a/tests/skills/cases/role-compile/synonym-rights.json b/tests/skills/cases/role-compile/synonym-rights.json index 5433c667..f170ed53 100644 --- a/tests/skills/cases/role-compile/synonym-rights.json +++ b/tests/skills/cases/role-compile/synonym-rights.json @@ -16,7 +16,7 @@ "input": { "name": "Кладовщик", "rights": [ - "Catalog.Товары: Read View" + "Catalog.Товары: Read, View" ] }, "validatePath": "Roles/Кладовщик", diff --git a/tests/skills/cases/role-validate/error-forbidden-type.json b/tests/skills/cases/role-validate/error-forbidden-type.json new file mode 100644 index 00000000..ea7376d7 --- /dev/null +++ b/tests/skills/cases/role-validate/error-forbidden-type.json @@ -0,0 +1,13 @@ +{ + "name": "Ошибка: блоки прав на типы без прав в ролях", + "setup": "fixture:no-rights-types", + "params": { "rightsPath": "Roles/РольВсеТипы/Ext/Rights.xml" }, + "expectError": true, + "noSnapshot": "негативный кейс: навык ничего не пишет, проверяется текст и код возврата", + "expect": { + "stdoutContains": [ + "Enum.ЮрФизЛицо: тип 'Enum' не имеет прав в роли", + "CommonModule.ОбщегоНазначения: тип 'CommonModule' не имеет прав в роли" + ] + } +} diff --git a/tests/skills/cases/role-validate/external-data-source.json b/tests/skills/cases/role-validate/external-data-source.json new file mode 100644 index 00000000..12486638 --- /dev/null +++ b/tests/skills/cases/role-validate/external-data-source.json @@ -0,0 +1,10 @@ +{ + "name": "Внешний источник данных: все уровни дерева проходят без замечаний", + "setup": "fixture:external-data-source", + "params": { "rightsPath": "Roles/ПраваНаВнешнийИсточник/Ext/Rights.xml" }, + "noSnapshot": "валидатор не пишет файлов, проверяется чистый вывод", + "expect": { + "stdoutContains": "Validation OK", + "stdoutNotContains": ["[WARN]", "[ERROR]"] + } +} diff --git a/tests/skills/cases/role-validate/fixtures/bad-root/BadRole/Ext/Rights.xml b/tests/skills/cases/role-validate/fixtures/bad-root/Roles/BadRole/Ext/Rights.xml similarity index 100% rename from tests/skills/cases/role-validate/fixtures/bad-root/BadRole/Ext/Rights.xml rename to tests/skills/cases/role-validate/fixtures/bad-root/Roles/BadRole/Ext/Rights.xml diff --git a/tests/skills/cases/role-validate/fixtures/external-data-source/Roles/ПраваНаВнешнийИсточник/Ext/Rights.xml b/tests/skills/cases/role-validate/fixtures/external-data-source/Roles/ПраваНаВнешнийИсточник/Ext/Rights.xml new file mode 100644 index 00000000..1bfcf022 --- /dev/null +++ b/tests/skills/cases/role-validate/fixtures/external-data-source/Roles/ПраваНаВнешнийИсточник/Ext/Rights.xml @@ -0,0 +1,205 @@ + + + false + false + false + + ExternalDataSource.ВнешнийИсточникДанных1.Cube.Куб1.Resource.Ресурс1 + + View + true + + + + ExternalDataSource.ВнешнийИсточникДанных1.Table.Таблица1 + + Read + true + + + Insert + true + + + Update + true + + + Delete + true + + + View + true + + + InteractiveInsert + true + + + Edit + true + + + InteractiveDelete + true + + + InputByString + true + + + + ExternalDataSource.ВнешнийИсточникДанных1.Cube.Куб1 + + Read + true + + + View + true + + + + ExternalDataSource.ВнешнийИсточникДанных1.Table.Таблица1.Command.Команда1 + + View + true + + + + ExternalDataSource.ВнешнийИсточникДанных1 + + Use + true + + + Administration + true + + + StandardAuthenticationChange + true + + + SessionStandardAuthenticationChange + true + + + SessionOSAuthenticationChange + true + + + + ExternalDataSource.ВнешнийИсточникДанных1.Cube.Куб1.DimensionTable.ТаблицаИзмерения1.Command.Команда1 + + View + true + + + + ExternalDataSource.ВнешнийИсточникДанных1.Cube.Куб1.Dimension.Измерение1 + + View + true + + + + ExternalDataSource.ВнешнийИсточникДанных1.Table.Таблица1.Field.Поле1 + + View + true + + + Edit + true + + + + ExternalDataSource.ВнешнийИсточникДанных1.Cube.Куб1.DimensionTable.ТаблицаИзмерения1 + + Read + true + + + View + true + + + + ExternalDataSource.ВнешнийИсточникДанных1.Function.Функция1 + + Use + true + + + View + true + + + + ExternalDataSource.ВнешнийИсточникДанных1.Cube.Куб1.DimensionTable.ТаблицаИзмерения1.Field.Поле1 + + View + true + + + Edit + true + + + + ExternalDataSource.ВнешнийИсточникДанных1.Cube.Куб1.Command.Команда1 + + View + true + + + + Configuration.Конфигурация + + ThinClient + true + + + WebClient + true + + + MobileClient + true + + + MainWindowModeNormal + true + + + MainWindowModeWorkplace + true + + + MainWindowModeEmbeddedWorkplace + true + + + MainWindowModeFullscreenWorkplace + true + + + MainWindowModeKiosk + true + + + AnalyticsSystemClient + true + + + SaveUserData + true + + + Output + true + + + \ No newline at end of file diff --git a/tests/skills/cases/role-validate/fixtures/external-data-source/Roles/ПраваНаПерерасчет/Ext/Rights.xml b/tests/skills/cases/role-validate/fixtures/external-data-source/Roles/ПраваНаПерерасчет/Ext/Rights.xml new file mode 100644 index 00000000..ef824fe9 --- /dev/null +++ b/tests/skills/cases/role-validate/fixtures/external-data-source/Roles/ПраваНаПерерасчет/Ext/Rights.xml @@ -0,0 +1,64 @@ + + + false + true + false + + Configuration.Конфигурация + + ThinClient + true + + + WebClient + true + + + MobileClient + true + + + MainWindowModeNormal + true + + + MainWindowModeWorkplace + true + + + MainWindowModeEmbeddedWorkplace + true + + + MainWindowModeFullscreenWorkplace + true + + + MainWindowModeKiosk + true + + + AnalyticsSystemClient + true + + + SaveUserData + true + + + Output + true + + + + CalculationRegister.РегистрРасчета1.Recalculation.Перерасчет1 + + Read + true + + + Update + true + + + \ No newline at end of file diff --git a/tests/skills/cases/role-validate/fixtures/no-rights-types/Roles/РольВсеТипы/Ext/Rights.xml b/tests/skills/cases/role-validate/fixtures/no-rights-types/Roles/РольВсеТипы/Ext/Rights.xml new file mode 100644 index 00000000..8ccabf5b --- /dev/null +++ b/tests/skills/cases/role-validate/fixtures/no-rights-types/Roles/РольВсеТипы/Ext/Rights.xml @@ -0,0 +1,83 @@ + + + false + true + false + + Enum.ЮрФизЛицо + + Read + true + + + + CommonModule.ОбщегоНазначения + + Read + true + + + + DefinedType.Контрагент + + Read + true + + + + CommonPicture.Логотип + + View + true + + + + CommonTemplate.Макет + + View + true + + + + Language.Русский + + View + true + + + + FunctionalOption.ИспользоватьВалюты + + View + true + + + + FunctionalOptionsParameter.Организация + + View + true + + + + EventSubscription.ЗаписатьВерсиюОбъекта + + View + true + + + + ScheduledJob.ОбновлениеИндекса + + View + true + + + + StyleItem.ВажнаяНадписьШрифт + + View + true + + + \ No newline at end of file diff --git a/tests/skills/cases/role-validate/recalculation.json b/tests/skills/cases/role-validate/recalculation.json new file mode 100644 index 00000000..8b7316de --- /dev/null +++ b/tests/skills/cases/role-validate/recalculation.json @@ -0,0 +1,10 @@ +{ + "name": "Перерасчёт регистра расчёта проходит без замечаний", + "setup": "fixture:external-data-source", + "params": { "rightsPath": "Roles/ПраваНаПерерасчет/Ext/Rights.xml" }, + "noSnapshot": "валидатор не пишет файлов, проверяется чистый вывод", + "expect": { + "stdoutContains": "Validation OK", + "stdoutNotContains": ["[WARN]", "[ERROR]"] + } +} diff --git a/tests/skills/cases/role-validate/snapshots/valid-role/Roles/Тест/Ext/Rights.xml b/tests/skills/cases/role-validate/snapshots/valid-role/Roles/Тест/Ext/Rights.xml index 9a1537d2..57bb47e8 100644 --- a/tests/skills/cases/role-validate/snapshots/valid-role/Roles/Тест/Ext/Rights.xml +++ b/tests/skills/cases/role-validate/snapshots/valid-role/Roles/Тест/Ext/Rights.xml @@ -3,4 +3,11 @@ false true false + + Catalog.Товары + + Read + true + + \ No newline at end of file diff --git a/tests/skills/cases/role-validate/valid-role.json b/tests/skills/cases/role-validate/valid-role.json index 163bb3b9..7df0aece 100644 --- a/tests/skills/cases/role-validate/valid-role.json +++ b/tests/skills/cases/role-validate/valid-role.json @@ -16,13 +16,8 @@ "script": "role-compile/scripts/role-compile", "input": { "name": "Тест", - "rights": [ - { - "object": "Catalog.Товары", - "rights": [ - "Read" - ] - } + "objects": [ + "Catalog.Товары: Read" ] }, "args": { diff --git a/tests/skills/check-type-maps.mjs b/tests/skills/check-type-maps.mjs index 52e6966b..5a9d57d5 100644 --- a/tests/skills/check-type-maps.mjs +++ b/tests/skills/check-type-maps.mjs @@ -60,8 +60,10 @@ const MAPS = [ // вокабуляры: значения — канонические имена, полнота не требуется { skill: 'role-compile', file: 'role-compile', kind: 'alias', py: 'TYPE_ALIASES', ps1: null, - extraTargets: ['Configuration', 'Attribute', 'StandardAttribute', 'TabularSection', - 'Dimension', 'Resource', 'Command', 'AddressingAttribute'], + // ExternalDataSource — объект метаданных с правами в ролях, но в таблице порядка + // ChildObjects его нет: позиция в порядке не измерена (в корпусе внешних источников нет). + extraTargets: ['Configuration', 'ExternalDataSource', 'Attribute', 'StandardAttribute', + 'TabularSection', 'Dimension', 'Resource', 'Command', 'AddressingAttribute'], }, { skill: 'interface-edit', file: 'interface-edit', kind: 'alias', py: 'TYPE_NORM_MAP', ps1: null }, { skill: 'subsystem-edit', file: 'subsystem-edit', kind: 'alias', py: 'CONTENT_TYPE_MAP', ps1: null }, diff --git a/tests/skills/integration/build-config.test.mjs b/tests/skills/integration/build-config.test.mjs index 81878a1e..258d0e77 100644 --- a/tests/skills/integration/build-config.test.mjs +++ b/tests/skills/integration/build-config.test.mjs @@ -238,11 +238,11 @@ export const steps = [ input: { name: 'Кладовщик', objects: [ - 'Catalog.Контрагенты: Read View', - 'Catalog.Номенклатура: Read View', - 'Document.ПриходнаяНакладная: Read View Add Update', + 'Catalog.Контрагенты: Read, View', + 'Catalog.Номенклатура: Read, View', + 'Document.ПриходнаяНакладная: Read, View, Insert, Update', 'AccumulationRegister.ОстаткиТоваров: Read', - 'Report.ОстаткиТоваров: Use View', + 'Report.ОстаткиТоваров: Use, View', ], }, args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}' }, diff --git a/tests/skills/integration/build-webtest-config.test.mjs b/tests/skills/integration/build-webtest-config.test.mjs index 3f27e767..144b98dd 100644 --- a/tests/skills/integration/build-webtest-config.test.mjs +++ b/tests/skills/integration/build-webtest-config.test.mjs @@ -2027,20 +2027,20 @@ export const steps = [ input: { name: 'Администратор', objects: [ - 'Catalog.Организации: Read View Add Update Delete', - 'Catalog.Контрагенты: Read View Add Update Delete', - 'Catalog.КонтактныеЛица: Read View Add Update Delete', - 'Catalog.Номенклатура: Read View Add Update Delete', - 'Document.ПриходнаяНакладная: Read View Add Update Delete Posting UnPosting', - 'InformationRegister.КурсыВалют: Read View Add Update Delete', - 'Report.ОстаткиТоваров: Use View', - 'DataProcessor.ДеревоНоменклатуры: Use View', - 'DataProcessor.МножественныйВыбор: Use View', - 'DataProcessor.БезшапочнаяТаблица: Use View', - 'DataProcessor.МногострочнаяШапка: Use View', - 'DataProcessor.ГруппыКолонок: Use View', - 'DataProcessor.ПроверкаДоступности: Use View', - 'DataProcessor.СтраницаНастроек: Use View', + 'Catalog.Организации: Read, View, Insert, Update, Delete', + 'Catalog.Контрагенты: Read, View, Insert, Update, Delete', + 'Catalog.КонтактныеЛица: Read, View, Insert, Update, Delete', + 'Catalog.Номенклатура: Read, View, Insert, Update, Delete', + 'Document.ПриходнаяНакладная: Read, View, Insert, Update, Delete, Posting, UndoPosting', + 'InformationRegister.КурсыВалют: Read, View, Update, Edit', + 'Report.ОстаткиТоваров: Use, View', + 'DataProcessor.ДеревоНоменклатуры: Use, View', + 'DataProcessor.МножественныйВыбор: Use, View', + 'DataProcessor.БезшапочнаяТаблица: Use, View', + 'DataProcessor.МногострочнаяШапка: Use, View', + 'DataProcessor.ГруппыКолонок: Use, View', + 'DataProcessor.ПроверкаДоступности: Use, View', + 'DataProcessor.СтраницаНастроек: Use, View', ], }, args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}' }, diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs index b1f3ba2b..357dc291 100644 --- a/tests/skills/runner.mjs +++ b/tests/skills/runner.mjs @@ -411,7 +411,7 @@ function checkFileContains(workDir, spec, expectPresent) { // который молча ничего не проверяет (так уже было с 9 кейсами meta-edit) — // поэтому он ошибка, а не игнор. const KNOWN_EXPECT_KEYS = new Set([ - 'files', 'stdoutContains', 'stdoutNotContains', 'stderrContains', 'preserves', + 'files', 'filesAbsent', 'stdoutContains', 'stdoutNotContains', 'stderrContains', 'preserves', 'fileContains', 'fileNotContains', 'filesEqual', ]); @@ -841,6 +841,16 @@ async function runCaseAsync(testCase, opts) { if (!stderr.includes(needle)) errors.push(`stderr does not contain "${needle}"`); } } + // Отсутствие файла — тоже утверждение, и нужно оно чаще всего НЕГАТИВНОМУ кейсу: + // «отказ произошёл до записи». В позитивной ветке (где живёт expect.files) такой + // проверки не было бы ровно там, где она единственная содержательная. + if (caseData.expect?.filesAbsent) { + const paths = Array.isArray(caseData.expect.filesAbsent) + ? caseData.expect.filesAbsent : [caseData.expect.filesAbsent]; + for (const p of paths) { + if (existsSync(join(workDir, p))) errors.push(`File must not exist: ${p}`); + } + } } if (!caseData.expectError) { if (caseData.expect?.preserves) { @@ -1066,6 +1076,16 @@ function runCase(testCase, opts) { if (!stderr.includes(needle)) errors.push(`stderr does not contain "${needle}"`); } } + // Отсутствие файла — тоже утверждение, и нужно оно чаще всего НЕГАТИВНОМУ кейсу: + // «отказ произошёл до записи». В позитивной ветке (где живёт expect.files) такой + // проверки не было бы ровно там, где она единственная содержательная. + if (caseData.expect?.filesAbsent) { + const paths = Array.isArray(caseData.expect.filesAbsent) + ? caseData.expect.filesAbsent : [caseData.expect.filesAbsent]; + for (const p of paths) { + if (existsSync(join(workDir, p))) errors.push(`File must not exist: ${p}`); + } + } } if (!caseData.expectError) {