Skip to content
24 changes: 23 additions & 1 deletion cmd/thv-operator/pkg/registryapi/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ type Config struct {
RegistryName string `yaml:"registryName,omitempty"`
Registries []RegistryConfig `yaml:"registries"`
Database *DatabaseConfig `yaml:"database,omitempty"`
Auth *AuthConfig `yaml:"auth,omitempty"`
}

// DatabaseConfig defines PostgreSQL database configuration
Expand Down Expand Up @@ -153,6 +154,22 @@ type RegistryConfig struct {
Filter *FilterConfig `yaml:"filter,omitempty"`
}

// AuthMode represents the authentication mode
type AuthMode string

const (
// AuthModeAnonymous allows unauthenticated access
AuthModeAnonymous AuthMode = "anonymous"
)

// AuthConfig defines authentication configuration for the registry server
type AuthConfig struct {
// Mode specifies the authentication mode (anonymous or oauth)
// Defaults to "oauth" if not specified (security-by-default).
// Use "anonymous" to explicitly disable authentication for development.
Mode AuthMode `yaml:"mode,omitempty"`
}

// KubernetesConfig defines a Kubernetes-based registry source where data is discovered
// from MCPServer resources in the cluster. This is the default type for the built-in "default" registry.
type KubernetesConfig struct {
Expand Down Expand Up @@ -244,7 +261,12 @@ func (c *Config) ToConfigMapWithContentChecksum(mcpRegistry *mcpv1alpha1.MCPRegi
const DefaultRegistryName = "default"

func (cm *configManager) BuildConfig() (*Config, error) {
config := Config{}
config := Config{
// default to anonymous authentication until we model it consistently in the MCPRegistry CRD
Auth: &AuthConfig{
Mode: AuthModeAnonymous,
},
}

mcpRegistry := cm.mcpRegistry

Expand Down
19 changes: 19 additions & 0 deletions cmd/thv-operator/pkg/registryapi/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ func TestBuildConfig_ConfigMapSource(t *testing.T) {
assert.Equal(t, filepath.Join(RegistryJSONFilePath, "configmap-registry", RegistryJSONFileName), config.Registries[1].File.Path)
require.NotNil(t, config.Registries[1].SyncPolicy)
assert.Equal(t, "1h", config.Registries[1].SyncPolicy.Interval)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})

}
Expand Down Expand Up @@ -370,6 +371,7 @@ func TestBuildConfig_GitSource(t *testing.T) {
assert.Empty(t, config.Registries[1].Git.Commit)
require.NotNil(t, config.Registries[1].SyncPolicy)
assert.Equal(t, "1h", config.Registries[1].SyncPolicy.Interval)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})

t.Run("valid git source with tag", func(t *testing.T) {
Expand Down Expand Up @@ -417,6 +419,7 @@ func TestBuildConfig_GitSource(t *testing.T) {
assert.Empty(t, config.Registries[1].Git.Commit)
require.NotNil(t, config.Registries[1].SyncPolicy)
assert.Equal(t, "1h", config.Registries[1].SyncPolicy.Interval)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})

t.Run("valid git source with commit", func(t *testing.T) {
Expand Down Expand Up @@ -464,6 +467,7 @@ func TestBuildConfig_GitSource(t *testing.T) {
assert.Equal(t, "abc123def456", config.Registries[1].Git.Commit)
require.NotNil(t, config.Registries[1].SyncPolicy)
assert.Equal(t, "1h", config.Registries[1].SyncPolicy.Interval)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})
}

Expand Down Expand Up @@ -566,6 +570,7 @@ func TestBuildConfig_APISource(t *testing.T) {
assert.Nil(t, config.Registries[1].Git)
require.NotNil(t, config.Registries[1].SyncPolicy)
assert.Equal(t, "1h", config.Registries[1].SyncPolicy.Interval)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})
}

Expand Down Expand Up @@ -610,6 +615,7 @@ func TestBuildConfig_SyncPolicy(t *testing.T) {
require.NotNil(t, config.Registries[0].Kubernetes)
// Second registry should be the user-specified one
assert.Nil(t, config.Registries[1].SyncPolicy)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})

t.Run("empty interval", func(t *testing.T) {
Expand Down Expand Up @@ -684,6 +690,7 @@ func TestBuildConfig_SyncPolicy(t *testing.T) {
// Second registry should be the user-specified one
require.NotNil(t, config.Registries[1].SyncPolicy)
assert.Equal(t, "30m", config.Registries[1].SyncPolicy.Interval)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})
}

Expand Down Expand Up @@ -731,6 +738,7 @@ func TestBuildConfig_Filter(t *testing.T) {
require.NotNil(t, config.Registries[0].Kubernetes)
// Filter should be nil when not provided for the user-specified registry
assert.Nil(t, config.Registries[1].Filter)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})

