Skip to content

Commit f860012

Browse files
committed
[#3109] Implement authorization abstraction
This patch implements an 'authorizer' abstraction under pkg/authz, and then moves the Cedar implementation as the first/canonical form under pkg/authz/authorizers/cedar. The configuration schema remains untouched, though the mechanism for loading configuration has been reworked to avoid violating the authorizer abstraction with Cedar-isms. This fixes #3109 Signed-off-by: Greg Haskins <greg@manetu.com>
1 parent 5866023 commit f860012

File tree

24 files changed

+829
-429
lines changed

24 files changed

+829
-429
lines changed

cmd/thv-operator/controllers/mcpremoteproxy_runconfig_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030

3131
mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
3232
"github.com/stacklok/toolhive/pkg/authz"
33+
"github.com/stacklok/toolhive/pkg/authz/authorizers/cedar"
3334
"github.com/stacklok/toolhive/pkg/runner"
3435
transporttypes "github.com/stacklok/toolhive/pkg/transport/types"
3536
)
@@ -158,10 +159,12 @@ func TestCreateRunConfigFromMCPRemoteProxy(t *testing.T) {
158159
t.Helper()
159160
assert.Equal(t, "authz-proxy", config.Name)
160161
assert.NotNil(t, config.AuthzConfig)
161-
assert.Equal(t, authz.ConfigTypeCedarV1, config.AuthzConfig.Type)
162-
assert.NotNil(t, config.AuthzConfig.Cedar)
163-
assert.Len(t, config.AuthzConfig.Cedar.Policies, 2)
164-
assert.Contains(t, config.AuthzConfig.Cedar.Policies[0], "tools/list")
162+
assert.Equal(t, authz.ConfigType(cedar.ConfigType), config.AuthzConfig.Type)
163+
164+
cedarCfg, err := cedar.ExtractConfig(config.AuthzConfig)
165+
require.NoError(t, err)
166+
assert.Len(t, cedarCfg.Options.Policies, 2)
167+
assert.Contains(t, cedarCfg.Options.Policies[0], "tools/list")
165168
},
166169
},
167170
{

cmd/thv-operator/controllers/mcpserver_runconfig_test.go

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
ctrlutil "github.com/stacklok/toolhive/cmd/thv-operator/pkg/controllerutil"
2020
"github.com/stacklok/toolhive/cmd/thv-operator/pkg/runconfig/configmap/checksum"
2121
"github.com/stacklok/toolhive/pkg/authz"
22+
"github.com/stacklok/toolhive/pkg/authz/authorizers/cedar"
2223
"github.com/stacklok/toolhive/pkg/container/kubernetes"
2324
"github.com/stacklok/toolhive/pkg/runner"
2425
transporttypes "github.com/stacklok/toolhive/pkg/transport/types"
@@ -322,14 +323,15 @@ func TestCreateRunConfigFromMCPServer(t *testing.T) {
322323
// Verify authorization config is set
323324
assert.NotNil(t, config.AuthzConfig)
324325
assert.Equal(t, "v1", config.AuthzConfig.Version)
325-
assert.Equal(t, authz.ConfigTypeCedarV1, config.AuthzConfig.Type)
326-
assert.NotNil(t, config.AuthzConfig.Cedar)
326+
assert.Equal(t, authz.ConfigType(cedar.ConfigType), config.AuthzConfig.Type)
327327

328328
// Check Cedar-specific configuration
329-
assert.Len(t, config.AuthzConfig.Cedar.Policies, 2)
330-
assert.Contains(t, config.AuthzConfig.Cedar.Policies, `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`)
331-
assert.Contains(t, config.AuthzConfig.Cedar.Policies, `permit(principal, action == Action::"get_prompt", resource == Prompt::"greeting");`)
332-
assert.Equal(t, `[{"uid": {"type": "User", "id": "user1"}, "attrs": {}}]`, config.AuthzConfig.Cedar.EntitiesJSON)
329+
cedarCfg, err := cedar.ExtractConfig(config.AuthzConfig)
330+
require.NoError(t, err)
331+
assert.Len(t, cedarCfg.Options.Policies, 2)
332+
assert.Contains(t, cedarCfg.Options.Policies, `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`)
333+
assert.Contains(t, cedarCfg.Options.Policies, `permit(principal, action == Action::"get_prompt", resource == Prompt::"greeting");`)
334+
assert.Equal(t, `[{"uid": {"type": "User", "id": "user1"}, "attrs": {}}]`, cedarCfg.Options.EntitiesJSON)
333335
},
334336
},
335337
{
@@ -359,11 +361,13 @@ func TestCreateRunConfigFromMCPServer(t *testing.T) {
359361
// For ConfigMap type, with new feature, authorization config is embedded in RunConfig
360362
require.NotNil(t, config.AuthzConfig)
361363
assert.Equal(t, "v1", config.AuthzConfig.Version)
362-
assert.Equal(t, authz.ConfigTypeCedarV1, config.AuthzConfig.Type)
363-
require.NotNil(t, config.AuthzConfig.Cedar)
364-
assert.Len(t, config.AuthzConfig.Cedar.Policies, 1)
365-
assert.Contains(t, config.AuthzConfig.Cedar.Policies[0], "call_tool")
366-
assert.Equal(t, "[]", config.AuthzConfig.Cedar.EntitiesJSON)
364+
assert.Equal(t, authz.ConfigType(cedar.ConfigType), config.AuthzConfig.Type)
365+
366+
cedarCfg, err := cedar.ExtractConfig(config.AuthzConfig)
367+
require.NoError(t, err)
368+
assert.Len(t, cedarCfg.Options.Policies, 1)
369+
assert.Contains(t, cedarCfg.Options.Policies[0], "call_tool")
370+
assert.Equal(t, "[]", cedarCfg.Options.EntitiesJSON)
367371
},
368372
},
369373
{
@@ -748,14 +752,15 @@ func TestEnsureRunConfigConfigMap(t *testing.T) {
748752
// Verify authorization configuration is properly serialized
749753
assert.NotNil(t, runConfig.AuthzConfig, "AuthzConfig should be present in runconfig.json")
750754
assert.Equal(t, "v1", runConfig.AuthzConfig.Version)
751-
assert.Equal(t, authz.ConfigTypeCedarV1, runConfig.AuthzConfig.Type)
752-
assert.NotNil(t, runConfig.AuthzConfig.Cedar)
755+
assert.Equal(t, authz.ConfigType(cedar.ConfigType), runConfig.AuthzConfig.Type)
753756

754757
// Check Cedar-specific configuration
755-
assert.Len(t, runConfig.AuthzConfig.Cedar.Policies, 2)
756-
assert.Contains(t, runConfig.AuthzConfig.Cedar.Policies, `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`)
757-
assert.Contains(t, runConfig.AuthzConfig.Cedar.Policies, `permit(principal, action == Action::"get_prompt", resource == Prompt::"greeting");`)
758-
assert.Equal(t, `[{"uid": {"type": "User", "id": "user1"}, "attrs": {}}]`, runConfig.AuthzConfig.Cedar.EntitiesJSON)
758+
cedarCfg, err := cedar.ExtractConfig(runConfig.AuthzConfig)
759+
require.NoError(t, err)
760+
assert.Len(t, cedarCfg.Options.Policies, 2)
761+
assert.Contains(t, cedarCfg.Options.Policies, `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`)
762+
assert.Contains(t, cedarCfg.Options.Policies, `permit(principal, action == Action::"get_prompt", resource == Prompt::"greeting");`)
763+
assert.Equal(t, `[{"uid": {"type": "User", "id": "user1"}, "attrs": {}}]`, cedarCfg.Options.EntitiesJSON)
759764
},
760765
},
761766
{
@@ -968,12 +973,14 @@ func TestEnsureRunConfigConfigMap(t *testing.T) {
968973

969974
require.NotNil(t, runConfig.AuthzConfig)
970975
assert.Equal(t, "v1", runConfig.AuthzConfig.Version)
971-
assert.Equal(t, authz.ConfigTypeCedarV1, runConfig.AuthzConfig.Type)
972-
require.NotNil(t, runConfig.AuthzConfig.Cedar)
973-
assert.Len(t, runConfig.AuthzConfig.Cedar.Policies, 2)
974-
assert.Contains(t, runConfig.AuthzConfig.Cedar.Policies, `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`)
975-
assert.Contains(t, runConfig.AuthzConfig.Cedar.Policies, `permit(principal, action == Action::"get_prompt", resource == Prompt::"greeting");`)
976-
assert.Equal(t, `[{"uid": {"type": "User", "id": "user1"}, "attrs": {}}]`, runConfig.AuthzConfig.Cedar.EntitiesJSON)
976+
assert.Equal(t, authz.ConfigType(cedar.ConfigType), runConfig.AuthzConfig.Type)
977+
978+
cedarCfg, err := cedar.ExtractConfig(runConfig.AuthzConfig)
979+
require.NoError(t, err)
980+
assert.Len(t, cedarCfg.Options.Policies, 2)
981+
assert.Contains(t, cedarCfg.Options.Policies, `permit(principal, action == Action::"call_tool", resource == Tool::"weather");`)
982+
assert.Contains(t, cedarCfg.Options.Policies, `permit(principal, action == Action::"get_prompt", resource == Prompt::"greeting");`)
983+
assert.Equal(t, `[{"uid": {"type": "User", "id": "user1"}, "attrs": {}}]`, cedarCfg.Options.EntitiesJSON)
977984
})
978985
}
979986

