From 7f3ecc0d9373c622f9eab70775a97849688dd1b4 Mon Sep 17 00:00:00 2001 From: Kleber Rocha Date: Thu, 28 May 2026 12:10:15 -0300 Subject: [PATCH] docs: lock down permission string format (`:`, single colon) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md never declared the permission string format explicitly — only examples — and the `$schema` referenced in module.json is not published, so editors and LLMs get no formal hint. The shell parses with `indexOf(":")`, which silently accepts malformed inputs like `my-module:conta:read` (action becomes "conta:read") and yields a dead permission no RBAC role can ever grant. - CLAUDE.md: new "Permission strings — exactly two segments" section near the nav docs, with the rule, the why (silent failure), and four worked examples (correct + three wrong patterns). - check-module skill: new check 2.6 "Formato das permissoes" validates declared `permissions[]` and `requiredPermission` in nav/perspectives against `:`. Subsequent sections renumbered. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/skills/check-module/SKILL.md | 69 +++++++++++++++++++++++++--- CLAUDE.md | 20 ++++++++ 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/.claude/skills/check-module/SKILL.md b/.claude/skills/check-module/SKILL.md index b8777e4..6dd82f7 100644 --- a/.claude/skills/check-module/SKILL.md +++ b/.claude/skills/check-module/SKILL.md @@ -1,6 +1,6 @@ --- name: check-module -description: Valida localmente se o module.json e arquivos derivados estao em conformidade com o contrato do Shell API. Roda em segundos e pega os erros mais comuns (defaults nao personalizados, mutual exclusivity nav/perspectives, paths com drift, perms orfas, symlink do chart faltando) antes de o registro falhar no deploy. +description: Valida localmente se o module.json e arquivos derivados estao em conformidade com o contrato do Shell API. Roda em segundos e pega os erros mais comuns (defaults nao personalizados, mutual exclusivity nav/perspectives, paths com drift, formato de permissao invalido, perms orfas, symlink do chart faltando) antes de o registro falhar no deploy. user-invocable: true disable-model-invocation: false allowed-tools: Read, Glob, Grep, Bash @@ -113,7 +113,62 @@ while IFS= read -r p; do done <<< "$PATHS" ``` -### 2.6 — Permissoes referenciadas existem em `permissions[]` (severity: warn) +### 2.6 — Formato das permissoes (severity: error) + +O shell parsa cada string em `permissions[]` (e `requiredPermission` em nav/perspectives) com `indexOf(":")` — divide no **primeiro** colon. Isso significa que strings malformadas podem parsar e ainda assim virar permissao morta: o registro passa, o modulo aparece, mas nenhuma role do RBAC bate a "acao" resultante. O dev so descobre quando algum usuario reclama que nao consegue acessar. + +Formato correto: `:` — exatamente um colon, recurso igual ao id do modulo, acao em kebab-case simples (`[a-z][a-z0-9_-]*`). + +Armadilhas tipicas que parsam mas nao funcionam: + +- `my-module:conta:read` — extra segment. Parser produz `resource=my-module, action="conta:read"`. Nenhuma role concede `"conta:read"`. **Esse e o caso que motivou este check.** +- `outro-modulo:read` num modulo cujo id e `my-module` — declarando ownership de outro modulo. +- `MyModule:Read` — RBAC e case-sensitive, nunca casa com a role. +- `my-module:` ou `:read` — segmento vazio, parser retorna `null`. + +Roda **antes** de 2.7 pra que o orphan check ali nao gere ruido sobre strings ja sabidamente quebradas. + +```bash +validate_perm_format() { + local label="$1" perm="$2" + [ -z "$perm" ] && return + local colons resource action + colons=$(awk -F: '{print NF-1}' <<< "$perm") + if [ "$colons" -eq 0 ]; then + PROBLEMS+=("erro: $label \"$perm\" sem ':' — formato e \"$ID:\"") + return + fi + if [ "$colons" -gt 1 ]; then + PROBLEMS+=("erro: $label \"$perm\" tem $colons ':' — formato e \"$ID:\", nao \"$ID::\"") + return + fi + resource="${perm%%:*}" + action="${perm#*:}" + if [ -z "$resource" ] || [ -z "$action" ]; then + PROBLEMS+=("erro: $label \"$perm\" tem segmento vazio") + return + fi + if [ "$resource" != "$ID" ]; then + PROBLEMS+=("erro: $label \"$perm\" usa recurso \"$resource\" diferente do id do modulo \"$ID\"") + return + fi + if ! [[ "$action" =~ ^[a-z][a-z0-9_-]*$ ]]; then + PROBLEMS+=("aviso: $label \"$perm\" — acao \"$action\" foge do kebab-case [a-z][a-z0-9_-]*; RBAC e case-sensitive") + fi +} + +# Declared (permissions raiz) +while IFS= read -r p; do validate_perm_format "permissao" "$p"; done <<< \ + "$(jq -r '(.permissions // []) | if type == "array" then .[] else empty end' module.json)" + +# Used (requiredPermission em nav/perspectives) — mesmo check, label diferente +while IFS= read -r p; do validate_perm_format "requiredPermission" "$p"; done <<< \ + "$(jq -r '((.navigation // []) + (.perspectives // [] | map(.navigation // []) | add // [])) | .[] | .requiredPermission // empty' module.json)" +``` + +> Por que erro e nao aviso, ja que o shell aceita a string? Porque o sintoma e silencioso. Registro funciona, sidebar funciona — so o RBAC nunca casa. Falha invisivel em ambiente vale mais ser barrada localmente. + +### 2.7 — Permissoes referenciadas existem em `permissions[]` (severity: warn) Cada `requiredPermission` usada em `navigation`/`perspectives` deve estar declarada no array `permissions` raiz. Se o nav usa `module:approve` mas `permissions[]` so tem `module:read`, e bug certo (RBAC nao vai conseguir conceder). @@ -130,7 +185,7 @@ ORPHANS=$(comm -23 <(echo "$USED") <(echo "$DECLARED")) Cada linha em `ORPHANS` vira warn. Se `permissions` nao for array, isso ja vira error em 2.2 — aqui so evitamos abortar a execucao. -### 2.7 — Symlink do chart (severity: error) +### 2.8 — Symlink do chart (severity: error) Se existe `charts/$ID/`, deve existir `charts/$ID/module.json` como **symlink** para `../../module.json`. Sem isso, o registration ConfigMap renderiza `module.json` errado em deploy (o chart le via `.Files.Get`). @@ -144,11 +199,11 @@ if [ -d "charts/$ID" ]; then fi ``` -### 2.8 — `platform.shellApiUrl` configurada (severity: warn) +### 2.9 — `platform.shellApiUrl` configurada (severity: warn) Necessario para `platform-setup` rodar. Se vazio ou ainda contem `stage.cora.team` num modulo prod, sinalize. -### 2.9 — `accent` no enum V2 (severity: error quando declarado, warn quando ausente) +### 2.10 — `accent` no enum V2 (severity: error quando declarado, warn quando ausente) O shell aceita 7 valores fechados pro `accent`. Se declarado fora desse set, o `POST /api/modules` rejeita. Se ausente, o shell auto-infere via hash deterministico do `id` — sem custo, mas vale informar pro dev: @@ -167,7 +222,7 @@ case "$ACCENT" in esac ``` -### 2.10 — `icon` ausente (severity: warn) +### 2.11 — `icon` ausente (severity: warn) `icon` e nome lucide em kebab-case. Sem ele, o shell usa o default do `group`. Se ambos `accent` e `icon` ausentes, modulos do mesmo grupo ficam visualmente indistinguiveis na grid: @@ -178,7 +233,7 @@ if [ -z "$ICON" ]; then fi ``` -### 2.11 — Hardcoded colors no `src/` (severity: warn) +### 2.12 — Hardcoded colors no `src/` (severity: warn) Modulos devem consumir CSS custom properties (`var(--token)`) em vez de fixar hex. Cores fixas escapam quando o shell troca de tema (light/dark) ou quando o palette V2 evolui. diff --git a/CLAUDE.md b/CLAUDE.md index dd1ac92..d9a942a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,6 +26,26 @@ To switch pages inside the module, use `useModuleRoute(moduleManifest.id)` ([src Use `Tabs` from `@/components/ui/tabs` **only** for tightly-coupled sub-views inside a single page. Distinct pages always belong in `navigation[]`. +## Permission strings — exactly two segments + +Every string in `permissions[]` and every `requiredPermission` in nav/perspectives follows **`:`**. One colon. The module-id segment must equal this module's `id`; the action is kebab-case (`[a-z][a-z0-9_-]*`). The shell parses with `indexOf(":")` (splits on the first colon), so extra segments silently collapse into a malformed action that **no RBAC role can ever grant** — the module registers, the sidebar renders, and the user gets a permanent permission-denied with no error in any log. + +```jsonc +// ✅ correct +"permissions": ["my-module:read", "my-module:write", "my-module:approve"] + +// ❌ wrong — three segments. Parser produces action="conta:read". Dead permission. +"permissions": ["my-module:conta:read"] + +// ❌ wrong — resource segment differs from module id. +"permissions": ["outro-modulo:read"] + +// ❌ wrong — RBAC is case-sensitive. +"permissions": ["MyModule:Read"] +``` + +If you need to scope by sub-resource, encode it in the action with `-` or `_`, not `:` — e.g. `my-module:conta-read`, `my-module:invoice_approve`. `/check-module` enforces this format and treats violations as errors. + ## ShellContext API ```typescript