Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ tasks:
platforms: [linux, darwin]
# we have to use ldflags to avoid the LC_DYSYMTAB linker error.
# https://github.com/stacklok/toolhive/issues/1687
- go test -ldflags=-extldflags=-Wl,-w -json -race -coverprofile=coverage/coverage.out $(go list ./... | grep -v '/test/e2e' | grep -v '/cmd/thv-operator/test-integration') | gotestfmt -hide "all"
- go test -ldflags=-extldflags=-Wl,-w -json -race -coverpkg=./... -coverprofile=coverage/coverage.out $(go list ./... | grep -v '/test/e2e' | grep -v '/cmd/thv-operator/test-integration') | gotestfmt -hide "all"
- go tool cover -func=coverage/coverage.out
- echo "Generating HTML coverage report in coverage/coverage.html"
- go tool cover -html=coverage/coverage.out -o coverage/coverage.html
Expand All @@ -101,7 +101,7 @@ tasks:
cmds:
- cmd: cmd.exe /c mkdir coverage
ignore_error: true # Windows has no mkdir -p, so just ignore error if it exists
- go test -race -coverprofile=coverage/coverage.out {{.DIR_LIST | catLines}}
- go test -race -coverpkg=./... -coverprofile=coverage/coverage.out {{.DIR_LIST | catLines}}
- go tool cover -func=coverage/coverage.out
- echo "Generating HTML coverage report in coverage/coverage.html"
- go tool cover -html=coverage/coverage.out -o coverage/coverage.html
Expand Down
11 changes: 7 additions & 4 deletions cmd/thv-operator/controllers/mcpremoteproxy_runconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (

mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
"github.com/stacklok/toolhive/pkg/authz"
"github.com/stacklok/toolhive/pkg/authz/authorizers/cedar"
"github.com/stacklok/toolhive/pkg/runner"
transporttypes "github.com/stacklok/toolhive/pkg/transport/types"
)
Expand Down Expand Up @@ -158,10 +159,12 @@ func TestCreateRunConfigFromMCPRemoteProxy(t *testing.T) {
t.Helper()
assert.Equal(t, "authz-proxy", config.Name)
assert.NotNil(t, config.AuthzConfig)
assert.Equal(t, authz.ConfigTypeCedarV1, config.AuthzConfig.Type)
assert.NotNil(t, config.AuthzConfig.Cedar)
assert.Len(t, config.AuthzConfig.Cedar.Policies, 2)
assert.Contains(t, config.AuthzConfig.Cedar.Policies[0], "tools/list")
assert.Equal(t, authz.ConfigType(cedar.ConfigType), config.AuthzConfig.Type)

cedarCfg, err := cedar.ExtractConfig(config.AuthzConfig)
require.NoError(t, err)
assert.Len(t, cedarCfg.Options.Policies, 2)
assert.Contains(t, cedarCfg.Options.Policies[0], "tools/list")
},
},
{
Expand Down
53 changes: 30 additions & 23 deletions cmd/thv-operator/controllers/mcpserver_runconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
ctrlutil "github.com/stacklok/toolhive/cmd/thv-operator/pkg/controllerutil"
"github.com/stacklok/toolhive/cmd/thv-operator/pkg/runconfig/configmap/checksum"
"github.com/stacklok/toolhive/pkg/authz"
"github.com/stacklok/toolhive/pkg/authz/authorizers/cedar"
"github.com/stacklok/toolhive/pkg/container/kubernetes"
"github.com/stacklok/toolhive/pkg/runner"
transporttypes "github.com/stacklok/toolhive/pkg/transport/types"
Expand Down Expand Up @@ -322,14 +323,15 @@ func TestCreateRunConfigFromMCPServer(t *testing.T) {
// Verify authorization config is set
assert.NotNil(t, config.AuthzConfig)
assert.Equal(t, "v1", config.AuthzConfig.Version)
assert.Equal(t, authz.ConfigTypeCedarV1, config.AuthzConfig.Type)
assert.NotNil(t, config.AuthzConfig.Cedar)
assert.Equal(t, authz.ConfigType(cedar.ConfigType), config.AuthzConfig.Type)

// Check Cedar-specific configuration
assert.Len(t, config.AuthzConfig.Cedar.Policies, 2)
assert.Contains(t, config.AuthzConfig.Cedar.Policies, `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`)
assert.Contains(t, config.AuthzConfig.Cedar.Policies, `permit(principal, action == Action::"get_prompt", resource == Prompt::"greeting");`)
assert.Equal(t, `[{"uid": {"type": "User", "id": "user1"}, "attrs": {}}]`, config.AuthzConfig.Cedar.EntitiesJSON)
cedarCfg, err := cedar.ExtractConfig(config.AuthzConfig)
require.NoError(t, err)
assert.Len(t, cedarCfg.Options.Policies, 2)
assert.Contains(t, cedarCfg.Options.Policies, `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`)
assert.Contains(t, cedarCfg.Options.Policies, `permit(principal, action == Action::"get_prompt", resource == Prompt::"greeting");`)
assert.Equal(t, `[{"uid": {"type": "User", "id": "user1"}, "attrs": {}}]`, cedarCfg.Options.EntitiesJSON)
},
},
{
Expand Down Expand Up @@ -359,11 +361,13 @@ func TestCreateRunConfigFromMCPServer(t *testing.T) {
// For ConfigMap type, with new feature, authorization config is embedded in RunConfig
require.NotNil(t, config.AuthzConfig)
assert.Equal(t, "v1", config.AuthzConfig.Version)
assert.Equal(t, authz.ConfigTypeCedarV1, config.AuthzConfig.Type)
require.NotNil(t, config.AuthzConfig.Cedar)
assert.Len(t, config.AuthzConfig.Cedar.Policies, 1)
assert.Contains(t, config.AuthzConfig.Cedar.Policies[0], "call_tool")
assert.Equal(t, "[]", config.AuthzConfig.Cedar.EntitiesJSON)
assert.Equal(t, authz.ConfigType(cedar.ConfigType), config.AuthzConfig.Type)

cedarCfg, err := cedar.ExtractConfig(config.AuthzConfig)
require.NoError(t, err)
assert.Len(t, cedarCfg.Options.Policies, 1)
assert.Contains(t, cedarCfg.Options.Policies[0], "call_tool")
assert.Equal(t, "[]", cedarCfg.Options.EntitiesJSON)
},
},
{
Expand Down Expand Up @@ -748,14 +752,15 @@ func TestEnsureRunConfigConfigMap(t *testing.T) {
// Verify authorization configuration is properly serialized
assert.NotNil(t, runConfig.AuthzConfig, "AuthzConfig should be present in runconfig.json")
assert.Equal(t, "v1", runConfig.AuthzConfig.Version)
assert.Equal(t, authz.ConfigTypeCedarV1, runConfig.AuthzConfig.Type)
assert.NotNil(t, runConfig.AuthzConfig.Cedar)
assert.Equal(t, authz.ConfigType(cedar.ConfigType), runConfig.AuthzConfig.Type)

// Check Cedar-specific configuration
assert.Len(t, runConfig.AuthzConfig.Cedar.Policies, 2)
assert.Contains(t, runConfig.AuthzConfig.Cedar.Policies, `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`)
assert.Contains(t, runConfig.AuthzConfig.Cedar.Policies, `permit(principal, action == Action::"get_prompt", resource == Prompt::"greeting");`)
assert.Equal(t, `[{"uid": {"type": "User", "id": "user1"}, "attrs": {}}]`, runConfig.AuthzConfig.Cedar.EntitiesJSON)
cedarCfg, err := cedar.ExtractConfig(runConfig.AuthzConfig)
require.NoError(t, err)
assert.Len(t, cedarCfg.Options.Policies, 2)
assert.Contains(t, cedarCfg.Options.Policies, `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`)
assert.Contains(t, cedarCfg.Options.Policies, `permit(principal, action == Action::"get_prompt", resource == Prompt::"greeting");`)
assert.Equal(t, `[{"uid": {"type": "User", "id": "user1"}, "attrs": {}}]`, cedarCfg.Options.EntitiesJSON)
},
},
{
Expand Down Expand Up @@ -968,12 +973,14 @@ func TestEnsureRunConfigConfigMap(t *testing.T) {

require.NotNil(t, runConfig.AuthzConfig)
assert.Equal(t, "v1", runConfig.AuthzConfig.Version)
assert.Equal(t, authz.ConfigTypeCedarV1, runConfig.AuthzConfig.Type)
require.NotNil(t, runConfig.AuthzConfig.Cedar)
assert.Len(t, runConfig.AuthzConfig.Cedar.Policies, 2)
assert.Contains(t, runConfig.AuthzConfig.Cedar.Policies, `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`)
assert.Contains(t, runConfig.AuthzConfig.Cedar.Policies, `permit(principal, action == Action::"get_prompt", resource == Prompt::"greeting");`)
assert.Equal(t, `[{"uid": {"type": "User", "id": "user1"}, "attrs": {}}]`, runConfig.AuthzConfig.Cedar.EntitiesJSON)
assert.Equal(t, authz.ConfigType(cedar.ConfigType), runConfig.AuthzConfig.Type)

cedarCfg, err := cedar.ExtractConfig(runConfig.AuthzConfig)
require.NoError(t, err)
assert.Len(t, cedarCfg.Options.Policies, 2)
assert.Contains(t, cedarCfg.Options.Policies, `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`)
assert.Contains(t, cedarCfg.Options.Policies, `permit(principal, action == Action::"get_prompt", resource == Prompt::"greeting");`)
assert.Equal(t, `[{"uid": {"type": "User", "id": "user1"}, "attrs": {}}]`, cedarCfg.Options.EntitiesJSON)
})
}

