From b4616196828cf7a0e52a3b0f8245f7fc7bcf60d5 Mon Sep 17 00:00:00 2001 From: peusebiu Date: Tue, 13 Aug 2024 01:11:53 +0300 Subject: [PATCH] fix(authn): make hashing/encryption keys used to secure cookies (#2536) fix(authn): configurable hashing/encryption keys used to secure cookies If they are not configured zot will generate a random hashing key at startup, invalidating all cookies if zot is restarted. closes: #2526 Signed-off-by: Petu Eusebiu --- examples/README.md | 30 ++++++++++++++++ examples/config-openid.json | 1 + examples/sessionKeys.json | 4 +++ pkg/api/authn.go | 33 +++++++++-------- pkg/api/authn_test.go | 4 +-- pkg/api/config/config.go | 20 +++++++---- pkg/api/controller.go | 10 +++++- pkg/api/controller_test.go | 30 +++++++++++----- pkg/api/cookiestore.go | 22 ++---------- pkg/cli/server/root.go | 65 ++++++++++++++++++++++----------- pkg/cli/server/root_test.go | 71 +++++++++++++++++++++++++++++++++++++ 11 files changed, 219 insertions(+), 71 deletions(-) create mode 100644 examples/sessionKeys.json diff --git a/examples/README.md b/examples/README.md index 2b11c87f..f817e078 100644 --- a/examples/README.md +++ b/examples/README.md @@ -370,6 +370,36 @@ Using that cookie on subsequent calls will authenticate them, asumming the cooki In case of using filesystem storage sessions are saved in zot's root directory. In case of using cloud storage sessions are saved in memory. + +### Securing session based login + +In order to secure session cookies used in session based authentication process you need to set the path to a file containg keys used to hash and encrypt the cookies: + +`sessionKeysFile` + +``` + "auth": { + "htpasswd": { + "path": "test/data/htpasswd" + }, + "sessionKeysFile": "/home/user/keys", + "apikey": true, + } +``` + +``` +user@host:~/zot$ cat ../keys | jq +{ + "hashKey": "my-very-secret", + "encryptKey": "another-secret" +} +``` + +- hashKey - used to authenticate the cookie value using HMAC. It is recommended to use a key with exactly 32 or 64 bytes. +- encryptKey - this is optional, used to encrypt the cookie value. If set, the length must correspond to the block size of the encryption algorithm. For AES, used by default, valid lengths are 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. + +If at least hashKey is not set zot will create a random one which on zot restarts it will invalidate all currently valid cookies and their sessions, requiring all users to login again. + #### API keys zot allows authentication for REST API calls using your API key as an alternative to your password. diff --git a/examples/config-openid.json b/examples/config-openid.json index 5fb2bee9..9fc70320 100644 --- a/examples/config-openid.json +++ b/examples/config-openid.json @@ -13,6 +13,7 @@ "htpasswd": { "path": "test/data/htpasswd" }, + "sessionKeysFile": "examples/sessionKeys.json", "apikey": true, "openid": { "providers": { diff --git a/examples/sessionKeys.json b/examples/sessionKeys.json new file mode 100644 index 00000000..d50b2ac7 --- /dev/null +++ b/examples/sessionKeys.json @@ -0,0 +1,4 @@ +{ + "hashKey": "3lrioGLGO2RfG9Y7HQGgWa3ayBjMLw2auMXqEWcSXjQKc9SoQ3fKTIbO+toPYa7e", + "encryptKey": "KOzt01JrDz2uC//UBC5ZikxQw4owfmI8" +} diff --git a/pkg/api/authn.go b/pkg/api/authn.go index dca550f3..d1c390ab 100644 --- a/pkg/api/authn.go +++ b/pkg/api/authn.go @@ -22,7 +22,6 @@ import ( "github.com/google/go-github/v52/github" "github.com/google/uuid" "github.com/gorilla/mux" - "github.com/gorilla/securecookie" "github.com/gorilla/sessions" godigest "github.com/opencontainers/go-digest" "github.com/zitadel/oidc/v3/pkg/client/rp" @@ -334,10 +333,12 @@ func (amw *AuthnMiddleware) tryAuthnHandlers(ctlr *Controller) mux.MiddlewareFun for provider := range ctlr.Config.HTTP.Auth.OpenID.Providers { if config.IsOpenIDSupported(provider) { - rp := NewRelyingPartyOIDC(context.TODO(), ctlr.Config, provider, ctlr.Log) + rp := NewRelyingPartyOIDC(context.TODO(), ctlr.Config, provider, ctlr.Config.HTTP.Auth.SessionHashKey, + ctlr.Config.HTTP.Auth.SessionEncryptKey, ctlr.Log) ctlr.RelyingParties[provider] = rp } else if config.IsOauth2Supported(provider) { - rp := NewRelyingPartyGithub(ctlr.Config, provider, ctlr.Log) + rp := NewRelyingPartyGithub(ctlr.Config, provider, ctlr.Config.HTTP.Auth.SessionHashKey, + ctlr.Config.HTTP.Auth.SessionEncryptKey, ctlr.Log) ctlr.RelyingParties[provider] = rp } } @@ -610,20 +611,25 @@ func (rh *RouteHandler) AuthURLHandler() http.HandlerFunc { } } -func NewRelyingPartyOIDC(ctx context.Context, config *config.Config, provider string, log log.Logger) rp.RelyingParty { - issuer, clientID, clientSecret, redirectURI, scopes, options := getRelyingPartyArgs(config, provider, log) +func NewRelyingPartyOIDC(ctx context.Context, config *config.Config, provider string, + hashKey, encryptKey []byte, log log.Logger, +) rp.RelyingParty { + issuer, clientID, clientSecret, redirectURI, scopes, options := getRelyingPartyArgs(config, + provider, hashKey, encryptKey, log) relyingParty, err := rp.NewRelyingPartyOIDC(ctx, issuer, clientID, clientSecret, redirectURI, scopes, options...) if err != nil { log.Panic().Err(err).Str("issuer", issuer).Str("redirectURI", redirectURI).Strs("scopes", scopes). - Msg("failed to get new relying party oicd") + Msg("failed to initialize new relying party oidc") } return relyingParty } -func NewRelyingPartyGithub(config *config.Config, provider string, log log.Logger) rp.RelyingParty { - _, clientID, clientSecret, redirectURI, scopes, options := getRelyingPartyArgs(config, provider, log) +func NewRelyingPartyGithub(config *config.Config, provider string, hashKey, encryptKey []byte, log log.Logger, +) rp.RelyingParty { + _, clientID, clientSecret, redirectURI, scopes, + options := getRelyingPartyArgs(config, provider, hashKey, encryptKey, log) rpConfig := &oauth2.Config{ ClientID: clientID, @@ -636,13 +642,13 @@ func NewRelyingPartyGithub(config *config.Config, provider string, log log.Logge relyingParty, err := rp.NewRelyingPartyOAuth(rpConfig, options...) if err != nil { log.Panic().Err(err).Str("redirectURI", redirectURI).Strs("scopes", scopes). - Msg("failed to get new relying party oauth") + Msg("failed to initialize new relying party oauth") } return relyingParty } -func getRelyingPartyArgs(cfg *config.Config, provider string, log log.Logger) ( +func getRelyingPartyArgs(cfg *config.Config, provider string, hashKey, encryptKey []byte, log log.Logger) ( string, string, string, string, []string, []rp.Option, ) { if _, ok := cfg.HTTP.Auth.OpenID.Providers[provider]; !ok { @@ -683,9 +689,8 @@ func getRelyingPartyArgs(cfg *config.Config, provider string, log log.Logger) ( rp.WithVerifierOpts(rp.WithIssuedAtOffset(issuedAtOffset)), } - key := securecookie.GenerateRandomKey(32) //nolint:mnd + cookieHandler := httphelper.NewCookieHandler(hashKey, encryptKey, httphelper.WithMaxAge(relyingPartyCookieMaxAge)) - cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithMaxAge(relyingPartyCookieMaxAge)) options = append(options, rp.WithCookieHandler(cookieHandler)) if clientSecret == "" { @@ -839,14 +844,14 @@ func OAuth2Callback(ctlr *Controller, w http.ResponseWriter, r *http.Request, st stateOrigin, ok := stateCookie.Values["state"].(string) if !ok { ctlr.Log.Error().Err(zerr.ErrInvalidStateCookie).Str("component", "openID"). - Msg(": failed to get 'state' cookie from request") + Msg("failed to get 'state' cookie from request") return "", zerr.ErrInvalidStateCookie } if stateOrigin != state { ctlr.Log.Error().Err(zerr.ErrInvalidStateCookie).Str("component", "openID"). - Msg(": 'state' cookie differs from the actual one") + Msg("'state' cookie differs from the actual one") return "", zerr.ErrInvalidStateCookie } diff --git a/pkg/api/authn_test.go b/pkg/api/authn_test.go index cd33940e..a05fd726 100644 --- a/pkg/api/authn_test.go +++ b/pkg/api/authn_test.go @@ -960,7 +960,7 @@ func TestCookiestoreCleanup(t *testing.T) { DefaultStore: imgStore, } - cookieStore, err := api.NewCookieStore(storeController) + cookieStore, err := api.NewCookieStore(storeController, nil, nil) So(err, ShouldBeNil) cookieStore.RunSessionCleaner(taskScheduler) @@ -995,7 +995,7 @@ func TestCookiestoreCleanup(t *testing.T) { DefaultStore: imgStore, } - cookieStore, err := api.NewCookieStore(storeController) + cookieStore, err := api.NewCookieStore(storeController, []byte("secret"), nil) So(err, ShouldBeNil) err = os.Chmod(rootDir, 0o000) diff --git a/pkg/api/config/config.go b/pkg/api/config/config.go index 062787af..b7d5c7f7 100644 --- a/pkg/api/config/config.go +++ b/pkg/api/config/config.go @@ -67,12 +67,15 @@ type AuthHTPasswd struct { } type AuthConfig struct { - FailDelay int - HTPasswd AuthHTPasswd - LDAP *LDAPConfig - Bearer *BearerConfig - OpenID *OpenIDConfig - APIKey bool + FailDelay int + HTPasswd AuthHTPasswd + LDAP *LDAPConfig + Bearer *BearerConfig + OpenID *OpenIDConfig + APIKey bool + SessionKeysFile string + SessionHashKey []byte `json:"-"` + SessionEncryptKey []byte `json:"-"` } type BearerConfig struct { @@ -81,6 +84,11 @@ type BearerConfig struct { Cert string } +type SessionKeys struct { + HashKey string + EncryptKey string `mapstructure:",omitempty"` +} + type OpenIDConfig struct { Providers map[string]OpenIDProviderConfig } diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 5a6d37a0..d9446ba7 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -15,6 +15,7 @@ import ( "time" "github.com/gorilla/mux" + "github.com/gorilla/securecookie" "github.com/zitadel/oidc/v3/pkg/client/rp" "zotregistry.dev/zot/errors" @@ -308,7 +309,14 @@ func (c *Controller) InitImageStore() error { func (c *Controller) initCookieStore() error { // setup sessions cookie store used to preserve logged in user in web sessions if c.Config.IsBasicAuthnEnabled() { - cookieStore, err := NewCookieStore(c.StoreController) + if c.Config.HTTP.Auth.SessionHashKey == nil { + c.Log.Warn().Msg("hashKey is not set in config, generating a random one") + + c.Config.HTTP.Auth.SessionHashKey = securecookie.GenerateRandomKey(64) //nolint: gomnd + } + + cookieStore, err := NewCookieStore(c.StoreController, c.Config.HTTP.Auth.SessionHashKey, + c.Config.HTTP.Auth.SessionEncryptKey) if err != nil { return err } diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 2c74fc3f..ac945742 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -2272,7 +2272,7 @@ func TestAuthnErrors(t *testing.T) { } So(func() { - api.NewRelyingPartyGithub(conf, "prov", log.NewLogger("debug", "")) + api.NewRelyingPartyGithub(conf, "prov", nil, nil, log.NewLogger("debug", "")) }, ShouldPanic) err = os.Chmod(tmpFile, 0o644) @@ -4099,7 +4099,7 @@ func TestNewRelyingPartyOIDC(t *testing.T) { } Convey("provider not found in config", func() { - So(func() { _ = api.NewRelyingPartyOIDC(ctx, conf, "notDex", log.NewLogger("debug", "")) }, ShouldPanic) + So(func() { _ = api.NewRelyingPartyOIDC(ctx, conf, "notDex", nil, nil, log.NewLogger("debug", "")) }, ShouldPanic) }) Convey("key path not found on disk", func() { @@ -4107,7 +4107,7 @@ func TestNewRelyingPartyOIDC(t *testing.T) { oidcProviderCfg.KeyPath = "path/to/file" conf.HTTP.Auth.OpenID.Providers["oidc"] = oidcProviderCfg - So(func() { _ = api.NewRelyingPartyOIDC(ctx, conf, "oidc", log.NewLogger("debug", "")) }, ShouldPanic) + So(func() { _ = api.NewRelyingPartyOIDC(ctx, conf, "oidc", nil, nil, log.NewLogger("debug", "")) }, ShouldPanic) }) Convey("https callback", func() { @@ -4116,7 +4116,7 @@ func TestNewRelyingPartyOIDC(t *testing.T) { Key: ServerKey, } - rp := api.NewRelyingPartyOIDC(ctx, conf, "oidc", log.NewLogger("debug", "")) + rp := api.NewRelyingPartyOIDC(ctx, conf, "oidc", nil, nil, log.NewLogger("debug", "")) So(rp, ShouldNotBeNil) }) @@ -4125,7 +4125,7 @@ func TestNewRelyingPartyOIDC(t *testing.T) { oidcProvider.ClientSecret = "" conf.HTTP.Auth.OpenID.Providers["oidc"] = oidcProvider - rp := api.NewRelyingPartyOIDC(ctx, conf, "oidc", log.NewLogger("debug", "")) + rp := api.NewRelyingPartyOIDC(ctx, conf, "oidc", nil, nil, log.NewLogger("debug", "")) So(rp, ShouldNotBeNil) }) @@ -4134,7 +4134,7 @@ func TestNewRelyingPartyOIDC(t *testing.T) { oidcProvider.Issuer = "" conf.HTTP.Auth.OpenID.Providers["oidc"] = oidcProvider - So(func() { _ = api.NewRelyingPartyOIDC(ctx, conf, "oidc", log.NewLogger("debug", "")) }, ShouldPanic) + So(func() { _ = api.NewRelyingPartyOIDC(ctx, conf, "oidc", nil, nil, log.NewLogger("debug", "")) }, ShouldPanic) }) }) } @@ -4148,9 +4148,10 @@ func TestOpenIDMiddleware(t *testing.T) { conf.HTTP.Port = port testCases := []struct { - testCaseName string - address string - externalURL string + testCaseName string + address string + externalURL string + useSessionKeys bool }{ { address: "0.0.0.0", @@ -4162,6 +4163,12 @@ func TestOpenIDMiddleware(t *testing.T) { externalURL: "", testCaseName: "without ExternalURL provided in config", }, + { + address: "127.0.0.1", + externalURL: "", + testCaseName: "without ExternalURL provided in config and session keys for cookies", + useSessionKeys: true, + }, } // need a username different than ldap one, to test both logic @@ -4246,6 +4253,11 @@ func TestOpenIDMiddleware(t *testing.T) { for _, testcase := range testCases { t.Run(testcase.testCaseName, func(t *testing.T) { + if testcase.useSessionKeys { + ctlr.Config.HTTP.Auth.SessionHashKey = []byte("3lrioGLGO2RfG9Y7HQGgWa3ayBjMLw2auMXqEWcSXjQKc9SoQ3fKTIbO+toPYa7e") + ctlr.Config.HTTP.Auth.SessionEncryptKey = []byte("KOzt01JrDz2uC//UBC5ZikxQw4owfmI8") + } + Convey("make controller", t, func() { dir := t.TempDir() diff --git a/pkg/api/cookiestore.go b/pkg/api/cookiestore.go index 6a58e234..c584a562 100644 --- a/pkg/api/cookiestore.go +++ b/pkg/api/cookiestore.go @@ -11,10 +11,8 @@ import ( "strings" "time" - "github.com/gorilla/securecookie" "github.com/gorilla/sessions" - zerr "zotregistry.dev/zot/errors" "zotregistry.dev/zot/pkg/scheduler" "zotregistry.dev/zot/pkg/storage" storageConstants "zotregistry.dev/zot/pkg/storage/constants" @@ -38,16 +36,11 @@ func (c *CookieStore) RunSessionCleaner(sch *scheduler.Scheduler) { } } -func NewCookieStore(storeController storage.StoreController) (*CookieStore, error) { +func NewCookieStore(storeController storage.StoreController, hashKey, encryptKey []byte) (*CookieStore, error) { // To store custom types in our cookies // we must first register them using gob.Register gob.Register(map[string]interface{}{}) - hashKey, err := getHashKey() - if err != nil { - return &CookieStore{}, err - } - var store sessions.Store var sessionsDir string @@ -60,14 +53,14 @@ func NewCookieStore(storeController storage.StoreController) (*CookieStore, erro return &CookieStore{}, err } - localStore := sessions.NewFilesystemStore(sessionsDir, hashKey) + localStore := sessions.NewFilesystemStore(sessionsDir, hashKey, encryptKey) localStore.MaxAge(cookiesMaxAge) store = localStore needsCleanup = true } else { - memStore := sessions.NewCookieStore(hashKey) + memStore := sessions.NewCookieStore(hashKey, encryptKey) memStore.MaxAge(cookiesMaxAge) @@ -81,15 +74,6 @@ func NewCookieStore(storeController storage.StoreController) (*CookieStore, erro }, nil } -func getHashKey() ([]byte, error) { - hashKey := securecookie.GenerateRandomKey(64) - if hashKey == nil { - return nil, zerr.ErrHashKeyNotCreated - } - - return hashKey, nil -} - func IsExpiredSession(dirEntry fs.DirEntry) bool { fileInfo, err := dirEntry.Info() if err != nil { // may have been deleted in the meantime diff --git a/pkg/cli/server/root.go b/pkg/cli/server/root.go index bd510669..6514385e 100644 --- a/pkg/cli/server/root.go +++ b/pkg/cli/server/root.go @@ -839,6 +839,12 @@ func LoadConfiguration(config *config.Config, configPath string) error { return err } + if err := loadSessionKeys(config); err != nil { + log.Error().Err(err).Msg("failed to read sessionKeysFile") + + return err + } + // defaults applyDefaultValues(config, viperInstance, log) @@ -853,6 +859,26 @@ func LoadConfiguration(config *config.Config, configPath string) error { return nil } +func loadSessionKeys(conf *config.Config) error { + if conf.HTTP.Auth != nil && conf.HTTP.Auth.SessionKeysFile != "" { + var sessionKeys config.SessionKeys + + if err := readSecretFile(conf.HTTP.Auth.SessionKeysFile, &sessionKeys, false); err != nil { + return err + } + + if sessionKeys.HashKey != "" { + conf.HTTP.Auth.SessionHashKey = []byte(sessionKeys.HashKey) + } + + if sessionKeys.EncryptKey != "" { + conf.HTTP.Auth.SessionEncryptKey = []byte(sessionKeys.EncryptKey) + } + } + + return nil +} + func updateLDAPConfig(conf *config.Config) error { if conf.HTTP.Auth == nil || conf.HTTP.Auth.LDAP == nil { return nil @@ -864,8 +890,9 @@ func updateLDAPConfig(conf *config.Config) error { return nil } - newLDAPCredentials, err := readLDAPCredentials(conf.HTTP.Auth.LDAP.CredentialsFile) - if err != nil { + var newLDAPCredentials config.LDAPCredentials + + if err := readSecretFile(conf.HTTP.Auth.LDAP.CredentialsFile, &newLDAPCredentials, true); err != nil { return err } @@ -875,48 +902,46 @@ func updateLDAPConfig(conf *config.Config) error { return nil } -func readLDAPCredentials(ldapConfigPath string) (config.LDAPCredentials, error) { +func readSecretFile(path string, v any, checkUnsetFields bool) error { //nolint: varnamelen viperInstance := viper.NewWithOptions(viper.KeyDelimiter("::")) - viperInstance.SetConfigFile(ldapConfigPath) + viperInstance.SetConfigFile(path) if err := viperInstance.ReadInConfig(); err != nil { - log.Error().Err(err).Msg("failed to read configuration") + log.Error().Err(err).Str("path", path).Msg("failed to read secret file configuration") - return config.LDAPCredentials{}, errors.Join(zerr.ErrBadConfig, err) + return errors.Join(zerr.ErrBadConfig, err) } - var ldapCredentials config.LDAPCredentials - metaData := &mapstructure.Metadata{} - if err := viperInstance.Unmarshal(&ldapCredentials, metadataConfig(metaData)); err != nil { - log.Error().Err(err).Msg("failed to unmarshal ldap credentials config") + if err := viperInstance.Unmarshal(v, metadataConfig(metaData)); err != nil { + log.Error().Err(err).Str("path", path).Msg("failed to unmarshal secret file config") - return config.LDAPCredentials{}, errors.Join(zerr.ErrBadConfig, err) + return errors.Join(zerr.ErrBadConfig, 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") + log.Error().Err(zerr.ErrBadConfig).Str("path", path). + Msg("failed to load secret file due to the absence of any key:value pair") - return config.LDAPCredentials{}, zerr.ErrBadConfig + return 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") + log.Error().Err(zerr.ErrBadConfig).Str("path", path).Strs("keys", metaData.Unused). + Msg("failed to load secret file due to unknown keys") - return config.LDAPCredentials{}, zerr.ErrBadConfig + return zerr.ErrBadConfig } - if len(metaData.Unset) > 0 { + if checkUnsetFields && 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 zerr.ErrBadConfig } - return ldapCredentials, nil + return nil } func authzContainsOnlyAnonymousPolicy(cfg *config.Config) bool { diff --git a/pkg/cli/server/root_test.go b/pkg/cli/server/root_test.go index 9a993ed3..25519c18 100644 --- a/pkg/cli/server/root_test.go +++ b/pkg/cli/server/root_test.go @@ -1350,6 +1350,77 @@ storage: So(err, ShouldBeNil) }) + Convey("Test verify good session keys config with both keys", 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(`{ + "hashKey":"very-secret", + "encryptKey":"another-secret" + }`) + + _, 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":{"htpasswd":{"path":"test/data/htpasswd"}, "sessionKeysFile": "%s", + "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 good session keys config with one 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()) + + content := []byte(`{ + "hashKey":"very-secret" + }`) + + _, 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":{"htpasswd":{"path":"test/data/htpasswd"}, "sessionKeysFile": "%s", + "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 good ldap config", t, func(c C) { tmpFile, err := os.CreateTemp("", "zot-test*.json") So(err, ShouldBeNil)