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:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user