diff --git a/client/web/admin/src/components/Settings/UI/CUIBrandingEditor.vue b/client/web/admin/src/components/Settings/UI/CUIBrandingEditor.vue index 5aaf0f440..ef36aee48 100644 --- a/client/web/admin/src/components/Settings/UI/CUIBrandingEditor.vue +++ b/client/web/admin/src/components/Settings/UI/CUIBrandingEditor.vue @@ -192,12 +192,10 @@ export default { this.themes = [ { id: 'light', - title: this.$t('light'), values: this.lightModeVariables, }, { id: 'dark', - title: this.$t('dark'), values: this.darkModeVariables, }, ] diff --git a/lib/js/src/api-clients/system.ts b/lib/js/src/api-clients/system.ts index 689c8f9b6..8c5b39380 100644 --- a/lib/js/src/api-clients/system.ts +++ b/lib/js/src/api-clients/system.ts @@ -1398,6 +1398,7 @@ export default class System { handle, kind, labels, + meta, } = (a as KV) || {} if (!email) { throw Error('field email is empty') @@ -1413,6 +1414,7 @@ export default class System { handle, kind, labels, + meta, } return this.api().request(cfg).then(result => stdResolve(result)) } diff --git a/server/app/boot_levels.go b/server/app/boot_levels.go index 3d35fb9cd..172e27a0b 100644 --- a/server/app/boot_levels.go +++ b/server/app/boot_levels.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "fmt" + "github.com/cortezaproject/corteza/server/pkg/sass" "net/url" "os" "regexp" @@ -34,7 +35,6 @@ import ( "github.com/cortezaproject/corteza/server/pkg/options" "github.com/cortezaproject/corteza/server/pkg/provision" "github.com/cortezaproject/corteza/server/pkg/rbac" - "github.com/cortezaproject/corteza/server/pkg/sass" "github.com/cortezaproject/corteza/server/pkg/scheduler" "github.com/cortezaproject/corteza/server/pkg/sentry" "github.com/cortezaproject/corteza/server/pkg/valuestore" @@ -562,7 +562,7 @@ func (app *CortezaApp) Activate(ctx context.Context) (err error) { app.AuthService.Watch(ctx) //Generate CSS for webapps - if err = service.GenerateCSS(sysService.CurrentSettings, app.Opt.Webapp.ScssDirPath); err != nil { + if err = service.GenerateCSS(sysService.CurrentSettings, app.Opt.Webapp.ScssDirPath, app.Log); err != nil { return fmt.Errorf("could not generate css for webapps: %w", err) } @@ -967,11 +967,7 @@ func updateSmtpSettings(log *zap.Logger, current *types.AppSettings) { } func updateSassInstallSettings(ctx context.Context, log *zap.Logger) { - var sassInstalled bool - - if sass.DartSassTranspiler(log) != nil { - sassInstalled = true - } + sassInstalled := sass.DartSassTranspiler(log) != nil // update dart-sass installed setting err := updateSetting(ctx, "ui.studio.sass-installed", sassInstalled) diff --git a/server/pkg/provision/stylesheet.go b/server/pkg/provision/stylesheet.go index 166046aaf..5ac704f5f 100644 --- a/server/pkg/provision/stylesheet.go +++ b/server/pkg/provision/stylesheet.go @@ -3,13 +3,15 @@ package provision import ( "context" "encoding/json" + "github.com/cortezaproject/corteza/server/pkg/sass" "github.com/cortezaproject/corteza/server/store" "github.com/cortezaproject/corteza/server/system/types" "go.uber.org/zap" "strconv" - "unicode" ) +// updateWebappTheme is a function that provisions webapp themes. +// It migrates the old custom css and branding sass settings to the new webapp themes setting. func updateWebappTheme(ctx context.Context, log *zap.Logger, s store.Storer) (err error) { vv, _, err := store.SearchSettingValues(ctx, s, types.SettingsFilter{}) if err != nil { @@ -27,31 +29,25 @@ func updateWebappTheme(ctx context.Context, log *zap.Logger, s store.Storer) (er var themes []types.Theme for _, themeID := range themeIDs { - title := []rune(themeID) - title[0] = unicode.ToUpper(title[0]) - - if len(themeIDs) > 2 { - if themeID == "general" { - themes = append(themes, types.Theme{ - ID: themeID, - Title: string(title), - Values: oldValueStr, - }) - continue - } - + if len(themeIDs) == 2 { themes = append(themes, types.Theme{ ID: themeID, - Title: string(title), - Values: "", + Values: oldValueStr, + }) + continue + } + + if themeID == sass.GeneralTheme { + themes = append(themes, types.Theme{ + ID: themeID, + Values: oldValueStr, }) continue } themes = append(themes, types.Theme{ ID: themeID, - Title: string(title), - Values: oldValueStr, + Values: "", }) } @@ -71,7 +67,7 @@ func updateWebappTheme(ctx context.Context, log *zap.Logger, s store.Storer) (er return err } - // delete old custom css from the database + // delete old custom css and branding sass settings from the database err = store.DeleteSettingValue(ctx, s, oldValue) if err != nil { return err @@ -82,7 +78,7 @@ func updateWebappTheme(ctx context.Context, log *zap.Logger, s store.Storer) (er // provision custom CSS if !oldCustomCSS.IsNull() { - err = provisionTheme("ui.studio.custom-css", oldCustomCSS, "general", "light", "dark") + err = provisionTheme("ui.studio.custom-css", oldCustomCSS, sass.GeneralTheme, sass.LightTheme, sass.DarkTheme) if err != nil { return err } @@ -90,7 +86,7 @@ func updateWebappTheme(ctx context.Context, log *zap.Logger, s store.Storer) (er // provision branding sass if !oldBranding.IsNull() { - err = provisionTheme("ui.studio.themes", oldBranding, "light", "dark") + err = provisionTheme("ui.studio.themes", oldBranding, sass.LightTheme, sass.DarkTheme) } return nil diff --git a/server/pkg/sass/processor.go b/server/pkg/sass/processor.go index e974718fb..3aaad8f38 100644 --- a/server/pkg/sass/processor.go +++ b/server/pkg/sass/processor.go @@ -15,6 +15,15 @@ import ( "go.uber.org/zap" ) +const ( + GeneralTheme = "general" + LightTheme = "light" + DarkTheme = "dark" + SectionRoot = "root" + SectionMain = "main" + SectionTheme = "theme" +) + var ( StylesheetCache = newStylesheetCache() sassVariablesPattern = regexp.MustCompile(`(\$[a-zA-Z_-]+):\s*([^;]+);`) @@ -49,19 +58,21 @@ func DefaultCSS(log *zap.Logger, customCSS string) string { func Transpile(transpiler *godartsass.Transpiler, log *zap.Logger, themeID, themeSASS, customCSS, sassDirPath string) (err error) { // process root section - err = processSass(transpiler, log, "root", themeID, themeSASS, customCSS, sassDirPath) + err = processSass(transpiler, log, SectionRoot, themeID, themeSASS, customCSS, sassDirPath) if err != nil { return err } // process main section - err = processSass(transpiler, log, "main", themeID, themeSASS, customCSS, sassDirPath) - if err != nil { - return err + if themeID == LightTheme { + err = processSass(transpiler, log, SectionMain, themeID, themeSASS, customCSS, sassDirPath) + if err != nil { + return err + } } //process theme section - err = processSass(transpiler, log, "theme", themeID, themeSASS, customCSS, sassDirPath) + err = processSass(transpiler, log, SectionTheme, themeID, themeSASS, customCSS, sassDirPath) if err != nil { return err } diff --git a/server/system/rest.yaml b/server/system/rest.yaml index e4a41ad92..0cfc8b5d6 100644 --- a/server/system/rest.yaml +++ b/server/system/rest.yaml @@ -542,6 +542,10 @@ endpoints: name: labels title: Labels parser: label.ParseStrings + - name: meta + type: "*types.UserMeta" + title: Additional user info + parser: types.ParseUserMeta - name: update method: PUT title: Update user details diff --git a/server/system/rest/request/user.go b/server/system/rest/request/user.go index 3e3baa8e7..8f71a31a3 100644 --- a/server/system/rest/request/user.go +++ b/server/system/rest/request/user.go @@ -143,6 +143,11 @@ type ( // // Labels Labels map[string]string + + // Meta POST parameter + // + // Additional user info + Meta *types.UserMeta } UserUpdate struct { @@ -629,6 +634,7 @@ func (r UserCreate) Auditable() map[string]interface{} { "handle": r.Handle, "kind": r.Kind, "labels": r.Labels, + "meta": r.Meta, } } @@ -657,6 +663,11 @@ func (r UserCreate) GetLabels() map[string]string { return r.Labels } +// Auditable returns all auditable/loggable parameters +func (r UserCreate) GetMeta() *types.UserMeta { + return r.Meta +} + // Fill processes request and fills internal variables func (r *UserCreate) Fill(req *http.Request) (err error) { @@ -717,6 +728,18 @@ func (r *UserCreate) Fill(req *http.Request) (err error) { return err } } + + if val, ok := req.MultipartForm.Value["meta[]"]; ok { + r.Meta, err = types.ParseUserMeta(val) + if err != nil { + return err + } + } else if val, ok := req.MultipartForm.Value["meta"]; ok { + r.Meta, err = types.ParseUserMeta(val) + if err != nil { + return err + } + } } } @@ -766,6 +789,18 @@ func (r *UserCreate) Fill(req *http.Request) (err error) { return err } } + + if val, ok := req.Form["meta[]"]; ok { + r.Meta, err = types.ParseUserMeta(val) + if err != nil { + return err + } + } else if val, ok := req.Form["meta"]; ok { + r.Meta, err = types.ParseUserMeta(val) + if err != nil { + return err + } + } } return err diff --git a/server/system/rest/user.go b/server/system/rest/user.go index 093bd0e97..a9ad0f34a 100644 --- a/server/system/rest/user.go +++ b/server/system/rest/user.go @@ -139,6 +139,7 @@ func (ctrl User) Create(ctx context.Context, r *request.UserCreate) (interface{} Handle: r.Handle, Kind: r.Kind, Labels: r.Labels, + Meta: r.Meta, } res, err := ctrl.user.Create(ctx, user) diff --git a/server/system/service/stylesheet.go b/server/system/service/stylesheet.go index 13741198b..57dbd4e80 100644 --- a/server/system/service/stylesheet.go +++ b/server/system/service/stylesheet.go @@ -1,26 +1,25 @@ package service import ( + "fmt" + "github.com/cespare/xxhash/v2" "strings" - "github.com/cortezaproject/corteza/server/pkg/logger" "github.com/cortezaproject/corteza/server/pkg/sass" - "github.com/cortezaproject/corteza/server/pkg/str" "github.com/cortezaproject/corteza/server/system/types" "go.uber.org/zap" ) // GenerateCSS takes care of creating CSS for webapps by reading SASS content from embedded assets, -// combining it with brandingSass and customCSS, and then transpiling it using the dart-sass compiler. +// combining it with different themeSASS and customCSS themes, and then transpiling it using the dart-sass compiler. // // If dart sass isn't installed on the host machine, it will default to css content from the minified-custom.css which is // generated from [Boostrap, bootstrap-vue and custom variables sass content]. // If dart isn't installed on the host machine, customCustom css will continue to function, but without sass support. // // In case of an error, it will return default css and log out the error -func GenerateCSS(settings *types.AppSettings, sassDirPath string) (err error) { +func GenerateCSS(settings *types.AppSettings, sassDirPath string, log *zap.Logger) (err error) { var ( - log = logger.Default() studio = settings.UI.Studio customCSSMap = make(map[string]string) ) @@ -29,7 +28,7 @@ func GenerateCSS(settings *types.AppSettings, sassDirPath string) (err error) { customCSSMap[customCSS.ID] = customCSS.Values } - sass.DefaultCSS(log, customCSSMap["general"]) + sass.DefaultCSS(log, customCSSMap[sass.GeneralTheme]) if studio.Themes == nil && studio.CustomCSS == nil { return @@ -62,7 +61,7 @@ func GenerateCSS(settings *types.AppSettings, sassDirPath string) (err error) { } else { if studio.CustomCSS != nil { for key := range customCSSMap { - if key == "general" { + if key == sass.GeneralTheme { continue } @@ -83,7 +82,7 @@ func GenerateCSS(settings *types.AppSettings, sassDirPath string) (err error) { func processCustomCSS(themeID string, customCSSMap map[string]string) (customCSS string) { var stringsBuilder strings.Builder - stringsBuilder.WriteString(customCSSMap["general"]) + stringsBuilder.WriteString(customCSSMap[sass.GeneralTheme]) stringsBuilder.WriteString("\n") stringsBuilder.WriteString(customCSSMap[themeID]) @@ -107,7 +106,7 @@ func updateCSS(current, old, compStyles *types.SettingValue, name, sassDirPath s } for key := range currentThemesMap { - if str.HashStringSHA256(oldThemesMap[key]) == str.HashStringSHA256(currentThemesMap[key]) { + if xxhash.Sum64String(oldThemesMap[key]) == xxhash.Sum64String(currentThemesMap[key]) { continue } @@ -116,10 +115,9 @@ func updateCSS(current, old, compStyles *types.SettingValue, name, sassDirPath s continue } - if key == "general" { + if key == sass.GeneralTheme { if complimentaryStylesMap == nil { - themeID := "light" - transpileSASS(themeID, complimentaryStylesMap[themeID], currentThemesMap) + transpileSASS(sass.LightTheme, complimentaryStylesMap[sass.LightTheme], currentThemesMap) continue } @@ -131,7 +129,6 @@ func updateCSS(current, old, compStyles *types.SettingValue, name, sassDirPath s transpileSASS(key, complimentaryStylesMap[key], currentThemesMap) } - } func themeMap(settingsValue *types.SettingValue) (themeMap map[string]string) { @@ -152,25 +149,27 @@ func themeMap(settingsValue *types.SettingValue) (themeMap map[string]string) { } func FetchCSS() string { - var stringsBuilder strings.Builder + var ( + stringsBuilder strings.Builder + rootLight = sass.StylesheetCache.Get(fmt.Sprintf("%s-%s", sass.SectionRoot, sass.LightTheme)) + ) - if sass.StylesheetCache.Get("root-light") == "" { + if rootLight == "" { return sass.StylesheetCache.Get("default-theme") } // root css section - stringsBuilder.WriteString(sass.StylesheetCache.Get("root-light")) + stringsBuilder.WriteString(rootLight) stringsBuilder.WriteString("\n") - stringsBuilder.WriteString(sass.StylesheetCache.Get("root-dark")) - + stringsBuilder.WriteString(sass.StylesheetCache.Get(fmt.Sprintf("%s-%s", sass.SectionRoot, sass.DarkTheme))) stringsBuilder.WriteString("\n") //theme css section - stringsBuilder.WriteString(sass.StylesheetCache.Get("theme-dark")) + stringsBuilder.WriteString(sass.StylesheetCache.Get(fmt.Sprintf("%s-%s", sass.SectionTheme, sass.DarkTheme))) stringsBuilder.WriteString("\n") // body css section - stringsBuilder.WriteString(sass.StylesheetCache.Get("main-light")) + stringsBuilder.WriteString(sass.StylesheetCache.Get(fmt.Sprintf("%s-%s", sass.SectionMain, sass.LightTheme))) stringsBuilder.WriteString("\n") return stringsBuilder.String() diff --git a/server/system/types/app_settings.go b/server/system/types/app_settings.go index 3b3ce58c7..af3f94ad2 100644 --- a/server/system/types/app_settings.go +++ b/server/system/types/app_settings.go @@ -1,650 +1,649 @@ package types import ( - "fmt" - "strings" - "time" + "fmt" + "strings" + "time" ) const ( - oidcProviderPrefix = "openid-connect." // must match const in "github.com/cortezaproject/corteza/server/auth/external" external.go + oidcProviderPrefix = "openid-connect." // must match const in "github.com/cortezaproject/corteza/server/auth/external" external.go - ExternalProviderUsageIdentity = "identity" - ExternalProviderUsageAPI = "api" + ExternalProviderUsageIdentity = "identity" + ExternalProviderUsageAPI = "api" ) type ( - // AppSettings type is structured representation of all application settings - // - // Raw settings keys are hyphen (kebab) case, separated with a dot (.) that indicates sub-level - // JSON properties for settings are NOT converted (lower-cased, etc...) - // Use `json:"-"` tag to hide settings on REST endpoint - AppSettings struct { - Privacy struct { - Mask struct { - // Enable masking of user's email (value replaced with ######) - Email bool - - // Enable masking of user's name (value replaced with ######) - Name bool - } - } `json:"-"` - - General struct { - Mail struct { - Logo string - Header string `kv:"header.en"` - Footer string `kv:"footer.en"` - } - } `json:"-"` - - SMTP struct { - Servers []SmtpServers `json:"-" kv:"servers,final"` - } `json:"-" kv:"smtp"` - - Auth struct { - Internal struct { - // Is internal authentication (username + password) enabled - Enabled bool `json:"-"` - - Signup struct { - // Can users register - Enabled bool - - // Users must confirm their emails when signing-up - EmailConfirmationRequired bool `kv:"email-confirmation-required"` - } `json:"-"` - - // Can users reset their passwords - PasswordReset struct{ Enabled bool } `json:"-" kv:"password-reset"` - - // When enabled, users added via CLI will receive an email with a link to reset their password. - SendUserInviteEmail struct { - Enabled bool - Expires uint - } `kv:"send-user-invite-email" json:"sendUserInviteEmail"` - - // PasswordCreate setting for create password for user via generated link with token - // If user has no password then link redirects to create password page - // Otherwise it redirects to profile page of that user - // link can be generated through useradd cli command with `make-password-link` flag - PasswordCreate struct { - Enabled bool - Expires uint - } `json:"-" kv:"password-create"` - - // Splits credentials check into 2 parts - // If user has password credentials it offers him to enter the password - // Otherwise we offer the user to choose among the enabled external providers - // If only one ext. provider is enabled, user is automatically redirected there - SplitCredentialsCheck bool `json:"-" kv:"split-credentials-check"` - - PasswordConstraints PasswordConstraints `kv:"password-constraints" json:"passwordConstraints"` - - ProfileAvatar struct{ Enabled bool } `kv:"profile-avatar" json:"profile-avatar"` - } `json:"internal"` - - External struct { - // Is external authentication - Enabled bool `json:"enabled"` - - // Saml - Saml struct { - Enabled bool - - // IdP name used on the login form - Name string `kv:"name"` - - // SAML certificate - Cert string `kv:"cert"` - - // SAML certificate private key - Key string `kv:"key"` - - // Sign AuthNRequest and assertion - SignRequests bool `kv:"sign-requests"` - - // Signature method for signing - SignMethod string `kv:"sign-method"` - - // Post or redirect binding - Binding string `kv:"binding"` - - // Identity provider settings - IDP struct { - URL string `kv:"url"` - - // identifier payload from idp - IdentName string `kv:"ident-name"` - IdentHandle string `kv:"ident-handle"` - IdentIdentifier string `kv:"ident-identifier"` - } `kv:"idp"` - - Security ExternalAuthProviderSecurity `json:"-" kv:"security,final"` - } `json:"-"` - - // all external providers we know - Providers ExternalAuthProviderSet `json:"providers"` - } `json:"external"` - - MultiFactor struct { - EmailOTP struct { - // Can users use email for MFA - Enabled bool - - // Is MFA with email enforced? - Enforced bool - - // Require fresh Email OTP on every client authorization - // Strict bool - - Expires uint - } `kv:"email-otp"` - - TOTP struct { - // Can users use TOTP for MFA - Enabled bool - - // Is MFA with TOTP enforced? - Enforced bool - - // Require fresh TOTP on every client authorization - // Strict bool - - // TOTP issuer, defaults to "Corteza" - Issuer string - } `kv:"totp"` - } `json:"-" kv:"multi-factor"` - - Mail struct { - FromAddress string `kv:"from-address"` - FromName string `kv:"from-name"` - } `json:"-"` - - // Auth Background Image settings - UI struct { - BackgroundImageSrc string `kv:"background-image-src" json:"backgroundImageSrc"` - Styles string `kv:"styles" json:"styles"` - } `kv:"ui" json:"ui"` - } `json:"auth"` - - Compose struct { - // Compose UI settings - UI struct { - // Sidebar specific settings - Sidebar struct { - // Hide namespace list - HideNamespaceList bool `json:"hideNamespaceList"` - - // Hide namespace link at the end of the list - HideNamespaceListLink bool `json:"hideNamespaceListLink"` - } `kv:"sidebar,final" json:"sidebar"` - - // Record toolbar specific settings - RecordToolbar struct { - HideNew bool `json:"hideNew"` - HideEdit bool `json:"hideEdit"` - HideSubmit bool `json:"hideSubmit"` - HideDelete bool `json:"hideDelete"` - HideClone bool `json:"hideClone"` - HideBack bool `json:"hideBack"` - } `kv:"record-toolbar,final" json:"record-toolbar"` - } `kv:"ui" json:"ui"` - - // Record related settings - Record struct { - // @todo implementation - Attachments struct { - // What is max size (in MB, so: MaxSize x 2^20) - MaxSize uint `kv:"max-size"` - - // List of mime-types we support, - Mimetypes []string - } - } - - // Page related settings - Page struct { - // @todo implementation - Attachments struct { - // What is max size (in MB, so: MaxSize x 2^20) - MaxSize uint `kv:"max-size"` - - // List of mime-types we support, - Mimetypes []string - } - } - - // Icon related settings - Icon struct { - // @todo implementation - Attachments struct { - // What is max size (in MB, so: MaxSize x 2^20) - MaxSize uint `kv:"max-size"` - - // List of mime-types we support, - Mimetypes []string - } - } - } `kv:"compose" json:"compose"` - - // Federation settings - Federation struct { - // This only holds the value of FEDERATION_ENABLED for now - // - Enabled bool `kv:"-" json:"enabled"` - } `kv:"federation" json:"federation"` - - // Integration gateway settings - Apigw ApigwSettings `kv:"apigw" json:"apigw"` - - // UserInterface settings - UI struct { - MainLogo string `kv:"main-logo" json:"mainLogo"` - IconLogo string `kv:"icon-logo" json:"iconLogo"` - - Sidebar struct { - // General sidebar settings - Disabled bool `json:"disabled"` - } `kv:"sidebar,final" json:"sidebar"` - - Studio struct { - SassInstalled bool `kv:"sass-installed" json:"sass-installed"` - - Themes []struct{ Theme } `kv:"themes" json:"themes"` - CustomCSS []struct{ Theme } `kv:"custom-css" json:"customCSS"` - } `kv:"studio" json:"studio"` - - Topbar struct { - HideAppSelector bool `json:"hideAppSelector"` - HideHelp bool `json:"hideHelp"` - HideForumLink bool `json:"hideForumLink"` - HideDocumentationLink bool `json:"hideDocumentationLink"` - HideFeedbackLink bool `json:"hideFeedbackLink"` - HideProfile bool `json:"hideProfile"` - HideChangePasswordLink bool `json:"hideChangePasswordLink"` - HideProfileLink bool `json:"hideProfileLink"` - - HelpLinks []struct { - Handle string `json:"handle"` - URL string `json:"url"` - NewTab bool `json:"newTab"` - } `json:"helpLinks"` - - ProfileLinks []struct { - Handle string `json:"handle"` - URL string `json:"url"` - NewTab bool `json:"newTab"` - } `json:"profileLinks"` - } `kv:"topbar,final" json:"topbar"` - - Charts struct { - ColorSchemes []struct { - ID string `json:"id"` - Name string `json:"name"` - Colors []string `json:"colors"` - } `kv:"colorSchemes" json:"colorSchemes"` - } `kv:"charts" json:"charts"` - } `kv:"ui" json:"ui"` - - ResourceTranslations struct { - // List of all languages (resource translations) enabled and - // available for resource translations (these are module names, - // field labels, descriptions, ...) - - // This is always a subset of all languages available - // in Corteza instance (LOCALE_LANGUAGES) - // - // Note: later, we will enable this to contain languages - // that are not part of LOCALE_LANGUAGES - // - // 1st language in the set is also a default one - // - // IsEmpty slice defaults to LOCALE_LANGUAGES - Languages []string `kv:"languages" json:"languages"` - } `kv:"resource-translations" json:"resourceTranslations"` - - Discovery struct { - // Enable indexing - Enabled bool `kv:"enabled" json:"enabled"` - - SystemUsers struct { - // Enable indexing of users - Enabled bool `kv:"enabled" json:"enabled"` - } `kv:"system-users" json:"system-users"` - - SystemApplications struct { - // Enable indexing of applications - Enabled bool `kv:"enabled" json:"enabled"` - } `kv:"system-applications" json:"system-applications"` - - SystemRoles struct { - // Enable indexing of roles - Enabled bool `kv:"enabled" json:"enabled"` - } `kv:"system-roles" json:"system-roles"` - - SystemTemplates struct { - // Enable indexing of templates - Enabled bool `kv:"enabled" json:"enabled"` - } `kv:"system-templates" json:"system-templates"` - - AutomationWorkflows struct { - // Enable indexing of workflows - Enabled bool `kv:"enabled" json:"enabled"` - } `kv:"automation-workflows" json:"automation-workflows"` - - ComposeNamespaces struct { - // Enable indexing of compose namespaces - Enabled bool `kv:"enabled" json:"enabled"` - } `kv:"compose-namespaces" json:"compose-namespaces"` - - ComposeCharts struct { - // Enable indexing of compose charts - Enabled bool `kv:"enabled" json:"enabled"` - } `kv:"compose-charts" json:"compose-charts"` - - ComposePages struct { - // Enable indexing of compose pages - Enabled bool `kv:"enabled" json:"enabled"` - } `kv:"compose-pages" json:"compose-pages"` - - ComposeModules struct { - // Enable indexing of compose modules - Enabled bool `kv:"enabled" json:"enabled"` - } `kv:"compose-modules" json:"compose-modules"` - - ComposeRecords struct { - // Enable indexing of compose records - Enabled bool `kv:"enabled" json:"enabled"` - } `kv:"compose-records" json:"compose-records"` - } `kv:"discovery" json:"discovery"` - } - - ExternalAuthProviderSet []*ExternalAuthProvider - - ExternalAuthProvider struct { - Enabled bool `json:"enabled"` - Handle string `json:"handle"` - Label string `json:"label"` - Key string `json:"-"` - Secret string `json:"-"` - Scope string `json:"scope"` - RedirectUrl string `json:"-" kv:"redirect"` - IssuerUrl string `json:"-" kv:"issuer"` - Weight int `json:"-"` - - // potentially just use home-brew scopes? - Usage []string `json:"usage" kv:"usage"` - - Security ExternalAuthProviderSecurity `json:"-" kv:"security,final"` - } - - ExternalAuthProviderSecurity struct { - // Subset of roles, permitted to be used with this client - // when authorizing via this auth provider. - // - // IDs are intentionally stored as strings to support JS (int64 only) - // - PermittedRoles []string `json:"permittedRoles,omitempty"` - - // Subset of roles, prohibited to be used with this client - // when authorizing via this auth provider. - // - // IDs are intentionally stored as strings to support JS (int64 only) - // - ProhibitedRoles []string `json:"prohibitedRoles,omitempty"` - - // Set of additional roles that are forced on this user - // when authorizing via this auth provider. - // - // IDs are intentionally stored as strings to support JS (int64 only) - ForcedRoles []string `json:"forcedRoles,omitempty"` - - // Map external roles or groups to internal - // - // If IdP provides a list of roles (groups) along side authenticated user - // these roles can be mapped to the valid local roles - // - // @todo implement mapped roles - // MappedRoles map[string]string `json:"mappedRoles,omitempty"` - } - - Theme struct { - ID string `json:"id"` - Title string `json:"title"` - Values string `json:"values"` - } - - SmtpServers struct { - Host string `json:"host"` - Port int `json:"port,string"` - User string `json:"user"` - Pass string `json:"pass"` - From string `json:"from"` - TlsInsecure bool `json:"tlsInsecure"` - TlsServerName string `json:"tlsServerName"` - } - - ApigwSettings struct { - Enabled bool `kv:"enabled" json:"enabled"` - - Profiler struct { - Enabled bool `kv:"enabled" json:"enabled"` - Global bool `kv:"global" json:"global"` - } `kv:"profiler" json:"profiler"` - - Proxy struct { - FollowRedirects bool `kv:"follow-redirects" json:"follow-redirects"` - OutboundTimeout time.Duration `kv:"outbound-timeout" json:"outbound-timeout"` - } `kv:"proxy" json:"proxy"` - } - - PasswordConstraints struct { - // Should the environment not enforce the constraints - PasswordSecurity bool `kv:"-" json:"passwordSecurity"` - - // The min password length - MinLength uint `kv:"min-length" json:"minLength"` - - // Minimum number of uppercase letters in password - MinUpperCase uint `kv:"min-upper-case" json:"minUpperCase"` - - // Minimum number of lowercase letters in password - MinLowerCase uint `kv:"min-lower-case" json:"minLowerCase"` - - // The min number of numeric characters - MinNumCount uint `kv:"min-num-count" json:"minNumCount"` - - // The min number of special characters - MinSpecialCount uint `kv:"min-special-count" json:"minSpecialCount"` - } + // AppSettings type is structured representation of all application settings + // + // Raw settings keys are hyphen (kebab) case, separated with a dot (.) that indicates sub-level + // JSON properties for settings are NOT converted (lower-cased, etc...) + // Use `json:"-"` tag to hide settings on REST endpoint + AppSettings struct { + Privacy struct { + Mask struct { + // Enable masking of user's email (value replaced with ######) + Email bool + + // Enable masking of user's name (value replaced with ######) + Name bool + } + } `json:"-"` + + General struct { + Mail struct { + Logo string + Header string `kv:"header.en"` + Footer string `kv:"footer.en"` + } + } `json:"-"` + + SMTP struct { + Servers []SmtpServers `json:"-" kv:"servers,final"` + } `json:"-" kv:"smtp"` + + Auth struct { + Internal struct { + // Is internal authentication (username + password) enabled + Enabled bool `json:"-"` + + Signup struct { + // Can users register + Enabled bool + + // Users must confirm their emails when signing-up + EmailConfirmationRequired bool `kv:"email-confirmation-required"` + } `json:"-"` + + // Can users reset their passwords + PasswordReset struct{ Enabled bool } `json:"-" kv:"password-reset"` + + // When enabled, users added via CLI will receive an email with a link to reset their password. + SendUserInviteEmail struct { + Enabled bool + Expires uint + } `kv:"send-user-invite-email" json:"sendUserInviteEmail"` + + // PasswordCreate setting for create password for user via generated link with token + // If user has no password then link redirects to create password page + // Otherwise it redirects to profile page of that user + // link can be generated through useradd cli command with `make-password-link` flag + PasswordCreate struct { + Enabled bool + Expires uint + } `json:"-" kv:"password-create"` + + // Splits credentials check into 2 parts + // If user has password credentials it offers him to enter the password + // Otherwise we offer the user to choose among the enabled external providers + // If only one ext. provider is enabled, user is automatically redirected there + SplitCredentialsCheck bool `json:"-" kv:"split-credentials-check"` + + PasswordConstraints PasswordConstraints `kv:"password-constraints" json:"passwordConstraints"` + + ProfileAvatar struct{ Enabled bool } `kv:"profile-avatar" json:"profile-avatar"` + } `json:"internal"` + + External struct { + // Is external authentication + Enabled bool `json:"enabled"` + + // Saml + Saml struct { + Enabled bool + + // IdP name used on the login form + Name string `kv:"name"` + + // SAML certificate + Cert string `kv:"cert"` + + // SAML certificate private key + Key string `kv:"key"` + + // Sign AuthNRequest and assertion + SignRequests bool `kv:"sign-requests"` + + // Signature method for signing + SignMethod string `kv:"sign-method"` + + // Post or redirect binding + Binding string `kv:"binding"` + + // Identity provider settings + IDP struct { + URL string `kv:"url"` + + // identifier payload from idp + IdentName string `kv:"ident-name"` + IdentHandle string `kv:"ident-handle"` + IdentIdentifier string `kv:"ident-identifier"` + } `kv:"idp"` + + Security ExternalAuthProviderSecurity `json:"-" kv:"security,final"` + } `json:"-"` + + // all external providers we know + Providers ExternalAuthProviderSet `json:"providers"` + } `json:"external"` + + MultiFactor struct { + EmailOTP struct { + // Can users use email for MFA + Enabled bool + + // Is MFA with email enforced? + Enforced bool + + // Require fresh Email OTP on every client authorization + // Strict bool + + Expires uint + } `kv:"email-otp"` + + TOTP struct { + // Can users use TOTP for MFA + Enabled bool + + // Is MFA with TOTP enforced? + Enforced bool + + // Require fresh TOTP on every client authorization + // Strict bool + + // TOTP issuer, defaults to "Corteza" + Issuer string + } `kv:"totp"` + } `json:"-" kv:"multi-factor"` + + Mail struct { + FromAddress string `kv:"from-address"` + FromName string `kv:"from-name"` + } `json:"-"` + + // Auth Background Image settings + UI struct { + BackgroundImageSrc string `kv:"background-image-src" json:"backgroundImageSrc"` + Styles string `kv:"styles" json:"styles"` + } `kv:"ui" json:"ui"` + } `json:"auth"` + + Compose struct { + // Compose UI settings + UI struct { + // Sidebar specific settings + Sidebar struct { + // Hide namespace list + HideNamespaceList bool `json:"hideNamespaceList"` + + // Hide namespace link at the end of the list + HideNamespaceListLink bool `json:"hideNamespaceListLink"` + } `kv:"sidebar,final" json:"sidebar"` + + // Record toolbar specific settings + RecordToolbar struct { + HideNew bool `json:"hideNew"` + HideEdit bool `json:"hideEdit"` + HideSubmit bool `json:"hideSubmit"` + HideDelete bool `json:"hideDelete"` + HideClone bool `json:"hideClone"` + HideBack bool `json:"hideBack"` + } `kv:"record-toolbar,final" json:"record-toolbar"` + } `kv:"ui" json:"ui"` + + // Record related settings + Record struct { + // @todo implementation + Attachments struct { + // What is max size (in MB, so: MaxSize x 2^20) + MaxSize uint `kv:"max-size"` + + // List of mime-types we support, + Mimetypes []string + } + } + + // Page related settings + Page struct { + // @todo implementation + Attachments struct { + // What is max size (in MB, so: MaxSize x 2^20) + MaxSize uint `kv:"max-size"` + + // List of mime-types we support, + Mimetypes []string + } + } + + // Icon related settings + Icon struct { + // @todo implementation + Attachments struct { + // What is max size (in MB, so: MaxSize x 2^20) + MaxSize uint `kv:"max-size"` + + // List of mime-types we support, + Mimetypes []string + } + } + } `kv:"compose" json:"compose"` + + // Federation settings + Federation struct { + // This only holds the value of FEDERATION_ENABLED for now + // + Enabled bool `kv:"-" json:"enabled"` + } `kv:"federation" json:"federation"` + + // Integration gateway settings + Apigw ApigwSettings `kv:"apigw" json:"apigw"` + + // UserInterface settings + UI struct { + MainLogo string `kv:"main-logo" json:"mainLogo"` + IconLogo string `kv:"icon-logo" json:"iconLogo"` + + Sidebar struct { + // General sidebar settings + Disabled bool `json:"disabled"` + } `kv:"sidebar,final" json:"sidebar"` + + Studio struct { + SassInstalled bool `kv:"sass-installed" json:"sass-installed"` + + Themes []struct{ Theme } `kv:"themes" json:"themes"` + CustomCSS []struct{ Theme } `kv:"custom-css" json:"customCSS"` + } `kv:"studio" json:"studio"` + + Topbar struct { + HideAppSelector bool `json:"hideAppSelector"` + HideHelp bool `json:"hideHelp"` + HideForumLink bool `json:"hideForumLink"` + HideDocumentationLink bool `json:"hideDocumentationLink"` + HideFeedbackLink bool `json:"hideFeedbackLink"` + HideProfile bool `json:"hideProfile"` + HideChangePasswordLink bool `json:"hideChangePasswordLink"` + HideProfileLink bool `json:"hideProfileLink"` + + HelpLinks []struct { + Handle string `json:"handle"` + URL string `json:"url"` + NewTab bool `json:"newTab"` + } `json:"helpLinks"` + + ProfileLinks []struct { + Handle string `json:"handle"` + URL string `json:"url"` + NewTab bool `json:"newTab"` + } `json:"profileLinks"` + } `kv:"topbar,final" json:"topbar"` + + Charts struct { + ColorSchemes []struct { + ID string `json:"id"` + Name string `json:"name"` + Colors []string `json:"colors"` + } `kv:"colorSchemes" json:"colorSchemes"` + } `kv:"charts" json:"charts"` + } `kv:"ui" json:"ui"` + + ResourceTranslations struct { + // List of all languages (resource translations) enabled and + // available for resource translations (these are module names, + // field labels, descriptions, ...) + + // This is always a subset of all languages available + // in Corteza instance (LOCALE_LANGUAGES) + // + // Note: later, we will enable this to contain languages + // that are not part of LOCALE_LANGUAGES + // + // 1st language in the set is also a default one + // + // IsEmpty slice defaults to LOCALE_LANGUAGES + Languages []string `kv:"languages" json:"languages"` + } `kv:"resource-translations" json:"resourceTranslations"` + + Discovery struct { + // Enable indexing + Enabled bool `kv:"enabled" json:"enabled"` + + SystemUsers struct { + // Enable indexing of users + Enabled bool `kv:"enabled" json:"enabled"` + } `kv:"system-users" json:"system-users"` + + SystemApplications struct { + // Enable indexing of applications + Enabled bool `kv:"enabled" json:"enabled"` + } `kv:"system-applications" json:"system-applications"` + + SystemRoles struct { + // Enable indexing of roles + Enabled bool `kv:"enabled" json:"enabled"` + } `kv:"system-roles" json:"system-roles"` + + SystemTemplates struct { + // Enable indexing of templates + Enabled bool `kv:"enabled" json:"enabled"` + } `kv:"system-templates" json:"system-templates"` + + AutomationWorkflows struct { + // Enable indexing of workflows + Enabled bool `kv:"enabled" json:"enabled"` + } `kv:"automation-workflows" json:"automation-workflows"` + + ComposeNamespaces struct { + // Enable indexing of compose namespaces + Enabled bool `kv:"enabled" json:"enabled"` + } `kv:"compose-namespaces" json:"compose-namespaces"` + + ComposeCharts struct { + // Enable indexing of compose charts + Enabled bool `kv:"enabled" json:"enabled"` + } `kv:"compose-charts" json:"compose-charts"` + + ComposePages struct { + // Enable indexing of compose pages + Enabled bool `kv:"enabled" json:"enabled"` + } `kv:"compose-pages" json:"compose-pages"` + + ComposeModules struct { + // Enable indexing of compose modules + Enabled bool `kv:"enabled" json:"enabled"` + } `kv:"compose-modules" json:"compose-modules"` + + ComposeRecords struct { + // Enable indexing of compose records + Enabled bool `kv:"enabled" json:"enabled"` + } `kv:"compose-records" json:"compose-records"` + } `kv:"discovery" json:"discovery"` + } + + ExternalAuthProviderSet []*ExternalAuthProvider + + ExternalAuthProvider struct { + Enabled bool `json:"enabled"` + Handle string `json:"handle"` + Label string `json:"label"` + Key string `json:"-"` + Secret string `json:"-"` + Scope string `json:"scope"` + RedirectUrl string `json:"-" kv:"redirect"` + IssuerUrl string `json:"-" kv:"issuer"` + Weight int `json:"-"` + + // potentially just use home-brew scopes? + Usage []string `json:"usage" kv:"usage"` + + Security ExternalAuthProviderSecurity `json:"-" kv:"security,final"` + } + + ExternalAuthProviderSecurity struct { + // Subset of roles, permitted to be used with this client + // when authorizing via this auth provider. + // + // IDs are intentionally stored as strings to support JS (int64 only) + // + PermittedRoles []string `json:"permittedRoles,omitempty"` + + // Subset of roles, prohibited to be used with this client + // when authorizing via this auth provider. + // + // IDs are intentionally stored as strings to support JS (int64 only) + // + ProhibitedRoles []string `json:"prohibitedRoles,omitempty"` + + // Set of additional roles that are forced on this user + // when authorizing via this auth provider. + // + // IDs are intentionally stored as strings to support JS (int64 only) + ForcedRoles []string `json:"forcedRoles,omitempty"` + + // Map external roles or groups to internal + // + // If IdP provides a list of roles (groups) along side authenticated user + // these roles can be mapped to the valid local roles + // + // @todo implement mapped roles + // MappedRoles map[string]string `json:"mappedRoles,omitempty"` + } + + Theme struct { + ID string `json:"id"` + Values string `json:"values"` + } + + SmtpServers struct { + Host string `json:"host"` + Port int `json:"port,string"` + User string `json:"user"` + Pass string `json:"pass"` + From string `json:"from"` + TlsInsecure bool `json:"tlsInsecure"` + TlsServerName string `json:"tlsServerName"` + } + + ApigwSettings struct { + Enabled bool `kv:"enabled" json:"enabled"` + + Profiler struct { + Enabled bool `kv:"enabled" json:"enabled"` + Global bool `kv:"global" json:"global"` + } `kv:"profiler" json:"profiler"` + + Proxy struct { + FollowRedirects bool `kv:"follow-redirects" json:"follow-redirects"` + OutboundTimeout time.Duration `kv:"outbound-timeout" json:"outbound-timeout"` + } `kv:"proxy" json:"proxy"` + } + + PasswordConstraints struct { + // Should the environment not enforce the constraints + PasswordSecurity bool `kv:"-" json:"passwordSecurity"` + + // The min password length + MinLength uint `kv:"min-length" json:"minLength"` + + // Minimum number of uppercase letters in password + MinUpperCase uint `kv:"min-upper-case" json:"minUpperCase"` + + // Minimum number of lowercase letters in password + MinLowerCase uint `kv:"min-lower-case" json:"minLowerCase"` + + // The min number of numeric characters + MinNumCount uint `kv:"min-num-count" json:"minNumCount"` + + // The min number of special characters + MinSpecialCount uint `kv:"min-special-count" json:"minSpecialCount"` + } ) // WithDefaults sets defaults on copy (!!) of settings // to avoid any unintended corruption or leaks func (cs AppSettings) WithDefaults() *AppSettings { - if len(strings.TrimSpace(cs.UI.IconLogo)) == 0 { - cs.UI.IconLogo = "/assets/favicon32x32.png" - } - if len(strings.TrimSpace(cs.UI.MainLogo)) == 0 { - cs.UI.MainLogo = "/assets/logo.png" - } + if len(strings.TrimSpace(cs.UI.IconLogo)) == 0 { + cs.UI.IconLogo = "/assets/favicon32x32.png" + } + if len(strings.TrimSpace(cs.UI.MainLogo)) == 0 { + cs.UI.MainLogo = "/assets/logo.png" + } - return &cs + return &cs } func (set *ExternalAuthProvider) ValidConfiguration() bool { - if !set.Enabled || set.Handle == "" || set.Key == "" || set.Secret == "" { - return false - } + if !set.Enabled || set.Handle == "" || set.Key == "" || set.Secret == "" { + return false + } - if strings.HasPrefix(set.Handle, oidcProviderPrefix) && set.IssuerUrl == "" { - // OIDC IdPs need to have issuer URL - return false - } + if strings.HasPrefix(set.Handle, oidcProviderPrefix) && set.IssuerUrl == "" { + // OIDC IdPs need to have issuer URL + return false + } - return true + return true } // DecodeKV translates settings' KV into internal system external auth settings func (set *ExternalAuthProviderSet) DecodeKV(kv SettingsKV, prefix string) (err error) { - if *set == nil { - *set = ExternalAuthProviderSet{} - } + if *set == nil { + *set = ExternalAuthProviderSet{} + } - // create standard provider set - permanent := map[string]bool{"github": true, "facebook": true, "google": true, "linkedin": true, "nylas": true} - // and make a working copy - providers := make(map[string]bool) - for k, v := range permanent { - providers[k] = v - } + // create standard provider set + permanent := map[string]bool{"github": true, "facebook": true, "google": true, "linkedin": true, "nylas": true} + // and make a working copy + providers := make(map[string]bool) + for k, v := range permanent { + providers[k] = v + } - // remove prefix - kv = kv.CutPrefix(prefix + ".") + // remove prefix + kv = kv.CutPrefix(prefix + ".") - // add all additional providers (prefixed with "openid-connect.") - for p := range kv { - if !strings.HasPrefix(p, oidcProviderPrefix) { - continue - } + // add all additional providers (prefixed with "openid-connect.") + for p := range kv { + if !strings.HasPrefix(p, oidcProviderPrefix) { + continue + } - l := len(oidcProviderPrefix) - dotPos := strings.Index(p[l:], ".") + l - if dotPos > 0 { - providers[p[:dotPos]] = true - } - } + l := len(oidcProviderPrefix) + 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) - } + // 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 = DecodeKV(kv.CutPrefix(handle+"."), p) - if err != nil { - return - } - } + err = DecodeKV(kv.CutPrefix(handle+"."), p) + if err != nil { + return + } + } - // Cleanup - var clean = ExternalAuthProviderSet{} - for _, p := range *set { - if p.empty() && !permanent[p.Handle] { - continue - } + // Cleanup + var clean = ExternalAuthProviderSet{} + for _, p := range *set { + if p.empty() && !permanent[p.Handle] { + continue + } - if p.Label == "" { - switch p.Handle { - case "github": - p.Label = "GitHub" - case "nylas": - p.Label = "Nylas" - case "linkedin": - p.Label = "LinkedIn" - case "corteza-iam", "corteza", "corteza-one": - // Some legacy provider naming - p.Label = "Corteza IAM" - case "crust-iam", "crust", "crust-unify": - // Some legacy provider naming - p.Label = "Crust IAM" - default: - if strings.HasPrefix(p.Handle, oidcProviderPrefix) { - p.Label = strings.Title(p.Handle[len(oidcProviderPrefix):]) - } else { - p.Label = strings.Title(p.Handle) - } - } - } + if p.Label == "" { + switch p.Handle { + case "github": + p.Label = "GitHub" + case "nylas": + p.Label = "Nylas" + case "linkedin": + p.Label = "LinkedIn" + case "corteza-iam", "corteza", "corteza-one": + // Some legacy provider naming + p.Label = "Corteza IAM" + case "crust-iam", "crust", "crust-unify": + // Some legacy provider naming + p.Label = "Crust IAM" + default: + if strings.HasPrefix(p.Handle, oidcProviderPrefix) { + p.Label = strings.Title(p.Handle[len(oidcProviderPrefix):]) + } else { + p.Label = strings.Title(p.Handle) + } + } + } - clean = append(clean, p) - } + clean = append(clean, p) + } - *set = clean + *set = clean - return + return } func (set ExternalAuthProviderSet) FindByHandle(handle string) *ExternalAuthProvider { - for p := range set { - if set[p].Handle == handle { - return set[p] - } - } + for p := range set { + if set[p].Handle == handle { + return set[p] + } + } - return nil + 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 { - if set[i].Weight != set[j].Weight { - // Sort by weight - return set[i].Weight < set[j].Weight - } + if set[i].Weight != set[j].Weight { + // Sort by weight + return set[i].Weight < set[j].Weight + } - if set[i].Label+set[j].Label != "" { - // If at least one of the - return set[i].Label < set[j].Label - } + if set[i].Label+set[j].Label != "" { + // If at least one of the + return set[i].Label < set[j].Label + } - return set[i].Handle < set[j].Handle + return set[i].Handle < set[j].Handle } // Returns enabled providers, sorted with their redirect-URLs set... func (set ExternalAuthProviderSet) Valid() (out ExternalAuthProviderSet) { - for _, eap := range set { - if eap.empty() || !eap.Enabled { - continue - } + for _, eap := range set { + if eap.empty() || !eap.Enabled { + continue + } - out = append(out, eap) - } + out = append(out, eap) + } - return + return } var _ KVDecoder = &ExternalAuthProviderSet{} func (eap ExternalAuthProvider) EncodeKV() (vv SettingValueSet, err error) { - if eap.Handle == "" { - return nil, fmt.Errorf("cannot encode external auth provider without handle") - } - var ( - prefix = "auth.external.providers." + eap.Handle + "." - pairs = map[string]interface{}{ - "enabled": eap.Enabled, - "label": eap.Label, - "key": eap.Key, - "secret": eap.Secret, - "scope": eap.Scope, - "issuer": eap.IssuerUrl, - "redirect": eap.RedirectUrl, - "weight": eap.Weight, - } - ) + if eap.Handle == "" { + return nil, fmt.Errorf("cannot encode external auth provider without handle") + } + var ( + prefix = "auth.external.providers." + eap.Handle + "." + pairs = map[string]interface{}{ + "enabled": eap.Enabled, + "label": eap.Label, + "key": eap.Key, + "secret": eap.Secret, + "scope": eap.Scope, + "issuer": eap.IssuerUrl, + "redirect": eap.RedirectUrl, + "weight": eap.Weight, + } + ) - for key, value := range pairs { - v := &SettingValue{Name: prefix + key} + for key, value := range pairs { + v := &SettingValue{Name: prefix + key} - if err = v.SetSetting(value); err != nil { - return - } + if err = v.SetSetting(value); err != nil { + return + } - vv = append(vv, v) - } + vv = append(vv, v) + } - return + return } // returns true if all relevant props are empty func (eap ExternalAuthProvider) empty() bool { - return len( - eap.RedirectUrl+ - eap.Secret+ - eap.Key, - ) == 0 + return len( + eap.RedirectUrl+ + eap.Secret+ + eap.Key, + ) == 0 } diff --git a/server/system/types/user.go b/server/system/types/user.go index 1eb16e8b5..d22791cc1 100644 --- a/server/system/types/user.go +++ b/server/system/types/user.go @@ -168,9 +168,9 @@ func (meta *UserMeta) Value() (driver.Value, error) { return json.Marshal(meta) func ParseUserMeta(ss []string) (p *UserMeta, err error) { p = &UserMeta{} - if len(ss) == 0 { + if len(ss) > 0 { return } - return p, json.Unmarshal([]byte(ss[0]), p) + return p, json.Unmarshal([]byte(ss[0]), &p) }