Add method interceptor tests to ensure that authz is configured for every RPC

This commit is contained in:
Theodor S. Midtlien
2026-09-08 17:38:39 +02:00
parent 2df40fcc26
commit a79905f3ea
+113
View File
@@ -0,0 +1,113 @@
package ipcauth
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/reflect/protoregistry"
nbproto "github.com/netbirdio/netbird/client/proto"
)
// requestPrototypes returns a zero value of each RPC's request message, keyed by
// method name.
func requestPrototypes(t *testing.T) map[string]any {
t.Helper()
sd := nbproto.File_daemon_proto.Services().ByName("DaemonService")
require.NotNil(t, sd, "DaemonService is missing from the daemon proto descriptor")
out := make(map[string]any, sd.Methods().Len())
for i := 0; i < sd.Methods().Len(); i++ {
md := sd.Methods().Get(i)
mt, err := protoregistry.GlobalTypes.FindMessageByName(md.Input().FullName())
require.NoError(t, err, "request type for %s is not registered", md.Name())
out[string(md.Name())] = mt.New().Interface()
}
return out
}
// Every RPC on DaemonService must carry an explicit policy.
func TestMethodPoliciesCoverService(t *testing.T) {
registered := make(map[string]bool, len(methodPolicies))
for method := range methodPolicies {
registered[strings.TrimPrefix(method, servicePath)] = true
}
for _, m := range nbproto.DaemonService_ServiceDesc.Methods {
assert.True(t, registered[m.MethodName],
"%s has no entry in methodPolicies, add one in methods.go", m.MethodName)
delete(registered, m.MethodName)
}
for _, s := range nbproto.DaemonService_ServiceDesc.Streams {
assert.True(t, registered[s.StreamName],
"%s has no entry in methodPolicies, add one in methods.go", s.StreamName)
delete(registered, s.StreamName)
}
for stale := range registered {
assert.Fail(t, "stale policy entry",
"methodPolicies has %s, which DaemonService no longer defines", stale)
}
}
// A key without the service prefix can never matched.
func TestMethodPolicyKeysAreFullMethodNames(t *testing.T) {
for method := range methodPolicies {
assert.True(t, strings.HasPrefix(method, servicePath),
"%q is not a full gRPC method name and can never match a request", method)
}
}
// TargetsProfile promises the request names a profile.
func TestTargetScopedRequestsExposeATarget(t *testing.T) {
prototypes := requestPrototypes(t)
for method, policy := range methodPolicies {
if !policy.TargetsProfile {
continue
}
name := strings.TrimPrefix(method, servicePath)
msg, ok := prototypes[name]
require.True(t, ok, "%s is marked TargetsProfile but is not a unary method on DaemonService", name)
_, named := targetProfile(msg)
assert.True(t, named,
"%s is marked TargetsProfile but its request exposes neither GetHandle nor GetProfileName", name)
}
}
// Ensure that we are not missing request that names a profile.
func TestRequestsWithATargetAreDeclared(t *testing.T) {
// AddProfileRequest.ProfileName is the name of the profile to create. It
// does not exist yet, so there is no owner to authorize against.
exceptions := map[string]string{
"AddProfile": "ProfileName names the profile to create, not one to authorize",
}
for name, msg := range requestPrototypes(t) {
if _, named := targetProfile(msg); !named {
continue
}
policy := methodPolicies[servicePath+name]
if reason, excepted := exceptions[name]; excepted {
assert.False(t, policy.TargetsProfile,
"%s is listed as an exception (%s) but is marked TargetsProfile", name, reason)
continue
}
assert.True(t, policy.TargetsProfile,
"%s carries a profile target but is not marked TargetsProfile", name)
}
}
// The stream interceptor runs before any message is read, so msg is nil and no
// target can be extracted.
func TestNoStreamingMethodTargetsProfile(t *testing.T) {
for _, s := range nbproto.DaemonService_ServiceDesc.Streams {
assert.False(t, methodPolicies[servicePath+s.StreamName].TargetsProfile,
"%s is a stream and cannot carry a profile target", s.StreamName)
}
}