Migrate gRPC server to mcdsl grpcserver package

Replace metacrypt's hand-rolled gRPC interceptor chain with the mcdsl
grpcserver package, which provides TLS setup, logging, and method-map
auth (public/auth-required/admin-required) out of the box.

Metacrypt-specific interceptors are preserved as hooks:
- sealInterceptor runs as a PreInterceptor (before logging/auth)
- auditInterceptor runs as a PostInterceptor (after auth)

The three legacy method maps (seal/auth/admin) are restructured into
mcdsl's MethodMap (Public/AuthRequired/AdminRequired) plus a separate
seal-required map for the PreInterceptor. Token context is now stored
via mcdsl/auth.ContextWithTokenInfo instead of a package-local key.

Bumps mcdsl from v1.0.0 to v1.0.1 (adds PreInterceptors/PostInterceptors
to grpcserver.Options).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 14:42:41 -07:00
parent d308db8598
commit 310ed83f28
12 changed files with 264 additions and 378 deletions

View File

@@ -158,114 +158,6 @@ func TestSealInterceptor_SkipsUnlistedMethod(t *testing.T) {
}
}
func TestAuthInterceptor_MissingToken(t *testing.T) {
authenticator, _ := auth.NewAuthenticator(auth.Config{ServerURL: "http://localhost:0"}, slog.Default())
methods := map[string]bool{"/test.Service/Method": true}
interceptor := authInterceptor(authenticator, slog.Default(), methods)
_, err := interceptor(context.Background(), nil, methodInfo("/test.Service/Method"), okHandler)
if err == nil {
t.Fatal("expected error for missing token")
}
if code := status.Code(err); code != codes.Unauthenticated {
t.Errorf("expected Unauthenticated, got %v", code)
}
}
func TestAuthInterceptor_SkipsUnlistedMethod(t *testing.T) {
authenticator, _ := auth.NewAuthenticator(auth.Config{ServerURL: "http://localhost:0"}, slog.Default())
methods := map[string]bool{"/test.Service/Other": true}
interceptor := authInterceptor(authenticator, slog.Default(), methods)
resp, err := interceptor(context.Background(), nil, methodInfo("/test.Service/Method"), okHandler)
if err != nil {
t.Fatalf("expected pass-through, got: %v", err)
}
if resp != "ok" {
t.Errorf("expected 'ok', got %v", resp)
}
}
func TestAdminInterceptor_NoTokenInfo(t *testing.T) {
methods := map[string]bool{"/test.Service/Admin": true}
interceptor := adminInterceptor(slog.Default(), methods)
_, err := interceptor(context.Background(), nil, methodInfo("/test.Service/Admin"), okHandler)
if err == nil {
t.Fatal("expected error when no token info in context")
}
if code := status.Code(err); code != codes.PermissionDenied {
t.Errorf("expected PermissionDenied, got %v", code)
}
}
func TestAdminInterceptor_NonAdmin(t *testing.T) {
methods := map[string]bool{"/test.Service/Admin": true}
interceptor := adminInterceptor(slog.Default(), methods)
ctx := context.WithValue(context.Background(), tokenInfoKey, &auth.TokenInfo{
Username: "user",
IsAdmin: false,
})
_, err := interceptor(ctx, nil, methodInfo("/test.Service/Admin"), okHandler)
if err == nil {
t.Fatal("expected error for non-admin")
}
if code := status.Code(err); code != codes.PermissionDenied {
t.Errorf("expected PermissionDenied, got %v", code)
}
}
func TestAdminInterceptor_Admin(t *testing.T) {
methods := map[string]bool{"/test.Service/Admin": true}
interceptor := adminInterceptor(slog.Default(), methods)
ctx := context.WithValue(context.Background(), tokenInfoKey, &auth.TokenInfo{
Username: "admin",
IsAdmin: true,
})
resp, err := interceptor(ctx, nil, methodInfo("/test.Service/Admin"), okHandler)
if err != nil {
t.Fatalf("expected success for admin, got: %v", err)
}
if resp != "ok" {
t.Errorf("expected 'ok', got %v", resp)
}
}
func TestAdminInterceptor_SkipsUnlistedMethod(t *testing.T) {
methods := map[string]bool{"/test.Service/Other": true}
interceptor := adminInterceptor(slog.Default(), methods)
// No token info in context — but method not listed, so should pass through.
resp, err := interceptor(context.Background(), nil, methodInfo("/test.Service/Method"), okHandler)
if err != nil {
t.Fatalf("expected pass-through, got: %v", err)
}
if resp != "ok" {
t.Errorf("expected 'ok', got %v", resp)
}
}
func TestChainInterceptors(t *testing.T) {
var order []int
makeInterceptor := func(n int) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
order = append(order, n)
return handler(ctx, req)
}
}
chained := chainInterceptors(makeInterceptor(1), makeInterceptor(2), makeInterceptor(3))
_, err := chained(context.Background(), nil, methodInfo("/test/Method"), okHandler)
if err != nil {
t.Fatalf("chain: %v", err)
}
if len(order) != 3 || order[0] != 1 || order[1] != 2 || order[2] != 3 {
t.Errorf("expected execution order [1 2 3], got %v", order)
}
}
func TestExtractToken(t *testing.T) {
tests := []struct {
name string
@@ -293,6 +185,21 @@ func TestExtractToken(t *testing.T) {
}
}
func TestCallerUsername(t *testing.T) {
// No token info in context.
if got := callerUsername(context.Background()); got != "" {
t.Errorf("expected empty, got %q", got)
}
// With token info.
ctx := auth.ContextWithTokenInfo(context.Background(), &auth.TokenInfo{
Username: "alice",
})
if got := callerUsername(ctx); got != "alice" {
t.Errorf("expected 'alice', got %q", got)
}
}
// ---- systemServer tests ----
func TestSystemStatus(t *testing.T) {
@@ -668,7 +575,7 @@ func TestAuthTokenInfo_FromContext(t *testing.T) {
as := &authServer{s: srv}
ti := &auth.TokenInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false}
ctx := context.WithValue(context.Background(), tokenInfoKey, ti)
ctx := auth.ContextWithTokenInfo(context.Background(), ti)
resp, err := as.TokenInfo(ctx, &pb.TokenInfoRequest{})
if err != nil {
@@ -718,3 +625,33 @@ func TestPbToRuleRoundtrip(t *testing.T) {
t.Errorf("roundtrip Effect: got %q, want %q", back.Effect, original.Effect)
}
}
// ---- method map tests ----
func TestMethodMapCompleteness(t *testing.T) {
// Verify every method in sealRequired is also in one of the auth maps
// (public, authRequired, or adminRequired).
mm := methodMap()
seal := sealRequiredMethods()
for method := range seal {
if !mm.Public[method] && !mm.AuthRequired[method] && !mm.AdminRequired[method] {
t.Errorf("seal-required method %s is not in any auth map (public/auth/admin)", method)
}
}
// Verify public/auth/admin maps don't overlap.
for method := range mm.Public {
if mm.AuthRequired[method] {
t.Errorf("method %s is in both Public and AuthRequired", method)
}
if mm.AdminRequired[method] {
t.Errorf("method %s is in both Public and AdminRequired", method)
}
}
for method := range mm.AuthRequired {
if mm.AdminRequired[method] {
t.Errorf("method %s is in both AuthRequired and AdminRequired", method)
}
}
}