package main import ( "path/filepath" "sort" "strings" ) // coreSDKModulePath is the shared Go SDK that carries the canonical proto // definitions (blockninja.v1, orchestrator.v1, helpdesk.v1) consumed by the // serving backends. It has no connect server and no RBAC interceptor of its // own — RBAC is enforced in the repos that actually serve these procedures // (cms for blockninja.v1, orchestrator for orchestrator.v1), where check 2 // runs against their interceptors. Scanning the SDK in isolation would flag // every shared procedure as "missing"; that is a not-applicable result, not a // finding. const coreSDKModulePath = "git.dev.alexdunmow.com/block/core" // isDefinitionsOnlySDKTarget reports whether the backend target is the shared // proto SDK (core): it carries proto but serves nothing, so it has neither an // RBAC interceptor nor a designated serving package prefix. Such a target is // out of scope for check 2 — see coreSDKModulePath. func isDefinitionsOnlySDKTarget(target backendScanTarget) bool { if len(target.allowedPackagePrefixes) > 0 { return false // a designated serving backend (cms/orchestrator) } if fileExists(filepath.Join(target.root, "internal/rbac/interceptor.go")) { return false // has a serving RBAC interceptor } modPath, err := readModulePath(target.root) if err != nil { return false } return modPath == coreSDKModulePath } func init() { register(Check{ Seq: 20, ID: "2", Title: "RPC methods registered in RBAC interceptor", Run: func(ctx *ScanContext, rep *Reporter) { protoMethods := make([]string, 0) rbacMethods := make(map[string]string) protoMethodSet := make(map[string]bool) requiresRPCDiscovery := false var rbacSummaries []rbacTargetSummary var definitionsOnlySDKLabels []string for _, target := range ctx.backendTargets { // The shared proto SDK (core) serves nothing — RBAC is enforced // in the consuming repos, where this check runs. Don't fold its // procedures into the missing-set; check 2 is not applicable. if isDefinitionsOnlySDKTarget(target) { definitionsOnlySDKLabels = append(definitionsOnlySDKLabels, target.displayOrRoot()) continue } 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 { rep.Fatal("failed to parse proto files for %s: %v", target.displayOrRoot(), err) } 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 { rep.Fatal("failed to parse RBAC interceptor for %s: %v", target.displayOrRoot(), err) } 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 { rep.Fatal("failed to parse proto files for %s: %v", target.displayOrRoot(), err) } 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 { rep.Fatal("failed to parse RBAC roles for %s: %v", target.displayOrRoot(), err) } 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 { rep.Fatal("failed to parse plugin proto files for %s: %v", target.display, err) } 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 { rep.Fatal("failed to parse plugin RBAC roles for %s: %v", target.display, err) } for method, role := range pluginRoleMethods { rbacMethods[method] = role summary.roleByMethod[method] = role } rbacSummaries = append(rbacSummaries, summary) } if len(protoMethods) == 0 { if requiresRPCDiscovery { rep.Fatal("no RPC procedures found in proto sources or generated Connect files") } if len(definitionsOnlySDKLabels) > 0 { rep.Skip("%s — definitions-only proto SDK; RBAC enforced in the serving repos (cms/orchestrator), not here", strings.Join(definitionsOnlySDKLabels, ", ")) } else { rep.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 { rep.Fail("%s — no RPC procedures found", summary.label) } else { rep.OK("%s — no proto-backed RPC procedures", summary.label) } continue } rep.OK("%s — %d RPC methods discovered", summary.label, len(summary.protoMethods)) if summary.missingCount > 0 { rep.Findingf("missing RBAC entries: %d", summary.missingCount) } if summary.staleCount > 0 { rep.Findingf("stale RBAC entries: %d", summary.staleCount) } } if len(missing) > 0 { rep.Fail("%d RPC method(s) NOT in MethodRoles (will get permission_denied)", len(missing)) for _, m := range missing { rep.Findingf("- %s", m) } } if len(stale) > 0 { rep.Warn("%d RBAC entries have no matching proto method (stale?)", len(stale)) for _, m := range stale { rep.Findingf("- %s", m) } } if len(missing) == 0 && len(stale) == 0 { rep.OK("All %d RPC methods are registered in MethodRoles", len(protoMethods)) } else if len(missing) == 0 { rep.OK("All %d RPC methods are registered (but %d stale entries exist)", len(protoMethods), len(stale)) } } }, }) }