e04ed9c211
CF: Deploy Dev Docs / deploy (push) Has been cancelled
Sync Labels / build (push) Has been cancelled
tests / unit tests (macos-latest) (push) Has been cancelled
tests / unit tests (windows-latest) (push) Has been cancelled
tests / unit tests (ubuntu-latest) (push) Has been cancelled
182 lines
4.6 KiB
Go
182 lines
4.6 KiB
Go
// Copyright 2026 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package util
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"testing"
|
|
|
|
"github.com/googleapis/mcp-toolbox/internal/auth"
|
|
"github.com/googleapis/mcp-toolbox/internal/util"
|
|
)
|
|
|
|
type mockAuthService struct {
|
|
name string
|
|
mcpEnabled bool
|
|
}
|
|
|
|
func (aS mockAuthService) AuthServiceType() string {
|
|
return "mock"
|
|
}
|
|
|
|
func (aS mockAuthService) GetName() string {
|
|
return aS.name
|
|
}
|
|
|
|
func (aS mockAuthService) GetClaimsFromHeader(context.Context, http.Header) (map[string]any, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
type mockAuthServiceConfig struct {
|
|
name string
|
|
}
|
|
|
|
func (c mockAuthServiceConfig) AuthServiceConfigType() string {
|
|
return "mock"
|
|
}
|
|
|
|
func (c mockAuthServiceConfig) IsMCPEnabled() bool {
|
|
return false
|
|
}
|
|
|
|
func (c mockAuthServiceConfig) Initialize() (auth.AuthService, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (aS mockAuthService) ToConfig() auth.AuthServiceConfig {
|
|
return mockAuthServiceConfig{
|
|
name: aS.name,
|
|
}
|
|
}
|
|
|
|
func (aS mockAuthService) IsMCPEnabled() bool {
|
|
return aS.mcpEnabled
|
|
}
|
|
|
|
func (aS mockAuthService) GetScopesRequired() []string {
|
|
return nil
|
|
}
|
|
|
|
func (aS mockAuthService) GetAuthorizationServer() string {
|
|
return ""
|
|
}
|
|
|
|
func (aS mockAuthService) ValidateMCPAuth(context.Context, http.Header) (map[string]any, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func TestValidateScopes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
toolScopes []string
|
|
authServices map[string]auth.AuthService
|
|
setupCtx func(context.Context) context.Context
|
|
wantErr bool
|
|
errMessage string
|
|
}{
|
|
{
|
|
name: "no required scopes",
|
|
toolScopes: nil,
|
|
authServices: map[string]auth.AuthService{
|
|
"svc1": mockAuthService{name: "svc1", mcpEnabled: true},
|
|
},
|
|
setupCtx: func(ctx context.Context) context.Context {
|
|
return ctx
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "mcp auth service not enabled",
|
|
toolScopes: []string{"read:files"},
|
|
authServices: map[string]auth.AuthService{
|
|
"svc1": mockAuthService{name: "svc1", mcpEnabled: false},
|
|
},
|
|
setupCtx: func(ctx context.Context) context.Context {
|
|
return ctx
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "missing claims in context",
|
|
toolScopes: []string{"read:files"},
|
|
authServices: map[string]auth.AuthService{
|
|
"svc1": mockAuthService{name: "svc1", mcpEnabled: true},
|
|
},
|
|
setupCtx: func(ctx context.Context) context.Context {
|
|
return ctx
|
|
},
|
|
wantErr: true,
|
|
errMessage: "missing claims for MCP authorization",
|
|
},
|
|
{
|
|
name: "insufficient scopes",
|
|
toolScopes: []string{"read:files", "write:files"},
|
|
authServices: map[string]auth.AuthService{
|
|
"svc1": mockAuthService{name: "svc1", mcpEnabled: true},
|
|
},
|
|
setupCtx: func(ctx context.Context) context.Context {
|
|
claims := map[string]any{
|
|
"scope": "read:files",
|
|
}
|
|
return util.WithAuthTokenClaims(ctx, claims)
|
|
},
|
|
wantErr: true,
|
|
errMessage: "insufficient scopes for this tool",
|
|
},
|
|
{
|
|
name: "sufficient scopes",
|
|
toolScopes: []string{"read:files", "write:files"},
|
|
authServices: map[string]auth.AuthService{
|
|
"svc1": mockAuthService{name: "svc1", mcpEnabled: true},
|
|
},
|
|
setupCtx: func(ctx context.Context) context.Context {
|
|
claims := map[string]any{
|
|
"scope": "read:files write:files other:scope",
|
|
}
|
|
return util.WithAuthTokenClaims(ctx, claims)
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx := tc.setupCtx(context.Background())
|
|
err := ValidateScopes(ctx, tc.toolScopes, tc.authServices)
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
var authErr *auth.MCPAuthError
|
|
if !errors.As(err, &authErr) {
|
|
t.Fatalf("expected auth.MCPAuthError, got: %T", err)
|
|
}
|
|
if authErr.Code != http.StatusForbidden {
|
|
t.Errorf("expected Code StatusForbidden (403), got: %d", authErr.Code)
|
|
}
|
|
if authErr.Message != tc.errMessage {
|
|
t.Errorf("expected error message %q, got %q", tc.errMessage, authErr.Message)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|