Conversation
Review of #479, finding 37 (after nested naming moved to #480): - access[] with a path and namespaceRules is refused: the generator would name the Role access-to-<module>-<name> in templates/<path>/rbac-to-us.yaml, where the placement rule wants access-to-<dir>-; bootstrap keeps such a Role hand-written instead of producing access-to-<module>-access-to-...; - an account named <module>-<dir> is accepted only in a namespace of the platform, and there without namespaceRules or bindRoles, whose objects the placement rule would name <module>:<dir>; - a test runs the placement rule over what the generator writes for the shapes this version supports. The platform namespaces now live in rbaccontract, shared with the placement rule. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
8da8b0e to
9f5da1d
Compare
…ry documents
- A document counts as rendered only by an object of the same template,
in the namespace it names if it names one: a computed name such as
{{ .Chart.Name }} matched any object of its kind in the module, and a
conditional document then fell out of the "not rendered" note.
- Several library documents in one template are one answer only when
their conditions agree, as for documents matched by name: an include
under a condition beside one without otherwise gave its objects no
condition, and a capability it renders no TODO.
Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…library's When the library documents of a template disagree on the condition, Locate still answers that a library renders the object, its condition unknown: the object stays unmanaged and its file hand-written instead of being imported as the module's own, and a legacy role or capability sync owns by class gets a TODO condition, so the run stays red. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…ry documents
- A document counts as rendered only by an object of the same template,
in the namespace it names if it names one: a computed name such as
{{ .Chart.Name }} matched any object of its kind in the module, and a
conditional document then fell out of the "not rendered" note.
- Several library documents in one template are one answer only when
their conditions agree, as for documents matched by name: an include
under a condition beside one without otherwise gave its objects no
condition, and a capability it renders no TODO.
Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…library's When the library documents of a template disagree on the condition, Locate still answers that a library renders the object, its condition unknown: the object stays unmanaged and its file hand-written instead of being imported as the module's own, and a legacy role or capability sync owns by class gets a TODO condition, so the run stays red. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
d413db8 to
5f50bd4
Compare
| validateWhen(d.PrometheusAccess.When, "prometheusAccess", report) | ||
| validateObjectLabels(d.PrometheusAccess.Labels, "prometheusAccess.labels", report) | ||
|
|
||
| for kind, names := range map[string][]string{"deployments": d.PrometheusAccess.Deployments, "daemonsets": d.PrometheusAccess.DaemonSets, "statefulsets": d.PrometheusAccess.StatefulSets} { |
There was a problem hiding this comment.
🔵 11. prometheusAccess workload findings come out in random order
range over a map literal (deployments/daemonsets/statefulsets) makes the order of these validation errors vary between runs. The texts are the same, so dedupe is fine, but output and golden tests are not stable -- the same class as finding 7. A slice of pairs fixes it.
There was a problem hiding this comment.
No change needed: ValidateFor sorts the errors by their text before returning (validate.go:220), and each message names its list and index, so the map order never reaches the output.
Follow-up of #479 (review, scope): what was taken out of the pilot PR because the ADR does not describe it yet. - bootstrap reads the template text around each object: {{ if }} becomes `when`, objects in range/with/define and helm_lib includes stay hand-written, objects no render showed are named and fail the fix; - serviceAccounts[].annotations/rbacAnnotations, access[].when, labels and annotations on access and prometheusAccess, compared by sync and imported by bootstrap; - nested account paths with placement-conform names, the namespace label on every module namespace but default, the edit-distance misspelling warning; - validation refusals (built-in scopes, */<sub>, capability texts, names, empty values, bare functions in when) and the refusal of global rbac settings in a module .dmtlint.yaml. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…apabilities Review of #479, findings 33-35, for the follow-up: - 35: the template reader is text/template/parse instead of patterns: a condition is the pipeline as the parser prints it (string literals kept, `{{if(.x)}}` and else-if chains read), a `---` or an object inside a comment is none, metadata is read at any indentation and in flow style, and a document whose name cannot be read matches no object. - 34: a document with a computed name that no render showed is listed as the template writes it. - 33: a capability or a legacy role under a condition leaves a TODO reason on the resources it grants: resources[] have no `when`, and the regenerated role would render for every value. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…ry documents
- A document counts as rendered only by an object of the same template,
in the namespace it names if it names one: a computed name such as
{{ .Chart.Name }} matched any object of its kind in the module, and a
conditional document then fell out of the "not rendered" note.
- Several library documents in one template are one answer only when
their conditions agree, as for documents matched by name: an include
under a condition beside one without otherwise gave its objects no
condition, and a capability it renders no TODO.
Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…library's When the library documents of a template disagree on the condition, Locate still answers that a library renders the object, its condition unknown: the object stays unmanaged and its file hand-written instead of being imported as the module's own, and a legacy role or capability sync owns by class gets a TODO condition, so the run stays red. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A declaration that does not parse or validate, one the generator cannot turn into objects, a broken module.yaml, a declaration in an edition overlay and an rbac.yaml of the earlier shape are reported by the linter without an autofix; --fix no longer attaches a fix that fails on purpose to them. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The fix that writes the first rbac.yaml and the one that appends a coverage stub return nil once the file is written; the TODO values they leave are lint findings without a fix, reported by the lint that follows --fix. The TODO finding of coverage no longer carries a fix that fails on purpose. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…ader, no Dropped The templates carry no generator header and no list of owned objects, and there is no aside copy for a file kept by hand. A file the declaration produces is rewritten from it by --fix unless the lint finds a case in it that only a change of the templates or of the declaration closes -- an object the declaration does not produce, one it puts in another file, a document the linter cannot read, the version gate, the legacy scheme; the finding then names the case and carries no fix. The case is read from the template text, which is the same in every render variant, and the render adds what the text cannot show; a variant that finds one keeps the fix of every other variant off the file. A template the declaration produces nothing for is a finding without a fix: the autofix deletes no file. Dropped goes from the object store: the render already warns about a template it skipped, and sync does not report the objects the text of a template holds when nothing rendered from it. The text of a file is no longer compared byte for byte; the render is judged. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
… on purpose No rbac fix fails on purpose any more, so the exit on a fix error whatever the finding's level goes, with Manager.HasFailedFixes and LintRuleErrorsList.ContainsFailedFixes: after --fix the lint reports what is left at its own level, as for every other linter. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The coverage and sync sections say what is a lint finding without a fix and what the autofix does, without the generator header, the aside copies, the orphan deletion or a --fix that fails on purpose. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…e texts once per check Check builds a syncRun -- where the declaration puts each object, the rendered objects the rule owns, the rendered objects by template and the text of every template -- and the steps read it instead of building the same maps and reading the same files again. No change in what is reported or fixed. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
templateTexts dropped a template it could not read, so the fix found no case in it and wrote over a file the lint never saw. Its read error is kept, and the file is a case without a fix: what it holds is unknown. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
resetFixState held fixState while it took fixOutcomes, the reverse of fixOnce, whose fix takes fixState under fixOutcomes: the two could deadlock. It now takes them in turn; the comment on fixOutcomes says why it is held across the fix, file I/O included. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
An object in the wrong file was reported up to three times in one finding: by the divergence, by the case of the text, by the render seen from the other file and by the text of the other file. The divergence states the fact once and the case says the one thing to do (move X to Y, move X here from Z). The legacy RBACv2 scheme is explained once, by the case that keeps the fix off. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The doc of SyncRule and CoverageRule, the fix state and writeBootstrapped describe the lint-or-fix model; the note on an object kept in a file the declaration writes says the file gets no fix until it is moved, not that the fix drops it or writes a copy beside it; a role without rules is no longer said to be dropped by a fix it may not get; the bootstrap parse error is one wrapped sentence; regenerateFix is rewriteFix, and the comments say rewrite. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The capability marker, a ServiceAccount name and a label or annotation key are checked with validation.IsValidLabelValue, IsDNS1123Subdomain and IsQualifiedName instead of regular expressions of their own. IsQualifiedName is stricter: it enforces the 63-character name and the 253-character prefix the message already stated, where the expression accepted a key with an 80-character name. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Two regressions that no code path can break any more (no file is deleted; the move is covered by TestSync_ObjectHeldByAnotherTemplateIsNotWrittenTwice) and a copy of TestSync_FileWithForeignObjectsIsNotRegenerated go; assertLintOnly also requires that no fix is attached; fixOnce is tested with a call counter; the removal-log regression checks that the change is logged as a removal. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The comment on top of the declaration bootstrap writes no longer says it was written by dmt: it says what the file is and that its TODOs are decisions dmt lint reports; the notes stay. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
writeBootstrapped returned an error when its own output did not parse, although the file was written: a fix that did its work and failed. It writes and returns nil; an rbac.yaml that does not parse is a finding of the next lint, with the line. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Findings, notes and the README say what --fix writes and what the declaration produces or accepts instead of what a generator does: nothing is compared or written until the declaration parses, keys are set by dmt or by Helm, an object will be named by --fix, the templates are written from rbac.yaml. The e2e case sync-fix-regenerates is sync-fix-writes-missing-file. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A scope left as TODO was reported by coverage and, as an invalid scope, by the validation of sync. The validation reports it as undecided, as it does a TODO when, and stops sync on it -- the templates depend on the scope; coverage reports the TODO noAccess and reason values. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The finding said every TODO and note in the written rbac.yaml is a decision for a person before --fix rewrites the templates, which nothing enforced. It says what holds: every TODO is a decision dmt lint reports, and the notes say what the declaration does not carry. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A when may call a helper of the chart (include "cert_manager.yandex_dns_configured" .), so the condition line --fix writes for it held an include and the lint read the document as unreadable: the file could never get a fix. Only an abort of the render (fail, required) in a condition line makes a document someone else's. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A module may ship a role of its own outside the role model that aggregates capabilities by a label of the module (state-snapshotter's backup agent role, agreed with the ADR author), and a capability may have an action other than the one of its level (download_snapshots; the platform has kubernetes access_terminal). The declaration could say neither, and a label of the module on a capability was dropped by --fix and by bootstrap without a finding: sync compared the labels of the role model only. capabilities.<lineage>.<action> takes a level, which an action of its own requires and a resource entry grants under that action, and labels, none of them dmt's. The generator writes both, bootstrap reads them back from the render, and sync compares every label of a capability but helm_lib's. The e2e case sync-module-role holds such a role, its capabilities and the alias of its old name. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…ries What sync checks on a capability and what the bootstrap notes as dropped now say that the labels of the module are compared and carried into the capabilities entry. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…on a file without a declaration A module.yaml that does not parse and a CRD document that does not parse are findings of the module and openapi linters; sync and coverage stop on the first and skip the second without reporting them again. An rbac.yaml that holds no declaration of this version -- an empty document, the earlier shape -- passed the load, so coverage attached the stub fix to it and the fix failed on a file that is not a mapping; coverage now leaves such a file to the finding of sync. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The strict check of the rbac configuration keys in the shared config loader, the refusal of a module .dmtlint.yaml that sets global.linters-settings.rbac, the nolintlint setting and the change of GetFixes for ignored findings are gone: none of them is needed by the rbac rules, and each changed dmt for every linter or for rbac alone. So that --fix does not rewrite files on behalf of findings nobody sees, a rule of the declaration at the ignored level is not run. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A variant reports a file with its fix before a later variant withholds it, and dmt has no point after the last variant where a rule could revise its findings; the README says so, what the fix then does and when it happens. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…on across the render A rewrite dropped or widened rights without a finding in several cases, and a grant declared under a condition that already holds was never written. - A file whose text holds a condition the declaration does not write, an object exclude-rules.sync keeps out of the comparison, a label or annotation of a legacy role or a capability the format cannot hold, the secrets of a ServiceAccount, and a template that is a symbolic link now keep the fix off the file, and the finding names the case. The localized texts of a capability are compared. - A condition holds when anything the declaration writes under it renders, in any file, rules included: an absent object or rule under it is drift the fix writes. - The template reader keeps a string literal with a brace inside a condition and a field named required, so every file the declaration produces reads back as its own. - The coverage stub is not offered for a symbolic link, the rewrite log prints empty lists, and a missing file is logged among the additions; an empty rbac.yaml is named as empty. - Tests that could not fail now make the file diverge or differ on disk first, so that a fix that ran or was kept off shows. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Bootstrap took the last render variant of an object whose rules differ between --matrix variants and did not count a variant with no RBAC object, so a conditional right could be written as undecided and removed by the next fix; the variants now give the union, marked as varied by a block. A scrape Role became prometheusAccess whatever it granted, narrowing verbs or writing an empty section; it now does so only for get on the named workloads. An object left hand-written by several passes was listed, and counted, once per pass. The generator writes no system capability for a module without a subsystem: it would aggregate into no role and fail the contract. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A component path is a clean relative path of DNS labels, the name of an access entry or an extra role a valid role name, and a when a single line: otherwise the file the declaration names never matches the render, or the rewrite writes a document the lint cannot read back. A trailing --- holds no document. An own action without a level gets one finding, and a view or edit key names the level that grants it. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The README says what now keeps the fix off a file, that a condition is judged across the render, where the namespace label goes, what the coverage stub does to the layout of the file and when sync stays silent. The e2e README lists sync-module-role, and two coverage cases describe the level they expect. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…ly what keeps its metadata A condition the declaration writes around an object or a rule that the template lacked was never reported: the rule rendered for every value, so it counted as holding. The conditions each object and its rules render under are now read from the text of the template and of what the declaration writes, per object, both ways: a missing one is a divergence the fix writes, an extra one a divergence without a fix. The text is the same in every variant, so a template gated by a condition the declaration does not write is a finding even where nothing of it renders. An object under an old name counted as renamed on its rules and subjects alone, so a rewrite dropped its rbac.authorization.k8s.io aggregation labels and helm.sh/resource-policy and logged nothing; it is a rename now only when it has no aggregationRule and the new object keeps every label and annotation outside the role model's own. The legacy-scheme case names the smallest kind the file renders, not the one map order gives. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
An object written in two branches of one block inside one document, or as a named and a computed name in two branches, was given the condition of the first branch found. Such an object now stays hand-written with the reason, and Locate refuses candidates of the named and the computed kind that disagree. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…te writes The labels of accounts, access entries and the scrape access are held to valid values and may not set heritage or module; bound roles and cluster roles to role names and a namespace to a DNS label; ServiceAccount subjects to their name and namespace rules; scraped workloads to DNS subdomains. They rendered and linted before and failed only on install. The comment on Validate says what coverage does with a declaration that does not validate. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
… and the new validation Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A module may ship a subsystem of its own (virtualization: module.yaml subsystems, its d8:subsystem:virtualization:<level> roles and the capabilities that aggregate into it). The contract called that lineage unknown and the validation of rbac.yaml refused it in subsystems. A subsystem the module's module.yaml declares beyond the seven the platform ships now has the levels of every subsystem for that module; another module's own subsystem stays unknown. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A module.yaml subsystem beyond the platform's counted as the module's own whatever it was, so a typo silenced the contract and reached the declaration check and the generator. It is the module's own now only when the render holds a d8:subsystem:<name>:<level> role labelled with the module; any other is a contract finding on module.yaml, and sync validates and derives against the filtered list. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A block opened inside a document and still open at the next separator
was left off that document, so its object read back as unconditional.
It belongs to its own document now when document content follows the
opening line; the generator's {{- if X }} right before the next ---
still belongs to the next object only, which a round trip over the
generated files checks.
Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
a7ec1e4 to
1d770f8
Compare
| // renderedSubsystems are the subsystems the module renders a d8:subsystem:<name>:<level> ClusterRole | ||
| // for, labelled as the module's own. | ||
| func renderedSubsystems(m pkg.Module) map[string]bool { | ||
| rendered := map[string]bool{} |
There was a problem hiding this comment.
🔵 12. A subsystem whose roles render conditionally is "unrendered" in the variants where they don't render
renderedSubsystems reads the current render variant only. If a module's own d8:subsystem:<name>:* roles are rendered under a condition (the version gate, a values flag), then in a --matrix variant where they don't render the contract rule reports the subsystem on module.yaml as unrendered, and knownSubsystems (sync.go:159) drops it, so ValidateFor rejects the rbac.yaml subsystems: entry and sync says "nothing is compared or written until the declaration is valid" for that variant: a false finding plus a blind sync.
This comes from reading the code only; I did not reproduce it. If no module gates its own subsystem roles (virtualization being the known case), feel free to drop it.
There was a problem hiding this comment.
No change needed: --matrix varies only the module's own values (buildOverride keys the overrides by the module name), and the version gate reads global.deckhouseVersion, which is dev/test in every render, so the gate always renders the new roles. No module gates its own subsystem roles on a module value: none in deckhouse main declares one, and virtualization has no d8:subsystem:virtualization:* roles yet.
Description
Stacked on #479, and the final version of the module RBAC declaration: #479 is the skeleton and is not merged without this PR. Together they add
modules/<module>/rbac.yaml, one declaration of a module's RBAC, and three rules of therbaclinter around it --contract,coverageandsync-- with--fixwriting the first declaration from the render and the templates from the declaration. The full description ispkg/linters/rbac/README.md, section "The module RBAC declaration"; what follows is what the two PRs deliver, and what this one changes in the skeleton.The declaration.
rbac.deckhouse.io/v1alpha1, unknown keys refused, validated before anything is compared or written:resources[]-- user-facing access to every resource the module ships, in both role models: RBACv2namespaceandsystemlevels (capabilitiesd8:namespace-capability:<module>:<action>andd8:system-capability:<module>:<action>) and legacyuser-authzlevels (d8:user-authz:<module>:<level>); explicit verbs,whenconditions,noAccesswith a reason,scopeandreasonwhere the linter cannot know them.capabilities-- keyed<lineage>.<action>: localized texts outside the view/edit convention; a capability with an action of its own (download_snapshots, as the platform'saccess_terminal), which a resource entry grants under its action andlevelplaces in a level of the lineage;labelsof the module, by which a role of the module outside the role model aggregates its capabilities.serviceAccounts[]with cluster and namespace rules, bindings to existing roles,extraClusterRoles,labels,annotationsandrbacAnnotations,pathfor component directories (nested ones included),automountServiceAccountToken;access[]for arbitrary subjects andprometheusAccessfor the scraper, both withlabels,annotationsandwhen.The rules.
contractruns on every module, with or withoutrbac.yaml: the rendered ClusterRoles undertemplates/rbacv2/follow the platform's label and naming contract. It is a port of the platform'stesting/rbacv2validation, so an external module is checked as an in-tree one; a module still on the scheme before DKP 1.78 gets one "migrate" finding per object. A subsystem the module's ownmodule.yamldeclares beyond the seven the platform ships (virtualization) is a lineage of that module, for itsd8:subsystem:<name>:*roles, its capabilities and thesubsystemsofrbac.yaml; another module's own subsystem stays unknown.coverage: every CRD undercrds/has an entry that grants levels or denies access with a reason.--fixappends an undecidednoAccess: "TODO"stub, which the lint then reports.sync: the render and the declaration say the same thing, in both directions -- rules as(apiGroup, resource, resourceName, verb)tuples, aggregation edges, the labels of the module and the localized texts of a capability, roleRefs and subjects. Awhenexcuses an absent object or rule only while the condition is false: when anything the declaration writes under it renders, in any file, it holds, and a grant newly declared under it is written by--fix. The conditions around each object and its rules are also compared with the text of the template, both ways, so every render variant agrees: a condition the declaration writes that the template lacks is written by--fix, one the template holds that the declaration does not write keeps the fix off the file. It owns three classes of objects (legacy roles, the module's own capabilities, the objects whose names the declaration builds); everything else in the render is neither written nor reported. The render is judged, not the text of a template.Either a lint finding or a fix that succeeds. Every case is one or the other, and this PR moves every refusal of the skeleton's autofix to lint time:
module.yaml, a declaration in an edition overlay and anrbac.yamlof the earlier shape are findings without a fix.rbac.yaml,--fixwrites it from the render and succeeds; theTODOvalues it leaves are reported by the lint that follows. The coverage stub works the same way.range, a computed name, afail/requiredguard), the version gate, the legacy scheme, a condition ({{ if }}) the declaration does not write, an objectexclude-rules.synckeeps out of the comparison, a label or annotation of a legacy role or a capability the format cannot hold, the secrets of a ServiceAccount, a template that is a symbolic link. The finding then names the case and carries no fix: a rewrite never drops or widens what sync does not compare. The case is read from the template text, which is the same in every render variant; under--matrixa variant that finds one keeps the fix of every other variant off the file.--fixexits by the level of what is left, as for every other linter. The object store keeps noDroppedlist: the render already warns about a template it skipped.The first declaration. Bootstrap reads the template text around each rendered object with
text/template/parse:{{ if X }}becomeswhen: X, an{{ else }}branchnot (X), nested blocks anand; a condition with a template variable, and an account whose objects render under different conditions, become aTODO. Objects insiderange,withordefine, ones a named template renders (helm_lib), objects of a subchart and roles without rules stay hand-written; an RBAC object the text holds but no render showed is named in a note on top of the written file. Labels and annotations are imported where the format holds them and named in a note where it does not.Configuration.
global.linters-settings.rbac.rules.{contract,coverage,sync}.impactsets each rule's level, read from the root.dmtlint.yamlas every dmt setting;linters-settings.rbac.exclude-rules.{contract,coverage,sync}excludes objects orgroup/resourcekeys. The three rules start atwarnwherever nothing sets them, and a rule atignoredis not run, so--fixrewrites nothing on behalf of findings nobody sees; the four originalrbacrules keep their level and their output. Outside the linter the PRs change only the wiring of these levels and exclusions, the registration of the rules, andno-cyrillic, which leaves alone the module'srbac.yamland theru.meta.deckhouse.io/title|descriptionlines the role model requires -- Cyrillic anywhere else in those files is still reported.Behaviour to know.
error, as for every linter:rules: {sync: {impact: ignore}}(forignored) runs sync aterror, and a misspelled rule key such ascoverge:is dropped. The rbac blocks get no stricter key check than the other linters.--fixdoes not lint again (cmd/dmt/main.go): a coverage stub is a fix that succeeds, sodmt lint --fixexits 0 with coverage aterror, and the next plain run reports theTODOit wrote.not (X)for an{{ else }}branch andand (A) (B)for nested blocks, which the declaration writes as{{- if <when> }}; until the template's blocks are rewritten that way by hand, such a file is a finding without a fix (the template holds a condition the declaration does not write).Verified.
-race, golangci-lint 2.13. The e2e cases undertest/e2e/testdata/rbac/cover the three schemes a module can be on (legacy, 1.78, both behind the version gate), a bootstrap, a clean declaration, a hand edit, a foreign object that keeps the fix off, a missing file the fix writes, a template the render skips, and a role of the module outside the role model with its capabilities and the alias of its old name.main, 41 modules):dmt lint --linter rbacgives no errors; the 40 warnings are 37 declarations still missing and 3 contract findings (a cluster-scoped resource in a namespace capability). The cert-manager pilot, declared inrbac.yaml, is clean. Bootstrap on every module writes a declaration that parses, and no fix fails.Why do we need it, and what problem does it solve?
A module's RBAC lives in four places nobody keeps in step: the RBACv2 capability templates, the legacy
user-authzroles,rbac-for-us.yamlandrbac-to-us.yaml. On the platform tree 66 of 181 CRDs have no user-facing access at all, and nothing says whether that is a decision or an omission. A change to the platform's role contract is a hand edit of every module, and the platform test that checks the contract sees only in-tree modules, so external modules drift until aggregation breaks in a cluster.The declaration makes the decision explicit per resource, and the templates follow from it. The linter is where this belongs:
dmtalready renders the chart, sosynccompares rendered objects rather than template text, and an external module gets the same check as an in-tree one from the tool it already runs. Design: ADRplatform-security/2026-04-27-module-rbac-yaml.md(architecture-decision-records).