cmd/thv-operator/pkg/controllerutil/authz.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
1818
"github.com/stacklok/toolhive/cmd/thv-operator/pkg/kubernetes/configmaps"
1919
"github.com/stacklok/toolhive/pkg/authz"
20+
"github.com/stacklok/toolhive/pkg/authz/authorizers/cedar"
2021
"github.com/stacklok/toolhive/pkg/runner"
2122
)
2223

@@ -183,14 +184,18 @@ func AddAuthzConfigOptions(
183184
policies := authzRef.Inline.Policies
184185
entitiesJSON := authzRef.Inline.EntitiesJSON
185186

186-
// Create authorization config
187-
authzCfg := &authz.Config{
187+
// Create authorization config using the full config structure
188+
// This maintains backwards compatibility with the v1.0 schema
189+
authzCfg, err := authz.NewConfig(cedar.Config{
188190
Version: "v1",
189-
Type: authz.ConfigTypeCedarV1,
190-
Cedar: &authz.CedarConfig{
191+
Type: cedar.ConfigType,
192+
Options: &cedar.ConfigOptions{
191193
Policies: policies,
192194
EntitiesJSON: entitiesJSON,
193195
},
196+
})
197+
if err != nil {
198+
return fmt.Errorf("failed to create authz config: %w", err)
194199
}
195200

196201
// Add authorization config to options

cmd/thv-operator/test-integration/mcp-server/mcpserver_runconfig_integration_test.go

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
mcpv1alpha1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1alpha1"
1717
"github.com/stacklok/toolhive/cmd/thv-operator/pkg/runconfig/configmap/checksum"
1818
"github.com/stacklok/toolhive/pkg/authz"
19+
"github.com/stacklok/toolhive/pkg/authz/authorizers/cedar"
1920
"github.com/stacklok/toolhive/pkg/runner"
2021
transporttypes "github.com/stacklok/toolhive/pkg/transport/types"
2122
)
@@ -481,12 +482,14 @@ var _ = Describe("RunConfig ConfigMap Integration Tests", func() {
481482
// Verify authorization configuration
482483
Expect(runConfig.AuthzConfig).NotTo(BeNil())
483484
Expect(runConfig.AuthzConfig.Version).To(Equal("v1"))
484-
Expect(runConfig.AuthzConfig.Type).To(Equal(authz.ConfigTypeCedarV1))
485-
Expect(runConfig.AuthzConfig.Cedar).NotTo(BeNil())
486-
Expect(runConfig.AuthzConfig.Cedar.Policies).To(HaveLen(2))
487-
Expect(runConfig.AuthzConfig.Cedar.Policies[0]).To(ContainSubstring("call_tool"))
488-
Expect(runConfig.AuthzConfig.Cedar.Policies[1]).To(ContainSubstring("get_prompt"))
489-
Expect(runConfig.AuthzConfig.Cedar.EntitiesJSON).To(ContainSubstring("user1"))
485+
Expect(runConfig.AuthzConfig.Type).To(Equal(authz.ConfigType(cedar.ConfigType)))
486+
487+
cedarCfg, err := cedar.ExtractConfig(runConfig.AuthzConfig)
488+
Expect(err).NotTo(HaveOccurred())
489+
Expect(cedarCfg.Options.Policies).To(HaveLen(2))
490+
Expect(cedarCfg.Options.Policies[0]).To(ContainSubstring("call_tool"))
491+
Expect(cedarCfg.Options.Policies[1]).To(ContainSubstring("get_prompt"))
492+
Expect(cedarCfg.Options.EntitiesJSON).To(ContainSubstring("user1"))
490493
})
491494

492495
It("Should handle deterministic ConfigMap generation", func() {
@@ -795,26 +798,27 @@ var _ = Describe("RunConfig ConfigMap Integration Tests", func() {
795798
// Verify authorization configuration was embedded from external ConfigMap
796799
Expect(runConfig.AuthzConfig).NotTo(BeNil())
797800
Expect(runConfig.AuthzConfig.Version).To(Equal("v1"))
798-
Expect(runConfig.AuthzConfig.Type).To(Equal(authz.ConfigTypeCedarV1))
801+
Expect(runConfig.AuthzConfig.Type).To(Equal(authz.ConfigType(cedar.ConfigType)))
799802

800803
// Verify Cedar configuration
801-
Expect(runConfig.AuthzConfig.Cedar).NotTo(BeNil())
804+
cedarCfg, err := cedar.ExtractConfig(runConfig.AuthzConfig)
805+
Expect(err).NotTo(HaveOccurred())
802806

803807
// Check policies are present
804-
Expect(runConfig.AuthzConfig.Cedar.Policies).To(HaveLen(3))
805-
Expect(runConfig.AuthzConfig.Cedar.Policies[0]).To(ContainSubstring("call_tool"))
806-
Expect(runConfig.AuthzConfig.Cedar.Policies[0]).To(ContainSubstring("weather"))
807-
Expect(runConfig.AuthzConfig.Cedar.Policies[1]).To(ContainSubstring("get_prompt"))
808-
Expect(runConfig.AuthzConfig.Cedar.Policies[1]).To(ContainSubstring("greeting"))
809-
Expect(runConfig.AuthzConfig.Cedar.Policies[2]).To(ContainSubstring("forbid"))
810-
Expect(runConfig.AuthzConfig.Cedar.Policies[2]).To(ContainSubstring("sensitive_data"))
808+
Expect(cedarCfg.Options.Policies).To(HaveLen(3))
809+
Expect(cedarCfg.Options.Policies[0]).To(ContainSubstring("call_tool"))
810+
Expect(cedarCfg.Options.Policies[0]).To(ContainSubstring("weather"))
811+
Expect(cedarCfg.Options.Policies[1]).To(ContainSubstring("get_prompt"))
812+
Expect(cedarCfg.Options.Policies[1]).To(ContainSubstring("greeting"))
813+
Expect(cedarCfg.Options.Policies[2]).To(ContainSubstring("forbid"))
814+
Expect(cedarCfg.Options.Policies[2]).To(ContainSubstring("sensitive_data"))
811815

812816
// Verify entities are embedded
813-
Expect(runConfig.AuthzConfig.Cedar.EntitiesJSON).NotTo(BeEmpty())
817+
Expect(cedarCfg.Options.EntitiesJSON).NotTo(BeEmpty())
814818

815819
// Parse entities to verify they're correctly embedded
816820
var entities []interface{}
817-
err = json.Unmarshal([]byte(runConfig.AuthzConfig.Cedar.EntitiesJSON), &entities)
821+
err = json.Unmarshal([]byte(cedarCfg.Options.EntitiesJSON), &entities)
818822
Expect(err).NotTo(HaveOccurred())
819823
Expect(entities).To(HaveLen(2))
820824

0 commit comments

Comments
 (0)