Add configurable CORS
Change-Id: I6b902e028c1f192987b4d7d6415aad456137c520
diff --git a/config/config.go b/config/config.go
index 7fa600e..6c4c9bf 100644
--- a/config/config.go
+++ b/config/config.go
@@ -20,8 +20,8 @@
defaultServiceURL = "https://korap.ids-mannheim.de/plugin/koralmapper"
defaultCookieName = "km-config"
defaultPort = 5725
- defaultLogLevel = "warn"
- defaultRateLimit = 100
+ defaultLogLevel = "warn"
+ defaultRateLimit = 100
)
// MappingRule represents a single mapping rule in the configuration
@@ -92,16 +92,17 @@
// MappingConfig represents the root configuration containing multiple mapping lists
type MappingConfig struct {
- SDK string `yaml:"sdk,omitempty"`
- Stylesheet string `yaml:"stylesheet,omitempty"`
- Server string `yaml:"server,omitempty"`
- ServiceURL string `yaml:"serviceURL,omitempty"`
- CookieName string `yaml:"cookieName,omitempty"`
- BasePath string `yaml:"basePath,omitempty"` // restricts config file loading to this directory tree
- Port int `yaml:"port,omitempty"`
- LogLevel string `yaml:"loglevel,omitempty"`
- RateLimit int `yaml:"rateLimit,omitempty"` // max requests per minute per IP (0 = use default 100)
- Lists []MappingList `yaml:"lists,omitempty"`
+ SDK string `yaml:"sdk,omitempty"`
+ Stylesheet string `yaml:"stylesheet,omitempty"`
+ Server string `yaml:"server,omitempty"`
+ ServiceURL string `yaml:"serviceURL,omitempty"`
+ CookieName string `yaml:"cookieName,omitempty"`
+ BasePath string `yaml:"basePath,omitempty"` // restricts config file loading to this directory tree
+ AllowOrigins string `yaml:"allowOrigins,omitempty"` // comma-separated list of allowed CORS origins
+ Port int `yaml:"port,omitempty"`
+ LogLevel string `yaml:"loglevel,omitempty"`
+ RateLimit int `yaml:"rateLimit,omitempty"` // max requests per minute per IP (0 = use default 100)
+ Lists []MappingList `yaml:"lists,omitempty"`
}
// AllowedBasePath restricts file loading to a specific directory tree.
@@ -255,15 +256,16 @@
// Create final configuration
result := &MappingConfig{
- SDK: globalConfig.SDK,
- Stylesheet: globalConfig.Stylesheet,
- Server: globalConfig.Server,
- ServiceURL: globalConfig.ServiceURL,
- BasePath: globalConfig.BasePath,
- Port: globalConfig.Port,
- LogLevel: globalConfig.LogLevel,
- RateLimit: globalConfig.RateLimit,
- Lists: allLists,
+ SDK: globalConfig.SDK,
+ Stylesheet: globalConfig.Stylesheet,
+ Server: globalConfig.Server,
+ ServiceURL: globalConfig.ServiceURL,
+ BasePath: globalConfig.BasePath,
+ AllowOrigins: globalConfig.AllowOrigins,
+ Port: globalConfig.Port,
+ LogLevel: globalConfig.LogLevel,
+ RateLimit: globalConfig.RateLimit,
+ Lists: allLists,
}
// Apply environment variable overrides (ENV > config file)
@@ -292,6 +294,13 @@
}
}
+ // AllowOrigins defaults to the Server value (with trailing slash
+ // stripped to form a proper origin). This avoids duplicating the
+ // server URL string and keeps CORS in sync with the deployment.
+ if config.AllowOrigins == "" {
+ config.AllowOrigins = strings.TrimRight(config.Server, "/")
+ }
+
if config.Port == 0 {
config.Port = defaultPort
}
@@ -305,13 +314,14 @@
// Non-empty environment values override any previously loaded config values.
func ApplyEnvOverrides(config *MappingConfig) {
envMappings := map[string]*string{
- "KORAL_MAPPER_SERVER": &config.Server,
- "KORAL_MAPPER_SDK": &config.SDK,
- "KORAL_MAPPER_STYLESHEET": &config.Stylesheet,
- "KORAL_MAPPER_SERVICE_URL": &config.ServiceURL,
- "KORAL_MAPPER_COOKIE_NAME": &config.CookieName,
- "KORAL_MAPPER_LOG_LEVEL": &config.LogLevel,
- "KORAL_MAPPER_BASE_PATH": &config.BasePath,
+ "KORAL_MAPPER_SERVER": &config.Server,
+ "KORAL_MAPPER_SDK": &config.SDK,
+ "KORAL_MAPPER_STYLESHEET": &config.Stylesheet,
+ "KORAL_MAPPER_SERVICE_URL": &config.ServiceURL,
+ "KORAL_MAPPER_COOKIE_NAME": &config.CookieName,
+ "KORAL_MAPPER_LOG_LEVEL": &config.LogLevel,
+ "KORAL_MAPPER_BASE_PATH": &config.BasePath,
+ "KORAL_MAPPER_ALLOW_ORIGINS": &config.AllowOrigins,
}
for envKey, field := range envMappings {
diff --git a/config/config_test.go b/config/config_test.go
index 67aa821..bac6df0 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -961,6 +961,7 @@
"KORAL_MAPPER_COOKIE_NAME",
"KORAL_MAPPER_PORT",
"KORAL_MAPPER_LOG_LEVEL",
+ "KORAL_MAPPER_ALLOW_ORIGINS",
}
clearEnv := func() {
@@ -1101,6 +1102,7 @@
"KORAL_MAPPER_STYLESHEET",
"KORAL_MAPPER_SERVICE_URL",
"KORAL_MAPPER_COOKIE_NAME",
+ "KORAL_MAPPER_ALLOW_ORIGINS",
}
clearEnv := func() {
for _, key := range envKeys {
@@ -1261,6 +1263,79 @@
"KORAL_MAPPER_RATE_LIMIT env var should override YAML value")
}
+func TestAllowOriginsDefault(t *testing.T) {
+ cfg := &MappingConfig{}
+ ApplyDefaults(cfg)
+ // AllowOrigins should derive from the Server default (trailing slash stripped)
+ assert.Equal(t, "https://korap.ids-mannheim.de", cfg.AllowOrigins,
+ "default AllowOrigins should derive from defaultServer")
+}
+
+func TestAllowOriginsDerivedFromCustomServer(t *testing.T) {
+ cfg := &MappingConfig{
+ Server: "https://custom.example.com/",
+ }
+ ApplyDefaults(cfg)
+ assert.Equal(t, "https://custom.example.com", cfg.AllowOrigins,
+ "AllowOrigins should derive from the configured Server (trailing slash stripped)")
+}
+
+func TestAllowOriginsExplicitNotOverriddenByServer(t *testing.T) {
+ cfg := &MappingConfig{
+ Server: "https://custom.example.com/",
+ AllowOrigins: "https://explicit-origin.example.com",
+ }
+ ApplyDefaults(cfg)
+ assert.Equal(t, "https://explicit-origin.example.com", cfg.AllowOrigins,
+ "explicit AllowOrigins should not be overridden by Server default")
+}
+
+func TestAllowOriginsFromYAML(t *testing.T) {
+ content := `
+allowOrigins: "https://custom.example.com,https://other.example.com"
+lists:
+ - id: test-mapper
+ mappings:
+ - "[A] <> [B]"
+`
+ tmpfile, err := os.CreateTemp("", "config-cors-*.yaml")
+ require.NoError(t, err)
+ defer os.Remove(tmpfile.Name())
+
+ _, err = tmpfile.WriteString(content)
+ require.NoError(t, err)
+ require.NoError(t, tmpfile.Close())
+
+ cfg, err := LoadFromSources(tmpfile.Name(), nil)
+ require.NoError(t, err)
+ assert.Equal(t, "https://custom.example.com,https://other.example.com",
+ cfg.AllowOrigins)
+}
+
+func TestAllowOriginsEnvOverride(t *testing.T) {
+ t.Setenv("KORAL_MAPPER_ALLOW_ORIGINS", "https://env-origin.example.com")
+
+ content := `
+allowOrigins: "https://yaml-origin.example.com"
+lists:
+ - id: test-mapper
+ mappings:
+ - "[A] <> [B]"
+`
+ tmpfile, err := os.CreateTemp("", "config-cors-env-*.yaml")
+ require.NoError(t, err)
+ defer os.Remove(tmpfile.Name())
+
+ _, err = tmpfile.WriteString(content)
+ require.NoError(t, err)
+ require.NoError(t, tmpfile.Close())
+
+ cfg, err := LoadFromSources(tmpfile.Name(), nil)
+ require.NoError(t, err)
+ assert.Equal(t, "https://env-origin.example.com", cfg.AllowOrigins,
+ "KORAL_MAPPER_ALLOW_ORIGINS env var should override YAML value")
+}
+
func TestSanitizeFilePathRejectsOutsideBase(t *testing.T) {
// Set base to a specific directory and verify paths outside are rejected
tmpDir, err := os.MkdirTemp("", "koral-base-*")