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) <noreply@anthropic.com>
This commit is contained in:
2026-03-28 16:07:17 -07:00
parent dd167b8e0b
commit 86d516acf6
2 changed files with 30 additions and 5 deletions

View File

@@ -206,7 +206,10 @@ func TokenInfoFromContext(ctx context.Context) *TokenInfo {
} }
// AuthInterceptor returns a gRPC unary server interceptor that validates // 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 { func AuthInterceptor(validator TokenValidator) grpc.UnaryServerInterceptor {
return func( return func(
ctx context.Context, ctx context.Context,
@@ -240,9 +243,9 @@ func AuthInterceptor(validator TokenValidator) grpc.UnaryServerInterceptor {
return nil, status.Error(codes.Unauthenticated, "invalid token") return nil, status.Error(codes.Unauthenticated, "invalid token")
} }
if !tokenInfo.HasRole("admin") { if tokenInfo.HasRole("guest") {
slog.Warn("permission denied", "method", info.FullMethod, "user", tokenInfo.Username) slog.Warn("guest access denied", "method", info.FullMethod, "user", tokenInfo.Username)
return nil, status.Error(codes.PermissionDenied, "admin role required") return nil, status.Error(codes.PermissionDenied, "guest access not permitted")
} }
slog.Info("rpc", "method", info.FullMethod, "user", tokenInfo.Username, "account_type", tokenInfo.AccountType) slog.Info("rpc", "method", info.FullMethod, "user", tokenInfo.Username, "account_type", tokenInfo.AccountType)

View File

@@ -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) { server := mockMCIAS(t, func(authHeader string) (any, int) {
return &TokenInfo{ return &TokenInfo{
Valid: true, Valid: true,
@@ -142,6 +142,28 @@ func TestInterceptorRejectsNonAdmin(t *testing.T) {
md := metadata.Pairs("authorization", "Bearer user-token") md := metadata.Pairs("authorization", "Bearer user-token")
ctx := metadata.NewIncomingContext(context.Background(), md) 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) _, err := callInterceptor(ctx, v)
if err == nil { if err == nil {
t.Fatal("expected error, got nil") t.Fatal("expected error, got nil")