From ab82db4d5497f3eb5729fb8ac835352eb9c576e0 Mon Sep 17 00:00:00 2001 From: onidoru Date: Wed, 31 Jan 2024 18:30:54 +0200 Subject: [PATCH 1/5] fix: Add credentials config verification (cherry picked from commit e7fdfa0bcc8d5bf80163cae53a821682cf5322fe) Signed-off-by: Andrei Aaron --- .github/workflows/verify-config.yaml | 46 ++++++++++++++++++++++++++++ Makefile | 2 +- examples/README.md | 4 +-- examples/config-example.json | 3 +- examples/config-example.yaml | 3 +- pkg/cli/server/root.go | 4 +-- 6 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/verify-config.yaml diff --git a/.github/workflows/verify-config.yaml b/.github/workflows/verify-config.yaml new file mode 100644 index 000000000..9f4f02616 --- /dev/null +++ b/.github/workflows/verify-config.yaml @@ -0,0 +1,46 @@ +name: "Verify Example Config Files" + +# Validate all example config files are relevant and valid. + +on: + push: + branches: + - main + pull_request: + branches: [main] + release: + types: + - published + +permissions: read-all + +jobs: + verify-config: + name: Verify Config Files + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install go + uses: actions/setup-go@v5 + with: + cache: false + go-version: 1.20.x + - name: Cache go dependencies + id: cache-go-dependencies + uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod + key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-mod- + - name: Install go dependencies + if: steps.cache-go-dependencies.outputs.cache-hit != 'true' + run: | + cd $GITHUB_WORKSPACE + go mod download + - uses: ./.github/actions/setup-localstack + - name: run verify-config + run: | + cd $GITHUB_WORKSPACE + make verify-config diff --git a/Makefile b/Makefile index 4415b2eb0..af1c99db8 100644 --- a/Makefile +++ b/Makefile @@ -392,7 +392,7 @@ verify-config: _verify-config verify-config-warnings verify-config-commited .PHONY: _verify-config _verify-config: binary rm -f output.txt - $(foreach file, $(wildcard examples/config-*), ./bin/zot-$(OS)-$(ARCH) verify $(file) 2>&1 | tee -a output.txt || exit 1;) + $(foreach file, $(filter-out examples/config-ldap-credentials.json, $(wildcard examples/config-*)), ./bin/zot-$(OS)-$(ARCH) verify $(file) 2>&1 | tee -a output.txt || exit 1;) .PHONY: verify-config-warnings verify-config-warnings: _verify-config diff --git a/examples/README.md b/examples/README.md index 16efa6ce1..03db18f12 100644 --- a/examples/README.md +++ b/examples/README.md @@ -225,14 +225,14 @@ authentication: "startTLS":false, "baseDN":"ou=Users,dc=example,dc=org", "userAttribute":"uid", - "bindDN":"cn=ldap-searcher,ou=Users,dc=example,dc=org", - "bindPassword":"ldap-searcher-password", + "credentialsFile": "config-ldap-credentials.json", "skipVerify":false, "subtreeSearch":true }, ``` NOTE: When both htpasswd and LDAP configuration are specified, LDAP authentication is given preference. +NOTE: The separate file for storing DN and password credentials must be created. You can see example in `examples/config-ldap-credentials.json` file. **OAuth2 authentication** (client credentials grant type) support via _Bearer Token_ configured with: diff --git a/examples/config-example.json b/examples/config-example.json index e61b97d44..729c8a383 100644 --- a/examples/config-example.json +++ b/examples/config-example.json @@ -18,8 +18,7 @@ "startTLS": false, "baseDN": "ou=Users,dc=example,dc=org", "userAttribute": "uid", - "bindDN": "cn=ldap-searcher,ou=Users,dc=example,dc=org", - "bindPassword": "ldap-searcher-password", + "credentialsFile": "examples/config-ldap-credentials.json", "skipVerify": false, "subtreeSearch": true }, diff --git a/examples/config-example.yaml b/examples/config-example.yaml index cdbe537d8..be0180f14 100644 --- a/examples/config-example.yaml +++ b/examples/config-example.yaml @@ -8,8 +8,7 @@ http: ldap: address: ldap.example.org basedn: ou=Users,dc=example,dc=org - binddn: cn=ldap-searcher,ou=Users,dc=example,dc=org - bindpassword: ldap-searcher-password + credentialsFile: examples/config-ldap-credentials.json port: 389 skipverify: false starttls: false diff --git a/pkg/cli/server/root.go b/pkg/cli/server/root.go index 71fe9f773..b89228635 100644 --- a/pkg/cli/server/root.go +++ b/pkg/cli/server/root.go @@ -861,8 +861,8 @@ func readLDAPCredentials(ldapConfigPath string) (config.LDAPCredentials, error) var ldapCredentials config.LDAPCredentials - if err := viperInstance.Unmarshal(&ldapCredentials); err != nil { - log.Error().Err(err).Msg("failed to unmarshal new config") + if err := viperInstance.UnmarshalExact(&ldapCredentials); err != nil { + log.Error().Err(err).Msg("failed to unmarshal ldap credentials config") return config.LDAPCredentials{}, err } From ff37f158ccb52cdd30ce2cafa1bbd73c92277e73 Mon Sep 17 00:00:00 2001 From: Nikita Kotikov <25552941+onidoru@users.noreply.github.com> Date: Thu, 15 Feb 2024 23:05:10 +0200 Subject: [PATCH 2/5] fix: Update golang version to 1.21.x Signed-off-by: onidoru <25552941+onidoru@users.noreply.github.com> Signed-off-by: Nikita Kotikov <25552941+onidoru@users.noreply.github.com> (cherry picked from commit cbc0f89dfb4067a9deafe6b602a7c1a6d61b3413) Signed-off-by: Andrei Aaron --- .github/workflows/verify-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-config.yaml b/.github/workflows/verify-config.yaml index 9f4f02616..0fc9c36be 100644 --- a/.github/workflows/verify-config.yaml +++ b/.github/workflows/verify-config.yaml @@ -24,7 +24,7 @@ jobs: uses: actions/setup-go@v5 with: cache: false - go-version: 1.20.x + go-version: 1.21.x - name: Cache go dependencies id: cache-go-dependencies uses: actions/cache@v4 From f21631314b92cd4a7234d03104e258c736c83cb3 Mon Sep 17 00:00:00 2001 From: Nikita Kotikov <25552941+onidoru@users.noreply.github.com> Date: Thu, 15 Feb 2024 22:55:49 +0200 Subject: [PATCH 3/5] fix: LDAP credentials files are now required, add more tests Signed-off-by: onidoru <25552941+onidoru@users.noreply.github.com> Signed-off-by: Nikita Kotikov <25552941+onidoru@users.noreply.github.com> (cherry picked from commit b74366d50b126581d086b47dc620589375874805) Signed-off-by: Andrei Aaron --- .github/workflows/verify-config.yaml | 1 - pkg/cli/server/root.go | 24 ++++++++- pkg/cli/server/root_test.go | 77 ++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify-config.yaml b/.github/workflows/verify-config.yaml index 0fc9c36be..5486f9520 100644 --- a/.github/workflows/verify-config.yaml +++ b/.github/workflows/verify-config.yaml @@ -39,7 +39,6 @@ jobs: run: | cd $GITHUB_WORKSPACE go mod download - - uses: ./.github/actions/setup-localstack - name: run verify-config run: | cd $GITHUB_WORKSPACE diff --git a/pkg/cli/server/root.go b/pkg/cli/server/root.go index b89228635..022af65d6 100644 --- a/pkg/cli/server/root.go +++ b/pkg/cli/server/root.go @@ -861,12 +861,34 @@ func readLDAPCredentials(ldapConfigPath string) (config.LDAPCredentials, error) var ldapCredentials config.LDAPCredentials - if err := viperInstance.UnmarshalExact(&ldapCredentials); err != nil { + metaData := &mapstructure.Metadata{} + if err := viperInstance.UnmarshalExact(&ldapCredentials, metadataConfig(metaData)); err != nil { log.Error().Err(err).Msg("failed to unmarshal ldap credentials config") return config.LDAPCredentials{}, err } + if len(metaData.Keys) == 0 { + log.Error().Err(zerr.ErrBadConfig). + Msg("failed to load ldap credentials config due to the absence of any key:value pair") + + return config.LDAPCredentials{}, zerr.ErrBadConfig + } + + if len(metaData.Unused) > 0 { + log.Error().Err(zerr.ErrBadConfig).Strs("keys", metaData.Unused). + Msg("failed to load ldap credentials config due to unknown keys") + + return config.LDAPCredentials{}, zerr.ErrBadConfig + } + + if len(metaData.Unset) > 0 { + log.Error().Err(zerr.ErrBadConfig).Strs("keys", metaData.Unset). + Msg("failed to load ldap credentials config due to unset keys") + + return config.LDAPCredentials{}, zerr.ErrBadConfig + } + return ldapCredentials, nil } diff --git a/pkg/cli/server/root_test.go b/pkg/cli/server/root_test.go index ad7e62304..3b7ea3c68 100644 --- a/pkg/cli/server/root_test.go +++ b/pkg/cli/server/root_test.go @@ -1236,6 +1236,83 @@ storage: err = cli.NewServerRootCmd().Execute() So(err, ShouldBeNil) }) + + Convey("Test verify good ldap config", t, func(c C) { + tmpFile, err := os.CreateTemp("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpFile.Name()) + + tmpCredsFile, err := os.CreateTemp("", "zot-cred*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpCredsFile.Name()) + + content := []byte(`{ + "bindDN":"cn=ldap-searcher,ou=Users,dc=example,dc=org", + "bindPassword":"ldap-searcher-password" + }`) + + _, err = tmpCredsFile.Write(content) + So(err, ShouldBeNil) + err = tmpCredsFile.Close() + So(err, ShouldBeNil) + + content = []byte(fmt.Sprintf(`{ "distSpecVersion": "1.1.0-dev", + "storage": { "rootDirectory": "/tmp/zot" }, "http": { "address": "127.0.0.1", "port": "8080", + "auth": { "ldap": { "credentialsFile": "%v", "address": "ldap.example.org", "port": 389, + "startTLS": false, "baseDN": "ou=Users,dc=example,dc=org", + "userAttribute": "uid", "userGroupAttribute": "memberOf", "skipVerify": true, "subtreeSearch": true }, + "failDelay": 5 } }, "log": { "level": "debug" } }`, + tmpCredsFile.Name()), + ) + + _, err = tmpFile.Write(content) + So(err, ShouldBeNil) + err = tmpFile.Close() + So(err, ShouldBeNil) + + os.Args = []string{"cli_test", "verify", tmpFile.Name()} + err = cli.NewServerRootCmd().Execute() + So(err, ShouldBeNil) + }) + + Convey("Test verify bad ldap config", t, func(c C) { + tmpFile, err := os.CreateTemp("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpFile.Name()) + + tmpCredsFile, err := os.CreateTemp("", "zot-cred*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpCredsFile.Name()) + + // `bindDN` key is missing + content := []byte(`{ + "bindPassword":"ldap-searcher-password" + }`) + + _, err = tmpCredsFile.Write(content) + So(err, ShouldBeNil) + err = tmpCredsFile.Close() + So(err, ShouldBeNil) + + content = []byte(fmt.Sprintf(`{ "distSpecVersion": "1.1.0-dev", + "storage": { "rootDirectory": "/tmp/zot" }, "http": { "address": "127.0.0.1", "port": "8080", + "auth": { "ldap": { "credentialsFile": "%v", "address": "ldap.example.org", "port": 389, + "startTLS": false, "baseDN": "ou=Users,dc=example,dc=org", + "userAttribute": "uid", "userGroupAttribute": "memberOf", "skipVerify": true, "subtreeSearch": true }, + "failDelay": 5 } }, "log": { "level": "debug" } }`, + tmpCredsFile.Name()), + ) + + _, err = tmpFile.Write(content) + So(err, ShouldBeNil) + err = tmpFile.Close() + So(err, ShouldBeNil) + + os.Args = []string{"cli_test", "verify", tmpFile.Name()} + err = cli.NewServerRootCmd().Execute() + So(err, ShouldNotBeNil) + So(err.Error(), ShouldContainSubstring, "invalid server config") + }) } func TestApiKeyConfig(t *testing.T) { From 7a15a8d190e561b4bc1b004be5721627fa22d00b Mon Sep 17 00:00:00 2001 From: Nikita Kotikov <25552941+onidoru@users.noreply.github.com> Date: Fri, 16 Feb 2024 22:21:19 +0200 Subject: [PATCH 4/5] fix: Update error handling, add more tests Signed-off-by: onidoru <25552941+onidoru@users.noreply.github.com> Signed-off-by: Nikita Kotikov <25552941+onidoru@users.noreply.github.com> (cherry picked from commit 8a61bbc2d482e1bd5c0c6240dde59c9850ac417b) Signed-off-by: Andrei Aaron --- pkg/cli/server/root.go | 5 ++- pkg/cli/server/root_test.go | 80 ++++++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/pkg/cli/server/root.go b/pkg/cli/server/root.go index 022af65d6..e61a8962c 100644 --- a/pkg/cli/server/root.go +++ b/pkg/cli/server/root.go @@ -2,6 +2,7 @@ package server import ( "context" + "errors" "fmt" "net" "net/http" @@ -856,7 +857,7 @@ func readLDAPCredentials(ldapConfigPath string) (config.LDAPCredentials, error) if err := viperInstance.ReadInConfig(); err != nil { log.Error().Err(err).Msg("failed to read configuration") - return config.LDAPCredentials{}, err + return config.LDAPCredentials{}, errors.Join(zerr.ErrBadConfig, err) } var ldapCredentials config.LDAPCredentials @@ -865,7 +866,7 @@ func readLDAPCredentials(ldapConfigPath string) (config.LDAPCredentials, error) if err := viperInstance.UnmarshalExact(&ldapCredentials, metadataConfig(metaData)); err != nil { log.Error().Err(err).Msg("failed to unmarshal ldap credentials config") - return config.LDAPCredentials{}, err + return config.LDAPCredentials{}, errors.Join(zerr.ErrBadConfig, err) } if len(metaData.Keys) == 0 { diff --git a/pkg/cli/server/root_test.go b/pkg/cli/server/root_test.go index 3b7ea3c68..49e62c3f1 100644 --- a/pkg/cli/server/root_test.go +++ b/pkg/cli/server/root_test.go @@ -1275,7 +1275,7 @@ storage: So(err, ShouldBeNil) }) - Convey("Test verify bad ldap config", t, func(c C) { + Convey("Test verify bad ldap config: key is missing", t, func(c C) { tmpFile, err := os.CreateTemp("", "zot-test*.json") So(err, ShouldBeNil) defer os.Remove(tmpFile.Name()) @@ -1313,6 +1313,84 @@ storage: So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "invalid server config") }) + + Convey("Test verify bad ldap config: unused key", t, func(c C) { + tmpFile, err := os.CreateTemp("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpFile.Name()) + + tmpCredsFile, err := os.CreateTemp("", "zot-cred*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpCredsFile.Name()) + + // `bindDN` key is missing + content := []byte(`{ + "bindDN":"cn=ldap-searcher,ou=Users,dc=example,dc=org", + "bindPassword":"ldap-searcher-password", + "extraKey": "extraValue" + }`) + + _, err = tmpCredsFile.Write(content) + So(err, ShouldBeNil) + err = tmpCredsFile.Close() + So(err, ShouldBeNil) + + content = []byte(fmt.Sprintf(`{ "distSpecVersion": "1.1.0-dev", + "storage": { "rootDirectory": "/tmp/zot" }, "http": { "address": "127.0.0.1", "port": "8080", + "auth": { "ldap": { "credentialsFile": "%v", "address": "ldap.example.org", "port": 389, + "startTLS": false, "baseDN": "ou=Users,dc=example,dc=org", + "userAttribute": "uid", "userGroupAttribute": "memberOf", "skipVerify": true, "subtreeSearch": true }, + "failDelay": 5 } }, "log": { "level": "debug" } }`, + tmpCredsFile.Name()), + ) + + _, err = tmpFile.Write(content) + So(err, ShouldBeNil) + err = tmpFile.Close() + So(err, ShouldBeNil) + + os.Args = []string{"cli_test", "verify", tmpFile.Name()} + err = cli.NewServerRootCmd().Execute() + So(err, ShouldNotBeNil) + So(err.Error(), ShouldContainSubstring, "invalid server config") + }) + + Convey("Test verify bad ldap config: no keys set", t, func(c C) { + tmpFile, err := os.CreateTemp("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpFile.Name()) + + tmpCredsFile, err := os.CreateTemp("", "zot-cred*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpCredsFile.Name()) + + // `bindDN` key is missing + content := []byte(``) + + _, err = tmpCredsFile.Write(content) + So(err, ShouldBeNil) + err = tmpCredsFile.Close() + So(err, ShouldBeNil) + + content = []byte(fmt.Sprintf(`{ "distSpecVersion": "1.1.0-dev", + "storage": { "rootDirectory": "/tmp/zot" }, "http": { "address": "127.0.0.1", "port": "8080", + "auth": { "ldap": { "credentialsFile": "%v", "address": "ldap.example.org", "port": 389, + "startTLS": false, "baseDN": "ou=Users,dc=example,dc=org", + "userAttribute": "uid", "userGroupAttribute": "memberOf", "skipVerify": true, "subtreeSearch": true }, + "failDelay": 5 } }, "log": { "level": "debug" } }`, + tmpCredsFile.Name()), + ) + + _, err = tmpFile.Write(content) + So(err, ShouldBeNil) + err = tmpFile.Close() + So(err, ShouldBeNil) + + os.Args = []string{"cli_test", "verify", tmpFile.Name()} + err = cli.NewServerRootCmd().Execute() + So(err, ShouldNotBeNil) + So(err.Error(), ShouldContainSubstring, "invalid server config") + }) } func TestApiKeyConfig(t *testing.T) { From 95cb5b473c76a29375678d134fd2ee29b2aa3544 Mon Sep 17 00:00:00 2001 From: Andrei Aaron Date: Fri, 15 Mar 2024 20:12:21 +0000 Subject: [PATCH 5/5] fix: Add coverage Signed-off-by: Andrei Aaron --- pkg/cli/server/root.go | 2 +- pkg/cli/server/root_test.go | 54 ++++++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/pkg/cli/server/root.go b/pkg/cli/server/root.go index e61a8962c..2ce6fa69c 100644 --- a/pkg/cli/server/root.go +++ b/pkg/cli/server/root.go @@ -863,7 +863,7 @@ func readLDAPCredentials(ldapConfigPath string) (config.LDAPCredentials, error) var ldapCredentials config.LDAPCredentials metaData := &mapstructure.Metadata{} - if err := viperInstance.UnmarshalExact(&ldapCredentials, metadataConfig(metaData)); err != nil { + if err := viperInstance.Unmarshal(&ldapCredentials, metadataConfig(metaData)); err != nil { log.Error().Err(err).Msg("failed to unmarshal ldap credentials config") return config.LDAPCredentials{}, errors.Join(zerr.ErrBadConfig, err) diff --git a/pkg/cli/server/root_test.go b/pkg/cli/server/root_test.go index 49e62c3f1..38c08c3c4 100644 --- a/pkg/cli/server/root_test.go +++ b/pkg/cli/server/root_test.go @@ -1159,7 +1159,7 @@ storage: content := []byte(`{"distSpecVersion":"1.1.0","storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", "auth":{"openid":{"providers":{"oidc":{"issuer":"http://127.0.0.1:5556/dex", - "clientid":"client_id","scopes":["openid"]}}}}}, + "clientid":"client_id","scopes":["openid"]}}}}}, "log":{"level":"debug"}}`) _, err = tmpfile.Write(content) So(err, ShouldBeNil) @@ -1247,8 +1247,8 @@ storage: defer os.Remove(tmpCredsFile.Name()) content := []byte(`{ - "bindDN":"cn=ldap-searcher,ou=Users,dc=example,dc=org", - "bindPassword":"ldap-searcher-password" + "bindDN":"cn=ldap-searcher,ou=Users,dc=example,dc=org", + "bindPassword":"ldap-searcher-password" }`) _, err = tmpCredsFile.Write(content) @@ -1286,7 +1286,7 @@ storage: // `bindDN` key is missing content := []byte(`{ - "bindPassword":"ldap-searcher-password" + "bindPassword":"ldap-searcher-password" }`) _, err = tmpCredsFile.Write(content) @@ -1323,10 +1323,9 @@ storage: So(err, ShouldBeNil) defer os.Remove(tmpCredsFile.Name()) - // `bindDN` key is missing content := []byte(`{ - "bindDN":"cn=ldap-searcher,ou=Users,dc=example,dc=org", - "bindPassword":"ldap-searcher-password", + "bindDN":"cn=ldap-searcher,ou=Users,dc=example,dc=org", + "bindPassword":"ldap-searcher-password", "extraKey": "extraValue" }`) @@ -1355,7 +1354,7 @@ storage: So(err.Error(), ShouldContainSubstring, "invalid server config") }) - Convey("Test verify bad ldap config: no keys set", t, func(c C) { + Convey("Test verify bad ldap config: empty credentials file", t, func(c C) { tmpFile, err := os.CreateTemp("", "zot-test*.json") So(err, ShouldBeNil) defer os.Remove(tmpFile.Name()) @@ -1391,6 +1390,43 @@ storage: So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "invalid server config") }) + + Convey("Test verify bad ldap config: no keys set in credentials file", t, func(c C) { + tmpFile, err := os.CreateTemp("", "zot-test*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpFile.Name()) + + tmpCredsFile, err := os.CreateTemp("", "zot-cred*.json") + So(err, ShouldBeNil) + defer os.Remove(tmpCredsFile.Name()) + + // empty json + content := []byte(`{}`) + + _, err = tmpCredsFile.Write(content) + So(err, ShouldBeNil) + err = tmpCredsFile.Close() + So(err, ShouldBeNil) + + content = []byte(fmt.Sprintf(`{ "distSpecVersion": "1.1.0-dev", + "storage": { "rootDirectory": "/tmp/zot" }, "http": { "address": "127.0.0.1", "port": "8080", + "auth": { "ldap": { "credentialsFile": "%v", "address": "ldap.example.org", "port": 389, + "startTLS": false, "baseDN": "ou=Users,dc=example,dc=org", + "userAttribute": "uid", "userGroupAttribute": "memberOf", "skipVerify": true, "subtreeSearch": true }, + "failDelay": 5 } }, "log": { "level": "debug" } }`, + tmpCredsFile.Name()), + ) + + _, err = tmpFile.Write(content) + So(err, ShouldBeNil) + err = tmpFile.Close() + So(err, ShouldBeNil) + + os.Args = []string{"cli_test", "verify", tmpFile.Name()} + err = cli.NewServerRootCmd().Execute() + So(err, ShouldNotBeNil) + So(err.Error(), ShouldContainSubstring, "invalid server config") + }) } func TestApiKeyConfig(t *testing.T) { @@ -1403,7 +1439,7 @@ func TestApiKeyConfig(t *testing.T) { content := []byte(`{"distSpecVersion":"1.1.0","storage":{"rootDirectory":"/tmp/zot"}, "http":{"address":"127.0.0.1","port":"8080","realm":"zot", "auth":{"openid":{"providers":{"oidc":{"issuer":"http://127.0.0.1:5556/dex", - "clientid":"client_id","scopes":["openid"]}}}}}, + "clientid":"client_id","scopes":["openid"]}}}}}, "log":{"level":"debug"}}`) err = os.WriteFile(tmpfile.Name(), content, 0o0600)