diff --git a/.golangci.yml b/.golangci.yml index 31f4b033..2f4ee6ea 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -53,6 +53,11 @@ linters: - revive path: util/.*\.go$ text: "var-naming: avoid meaningless package names" + # pkg/version conflicts with go/version (added in Go 1.22) + - linters: + - revive + path: pkg/version/.*\.go$ + text: "var-naming: avoid package names that conflict with" - linters: - prealloc path: _test\.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 967455db..c3a95f7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - [#2851](https://github.com/oauth2-proxy/oauth2-proxy/pull/2851) feat: add support for specifying allowed OIDC JWT signing algorithms (#2753) (@andoks / @tuunit) - [#3369](https://github.com/oauth2-proxy/oauth2-proxy/pull/3369) fix: use CSRFExpire instead of Expire for CSRF cookie validation (@Br1an67) - [#3365](https://github.com/oauth2-proxy/oauth2-proxy/pull/3365) fix: filter empty strings from allowed groups (@Br1an67) +- [#3338](https://github.com/oauth2-proxy/oauth2-proxy/pull/3338) feat: add --config-test flag for validating configuration (@MayorFaj) # V7.14.3 diff --git a/docs/docs/configuration/alpha_config.md b/docs/docs/configuration/alpha_config.md index 385a9f85..d8cce916 100644 --- a/docs/docs/configuration/alpha_config.md +++ b/docs/docs/configuration/alpha_config.md @@ -106,6 +106,17 @@ the new config. oauth2-proxy --alpha-config ./path/to/new/config.yaml --config ./path/to/existing/config.cfg ``` +### Validating Alpha Configuration + +Use `--config-test` to validate your alpha configuration without starting the proxy: + +```bash +oauth2-proxy --config core.cfg --alpha-config alpha.yaml --config-test +``` + +This is useful for CI/CD pipelines to catch configuration errors before deployment. +See the [Configuration Validation](./overview.md#configuration-validation) section for more details. + ### How to use environment variables The alpha package supports the use of environment variables in place of yaml values, allowing sensitive data to be pulled from somewhere other than the yaml file. diff --git a/docs/docs/configuration/alpha_config.md.tmpl b/docs/docs/configuration/alpha_config.md.tmpl index 081657c4..2a9684da 100644 --- a/docs/docs/configuration/alpha_config.md.tmpl +++ b/docs/docs/configuration/alpha_config.md.tmpl @@ -106,6 +106,17 @@ the new config. oauth2-proxy --alpha-config ./path/to/new/config.yaml --config ./path/to/existing/config.cfg ``` +### Validating Alpha Configuration + +Use `--config-test` to validate your alpha configuration without starting the proxy: + +```bash +oauth2-proxy --config core.cfg --alpha-config alpha.yaml --config-test +``` + +This is useful for CI/CD pipelines to catch configuration errors before deployment. +See the [Configuration Validation](./overview.md#configuration-validation) section for more details. + ### How to use environment variables The alpha package supports the use of environment variables in place of yaml values, allowing sensitive data to be pulled from somewhere other than the yaml file. diff --git a/docs/docs/configuration/overview.md b/docs/docs/configuration/overview.md index b4786cf7..7d8a1c09 100644 --- a/docs/docs/configuration/overview.md +++ b/docs/docs/configuration/overview.md @@ -66,11 +66,48 @@ An example [oauth2-proxy.cfg](https://github.com/oauth2-proxy/oauth2-proxy/blob/ ### Command Line Options -| Flag | Description | -| ----------- | -------------------- | -| `--config` | path to config file | -| `--version` | print version string | +| Flag | Description | +| ---------------- | ------------------------------------------------------- | +| `--config` | path to config file | +| `--config-test` | test configuration and exit (for CI/CD validation) | +| `--version` | print version string | +## Configuration Validation + +The `--config-test` flag validates your configuration file without starting the proxy server. This is useful for: +- **CI/CD pipelines**: Pre-deployment validation +- **Configuration management**: Testing before applying changes +- **Debugging**: Verifying syntax and required fields + +### Usage + +```bash +# Test legacy config +oauth2-proxy --config /etc/oauth2-proxy.cfg --config-test + +# Test alpha config +oauth2-proxy --config /etc/core.cfg --alpha-config /etc/alpha.yaml --config-test + +# CI/CD pre-deployment check +# Returns with exit code 1 if any validation errors occur +oauth2-proxy --config new-config.cfg --config-test +``` + +### Exit Codes + +- **0**: Configuration is valid ✅ +- **1**: Configuration is invalid (errors printed to stderr) ❌ + +### Validation Coverage + +The `--config-test` flag performs the **same comprehensive validation** as normal startup, including: +- Required fields (client ID, client secret, cookie secret, etc.) +- Syntax validation (TOML/YAML parsing) +- Provider configuration +- Upstream server definitions +- Session store connectivity (e.g., Redis network checks if configured) + +**Note**: Cannot be combined with `--convert-config-to-alpha`. ### General Provider Options diff --git a/main.go b/main.go index 42e8bab0..ba970679 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ func main() { alphaConfig := configFlagSet.String("alpha-config", "", "path to alpha config file (use at your own risk - the structure in this config file may change between minor releases)") convertConfig := configFlagSet.Bool("convert-config-to-alpha", false, "if true, the proxy will load configuration as normal and convert existing configuration to the alpha config structure, and print it to stdout") showVersion := configFlagSet.Bool("version", false, "print version string") + configTest := configFlagSet.Bool("config-test", false, "test the configuration and exit") configFlagSet.Parse(os.Args[1:]) if *showVersion { @@ -37,11 +38,24 @@ func main() { logger.Fatal("cannot use alpha-config and convert-config-to-alpha together") } + if *configTest && *convertConfig { + logger.Fatal("cannot use config-test and convert-config-to-alpha together") + } + opts, err := loadConfiguration(*config, *alphaConfig, configFlagSet, os.Args[1:]) if err != nil { logger.Fatalf("ERROR: %v", err) } + if *configTest { + if err = validation.Validate(opts); err != nil { + logger.Errorf("%s", err) + os.Exit(1) + } + fmt.Println("configuration is valid") + return + } + if *convertConfig { if err := printConvertedConfig(opts); err != nil { logger.Fatalf("ERROR: could not convert config: %v", err) diff --git a/main_test.go b/main_test.go index a90f1a38..c7c7057d 100644 --- a/main_test.go +++ b/main_test.go @@ -7,6 +7,7 @@ import ( "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options" . "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options/testutil" "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/util/ptr" + "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/validation" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/format" @@ -292,4 +293,78 @@ redirect_url="http://localhost:4180/oauth2/callback" expectedErr: errors.New("failed to load legacy options: failed to load config: error unmarshalling config: decoding failed due to the following error(s):\n\n'' has invalid keys: unknown_field"), }), ) + + Describe("Config Test Mode", func() { + const validConfig = ` +http_address="127.0.0.1:4180" +upstreams="http://httpbin" +client_id="oauth2-proxy" +client_secret="b2F1dGgyLXByb3h5LWNsaWVudC1zZWNyZXQK" +cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" +email_domains="example.com" +cookie_secure="false" +redirect_url="http://localhost:4180/oauth2/callback" +` + + const invalidConfig = ` +http_address="127.0.0.1:4180" +upstreams="http://httpbin" +email_domains="example.com" +cookie_secure="false" +redirect_url="http://localhost:4180/oauth2/callback" +` + + writeTempConfig := func(content string) string { + file, err := os.CreateTemp("", "oauth2-proxy-test-config-XXXX.cfg") + Expect(err).ToNot(HaveOccurred()) + defer file.Close() + + _, err = file.WriteString(content) + Expect(err).ToNot(HaveOccurred()) + return file.Name() + } + + It("should pass validation with a valid configuration", func() { + configFile := writeTempConfig(validConfig) + defer os.Remove(configFile) + + flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) + opts, err := loadConfiguration(configFile, "", flagSet, []string{}) + Expect(err).ToNot(HaveOccurred()) + + err = validation.Validate(opts) + Expect(err).ToNot(HaveOccurred()) + }) + + It("should fail validation with an invalid configuration (missing required fields)", func() { + configFile := writeTempConfig(invalidConfig) + defer os.Remove(configFile) + + flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) + opts, err := loadConfiguration(configFile, "", flagSet, []string{}) + Expect(err).ToNot(HaveOccurred()) + + err = validation.Validate(opts) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("invalid configuration")) + }) + + It("should fail to load a configuration file with syntax errors", func() { + configFile := writeTempConfig("this is not valid toml ===") + defer os.Remove(configFile) + + flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) + _, err := loadConfiguration(configFile, "", flagSet, []string{}) + Expect(err).To(HaveOccurred()) + }) + + It("should register the config-test flag", func() { + flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) + flagSet.ParseErrorsAllowlist.UnknownFlags = true + configTest := flagSet.Bool("config-test", false, "test the configuration and exit") + err := flagSet.Parse([]string{"--config-test"}) + Expect(err).ToNot(HaveOccurred()) + Expect(*configTest).To(BeTrue()) + }) + }) })