feat: integrate openID auth logic and user profile management (#1381)

This change introduces OpenID authn by using providers such as Github,
Gitlab, Google and Dex.
User sessions are now used for web clients to identify
and persist an authenticated users session, thus not requiring every request to
use credentials.
Another change is apikey feature, users can create/revoke their api keys and use them
to authenticate when using cli clients such as skopeo.

eg:
login:
/auth/login?provider=github
/auth/login?provider=gitlab
and so on

logout:
/auth/logout

redirectURL:
/auth/callback/github
/auth/callback/gitlab
and so on

If network policy doesn't allow inbound connections, this callback wont work!

for more info read documentation added in this commit.

Signed-off-by: Alex Stan <alexandrustan96@yahoo.ro>
Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
Co-authored-by: Alex Stan <alexandrustan96@yahoo.ro>
This commit is contained in:
peusebiu
2023-07-07 19:27:10 +03:00
committed by GitHub
parent 5494a1b8d6
commit 17d1338af1
51 changed files with 5467 additions and 624 deletions
+63 -2
View File
@@ -1,5 +1,5 @@
//go:build sync && scrub && metrics && search
// +build sync,scrub,metrics,search
//go:build sync && scrub && metrics && search && apikey
// +build sync,scrub,metrics,search,apikey
package cli_test
@@ -857,6 +857,67 @@ func TestServeMgmtExtension(t *testing.T) {
})
}
func TestServeAPIKeyExtension(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
Convey("apikey implicitly enabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"apikey": {
}
}
}`
logPath, err := runCLIWithConfig(t.TempDir(), content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
defer os.Remove(logPath) // clean up
So(string(data), ShouldContainSubstring, "\"APIKey\":{\"Enable\":true}")
})
Convey("apikey disabled", t, func(c C) {
content := `{
"storage": {
"rootDirectory": "%s"
},
"http": {
"address": "127.0.0.1",
"port": "%s"
},
"log": {
"level": "debug",
"output": "%s"
},
"extensions": {
"apikey": {
"enable": "false"
}
}
}`
logPath, err := runCLIWithConfig(t.TempDir(), content)
So(err, ShouldBeNil)
data, err := os.ReadFile(logPath)
So(err, ShouldBeNil)
defer os.Remove(logPath) // clean up
So(string(data), ShouldContainSubstring, "\"APIKey\":{\"Enable\":false}")
})
}
func readLogFileAndSearchString(logPath string, stringToMatch string, timeout time.Duration) (bool, error) { //nolint:unparam,lll
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
defer cancelFunc()
+52 -4
View File
@@ -361,6 +361,10 @@ func validateConfiguration(config *config.Config) error {
return err
}
if err := validateOpenIDConfig(config); err != nil {
return err
}
if err := validateSync(config); err != nil {
return err
}
@@ -377,7 +381,7 @@ func validateConfiguration(config *config.Config) error {
return err
}
// check authorization config, it should have basic auth enabled or ldap
// check authorization config, it should have basic auth enabled or ldap, api keys or OpenID
if config.HTTP.AccessControl != nil {
// checking for anonymous policy only authorization config: no users, no policies but anonymous policy
if err := validateAuthzPolicies(config); err != nil {
@@ -435,11 +439,42 @@ func validateConfiguration(config *config.Config) error {
return nil
}
func validateOpenIDConfig(config *config.Config) error {
if config.HTTP.Auth != nil && config.HTTP.Auth.OpenID != nil {
for provider, providerConfig := range config.HTTP.Auth.OpenID.Providers {
//nolint: gocritic
if api.IsOpenIDSupported(provider) {
if providerConfig.ClientID == "" || providerConfig.Issuer == "" ||
len(providerConfig.Scopes) == 0 {
log.Error().Err(errors.ErrBadConfig).
Msg("OpenID provider config requires clientid, issuer and scopes parameters")
return errors.ErrBadConfig
}
} else if api.IsOauth2Supported(provider) {
if providerConfig.ClientID == "" || len(providerConfig.Scopes) == 0 {
log.Error().Err(errors.ErrBadConfig).
Msg("OAuth2 provider config requires clientid and scopes parameters")
return errors.ErrBadConfig
}
} else {
log.Error().Err(errors.ErrBadConfig).
Msg("unsupported openid/oauth2 provider")
return errors.ErrBadConfig
}
}
}
return nil
}
func validateAuthzPolicies(config *config.Config) error {
if (config.HTTP.Auth == nil || (config.HTTP.Auth.HTPasswd.Path == "" && config.HTTP.Auth.LDAP == nil)) &&
!authzContainsOnlyAnonymousPolicy(config) {
if (config.HTTP.Auth == nil || (config.HTTP.Auth.HTPasswd.Path == "" && config.HTTP.Auth.LDAP == nil &&
config.HTTP.Auth.OpenID == nil)) && !authzContainsOnlyAnonymousPolicy(config) {
log.Error().Err(errors.ErrBadConfig).
Msg("access control config requires httpasswd, ldap authentication " +
Msg("access control config requires one of httpasswd, ldap or openid authentication " +
"or using only 'anonymousPolicy' policies")
return errors.ErrBadConfig
@@ -484,6 +519,13 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
// Note: In case mgmt is not empty the config.Extensions will not be nil and we will not reach here
config.Extensions.Mgmt = &extconf.MgmtConfig{}
}
_, ok = extMap["apikey"]
if ok {
// we found a config like `"extensions": {"mgmt:": {}}`
// Note: In case mgmt is not empty the config.Extensions will not be nil and we will not reach here
config.Extensions.APIKey = &extconf.APIKeyConfig{}
}
}
if config.Extensions != nil {
@@ -550,6 +592,12 @@ func applyDefaultValues(config *config.Config, viperInstance *viper.Viper) {
}
}
if config.Extensions.APIKey != nil {
if config.Extensions.APIKey.Enable == nil {
config.Extensions.APIKey.Enable = &defaultVal
}
}
if config.Extensions.Scrub != nil {
if config.Extensions.Scrub.Enable == nil {
config.Extensions.Scrub.Enable = &defaultVal
+65
View File
@@ -952,6 +952,71 @@ func TestVerify(t *testing.T) {
So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic)
})
Convey("Test verify openid config with missing parameter", t, func(c C) {
tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpfile.Name()) // clean up
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"openid":{"providers":{"dex":{"issuer":"http://127.0.0.1:5556/dex"}}}}},
"log":{"level":"debug"}}`)
_, err = tmpfile.Write(content)
So(err, ShouldBeNil)
err = tmpfile.Close()
So(err, ShouldBeNil)
os.Args = []string{"cli_test", "verify", tmpfile.Name()}
So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic)
})
Convey("Test verify oauth2 config with missing parameter", t, func(c C) {
tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpfile.Name()) // clean up
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"openid":{"providers":{"github":{"clientid":"client_id"}}}}},
"log":{"level":"debug"}}`)
_, err = tmpfile.Write(content)
So(err, ShouldBeNil)
err = tmpfile.Close()
So(err, ShouldBeNil)
os.Args = []string{"cli_test", "verify", tmpfile.Name()}
So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic)
})
Convey("Test verify openid config with unsupported provider", t, func(c C) {
tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpfile.Name()) // clean up
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"openid":{"providers":{"unsupported":{"issuer":"http://127.0.0.1:5556/dex"}}}}},
"log":{"level":"debug"}}`)
_, err = tmpfile.Write(content)
So(err, ShouldBeNil)
err = tmpfile.Close()
So(err, ShouldBeNil)
os.Args = []string{"cli_test", "verify", tmpfile.Name()}
So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldPanic)
})
Convey("Test verify openid config without apikey extension enabled", t, func(c C) {
tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpfile.Name()) // clean up
content := []byte(`{"distSpecVersion":"1.1.0-dev","storage":{"rootDirectory":"/tmp/zot"},
"http":{"address":"127.0.0.1","port":"8080","realm":"zot",
"auth":{"openid":{"providers":{"dex":{"issuer":"http://127.0.0.1:5556/dex",
"clientid":"client_id","scopes":["openid"]}}}}},
"log":{"level":"debug"}}`)
_, err = tmpfile.Write(content)
So(err, ShouldBeNil)
err = tmpfile.Close()
So(err, ShouldBeNil)
os.Args = []string{"cli_test", "verify", tmpfile.Name()}
So(func() { _ = cli.NewServerRootCmd().Execute() }, ShouldNotPanic)
})
Convey("Test verify config with missing basedn key", t, func(c C) {
tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil)