Conversation
…t constants rbacyaml loads and validates modules/<module>/rbac.yaml (rbac.deckhouse.io/v1alpha1): strict keys, one entry per resource with the levels of both role models or a documented denial, scope from the module's CRDs or declared for external resources, verbs listed explicitly, localized texts required for capabilities outside the view/edit convention, and a reason wherever the declaration widens access beyond what the resource alone implies (a whole group, a namespaced resource at a system level). Normalization gives the generator and the sync rule a canonical order. rbaccontract holds the lineages, per-lineage levels, legacy access levels, label keys and the conventional localized texts, each with its source in the deckhouse repository. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…on in rbac.yaml coverage runs only when the module has an rbac.yaml. It reads the CRDs under crds/ at any depth, selecting them by kind, and requires an entry for each: levels of either role model, or noAccess with the reason. A CRD without an entry gets an autofix that appends an undecided stub (noAccess: "TODO") and then still reports the finding -- the stub is not a decision, and a --fix run that wrote stubs must not end green. A stub left as TODO is an error without a fix; a resource of a known group that no CRD spells is a warning. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
… contract on rendered objects contract ports the first part of deckhouse/testing/rbacv2/rbacv2_templates_validation_test.go to the rendered ClusterRoles under templates/rbacv2/, so that a module outside the platform repository is held to the same contract: the d8: prefix, the four localized annotations, kind and scope labels, the shape of roles and capabilities, aggregation labels with a lineage and a level of that lineage, delegatable only on namespace/project roles. New here, as a warning for now: a cluster-scoped resource inside a namespace capability grants nothing through the RoleBinding it is bound with. The scope comes from the module's CRDs or its rbac.yaml; a resource the run knows nothing about is not judged. The rule needs no rbac.yaml. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…per-rule levels and exclusions
The root configuration now accepts linters-settings.rbac.rules.{coverage,sync,contract}.impact, and
the module configuration exclude-rules.{coverage,contract,sync}; the levels fall back to the
linter's. The four original rbac rules keep the linter level as before: wiring them up would change
the severity of existing findings. Both new rules are registered in the static scope.
Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…plates rendered from it generate.Build derives the RBAC objects a declaration produces (namespace and system capabilities, legacy roles, ServiceAccount rights, external access) with their names, labels, localized texts and rules, following the ADR table; generate.Render writes them as Helm templates under a header the sync autofix recognizes. The model is what the sync rule compares the rendered chart against, so the comparison and the generated text can never disagree. Golden files for the cert-manager example pin the output; rendering is a pure function of the model. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…in both directions, and --fix regenerates the templates sync compares the rendered objects of the three classes it owns (legacy roles, the module's capabilities, the objects the generator names) with the model built from the declaration: rules as (group, resource, name, verb) tuples, aggregation edges of capabilities, binding subjects and role references, the marker and module/namespace labels the generator writes. A rule under when that did not render is not a divergence; a rule without when that did not render is. Findings are one per template file and carry the fix command; an object the declaration does not produce in a file it does not generate is a person's decision. The autofix regenerates a file from the declaration with two safeguards: a file without the generator header is maintained by hand and gets the generated text beside it as <file>.generated; a regeneration that would drop a right or an aggregation edge the render grants today is refused with the list of what would be lost. Everything the fix needs is captured while the render exists. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…through helm_lib The cases vendor deckhouse_lib_helm as the container cases do, and every one asserts that the module rendered (expectPass on the manager linter): without it a render failure would satisfy any expectPass. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
viper drops an unknown key without a word; for the per-rule levels and exclusions of the rbac linter that silence leaves a rule at full strength -- or off -- with nobody noticing. The two rbac blocks (global.linters-settings.rbac with its rules, linters-settings.rbac with its exclude-rules) are held to their known keys; the other linters keep the lenient behaviour. Adds the mapping test for the per-rule levels: coverage, sync and contract read their own level from the root configuration and fall back to the linter's, the four original rules keep the linter level. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…nd sync rules Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
A rule switched off with impact: ignore still had its fix closures collected and run by --fix, so files could change on behalf of findings nobody sees. GetFixes now skips findings at the ignored level. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…spec - contract: the module label of a framework role or capability must be the module's name (R21); helpers without a kind label, such as d8:dict, are not judged. - sync: a generated file whose header names another contract version is a divergence and is regenerated (R40); an rbac.yaml inside an edition overlay (ee/modules, ee/be/modules, ...) is an error, the declaration lives in modules/<module>/ only (R8a, D7). - coverage and sync autofixes run once per target however many render variants reported it (R36); the render variants record what they grant at lint time and the sync fix judges the union, so a right rendered only under some values is not dropped (D3). - rbac.yaml: a when condition must parse as a Helm expression (R13c). - e2e: the hand-labelled contract cases carry the module label. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
contract, coverage and sync are new to every tree: a module without rbac.yaml sees only contract, and the platform tree still carries six dead rbac.yaml files of an older shape and rules the contract flags. Like the style rules of the documentation linter they now default to warn, whatever impact the rbac linter has, and a tree raises them to error in its root .dmtlint.yaml once its modules are clean. The four original rules keep the linter level. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…e that serves both A module meets the new rules in one of three states: only the manage/use scheme that preceded the 1.78 role model (an external module not yet migrated), only the 1.78 scheme, or both behind the version gate that rbacv2-migrate-module.sh writes (include "<module>.rbacv2_new_scheme"). - contract: a legacy object (kind: use or manage) gets one finding -- migrate -- instead of failing every check of the contract; a legacy object rendered from a gated template is not reported, the module serves both models on purpose. - sync: when a generated file's template renders the legacy scheme, the finding names that as the cause once instead of listing the absent objects as a mystery; the legacy objects are not reported as extra. - sync --fix: a gated template is never regenerated -- regenerating it would drop the legacy branch -- and the refusal says so; the gate is detected by its helper name, not only by a literal deckhouseVersion. The check runs before the header guard, so a gated file is not called hand-maintained. - e2e: one case per state (scheme-legacy-only, scheme-legacy-with-declaration, scheme-dual, the last written by the platform's migration script). Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The RBACv2 role model requires a Russian title and description on every role and capability (ru.meta.deckhouse.io/*), and the rbac contract rule enforces them; the files rule then reported the same lines as Cyrillic in source. 41 in-tree modules carry an identical exclude-rules entry for templates/rbacv2 to reconcile the two, and every external module would need one too. The files rule now skips those annotation lines; Cyrillic anywhere else in the same file is still reported. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Thirteen changes a module goes through were run against the rules: a CRD added, removed, renamed or rescoped; a verb dropped from the declaration; a rule added to a template by hand; a generated file or the declaration deleted; subsystems changed; a subresource and a whole-group entry; a rule under when. Three did not hold: - coverage: a noAccess entry whose group has no CRD left in the module was silent -- a removed CRD is indistinguishable from an external resource nobody grants. It is a warning now unless the entry names a scope. - sync: a rule under when whose condition is false today is absent from the render without being a divergence (D4), so it never reached the template through --fix. For a file that carries the generator header the text is now compared with what the declaration renders; a file without the header is still judged by its render only. A file of another contract version is the same check. - sync --fix: the copy written beside a hand-maintained file lived inside templates/, and Helm renders every file there whatever its extension, so the module rendered a second set of objects. The copy is _<file>.generated now: an underscore-prefixed file is a partial to Helm. The drop-guard refusal names the way out: delete the file and run --fix again to regenerate it without the right. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…rgence The full loop on an external module -- every RBAC template deleted, --fix asked to write them back -- lost three ServiceAccounts and 88 rules without a finding: the files held only objects under when, the conditions were true and the objects rendered before, and once the files were gone their absence from the render read as "conditional, not rendered" (D4). The render cannot tell a false condition from a template nobody wrote; the text can. A file the declaration produces that does not exist while an object it holds is absent from the render is now reported and created by --fix. A file that exists is judged as before. The sync tests write the generated files to disk before simulating the render, as a module that rendered them would have them. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…ger showed Running the whole cycle -- declaration from the rendered objects, every RBAC template regenerated -- on three platform modules found three things: - sync owned every capability that carried the module label, including the ones the declaration cannot produce: the project lineage of multitenancy-manager and the platform-wide capabilities of user-authz, which are named after a lineage rather than the module. They were about to be reported as objects the declaration does not produce. The capability class is now the module's own namespace and system capabilities by name; the rest stays hand-written and unreported (D2). - a generated file is written whole, so an object in it that the declaration does not produce -- a controller ClusterRole beside a declared ServiceAccount (cert-manager's webhook, user-authz's permission browser) -- would vanish with the rewrite, and the advice to delete the file and run --fix again would lose it too. The fix now refuses first of all when the file holds such objects and names them; every render variant's list is joined. - no-cyrillic judged rbac.yaml for the Russian titles and descriptions the declaration requires of capabilities outside the view/edit convention; it is the module's documentation, like module.yaml, and is skipped. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
… from its render
Decided with the platform owner on 2026-09-22, after the loop on four modules:
- bootstrap: a module without rbac.yaml gets a sync finding, and --fix writes
the declaration from the RBAC objects the module renders today -- the file
a person would have transcribed from the templates, with a TODO wherever a
decision is still theirs (a resource with no CRD and an unknown scope, a
CRD nobody grants, a namespaced resource granted cluster-wide) and a note
for every object the generator will name differently or cannot describe.
From then on rbac.yaml is the source and the templates follow it. This
replaces R22 ("contract only without a declaration") and the ADR's "coverage
creates the file".
- the declaration wins: a right it no longer names leaves the template on
--fix, the finding that led there having listed it. This replaces D3
("autofix never removes a right"), whose danger -- the first run deleting
what nobody had declared yet -- is gone once the declaration starts from
the render.
- serviceAccounts[].extraClusterRoles: further ClusterRoles in the account's
file, bound to it or not (cert-manager's per-controller roles, an
aggregated apiserver's requester role), named d8:<module>:<account>:<name>
or exactly as given when they start with d8:.
- serviceAccounts[].automountServiceAccountToken: the generator no longer
forces false; the import keeps true where the account mounted its token,
so no pod loses it.
- access[].path: cluster and namespace grants may live in a component's
rbac-for-us.yaml / rbac-to-us.yaml, where the module kept them.
- the foreign-objects safeguard recognizes an object the generator produces
under another name -- a binding with the same roleRef and subjects, a role
with the same rules -- as replaced, not lost.
On operator-trivy, cert-manager, user-authz and multitenancy-manager the flow
now runs end to end: every RBAC template regenerated from the written
declaration, the rendered rights identical before and after up to the dead
clusterissuers rule of cert-manager and the renames the generator's naming
forces.
Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…nd the declaration as the source Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…n CI) Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
4 tasks
Fifteen corner cases were run against the rules on a synthetic module (a
module without subsystems or with a non-d8 namespace, a marker past 63
characters, an account named unlike its directory and nested directories,
a declared object rendered from another file, when conditions with braces,
core-group resources and wildcard verbs, BOM and CRLF in rbac.yaml and in a
generated file, an rbac.yaml of the earlier shape, duplicate objects, a
bootstrap with a conditional object off by default, a read-only template,
duplicate subjects). Nine did not hold:
- system levels on a module that aggregates into no subsystem produced
capabilities nobody aggregates; the generator refuses and asks for
subsystems in rbac.yaml.
- a capability marker longer than a label value (a module name of 32
characters and up with a namespace superadmin level) was written and
would fail the contract; the generator refuses.
- an account named unlike its directory, or in a nested directory, produced
objects the placement rule rejects; the generator refuses with the names
placement wants.
- an object the declaration produces but rendered from another file was
called foreign without saying where it belongs; the refusal now names the
file the declaration puts it in.
- built-in Kubernetes resources (""/configmaps, apps/deployments, ...)
needed an explicit scope; the validator and the importer share one table
of well-known scopes, and an unknown resource still asks for one.
- an rbac.yaml of the earlier, never consumed shape gave a bare parse error;
it is named for what it is, with the way out.
- under --matrix the first declaration was written from whichever variant
ran its fix first; it is written from the union of every variant, and the
file's header says that objects off by default need a run with
--values-file before the first regeneration.
- a generated file with CRLF line endings lost its header and became
hand-maintained in silence; the header is recognized and the file
regenerated.
- duplicate subjects of an access entry passed validation.
Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…he corner-case behaviour Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
The ADR (ccd35e0) names it among the drawbacks: a --fix run without a preceding dmt lint would remove rights the declaration no longer names with nothing said. The autofix now logs, per regenerated file, the rights and objects the render had and the declaration did not name, and logs the plain regeneration otherwise. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Review of #479, finding 38: for an account of templates/<dir>/ in default or kube-system the placement rule wants d8-<module>-<dir>, which the generator refuses. It is older than this round; the README says so and names the modules it keeps out. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
…t hold Review of #479, finding 40: after the scope cut bootstrap dropped conditions, annotations and labels with only the generic header as a warning. Without bringing the features back, the written file now names per object: - the labels and annotations the format has no field for (werf.io hooks, helm.sh/resource-policy, gatekeeper.sh/system, k8s-app ...); heritage, module, an account's app label, Helm's meta.helm.sh keys and the generator's own are not listed; - under --matrix, every object some render variants did not render: its condition is not in the declaration. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Jabejixo
added a commit
that referenced
this pull request
Sep 24, 2026
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>
Jabejixo
added a commit
that referenced
this pull request
Sep 24, 2026
…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>
AlwxSin
reviewed
Sep 24, 2026
- 41: an object only some render variants rendered stays hand-written where the declaration has no `when` for it (access entries, the scrape access); an account with such objects gets a TODO `when`. The note that asked for a `when` the format could not hold is gone. - 42: an object of the module in a file that also holds a document a helm_lib include renders stays hand-written: the generator writes the whole file and could never regenerate it. - 43: the generator refuses an account of a component directory in default and kube-system, naming the known limitation, instead of steering into a name the placement rule rejects. - 46: the cloud-data-discoverer Role in kube-system is listed among the limits in the README. The placement test says it cannot prove the root shapes while the placement rule skips the root files (45). Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Review of #479, finding 41 (completion): the note removed in e394b0e also covered capabilities and legacy roles only some render variants rendered. They cannot stay hand-written (sync owns them by class) and resources[] have no `when`, so the entries they grant get a TODO reason and the run stays red until someone decides. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Jabejixo
added a commit
that referenced
this pull request
Sep 24, 2026
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>
Jabejixo
added a commit
that referenced
this pull request
Sep 24, 2026
…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>
AlwxSin
reviewed
Sep 24, 2026
- 47: a `when` excuses an absent declared object only while it is false: when another object of the file under the same `when` rendered, the absent one is reported. - 48: a partially rendered object kept hand-written in a file the declaration also writes is named, with the way out (a file of its own before --fix). - 49: bootstrap keeps an account of a component directory in default or kube-system hand-written, with what binds it, instead of writing an entry the generator refuses. - 50: the library-file mark comes from the render (another object of the template is the library's), not from the text; holdsLibraryDocument is gone. - 51: a TODO `when` is reported as an open decision, and the TODO for an account that always renders does not invite narrowing it. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Jabejixo
added a commit
that referenced
this pull request
Sep 24, 2026
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>
Jabejixo
added a commit
that referenced
this pull request
Sep 24, 2026
…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>
AlwxSin
reviewed
Sep 24, 2026
- 52: under --matrix the variants that rendered each object are kept; objects of an account that render in other variants than the account get a TODO saying no single `when` holds them, instead of asking for one (node-manager's cluster-autoscaler). - 53: a Role counts as an account's own, or an access entry's, only when that binding is its only one; a ClusterRole's bindings include the RoleBindings to it. A shared role stays hand-written with its bindings, whatever order the accounts come in. - 48: the note says to move the object to another component directory. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Jabejixo
added a commit
that referenced
this pull request
Sep 24, 2026
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>
Jabejixo
added a commit
that referenced
this pull request
Sep 24, 2026
…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>
AlwxSin
approved these changes
Sep 25, 2026
Jabejixo
marked this pull request as ready for review
September 25, 2026 07:17
The four rbac cases that render the generated templates carried their own copy of deckhouse_lib_helm-1.72.1.tgz; they now symlink test/e2e/lib like the container case does. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Jabejixo
added a commit
that referenced
this pull request
Sep 25, 2026
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>
Jabejixo
added a commit
that referenced
this pull request
Sep 25, 2026
…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>
AlwxSin
reviewed
Sep 29, 2026
The TODO about the entries after d8-system moves onto rbaccontract.DeckhouseNamespaces, where the list now lives. The .tpl case of the no-cyrillic YAML check is dropped: .tpl files never reach checkFile, and checking them would be a behaviour change of its own. Signed-off-by: Ivan Zvyagintsev <ivan.zvyagintsev@flant.com>
Jabejixo
added a commit
that referenced
this pull request
Sep 29, 2026
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>
Jabejixo
added a commit
that referenced
this pull request
Sep 29, 2026
…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>
AlwxSin
approved these changes
Sep 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Do not merge without #480. This PR is the skeleton of the module RBAC declaration; the final version is #480, stacked on it, and the two are merged together (#480 into this branch, then this branch into
main). The full description of what the two deliver is in #480.Why two PRs: the skeleton is about 13 000 lines of code, tests and docs and was reviewed as such. #480 then reworks its autofix and extends its format -- about 6 000 lines added and 2 700 removed on top of it. As a diff against the reviewed skeleton, #480 shows exactly what changed in the behavior of
--fixand why; squashed into one PR, that change would not be visible at all, only the final code. Several parts of the skeleton are replaced there, so this branch is not a state to release on its own:# dmt:ownslist in the templates, the_<file>.generatedcopy beside a file kept by hand, the deletion of orphan files and fixes that fail on purpose -- in [dmt] feat: module rbac.yaml declaration, final version (stacked on #479) #480 every case is either a lint finding without a fix or a fix that succeeds;Droppedlist of the object store and the byte-for-byte comparison of a generated file's text -- in [dmt] feat: module rbac.yaml declaration, final version (stacked on #479) #480 the render is judged, not the text;What the skeleton lays down, and #480 keeps:
modules/<module>/rbac.yaml(rbac.deckhouse.io/v1alpha1): user-facing access to every resource the module ships in both role models (RBACv2 capabilities and legacyuser-authzroles), the rights of its ServiceAccounts, the access it grants to others. It is validated before anything is compared or generated: explicit verb lists,namespacelevels for namespaced resources only, ascopewhere the module ships no CRD, areasonfor whole-group entries and namespaced resources granted cluster-wide, localized capability texts,whenconditions that parse as Helm expressions, unknown keys refused.contract, on every module: the rendered ClusterRoles undertemplates/rbacv2/follow the platform's label and naming contract -- a port of the platform'stesting/rbacv2validation, so an external module is checked as an in-tree one. A module on the scheme before DKP 1.78 gets one "migrate" finding per object.coverage: every CRD undercrds/has an entry that grants levels or denies access with a reason.sync: the render and the declaration say the same thing in both directions, compared as(apiGroup, resource, resourceName, verb)tuples plus aggregation edges, roleRefs and subjects; it owns legacy roles, the module's own capabilities and the objects whose names the declaration builds, and nothing else in the render.--fixand the bootstrap that writes the first declaration from the render; the version gate ofrbacv2-migrate-module.shis understood, and a gated file is never rewritten.global.linters-settings.rbac.rules, exclusions inlinters-settings.rbac.exclude-rules, the three rules atwarnby default;no-cyrillicleaves alonerbac.yamland theru.meta.deckhouse.io/*lines the role model requires. The skeleton also holds the rbac configuration keys to a strict check in the shared config loader, refuses a module.dmtlint.yamlthat setsglobal.linters-settings.rbac, turns onnolintlintand keepsGetFixesoff ignored findings; [dmt] feat: module rbac.yaml declaration, final version (stacked on #479) #480 removes all four, since the rules need none of them -- an ignored rule is simply not run.pkg/linters/rbac/README.md, unit tests, e2e cases undertest/e2e/testdata/rbac/.Verified on the skeleton: the full loop (every RBAC template deleted and written back from the declaration) on operator-trivy, cert-manager, user-authz and multitenancy-manager left the rendered rights identical, apart from the object names the declaration's naming forces. #480 is verified again on the final version.
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; see #480 for the complete picture. Design: ADRplatform-security/2026-04-27-module-rbac-yaml.md(architecture-decision-records).