Refactor (auth) settings, enable update w/o restart

This commit is contained in:
Denis Arh
2019-10-30 12:58:17 +01:00
parent 63bfb18259
commit 05f0647804
21 changed files with 613 additions and 640 deletions
+59 -48
View File
@@ -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
}
}
}
+39 -1
View File
@@ -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)
}
+3 -3
View File
@@ -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
}
+11 -3
View File
@@ -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())
}
+1 -1
View File
@@ -12,7 +12,7 @@ const (
)
func Init() {
setupGoth(service.DefaultAuthSettings)
setupGoth(service.CurrentSettings)
}
func log() *zap.Logger {
+13 -13
View File
@@ -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":
+78 -17
View File
@@ -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")
}
+14 -8
View File
@@ -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 (<name> <provider> ....)
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
+19 -5
View File
@@ -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) {
+18 -18
View File
@@ -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")
}
+16 -19
View File
@@ -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
-334
View File
@@ -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 (<frontend password reset url> "?token=" + <token>)
FrontendUrlPasswordReset string
// EmailAddress confirmation path (<frontend email confirmation url> "?token=" + <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
}
-95
View File
@@ -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)
}
})
}
}
+8 -8
View File
@@ -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 {
+9 -14
View File
@@ -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)
+60 -40
View File
@@ -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())
}
+1 -1
View File
@@ -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},
+3 -3
View File
@@ -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")
}
+2 -9
View File
@@ -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))
}
{
+188
View File
@@ -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 (<frontend password reset url> "?token=" + <token>)
PasswordReset string `kv:"password-reset"`
// EmailAddress confirmation path (<frontend email confirmation url> "?token=" + <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
}
+71
View File
@@ -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"})
}