369 lines
8.9 KiB
Go
369 lines
8.9 KiB
Go
package system
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/spf13/cast"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/cortezaproject/corteza-server/pkg/logger"
|
|
"github.com/cortezaproject/corteza-server/pkg/rand"
|
|
settings "github.com/cortezaproject/corteza-server/pkg/settings"
|
|
"github.com/cortezaproject/corteza-server/system/service"
|
|
)
|
|
|
|
var (
|
|
IsMonolith bool
|
|
)
|
|
|
|
// 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(ctx context.Context, log *zap.Logger, svc settings.Service) (err error) {
|
|
type (
|
|
stringWrapper func() string
|
|
boolWrapper func() bool
|
|
)
|
|
|
|
var (
|
|
current settings.ValueSet
|
|
)
|
|
|
|
if log == nil {
|
|
log = zap.NewNop()
|
|
}
|
|
|
|
log = service.DefaultLogger.Named("auth-settings-discovery")
|
|
|
|
current, err = svc.FindByPrefix(ctx, "auth.")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
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
|
|
//
|
|
// @todo generalize and move under settings
|
|
set = func(name string, env string, def interface{}, maskSensitive bool) {
|
|
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", logger.MaskIf("value", v, maskSensitive))
|
|
return
|
|
}
|
|
|
|
v = &settings.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), logger.MaskIf("value", value, maskSensitive))
|
|
} 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 IsMonolith {
|
|
path = "/system" + 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{}
|
|
|
|
// mask value if sensitive
|
|
mask bool
|
|
}{
|
|
// // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // //
|
|
// External auth
|
|
|
|
// Enable external auth
|
|
{
|
|
"auth.external.enabled",
|
|
"PROVISION_SETTINGS_AUTH_EXTERNAL_ENABLED",
|
|
wrapBool(true),
|
|
false},
|
|
|
|
{
|
|
"auth.external.redirect-url",
|
|
"PROVISION_SETTINGS_AUTH_EXTERNAL_REDIRECT_URL",
|
|
externalAuthRedirectUrl(),
|
|
false},
|
|
|
|
{
|
|
"auth.external.session-store-secret",
|
|
"PROVISION_SETTINGS_AUTH_EXTERNAL_SESSION_STORE_SECRET",
|
|
rand,
|
|
true},
|
|
|
|
// Disable external auth
|
|
{
|
|
"auth.external.session-store-secure",
|
|
"PROVISION_SETTINGS_AUTH_EXTERNAL_SESSION_STORE_SECURE",
|
|
isSecure(),
|
|
false},
|
|
|
|
// // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // //
|
|
// Auth frontend
|
|
|
|
{
|
|
"auth.frontend.url.base",
|
|
"PROVISION_SETTINGS_AUTH_FRONTEND_URL_BASE",
|
|
frontendUrl("/"),
|
|
false},
|
|
|
|
// @todo w/o token=
|
|
{
|
|
"auth.frontend.url.password-reset",
|
|
"PROVISION_SETTINGS_AUTH_FRONTEND_URL_PASSWORD_RESET",
|
|
frontendUrl("/auth/reset-password?token="),
|
|
false},
|
|
|
|
// @todo w/o token=
|
|
{
|
|
"auth.frontend.url.email-confirmation",
|
|
"PROVISION_SETTINGS_AUTH_FRONTEND_URL_EMAIL_CONFIRMATION",
|
|
frontendUrl("/auth/confirm-email?token="),
|
|
false},
|
|
|
|
// @todo check if this is correct?!
|
|
{
|
|
"auth.frontend.url.redirect",
|
|
"PROVISION_SETTINGS_AUTH_FRONTEND_URL_REDIRECT",
|
|
frontendUrl("/auth"),
|
|
false},
|
|
|
|
// Auth email
|
|
{
|
|
"auth.mail.from-address",
|
|
"PROVISION_SETTINGS_AUTH_EMAIL_FROM_ADDRESS",
|
|
wrapString("info@example.tld"),
|
|
false},
|
|
|
|
{
|
|
"auth.mail.from-name",
|
|
"PROVISION_SETTINGS_AUTH_EMAIL_FROM_NAME",
|
|
wrapString("Example Sender"),
|
|
false},
|
|
|
|
// // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // // //
|
|
// Enable internal login
|
|
{
|
|
"auth.internal.enabled",
|
|
"PROVISION_SETTINGS_AUTH_INTERNAL_ENABLED",
|
|
wrapBool(true),
|
|
false},
|
|
|
|
// Enable internal signup
|
|
{
|
|
"auth.internal.signup.enabled",
|
|
"PROVISION_SETTINGS_AUTH_INTERNAL_SIGNUP_ENABLED",
|
|
wrapBool(true),
|
|
false},
|
|
|
|
// Enable email confirmation if we have email capabilities
|
|
{
|
|
"auth.internal.signup.email-confirmation-required",
|
|
"PROVISION_SETTINGS_AUTH_INTERNAL_SIGNUP_EMAIL_CONFIRMATION_REQUIRED",
|
|
emailCapabilities(),
|
|
false},
|
|
|
|
// Enable password reset if we have email capabilities
|
|
{
|
|
"auth.internal.password-reset.enabled",
|
|
"PROVISION_SETTINGS_AUTH_INTERNAL_PASSWORD_RESET_ENABLED",
|
|
emailCapabilities(),
|
|
false},
|
|
}
|
|
|
|
for _, item := range list {
|
|
set(item.nme, item.env, item.def, item.mask)
|
|
}
|
|
|
|
return svc.BulkSet(ctx, new)
|
|
}
|