check-safety/targets.go
Alex Dunmow 73259c6b30 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>
2026-06-18 11:04:40 +08:00

375 lines
9.8 KiB
Go

package main
import (
"os"
"path/filepath"
"sort"
)
type pluginScanTarget struct {
root string
display string
frontendDir string
frontendLabel string
}
type backendScanTarget struct {
root string
display string
protoRoot string
allowedPackagePrefixes []string
}
type frontendScanTarget struct {
dir string
label string
pluginRules bool
managedConfigs bool
}
type rbacTargetSummary struct {
label string
protoMethods []string
roleByMethod map[string]string
missingCount int
staleCount int
requiresRPCs bool
}
func (t backendScanTarget) displayOrRoot() string {
if t.display != "" {
return t.display
}
return t.root
}
func resolveScanRoots(target string) (backendDir string, repoRoot string, err error) {
absTarget, err := filepath.Abs(target)
if err != nil {
return "", "", err
}
if dirExists(filepath.Join(absTarget, "backend", "cmd", "check-safety")) {
return filepath.Join(absTarget, "backend"), absTarget, nil
}
// Consolidated CMS layout — Go module at backend/, frontend at web/src.
// The clause above keys on backend/cmd/check-safety, which left the CMS
// tree when check-safety was hoisted to its own repo; without this one
// the CMS resolves as its own backend, web/ is classified as a plugin
// frontend, and the strict plugin rules bypass allowedFrontendFiles.
if fileExists(filepath.Join(absTarget, "backend", "go.mod")) && dirExists(filepath.Join(absTarget, "web", "src")) {
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")) {
parent := filepath.Clean(filepath.Join(absTarget, ".."))
if fileExists(filepath.Join(parent, "buf.yaml")) {
return absTarget, parent, nil
}
return absTarget, absTarget, nil
}
return absTarget, absTarget, nil
}
func defaultScanTargetDir(targetDir string, pluginRoots []string, cwd string) string {
if len(pluginRoots) > 0 || targetDir != "." {
return targetDir
}
if fileExists(filepath.Join(cwd, "plugin.mod")) {
return cwd
}
return filepath.Join(blockNinjaRepoRoot(), "backend")
}
func resolvePluginTargets(repoRoot string, roots []string) (targets []pluginScanTarget, unresolved []string) {
seenRoots := make(map[string]bool)
seenFrontends := make(map[string]bool)
for _, root := range roots {
resolvedRoot, err := expandPath(root)
if err != nil {
unresolved = append(unresolved, shortPluginLabel(root))
continue
}
absRoot, err := filepath.Abs(resolvedRoot)
if err != nil {
unresolved = append(unresolved, shortPluginLabel(root))
continue
}
info, err := os.Stat(absRoot)
if err != nil || !info.IsDir() {
unresolved = append(unresolved, shortPluginLabel(root))
continue
}
if seenRoots[absRoot] {
continue
}
seenRoots[absRoot] = true
target := pluginScanTarget{
root: absRoot,
display: shortPluginLabel(absRoot),
}
if frontendDir, ok := findPluginFrontendDir(absRoot); ok {
if !seenFrontends[frontendDir] {
target.frontendDir = frontendDir
target.frontendLabel = target.display
seenFrontends[frontendDir] = true
}
}
targets = append(targets, target)
}
sort.Slice(targets, func(i, j int) bool {
return targets[i].display < targets[j].display
})
return targets, unresolved
}
func collectBackendScanTargets(repoRoot, backendDir string, includeCoreTargets bool) []backendScanTarget {
var targets []backendScanTarget
seen := make(map[string]bool)
if !includeCoreTargets {
return nil
}
addTarget := func(root string, allowedPrefixes []string) {
if root == "" || seen[root] || !dirExists(root) {
return
}
seen[root] = true
targets = append(targets, backendScanTarget{
root: root,
display: normalizeDisplayLabel(displayLintPath(repoRoot, root)),
protoRoot: resolveProtoScanRoot(root),
allowedPackagePrefixes: allowedPrefixes,
})
}
addTarget(backendDir, inferAllowedPackagePrefixes(repoRoot, backendDir))
sort.Slice(targets, func(i, j int) bool {
return targets[i].displayOrRoot() < targets[j].displayOrRoot()
})
return targets
}
func filterPluginTargets(targets []pluginScanTarget, backendDir string) []pluginScanTarget {
filtered := make([]pluginScanTarget, 0, len(targets))
for _, target := range targets {
if samePath(target.root, backendDir) {
continue
}
filtered = append(filtered, target)
}
return filtered
}
func collectFrontendScanTargets(repoRoot, backendDir string, pluginTargets []pluginScanTarget, includeCoreTargets bool) []frontendScanTarget {
var targets []frontendScanTarget
seen := make(map[string]bool)
addTarget := func(dir, label string, pluginRules bool, managedConfigs bool) {
if dir == "" || seen[dir] || !dirExists(dir) {
return
}
seen[dir] = true
targets = append(targets, frontendScanTarget{
dir: dir,
label: normalizeDisplayLabel(label),
pluginRules: pluginRules,
managedConfigs: managedConfigs,
})
}
if includeCoreTargets && samePath(backendDir, filepath.Join(repoRoot, "backend")) {
addTarget(filepath.Join(repoRoot, "web", "src"), "web/src", false, false)
addTarget(filepath.Join(orchestratorRepoRoot(), "frontend", "src"), "orchestrator/frontend/src", false, true)
for _, pluginWebDir := range discoverInternalPluginWebDirs(backendDir) {
rel, _ := filepath.Rel(repoRoot, pluginWebDir)
addTarget(pluginWebDir, rel, true, true)
}
} else if includeCoreTargets {
if frontendDir, ok := findPluginFrontendDir(backendDir); ok {
addTarget(frontendDir, displayLintPath(repoRoot, frontendDir), true, true)
}
}
for _, target := range pluginTargets {
if target.frontendDir == "" {
continue
}
addTarget(target.frontendDir, target.frontendLabel, true, true)
}
sort.Slice(targets, func(i, j int) bool {
return targets[i].label < targets[j].label
})
return targets
}
func shouldIncludeCoreTargets(targetDir, backendDir string, pluginRoots []string, pluginPageDirs []string) bool {
if len(pluginRoots) == 0 && len(pluginPageDirs) == 0 {
return true
}
blockNinjaRepo := blockNinjaRepoRoot()
if samePath(targetDir, blockNinjaRepo) || samePath(targetDir, filepath.Join(blockNinjaRepo, "backend")) {
return false
}
if samePath(backendDir, filepath.Join(blockNinjaRepo, "backend")) &&
(samePath(targetDir, filepath.Join(blockNinjaRepo, "backend")) || samePath(targetDir, blockNinjaRepo)) {
return false
}
return true
}
func appendManualFrontendTargets(repoRoot string, targets []frontendScanTarget, dirs []string) []frontendScanTarget {
if len(dirs) == 0 {
return targets
}
seen := make(map[string]bool, len(targets))
for _, target := range targets {
if target.dir == "" {
continue
}
absDir, err := filepath.Abs(target.dir)
if err != nil {
continue
}
seen[absDir] = true
}
for _, dir := range dirs {
absDir, err := filepath.Abs(dir)
if err != nil || seen[absDir] {
continue
}
seen[absDir] = true
targets = append(targets, frontendScanTarget{
dir: absDir,
label: normalizeDisplayLabel(displayLintPath(repoRoot, absDir)),
pluginRules: true,
managedConfigs: true,
})
}
sort.Slice(targets, func(i, j int) bool {
return targets[i].label < targets[j].label
})
return targets
}
func collectESLintDirectiveTargets(repoRoot string, frontendTargets []frontendScanTarget) []frontendScanTarget {
var targets []frontendScanTarget
seen := make(map[string]bool)
addTarget := func(dir, label string) {
if dir == "" || !dirExists(dir) {
return
}
absDir, err := filepath.Abs(dir)
if err != nil || seen[absDir] {
return
}
seen[absDir] = true
targets = append(targets, frontendScanTarget{
dir: absDir,
label: normalizeDisplayLabel(label),
})
}
for _, target := range frontendTargets {
addTarget(target.dir, target.label)
}
if samePath(repoRoot, blockNinjaRepoRoot()) {
addTarget(filepath.Join(repoRoot, "packages", "ui", "src"), "packages/ui/src")
}
sort.Slice(targets, func(i, j int) bool {
return targets[i].label < targets[j].label
})
return targets
}
func discoverInternalPluginWebDirs(backendDir string) []string {
pluginsDir := filepath.Join(backendDir, "internal", "plugins")
entries, err := os.ReadDir(pluginsDir)
if err != nil {
return nil
}
var dirs []string
for _, entry := range entries {
if !entry.IsDir() {
continue
}
webDir := filepath.Join(pluginsDir, entry.Name(), "web")
if dirLooksLikeFrontendSource(webDir) {
abs, err := filepath.Abs(webDir)
if err != nil {
continue
}
dirs = append(dirs, abs)
}
}
return dirs
}
func findPluginFrontendDir(root string) (string, bool) {
for _, candidate := range []string{
root,
filepath.Join(root, "src"),
filepath.Join(root, "web"),
filepath.Join(root, "web", "src"),
filepath.Join(root, "frontend"),
filepath.Join(root, "frontend", "src"),
} {
if !dirLooksLikeFrontendSource(candidate) {
continue
}
absCandidate, err := filepath.Abs(candidate)
if err != nil {
continue
}
return absCandidate, true
}
return "", false
}
func inferAllowedPackagePrefixes(repoRoot, backendDir string) []string {
switch {
case samePath(repoRoot, orchestratorRepoRoot()):
return []string{"orchestrator."}
case samePath(backendDir, filepath.Join(repoRoot, "backend")):
return []string{"blockninja."}
default:
return nil
}
}
func resolveProtoScanRoot(backendDir string) string {
parent := filepath.Clean(filepath.Join(backendDir, ".."))
if fileExists(filepath.Join(parent, "buf.yaml")) || dirExists(filepath.Join(parent, "proto")) {
return parent
}
return backendDir
}