diff --git a/pkg/settings/kv_decoder.go b/pkg/settings/kv_decoder.go index 3901f0181..7cf77d08a 100644 --- a/pkg/settings/kv_decoder.go +++ b/pkg/settings/kv_decoder.go @@ -7,18 +7,13 @@ import ( ) type ( -// @todo support Decoder interface -// Decoder interface { -// Decode(kv KV, prefix string) error -// } + // KVDecoder interface for custom decoding logic + KVDecoder interface { + DecodeKV(KV, string) error + } ) -var ( -// @todo support Decoder interface -// decoderTyEl = reflect.TypeOf((*Decoder)(nil)).Elem() -) - -// Decode converts key-value (KV) into structs using tags & field names +// DecodeKV converts key-value (KV) into structs using tags & field names // // Supports decoding into all scalar types, can handle nested structures and simple maps (1 dim, string as key) // @@ -32,33 +27,40 @@ var ( // Number int // } // -func Decode(kv KV, dst interface{}, pp ...string) (err error) { - v := reflect.ValueOf(dst) - if v.Kind() != reflect.Ptr { +func DecodeKV(kv KV, dst interface{}, pp ...string) (err error) { + valueOf := reflect.ValueOf(dst) + if valueOf.Kind() != reflect.Ptr { return errors.New("expecting a pointer, not a value") } - if v.IsNil() { + if valueOf.IsNil() { return errors.New("nil pointer passed") } - v = v.Elem() - var prefix string if len(pp) > 0 { // If called with prefix, join string slice + 1 empty string (to ensure tailing dot) prefix = strings.Join(append(pp, ""), ".") } - length := v.NumField() + valueOf = valueOf.Elem() + + length := valueOf.NumField() for i := 0; i < length; i++ { - var ( - f = v.Field(i) - t = v.Type().Field(i) + var structField = valueOf.Field(i) - key = prefix + strings.ToLower(t.Name[:1]) + t.Name[1:] - tag = t.Tag.Get("kv") + if !structField.CanSet() { + continue + } + + var ( + structFType = valueOf.Type().Field(i) + + // whe nwe use name of the struct field directly, remove upper-case from first letter + key = prefix + strings.ToLower(structFType.Name[:1]) + structFType.Name[1:] + + tag = structFType.Tag.Get("kv") tagFlags []string ) @@ -68,10 +70,6 @@ func Decode(kv KV, dst interface{}, pp ...string) (err error) { continue } - // if !f.CanSet() { - // return errors.New("unexpected pointer for field " + t.Name) - // } - if tag != "" { tagFlags = strings.Split(tag, ",") @@ -85,23 +83,30 @@ func Decode(kv KV, dst interface{}, pp ...string) (err error) { } } - // @todo handle Decoder interface - // if f.Type().Implements(decoderTyEl) { - // result := reflect.ValueOf(&t).MethodByName("Decode").Call([]reflect.Value{ - // reflect.ValueOf(kv.Filter(key)), - // reflect.ValueOf(prefix), - // }) - // - // if len(result) != 1 { - // return errors.New("internal error, Decoder signature does not match") - // } - // } + var structValue interface{} + + if structField.Kind() == reflect.Ptr { + structValue = structField.Interface() + } else { + structValue = structField.Addr().Interface() + } + + // Handle custom KVDecoder + if decodeMethod := reflect.ValueOf(structValue).MethodByName("DecodeKV"); decodeMethod.IsValid() { + if decode, ok := decodeMethod.Interface().(func(KV, string) error); !ok { + panic("invalid DecodeKV() function signature") + } else if err = decode(kv, key); err != nil { + return + } else { + continue + } + } // Handles structs // - // It calls Decode recursively - if f.Kind() == reflect.Struct { - if err = Decode(kv.Filter(key), f.Addr().Interface(), key); err != nil { + // It calls DecodeKV recursively + if structField.Kind() == reflect.Struct { + if err = DecodeKV(kv.Filter(key), structValue, key); err != nil { return } @@ -109,26 +114,32 @@ func Decode(kv KV, dst interface{}, pp ...string) (err error) { } // Handles map values - if f.Kind() == reflect.Map { - if f.IsNil() { + if structField.Kind() == reflect.Map { + if structField.IsNil() { // allocate new map - f.Set(reflect.MakeMap(f.Type())) + structField.Set(reflect.MakeMap(structField.Type())) } + // cut KV key prefix and use the rest for the map key for k, val := range kv.CutPrefix(key + ".") { - mapValue := reflect.New(f.Type().Elem()) - val.Unmarshal(mapValue.Interface()) - f.SetMapIndex(reflect.ValueOf(k), mapValue.Elem()) + mapValue := reflect.New(structField.Type().Elem()) + err = val.Unmarshal(mapValue.Interface()) + if err != nil { + return + } + + structField.SetMapIndex(reflect.ValueOf(k), mapValue.Elem()) } continue } + // Native type if val, ok := kv[key]; ok { - if err = val.Unmarshal(f.Addr().Interface()); err != nil { + // Always use pointer to value + if err = val.Unmarshal(structField.Addr().Interface()); err != nil { return } - } } diff --git a/pkg/settings/kv_decoder_test.go b/pkg/settings/kv_decoder_test.go index b38cf09b8..47558fb51 100644 --- a/pkg/settings/kv_decoder_test.go +++ b/pkg/settings/kv_decoder_test.go @@ -91,6 +91,44 @@ func TestDecode(t *testing.T) { // setting this externaly (embedded structs) eq.Sub.Bar.Foo = "foobar" - require.NoError(t, Decode(kv, &aux)) + require.NoError(t, DecodeKV(kv, &aux)) require.Equal(t, eq, aux) } + +type ( + decodeHandlerBase struct { + Foo decodeHandlerSub + Bar *decodeHandlerSub + } + + decodeHandlerSub struct { + set int + } +) + +func (b *decodeHandlerSub) DecodeKV(kv KV, prefix string) error { + b.set++ + return nil +} + +var _ KVDecoder = &decodeHandlerSub{} + +func TestDecodeHandler(t *testing.T) { + var ( + kv = KV{ + // should panic if DecodeKV is not called: + // cannot unmarshal number into Go value of type settings.decodeHandlerSub + "foo": types.JSONText(`1`), + } + + aux = decodeHandlerBase{ + Foo: decodeHandlerSub{}, + Bar: &decodeHandlerSub{}, + } + ) + require.NoError(t, DecodeKV(kv, &aux)) + require.NotNil(t, aux.Foo) + require.NotNil(t, aux.Bar) + require.Equal(t, 1, aux.Foo.set) + require.Equal(t, 1, aux.Bar.set) +} diff --git a/pkg/settings/types.go b/pkg/settings/types.go index 65d921d68..f0e7da9e2 100644 --- a/pkg/settings/types.go +++ b/pkg/settings/types.go @@ -133,9 +133,9 @@ func (kv KV) CutPrefix(prefix string) KV { return out } -// Decode is a helper function on KV that calls Decode() and passes on the dst +// DecodeKV is a helper function on KV that calls DecodeKV() and passes on the dst func (kv KV) Decode(dst interface{}) error { - return Decode(kv, dst) + return DecodeKV(kv, dst) } // Replace finds and updates existing or appends new value @@ -176,7 +176,7 @@ input: continue } - if s.String() == i.String() { + if s.Value.String() == i.Value.String() { // Value did not change, continue with next input set continue input } diff --git a/pkg/settings/types_test.go b/pkg/settings/types_test.go index 949e43924..09421158f 100644 --- a/pkg/settings/types_test.go +++ b/pkg/settings/types_test.go @@ -89,17 +89,25 @@ func TestValueSet_Changed(t *testing.T) { return o } - org = ValueSet{msv("a", "a1"), msv("b", "b1"), msv("d", "d1")} - inp = ValueSet{msv("a", "a2"), msv("c", "c1"), msv("d", "d1")} + // make bool value + mbv = func(n string, v bool) *Value { + o := &Value{Name: n} + _ = o.SetValue(v) + return o + } + + org = ValueSet{msv("a", "a1"), msv("b", "b1"), msv("d", "d1"), mbv("bool", true)} + inp = ValueSet{msv("a", "a2"), msv("c", "c1"), msv("d", "d1"), mbv("bool", false)} out ValueSet ) out = org.Changed(inp) - req.Len(out, 2) + req.Len(out, 3) req.Equal("a2", out.First("a").String()) req.Equal("", out.First("b").String()) req.Equal("c1", out.First("c").String()) req.Equal("", out.First("d").String()) + req.Equal(false, out.First("bool").Bool()) } diff --git a/system/auth/external/external.go b/system/auth/external/external.go index d6b728740..51ef8815d 100644 --- a/system/auth/external/external.go +++ b/system/auth/external/external.go @@ -12,7 +12,7 @@ const ( ) func Init() { - setupGoth(service.DefaultAuthSettings) + setupGoth(service.CurrentSettings) } func log() *zap.Logger { diff --git a/system/auth/external/goth.go b/system/auth/external/goth.go index 3438a1b80..954e3d8ad 100644 --- a/system/auth/external/goth.go +++ b/system/auth/external/goth.go @@ -13,7 +13,7 @@ import ( "github.com/markbates/goth/providers/openidConnect" "go.uber.org/zap" - "github.com/cortezaproject/corteza-server/system/service" + "github.com/cortezaproject/corteza-server/system/types" ) // We're expecting that our users will be able to complete @@ -24,16 +24,16 @@ const ( WellKnown = "/.well-known/openid-configuration" ) -func setupGoth(as *service.AuthSettings) { - if !as.ExternalEnabled { +func setupGoth(s *types.Settings) { + if !s.Auth.External.Enabled { log().Info("external authentication disabled") return } - store := sessions.NewCookieStore([]byte(as.ExternalSessionStoreSecret)) + store := sessions.NewCookieStore([]byte(s.Auth.External.SessionStoreSecret)) store.MaxAge(gothMaxSessionStoreAge) store.Options.HttpOnly = true - store.Options.Secure = as.ExternalSessionStoreSecure + store.Options.Secure = s.Auth.External.SessionStoreSecure gothic.Store = store log().Debug("registering cookie session store") @@ -43,11 +43,11 @@ func setupGoth(as *service.AuthSettings) { } - setupGothProviders(as) + setupGothProviders(s) } -func setupGothProviders(as *service.AuthSettings) { +func setupGothProviders(s *types.Settings) { var ( err error ) @@ -59,7 +59,7 @@ func setupGothProviders(as *service.AuthSettings) { } var enabled = 0 - for _, pc := range as.ExternalProviders { + for _, pc := range s.Auth.External.Providers { if pc.Enabled { enabled++ } @@ -67,16 +67,16 @@ func setupGothProviders(as *service.AuthSettings) { log().Debug("initializing enabled external authentication providers", zap.Int("count", enabled)) - for name, pc := range as.ExternalProviders { + for _, pc := range s.Auth.External.Providers { var provider goth.Provider - log := log().With(zap.String("provider", name)) + log := log().With(zap.String("provider", pc.Handle)) if !pc.Enabled { continue } - if strings.Index(name, OIDC_PROVIDER_PREFIX) == 0 { + if strings.Index(pc.Handle, OIDC_PROVIDER_PREFIX) == 0 { if pc.IssuerUrl == "" { log.Error("failed to discover OIDC provider, URL empty") continue @@ -88,10 +88,10 @@ func setupGothProviders(as *service.AuthSettings) { log.Error("failed to discover OIDC provider", zap.Error(err), zap.String("well-known", wellKnown)) continue } else { - provider.SetName(name) + provider.SetName(pc.Handle) } } else { - switch name { + switch pc.Handle { case "github": provider = github.New(pc.Key, pc.Secret, pc.RedirectUrl, "user:email") case "facebook": diff --git a/system/auth/external/register.go b/system/auth/external/register.go index fcffaed67..741968cde 100644 --- a/system/auth/external/register.go +++ b/system/auth/external/register.go @@ -2,24 +2,27 @@ package external import ( "context" - "errors" "fmt" + "io/ioutil" + "net/http" "net/url" "strings" "github.com/crusttech/go-oidc" + "github.com/pkg/errors" "go.uber.org/zap" "github.com/cortezaproject/corteza-server/pkg/settings" "github.com/cortezaproject/corteza-server/system/service" + "github.com/cortezaproject/corteza-server/system/types" ) -func AddProvider(name string, eap *service.AuthSettingsExternalAuthProvider, force bool) error { +func AddProvider(eap *types.ExternalAuthProvider, force bool) error { var ( - as = service.DefaultAuthSettings + s = service.CurrentSettings log = log().With( zap.Bool("force", force), - zap.String("name", name), + zap.String("handle", eap.Handle), zap.String("key", eap.Key), ) ) @@ -31,12 +34,12 @@ func AddProvider(name string, eap *service.AuthSettingsExternalAuthProvider, for log.Info("adding external auth provider") if !force { - if e, exists := as.ExternalProviders[name]; exists && e.Key == eap.Key && e.Secret == eap.Secret { + if ex := s.Auth.External.Providers.FindByHandle(eap.Handle); ex != nil && ex.Key == eap.Key && ex.Secret == eap.Secret { return nil } } - if vv, err := eap.MakeValueSet(name); err != nil { + if vv, err := eap.EncodeKV(); err != nil { log.Error("could not prepare settings", zap.Error(err)) return err } else if err = service.DefaultIntSettings.BulkSet(vv); err != nil { @@ -50,11 +53,11 @@ func AddProvider(name string, eap *service.AuthSettingsExternalAuthProvider, for // @todo remove dependency on github.com/crusttech/go-oidc (and github.com/coreos/go-oidc) // and move client registration to corteza codebase -func DiscoverOidcProvider(ctx context.Context, eas *service.AuthSettings, name, url string) (eap *service.AuthSettingsExternalAuthProvider, err error) { +func DiscoverOidcProvider(ctx context.Context, s *types.Settings, name, url string) (eap *types.ExternalAuthProvider, err error) { var ( provider *oidc.Provider client *oidc.Client - redirectUrl = fmt.Sprintf(eas.ExternalRedirectUrl, OIDC_PROVIDER_PREFIX+name) + redirectUrl = fmt.Sprintf(s.Auth.External.RedirectUrl, OIDC_PROVIDER_PREFIX+name) log = log().With( zap.String("redirect-url", redirectUrl), @@ -78,7 +81,8 @@ func DiscoverOidcProvider(ctx context.Context, eas *service.AuthSettings, name, return } - eap = &service.AuthSettingsExternalAuthProvider{ + eap = &types.ExternalAuthProvider{ + Handle: name, RedirectUrl: redirectUrl, Key: client.ID, Secret: client.Secret, @@ -90,13 +94,13 @@ func DiscoverOidcProvider(ctx context.Context, eas *service.AuthSettings, name, return } -func RegisterOidcProvider(ctx context.Context, name, providerUrl string, force, validate, enable bool) (eap *service.AuthSettingsExternalAuthProvider, err error) { +func RegisterOidcProvider(ctx context.Context, name, providerUrl string, force, validate, enable bool) (eap *types.ExternalAuthProvider, err error) { var ( - as = service.DefaultAuthSettings + s = service.CurrentSettings ) if !force { - if _, exists := as.ExternalProviders[OIDC_PROVIDER_PREFIX+name]; exists { + if s.Auth.External.Providers.FindByHandle(OIDC_PROVIDER_PREFIX+name) != nil { return } } @@ -104,17 +108,17 @@ func RegisterOidcProvider(ctx context.Context, name, providerUrl string, force, if validate { // Do basic validation of external auth settings // will fail if secret or url are not set - if err = as.StaticValidateExternal(); err != nil { + if err = staticValidateExternal(s); err != nil { return } // Do full rediredct-URL check - if err = as.ValidateExternalRedirectURL(); err != nil { + if err = validateExternalRedirectURL(s); err != nil { return } } - if as.ExternalRedirectUrl == "" { + if s.Auth.External.RedirectUrl == "" { return nil, errors.New("refusing to register OIDC provider without redirect url") } @@ -123,12 +127,12 @@ func RegisterOidcProvider(ctx context.Context, name, providerUrl string, force, return } - eap, err = DiscoverOidcProvider(ctx, as, name, p.String()) + eap, err = DiscoverOidcProvider(ctx, s, name, p.String()) if err != nil { return } - vv, err := eap.MakeValueSet(OIDC_PROVIDER_PREFIX + name) + vv, err := eap.EncodeKV() if err != nil { return } @@ -178,3 +182,60 @@ func parseExternalProviderUrl(in string) (p *url.URL, err error) { return } + +// StaticValidateExternal +// +// Simple checks of external auth settings +func staticValidateExternal(s *types.Settings) error { + if s.Auth.External.RedirectUrl == "" { + return errors.New("redirect URL is empty") + } + + const ( + tpt = "test-provider-test" + ) + p, err := url.Parse(fmt.Sprintf(s.Auth.External.RedirectUrl, tpt)) + if err != nil { + return errors.Wrap(err, "invalid redirect URL") + } + + if !strings.Contains(p.Path, tpt+"/callback") { + return errors.Wrap(err, "could find injected provider in the URL, make sure you use '%s' as a placeholder") + } + + if s.Auth.External.SessionStoreSecret == "" { + return errors.New("session store secret is empty") + } + + if s.Auth.External.SessionStoreSecure && p.Scheme != "https" { + return errors.New("session store is secure, redirect URL should have HTTPS") + } + + return nil +} + +// ValidateExternalRedirectURL +// +// Validates external redirect URL +func validateExternalRedirectURL(s *types.Settings) error { + const tpt = "test-provider-test" + const cb = "/callback" + + // Replace placeholders & remove /callback + var url = fmt.Sprintf(s.Auth.External.RedirectUrl, tpt) + url = url[0 : len(url)-len(cb)] + + rsp, err := http.DefaultClient.Get(url) + if err != nil { + return errors.Wrap(err, "could not get response from redirect URL") + } + + defer rsp.Body.Close() + body, err := ioutil.ReadAll(rsp.Body) + + if strings.Contains(string(body), tpt) { + return nil + } + + return errors.New("could not validate external auth redirection URL") +} diff --git a/system/provision-oidc-discovery.go b/system/provision-oidc-discovery.go index eeb631a15..d606ab666 100644 --- a/system/provision-oidc-discovery.go +++ b/system/provision-oidc-discovery.go @@ -11,13 +11,19 @@ import ( "github.com/cortezaproject/corteza-server/pkg/cli" "github.com/cortezaproject/corteza-server/pkg/cli/options" "github.com/cortezaproject/corteza-server/system/auth/external" - "github.com/cortezaproject/corteza-server/system/service" + "github.com/cortezaproject/corteza-server/system/types" ) +// Provisions OIDC providers from PROVISION_OIDC_PROVIDER env variable +// +// Env variable should contains space delimited pairs of providers ( ....) func oidcAutoDiscovery(ctx context.Context, cmd *cobra.Command, c *cli.Config) (err error) { var provider = strings.TrimSpace(options.EnvString("", "PROVISION_OIDC_PROVIDER", "")) - c.Log.Debug("OIDC auto discovery provision", zap.String("providers", provider)) + c.Log.Debug("OIDC auto discovery provision", + zap.String("envkey", "PROVISION_OIDC_PROVIDER"), + zap.String("providers", provider), + ) if len(provider) == 0 { return @@ -27,7 +33,7 @@ func oidcAutoDiscovery(ctx context.Context, cmd *cobra.Command, c *cli.Config) ( providers = strings.Split(provider, " ") plen = len(providers) name, purl string - eap *service.AuthSettingsExternalAuthProvider + eap *types.ExternalAuthProvider ) if plen%2 == 1 { @@ -82,7 +88,7 @@ func authAddExternals(ctx context.Context, cmd *cobra.Command, c *cli.Config) (e pp []string - eap service.AuthSettingsExternalAuthProvider + eap *types.ExternalAuthProvider ) for _, kind := range kinds { @@ -93,7 +99,7 @@ func authAddExternals(ctx context.Context, cmd *cobra.Command, c *cli.Config) (e continue } - eap = service.AuthSettingsExternalAuthProvider{Enabled: true} + eap = &types.ExternalAuthProvider{Enabled: true} if kind == "oidc" { pp = strings.SplitN(p, " ", 4) @@ -101,16 +107,16 @@ func authAddExternals(ctx context.Context, cmd *cobra.Command, c *cli.Config) (e // Spread name, issuer-url, key and secret from provision string for OIDC provider name, eap.IssuerUrl, eap.Key, eap.Secret = pp[0], pp[1], pp[2], pp[3] - name = external.OIDC_PROVIDER_PREFIX + name + eap.Handle = external.OIDC_PROVIDER_PREFIX + name } else { pp = strings.SplitN(p, " ", 2) // Spread key and secret from provision string eap.Key, eap.Secret = pp[0], pp[1] - name = kind + eap.Handle = kind } - _ = external.AddProvider(name, &eap, false) + _ = external.AddProvider(eap, false) } return diff --git a/system/rest/auth.go b/system/rest/auth.go index da7327d26..7b80e5e5d 100644 --- a/system/rest/auth.go +++ b/system/rest/auth.go @@ -12,6 +12,7 @@ import ( "github.com/cortezaproject/corteza-server/pkg/payload/outgoing" "github.com/cortezaproject/corteza-server/system/rest/request" "github.com/cortezaproject/corteza-server/system/service" + "github.com/cortezaproject/corteza-server/system/types" ) var _ = errors.Wrap @@ -19,7 +20,7 @@ var _ = errors.Wrap type ( Auth struct { tokenEncoder auth.TokenEncoder - authSettings authServiceSettingsProvider + settings *types.Settings authSvc service.AuthService } @@ -41,7 +42,7 @@ type ( func (Auth) New() *Auth { return &Auth{ tokenEncoder: auth.DefaultJwtHandler, - authSettings: service.DefaultAuthSettings, + settings: service.CurrentSettings, authSvc: service.DefaultAuth, } } @@ -75,14 +76,27 @@ func (ctrl *Auth) Logout(ctx context.Context, r *request.AuthLogout) (interface{ } func (ctrl *Auth) Settings(ctx context.Context, r *request.AuthSettings) (interface{}, error) { - f := ctrl.authSettings.Format() + var ( + int = ctrl.settings.Auth.Internal + ext = ctrl.settings.Auth.External + + out = map[string]interface{}{ + "internalEnabled": int.Enabled, + "internalPasswordResetEnabled": int.PasswordReset.Enabled, + "internalSignUpEmailConfirmationRequired": int.Signup.EmailConfirmationRequired, + "internalSignUpEnabled": int.Signup.Enabled, + + "externalEnabled": ext.Enabled, + "externalProviders": ext.Providers, + } + ) if err := ctrl.authSvc.With(ctx).CanRegister(); err != nil { // f["internalSignUpEnabled"] = false - f["signUpDisabled"] = err.Error() + out["signUpDisabled"] = err.Error() } - return f, nil + return out, nil } func (ctrl *Auth) ExchangeAuthToken(ctx context.Context, r *request.AuthExchangeAuthToken) (interface{}, error) { diff --git a/system/service/auth.go b/system/service/auth.go index 3af1f5d62..d85e71a93 100644 --- a/system/service/auth.go +++ b/system/service/auth.go @@ -30,7 +30,7 @@ type ( credentials repository.CredentialsRepository users repository.UserRepository roles repository.RoleRepository - settings *AuthSettings + settings *types.Settings notifications AuthNotificationService providerValidator func(string) error @@ -106,7 +106,7 @@ func (svc auth) With(ctx context.Context) AuthService { roles: repository.Role(ctx, db), subscription: CurrentSubscription, - settings: DefaultAuthSettings, + settings: CurrentSettings, notifications: DefaultAuthNotification, providerValidator: defaultProviderValidator, @@ -138,7 +138,7 @@ func (svc auth) log(ctx context.Context, fields ...zapcore.Field) *zap.Logger { // 2.3. create credentials for that social login // func (svc auth) External(profile goth.User) (u *types.User, err error) { - if !svc.settings.ExternalEnabled { + if !svc.settings.Auth.External.Enabled { return nil, errors.New("external authentication disabled") } @@ -262,7 +262,7 @@ func (svc auth) External(profile goth.User) (u *types.User, err error) { // FrontendRedirectURL - a proxy to frontend redirect url setting func (svc auth) FrontendRedirectURL() string { - return svc.settings.FrontendUrlRedirect + return svc.settings.Auth.Frontend.Url.Redirect } // InternalSignUp protocol @@ -271,11 +271,11 @@ func (svc auth) FrontendRedirectURL() string { // // We're accepting the whole user object here and copy all we need to the new user func (svc auth) InternalSignUp(input *types.User, password string) (u *types.User, err error) { - if !svc.settings.InternalEnabled { + if !svc.settings.Auth.Internal.Enabled { return nil, errors.New("internal authentication disabled") } - if !svc.settings.InternalSignUpEnabled { + if !svc.settings.Auth.Internal.Signup.Enabled { return nil, errors.New("internal signup disabled") } @@ -340,7 +340,7 @@ func (svc auth) InternalSignUp(input *types.User, password string) (u *types.Use Handle: input.Handle, // Do we need confirmed email? - EmailConfirmed: !svc.settings.InternalSignUpEmailConfirmationRequired, + EmailConfirmed: !svc.settings.Auth.Internal.Signup.EmailConfirmationRequired, }) if err != nil { @@ -383,7 +383,7 @@ func (svc auth) validateInternalSignUp(email string) (err error) { // Expects plain text password as an input func (svc auth) InternalLogin(email string, password string) (u *types.User, err error) { - if !svc.settings.InternalEnabled { + if !svc.settings.Auth.Internal.Enabled { return nil, errors.New("internal authentication disabled") } @@ -484,7 +484,7 @@ func (svc auth) checkPassword(password string, cc types.CredentialsSet) (err err func (svc auth) SetPassword(userID uint64, newPassword string) (err error) { log := svc.log(svc.ctx, zap.Uint64("userID", userID)) - if !svc.settings.InternalEnabled { + if !svc.settings.Auth.Internal.Enabled { return errors.New("internal authentication disabled") } @@ -506,7 +506,7 @@ func (svc auth) SetPassword(userID uint64, newPassword string) (err error) { func (svc auth) ChangePassword(userID uint64, oldPassword, newPassword string) (err error) { log := svc.log(svc.ctx, zap.Uint64("userID", userID)) - if !svc.settings.InternalEnabled { + if !svc.settings.Auth.Internal.Enabled { return errors.New("internal authentication disabled") } @@ -593,7 +593,7 @@ func (svc auth) ValidateAuthRequestToken(token string) (user *types.User, err er } func (svc auth) ValidateEmailConfirmationToken(token string) (user *types.User, err error) { - if !svc.settings.InternalEnabled { + if !svc.settings.Auth.Internal.Enabled { return nil, errors.New("internal authentication disabled") } @@ -611,11 +611,11 @@ func (svc auth) ValidateEmailConfirmationToken(token string) (user *types.User, } func (svc auth) ValidatePasswordResetToken(token string) (user *types.User, err error) { - if !svc.settings.InternalEnabled { + if !svc.settings.Auth.Internal.Enabled { return nil, errors.New("internal authentication disabled") } - if !svc.settings.InternalPasswordResetEnabled { + if !svc.settings.Auth.Internal.PasswordReset.Enabled { return nil, errors.New("password reset disabled") } @@ -635,12 +635,12 @@ func (svc auth) ValidatePasswordResetToken(token string) (user *types.User, err // ExchangePasswordResetToken exchanges reset password token for a new one and returns it with user info func (svc auth) ExchangePasswordResetToken(token string) (user *types.User, exchangedToken string, err error) { - if !svc.settings.InternalEnabled { + if !svc.settings.Auth.Internal.Enabled { err = errors.New("internal authentication disabled") return } - if !svc.settings.InternalPasswordResetEnabled { + if !svc.settings.Auth.Internal.PasswordReset.Enabled { err = errors.New("password reset disabled") return } @@ -662,7 +662,7 @@ func (svc auth) ExchangePasswordResetToken(token string) (user *types.User, exch } func (svc auth) SendEmailAddressConfirmationToken(email string) error { - if !svc.settings.InternalEnabled { + if !svc.settings.Auth.Internal.Enabled { return errors.New("internal authentication disabled") } @@ -699,11 +699,11 @@ func (svc auth) sendEmailAddressConfirmationToken(u *types.User) (err error) { func (svc auth) SendPasswordResetToken(email string) error { - if !svc.settings.InternalEnabled { + if !svc.settings.Auth.Internal.Enabled { return errors.New("internal authentication disabled") } - if !svc.settings.InternalPasswordResetEnabled { + if !svc.settings.Auth.Internal.PasswordReset.Enabled { return errors.New("password reset disabled") } diff --git a/system/service/auth_notification.go b/system/service/auth_notification.go index d3c0f369d..6a468dfcb 100644 --- a/system/service/auth_notification.go +++ b/system/service/auth_notification.go @@ -20,8 +20,7 @@ type ( logger *zap.Logger // @todo merge auth & system settings - authSettings *AuthSettings - settings *types.Settings + settings *types.Settings } AuthNotificationService interface { @@ -45,18 +44,16 @@ type ( func AuthNotification(ctx context.Context) AuthNotificationService { return (&authNotification{ - logger: DefaultLogger.Named("auth-notification"), - authSettings: DefaultAuthSettings, - settings: DefaultSystemSettings, + logger: DefaultLogger.Named("auth-notification"), + settings: CurrentSettings, }).With(ctx) } func (svc authNotification) With(ctx context.Context) AuthNotificationService { return &authNotification{ - ctx: ctx, - logger: logger.AddRequestID(ctx, svc.logger), - authSettings: svc.authSettings, - settings: svc.settings, + ctx: ctx, + logger: logger.AddRequestID(ctx, svc.logger), + settings: svc.settings, } } @@ -67,20 +64,20 @@ func (svc authNotification) log(ctx context.Context, fields ...zapcore.Field) *z func (svc authNotification) EmailConfirmation(lang string, emailAddress string, token string) error { return svc.send("email-confirmation", lang, authNotificationPayload{ EmailAddress: emailAddress, - URL: svc.authSettings.FrontendUrlEmailConfirmation + token, + URL: svc.settings.Auth.Frontend.Url.EmailConfirmation + token, }) } func (svc authNotification) PasswordReset(lang string, emailAddress string, token string) error { return svc.send("password-reset", lang, authNotificationPayload{ EmailAddress: emailAddress, - URL: svc.authSettings.FrontendUrlPasswordReset + token, + URL: svc.settings.Auth.Frontend.Url.PasswordReset + token, }) } func (svc authNotification) newMail() *gomail.Message { m := gomail.NewMessage() - m.SetAddressHeader("From", svc.authSettings.MailFromAddress, svc.authSettings.MailFromName) + m.SetAddressHeader("From", svc.settings.Auth.Mail.FromAddress, svc.settings.Auth.Mail.FromName) return m } @@ -88,9 +85,9 @@ func (svc authNotification) send(name, lang string, payload authNotificationPayl ntf := svc.newMail() payload.Logo = template.URL(svc.settings.General.Mail.Logo) - payload.BaseURL = svc.authSettings.FrontendUrlBase - payload.SignatureName = svc.authSettings.MailFromName - payload.SignatureEmail = svc.authSettings.MailFromAddress + payload.BaseURL = svc.settings.Auth.Frontend.Url.Base + payload.SignatureName = svc.settings.Auth.Mail.FromName + payload.SignatureEmail = svc.settings.Auth.Mail.FromAddress // @todo translations payload.EmailHeaderEn = template.HTML(svc.render(svc.settings.General.Mail.Header, payload)) @@ -100,12 +97,12 @@ func (svc authNotification) send(name, lang string, payload authNotificationPayl // @todo translations switch name { case "email-confirmation": - ntf.SetHeader("Subject", svc.render(svc.authSettings.MailEmailConfirmationSubject, payload)) - ntf.SetBody("text/html", svc.render(svc.authSettings.MailEmailConfirmationBody, payload)) + ntf.SetHeader("Subject", svc.render(svc.settings.Auth.Mail.EmailConfirmation.Subject, payload)) + ntf.SetBody("text/html", svc.render(svc.settings.Auth.Mail.EmailConfirmation.Body, payload)) case "password-reset": - ntf.SetHeader("Subject", svc.render(svc.authSettings.MailPasswordResetSubject, payload)) - ntf.SetBody("text/html", svc.render(svc.authSettings.MailPasswordResetBody, payload)) + ntf.SetHeader("Subject", svc.render(svc.settings.Auth.Mail.PasswordReset.Subject, payload)) + ntf.SetBody("text/html", svc.render(svc.settings.Auth.Mail.PasswordReset.Body, payload)) default: return ErrNoEmailTemplateForGivenOperation diff --git a/system/service/auth_settings.go b/system/service/auth_settings.go deleted file mode 100644 index cc274cd8c..000000000 --- a/system/service/auth_settings.go +++ /dev/null @@ -1,334 +0,0 @@ -package service - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "strings" - - "github.com/markbates/goth" - "github.com/pkg/errors" - - intset "github.com/cortezaproject/corteza-server/pkg/settings" -) - -type ( - AuthSettings struct { - // Password reset path ( "?token=" + ) - FrontendUrlPasswordReset string - - // EmailAddress confirmation path ( "?token=" + ) - FrontendUrlEmailConfirmation string - - // Where to redirect user after external auth flow - FrontendUrlRedirect string - - // Webapp Base URL - FrontendUrlBase string - - MailFromAddress string - MailFromName string - - MailEmailConfirmationSubject string - MailEmailConfirmationBody string - - MailPasswordResetSubject string - MailPasswordResetBody string - - // Is internal authentication (username + password) enabled - InternalEnabled bool - - // Can users register - InternalSignUpEnabled bool - - // Users should confirm their emails when signing-up - InternalSignUpEmailConfirmationRequired bool - - // Can users reset their passwords - InternalPasswordResetEnabled bool - - // Is external authentication - ExternalEnabled bool - - // Where to redirect (url used for registration) - ExternalRedirectUrl string - - // session secret to use - ExternalSessionStoreSecret string - - // session store should be secure - ExternalSessionStoreSecure bool - - // all external providers we know - ExternalProviders map[string]AuthSettingsExternalAuthProvider - } - - AuthSettingsExternalAuthProvider struct { - Enabled bool - Key string - Secret string - RedirectUrl string - IssuerUrl string - } -) - -// ParseAuthSettings maps from plain values to AuthSettings struct -// -// see settings.Initialize() func -func ParseAuthSettings(kv intset.KV) (as *AuthSettings, err error) { - as = &AuthSettings{} - as.ReadKV(kv) - return -} - -func (as *AuthSettings) ReadKV(kv intset.KV) (err error) { - as.FrontendUrlPasswordReset = kv.String("auth.frontend.url.password-reset") - as.FrontendUrlEmailConfirmation = kv.String("auth.frontend.url.email-confirmation") - as.FrontendUrlRedirect = kv.String("auth.frontend.url.redirect") - as.FrontendUrlBase = kv.String("auth.frontend.url.base") - - as.MailFromAddress = kv.String("auth.mail.from-address") - as.MailFromName = kv.String("auth.mail.from-name") - - // @todo translations - as.MailEmailConfirmationSubject = kv.String("auth.mail.email-confirmation.subject.en") - as.MailEmailConfirmationBody = kv.String("auth.mail.email-confirmation.body.en") - - as.MailPasswordResetSubject = kv.String("auth.mail.password-reset.subject.en") - as.MailPasswordResetBody = kv.String("auth.mail.password-reset.body.en") - - as.InternalEnabled = kv.Bool("auth.internal.enabled") - - as.InternalSignUpEnabled = kv.Bool("auth.internal.signup.enabled") - as.InternalSignUpEmailConfirmationRequired = kv.Bool("auth.internal.signup-email-confirmation-required") - - as.InternalPasswordResetEnabled = kv.Bool("auth.internal.password-reset.enabled") - - as.ExternalEnabled = kv.Bool("auth.external.enabled") - - as.ExternalRedirectUrl = kv.String("auth.external.redirect-url") - as.ExternalSessionStoreSecret = kv.String("auth.external.session-store-secret") - as.ExternalSessionStoreSecure = kv.Bool("auth.external.session-store-secure") - - as.ExternalProviders, err = as.parseExternalProviders(kv) - - return -} - -func (as *AuthSettings) parseExternalProviders(kv intset.KV) (map[string]AuthSettingsExternalAuthProvider, error) { - // Standard providers: - var ( - ep = map[string]AuthSettingsExternalAuthProvider{ - "github": {}, - "facebook": {}, - "google": {}, - "linkedin": {}, - } - - // Add all oidc providers we find - extKeyBase = "auth.external.providers." - oidcKeyBase = extKeyBase + "openid-connect." - ) - - for k := range kv.Filter(oidcKeyBase) { - if len(k) < len(oidcKeyBase)+2 { - // skip invalid keys - continue - } - - // find next dot: - name := k[len(oidcKeyBase):] - dotPos := strings.Index(name, ".") - if dotPos > 0 { - name = name[:dotPos] - } - - ep["openid-connect."+name] = AuthSettingsExternalAuthProvider{} - } - - for provider := range ep { - if p, err := as.parseExternalProvider(kv.Filter(extKeyBase + provider)); err != nil { - return nil, err - } else { - if as.ExternalRedirectUrl != "" && p.Enabled { - p.RedirectUrl = fmt.Sprintf(as.ExternalRedirectUrl, provider) - } - - ep[provider] = *p - } - - } - - return ep, nil -} - -// Parses external provider out of KV set -// -// Function only looks at the end of key string (after last dot) -// so passing multiple providers will result in overriding values -func (as *AuthSettings) parseExternalProvider(kv intset.KV) (p *AuthSettingsExternalAuthProvider, err error) { - p = &AuthSettingsExternalAuthProvider{} - - for k, v := range kv { - ld := strings.LastIndex(k, ".") - - switch k[ld+1:] { - case "enabled": - err = v.Unmarshal(&p.Enabled) - case "key": - err = v.Unmarshal(&p.Key) - case "secret": - err = v.Unmarshal(&p.Secret) - case "issuer": - err = v.Unmarshal(&p.IssuerUrl) - } - - if err != nil { - return - } - } - - return -} - -func (as AuthSettings) Format() map[string]interface{} { - type ( - externalProvider struct { - Label string `json:"label"` - Handle string `json:"handle"` - } - ) - - var ( - providers = []externalProvider{} - ) - - for p := range goth.GetProviders() { - - label := p - if strings.Index(p, "openid-connect.") == 0 { - label = strings.SplitN(p, ".", 2)[1] - } - - switch label { - case "corteza-iam", "corteza", "corteza-one": - label = "Corteza One" - case "crust-iam", "crust", "crust-unify": - label = "Crust Unify" - case "facebook": - label = "Facebook" - case "google": - label = "Google" - case "linkedin": - label = "LinkedIn" - case "github": - label = "GitHub" - } - - providers = append(providers, externalProvider{ - Label: label, - Handle: p, - }) - } - - return map[string]interface{}{ - "internalEnabled": as.InternalEnabled, - "internalPasswordResetEnabled": as.InternalPasswordResetEnabled, - "internalSignUpEmailConfirmationRequired": as.InternalSignUpEmailConfirmationRequired, - "internalSignUpEnabled": as.InternalSignUpEnabled, - - "externalEnabled": as.ExternalEnabled, - "externalProviders": providers, - } -} - -// StaticValidateExternal -// -// Simple checks of external auth settings -func (as AuthSettings) StaticValidateExternal() error { - if as.ExternalRedirectUrl == "" { - return errors.New("redirect URL is empty") - } - - const ( - tpt = "test-provider-test" - ) - p, err := url.Parse(fmt.Sprintf(as.ExternalRedirectUrl, tpt)) - if err != nil { - return errors.Wrap(err, "invalid redirect URL") - } - - if !strings.Contains(p.Path, tpt+"/callback") { - return errors.Wrap(err, "could find injected provider in the URL, make sure you use '%s' as a placeholder") - } - - if as.ExternalSessionStoreSecret == "" { - return errors.New("session store secret is empty") - } - - if as.ExternalSessionStoreSecure && p.Scheme != "https" { - return errors.New("session store is secure, redirect URL should have HTTPS") - } - - return nil -} - -// ValidateExternalRedirectURL -// -// Validates external redirect URL -func (as AuthSettings) ValidateExternalRedirectURL() error { - const tpt = "test-provider-test" - const cb = "/callback" - - // Replace placeholders & remove /callback - var url = fmt.Sprintf(as.ExternalRedirectUrl, tpt) - url = url[0 : len(url)-len(cb)] - - rsp, err := http.DefaultClient.Get(url) - if err != nil { - return errors.Wrap(err, "could not get response from redirect URL") - } - - defer rsp.Body.Close() - body, err := ioutil.ReadAll(rsp.Body) - - if strings.Contains(string(body), tpt) { - return nil - } - - return errors.New("could not validate external auth redirection URL") -} - -func (p AuthSettingsExternalAuthProvider) MakeValueSet(name string) (vv intset.ValueSet, err error) { - set := func(name string, value interface{}) error { - v := &intset.Value{Name: name} - if v.Value, err = json.Marshal(value); err != nil { - return err - } - - vv = append(vv, v) - return nil - } - - prefix := "auth.external.providers." + name - - if err = set(prefix+".enabled", p.Enabled); err != nil { - return nil, err - } - - if err = set(prefix+".key", p.Key); err != nil { - return nil, err - } - - if err = set(prefix+".secret", p.Secret); err != nil { - return nil, err - } - - if err = set(prefix+".issuer", p.IssuerUrl); err != nil { - return nil, err - } - - return vv, err -} diff --git a/system/service/auth_settings_test.go b/system/service/auth_settings_test.go deleted file mode 100644 index f7157721d..000000000 --- a/system/service/auth_settings_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package service - -import ( - "reflect" - "testing" - - "github.com/jmoiron/sqlx/types" - - intset "github.com/cortezaproject/corteza-server/pkg/settings" -) - -func Test_extractProviders(t *testing.T) { - type args struct { - redirectUrl string - kv intset.KV - } - tests := []struct { - name string - args args - wantProviders map[string]AuthSettingsExternalAuthProvider - wantErr bool - }{ - { - name: "Empty KV", - args: args{}, - wantProviders: map[string]AuthSettingsExternalAuthProvider{ - "github": AuthSettingsExternalAuthProvider{}, - "linkedin": AuthSettingsExternalAuthProvider{}, - "google": AuthSettingsExternalAuthProvider{}, - "facebook": AuthSettingsExternalAuthProvider{}, - }, - }, - - { - name: "Random config", - args: args{ - kv: intset.KV{ - "auth.external.redirect-url": types.JSONText(`"http://%s"`), - "auth.external.providers.openid-connect.foo.enabled": types.JSONText("true"), - "auth.external.providers.openid-connect.foo.issuer": types.JSONText(`"url"`), - "auth.external.providers.openid-connect.foo.key": types.JSONText(`"key"`), - "auth.external.providers.openid-connect.foo.secret": types.JSONText(`"secret"`), - "auth.external.providers.openid-connect.bar.enabled": types.JSONText("true"), - "auth.external.providers.openid-connect.bar.issuer": types.JSONText(`"url"`), - "auth.external.providers.openid-connect.bar.key": types.JSONText(`"key"`), - "auth.external.providers.openid-connect.bar.secret": types.JSONText(`"secret"`), - "auth.external.providers.openid-connect.baz.enabled": types.JSONText("false"), - "auth.external.providers.github.enabled": types.JSONText(`false`), - "auth.external.providers.facebook.enabled": types.JSONText(`true`), - "auth.external.providers.facebook.secret": types.JSONText(`"fb-secret"`), - }, - }, - - wantProviders: map[string]AuthSettingsExternalAuthProvider{ - "openid-connect.foo": AuthSettingsExternalAuthProvider{ - Enabled: true, - Key: "key", - Secret: "secret", - RedirectUrl: "http://openid-connect.foo", - IssuerUrl: "url", - }, - "openid-connect.bar": AuthSettingsExternalAuthProvider{ - Enabled: true, - Key: "key", - Secret: "secret", - RedirectUrl: "http://openid-connect.bar", - IssuerUrl: "url", - }, - "openid-connect.baz": AuthSettingsExternalAuthProvider{}, - "github": AuthSettingsExternalAuthProvider{}, - "linkedin": AuthSettingsExternalAuthProvider{}, - "google": AuthSettingsExternalAuthProvider{}, - "facebook": AuthSettingsExternalAuthProvider{ - Enabled: true, - Secret: "fb-secret", - RedirectUrl: "http://facebook", - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - as, err := ParseAuthSettings(tt.args.kv) - if (err != nil) != tt.wantErr { - t.Errorf("extractProviders() error = %v, wantErr %v", err, tt.wantErr) - return - } - - if !reflect.DeepEqual(as.ExternalProviders, tt.wantProviders) { - t.Errorf("extractProviders()\ngot: %v\nwant: %v\n", as.ExternalProviders, tt.wantProviders) - } - }) - } -} diff --git a/system/service/auth_test.go b/system/service/auth_test.go index a2a8fa11a..6f6f3d95e 100644 --- a/system/service/auth_test.go +++ b/system/service/auth_test.go @@ -34,7 +34,7 @@ func makeMockAuthService(u repository.UserRepository, c repository.CredentialsRe logger: zap.NewNop(), - settings: &AuthSettings{}, + settings: &types.Settings{}, now: func() *time.Time { return nil @@ -68,7 +68,7 @@ func TestAuth_External_Existing(t *testing.T) { usrRpoMock.EXPECT().FindByID(u.ID).Times(1).Return(u, nil) svc := makeMockAuthService(usrRpoMock, crdRpoMock) - svc.settings.ExternalEnabled = true + svc.settings.Auth.External.Enabled = true { auser, err := svc.External(p) @@ -113,7 +113,7 @@ func TestAuth_External_NonExisting(t *testing.T) { Return(uint(0)) svc := makeMockAuthService(usrRpoMock, crdRpoMock) - svc.settings.ExternalEnabled = true + svc.settings.Auth.External.Enabled = true { auser, err := svc.External(p) @@ -139,12 +139,12 @@ func Test_auth_validateInternalLogin(t *testing.T) { } svc := auth{ - logger: zap.NewNop(), - settings: &AuthSettings{ - InternalEnabled: true, - }, + logger: zap.NewNop(), + settings: &types.Settings{}, } + svc.settings.Auth.Internal.Enabled = true + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := svc.validateInternalLogin(tt.args.email, tt.args.password); (err != nil) != tt.wantErr { @@ -203,7 +203,7 @@ func Test_auth_checkPassword(t *testing.T) { svc := auth{ logger: zap.NewNop(), - settings: &AuthSettings{}, + settings: &types.Settings{}, } for _, tt := range tests { diff --git a/system/service/service.go b/system/service/service.go index c5893d1f2..6cf293189 100644 --- a/system/service/service.go +++ b/system/service/service.go @@ -80,8 +80,9 @@ var ( DefaultAutomationRunner automationRunner DefaultAuthNotification AuthNotificationService - DefaultAuthSettings *AuthSettings - DefaultSystemSettings *types.Settings + + // CurrentSettings represents current system settings + CurrentSettings = &types.Settings{} DefaultSink *sink @@ -103,7 +104,12 @@ func Init(ctx context.Context, log *zap.Logger, c Config) (err error) { DefaultAccessControl = AccessControl(DefaultPermissions) - DefaultSettings = Settings(ctx, DefaultIntSettings) + DefaultSettings = Settings(ctx, DefaultIntSettings, CurrentSettings) + + err = DefaultSettings.UpdateCurrent() + if err != nil { + return + } DefaultUser = User(ctx) DefaultRole = Role(ctx) @@ -111,17 +117,6 @@ func Init(ctx context.Context, log *zap.Logger, c Config) (err error) { DefaultApplication = Application(ctx) DefaultReminder = Reminder(ctx) - // Authentication helpers & services - DefaultAuthSettings, err = DefaultSettings.LoadAuthSettings() - if err != nil { - return - } - - DefaultSystemSettings, err = DefaultSettings.LoadSystemSettings() - if err != nil { - return - } - DefaultAuthNotification = AuthNotification(ctx) DefaultAuth = Auth(ctx) diff --git a/system/service/settings.go b/system/service/settings.go index 605cf0553..495380b07 100644 --- a/system/service/settings.go +++ b/system/service/settings.go @@ -24,6 +24,8 @@ type ( ac settingsAccessController internalSettings internalSettings.Service + + current *types.Settings } settingsAccessController interface { @@ -32,29 +34,27 @@ type ( } SettingsService interface { - With(ctx context.Context) SettingsService + With(ctx context.Context) *settings FindByPrefix(prefix string) (vv internalSettings.ValueSet, err error) Set(v *internalSettings.Value) (err error) BulkSet(vv internalSettings.ValueSet) (err error) Get(name string, ownedBy uint64) (out *internalSettings.Value, err error) - LoadAuthSettings() (*AuthSettings, error) - LoadSystemSettings() (*types.Settings, error) - UpdateAuthSettings(*AuthSettings) error - UpdateSystemSettings(*types.Settings) error + UpdateCurrent() error AutoDiscovery() error } ) -func Settings(ctx context.Context, intSet internalSettings.Service) SettingsService { +func Settings(ctx context.Context, intSet internalSettings.Service, current *types.Settings) *settings { return (&settings{ internalSettings: intSet, ac: DefaultAccessControl, logger: DefaultLogger.Named("settings"), + current: current, }).With(ctx) } -func (svc settings) With(ctx context.Context) SettingsService { +func (svc settings) With(ctx context.Context) *settings { db := repository.DB(ctx) return &settings{ @@ -64,6 +64,8 @@ func (svc settings) With(ctx context.Context) SettingsService { logger: svc.logger, internalSettings: svc.internalSettings.With(ctx), + + current: svc.current, } } @@ -79,12 +81,28 @@ func (svc settings) FindByPrefix(prefix string) (vv internalSettings.ValueSet, e return svc.internalSettings.FindByPrefix(prefix) } +// UpdateCurrent loads settings values from storage and updates current settings variable +// +// It accesses internal settings directly because +// we do not want any security checks for this +func (svc settings) UpdateCurrent() error { + if vv, err := svc.internalSettings.FindByPrefix(""); err != nil { + return err + } else { + return svc.updateCurrent(vv.KV()) + } +} + func (svc settings) Set(v *internalSettings.Value) (err error) { if !svc.ac.CanManageSettings(svc.ctx) { return errors.New("not allowed to manage settings") } - return svc.internalSettings.Set(v) + if err = svc.internalSettings.Set(v); err != nil { + return + } + + return svc.updateCurrent(internalSettings.KV{v.Name: v.Value}) } func (svc settings) BulkSet(vv internalSettings.ValueSet) (err error) { @@ -92,7 +110,34 @@ func (svc settings) BulkSet(vv internalSettings.ValueSet) (err error) { return errors.New("not allowed to manage settings") } - return svc.internalSettings.BulkSet(vv) + var old internalSettings.ValueSet + if old, err = svc.internalSettings.FindByPrefix(""); err != nil { + return + } else { + vv = old.Changed(vv) + } + + if err = svc.internalSettings.BulkSet(vv); err != nil { + return + } + + for _, v := range vv { + svc.log(svc.ctx, + zap.String("name", v.Name), + zap.Stringer("value", v.Value)).Info("settings changed") + } + + return svc.updateCurrent(vv.KV()) +} + +func (svc settings) updateCurrent(kv internalSettings.KV) (err error) { + // update current settings with new values + if err = kv.Decode(svc.current); err != nil { + return + } + + svc.log(svc.ctx).Info("current settings updated") + return } func (svc settings) Get(name string, ownedBy uint64) (out *internalSettings.Value, err error) { @@ -103,36 +148,6 @@ func (svc settings) Get(name string, ownedBy uint64) (out *internalSettings.Valu return svc.internalSettings.Get(name, ownedBy) } -// Loads auth.% settings, initializes & fills auth settings struct -func (svc settings) LoadAuthSettings() (*AuthSettings, error) { - as := &AuthSettings{} - return as, svc.UpdateAuthSettings(as) -} - -// Loads system.% settings, initializes & fills system settings struct -func (svc settings) LoadSystemSettings() (*types.Settings, error) { - s := &types.Settings{} - return s, svc.UpdateSystemSettings(s) -} - -func (svc settings) UpdateSystemSettings(s *types.Settings) error { - vv, err := svc.internalSettings.FindByPrefix("") - if err != nil { - return err - } - - return vv.KV().Decode(s) -} - -func (svc settings) UpdateAuthSettings(as *AuthSettings) error { - vv, err := svc.internalSettings.FindByPrefix("auth.") - if err != nil { - return err - } - - return as.ReadKV(vv.KV()) -} - // AutoDiscovery orchestrates settings auto discovery func (svc settings) AutoDiscovery() (err error) { var ( @@ -153,5 +168,10 @@ func (svc settings) AutoDiscovery() (err error) { return } - return svc.internalSettings.BulkSet(discovered) + err = svc.internalSettings.BulkSet(discovered) + if err != nil { + return + } + + return svc.updateCurrent(discovered.KV()) } diff --git a/system/service/settings_autodiscovery.go b/system/service/settings_autodiscovery.go index 9c68bb9a9..ac6fef84c 100644 --- a/system/service/settings_autodiscovery.go +++ b/system/service/settings_autodiscovery.go @@ -334,7 +334,7 @@ func authSettingsAutoDiscovery(log *zap.Logger, current internalSettings.ValueSe // Enable email confirmation if we have email capabilities { - "auth.internal.signup-email-confirmation-required", + "auth.internal.signup.email-confirmation-required", "PROVISION_SETTINGS_AUTH_INTERNAL_SIGNUP_EMAIL_CONFIRMATION_REQUIRED", emailCapabilities(), false}, diff --git a/system/service/user.go b/system/service/user.go index 1f842b536..8aad00d64 100644 --- a/system/service/user.go +++ b/system/service/user.go @@ -33,7 +33,7 @@ type ( ctx context.Context logger *zap.Logger - settings *AuthSettings + settings *types.Settings auth userAuth subscription userSubscriptionChecker @@ -115,7 +115,7 @@ func (svc user) With(ctx context.Context) UserService { logger: svc.logger, ac: DefaultAccessControl, - settings: DefaultAuthSettings, + settings: CurrentSettings, auth: DefaultAuth, subscription: CurrentSubscription, @@ -353,7 +353,7 @@ func (svc user) Unsuspend(ID uint64) (err error) { func (svc user) SetPassword(userID uint64, newPassword string) (err error) { log := svc.log(svc.ctx, zap.Uint64("userID", userID)) - if !svc.settings.InternalEnabled { + if !svc.settings.Auth.Internal.Enabled { return errors.New("internal authentication disabled") } diff --git a/system/system.go b/system/system.go index 2c1477acd..4180e0fb2 100644 --- a/system/system.go +++ b/system/system.go @@ -58,20 +58,13 @@ func Configure() *cli.Config { c.InitServices(ctx, c) if c.ProvisionOpt.Configuration { + // read system's config files (YAML) cli.HandleError(provisionConfig(ctx, cmd, c)) + cli.HandleError(makeDefaultApplications(ctx, cmd, c)) - cli.HandleError(settingsAutoDiscovery(ctx, cmd, c)) - - // Reload auto-configured settings - // adding externals and oidc auto discovery depends on redirect-url setting - cli.HandleError(service.DefaultSettings.UpdateAuthSettings(service.DefaultAuthSettings)) - cli.HandleError(authAddExternals(ctx, cmd, c)) cli.HandleError(oidcAutoDiscovery(ctx, cmd, c)) - - // Reload auto-configured settings - cli.HandleError(service.DefaultSettings.UpdateAuthSettings(service.DefaultAuthSettings)) } { diff --git a/system/types/settings.go b/system/types/settings.go index 2be352dea..51ba8db7b 100644 --- a/system/types/settings.go +++ b/system/types/settings.go @@ -1,6 +1,15 @@ package types +import ( + "strings" + + "github.com/pkg/errors" + + "github.com/cortezaproject/corteza-server/pkg/settings" +) + type ( + // Settings structured representation of current system settings Settings struct { General struct { Mail struct { @@ -9,5 +18,184 @@ type ( Footer string `kv:"footer.en"` } } + + Auth struct { + Internal struct { + // Is internal authentication (username + password) enabled + Enabled bool + + Signup struct { + // Can users register + Enabled bool + + // Users must confirm their emails when signing-up + EmailConfirmationRequired bool `kv:"email-confirmation-required"` + } + + // Can users reset their passwords + PasswordReset struct{ Enabled bool } `kv:"password-reset"` + } + + External struct { + // Is external authentication + Enabled bool + + // Where to redirect (url used for registration) + RedirectUrl string `kv:"redirect-url"` + + // session secret to use + SessionStoreSecret string `kv:"session-store-secret"` + + // session store should be secure + SessionStoreSecure bool `kv:"session-store-secure"` + + // all external providers we know + Providers ExternalAuthProviderSet + } + + Frontend struct { + Url struct { + // Password reset path ( "?token=" + ) + PasswordReset string `kv:"password-reset"` + + // EmailAddress confirmation path ( "?token=" + ) + EmailConfirmation string `kv:"email-confirmation"` + + // Where to redirect user after external auth flow + Redirect string + + // Webapp Base URL + Base string + } + } + + Mail struct { + FromAddress string `kv:"from-name"` + FromName string `kv:"from-address"` + + EmailConfirmation struct { + Subject string `kv:"subject.en"` + Body string `kv:"body.en"` + } `kv:"email-confirmation"` + + PasswordReset struct { + Subject string `kv:"subject.en"` + Body string `kv:"body.en"` + } `kv:"password-reset"` + } + } + } + + ExternalAuthProviderSet []*ExternalAuthProvider + + ExternalAuthProvider struct { + Enabled bool `json:"enabled"` + Handle string `json:"handle"` + Label string `json:"label"` + Key string `json:"-"` + Secret string `json:"-"` + RedirectUrl string `json:",omitempty" kv:"redirect"` + IssuerUrl string `json:",omitempty" kv:"issuer"` + Weight int `json:"-"` } ) + +// DecodeKV translates settings' KV into internal system external auth settings +func (set *ExternalAuthProviderSet) DecodeKV(kv settings.KV, prefix string) (err error) { + if *set == nil { + *set = ExternalAuthProviderSet{} + } + + // create standard provider set + providers := map[string]bool{"github": true, "facebook": true, "google": true, "linkedin": true} + + // remove prefix + kv = kv.CutPrefix(prefix + ".") + + // add all additional providers (prefixed with "openid-connect.") + oidcPrefix := "openid-connect." + for p := range kv { + if !strings.HasPrefix(p, oidcPrefix) { + continue + } + + l := len(oidcPrefix) + dotPos := strings.Index(p[l:], ".") + l + if dotPos > 0 { + providers[p[:dotPos]] = true + } + } + + // go over all added providers again add decode KV into each one + for handle := range providers { + p := (*set).FindByHandle(handle) + if p == nil { + p = &ExternalAuthProvider{Handle: handle} + (*set) = append((*set), p) + } + + err = settings.DecodeKV(kv.CutPrefix(handle+"."), p) + if err != nil { + return + } + + if p.Label == "" { + switch p.Handle { + case "corteza-iam", "corteza", "corteza-one": + p.Label = "Corteza One" + case "crust-iam", "crust", "crust-unify": + p.Label = "Crust Unify" + default: + strings.Title(p.Handle) + } + } + } + + return +} + +func (set ExternalAuthProviderSet) FindByHandle(handle string) *ExternalAuthProvider { + for p := range set { + if set[p].Handle == handle { + return set[p] + } + } + + return nil +} + +func (set ExternalAuthProviderSet) Len() int { return len(set) } +func (set ExternalAuthProviderSet) Swap(i, j int) { set[i], set[j] = set[j], set[i] } +func (set ExternalAuthProviderSet) Less(i, j int) bool { return set[i].Weight < set[j].Weight } + +var _ settings.KVDecoder = &ExternalAuthProviderSet{} + +func (p ExternalAuthProvider) EncodeKV() (vv settings.ValueSet, err error) { + if p.Handle == "" { + return nil, errors.New("can not encode external auth provider without handle") + } + var ( + prefix = "auth.external.providers." + p.Handle + "." + pairs = map[string]interface{}{ + "enabled": p.Enabled, + "label": p.Label, + "key": p.Key, + "secret": p.Secret, + "issuer": p.IssuerUrl, + "redirect": p.RedirectUrl, + "weight": p.Weight, + } + ) + + for key, value := range pairs { + v := &settings.Value{Name: prefix + key} + + if err = v.SetValue(value); err != nil { + return + } + + vv = append(vv, v) + } + + return +} diff --git a/system/types/settings_test.go b/system/types/settings_test.go new file mode 100644 index 000000000..ec57adb18 --- /dev/null +++ b/system/types/settings_test.go @@ -0,0 +1,71 @@ +package types + +import ( + "sort" + "testing" + + "github.com/jmoiron/sqlx/types" + "github.com/stretchr/testify/require" + + "github.com/cortezaproject/corteza-server/pkg/settings" +) + +// Hello! This file is auto-generated. + +func Test_settingsExtAuthProvidersDecode(t *testing.T) { + type ( + Dst struct { + Providers ExternalAuthProviderSet + } + ) + + var ( + aux = Dst{} + kv = settings.KV{ + "providers.foo.enabled": types.JSONText(`true`), + "providers.openid-connect.bar.enabled": types.JSONText(`true`), + "providers.openid-connect.bar.key": types.JSONText(`"K3Y"`), + "providers.google.enabled": types.JSONText(`true`), + "providers.google.key": types.JSONText(`"g00gl3"`), + } + + eq = Dst{ + Providers: ExternalAuthProviderSet{ + {Handle: "github"}, + {Handle: "facebook"}, + {Enabled: true, Key: "g00gl3", Handle: "google"}, + {Handle: "linkedin"}, + {Enabled: true, Key: "K3Y", Handle: "openid-connect.bar"}, + }, + } + ) + + sort.Sort(eq.Providers) + + require.NoError(t, settings.DecodeKV(kv, &aux)) + require.Len(t, aux.Providers, 5) + + require.Nil(t, + aux.Providers.FindByHandle("foo")) + + require.Equal(t, + aux.Providers.FindByHandle("openid-connect.bar"), + &ExternalAuthProvider{Enabled: true, Key: "K3Y", Handle: "openid-connect.bar"}) + + require.Equal(t, + aux.Providers.FindByHandle("google"), + &ExternalAuthProvider{Enabled: true, Key: "g00gl3", Handle: "google"}) + + require.Equal(t, + aux.Providers.FindByHandle("linkedin"), + &ExternalAuthProvider{Handle: "linkedin"}) + + require.Equal(t, + aux.Providers.FindByHandle("github"), + &ExternalAuthProvider{Handle: "github"}) + + require.Equal(t, + aux.Providers.FindByHandle("facebook"), + &ExternalAuthProvider{Handle: "facebook"}) + +}