Refactor (auth) settings, enable update w/o restart
This commit is contained in:
+59
-48
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -12,7 +12,7 @@ const (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
setupGoth(service.DefaultAuthSettings)
|
||||
setupGoth(service.CurrentSettings)
|
||||
}
|
||||
|
||||
func log() *zap.Logger {
|
||||
|
||||
Vendored
+13
-13
@@ -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":
|
||||
|
||||
Vendored
+78
-17
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"})
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user