From de26f15c8e6097d0f7e9bea4ae1aeb5e86cdb46f Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Mon, 15 Jul 2019 11:57:14 +0200 Subject: [PATCH] Refactor auth settings auto-discovery to new file --- system/autosetup.go | 2 +- system/internal/service/settings.go | 328 +---------------- .../service/settings_autodiscovery.go | 332 ++++++++++++++++++ 3 files changed, 337 insertions(+), 325 deletions(-) create mode 100644 system/internal/service/settings_autodiscovery.go diff --git a/system/autosetup.go b/system/autosetup.go index c1cbefc39..fcff004df 100644 --- a/system/autosetup.go +++ b/system/autosetup.go @@ -75,5 +75,5 @@ func makeDefaultApplications(ctx context.Context, cmd *cobra.Command, c *cli.Con } func discoverSettings(ctx context.Context, cmd *cobra.Command, c *cli.Config) error { - return service.DefaultSettings.With(ctx).Discover() + return service.DefaultSettings.With(ctx).AutoDiscovery() } diff --git a/system/internal/service/settings.go b/system/internal/service/settings.go index ad3834598..d869ad9f7 100644 --- a/system/internal/service/settings.go +++ b/system/internal/service/settings.go @@ -2,20 +2,13 @@ package service import ( "context" - "fmt" - "net/url" - "os" - "strings" _ "github.com/joho/godotenv/autoload" "github.com/pkg/errors" - "github.com/spf13/cast" "github.com/titpetric/factory" "go.uber.org/zap" - "github.com/cortezaproject/corteza-server/internal/rand" internalSettings "github.com/cortezaproject/corteza-server/internal/settings" - "github.com/cortezaproject/corteza-server/pkg/api" "github.com/cortezaproject/corteza-server/system/internal/repository" ) @@ -43,7 +36,7 @@ type ( Get(name string, ownedBy uint64) (out *internalSettings.Value, err error) LoadAuthSettings() (authSettings, error) - Discover() error + AutoDiscovery() error } ) @@ -114,7 +107,8 @@ func (svc settings) LoadAuthSettings() (authSettings, error) { return AuthSettings(vv.KV()), nil } -func (svc settings) Discover() (err error) { +// AutoDiscovery orchestrates settings auto discovery +func (svc settings) AutoDiscovery() (err error) { var ( current, discovered internalSettings.ValueSet ) @@ -124,324 +118,10 @@ func (svc settings) Discover() (err error) { return } - discovered, err = svc.discover(current) + discovered, err = authSettingsAutoDiscovery(svc.logger, current) if err != nil || len(discovered) == 0 { return } return svc.internalSettings.BulkSet(discovered) } - -// Discovers "auth.%" settings from the environment -// -// This could (should) probably be refactored into something more general. -func (svc settings) discover(current internalSettings.ValueSet) (internalSettings.ValueSet, error) { - - type ( - stringWrapper func() string - boolWrapper func() bool - ) - - var ( - new = current - - log = svc.logger.Named("discovery") - - // Setter - // - // Finds existing settings, tries with environmental "PROVISION_SETTINGS_AUTH_..." probing - // and falls back to default value - // - // We are extremely verbose here - we want to show all the info available and - // how settings were discovered and set - set = func(name string, env string, def interface{}) { - var ( - log = log.With( - zap.String("name", name), - ) - - v = current.First(name) - value interface{} - ) - - if v != nil { - // Nothing to discover, already set - log.Info("already set", zap.Any("value", v.String())) - return - } - - v = &internalSettings.Value{Name: name} - - value, envExists := os.LookupEnv(env) - - switch dfn := def.(type) { - case stringWrapper: - log = log.With(zap.String("type", "string")) - // already a string, no need to do any magic - if envExists { - log = log.With(zap.String("env", env), zap.Any("value", value)) - } else { - value = dfn() - log = log.With(zap.Any("default", value)) - } - case boolWrapper: - log = log.With(zap.String("type", "bool")) - - if envExists { - value = cast.ToBool(value) - log = log.With(zap.String("env", env), zap.Any("value", value)) - } else { - value = dfn() - log = log.With(zap.Any("default", value)) - } - - default: - log.Error("unsupported type") - return - } - - if err := v.SetValue(value); err != nil { - log.Error("could not set value", zap.Error(err)) - return - } - - log.Info("value auto-discovered") - - new.Replace(v) - } - - // Default value functions - // - // all are wrapped (stringWrapper, boolWrapper) to delay execution - // of the function to the very last point - - frontendUrl = func(path string) stringWrapper { - const ( - feBase = "auth.frontend.url.base" - extRedir = "auth.external.redirect-url" - ) - - return func() (base string) { - base = new.First(feBase).String() - - if len(base) == 0 { - - // Not found, try to get it from the external redirect URL - redirURL := new.First(extRedir).String() - if len(redirURL) == 0 { - return - } - - log.Info( - "discovering frontend url from '"+extRedir+"'", - zap.String(extRedir, redirURL)) - - // Removing placeholder - redirURL = fmt.Sprintf(redirURL, "") - - p, err := url.Parse(redirURL) - if err != nil { - log.Error("could not parse '"+extRedir+"'", zap.Error(err)) - return - } - - h := p.Host - s := "api." - if i := strings.Index(h, s); i > 0 { - // If there is a "api." prefix in the hostname of the external redirect-uri value - // cut it off and use that as a frontend url base - h = h[i+len(s):] - } - - base = p.Scheme + "://" + h - } - - if len(base) > 0 { - return strings.TrimRight(base, "/") + path - } - - return "" - } - } - - // Assuming secure backend when redirect URL starts with https:// - isSecure = func() boolWrapper { - return func() bool { - return strings.Index(new.First("auth.external.redirect-url").String(), "https://") == 0 - } - } - - // Assume we have emailing capabilities if SMTP_HOST variable is set - emailCapabilities = func() boolWrapper { - return func() bool { - val, has := os.LookupEnv("SMTP_HOST") - return has && len(val) > 0 - } - } - - // Where should external authentication providers redirect to? - // we need to set full, absolute URL to the callback endpoint - externalAuthRedirectUrl = func() stringWrapper { - return func() string { - var ( - path = "/auth/external/%s/callback" - - // All env keys we'll check, first that has any value set, will be used as hostname - keysWithHostnames = []string{ - "DOMAIN", - "LETSENCRYPT_HOST", - "VIRTUAL_HOST", - "HOSTNAME", - "HOST", - } - ) - - // Prefix path if we're running wrapped as a monolith: - if api.Monolith { - path = "/system" + path - } - - // Finally, add any prefix - path = strings.TrimRight(api.BaseURL, "/") + path - - log.Info("scanning env variables for hostname", zap.Strings("candidates", keysWithHostnames)) - - for _, key := range keysWithHostnames { - if host, has := os.LookupEnv(key); has { - log.Info("hostname env variable found", zap.String("env", key)) - // Make life easier for development in local environment, - // and set HTTP schema. Might cause problems if someone - // is using valid external hostname - if strings.Contains(host, "local.") { - return "http://" + host + path - } else { - return "https://" + host + path - } - } else { - } - } - - // Fallback is empty string - // this will cause error when doing OIDC auto-discovery (and we want that) - // @todo ^^ - return "" - } - } - - rand stringWrapper = func() string { - return string(rand.Bytes(64)) - } - - wrapBool = func(val bool) boolWrapper { - return func() bool { return val } - } - - wrapString = func(val string) stringWrapper { - return func() string { return val } - } - ) - - // List of name-value pairs we need to iterate and set - list := []struct { - // Setting name - nme string - - // provision environmental variable name - // we're using full variable name here so developers - // can find where things are comming from - env string - - // default value - // expects one of the *wrapper() functions - // this also determinate the value type of the setting and casting rules for the env value - def interface{} - }{ - // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // - // External auth - - // Enable external auth - { - "auth.external.enabled", - "PROVISION_SETTINGS_AUTH_EXTERNAL_ENABLED", - wrapBool(true)}, - - { - "auth.external.redirect-url", - "PROVISION_SETTINGS_AUTH_EXTERNAL_REDIRECT_URL", - externalAuthRedirectUrl()}, - - { - "auth.external.session-store-secret", - "PROVISION_SETTINGS_AUTH_EXTERNAL_SESSION_STORE_SECRET", - rand}, - - // Disable external auth - { - "auth.external.session-store-secure", - "PROVISION_SETTINGS_AUTH_EXTERNAL_SESSION_STORE_SECURE", - isSecure()}, - - // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // - // Auth frontend - - { - "auth.frontend.url.base", - "PROVISION_SETTINGS_AUTH_FRONTEND_URL_BASE", - frontendUrl("/")}, - - // @todo w/o token= - { - "auth.frontend.url.password-reset", - "PROVISION_SETTINGS_AUTH_FRONTEND_URL_PASSWORD_RESET", - frontendUrl("/auth/reset-password?token=")}, - - // @todo w/o token= - { - "auth.frontend.url.email-confirmation", - "PROVISION_SETTINGS_AUTH_FRONTEND_URL_EMAIL_CONFIRMATION", - frontendUrl("/auth/confirm-email?token=")}, - - // @todo check if this is correct?! - { - "auth.frontend.url.redirect", - "PROVISION_SETTINGS_AUTH_FRONTEND_URL_REDIRECT", - frontendUrl("/auth")}, - - // Auth email - { - "auth.mail.from-address", - "PROVISION_SETTINGS_AUTH_EMAIL_FROM_ADDRESS", - wrapString("to-be-configured@example.tld")}, - - { - "auth.mail.from-name", - "PROVISION_SETTINGS_AUTH_EMAIL_FROM_NAME", - wrapString("Corteza Team (to-be-configured)")}, - - // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // - // Enable internal signup - { - "auth.internal.signup.enabled", - "PROVISION_SETTINGS_AUTH_INTERNAL_SIGNUP_ENABLED", - wrapBool(true)}, - - // Enable email confirmation if we have email capabilities - { - "auth.internal.signup-email-confirmation-required", - "PROVISION_SETTINGS_AUTH_INTERNAL_SIGNUP_EMAIL_CONFIRMATION_REQUIRED", - emailCapabilities()}, - - // Enable password reset if we have email capabilities - { - "auth.internal.password-reset.enabled", - "PROVISION_SETTINGS_AUTH_INTERNAL_PASSWORD_RESET_ENABLED", - emailCapabilities()}, - } - - for _, item := range list { - set(item.nme, item.env, item.def) - } - - // return new, nil - return current.Changed(new), nil -} diff --git a/system/internal/service/settings_autodiscovery.go b/system/internal/service/settings_autodiscovery.go new file mode 100644 index 000000000..125d899cd --- /dev/null +++ b/system/internal/service/settings_autodiscovery.go @@ -0,0 +1,332 @@ +package service + +import ( + "fmt" + "net/url" + "os" + "strings" + + "github.com/spf13/cast" + "go.uber.org/zap" + + "github.com/cortezaproject/corteza-server/internal/rand" + internalSettings "github.com/cortezaproject/corteza-server/internal/settings" + "github.com/cortezaproject/corteza-server/pkg/api" +) + +// Discovers "auth.%" settings from the environment +// +// when other kinds of auto-discoverable settings come, lambdas inside will probably need a bit of refactoring +func authSettingsAutoDiscovery(log *zap.Logger, current internalSettings.ValueSet) (internalSettings.ValueSet, error) { + type ( + stringWrapper func() string + boolWrapper func() bool + ) + + if log == nil { + log = zap.NewNop() + } + + log = log.Named("discovery") + + var ( + new = current + + // Setter + // + // Finds existing settings, tries with environmental "PROVISION_SETTINGS_AUTH_..." probing + // and falls back to default value + // + // We are extremely verbose here - we want to show all the info available and + // how settings were discovered and set + set = func(name string, env string, def interface{}) { + var ( + log = log.With( + zap.String("name", name), + ) + + v = current.First(name) + value interface{} + ) + + if v != nil { + // Nothing to discover, already set + log.Info("already set", zap.Any("value", v.String())) + return + } + + v = &internalSettings.Value{Name: name} + + value, envExists := os.LookupEnv(env) + + switch dfn := def.(type) { + case stringWrapper: + log = log.With(zap.String("type", "string")) + // already a string, no need to do any magic + if envExists { + log = log.With(zap.String("env", env), zap.Any("value", value)) + } else { + value = dfn() + log = log.With(zap.Any("default", value)) + } + case boolWrapper: + log = log.With(zap.String("type", "bool")) + + if envExists { + value = cast.ToBool(value) + log = log.With(zap.String("env", env), zap.Any("value", value)) + } else { + value = dfn() + log = log.With(zap.Any("default", value)) + } + + default: + log.Error("unsupported type") + return + } + + if err := v.SetValue(value); err != nil { + log.Error("could not set value", zap.Error(err)) + return + } + + log.Info("value auto-discovered") + + new.Replace(v) + } + + // Default value functions + // + // all are wrapped (stringWrapper, boolWrapper) to delay execution + // of the function to the very last point + + frontendUrl = func(path string) stringWrapper { + const ( + feBase = "auth.frontend.url.base" + extRedir = "auth.external.redirect-url" + ) + + return func() (base string) { + base = new.First(feBase).String() + + if len(base) == 0 { + + // Not found, try to get it from the external redirect URL + redirURL := new.First(extRedir).String() + if len(redirURL) == 0 { + return + } + + log.Info( + "discovering frontend url from '"+extRedir+"'", + zap.String(extRedir, redirURL)) + + // Removing placeholder + redirURL = fmt.Sprintf(redirURL, "") + + p, err := url.Parse(redirURL) + if err != nil { + log.Error("could not parse '"+extRedir+"'", zap.Error(err)) + return + } + + h := p.Host + s := "api." + if i := strings.Index(h, s); i > 0 { + // If there is a "api." prefix in the hostname of the external redirect-uri value + // cut it off and use that as a frontend url base + h = h[i+len(s):] + } + + base = p.Scheme + "://" + h + } + + if len(base) > 0 { + return strings.TrimRight(base, "/") + path + } + + return "" + } + } + + // Assuming secure backend when redirect URL starts with https:// + isSecure = func() boolWrapper { + return func() bool { + return strings.Index(new.First("auth.external.redirect-url").String(), "https://") == 0 + } + } + + // Assume we have emailing capabilities if SMTP_HOST variable is set + emailCapabilities = func() boolWrapper { + return func() bool { + val, has := os.LookupEnv("SMTP_HOST") + return has && len(val) > 0 + } + } + + // Where should external authentication providers redirect to? + // we need to set full, absolute URL to the callback endpoint + externalAuthRedirectUrl = func() stringWrapper { + return func() string { + var ( + path = "/auth/external/%s/callback" + + // All env keys we'll check, first that has any value set, will be used as hostname + keysWithHostnames = []string{ + "DOMAIN", + "LETSENCRYPT_HOST", + "VIRTUAL_HOST", + "HOSTNAME", + "HOST", + } + ) + + // Prefix path if we're running wrapped as a monolith: + if api.Monolith { + path = "/system" + path + } + + // Finally, add any prefix + path = strings.TrimRight(api.BaseURL, "/") + path + + log.Info("scanning env variables for hostname", zap.Strings("candidates", keysWithHostnames)) + + for _, key := range keysWithHostnames { + if host, has := os.LookupEnv(key); has { + log.Info("hostname env variable found", zap.String("env", key)) + // Make life easier for development in local environment, + // and set HTTP schema. Might cause problems if someone + // is using valid external hostname + if strings.Contains(host, "local.") { + return "http://" + host + path + } else { + return "https://" + host + path + } + } else { + } + } + + // Fallback is empty string + // this will cause error when doing OIDC auto-discovery (and we want that) + // @todo ^^ + return "" + } + } + + rand stringWrapper = func() string { + return string(rand.Bytes(64)) + } + + wrapBool = func(val bool) boolWrapper { + return func() bool { return val } + } + + wrapString = func(val string) stringWrapper { + return func() string { return val } + } + ) + + // List of name-value pairs we need to iterate and set + list := []struct { + // Setting name + nme string + + // provision environmental variable name + // we're using full variable name here so developers + // can find where things are comming from + env string + + // default value + // expects one of the *wrapper() functions + // this also determinate the value type of the setting and casting rules for the env value + def interface{} + }{ + // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // + // External auth + + // Enable external auth + { + "auth.external.enabled", + "PROVISION_SETTINGS_AUTH_EXTERNAL_ENABLED", + wrapBool(true)}, + + { + "auth.external.redirect-url", + "PROVISION_SETTINGS_AUTH_EXTERNAL_REDIRECT_URL", + externalAuthRedirectUrl()}, + + { + "auth.external.session-store-secret", + "PROVISION_SETTINGS_AUTH_EXTERNAL_SESSION_STORE_SECRET", + rand}, + + // Disable external auth + { + "auth.external.session-store-secure", + "PROVISION_SETTINGS_AUTH_EXTERNAL_SESSION_STORE_SECURE", + isSecure()}, + + // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // + // Auth frontend + + { + "auth.frontend.url.base", + "PROVISION_SETTINGS_AUTH_FRONTEND_URL_BASE", + frontendUrl("/")}, + + // @todo w/o token= + { + "auth.frontend.url.password-reset", + "PROVISION_SETTINGS_AUTH_FRONTEND_URL_PASSWORD_RESET", + frontendUrl("/auth/reset-password?token=")}, + + // @todo w/o token= + { + "auth.frontend.url.email-confirmation", + "PROVISION_SETTINGS_AUTH_FRONTEND_URL_EMAIL_CONFIRMATION", + frontendUrl("/auth/confirm-email?token=")}, + + // @todo check if this is correct?! + { + "auth.frontend.url.redirect", + "PROVISION_SETTINGS_AUTH_FRONTEND_URL_REDIRECT", + frontendUrl("/auth")}, + + // Auth email + { + "auth.mail.from-address", + "PROVISION_SETTINGS_AUTH_EMAIL_FROM_ADDRESS", + wrapString("to-be-configured@example.tld")}, + + { + "auth.mail.from-name", + "PROVISION_SETTINGS_AUTH_EMAIL_FROM_NAME", + wrapString("Corteza Team (to-be-configured)")}, + + // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // + // Enable internal signup + { + "auth.internal.signup.enabled", + "PROVISION_SETTINGS_AUTH_INTERNAL_SIGNUP_ENABLED", + wrapBool(true)}, + + // Enable email confirmation if we have email capabilities + { + "auth.internal.signup-email-confirmation-required", + "PROVISION_SETTINGS_AUTH_INTERNAL_SIGNUP_EMAIL_CONFIRMATION_REQUIRED", + emailCapabilities()}, + + // Enable password reset if we have email capabilities + { + "auth.internal.password-reset.enabled", + "PROVISION_SETTINGS_AUTH_INTERNAL_PASSWORD_RESET_ENABLED", + emailCapabilities()}, + } + + for _, item := range list { + set(item.nme, item.env, item.def) + } + + // return new, nil + return current.Changed(new), nil +}