check-safety/check_rbac.go
Alex Dunmow cd88c808b0 initial: standalone check-safety module hoisted from CMS
Static safety/lint runner for the BlockNinja codebase. ~25 invariant
checks across Go and frontend sources. Was at git.dev.alexdunmow.com:block/ninja
in backend/cmd/check-safety/ until the 2026-06-06 consolidation moved
the BlockNinja repos under a shared ~/src/blockninja/ parent.

This repo is the standalone extraction:
- Own go.mod (git.dev.alexdunmow.com/block/check-safety, go 1.26.4)
- Vendored internal/{helpers,theme} from CMS (Go's internal/ rule
  blocks cross-module imports; vendoring is the workaround)
- CLI contract unchanged: `check-safety <target-dir> [--flags]`
- CMS Makefile shells into ../check-safety for safety-check /
  install-safety-checker targets

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 13:04:02 +08:00

215 lines
6.4 KiB
Go

package main
import (
"fmt"
"os"
"path/filepath"
"sort"
)
func init() {
register(Check{
Seq: 20,
ID: "2",
Title: "RPC methods registered in RBAC interceptor",
Run: func(ctx *ScanContext, rep *Reporter) {
// Check 2: RPC methods in RBAC interceptor
fmt.Println("=== Check 2: RPC methods registered in RBAC interceptor ===")
protoMethods := make([]string, 0)
rbacMethods := make(map[string]string)
protoMethodSet := make(map[string]bool)
requiresRPCDiscovery := false
var rbacSummaries []rbacTargetSummary
for _, target := range ctx.backendTargets {
rbacFile := filepath.Join(target.root, "internal/rbac/interceptor.go")
hasCoreRBAC := fileExists(rbacFile)
summary := rbacTargetSummary{
label: target.displayOrRoot(),
roleByMethod: make(map[string]string),
}
if hasCoreRBAC {
requiresRPCDiscovery = true
summary.requiresRPCs = true
targetProtoMethods, err := extractBufProcedures(target.protoRoot, target.allowedPackagePrefixes...)
if err != nil {
fmt.Printf(" ERROR: failed to parse proto files for %s: %v\n", target.displayOrRoot(), err)
os.Exit(2)
}
summary.protoMethods = append(summary.protoMethods, targetProtoMethods...)
for _, pm := range targetProtoMethods {
if protoMethodSet[pm] {
continue
}
protoMethodSet[pm] = true
protoMethods = append(protoMethods, pm)
}
targetRBACMethods, err := extractRBACMethodRoles(rbacFile)
if err != nil {
fmt.Printf(" ERROR: failed to parse RBAC interceptor for %s: %v\n", target.displayOrRoot(), err)
os.Exit(2)
}
for method, role := range targetRBACMethods {
rbacMethods[method] = role
summary.roleByMethod[method] = role
}
rbacSummaries = append(rbacSummaries, summary)
continue
}
if len(target.allowedPackagePrefixes) > 0 {
requiresRPCDiscovery = true
summary.requiresRPCs = true
}
targetProtoMethods, err := extractBufProcedures(target.root)
if err != nil {
fmt.Printf(" ERROR: failed to parse proto files for %s: %v\n", target.displayOrRoot(), err)
os.Exit(2)
}
summary.protoMethods = append(summary.protoMethods, targetProtoMethods...)
for _, pm := range targetProtoMethods {
if protoMethodSet[pm] {
continue
}
protoMethodSet[pm] = true
protoMethods = append(protoMethods, pm)
}
targetRoleMethods, err := extractPluginRoleMethods(target.root)
if err != nil {
fmt.Printf(" ERROR: failed to parse RBAC roles for %s: %v\n", target.displayOrRoot(), err)
os.Exit(2)
}
for method, role := range targetRoleMethods {
rbacMethods[method] = role
summary.roleByMethod[method] = role
}
rbacSummaries = append(rbacSummaries, summary)
}
for _, target := range ctx.pluginTargets {
summary := rbacTargetSummary{
label: target.display,
roleByMethod: make(map[string]string),
}
pluginProtoMethods, err := extractBufProcedures(target.root)
if err != nil {
fmt.Printf(" ERROR: failed to parse plugin proto files for %s: %v\n", target.display, err)
os.Exit(2)
}
summary.protoMethods = append(summary.protoMethods, pluginProtoMethods...)
for _, pm := range pluginProtoMethods {
if protoMethodSet[pm] {
continue
}
protoMethodSet[pm] = true
protoMethods = append(protoMethods, pm)
}
pluginRoleMethods, err := extractPluginRoleMethods(target.root)
if err != nil {
fmt.Printf(" ERROR: failed to parse plugin RBAC roles for %s: %v\n", target.display, err)
os.Exit(2)
}
for method, role := range pluginRoleMethods {
rbacMethods[method] = role
summary.roleByMethod[method] = role
}
rbacSummaries = append(rbacSummaries, summary)
}
if len(protoMethods) == 0 {
if requiresRPCDiscovery {
fmt.Println(" ERROR: no RPC procedures found in proto sources or generated Connect files")
os.Exit(2)
}
fmt.Println(" SKIP: no proto-backed RPC procedures found in scanned target(s)")
} else {
var missing []string
for _, m := range protoMethods {
if isExcludedServiceMethod(m) {
continue
}
if _, ok := rbacMethods[m]; !ok {
missing = append(missing, m)
}
}
var stale []string
for m := range rbacMethods {
if !protoMethodSet[m] {
if isExcludedServiceMethod(m) {
continue
}
stale = append(stale, m)
}
}
sort.Strings(stale)
for i := range rbacSummaries {
summary := &rbacSummaries[i]
for _, method := range summary.protoMethods {
if isExcludedServiceMethod(method) {
continue
}
if _, ok := summary.roleByMethod[method]; !ok {
summary.missingCount++
}
}
for method := range summary.roleByMethod {
if isExcludedServiceMethod(method) {
continue
}
if !containsString(summary.protoMethods, method) {
summary.staleCount++
}
}
}
for _, summary := range rbacSummaries {
if len(summary.protoMethods) == 0 {
if summary.requiresRPCs {
fmt.Printf(" ERROR: %s — no RPC procedures found\n", summary.label)
} else {
fmt.Printf(" OK: %s — no proto-backed RPC procedures\n", summary.label)
}
continue
}
fmt.Printf(" OK: %s — %d RPC methods discovered\n", summary.label, len(summary.protoMethods))
printRoleBreakdown(countRBACRoles(summary.protoMethods, summary.roleByMethod))
if summary.missingCount > 0 {
fmt.Printf(" missing RBAC entries: %d\n", summary.missingCount)
}
if summary.staleCount > 0 {
fmt.Printf(" stale RBAC entries: %d\n", summary.staleCount)
}
}
if len(missing) > 0 {
fmt.Printf(" FAIL: %d RPC method(s) NOT in MethodRoles (will get permission_denied):\n", len(missing))
for _, m := range missing {
fmt.Printf(" - %s\n", m)
}
rep.Fail()
}
if len(stale) > 0 {
fmt.Printf(" WARN: %d RBAC entries have no matching proto method (stale?):\n", len(stale))
for _, m := range stale {
fmt.Printf(" - %s\n", m)
}
}
if len(missing) == 0 && len(stale) == 0 {
fmt.Printf(" OK: All %d RPC methods are registered in MethodRoles\n", len(protoMethods))
} else if len(missing) == 0 {
fmt.Printf(" OK: All %d RPC methods are registered (but %d stale entries exist)\n", len(protoMethods), len(stale))
}
}
fmt.Println()
},
})
}