fix(check-safety): recognize methodRolesSeed RBAC table; resolve backend scan-root

extractRBACMethodRoles matched only a map literal named MethodRoles. The CMS now
keeps its RBAC table behind an atomic-pointer copy-on-write live table, with the
static literal renamed to methodRolesSeed -- so the validator read an empty table
and reported 641 phantom "missing RBAC entry" methods, blinding the gate to any
genuine unregistered RPC. Match methodRolesSeed as well as MethodRoles, and add a
regression test (TestExtractRBACMethodRolesParsesSeedTable).

resolveScanRoots: also resolve a `backend` directory containing go.mod whose
parent holds buf.yaml or proto/, so the checker targets that layout correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-06-18 11:04:40 +08:00
parent c048766075
commit 73259c6b30
3 changed files with 81 additions and 3 deletions

View File

@ -81,6 +81,35 @@ func TestCollectTargetsSkipsCoreWhenIncludeCoreTargetsFalse(t *testing.T) {
} }
} }
func TestResolveScanRootsDetectsConsolidatedBackendDir(t *testing.T) {
repoRoot := t.TempDir()
backendDir := filepath.Join(repoRoot, "backend")
protoDir := filepath.Join(repoRoot, "proto")
for _, dir := range []string{backendDir, protoDir} {
if err := os.MkdirAll(dir, 0755); err != nil {
t.Fatalf("mkdir %s: %v", dir, err)
}
}
if err := os.WriteFile(filepath.Join(backendDir, "go.mod"), []byte("module example.com/cms/backend\n"), 0644); err != nil {
t.Fatalf("write go.mod: %v", err)
}
if err := os.WriteFile(filepath.Join(repoRoot, "buf.yaml"), []byte("version: v2\n"), 0644); err != nil {
t.Fatalf("write buf.yaml: %v", err)
}
gotBackend, gotRepo, err := resolveScanRoots(backendDir)
if err != nil {
t.Fatalf("resolveScanRoots() error = %v", err)
}
if gotBackend != backendDir {
t.Fatalf("backendDir = %q, want %q", gotBackend, backendDir)
}
if gotRepo != repoRoot {
t.Fatalf("repoRoot = %q, want %q", gotRepo, repoRoot)
}
}
func TestDiscoverOrchestratorTestTargetsRequiresCoreScan(t *testing.T) { func TestDiscoverOrchestratorTestTargetsRequiresCoreScan(t *testing.T) {
repoRoot := blockNinjaRepoRoot() repoRoot := blockNinjaRepoRoot()
backendDir := filepath.Join(repoRoot, "backend") backendDir := filepath.Join(repoRoot, "backend")
@ -189,3 +218,39 @@ var MethodRoles = map[string]Role{
t.Fatalf("admin role = %q, want admin", methods["/example.v1.Service/Admin"]) t.Fatalf("admin role = %q, want admin", methods["/example.v1.Service/Admin"])
} }
} }
func TestExtractRBACMethodRolesParsesSeedTable(t *testing.T) {
// The CMS renamed the static RBAC table to methodRolesSeed, kept behind an
// atomic-pointer copy-on-write live table for thread safety. The extractor must
// recognise methodRolesSeed as well as MethodRoles, or every CMS RPC reads as
// "missing RBAC entry".
dir := t.TempDir()
file := filepath.Join(dir, "interceptor.go")
if err := os.WriteFile(file, []byte(`package rbac
type Role string
const (
RoleAdmin Role = "admin"
)
var methodRolesSeed = map[string]Role{
"/example.v1.Service/Public": "",
"/example.v1.Service/Admin": RoleAdmin,
}
`), 0644); err != nil {
t.Fatalf("write interceptor.go: %v", err)
}
methods, err := extractRBACMethodRoles(file)
if err != nil {
t.Fatalf("extractRBACMethodRoles() error = %v", err)
}
if methods["/example.v1.Service/Public"] != "public" {
t.Fatalf("public role = %q, want public", methods["/example.v1.Service/Public"])
}
if methods["/example.v1.Service/Admin"] != "admin" {
t.Fatalf("admin role = %q, want admin", methods["/example.v1.Service/Admin"])
}
}

View File

@ -630,7 +630,9 @@ func extractConnectProceduresRecursive(root string, allowedPackagePrefixes ...st
return procedures, nil return procedures, nil
} }
// extractRBACMethodRoles parses interceptor.go and extracts all keys and role values from the MethodRoles map. // extractRBACMethodRoles parses interceptor.go and extracts all keys and role values from the
// MethodRoles map (orchestrator/helpdesk) or the methodRolesSeed map (CMS — the static seed
// table behind the atomic-pointer copy-on-write live table; see interceptor.go).
func extractRBACMethodRoles(file string) (map[string]string, error) { func extractRBACMethodRoles(file string) (map[string]string, error) {
fset := token.NewFileSet() fset := token.NewFileSet()
f, err := parser.ParseFile(fset, file, nil, 0) f, err := parser.ParseFile(fset, file, nil, 0)
@ -641,9 +643,13 @@ func extractRBACMethodRoles(file string) (map[string]string, error) {
methods := make(map[string]string) methods := make(map[string]string)
ast.Inspect(f, func(n ast.Node) bool { ast.Inspect(f, func(n ast.Node) bool {
// Find: var MethodRoles = map[string]Role{ ... } // Find: var MethodRoles = map[string]Role{ ... } (orchestrator/helpdesk)
// or: var methodRolesSeed = map[string]Role{ ... } (CMS copy-on-write seed)
vs, ok := n.(*ast.ValueSpec) vs, ok := n.(*ast.ValueSpec)
if !ok || len(vs.Names) == 0 || vs.Names[0].Name != "MethodRoles" { if !ok || len(vs.Names) == 0 {
return true
}
if name := vs.Names[0].Name; name != "MethodRoles" && name != "methodRolesSeed" {
return true return true
} }
if len(vs.Values) == 0 { if len(vs.Values) == 0 {

View File

@ -62,6 +62,13 @@ func resolveScanRoots(target string) (backendDir string, repoRoot string, err er
return filepath.Join(absTarget, "backend"), absTarget, nil return filepath.Join(absTarget, "backend"), absTarget, nil
} }
if filepath.Base(absTarget) == "backend" && fileExists(filepath.Join(absTarget, "go.mod")) {
parent := filepath.Clean(filepath.Join(absTarget, ".."))
if fileExists(filepath.Join(parent, "buf.yaml")) || dirExists(filepath.Join(parent, "proto")) {
return absTarget, parent, nil
}
}
if dirExists(filepath.Join(absTarget, "cmd", "check-safety")) { if dirExists(filepath.Join(absTarget, "cmd", "check-safety")) {
parent := filepath.Clean(filepath.Join(absTarget, "..")) parent := filepath.Clean(filepath.Join(absTarget, ".."))
if fileExists(filepath.Join(parent, "buf.yaml")) { if fileExists(filepath.Join(parent, "buf.yaml")) {