fix(role-compile,role-edit): замыкание прав и фильтр значений по умолчанию

Замер на платформе (debug/role-edit/FINDINGS.md): роль хранит только то,
что отличается от её умолчаний. При setForNewObjects=false на верхнем
уровне живут разрешения, а любой запрет выбрасывается (проверены Update,
Edit, Delete; узел из одних запретов удаляется целиком); при true —
наоборот. У реквизитных вложенных ту же роль играет
setForAttributesByDefault. Конфликт решается в пользу разрешения.

Отсюда три правки, общие для обоих навыков:

1. Прямое замыкание идёт только от РАЗРЕШЁННЫХ прав. Раньше запрет тянул
   зависимости как разрешения: "Catalog.X: {Edit: false}" выдавал
   Read, Update и View — навык раздавал права на основании запрета.
2. Появилось обратное замыкание: запрет уносит права, которым
   запрещённое нужно. Сверено с платформой — при setForNewObjects=true
   она к Update=false дописывает те же десять запретов.
3. Записи, совпавшие с умолчанием роли, не пишутся: платформа их всё
   равно выбросит, а файл разошёлся бы с базой. Отброшенное
   перечисляется в stderr, сообщение операции объясняет причину.

Правило применяется только там, где замерено: внешние источники данных
под него не попадают. Обе функции заведены семьями в check-inline-drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGkXwoXTuafcu1SXMsauFq
This commit is contained in:
Nick Shirokov
2026-09-13 20:00:06 +03:00
co-authored by Claude Opus 5
parent b28c044e06
commit 9d21ad3429
35 changed files with 1709 additions and 38 deletions
+10 -4
View File
@@ -129,10 +129,16 @@ 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}}`.
Роль хранит только то, что отличается от её умолчаний, — всё совпавшее навык не пишет и говорит
об этом. При умолчаниях (`setForNewObjects: false`, `setForAttributesByDefault: true`) это значит:
объектам верхнего уровня права выдают, а реквизитам, ТЧ, измерениям и ресурсам — только
запрещают, потому что те наследуют права своего объекта:
```json
{"name": "Catalog.Товары.Attribute.Цена", "rights": {"View": false}}
```
Запрет тянет за собой права, которым запрещённое нужно: `View: false` у реквизита уносит `Edit`.
Роль расширения, включённая в основные (`DefaultRoles`), прав на заимствованные объекты давать не может — платформа это запрещает. Такие права выноси в отдельную роль вне основных.
@@ -1,4 +1,4 @@
# role-compile v1.41 — Compile 1C role from JSON
# role-compile v1.42 — Compile 1C role from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
@@ -901,32 +901,73 @@ $script:configurationLegacyDeps = @("AnalyticsSystemClient","MainWindowModeEmbed
$script:configurationLegacyRank = 218
# Замыкание набора прав объекта. Возвращает @{ Rights = <итог>; Added = <что дописано> }.
# Платформа хранит только то, что ОТЛИЧАЕТСЯ от значения по умолчанию для роли: при
# setForNewObjects=false на верхнем уровне живут разрешения, при true — запреты; у реквизитных
# вложенных объектов ту же роль играет setForAttributesByDefault. Совпавшее с умолчанием
# платформа выбрасывает при первой же загрузке, поэтому не пишем его и сами.
$script:attributeKinds = @(
"Attribute","StandardAttribute","TabularSection","StandardTabularSection",
"Dimension","Resource","AccountingFlag","ExtDimensionAccountingFlag","AddressingAttribute"
)
function Get-DefaultRightValue {
param([string]$objName, [string]$setForNewObjects, [string]$setForAttributesByDefault)
$parts = $objName -split '\.'
if ($parts.Count -lt 3) { return $setForNewObjects }
# Внешние источники данных под это правило не проверялись — трогаем только то, что замерено.
if ($parts[0] -eq 'ExternalDataSource') { return "false" }
$kind = $parts[$parts.Count-2]
if ($script:attributeKinds -contains $kind) { return $setForAttributesByDefault }
# Команды, подсистемы, операции сервисов флагами роли не управляются — там живут разрешения.
return "false"
}
function Close-RightsDependencies {
param([string]$objName, $rights, [int]$formatRank)
$parts = $objName -split '\.'
# У вложенных объектов (реквизит, ТЧ, измерение) зависимостей нет — платформа их не трогает.
if ($parts.Count -ge 3) { return @{ Rights = $rights; Added = @() } }
$nested = $parts.Count -ge 3
$objectType = $parts[0]
$allowed = $script:knownRights[$objectType]
$allowed = if ($nested) { Get-NestedRights -objectType $objectType -kind (Get-NestedKind $objName) }
else { $script:knownRights[$objectType] }
if (-not $allowed) { return @{ Rights = $rights; Added = @() } }
$have = [ordered]@{}
foreach ($r in $rights) { if (-not $have.Contains($r.Name)) { $have[$r.Name] = $r } }
$byType = $script:rightDepsByType[$objectType]
$added = @()
$queue = @($have.Keys)
# Вперёд — только от РАЗРЕШЁННЫХ прав: платформа замыкает выданное, а не запрещённое.
$queue = @($have.Keys | Where-Object { $have[$_].Value -eq "true" })
while ($queue.Count -gt 0) {
$name = $queue[0]
$queue = @($queue | Select-Object -Skip 1)
$need = if ($byType -and $byType.Contains($name)) { $byType[$name] } else { $script:rightDeps[$name] }
if (-not $need) { continue }
foreach ($dep in $need) {
if ($have.Contains($dep)) { continue }
if ($allowed -notcontains $dep) { continue }
if ($have.Contains($dep)) {
# Разрешение перебивает запрет — так поступает и платформа при загрузке.
if ($have[$dep].Value -ne "true") { $have[$dep].Value = "true"; $added += $dep; $queue += $dep }
continue
}
$have[$dep] = @{ Name = $dep; Value = "true"; Condition = $null }
$added += $dep
$queue += $dep
}
}
# Назад — от ЗАПРЕТОВ: право, которому запрещённое нужно, платформа запрещает следом.
$denyQueue = @($have.Keys | Where-Object { $have[$_].Value -ne "true" })
while ($denyQueue.Count -gt 0) {
$name = $denyQueue[0]
$denyQueue = @($denyQueue | Select-Object -Skip 1)
foreach ($candidate in $allowed) {
if ($candidate -eq $name) { continue }
$need = if ($byType -and $byType.Contains($candidate)) { $byType[$candidate] } else { $script:rightDeps[$candidate] }
if (-not $need -or $need -notcontains $name) { continue }
if ($have.Contains($candidate)) { continue }
$have[$candidate] = @{ Name = $candidate; Value = "false"; Condition = $null }
$added += $candidate
$denyQueue += $candidate
}
}
if ($objectType -eq 'Configuration' -and $formatRank -le $script:configurationLegacyRank -and $have.Count -gt 0) {
foreach ($dep in $script:configurationLegacyDeps) {
if ($have.Contains($dep)) { continue }
@@ -1479,6 +1520,19 @@ foreach ($o in $parsedObjects) {
$parsedObjects = @(Sort-ObjectsByUuid -objects $parsedObjects -configRoot $resolvedOutputDir)
foreach ($o in $parsedObjects) { $o.Rights = @(Sort-RightsCanonical -objName $o.Name -rights $o.Rights) }
# Записи, равные умолчанию роли, платформа не хранит — отбрасываем их сами и говорим об этом.
$droppedByDefault = @()
foreach ($obj in $parsedObjects) {
$defaultValue = Get-DefaultRightValue $obj.Name $sfno $sfab
$kept = @()
foreach ($right in $obj.Rights) {
if ($right.Value -eq $defaultValue) { $droppedByDefault += "$($obj.Name).$($right.Name)"; continue }
$kept += ,$right
}
$obj.Rights = $kept
}
$parsedObjects = @($parsedObjects | Where-Object { $_.Rights.Count -gt 0 })
# Object blocks
$totalRights = 0
foreach ($obj in $parsedObjects) {
@@ -1748,6 +1802,9 @@ Write-Host " UUID: $uuid"
Write-Host " Metadata: $metadataPath"
Write-Host " Rights: $rightsPath"
Write-Host " Objects: $($parsedObjects.Count), Rights: $totalRights, Templates: $templateCount"
if ($droppedByDefault.Count -gt 0) {
[Console]::Error.WriteLine("[role-compile] Не записаны права, совпадающие с умолчанием роли (платформа их не хранит): $($droppedByDefault -join ', ')")
}
foreach ($note in $closureNotes) { Write-Host $note }
switch ($regResult) {
"added" { Write-Host " Configuration.xml: <Role>$roleName</Role> added to ChildObjects" }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# role-compile v1.41 — Compile 1C role from JSON
# role-compile v1.42 — Compile 1C role from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -839,14 +839,37 @@ CONFIGURATION_LEGACY_DEPS = ["AnalyticsSystemClient", "MainWindowModeEmbeddedWor
CONFIGURATION_LEGACY_RANK = 218
# Платформа хранит только то, что ОТЛИЧАЕТСЯ от значения по умолчанию для роли: при
# setForNewObjects=false на верхнем уровне живут разрешения, при true — запреты; у реквизитных
# вложенных объектов ту же роль играет setForAttributesByDefault. Совпавшее с умолчанием
# платформа выбрасывает при первой же загрузке, поэтому не пишем его и сами.
ATTRIBUTE_KINDS = [
"Attribute", "StandardAttribute", "TabularSection", "StandardTabularSection",
"Dimension", "Resource", "AccountingFlag", "ExtDimensionAccountingFlag", "AddressingAttribute",
]
def get_default_right_value(object_name, set_for_new_objects, set_for_attributes_by_default):
parts = object_name.split('.')
if len(parts) < 3:
return set_for_new_objects
# Внешние источники данных под это правило не проверялись — трогаем только то, что замерено.
if parts[0] == 'ExternalDataSource':
return "false"
kind = parts[-2]
if kind in ATTRIBUTE_KINDS:
return set_for_attributes_by_default
# Команды, подсистемы, операции сервисов флагами роли не управляются — там живут разрешения.
return "false"
def close_rights_dependencies(object_name, rights, format_rank):
"""Замыкание набора прав объекта. Возвращает (итоговые права, что дописано)."""
parts = object_name.split('.')
# У вложенных объектов (реквизит, ТЧ, измерение) зависимостей нет — платформа их не трогает.
if len(parts) >= 3:
return rights, []
nested = len(parts) >= 3
object_type = parts[0]
allowed = KNOWN_RIGHTS.get(object_type)
allowed = (get_nested_rights(object_type, get_nested_kind(object_name)) if nested
else KNOWN_RIGHTS.get(object_type))
if not allowed:
return rights, []
have = {}
@@ -854,18 +877,39 @@ def close_rights_dependencies(object_name, rights, format_rank):
have.setdefault(r['Name'], r)
by_type = RIGHT_DEPS_BY_TYPE.get(object_type, {})
added = []
queue = list(have.keys())
# Вперёд — только от РАЗРЕШЁННЫХ прав: платформа замыкает выданное, а не запрещённое.
queue = [n for n in have if have[n]['Value'] == 'true']
while queue:
name = queue.pop(0)
need = by_type[name] if name in by_type else RIGHT_DEPS.get(name)
if not need:
continue
for dep in need:
if dep in have or dep not in allowed:
if dep not in allowed:
continue
if dep in have:
# Разрешение перебивает запрет — так поступает и платформа при загрузке.
if have[dep]['Value'] != 'true':
have[dep]['Value'] = 'true'
added.append(dep)
queue.append(dep)
continue
have[dep] = {'Name': dep, 'Value': 'true', 'Condition': None}
added.append(dep)
queue.append(dep)
# Назад — от ЗАПРЕТОВ: право, которому запрещённое нужно, платформа запрещает следом.
deny_queue = [n for n in have if have[n]['Value'] != 'true']
while deny_queue:
name = deny_queue.pop(0)
for candidate in allowed:
if candidate == name or candidate in have:
continue
need = by_type[candidate] if candidate in by_type else RIGHT_DEPS.get(candidate)
if not need or name not in need:
continue
have[candidate] = {'Name': candidate, 'Value': 'false', 'Condition': None}
added.append(candidate)
deny_queue.append(candidate)
if object_type == 'Configuration' and format_rank <= CONFIGURATION_LEGACY_RANK and have:
for dep in CONFIGURATION_LEGACY_DEPS:
if dep in have:
@@ -1730,6 +1774,19 @@ def main():
for o in parsed_objects:
o['Rights'] = sort_rights_canonical(o['Name'], o['Rights'])
# Записи, равные умолчанию роли, платформа не хранит — отбрасываем их сами и говорим об этом.
dropped_by_default = []
for obj in parsed_objects:
default_value = get_default_right_value(obj['Name'], sfno, sfab)
kept = []
for right in obj['Rights']:
if right['Value'] == default_value:
dropped_by_default.append(f"{obj['Name']}.{right['Name']}")
continue
kept.append(right)
obj['Rights'] = kept
parsed_objects = [o for o in parsed_objects if o['Rights']]
# Object blocks
total_rights = 0
for obj in parsed_objects:
@@ -1799,6 +1856,9 @@ def main():
print(f" Metadata: {metadata_path}")
print(f" Rights: {rights_path}")
print(f" Objects: {len(parsed_objects)}, Rights: {total_rights}, Templates: {template_count}")
if dropped_by_default:
print("[role-compile] Не записаны права, совпадающие с умолчанием роли "
f"(платформа их не хранит): {', '.join(dropped_by_default)}", file=sys.stderr)
for note in closure_notes:
print(note)
if reg_result == 'added':
+11 -4
View File
@@ -87,10 +87,17 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-edit.ps1" -Rol
отчёта — `Use`. Снятие работает в обратную сторону: снимаешь `Read` — уходят и права, которые его
требуют. Дописанное и снятое каскадом перечисляется в выводе.
Реквизиты, табличные части, измерения и ресурсы по умолчанию наследуют права своего объекта, и
выдавать их отдельно бессмысленно — платформа такую запись не сохранит. Ограничивают их через
`deny-rights`. Выдавать права реквизитам поштучно имеет смысл только в роли с
`setForAttributesByDefault=false`.
Роль хранит только то, что отличается от её умолчаний, — всё совпавшее навык не пишет и говорит
об этом:
| Где | Что имеет смысл |
|-----|-----------------|
| объект верхнего уровня, `setForNewObjects=false` (умолчание) | выдача прав; запрет бессмыслен |
| объект верхнего уровня, `setForNewObjects=true` | запрет; выдача бессмысленна |
| реквизит, ТЧ, измерение, ресурс, `setForAttributesByDefault=true` (умолчание) | запрет: права наследуются от объекта |
| они же при `setForAttributesByDefault=false` | и выдача, и запрет |
Запрет тянет за собой права, которым запрещённое нужно: `View=false` у реквизита уносит `Edit`.
Тип или имя права вне допустимого списка — отказ: файл не меняется, ошибки печатаются все разом.
Полные таблицы «тип → права» и виды вложенности — в справке `/role-compile`.
+67 -7
View File
@@ -1,4 +1,4 @@
# role-edit v1.2 — Edit existing 1C role rights in place
# role-edit v1.3 — Edit existing 1C role rights in place
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
@@ -854,32 +854,73 @@ $script:configurationLegacyDeps = @("AnalyticsSystemClient","MainWindowModeEmbed
$script:configurationLegacyRank = 218
# Замыкание набора прав объекта. Возвращает @{ Rights = <итог>; Added = <что дописано> }.
# Платформа хранит только то, что ОТЛИЧАЕТСЯ от значения по умолчанию для роли: при
# setForNewObjects=false на верхнем уровне живут разрешения, при true — запреты; у реквизитных
# вложенных объектов ту же роль играет setForAttributesByDefault. Совпавшее с умолчанием
# платформа выбрасывает при первой же загрузке, поэтому не пишем его и сами.
$script:attributeKinds = @(
"Attribute","StandardAttribute","TabularSection","StandardTabularSection",
"Dimension","Resource","AccountingFlag","ExtDimensionAccountingFlag","AddressingAttribute"
)
function Get-DefaultRightValue {
param([string]$objName, [string]$setForNewObjects, [string]$setForAttributesByDefault)
$parts = $objName -split '\.'
if ($parts.Count -lt 3) { return $setForNewObjects }
# Внешние источники данных под это правило не проверялись — трогаем только то, что замерено.
if ($parts[0] -eq 'ExternalDataSource') { return "false" }
$kind = $parts[$parts.Count-2]
if ($script:attributeKinds -contains $kind) { return $setForAttributesByDefault }
# Команды, подсистемы, операции сервисов флагами роли не управляются — там живут разрешения.
return "false"
}
function Close-RightsDependencies {
param([string]$objName, $rights, [int]$formatRank)
$parts = $objName -split '\.'
# У вложенных объектов (реквизит, ТЧ, измерение) зависимостей нет — платформа их не трогает.
if ($parts.Count -ge 3) { return @{ Rights = $rights; Added = @() } }
$nested = $parts.Count -ge 3
$objectType = $parts[0]
$allowed = $script:knownRights[$objectType]
$allowed = if ($nested) { Get-NestedRights -objectType $objectType -kind (Get-NestedKind $objName) }
else { $script:knownRights[$objectType] }
if (-not $allowed) { return @{ Rights = $rights; Added = @() } }
$have = [ordered]@{}
foreach ($r in $rights) { if (-not $have.Contains($r.Name)) { $have[$r.Name] = $r } }
$byType = $script:rightDepsByType[$objectType]
$added = @()
$queue = @($have.Keys)
# Вперёд — только от РАЗРЕШЁННЫХ прав: платформа замыкает выданное, а не запрещённое.
$queue = @($have.Keys | Where-Object { $have[$_].Value -eq "true" })
while ($queue.Count -gt 0) {
$name = $queue[0]
$queue = @($queue | Select-Object -Skip 1)
$need = if ($byType -and $byType.Contains($name)) { $byType[$name] } else { $script:rightDeps[$name] }
if (-not $need) { continue }
foreach ($dep in $need) {
if ($have.Contains($dep)) { continue }
if ($allowed -notcontains $dep) { continue }
if ($have.Contains($dep)) {
# Разрешение перебивает запрет — так поступает и платформа при загрузке.
if ($have[$dep].Value -ne "true") { $have[$dep].Value = "true"; $added += $dep; $queue += $dep }
continue
}
$have[$dep] = @{ Name = $dep; Value = "true"; Condition = $null }
$added += $dep
$queue += $dep
}
}
# Назад — от ЗАПРЕТОВ: право, которому запрещённое нужно, платформа запрещает следом.
$denyQueue = @($have.Keys | Where-Object { $have[$_].Value -ne "true" })
while ($denyQueue.Count -gt 0) {
$name = $denyQueue[0]
$denyQueue = @($denyQueue | Select-Object -Skip 1)
foreach ($candidate in $allowed) {
if ($candidate -eq $name) { continue }
$need = if ($byType -and $byType.Contains($candidate)) { $byType[$candidate] } else { $script:rightDeps[$candidate] }
if (-not $need -or $need -notcontains $name) { continue }
if ($have.Contains($candidate)) { continue }
$have[$candidate] = @{ Name = $candidate; Value = "false"; Condition = $null }
$added += $candidate
$denyQueue += $candidate
}
}
if ($objectType -eq 'Configuration' -and $formatRank -le $script:configurationLegacyRank -and $have.Count -gt 0) {
foreach ($dep in $script:configurationLegacyDeps) {
if ($have.Contains($dep)) { continue }
@@ -1452,6 +1493,18 @@ $script:ns = New-Object System.Xml.XmlNamespaceManager($script:xmlDoc.NameTable)
$script:ns.AddNamespace("rt", $script:mdNs)
$script:formatVersion = if ($script:root.HasAttribute("version")) { $script:root.GetAttribute("version") } else { "2.17" }
$script:formatRank = Get-FormatRank $script:formatVersion
# Умолчания роли решают, какие записи платформа хранит: совпавшее с умолчанием она выбрасывает.
$script:roleSfno = $script:root.SelectSingleNode("rt:setForNewObjects", $script:ns).InnerText
$script:roleSfab = $script:root.SelectSingleNode("rt:setForAttributesByDefault", $script:ns).InnerText
$script:droppedByDefault = @()
function Test-RightStored {
param([string]$objName, [string]$rightName, [string]$value)
$default = Get-DefaultRightValue $objName $script:roleSfno $script:roleSfab
if ($value -ne $default) { return $true }
$script:droppedByDefault += "$objName.$rightName"
return $false
}
$script:addCount = 0
$script:removeCount = 0
@@ -1737,6 +1790,7 @@ function Apply-AddRights($spec) {
$added = @()
$indent = Get-ChildIndent $objNode
foreach ($rightName in $final) {
if (-not (Test-RightStored $spec.Name $rightName 'true')) { continue }
$node = Find-RightNode $objNode $rightName
if ($node) {
if ((Get-RightNodeValue $node) -ne 'true') {
@@ -1790,6 +1844,7 @@ function Apply-SetRights($spec) {
$closure = Close-RightsDependencies -objName $spec.Name -rights @($spec.Rights | ForEach-Object { @{ Name = $_; Value = "true"; Condition = $null } }) -formatRank $script:formatRank
$indent = Get-ChildIndent $objNode
foreach ($rightName in @($closure.Rights | ForEach-Object { $_.Name })) {
if (-not (Test-RightStored $spec.Name $rightName 'true')) { continue }
$new = New-RightNode $rightName 'true' $indent
Insert-RightCanonical $objNode $new $spec.Name
$script:addCount++
@@ -1873,6 +1928,7 @@ function Apply-DenyRights($spec) {
$denied = @()
$indent = Get-ChildIndent $objNode
foreach ($rightName in $toDeny) {
if (-not (Test-RightStored $spec.Name $rightName 'false')) { continue }
$node = Find-RightNode $objNode $rightName
if ($node) {
if ((Get-RightNodeValue $node) -eq 'false') { continue }
@@ -1887,7 +1943,8 @@ function Apply-DenyRights($spec) {
$script:rightsDirty = $true
}
if ($denied.Count -eq 0) {
Add-Note " $($spec.Name): права уже запрещены, изменений нет"
$reason = if ($script:droppedByDefault -match [regex]::Escape($spec.Name)) { "запрет совпадает с умолчанием роли и платформой не хранится" } else { "права уже запрещены" }
Add-Note " $($spec.Name): $reason, изменений нет"
return
}
$cascade = @($denied | Where-Object { $spec.Rights -notcontains $_ })
@@ -2301,6 +2358,9 @@ Write-Host "[OK] Роль '$($script:paths.RoleName)' обновлена"
Write-Host " Rights: $($script:rightsPath)"
foreach ($note in $script:notes) { Write-Host $note }
Write-Host " Added: $($script:addCount), Removed: $($script:removeCount), Modified: $($script:modifyCount)"
if ($script:droppedByDefault.Count -gt 0) {
[Console]::Error.WriteLine("[role-edit] Не записаны права, совпадающие с умолчанием роли (платформа их не хранит): $($script:droppedByDefault -join ', ')")
}
if (-not $NoValidate) {
$validateScript = Join-Path (Join-Path $PSScriptRoot "..\..\role-validate") "scripts\role-validate.ps1"
+74 -8
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# role-edit v1.2 — Edit existing 1C role rights in place
# role-edit v1.3 — Edit existing 1C role rights in place
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -794,14 +794,37 @@ CONFIGURATION_LEGACY_DEPS = ["AnalyticsSystemClient", "MainWindowModeEmbeddedWor
CONFIGURATION_LEGACY_RANK = 218
# Платформа хранит только то, что ОТЛИЧАЕТСЯ от значения по умолчанию для роли: при
# setForNewObjects=false на верхнем уровне живут разрешения, при true — запреты; у реквизитных
# вложенных объектов ту же роль играет setForAttributesByDefault. Совпавшее с умолчанием
# платформа выбрасывает при первой же загрузке, поэтому не пишем его и сами.
ATTRIBUTE_KINDS = [
"Attribute", "StandardAttribute", "TabularSection", "StandardTabularSection",
"Dimension", "Resource", "AccountingFlag", "ExtDimensionAccountingFlag", "AddressingAttribute",
]
def get_default_right_value(object_name, set_for_new_objects, set_for_attributes_by_default):
parts = object_name.split('.')
if len(parts) < 3:
return set_for_new_objects
# Внешние источники данных под это правило не проверялись — трогаем только то, что замерено.
if parts[0] == 'ExternalDataSource':
return "false"
kind = parts[-2]
if kind in ATTRIBUTE_KINDS:
return set_for_attributes_by_default
# Команды, подсистемы, операции сервисов флагами роли не управляются — там живут разрешения.
return "false"
def close_rights_dependencies(object_name, rights, format_rank):
"""Замыкание набора прав объекта. Возвращает (итоговые права, что дописано)."""
parts = object_name.split('.')
# У вложенных объектов (реквизит, ТЧ, измерение) зависимостей нет — платформа их не трогает.
if len(parts) >= 3:
return rights, []
nested = len(parts) >= 3
object_type = parts[0]
allowed = KNOWN_RIGHTS.get(object_type)
allowed = (get_nested_rights(object_type, get_nested_kind(object_name)) if nested
else KNOWN_RIGHTS.get(object_type))
if not allowed:
return rights, []
have = {}
@@ -809,18 +832,39 @@ def close_rights_dependencies(object_name, rights, format_rank):
have.setdefault(r['Name'], r)
by_type = RIGHT_DEPS_BY_TYPE.get(object_type, {})
added = []
queue = list(have.keys())
# Вперёд — только от РАЗРЕШЁННЫХ прав: платформа замыкает выданное, а не запрещённое.
queue = [n for n in have if have[n]['Value'] == 'true']
while queue:
name = queue.pop(0)
need = by_type[name] if name in by_type else RIGHT_DEPS.get(name)
if not need:
continue
for dep in need:
if dep in have or dep not in allowed:
if dep not in allowed:
continue
if dep in have:
# Разрешение перебивает запрет — так поступает и платформа при загрузке.
if have[dep]['Value'] != 'true':
have[dep]['Value'] = 'true'
added.append(dep)
queue.append(dep)
continue
have[dep] = {'Name': dep, 'Value': 'true', 'Condition': None}
added.append(dep)
queue.append(dep)
# Назад — от ЗАПРЕТОВ: право, которому запрещённое нужно, платформа запрещает следом.
deny_queue = [n for n in have if have[n]['Value'] != 'true']
while deny_queue:
name = deny_queue.pop(0)
for candidate in allowed:
if candidate == name or candidate in have:
continue
need = by_type[candidate] if candidate in by_type else RIGHT_DEPS.get(candidate)
if not need or name not in need:
continue
have[candidate] = {'Name': candidate, 'Value': 'false', 'Condition': None}
added.append(candidate)
deny_queue.append(candidate)
if object_type == 'Configuration' and format_rank <= CONFIGURATION_LEGACY_RANK and have:
for dep in CONFIGURATION_LEGACY_DEPS:
if dep in have:
@@ -1501,10 +1545,20 @@ class Editor:
self.modify_count = 0
self.notes = []
self.pending = []
# Умолчания роли решают, какие записи платформа хранит: совпавшее с умолчанием она выбрасывает.
self.role_sfno = node_text(self.root, "setForNewObjects")
self.role_sfab = node_text(self.root, "setForAttributesByDefault")
self.dropped_by_default = []
def note(self, text):
self.notes.append(text)
def right_stored(self, obj_name, right_name, value):
if value != get_default_right_value(obj_name, self.role_sfno, self.role_sfab):
return True
self.dropped_by_default.append(f"{obj_name}.{right_name}")
return False
# --- Разбор значений операций ---
def parse_batch(self, value):
@@ -1751,6 +1805,8 @@ class Editor:
indent = self.child_indent(obj_node)
for right in closed:
name = right["Name"]
if not self.right_stored(spec["Name"], name, "true"):
continue
node = self.find_right(obj_node, name)
if node is not None:
if node_text(node, "value") != "true":
@@ -1792,6 +1848,8 @@ class Editor:
self.format_rank)
indent = self.child_indent(obj_node)
for right in closed:
if not self.right_stored(spec["Name"], right["Name"], "true"):
continue
new_el = self.make_right(right["Name"], "true", indent)
self.insert_right_canonical(obj_node, new_el, spec["Name"])
self.add_count += 1
@@ -1852,6 +1910,8 @@ class Editor:
denied = []
indent = self.child_indent(obj_node)
for right_name in to_deny:
if not self.right_stored(spec["Name"], right_name, "false"):
continue
node = self.find_right(obj_node, right_name)
if node is not None:
if node_text(node, "value") == "false":
@@ -1865,7 +1925,10 @@ class Editor:
denied.append(right_name)
self.rights_dirty = True
if not denied:
self.note(f" {spec['Name']}: права уже запрещены, изменений нет")
reason = ("запрет совпадает с умолчанием роли и платформой не хранится"
if any(d.startswith(spec['Name'] + '.') for d in self.dropped_by_default)
else "права уже запрещены")
self.note(f" {spec['Name']}: {reason}, изменений нет")
return
cascade = [r for r in denied if r not in spec["Rights"]]
note = f" {spec['Name']}: запрещено — {', '.join(denied)}"
@@ -2218,6 +2281,9 @@ def main():
for note in ed.notes:
print(note)
print(f" Added: {ed.add_count}, Removed: {ed.remove_count}, Modified: {ed.modify_count}")
if ed.dropped_by_default:
print("[role-edit] Не записаны права, совпадающие с умолчанием роли "
f"(платформа их не хранит): {', '.join(ed.dropped_by_default)}", file=sys.stderr)
if not args.NoValidate:
validate_script = os.path.normpath(os.path.join(