Expand Down
52 changes: 32 additions & 20 deletions cmd/thv-operator/pkg/controllerutil/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
"github.com/stacklok/toolhive/cmd/thv-operator/pkg/kubernetes/configmaps"
"github.com/stacklok/toolhive/pkg/authz"
"github.com/stacklok/toolhive/pkg/authz/authorizers/cedar"
"github.com/stacklok/toolhive/pkg/runner"
)

Expand Down Expand Up @@ -162,6 +163,36 @@ func EnsureAuthzConfigMap(
return nil
}

func addAuthzInlineConfigOptions(
authzRef *mcpv1alpha1.AuthzConfigRef,
options *[]runner.RunConfigBuilderOption,
) error {
if authzRef.Inline == nil {
return fmt.Errorf("inline authz config type specified but inline config is nil")
}

policies := authzRef.Inline.Policies
entitiesJSON := authzRef.Inline.EntitiesJSON

// Create authorization config using the full config structure
// This maintains backwards compatibility with the v1.0 schema
authzCfg, err := authz.NewConfig(cedar.Config{
Version: "v1",
Type: cedar.ConfigType,
Options: &cedar.ConfigOptions{
Policies: policies,
EntitiesJSON: entitiesJSON,
},
})
if err != nil {
return fmt.Errorf("failed to create authz config: %w", err)
}

// Add authorization config to options
*options = append(*options, runner.WithAuthzConfig(authzCfg))
return nil
}

// AddAuthzConfigOptions adds authorization configuration options to builder options
func AddAuthzConfigOptions(
ctx context.Context,
Expand All @@ -176,26 +207,7 @@ func AddAuthzConfigOptions(

switch authzRef.Type {
case mcpv1alpha1.AuthzConfigTypeInline:
if authzRef.Inline == nil {
return fmt.Errorf("inline authz config type specified but inline config is nil")
}

policies := authzRef.Inline.Policies
entitiesJSON := authzRef.Inline.EntitiesJSON

// Create authorization config
authzCfg := &authz.Config{
Version: "v1",
Type: authz.ConfigTypeCedarV1,
Cedar: &authz.CedarConfig{
Policies: policies,
EntitiesJSON: entitiesJSON,
},
}

// Add authorization config to options
*options = append(*options, runner.WithAuthzConfig(authzCfg))
return nil
return addAuthzInlineConfigOptions(authzRef, options)

case mcpv1alpha1.AuthzConfigTypeConfigMap:
// Validate reference
Expand Down
Loading
Loading