From b28c044e06c506cfd0a7bb429e320e713343d53e Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 13 Sep 2026 18:47:47 +0300 Subject: [PATCH] =?UTF-8?q?fix(role-edit):=20=D0=BE=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=8F=D1=8E=D1=82=D1=81=D1=8F=20=D0=B2=20=D0=BF=D0=BE=D1=80?= =?UTF-8?q?=D1=8F=D0=B4=D0=BA=D0=B5=20=D0=B2=D0=B2=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Внутри навыка они были сгруппированы по типам, поэтому список [remove-rights, add-rights] для одного права выполнялся наоборот: право сначала добавлялось, потом снималось. В инструкции об этом не было ни слова, а порядок ввода — то, чего ожидает читающий. Заодно: условие RLS со ссылкой на шаблон, которого в роли нет, больше не проходит молча — предупреждение в stderr (отказывать нельзя, шаблон могут добавить следующей операцией). Вычитка инструкций обоих навыков: сказано, что -Operation это одна операция, а несколько разных задают списком в файле (с примером); перечислены все три глобальных флага; про значение из файла вынесено в свой раздел; добавлено, что права реквизитам поштучно платформа не хранит — ограничивают их запретом. Из role-compile убрано объяснение, зачем навык замыкает набор: инструкция про применение. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FGkXwoXTuafcu1SXMsauFq --- .claude/skills/role-compile/SKILL.md | 8 +- .claude/skills/role-edit/SKILL.md | 55 ++++++++++---- .../skills/role-edit/scripts/role-edit.ps1 | 74 ++++++++++--------- .claude/skills/role-edit/scripts/role-edit.py | 20 +++-- 4 files changed, 97 insertions(+), 60 deletions(-) diff --git a/.claude/skills/role-compile/SKILL.md b/.claude/skills/role-compile/SKILL.md index d5da4dab3..9fcf5b54b 100644 --- a/.claude/skills/role-compile/SKILL.md +++ b/.claude/skills/role-compile/SKILL.md @@ -63,8 +63,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-compile.ps1" - ### Зависимые права Набор прав дополняется до замыкания: `Edit` тянет `Read`, `Update`, `View`; интерактивные права — -свой базовый набор; `View` у обработки и отчёта — `Use`. Дописанное перечисляется в выводе. Так же -поступает сама платформа при загрузке, поэтому без этого файл роли и база расходятся. +свой базовый набор; `View` у обработки и отчёта — `Use`. Дописанное перечисляется в выводе. ### Сервисы @@ -130,6 +129,11 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-compile.ps1" - Права на части объекта задаются точечным путём: `Catalog.Контрагенты.Attribute.ИНН: View, Edit`, `WebService.Обмен.Operation.Загрузить: Use`, `HTTPService.ЭДО.URLTemplate.ЕстьНовыеДокументы.Method.POST: Use`. +Реквизиты, табличные части, измерения и ресурсы по умолчанию наследуют права своего объекта: +выдавать их отдельно бессмысленно, платформа такую запись не сохранит. Поштучные права на них +имеют смысл только при `"setForAttributesByDefault": false`; ограничить реквизит в обычной роли +можно запретом — `{"name": "Catalog.Товары.Attribute.Цена", "rights": {"View": false}}`. + Роль расширения, включённая в основные (`DefaultRoles`), прав на заимствованные объекты давать не может — платформа это запрещает. Такие права выноси в отдельную роль вне основных. Полные таблицы «тип → права», виды вложенности (включая внешние источники данных), список типов без прав, таблицы пресетов и дополнительные примеры — в `dsl-reference.md`. diff --git a/.claude/skills/role-edit/SKILL.md b/.claude/skills/role-edit/SKILL.md index 9003e0df1..eb2821e4b 100644 --- a/.claude/skills/role-edit/SKILL.md +++ b/.claude/skills/role-edit/SKILL.md @@ -19,15 +19,17 @@ allowed-tools: | Параметр | Описание | |----------|----------| | `RolePath` | Каталог роли, `Roles/Имя.xml` или `Roles/Имя/Ext/Rights.xml` | -| `Operation` | Операция из таблицы ниже (альтернатива `DefinitionFile`) | -| `Value` | Значение операции. Пакет через `;;`. Текст можно взять из файла: `@путь` в позиции значения | -| `DefinitionFile` | JSON-массив операций `[{ "operation": "...", "value": "..." }]` | +| `Operation` | Одна операция из таблицы ниже | +| `Value` | Значение операции; несколько значений — через `;;` | +| `DefinitionFile` | JSON-массив операций (альтернатива `Operation`) — так задают несколько разных операций | | `NoValidate` | Не запускать `role-validate` после правки | ```powershell powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-edit.ps1" -RolePath "<роль>" -Operation -Value "<значение>" ``` +Операции применяются в том порядке, в котором перечислены. + ## Операции | Операция | Формат `Value` | Описание | @@ -35,14 +37,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-edit.ps1" -Rol | `add-rights` | `Тип.Имя: Право1, Право2` или `Тип.Имя: @пресет` | Добавить права, прежние сохранить | | `set-rights` | то же | Заменить набор прав объекта целиком | | `remove-rights` | `Тип.Имя: Право1, Право2` — снять права
`Тип.Имя` — удалить объект целиком | Снять права | -| `deny-rights` | `Тип.Имя: Право1, Право2` | Запретить права явно (`false`) | +| `deny-rights` | `Тип.Имя: Право1, Право2` | Запретить права явно | | `set-rls` | `Тип.Имя.Право: условие`
`Тип.Имя.Право[Поле1, Поле2]: условие` | Ограничение доступа к данным | | `remove-rls` | `Тип.Имя.Право` — снять все
`Тип.Имя.Право[Поле1]` — снять одну строку | Снять ограничение, право оставить | | `add-template` | `Имя(Параметры): условие` | Добавить шаблон ограничения | | `set-template` | то же | Заменить шаблон, а если его нет — добавить | | `remove-template` | `Имя(Параметры)` или `Имя` | Удалить шаблон | -| `modify-property` | `setForNewObjects=true` | Глобальные флаги роли | -| `set-synonym` | текст | Синоним роли (правит `Roles/Имя.xml`) | +| `modify-property` | `setForNewObjects=true`
`setForAttributesByDefault=false`
`independentRightsOfChildObjects=true` | Глобальные флаги роли | +| `set-synonym` | текст | Синоним роли | | `set-comment` | текст | Комментарий роли | ## Быстрые примеры @@ -51,7 +53,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-edit.ps1" -Rol # Добавить права нового регистра, ничего не потеряв ... -RolePath "src/Roles/Менеджер" -Operation add-rights -Value "InformationRegister.Цены: Read, Update" -# Пресет и пакет в одном вызове +# Пресет и несколько объектов в одном вызове ... -RolePath "src/Roles/Менеджер" -Operation add-rights -Value "Catalog.Товары: @view ;; Document.Заказ: @edit" # Снять права; объект без разрешающих прав удаляется целиком @@ -61,6 +63,19 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-edit.ps1" -Rol ... -RolePath "src/Roles/Менеджер" -Operation deny-rights -Value "Catalog.Товары.Attribute.Цена: View" ``` +Несколько разных операций — списком в файле: + +```json +[ + { "operation": "add-rights", "value": "Catalog.Товары: @view" }, + { "operation": "set-rls", "value": "Catalog.Товары.Read: #ПоОрганизации(\"\")" } +] +``` + +```powershell +... -RolePath "src/Roles/Менеджер" -DefinitionFile "операции.json" +``` + ## Права Имена объектов и прав — те же, что в `/role-compile`: английские (`Catalog`, `Read`) или русские @@ -72,16 +87,22 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-edit.ps1" -Rol отчёта — `Use`. Снятие работает в обратную сторону: снимаешь `Read` — уходят и права, которые его требуют. Дописанное и снятое каскадом перечисляется в выводе. +Реквизиты, табличные части, измерения и ресурсы по умолчанию наследуют права своего объекта, и +выдавать их отдельно бессмысленно — платформа такую запись не сохранит. Ограничивают их через +`deny-rights`. Выдавать права реквизитам поштучно имеет смысл только в роли с +`setForAttributesByDefault=false`. + Тип или имя права вне допустимого списка — отказ: файл не меняется, ошибки печатаются все разом. -Полные таблицы «тип → права» и виды вложенности — в `role-compile/dsl-reference.md`. +Полные таблицы «тип → права» и виды вложенности — в справке `/role-compile`. ## RLS Ограничения права — это список строк, как в таблице «Ограничения доступа к данным» редактора. -Строка без списка полей действует на все прочие поля, строка со списком — только на них: +Строка без списка полей действует на все прочие поля, строка со списком — только на них. +Типовой приём: закрыть данные целиком, но оставить поля представления ссылки открытыми, иначе +ссылки на объект превратятся в «объект не найден»: ```powershell -# Закрыть таблицу, но оставить ссылку живой: ссылочные поля без ограничения ... -Operation set-rls -Value "Catalog.Товары.Read: ГДЕ ЛОЖЬ" ... -Operation set-rls -Value "Catalog.Товары.Read[Ссылка, Код, Наименование]: " ``` @@ -89,18 +110,20 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-edit.ps1" -Rol Строка с тем же набором полей заменяется, с другим — добавляется. Ограничение на невыданное право — ошибка: платформа такое молча игнорирует. -Длинное условие держи в файле — адрес остаётся в команде, из файла приходит только текст: +Ссылка на шаблон — `#ИмяШаблона("")`; `&` в условиях экранируется автоматически. + +## Значение из файла + +Вместо текста можно указать `@путь` — условие RLS, тело шаблона, синоним и комментарий берутся из +файла. Относительный путь ищется рядом с `DefinitionFile`, затем в текущем каталоге. Разделитель +`;;` внутри файла остаётся обычным текстом. ```powershell ... -Operation set-rls -Value "Catalog.Товары.Read: @условие.txt" ... -Operation add-template -Value "ПоОрганизации(Мод): @условие.txt" +... -Operation set-comment -Value "@комментарий.txt" ``` -Так же берётся текст у `set-synonym` и `set-comment`: `-Value "@текст.txt"`. Разделитель пакета -`;;` внутри файла остаётся обычным текстом. - -Ссылка на шаблон — `#ИмяШаблона("")`; `&` в условиях экранируется автоматически. - ## Верификация ``` diff --git a/.claude/skills/role-edit/scripts/role-edit.ps1 b/.claude/skills/role-edit/scripts/role-edit.ps1 index 13f392373..ddf7e7952 100644 --- a/.claude/skills/role-edit/scripts/role-edit.ps1 +++ b/.claude/skills/role-edit/scripts/role-edit.ps1 @@ -1,4 +1,4 @@ -# role-edit v1.1 — Edit existing 1C role rights in place +# role-edit v1.2 — Edit existing 1C role rights in place # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills [CmdletBinding(PositionalBinding=$false)] param( @@ -1715,7 +1715,7 @@ function Do-AddRights([string]$batchVal) { $spec = Parse-RightsSpec $item if (-not $spec) { continue } foreach ($expanded in (Expand-ServiceEntry -parsed @{ Name = $spec.Name; Rights = @($spec.Rights | ForEach-Object { @{ Name = $_; Value = "true"; Condition = $null } }) } -configRoot $script:configRoot -name $script:paths.RoleName)) { - $script:pendingAdds += ,@{ Name = $expanded.Name; Rights = @($expanded.Rights | ForEach-Object { $_.Name }) } + $script:pending += ,@{ Kind = 'add-rights'; Spec = @{ Name = $expanded.Name; Rights = @($expanded.Rights | ForEach-Object { $_.Name }) } } } } } @@ -1771,7 +1771,7 @@ function Do-SetRights([string]$batchVal) { foreach ($item in (Parse-BatchValue $batchVal)) { $spec = Parse-RightsSpec $item if (-not $spec) { continue } - $script:pendingSets += ,$spec + $script:pending += ,@{ Kind = 'set-rights'; Spec = $spec } } } @@ -1804,7 +1804,7 @@ function Do-RemoveRights([string]$batchVal) { foreach ($item in (Parse-BatchValue $batchVal)) { $spec = Parse-RightsSpec $item -AllowNoRights if (-not $spec) { continue } - $script:pendingRemoves += ,$spec + $script:pending += ,@{ Kind = 'remove-rights'; Spec = $spec } } } @@ -1853,7 +1853,7 @@ function Do-DenyRights([string]$batchVal) { foreach ($item in (Parse-BatchValue $batchVal)) { $spec = Parse-RightsSpec $item if (-not $spec) { continue } - $script:pendingDenies += ,$spec + $script:pending += ,@{ Kind = 'deny-rights'; Spec = $spec } } } @@ -1935,7 +1935,7 @@ function Do-SetRls([string]$batchVal) { foreach ($item in (Parse-BatchValue $batchVal)) { $spec = Parse-RlsAddress $item -ConditionRequired if (-not $spec) { continue } - $script:pendingRls += ,$spec + $script:pending += ,@{ Kind = 'set-rls'; Spec = $spec } } } @@ -1956,6 +1956,15 @@ function Apply-SetRls($spec) { foreach ($node in $existing) { if (Test-SameFieldSet (Get-RestrictionFields $node) $spec.Fields) { $target = $node; break } } + # Ссылка на шаблон, которого в роли нет, — тихая ошибка в рантайме 1С. Отказывать нельзя: + # шаблон могут добавить следующей операцией или следующим вызовом. + foreach ($m in [regex]::Matches("$($spec.Condition)", '#([A-Za-zА-Яа-яЁё0-9_]+)\s*\(')) { + $templateName = $m.Groups[1].Value + if ($templateName -in @('Если', 'Тогда', 'Иначе', 'КонецЕсли')) { continue } + if (-not (Find-TemplateNode $templateName)) { + [Console]::Error.WriteLine("[role-edit] $($spec.Object).$($spec.Right): условие ссылается на шаблон '$templateName', которого в роли нет") + } + } $new = New-RestrictionNode $indent $spec.Fields $spec.Condition if ($target) { $rightNode.ReplaceChild($new, $target) | Out-Null @@ -1978,7 +1987,7 @@ function Do-RemoveRls([string]$batchVal) { foreach ($item in (Parse-BatchValue $batchVal)) { $spec = Parse-RlsAddress $item if (-not $spec) { continue } - $script:pendingRlsRemovals += ,$spec + $script:pending += ,@{ Kind = 'remove-rls'; Spec = $spec } } } @@ -2039,7 +2048,7 @@ function Do-AddTemplate([string]$batchVal) { foreach ($item in (Parse-BatchValue $batchVal)) { $spec = Parse-TemplateSpec $item if (-not $spec) { continue } - $script:pendingTemplateAdds += ,$spec + $script:pending += ,@{ Kind = 'add-template'; Spec = $spec } } } @@ -2067,14 +2076,14 @@ function Do-SetTemplate([string]$batchVal) { foreach ($item in (Parse-BatchValue $batchVal)) { $spec = Parse-TemplateSpec $item if (-not $spec) { continue } - $script:pendingTemplateSets += ,$spec + $script:pending += ,@{ Kind = 'set-template'; Spec = $spec } } } function Do-RemoveTemplate([string]$batchVal) { foreach ($item in (Parse-BatchValue $batchVal)) { $spec = Parse-TemplateSpec $item -NameOnly - $script:pendingTemplateRemovals += ,$spec + $script:pending += ,@{ Kind = 'remove-template'; Spec = $spec } } } @@ -2127,7 +2136,7 @@ function Do-ModifyProperty([string]$batchVal) { Add-ValidationError "$item : значение должно быть true или false" continue } - $script:pendingProperties += ,@{ Name = $canonical; Value = $value } + $script:pending += ,@{ Kind = 'modify-property'; Spec = @{ Name = $canonical; Value = $value } } } } @@ -2194,17 +2203,8 @@ function Edit-RoleMetadata([string]$field, [string]$text) { # --- Сбор и выполнение операций --- -$script:pendingAdds = @() -$script:pendingSets = @() -$script:pendingRemoves = @() -$script:pendingDenies = @() -$script:pendingRls = @() -$script:pendingRlsRemovals = @() -$script:pendingTemplateAdds = @() -$script:pendingTemplateSets = @() -$script:pendingTemplateRemovals = @() -$script:pendingProperties = @() -$script:pendingMeta = @() +# Очередь одна: операции применяются в том порядке, в котором их перечислили. +$script:pending = @() $operations = @() if ($DefinitionFile) { @@ -2233,8 +2233,8 @@ foreach ($op in $operations) { "set-template" { Do-SetTemplate $opValue } "remove-template" { Do-RemoveTemplate $opValue } "modify-property" { Do-ModifyProperty $opValue } - "set-synonym" { $script:pendingMeta += ,@{ Field = 'Synonym'; Text = (Resolve-TextFromFile $opValue $script:textBaseDir) } } - "set-comment" { $script:pendingMeta += ,@{ Field = 'Comment'; Text = (Resolve-TextFromFile $opValue $script:textBaseDir) } } + "set-synonym" { $script:pending += ,@{ Kind = 'set-meta'; Spec = @{ Field = 'Synonym'; Text = (Resolve-TextFromFile $opValue $script:textBaseDir) } } } + "set-comment" { $script:pending += ,@{ Kind = 'set-meta'; Spec = @{ Field = 'Comment'; Text = (Resolve-TextFromFile $opValue $script:textBaseDir) } } } default { Add-ValidationError "Неизвестная операция: $opName" } @@ -2249,17 +2249,21 @@ if ($script:validationErrors.Count -gt 0) { exit 1 } -foreach ($spec in $script:pendingAdds) { Apply-AddRights $spec } -foreach ($spec in $script:pendingSets) { Apply-SetRights $spec } -foreach ($spec in $script:pendingDenies) { Apply-DenyRights $spec } -foreach ($spec in $script:pendingRemoves) { Apply-RemoveRights $spec } -foreach ($spec in $script:pendingTemplateAdds) { Apply-AddTemplate $spec } -foreach ($spec in $script:pendingTemplateSets) { Apply-AddTemplate $spec -AllowReplace } -foreach ($spec in $script:pendingRls) { Apply-SetRls $spec } -foreach ($spec in $script:pendingRlsRemovals) { Apply-RemoveRls $spec } -foreach ($spec in $script:pendingTemplateRemovals) { Apply-RemoveTemplate $spec } -foreach ($spec in $script:pendingProperties) { Apply-ModifyProperty $spec } -foreach ($spec in $script:pendingMeta) { Edit-RoleMetadata $spec.Field $spec.Text } +foreach ($item in $script:pending) { + switch ($item.Kind) { + 'add-rights' { Apply-AddRights $item.Spec } + 'set-rights' { Apply-SetRights $item.Spec } + 'deny-rights' { Apply-DenyRights $item.Spec } + 'remove-rights' { Apply-RemoveRights $item.Spec } + 'add-template' { Apply-AddTemplate $item.Spec } + 'set-template' { Apply-AddTemplate $item.Spec -AllowReplace } + 'remove-template' { Apply-RemoveTemplate $item.Spec } + 'set-rls' { Apply-SetRls $item.Spec } + 'remove-rls' { Apply-RemoveRls $item.Spec } + 'modify-property' { Apply-ModifyProperty $item.Spec } + 'set-meta' { Edit-RoleMetadata $item.Spec.Field $item.Spec.Text } + } +} # Ошибка могла всплыть и на применении (RLS без права) — файл в этом случае не трогаем. if ($script:validationErrors.Count -gt 0) { diff --git a/.claude/skills/role-edit/scripts/role-edit.py b/.claude/skills/role-edit/scripts/role-edit.py index 57f53b606..763c6cfa7 100644 --- a/.claude/skills/role-edit/scripts/role-edit.py +++ b/.claude/skills/role-edit/scripts/role-edit.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# role-edit v1.1 — Edit existing 1C role rights in place +# role-edit v1.2 — Edit existing 1C role rights in place # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -1908,6 +1908,15 @@ class Editor: if self.same_field_set(self.restriction_fields(node), spec["Fields"]): target = node break + # Ссылка на шаблон, которого в роли нет, — тихая ошибка в рантайме 1С. Отказывать нельзя: + # шаблон могут добавить следующей операцией или следующим вызовом. + for m in re.finditer(r'#([A-Za-zА-Яа-яЁё0-9_]+)\s*\(', spec["Condition"] or ""): + template_name = m.group(1) + if template_name in ("Если", "Тогда", "Иначе", "КонецЕсли"): + continue + if self.find_template(template_name) is None: + print(f"[role-edit] {spec['Object']}.{spec['Right']}: условие ссылается на шаблон " + f"'{template_name}', которого в роли нет", file=sys.stderr) new_el = self.make_restriction(indent, spec["Fields"], spec["Condition"]) if target is not None: new_el.tail = target.tail @@ -2178,8 +2187,6 @@ def main(): refuse_if_errors() - order = ["add-rights", "set-rights", "deny-rights", "remove-rights", "add-template", "set-template", - "set-rls", "remove-rls", "remove-template", "modify-property", "set-synonym", "set-comment"] handlers = { "add-rights": ed.apply_add_rights, "set-rights": ed.apply_set_rights, @@ -2194,10 +2201,9 @@ def main(): "set-synonym": lambda s: ed.edit_role_metadata(s["Field"], s["Text"]), "set-comment": lambda s: ed.edit_role_metadata(s["Field"], s["Text"]), } - for key in order: - for op_key, spec in pending: - if op_key == key: - handlers[key](spec) + # Операции применяются в том порядке, в котором их перечислили. + for op_key, spec in pending: + handlers[op_key](spec) # Ошибка могла всплыть и на применении (RLS без права) — файл в этом случае не трогаем. refuse_if_errors()