From 86d516acf6a810b237b59cded262802621cc0c4d Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sat, 28 Mar 2026 16:07:17 -0700 Subject: [PATCH] Drop admin requirement from agent interceptor, reject guests The agent now accepts any authenticated user or system account, except those with the guest role. Admin is reserved for MCIAS account management and policy changes, not routine deploy/stop/start operations. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/auth/auth.go | 11 +++++++---- internal/auth/auth_test.go | 24 +++++++++++++++++++++++- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 28fb664..e13a36a 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -206,7 +206,10 @@ func TokenInfoFromContext(ctx context.Context) *TokenInfo { } // AuthInterceptor returns a gRPC unary server interceptor that validates -// bearer tokens and requires the "admin" role. +// bearer tokens. Any authenticated user or system account is accepted, +// except guests which are explicitly rejected. Admin role is not required +// for agent operations — it is reserved for MCIAS account management and +// policy changes. func AuthInterceptor(validator TokenValidator) grpc.UnaryServerInterceptor { return func( ctx context.Context, @@ -240,9 +243,9 @@ func AuthInterceptor(validator TokenValidator) grpc.UnaryServerInterceptor { return nil, status.Error(codes.Unauthenticated, "invalid token") } - if !tokenInfo.HasRole("admin") { - slog.Warn("permission denied", "method", info.FullMethod, "user", tokenInfo.Username) - return nil, status.Error(codes.PermissionDenied, "admin role required") + if tokenInfo.HasRole("guest") { + slog.Warn("guest access denied", "method", info.FullMethod, "user", tokenInfo.Username) + return nil, status.Error(codes.PermissionDenied, "guest access not permitted") } slog.Info("rpc", "method", info.FullMethod, "user", tokenInfo.Username, "account_type", tokenInfo.AccountType) diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index 803a614..fe6703a 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -126,7 +126,7 @@ func TestInterceptorRejectsInvalidToken(t *testing.T) { } } -func TestInterceptorRejectsNonAdmin(t *testing.T) { +func TestInterceptorAcceptsRegularUser(t *testing.T) { server := mockMCIAS(t, func(authHeader string) (any, int) { return &TokenInfo{ Valid: true, @@ -142,6 +142,28 @@ func TestInterceptorRejectsNonAdmin(t *testing.T) { md := metadata.Pairs("authorization", "Bearer user-token") ctx := metadata.NewIncomingContext(context.Background(), md) + _, err := callInterceptor(ctx, v) + if err != nil { + t.Fatalf("expected regular user to be accepted, got %v", err) + } +} + +func TestInterceptorRejectsGuest(t *testing.T) { + server := mockMCIAS(t, func(authHeader string) (any, int) { + return &TokenInfo{ + Valid: true, + Username: "visitor", + Roles: []string{"guest"}, + AccountType: "human", + }, http.StatusOK + }) + defer server.Close() + + v := validatorFromServer(t, server) + + md := metadata.Pairs("authorization", "Bearer guest-token") + ctx := metadata.NewIncomingContext(context.Background(), md) + _, err := callInterceptor(ctx, v) if err == nil { t.Fatal("expected error, got nil")