diff --git a/.claude/skills/role-compile/SKILL.md b/.claude/skills/role-compile/SKILL.md
index 9fcf5b54b..33b3052cf 100644
--- a/.claude/skills/role-compile/SKILL.md
+++ b/.claude/skills/role-compile/SKILL.md
@@ -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`), прав на заимствованные объекты давать не может — платформа это запрещает. Такие права выноси в отдельную роль вне основных.
diff --git a/.claude/skills/role-compile/scripts/role-compile.ps1 b/.claude/skills/role-compile/scripts/role-compile.ps1
index d61319dc6..0dcb35ea9 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.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: $roleName added to ChildObjects" }
diff --git a/.claude/skills/role-compile/scripts/role-compile.py b/.claude/skills/role-compile/scripts/role-compile.py
index 6b5dd5136..09b54abc1 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.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':
diff --git a/.claude/skills/role-edit/SKILL.md b/.claude/skills/role-edit/SKILL.md
index eb2821e4b..077949a71 100644
--- a/.claude/skills/role-edit/SKILL.md
+++ b/.claude/skills/role-edit/SKILL.md
@@ -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`.
diff --git a/.claude/skills/role-edit/scripts/role-edit.ps1 b/.claude/skills/role-edit/scripts/role-edit.ps1
index ddf7e7952..e54e39d64 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.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"
diff --git a/.claude/skills/role-edit/scripts/role-edit.py b/.claude/skills/role-edit/scripts/role-edit.py
index 763c6cfa7..783c219f7 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.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(
diff --git a/tests/skills/cases/role-compile/default-value-filter.json b/tests/skills/cases/role-compile/default-value-filter.json
new file mode 100644
index 000000000..93caaaf45
--- /dev/null
+++ b/tests/skills/cases/role-compile/default-value-filter.json
@@ -0,0 +1,27 @@
+{
+ "name": "Права, совпадающие с умолчанием роли, не пишутся — платформа их не хранит",
+ "setup": "fixture:view-preset-fx",
+ "input": {
+ "name": "УмолчанияРоли",
+ "synonym": "Умолчания роли",
+ "objects": [
+ {
+ "name": "Catalog.Номенклатура",
+ "preset": "view",
+ "rights": {
+ "Delete": false
+ }
+ }
+ ]
+ },
+ "validatePath": "Roles/УмолчанияРоли",
+ "expect": {
+ "stderrContains": [
+ "Не записаны права, совпадающие с умолчанием роли"
+ ],
+ "fileNotContains": {
+ "file": "Roles/УмолчанияРоли/Ext/Rights.xml",
+ "text": "false"
+ }
+ }
+}
diff --git a/tests/skills/cases/role-compile/nested-and-services.json b/tests/skills/cases/role-compile/nested-and-services.json
index a091488bd..bd8f3336e 100644
--- a/tests/skills/cases/role-compile/nested-and-services.json
+++ b/tests/skills/cases/role-compile/nested-and-services.json
@@ -14,7 +14,8 @@
"ExternalDataSource.Источник.Table.Заказы: Read, View",
"ExternalDataSource.Источник.Table.Заказы.Field.Сумма: View, Edit",
"ExternalDataSource.Источник.Cube.Продажи.Dimension.Период: View"
- ]
+ ],
+ "setForAttributesByDefault": false
},
"expect": {
"files": [
diff --git a/tests/skills/cases/role-compile/snapshots/default-value-filter/Catalogs/Номенклатура.xml b/tests/skills/cases/role-compile/snapshots/default-value-filter/Catalogs/Номенклатура.xml
new file mode 100644
index 000000000..0daed60e9
--- /dev/null
+++ b/tests/skills/cases/role-compile/snapshots/default-value-filter/Catalogs/Номенклатура.xml
@@ -0,0 +1,91 @@
+
+
+
+
+
+ UUID-002
+ UUID-003
+
+
+ UUID-004
+ UUID-005
+
+
+ UUID-006
+ UUID-007
+
+
+ UUID-008
+ UUID-009
+
+
+ UUID-010
+ UUID-011
+
+
+
+ Номенклатура
+
+
+ ru
+ Номенклатура
+
+
+
+ false
+ HierarchyFoldersAndItems
+ false
+ 2
+ true
+ true
+
+ ToItems
+ 9
+ 25
+ String
+ Variable
+ WholeCatalog
+ false
+ true
+ AsDescription
+
+ Auto
+ InDialog
+ false
+ BothWays
+
+ Catalog.Номенклатура.StandardAttribute.Description
+ Catalog.Номенклатура.StandardAttribute.Code
+
+ Begin
+ DontUse
+ Directly
+
+
+
+
+
+
+
+
+
+
+ false
+
+
+ Managed
+ Use
+
+
+
+
+
+ Use
+ Auto
+ DontUse
+ false
+ false
+
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/role-compile/snapshots/default-value-filter/Catalogs/Номенклатура/Ext/ObjectModule.bsl b/tests/skills/cases/role-compile/snapshots/default-value-filter/Catalogs/Номенклатура/Ext/ObjectModule.bsl
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/skills/cases/role-compile/snapshots/default-value-filter/Configuration.xml b/tests/skills/cases/role-compile/snapshots/default-value-filter/Configuration.xml
new file mode 100644
index 000000000..b706f2440
--- /dev/null
+++ b/tests/skills/cases/role-compile/snapshots/default-value-filter/Configuration.xml
@@ -0,0 +1,254 @@
+
+
+
+
+
+ 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/default-value-filter/DataProcessors/Загрузка.xml b/tests/skills/cases/role-compile/snapshots/default-value-filter/DataProcessors/Загрузка.xml
new file mode 100644
index 000000000..fa5caaf26
--- /dev/null
+++ b/tests/skills/cases/role-compile/snapshots/default-value-filter/DataProcessors/Загрузка.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+ UUID-002
+ UUID-003
+
+
+ UUID-004
+ UUID-005
+
+
+
+ Загрузка
+
+
+ ru
+ Загрузка
+
+
+
+ true
+
+
+ false
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/role-compile/snapshots/default-value-filter/DataProcessors/Загрузка/Ext/ManagerModule.bsl b/tests/skills/cases/role-compile/snapshots/default-value-filter/DataProcessors/Загрузка/Ext/ManagerModule.bsl
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/skills/cases/role-compile/snapshots/default-value-filter/DataProcessors/Загрузка/Ext/ObjectModule.bsl b/tests/skills/cases/role-compile/snapshots/default-value-filter/DataProcessors/Загрузка/Ext/ObjectModule.bsl
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/skills/cases/role-compile/snapshots/default-value-filter/Ext/ClientApplicationInterface.xml b/tests/skills/cases/role-compile/snapshots/default-value-filter/Ext/ClientApplicationInterface.xml
new file mode 100644
index 000000000..3c1161b2d
--- /dev/null
+++ b/tests/skills/cases/role-compile/snapshots/default-value-filter/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/default-value-filter/Languages/Русский.xml b/tests/skills/cases/role-compile/snapshots/default-value-filter/Languages/Русский.xml
new file mode 100644
index 000000000..37c60d786
--- /dev/null
+++ b/tests/skills/cases/role-compile/snapshots/default-value-filter/Languages/Русский.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Русский
+
+
+ ru
+ Русский
+
+
+
+ ru
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/role-compile/snapshots/default-value-filter/Roles/УмолчанияРоли.xml b/tests/skills/cases/role-compile/snapshots/default-value-filter/Roles/УмолчанияРоли.xml
new file mode 100644
index 000000000..f8a4fb260
--- /dev/null
+++ b/tests/skills/cases/role-compile/snapshots/default-value-filter/Roles/УмолчанияРоли.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ УмолчанияРоли
+
+
+ ru
+ Умолчания роли
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/role-compile/snapshots/default-value-filter/Roles/УмолчанияРоли/Ext/Rights.xml b/tests/skills/cases/role-compile/snapshots/default-value-filter/Roles/УмолчанияРоли/Ext/Rights.xml
new file mode 100644
index 000000000..7cccfe3b5
--- /dev/null
+++ b/tests/skills/cases/role-compile/snapshots/default-value-filter/Roles/УмолчанияРоли/Ext/Rights.xml
@@ -0,0 +1,21 @@
+
+
+ false
+ true
+ false
+
+
\ 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
index c0b3e410b..192882641 100644
--- 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
@@ -1,7 +1,7 @@
false
- true
+ false
false