diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 000000000..4e617c998 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,38 @@ +// Copyright 2017 Pilosa Corp. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +type Auth struct { + // Enable AuthZ/AuthN for featurebase server + Enable bool `toml:"enable"` + + // Application/Client ID + ClientId string `toml:"client-id"` + + // Client Secret + ClientSecret string `toml:"client-secret"` + + // Authorize URL + AuthorizeURL string `toml:"authorize-url"` + + // Token URL + TokenURL string `toml:"token-url"` + + // Group Endpoint URL + GroupEndpointURL string `toml:"group-endpoint-url"` + + // Scope URL + ScopeURL string `toml:"scope-url"` +} diff --git a/ctl/server.go b/ctl/server.go index d53bf81af..c5d43a937 100644 --- a/ctl/server.go +++ b/ctl/server.go @@ -121,4 +121,14 @@ func BuildServerFlags(cmd *cobra.Command, srv *server.Command) { // Toggle /schema/details endpoint. flags.BoolVar(&srv.Config.SchemaDetailsOn, "schema-details-on", true, "Disable /schema/details endpoint") + + // OAuth2.0 identity provider configuration + flags.BoolVar(&srv.Config.Auth.Enable, "auth.enable", false, "Enable AuthN/AuthZ of featurebase, disabled by default.") + flags.StringVar(&srv.Config.Auth.ClientId, "auth.client-id", srv.Config.Auth.ClientId, "Identity Provider's Application/Client ID.") + flags.StringVar(&srv.Config.Auth.ClientSecret, "auth.client-secret", srv.Config.Auth.ClientSecret, "Identity Provider's Client Secret.") + flags.StringVar(&srv.Config.Auth.AuthorizeURL, "auth.authorize-url", srv.Config.Auth.AuthorizeURL, "Identity Provider's Authorize URL.") + flags.StringVar(&srv.Config.Auth.TokenURL, "auth.token-url", srv.Config.Auth.TokenURL, "Identity Provider's Token URL.") + flags.StringVar(&srv.Config.Auth.GroupEndpointURL, "auth.group-endpoint-url", srv.Config.Auth.GroupEndpointURL, "Identity Provider's Group endpoint URL.") + flags.StringVar(&srv.Config.Auth.ScopeURL, "auth.scope-url", srv.Config.Auth.ScopeURL, "Identity Provider's Scope URL.") + } diff --git a/install/featurebase.conf b/install/featurebase.conf index 73895ed6d..540a410f4 100644 --- a/install/featurebase.conf +++ b/install/featurebase.conf @@ -371,3 +371,13 @@ log-path = "/var/log/molecula/featurebase.log" # ============================================================================== +# Enable/Disable AuthN/AuthZ for featurebase +# Can choose identity provider, pass authorize and user-info endpoints, and client id +# [auth] +# enable = false +# client-id = "" +# client-secret = "" +# authorize-url = "" +# token-url = "" +# group-endpoint-url = "" +# scope-url = "" \ No newline at end of file diff --git a/server/config.go b/server/config.go index a42eb7917..0c1989c7a 100644 --- a/server/config.go +++ b/server/config.go @@ -19,11 +19,13 @@ import ( "fmt" "log" "net" + "net/url" "runtime" "strconv" "strings" "time" + "github.com/molecula/featurebase/v2/auth" petcd "github.com/molecula/featurebase/v2/etcd" rbfcfg "github.com/molecula/featurebase/v2/rbf/cfg" "github.com/molecula/featurebase/v2/storage" @@ -240,6 +242,9 @@ type Config struct { // Toggles /schema/details endpoint. If off, it returns empty. SchemaDetailsOn bool `toml:"schema-details-on"` + + // Enable AuthZ/AuthN + Auth auth.Auth `toml:"auth"` } // Namespace returns the namespace to use based on the Future flag. @@ -273,6 +278,7 @@ func (c *Config) validate() error { "Etcd.ClusterURL", c.Etcd.ClusterURL, "Postgres.Bind", c.Postgres.Bind, } + ports := make(map[int]bool) n := len(hostPort) for i := 0; i < n; i += 2 { @@ -602,3 +608,46 @@ func lookupAddr(ctx context.Context, resolver *net.Resolver, host string) (strin // No IPv4 address, return the first resolved address instead. return addrs[0].String(), nil } + +func (c *Config) ValidateAuth() ([]error, error) { + if !c.Auth.Enable { + return []error{}, nil + } + authConfig := map[string]string{ + "ClientId": c.Auth.ClientId, + "ClientSecret": c.Auth.ClientSecret, + "AuthorizeURL": c.Auth.AuthorizeURL, + "TokenURL": c.Auth.TokenURL, + "GroupEndpointURL": c.Auth.GroupEndpointURL, + "ScopeURL": c.Auth.ScopeURL, + } + + errors := make([]error, 0) + for name, value := range authConfig { + if value == "" { + errors = append(errors, fmt.Errorf("Empty string for auth config %s", name)) + continue + } + + if strings.Contains(name, "URL") { + _, err := url.ParseRequestURI(value) + if err != nil { + errors = append(errors, fmt.Errorf("Invalid URL for auth config %s: %s", name, err)) + continue + } + } + } + if len(errors) > 0 { + return errors, fmt.Errorf("there were errors validating config") + } + return errors, nil +} + +func (c *Config) MustValidateAuth() { + if errors, err := c.ValidateAuth(); err != nil { + for _, e := range errors { + log.Println(e) + } + log.Fatal(err) + } +} diff --git a/server/config_internal_test.go b/server/config_internal_test.go index f48db1a16..8d5e1fea0 100644 --- a/server/config_internal_test.go +++ b/server/config_internal_test.go @@ -21,6 +21,8 @@ import ( "os" "strings" "testing" + + "github.com/molecula/featurebase/v2/auth" ) type addrs struct{ bind, advertise string } @@ -288,3 +290,213 @@ func TestConfig_validateAddrsGRPC(t *testing.T) { }) } } + +func TestConfig_validateAuth(t *testing.T) { + errorMesgEmpty := "Empty string" + errorMesgURL := "Invalid URL" + validTestURL := "https://url.com/" + validClientID := "clientid" + validClientSecret := "clientSecret" + notValidURL := "not-a-url" + emptyString := "" + enable := true + disable := false + + tests := []struct { + expErrs []string + input auth.Auth + }{ + + { + // Auth enabled, all configs are set to empty string + []string{ + errorMesgEmpty, + errorMesgEmpty, + errorMesgEmpty, + errorMesgEmpty, + errorMesgEmpty, + errorMesgEmpty, + }, + auth.Auth{ + Enable: enable, + ClientId: emptyString, + ClientSecret: emptyString, + AuthorizeURL: emptyString, + TokenURL: emptyString, + GroupEndpointURL: emptyString, + ScopeURL: emptyString, + }, + }, + { + // Auth enabled, some configs are set to empty string + []string{ + errorMesgEmpty, + errorMesgEmpty, + errorMesgEmpty, + errorMesgEmpty, + errorMesgEmpty, + }, + auth.Auth{ + Enable: enable, + ClientId: validClientID, + ClientSecret: emptyString, + AuthorizeURL: emptyString, + TokenURL: emptyString, + GroupEndpointURL: emptyString, + ScopeURL: emptyString, + }, + }, + { + // Auth enabled, some configs are set to empty string + []string{ + errorMesgEmpty, + errorMesgEmpty, + errorMesgEmpty, + errorMesgEmpty, + errorMesgEmpty, + }, + auth.Auth{ + Enable: enable, + ClientId: emptyString, + ClientSecret: validClientSecret, + AuthorizeURL: emptyString, + TokenURL: emptyString, + GroupEndpointURL: emptyString, + ScopeURL: emptyString, + }, + }, + { + // Auth enabled, some configs are set to empty string + []string{ + errorMesgEmpty, + errorMesgEmpty, + errorMesgEmpty, + errorMesgEmpty, + }, + auth.Auth{ + Enable: enable, + ClientId: validClientID, + ClientSecret: validClientSecret, + AuthorizeURL: emptyString, + TokenURL: emptyString, + GroupEndpointURL: emptyString, + ScopeURL: emptyString, + }, + }, + { + // Auth enabled, some configs are set to empty string + []string{ + errorMesgEmpty, + errorMesgEmpty, + errorMesgEmpty, + }, + auth.Auth{ + Enable: enable, + ClientId: validClientID, + ClientSecret: validClientSecret, + AuthorizeURL: validTestURL, + TokenURL: emptyString, + GroupEndpointURL: emptyString, + ScopeURL: emptyString, + }, + }, + { + // Auth enabled, some configs are set to empty string + []string{ + errorMesgEmpty, + errorMesgEmpty, + }, + auth.Auth{ + Enable: enable, + ClientId: validClientID, + ClientSecret: validClientSecret, + AuthorizeURL: validTestURL, + TokenURL: validTestURL, + GroupEndpointURL: emptyString, + ScopeURL: emptyString, + }, + }, + { + // Auth enabled, some strings are set to invalid URL + []string{ + errorMesgURL, + }, + auth.Auth{ + Enable: enable, + ClientId: validClientID, + ClientSecret: validClientSecret, + AuthorizeURL: notValidURL, + TokenURL: validTestURL, + GroupEndpointURL: validTestURL, + ScopeURL: validTestURL, + }, + }, + { + // Auth enabled, some strings are set to invalid URL + []string{ + errorMesgURL, + errorMesgURL, + }, + auth.Auth{ + Enable: enable, + ClientId: validClientID, + ClientSecret: validClientSecret, + AuthorizeURL: validTestURL, + TokenURL: notValidURL, + GroupEndpointURL: notValidURL, + ScopeURL: validTestURL, + }, + }, + { + // Auth enabled, all configs are set properly + []string{}, + auth.Auth{ + Enable: enable, + ClientId: validClientID, + ClientSecret: validClientSecret, + AuthorizeURL: validTestURL, + TokenURL: validTestURL, + GroupEndpointURL: validTestURL, + ScopeURL: validTestURL, + }, + }, + { + // Auth disabled, all configs are set to empty string + []string{}, + auth.Auth{ + Enable: disable, + ClientId: emptyString, + ClientSecret: emptyString, + AuthorizeURL: emptyString, + TokenURL: emptyString, + GroupEndpointURL: emptyString, + ScopeURL: emptyString, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + c := NewConfig() + c.Auth = test.input + + errors, err := c.ValidateAuth() + if len(test.expErrs) > 0 { + if err == nil { + t.Fatal("expected errors, but none were found") + } + } + + if len(errors) != len(test.expErrs) { + fmt.Printf("%+v\n", errors) + t.Fatalf("expected %v errors but got %v", len(test.expErrs), len(errors)) + } + + for i, e := range errors { + if !strings.Contains(e.Error(), test.expErrs[i]) { + t.Errorf("expected error to contain %s, but got %s", test.expErrs[i], e.Error()) + } + } + }) + } +} diff --git a/server/server.go b/server/server.go index 29eb20f1d..943813b07 100644 --- a/server/server.go +++ b/server/server.go @@ -234,6 +234,10 @@ func (m *Command) Start() (err error) { return errors.Wrap(err, "setting resource limits") } + if m.Config.Auth.Enable == true { + m.Config.MustValidateAuth() + } + // Initialize server. if err = m.Server.Open(); err != nil { return errors.Wrap(err, "opening server")