t.Run("filter with name filters", func(t *testing.T) {
Expand Down Expand Up @@ -781,6 +789,7 @@ func TestBuildConfig_Filter(t *testing.T) {
assert.Equal(t, []string{"*-deprecated", "*-test"}, config.Registries[1].Filter.Names.Exclude)
// Tags should be nil when not provided
assert.Nil(t, config.Registries[1].Filter.Tags)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})

t.Run("filter with tags", func(t *testing.T) {
Expand Down Expand Up @@ -830,6 +839,7 @@ func TestBuildConfig_Filter(t *testing.T) {
assert.Equal(t, []string{"beta", "alpha", "experimental"}, config.Registries[1].Filter.Tags.Exclude)
// Names should be nil when not provided
assert.Nil(t, config.Registries[1].Filter.Names)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})

t.Run("filter with both name filters and tags", func(t *testing.T) {
Expand Down Expand Up @@ -883,6 +893,7 @@ func TestBuildConfig_Filter(t *testing.T) {
require.NotNil(t, config.Registries[1].Filter.Tags)
assert.Equal(t, []string{"latest", "stable"}, config.Registries[1].Filter.Tags.Include)
assert.Equal(t, []string{"dev", "test"}, config.Registries[1].Filter.Tags.Exclude)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})

t.Run("filter with empty include and exclude lists", func(t *testing.T) {
Expand Down Expand Up @@ -939,6 +950,7 @@ func TestBuildConfig_Filter(t *testing.T) {
require.NotNil(t, config.Registries[1].Filter.Tags)
assert.Empty(t, config.Registries[1].Filter.Tags.Include)
assert.Empty(t, config.Registries[1].Filter.Tags.Exclude)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})
}

Expand Down Expand Up @@ -1097,6 +1109,7 @@ func TestBuildConfig_MultipleRegistries(t *testing.T) {
require.NotNil(t, config.Registries[2].Filter)
require.NotNil(t, config.Registries[2].Filter.Names)
assert.Equal(t, []string{"server-*"}, config.Registries[2].Filter.Names.Include)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
}

func TestBuildConfig_PVCSource(t *testing.T) {
Expand Down Expand Up @@ -1141,6 +1154,7 @@ func TestBuildConfig_PVCSource(t *testing.T) {
require.NotNil(t, config.Registries[1].File)
// Path: /config/registry/{registryName}/{pvcRef.path}
assert.Equal(t, filepath.Join(RegistryJSONFilePath, "pvc-registry", RegistryJSONFileName), config.Registries[1].File.Path)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})

t.Run("valid pvc source with subdirectory path", func(t *testing.T) {
Expand Down Expand Up @@ -1181,6 +1195,7 @@ func TestBuildConfig_PVCSource(t *testing.T) {
require.NotNil(t, config.Registries[1].File)
// Path: /config/registry/{registryName}/{pvcRef.path}
assert.Equal(t, filepath.Join(RegistryJSONFilePath, "production-registry", "production/v1/servers.json"), config.Registries[1].File.Path)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})

t.Run("valid pvc source with filter", func(t *testing.T) {
Expand Down Expand Up @@ -1233,6 +1248,7 @@ func TestBuildConfig_PVCSource(t *testing.T) {
assert.Equal(t, []string{"prod-*"}, config.Registries[1].Filter.Names.Include)
require.NotNil(t, config.Registries[1].Filter.Tags)
assert.Equal(t, []string{"production"}, config.Registries[1].Filter.Tags.Include)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})
}
func TestBuildConfig_DatabaseConfig(t *testing.T) {
Expand Down Expand Up @@ -1278,6 +1294,7 @@ func TestBuildConfig_DatabaseConfig(t *testing.T) {
assert.Equal(t, 10, config.Database.MaxOpenConns)
assert.Equal(t, 2, config.Database.MaxIdleConns)
assert.Equal(t, "30m", config.Database.ConnMaxLifetime)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})

t.Run("custom database config", func(t *testing.T) {
Expand Down Expand Up @@ -1330,6 +1347,7 @@ func TestBuildConfig_DatabaseConfig(t *testing.T) {
assert.Equal(t, 25, config.Database.MaxOpenConns)
assert.Equal(t, 5, config.Database.MaxIdleConns)
assert.Equal(t, "1h", config.Database.ConnMaxLifetime)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})

t.Run("partial database config uses defaults for missing fields", func(t *testing.T) {
Expand Down Expand Up @@ -1378,5 +1396,6 @@ func TestBuildConfig_DatabaseConfig(t *testing.T) {
assert.Equal(t, 10, config.Database.MaxOpenConns)
assert.Equal(t, 2, config.Database.MaxIdleConns)
assert.Equal(t, "30m", config.Database.ConnMaxLifetime)
assert.Equal(t, AuthModeAnonymous, config.Auth.Mode)
})
}
Loading