From 1ce7e53398073b2ccb82f7037dd5233ff0ea3efe Mon Sep 17 00:00:00 2001 From: sauls8t Date: Mon, 3 Sep 2018 17:36:54 +0100 Subject: [PATCH] Capture LDAP configuration --- domain/auth/ldap/ad_test.go | 8 +- domain/auth/ldap/endpoint.go | 314 ++++++----- domain/auth/ldap/ldap.go | 75 ++- domain/auth/ldap/local_test.go | 31 +- gui/app/components/customize/auth-settings.js | 88 +++ gui/app/constants/constants.js | 6 +- gui/app/pods/customize/auth/route.js | 25 + gui/app/pods/customize/auth/template.hbs | 2 +- gui/app/services/global.js | 13 + gui/app/styles/bootstrap.scss | 7 +- gui/app/styles/color.scss | 1 + gui/app/styles/vendor.scss | 531 +----------------- gui/app/styles/widget/widget-list-picker.scss | 58 +- .../components/customize/auth-settings.hbs | 200 +++++-- server/routing/routes.go | 2 +- 15 files changed, 647 insertions(+), 714 deletions(-) diff --git a/domain/auth/ldap/ad_test.go b/domain/auth/ldap/ad_test.go index fbfdf4b2..9f4aaf94 100644 --- a/domain/auth/ldap/ad_test.go +++ b/domain/auth/ldap/ad_test.go @@ -31,8 +31,8 @@ var testConfigPublicAD = lm.LDAPConfig{ BaseDN: "DC=mycompany,DC=local", BindDN: "CN=ad-admin,CN=Users,DC=mycompany,DC=local", BindPassword: "8B5tNRLvbk8K", - UserFilter: "", - GroupFilter: "", + UserFilter: "(|(objectCategory=person)(objectClass=user)(objectClass=inetOrgPerson))", + GroupFilter: "(|(cn=Accounting)(cn=IT))", AttributeUserRDN: "sAMAccountName", AttributeUserFirstname: "givenName", AttributeUserLastname: "sn", @@ -43,8 +43,6 @@ var testConfigPublicAD = lm.LDAPConfig{ } func TestUserFilter_PublicAD(t *testing.T) { - testConfigPublicAD.UserFilter = "(|(objectCategory=person)(objectClass=user)(objectClass=inetOrgPerson))" - e, err := executeUserFilter(testConfigPublicAD) if err != nil { t.Error("unable to exeucte user filter", err.Error()) @@ -64,8 +62,6 @@ func TestUserFilter_PublicAD(t *testing.T) { } func TestGroupFilter_PublicAD(t *testing.T) { - testConfigPublicAD.GroupFilter = "(|(cn=Accounting)(cn=IT))" - e, err := executeGroupFilter(testConfigPublicAD) if err != nil { t.Error("unable to exeucte group filter", err.Error()) diff --git a/domain/auth/ldap/endpoint.go b/domain/auth/ldap/endpoint.go index 86a0343b..eb138af6 100644 --- a/domain/auth/ldap/endpoint.go +++ b/domain/auth/ldap/endpoint.go @@ -14,9 +14,9 @@ package ldap import ( "crypto/tls" // "database/sql" - // "encoding/json" + "encoding/json" "fmt" - // "io/ioutil" + "io/ioutil" "net/http" // "sort" // "strings" @@ -24,15 +24,15 @@ import ( "github.com/documize/community/core/env" "github.com/documize/community/core/response" // "github.com/documize/community/core/secrets" - // "github.com/documize/community/core/streamutil" + "github.com/documize/community/core/streamutil" // "github.com/documize/community/core/stringutil" "github.com/documize/community/domain" // "github.com/documize/community/domain/auth" // usr "github.com/documize/community/domain/user" ath "github.com/documize/community/model/auth" lm "github.com/documize/community/model/auth" + "github.com/documize/community/model/user" ld "gopkg.in/ldap.v2" - // "github.com/documize/community/model/user" ) // Handler contains the runtime information such as logging and database. @@ -41,6 +41,70 @@ type Handler struct { Store *domain.Store } +// Preview connects to LDAP using paylaod and returns +// first 100 users for. +// and marks Keycloak disabled users as inactive. +func (h *Handler) Preview(w http.ResponseWriter, r *http.Request) { + h.Runtime.Log.Info("Sync'ing with LDAP") + + ctx := domain.GetRequestContext(r) + if !ctx.Administrator { + response.WriteForbiddenError(w) + return + } + + var result struct { + Message string `json:"message"` + IsError bool `json:"isError"` + Users []user.User `json:"users"` + } + + // Read the request. + defer streamutil.Close(r.Body) + body, err := ioutil.ReadAll(r.Body) + if err != nil { + result.Message = "Error: unable read request body" + result.IsError = true + response.WriteJSON(w, result) + h.Runtime.Log.Error(result.Message, err) + return + } + + // Decode LDAP config. + c := lm.LDAPConfig{} + err = json.Unmarshal(body, &c) + if err != nil { + result.Message = "Error: unable read LDAP configuration payload" + result.IsError = true + response.WriteJSON(w, result) + h.Runtime.Log.Error(result.Message, err) + return + } + + h.Runtime.Log.Info("Fetching LDAP users") + + users, err := fetchUsers(c) + if err != nil { + result.Message = "Error: unable fetch users from LDAP" + result.IsError = true + response.WriteJSON(w, result) + h.Runtime.Log.Error(result.Message, err) + return + } + + result.IsError = false + result.Message = fmt.Sprintf("Sync'ed with LDAP, found %d users", len(users)) + if len(users) > 100 { + result.Users = users[:100] + } else { + result.Users = users + } + + h.Runtime.Log.Info(result.Message) + + response.WriteJSON(w, result) +} + // Sync gets list of Keycloak users and inserts new users into Documize // and marks Keycloak disabled users as inactive. func (h *Handler) Sync(w http.ResponseWriter, r *http.Request) { @@ -233,145 +297,145 @@ func (h *Handler) Sync(w http.ResponseWriter, r *http.Request) { } // Authenticate checks Keycloak authentication credentials. -// func (h *Handler) Authenticate(w http.ResponseWriter, r *http.Request) { -// method := "authenticate" -// ctx := domain.GetRequestContext(r) +func (h *Handler) Authenticate(w http.ResponseWriter, r *http.Request) { + // method := "authenticate" + // ctx := domain.GetRequestContext(r) -// defer streamutil.Close(r.Body) -// body, err := ioutil.ReadAll(r.Body) -// if err != nil { -// response.WriteBadRequestError(w, method, "Bad payload") -// h.Runtime.Log.Error(method, err) -// return -// } + // defer streamutil.Close(r.Body) + // body, err := ioutil.ReadAll(r.Body) + // if err != nil { + // response.WriteBadRequestError(w, method, "Bad payload") + // h.Runtime.Log.Error(method, err) + // return + // } -// a := ath.KeycloakAuthRequest{} -// err = json.Unmarshal(body, &a) -// if err != nil { -// response.WriteBadRequestError(w, method, err.Error()) -// h.Runtime.Log.Error(method, err) -// return -// } + // a := ath.KeycloakAuthRequest{} + // err = json.Unmarshal(body, &a) + // if err != nil { + // response.WriteBadRequestError(w, method, err.Error()) + // h.Runtime.Log.Error(method, err) + // return + // } -// a.Domain = strings.TrimSpace(strings.ToLower(a.Domain)) -// a.Domain = h.Store.Organization.CheckDomain(ctx, a.Domain) // TODO optimize by removing this once js allows empty domains -// a.Email = strings.TrimSpace(strings.ToLower(a.Email)) + // a.Domain = strings.TrimSpace(strings.ToLower(a.Domain)) + // a.Domain = h.Store.Organization.CheckDomain(ctx, a.Domain) // TODO optimize by removing this once js allows empty domains + // a.Email = strings.TrimSpace(strings.ToLower(a.Email)) -// // Check for required fields. -// if len(a.Email) == 0 { -// response.WriteUnauthorizedError(w) -// return -// } + // // Check for required fields. + // if len(a.Email) == 0 { + // response.WriteUnauthorizedError(w) + // return + // } -// org, err := h.Store.Organization.GetOrganizationByDomain(a.Domain) -// if err != nil { -// response.WriteUnauthorizedError(w) -// h.Runtime.Log.Error(method, err) -// return -// } + // org, err := h.Store.Organization.GetOrganizationByDomain(a.Domain) + // if err != nil { + // response.WriteUnauthorizedError(w) + // h.Runtime.Log.Error(method, err) + // return + // } -// ctx.OrgID = org.RefID + // ctx.OrgID = org.RefID -// // Fetch Keycloak auth provider config -// ac := ath.KeycloakConfig{} -// err = json.Unmarshal([]byte(org.AuthConfig), &ac) -// if err != nil { -// response.WriteBadRequestError(w, method, "Unable to unmarshall Keycloak Public Key") -// h.Runtime.Log.Error(method, err) -// return -// } + // // Fetch Keycloak auth provider config + // ac := ath.KeycloakConfig{} + // err = json.Unmarshal([]byte(org.AuthConfig), &ac) + // if err != nil { + // response.WriteBadRequestError(w, method, "Unable to unmarshall Keycloak Public Key") + // h.Runtime.Log.Error(method, err) + // return + // } -// // Decode and prepare RSA Public Key used by keycloak to sign JWT. -// pkb, err := secrets.DecodeBase64([]byte(ac.PublicKey)) -// if err != nil { -// response.WriteBadRequestError(w, method, "Unable to base64 decode Keycloak Public Key") -// h.Runtime.Log.Error(method, err) -// return -// } -// pk := string(pkb) -// pk = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", pk) + // // Decode and prepare RSA Public Key used by keycloak to sign JWT. + // pkb, err := secrets.DecodeBase64([]byte(ac.PublicKey)) + // if err != nil { + // response.WriteBadRequestError(w, method, "Unable to base64 decode Keycloak Public Key") + // h.Runtime.Log.Error(method, err) + // return + // } + // pk := string(pkb) + // pk = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", pk) -// // Decode and verify Keycloak JWT -// claims, err := auth.DecodeKeycloakJWT(a.Token, pk) -// if err != nil { -// response.WriteBadRequestError(w, method, err.Error()) -// h.Runtime.Log.Info("decodeKeycloakJWT failed") -// return -// } + // // Decode and verify Keycloak JWT + // claims, err := auth.DecodeKeycloakJWT(a.Token, pk) + // if err != nil { + // response.WriteBadRequestError(w, method, err.Error()) + // h.Runtime.Log.Info("decodeKeycloakJWT failed") + // return + // } -// // Compare the contents from JWT with what we have. -// // Guards against MITM token tampering. -// if a.Email != claims["email"].(string) { -// response.WriteUnauthorizedError(w) -// h.Runtime.Log.Info(">> Start Keycloak debug") -// h.Runtime.Log.Info(a.Email) -// h.Runtime.Log.Info(claims["email"].(string)) -// h.Runtime.Log.Info(">> End Keycloak debug") -// return -// } + // // Compare the contents from JWT with what we have. + // // Guards against MITM token tampering. + // if a.Email != claims["email"].(string) { + // response.WriteUnauthorizedError(w) + // h.Runtime.Log.Info(">> Start Keycloak debug") + // h.Runtime.Log.Info(a.Email) + // h.Runtime.Log.Info(claims["email"].(string)) + // h.Runtime.Log.Info(">> End Keycloak debug") + // return + // } -// h.Runtime.Log.Info("keycloak logon attempt " + a.Email + " @ " + a.Domain) + // h.Runtime.Log.Info("keycloak logon attempt " + a.Email + " @ " + a.Domain) -// u, err := h.Store.User.GetByDomain(ctx, a.Domain, a.Email) -// if err != nil && err != sql.ErrNoRows { -// response.WriteServerError(w, method, err) -// h.Runtime.Log.Error(method, err) -// return -// } + // u, err := h.Store.User.GetByDomain(ctx, a.Domain, a.Email) + // if err != nil && err != sql.ErrNoRows { + // response.WriteServerError(w, method, err) + // h.Runtime.Log.Error(method, err) + // return + // } -// // Create user account if not found -// if err == sql.ErrNoRows { -// h.Runtime.Log.Info("keycloak add user " + a.Email + " @ " + a.Domain) + // // Create user account if not found + // if err == sql.ErrNoRows { + // h.Runtime.Log.Info("keycloak add user " + a.Email + " @ " + a.Domain) -// u = user.User{} -// u.Firstname = a.Firstname -// u.Lastname = a.Lastname -// u.Email = a.Email -// u.Initials = stringutil.MakeInitials(u.Firstname, u.Lastname) -// u.Salt = secrets.GenerateSalt() -// u.Password = secrets.GeneratePassword(secrets.GenerateRandomPassword(), u.Salt) + // u = user.User{} + // u.Firstname = a.Firstname + // u.Lastname = a.Lastname + // u.Email = a.Email + // u.Initials = stringutil.MakeInitials(u.Firstname, u.Lastname) + // u.Salt = secrets.GenerateSalt() + // u.Password = secrets.GeneratePassword(secrets.GenerateRandomPassword(), u.Salt) -// err = addUser(ctx, h.Runtime, h.Store, u, ac.DefaultPermissionAddSpace) -// if err != nil { -// response.WriteServerError(w, method, err) -// h.Runtime.Log.Error(method, err) -// return -// } -// } + // err = addUser(ctx, h.Runtime, h.Store, u, ac.DefaultPermissionAddSpace) + // if err != nil { + // response.WriteServerError(w, method, err) + // h.Runtime.Log.Error(method, err) + // return + // } + // } -// // Password correct and active user -// if a.Email != strings.TrimSpace(strings.ToLower(u.Email)) { -// response.WriteUnauthorizedError(w) -// return -// } + // // Password correct and active user + // if a.Email != strings.TrimSpace(strings.ToLower(u.Email)) { + // response.WriteUnauthorizedError(w) + // return + // } -// // Attach user accounts and work out permissions. -// usr.AttachUserAccounts(ctx, *h.Store, org.RefID, &u) + // // Attach user accounts and work out permissions. + // usr.AttachUserAccounts(ctx, *h.Store, org.RefID, &u) -// // No accounts signals data integrity problem -// // so we reject login request. -// if len(u.Accounts) == 0 { -// response.WriteUnauthorizedError(w) -// h.Runtime.Log.Error(method, err) -// return -// } + // // No accounts signals data integrity problem + // // so we reject login request. + // if len(u.Accounts) == 0 { + // response.WriteUnauthorizedError(w) + // h.Runtime.Log.Error(method, err) + // return + // } -// // Abort login request if account is disabled. -// for _, ac := range u.Accounts { -// if ac.OrgID == org.RefID { -// if ac.Active == false { -// response.WriteUnauthorizedError(w) -// h.Runtime.Log.Error(method, err) -// return -// } -// break -// } -// } + // // Abort login request if account is disabled. + // for _, ac := range u.Accounts { + // if ac.OrgID == org.RefID { + // if ac.Active == false { + // response.WriteUnauthorizedError(w) + // h.Runtime.Log.Error(method, err) + // return + // } + // break + // } + // } -// // Generate JWT token -// authModel := ath.AuthenticationModel{} -// authModel.Token = auth.GenerateJWT(h.Runtime, u.RefID, org.RefID, a.Domain) -// authModel.User = u + // // Generate JWT token + // authModel := ath.AuthenticationModel{} + // authModel.Token = auth.GenerateJWT(h.Runtime, u.RefID, org.RefID, a.Domain) + // authModel.User = u -// response.WriteJSON(w, authModel) -// } + // response.WriteJSON(w, authModel) +} diff --git a/domain/auth/ldap/ldap.go b/domain/auth/ldap/ldap.go index b3019ce9..8beb0be6 100644 --- a/domain/auth/ldap/ldap.go +++ b/domain/auth/ldap/ldap.go @@ -16,8 +16,9 @@ import ( "fmt" "strings" + "github.com/documize/community/core/stringutil" lm "github.com/documize/community/model/auth" - // "github.com/documize/community/model/user" + "github.com/documize/community/model/user" "github.com/pkg/errors" ld "gopkg.in/ldap.v2" ) @@ -201,9 +202,79 @@ func executeGroupFilter(c lm.LDAPConfig) (u []lm.LDAPUser, err error) { func extractUser(c lm.LDAPConfig, e *ld.Entry) (u lm.LDAPUser) { u.Firstname = e.GetAttributeValue(c.AttributeUserFirstname) u.Lastname = e.GetAttributeValue(c.AttributeUserLastname) - u.Email = e.GetAttributeValue(c.AttributeUserEmail) + u.Email = strings.ToLower(e.GetAttributeValue(c.AttributeUserEmail)) u.RemoteID = e.GetAttributeValue(c.AttributeUserRDN) u.CN = e.GetAttributeValue("cn") + // Make name elements from DisplayName if we can. + if (len(u.Firstname) == 0 || len(u.Firstname) == 0) && + len(e.GetAttributeValue(c.AttributeUserDisplayName)) > 0 { + } + + if len(u.Firstname) == 0 { + u.Firstname = "Empty" + } + if len(u.Lastname) == 0 { + u.Lastname = "Empty" + } + + return +} + +// ConvertUsers creates a unique list of users using email as primary key. +// The result is a collection of Documize user objects. +func convertUsers(c lm.LDAPConfig, lu []lm.LDAPUser) (du []user.User) { + for _, i := range lu { + add := true + for _, j := range du { + if len(j.Email) > 0 && i.Email == j.Email { + add = false + break + } + } + // skip if empty email address + add = len(i.Email) > 0 + if add { + nu := user.User{} + nu.Editor = c.DefaultPermissionAddSpace + nu.Active = true + nu.Email = i.Email + nu.ViewUsers = false + nu.Analytics = false + nu.Admin = false + nu.Global = false + nu.Firstname = i.Firstname + nu.Lastname = i.Lastname + nu.Initials = stringutil.MakeInitials(i.Firstname, i.Lastname) + + du = append(du, nu) + } + } + + return +} + +// FetchUsers from LDAP server using both User and Group filters. +func fetchUsers(c lm.LDAPConfig) (du []user.User, err error) { + du = []user.User{} + + e1, err := executeUserFilter(c) + if err != nil { + err = errors.Wrap(err, "unable to execute user filter") + return + } + + e2, err := executeGroupFilter(c) + if err != nil { + err = errors.Wrap(err, "unable to execute group filter") + return + } + + // convert users from LDAP format to Documize format. + e3 := []lm.LDAPUser{} + e3 = append(e3, e1...) + e3 = append(e3, e2...) + du = convertUsers(c, e3) + return } diff --git a/domain/auth/ldap/local_test.go b/domain/auth/ldap/local_test.go index 6e762147..2dcc5c6d 100644 --- a/domain/auth/ldap/local_test.go +++ b/domain/auth/ldap/local_test.go @@ -60,6 +60,32 @@ func TestUserFilter_LocalLDAP(t *testing.T) { } } +func TestDualFilters_LocalLDAP(t *testing.T) { + testConfigLocalLDAP.UserFilter = "(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))" + e1, err := executeUserFilter(testConfigLocalLDAP) + if err != nil { + t.Error("unable to exeucte user filter", err.Error()) + return + } + + testConfigLocalLDAP.GroupFilter = "(&(objectClass=group)(|(cn=ship_crew)(cn=admin_staff)))" + e2, err := executeGroupFilter(testConfigLocalLDAP) + if err != nil { + t.Error("unable to exeucte group filter", err.Error()) + return + } + + e3 := []lm.LDAPUser{} + e3 = append(e3, e1...) + e3 = append(e3, e2...) + users := convertUsers(testConfigLocalLDAP, e3) + + for _, u := range users { + t.Logf("(%s %s) @ %s\n", + u.Firstname, u.Lastname, u.Email) + } +} + func TestGroupFilter_LocalLDAP(t *testing.T) { testConfigLocalLDAP.GroupFilter = "(&(objectClass=group)(|(cn=ship_crew)(cn=admin_staff)))" @@ -74,11 +100,6 @@ func TestGroupFilter_LocalLDAP(t *testing.T) { } t.Logf("LDAP group search entries found: %d", len(e)) - - for _, u := range e { - t.Logf("[%s] %s (%s %s) @ %s\n", - u.RemoteID, u.CN, u.Firstname, u.Lastname, u.Email) - } } func TestAuthenticate_LocalLDAP(t *testing.T) { diff --git a/gui/app/components/customize/auth-settings.js b/gui/app/components/customize/auth-settings.js index d9d12950..9349d27f 100644 --- a/gui/app/components/customize/auth-settings.js +++ b/gui/app/components/customize/auth-settings.js @@ -20,12 +20,17 @@ import Component from '@ember/component'; export default Component.extend(Notifier, { appMeta: service(), + isDocumizeProvider: computed('authProvider', function() { return this.get('authProvider') === this.get('constants').AuthProvider.Documize; }), isKeycloakProvider: computed('authProvider', function() { return this.get('authProvider') === this.get('constants').AuthProvider.Keycloak; }), + isLDAPProvider: computed('authProvider', function() { + return this.get('authProvider') === this.get('constants').AuthProvider.LDAP; + }), + KeycloakUrlError: empty('keycloakConfig.url'), KeycloakRealmError: empty('keycloakConfig.realm'), KeycloakClientIdError: empty('keycloakConfig.clientId'), @@ -34,9 +39,28 @@ export default Component.extend(Notifier, { KeycloakAdminPasswordError: empty('keycloakConfig.adminPassword'), keycloakFailure: '', + ldapErrorServerHost: empty('ldapConfig.serverHost'), + ldapErrorServerPort: computed('ldapConfig.serverPort', function() { + return is.empty(this.get('ldapConfig.serverPort')) || is.not.number(parseInt(this.get('ldapConfig.serverPort'))); + }), + ldapErrorBindDN: empty('ldapConfig.bindDN'), + ldapErrorBindPassword: empty('ldapConfig.bindPassword'), + ldapErrorNoFilter: computed('ldapConfig.{userFilter,groupFilter}', function() { + return is.empty(this.get('ldapConfig.userFilter')) && is.empty(this.get('ldapConfig.groupFilter')); + }), + ldapErrorAttributeUserRDN: empty('ldapConfig.attributeUserRDN'), + ldapErrorAttributeUserFirstname: empty('ldapConfig.attributeUserFirstname'), + ldapErrorAttributeUserLastname: empty('ldapConfig.attributeUserLastname'), + ldapErrorAttributeUserEmail: empty('ldapConfig.attributeUserEmail'), + ldapErrorAttributeGroupMember: computed('ldapConfig.{groupFilter,attributeGroupMember}', function() { + return is.not.empty(this.get('ldapConfig.groupFilter')) && is.empty(this.get('ldapConfig.attributeGroupMember')); + }), + init() { this._super(...arguments); + let constants = this.get('constants'); + this.keycloakConfig = { url: '', realm: '', @@ -48,6 +72,27 @@ export default Component.extend(Notifier, { disableLogout: false, defaultPermissionAddSpace: false }; + + this.ldapConfig = { + serverType: constants.AuthProvider.ServerTypeLDAP, + serverHost: '', + serverPort: 389, + encryptionType: constants.AuthProvider.EncryptionTypeStartTLS, + baseDN: "", + bindDN: "cn=admin,dc=planetexpress,dc=com", + bindPassword: "GoodNewsEveryone", + userFilter: "(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))", + groupFilter: "(&(objectClass=group)(|(cn=ship_crew)(cn=admin_staff)))", + attributeUserRDN: "uid", + attributeUserFirstname: "givenName", + attributeUserLastname: "sn", + attributeUserEmail: "mail", + attributeUserDisplayName: "", + attributeUserGroupName: "", + attributeGroupMember: "member", + disableLogout: false, + defaultPermissionAddSpace: false + }; }, didReceiveAttrs() { @@ -60,6 +105,7 @@ export default Component.extend(Notifier, { case constants.AuthProvider.Documize: // nothing to do break; + case constants.AuthProvider.Keycloak: // eslint-disable-line no-case-declarations let config = this.get('authConfig'); @@ -74,6 +120,19 @@ export default Component.extend(Notifier, { this.set('keycloakConfig', config); break; + + case constants.AuthProvider.LDAP: // eslint-disable-line no-case-declarations + let ldapConfig = this.get('authConfig'); + + if (is.undefined(ldapConfig) || is.null(ldapConfig) || is.empty(ldapConfig) ) { + ldapConfig = {}; + } else { + ldapConfig.defaultPermissionAddSpace = ldapConfig.hasOwnProperty('defaultPermissionAddSpace') ? ldapConfig.defaultPermissionAddSpace : false; + ldapConfig.disableLogout = ldapConfig.hasOwnProperty('disableLogout') ? ldapConfig.disableLogout : true; + } + + this.set('ldapConfig', ldapConfig); + break; } }, @@ -88,6 +147,15 @@ export default Component.extend(Notifier, { this.set('authProvider', constants.AuthProvider.Keycloak); }, + onLDAP() { + let constants = this.get('constants'); + this.set('authProvider', constants.AuthProvider.LDAP); + }, + + onLDAPEncryption(e) { + this.set('ldapConfig.encryptionType', e); + }, + onSave() { let constants = this.get('constants'); let provider = this.get('authProvider'); @@ -99,6 +167,7 @@ export default Component.extend(Notifier, { case constants.AuthProvider.Documize: config = {}; break; + case constants.AuthProvider.Keycloak: if (this.get('KeycloakUrlError')) { this.$("#keycloak-url").focus(); @@ -142,13 +211,31 @@ export default Component.extend(Notifier, { set(config, 'publicKey', encoding.Base64.encode(this.get('keycloakConfig.publicKey'))); break; + + case constants.AuthProvider.LDAP: + if (this.get('ldapErrorServerHost')) { + this.$("#ldap-host").focus(); + return; + } + if (this.get('ldapErrorServerPort')) { + this.$("#ldap-port").focus(); + return; + } + + config = copy(this.get('ldapConfig')); + config.serverHost = config.serverHost.trim(); + config.serverPort = parseInt(this.get('ldapConfig.serverPort')); + break; } + debugger; + this.showWait(); let data = { authProvider: provider, authConfig: JSON.stringify(config) }; this.get('onSave')(data).then(() => { + // Without sync we cannot log in if (data.authProvider === constants.AuthProvider.Keycloak) { this.get('onSync')().then((response) => { if (response.isError) { @@ -165,6 +252,7 @@ export default Component.extend(Notifier, { } }); } + this.showDone(); }); } diff --git a/gui/app/constants/constants.js b/gui/app/constants/constants.js index ea526018..cd63062e 100644 --- a/gui/app/constants/constants.js +++ b/gui/app/constants/constants.js @@ -24,7 +24,11 @@ let constants = EmberObject.extend({ AuthProvider: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects Documize: 'documize', Keycloak: 'keycloak', - LDAP: 'ldap' + LDAP: 'ldap', + ServerTypeLDAP: 'ldap', + ServerTypeAD: 'ad', + EncryptionTypeNone: 'none', + EncryptionTypeStartTLS: 'starttls' }, DocumentActionType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects diff --git a/gui/app/pods/customize/auth/route.js b/gui/app/pods/customize/auth/route.js index 1ac01de0..d9669646 100644 --- a/gui/app/pods/customize/auth/route.js +++ b/gui/app/pods/customize/auth/route.js @@ -26,11 +26,36 @@ export default Route.extend(AuthenticatedRouteMixin, { }, model() { + let constants = this.get('constants'); + let data = { authProvider: this.get('appMeta.authProvider'), authConfig: null, }; + let config = { + ServerType: constants.AuthProvider.ServerTypeLDAP, + ServerHost: "127.0.0.1", + ServerPort: 389, + EncryptionType: constants.AuthProvider.EncryptionTypeStartTLS, + BaseDN: "ou=people,dc=planetexpress,dc=com", + BindDN: "cn=admin,dc=planetexpress,dc=com", + BindPassword: "GoodNewsEveryone", + UserFilter: "(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))", + GroupFilter: "(&(objectClass=group)(|(cn=ship_crew)(cn=admin_staff)))", + AttributeUserRDN: "uid", + AttributeUserFirstname: "givenName", + AttributeUserLastname: "sn", + AttributeUserEmail: "mail", + AttributeUserDisplayName: "", + AttributeUserGroupName: "", + AttributeGroupMember: "member", + }; + + this.get('global').previewLDAP(config).then((r) => { + console.log(r); + }); + return new EmberPromise((resolve) => { let constants = this.get('constants'); diff --git a/gui/app/pods/customize/auth/template.hbs b/gui/app/pods/customize/auth/template.hbs index c9a75bd0..f2a921ab 100644 --- a/gui/app/pods/customize/auth/template.hbs +++ b/gui/app/pods/customize/auth/template.hbs @@ -1,2 +1,2 @@ -{{customize/auth-settings authProvider=model.authProvider authConfig=model.authConfig +{{customize/auth-settings authProvider=model.authProvider authConfig=model.authConfig onSave=(action 'onSave') onSync=(action 'onSync') onChange=(action 'onChange')}} diff --git a/gui/app/services/global.js b/gui/app/services/global.js index a02a8c31..77a0b123 100644 --- a/gui/app/services/global.js +++ b/gui/app/services/global.js @@ -106,6 +106,19 @@ export default Service.extend({ } }, + previewLDAP(config) { + if(this.get('sessionService.isAdmin')) { + return this.get('ajax').request(`global/sync/ldap/preview`, { + method: 'POST', + data: JSON.stringify(config) + }).then((response) => { + return response; + }).catch((error) => { + return error; + }); + } + }, + // Returns product license. searchStatus() { if (this.get('sessionService.isGlobalAdmin')) { diff --git a/gui/app/styles/bootstrap.scss b/gui/app/styles/bootstrap.scss index 8b2a1061..d0f4236b 100644 --- a/gui/app/styles/bootstrap.scss +++ b/gui/app/styles/bootstrap.scss @@ -80,9 +80,14 @@ $input-btn-focus-color: rgba($color-primary, .25); font-weight: 500; } - > small { + > small, > div[class*="col"] > small { font-size: 1rem; } + + > small.highlight, > div[class*="col"] > small.highlight { + font-size: 1rem; + color: $color-orange !important; + } } // links diff --git a/gui/app/styles/color.scss b/gui/app/styles/color.scss index ecebf79f..820f62f8 100644 --- a/gui/app/styles/color.scss +++ b/gui/app/styles/color.scss @@ -39,6 +39,7 @@ $color-red: #9E0D1F; $color-green: #348A37; $color-blue: #2667af; $color-goldy: #FFD700; +$color-yellow: #fff8dc; $color-orange: #FFAD15; // widgets diff --git a/gui/app/styles/vendor.scss b/gui/app/styles/vendor.scss index 563cba37..b4f4748f 100644 --- a/gui/app/styles/vendor.scss +++ b/gui/app/styles/vendor.scss @@ -870,533 +870,14 @@ pre.code-toolbar > .toolbar span:focus { /* FINISH PrismJS */ -/* START Overlay Scrollbars */ +/* START x-toggle */ -/*! - * OverlayScrollbars - * https://github.com/KingSora/OverlayScrollbars - * - * Version: 1.4.5 - * - * Copyright KingSora. - * https://github.com/KingSora - * - * Released under the MIT license. - * Date: 18.05.2018 - */ - -/* -OVERLAY SCROLLBARS CORE: -*/ - -html.os-html, -html.os-html > .os-host { - display: block; - overflow: hidden; - box-sizing: border-box; - height: 100% !important; - width: 100% !important; - min-width: 100% !important; - min-height: 100% !important; - margin: 0 !important; - position: absolute !important; /* could be position: fixed; but it causes issues on iOS (-webkit-overflow-scrolling: touch) */ -} -html.os-html > .os-host > .os-padding { - position: absolute; /* could be position: fixed; but it causes issues on iOS (-webkit-overflow-scrolling: touch) */ -} -body.os-dragging, -body.os-dragging * { - cursor: default; -} -.os-host, -.os-host-textarea { - position: relative; - overflow: visible !important; -} -#hs-dummy-scrollbar-size { - position: fixed; - opacity: 0; - -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=0)'; - visibility: hidden; - overflow: scroll; - height: 500px; - width: 500px; -} -#hs-dummy-scrollbar-size, -.os-viewport { - -ms-overflow-style: scrollbar !important; -} -.os-viewport-native-scrollbars-invisible#hs-dummy-scrollbar-size::-webkit-scrollbar, -.os-viewport-native-scrollbars-invisible.os-viewport::-webkit-scrollbar, -.os-viewport-native-scrollbars-invisible#hs-dummy-scrollbar-size::-webkit-scrollbar-corner, -.os-viewport-native-scrollbars-invisible.os-viewport::-webkit-scrollbar-corner { - display: none !important; - width: 0px !important; - height: 0px !important; - visibility: hidden !important; - background: transparent !important; -} -.os-content-glue { - max-height: 100%; - max-width: 100%; - width: 100%; - pointer-events: none; -} -.os-padding { - direction: inherit; - position: absolute; - overflow: visible; - padding: 0; - margin: 0; - left: 0; - top: 0; - bottom: 0; - right: 0; - width: auto !important; - height: auto !important; - z-index: 1; -} -.os-host-overflow > .os-padding { - overflow: hidden; -} -.os-viewport { - direction: inherit !important; - box-sizing: inherit !important; - resize: none !important; - position: absolute; - overflow: hidden; - top: 0; - left: 0; - bottom: 0; - right: 0; - padding: 0; - margin: 0; - -webkit-overflow-scrolling: touch; -} -.os-content-arrange { - position: absolute; - z-index: -1; - min-height: 1px; - min-width: 1px; - pointer-events: none; -} -.os-content { - direction: inherit; - box-sizing: border-box !important; - position: relative; - display: block; - height: 100%; - width: 100%; - height: 100%; - width: 100%; - visibility: visible; -} -.os-host-textarea > .os-padding > .os-content { - overflow: hidden !important; -} -.os-content > .os-textarea { - direction: inherit !important; - float: none !important; - margin: 0 !important; - max-height: none !important; - max-width: none !important; - border: none !important; - border-radius: 0px !important; - background: transparent !important; - outline: 0px none transparent !important; - overflow: hidden !important; - resize: none !important; - position: absolute !important; - top: 0 !important; - left: 0 !important; - z-index: 1; - padding: 0px; -} -.os-host-rtl > .os-padding > .os-viewport > .os-content > .os-textarea { - right: 0 !important; -} -.os-content > .os-textarea-cover { - z-index: -1; - pointer-events: none; -} -.os-content > .os-textarea[wrap='off'] { - white-space: pre !important; - margin: 0px !important; -} -.os-text-inherit { - font-family: inherit; - font-size: inherit; - font-weight: inherit; - font-style: inherit; - font-variant: inherit; - text-transform: inherit; - text-decoration: inherit; - text-indent: inherit; - text-align: inherit; - text-shadow: inherit; - text-overflow: inherit; - letter-spacing: inherit; - word-spacing: inherit; - line-height: inherit; - unicode-bidi: inherit; - direction: inherit; - color: inherit; - cursor: text; -} -.os-resize-observer, -.os-resize-observer-host { - box-sizing: inherit; - display: block; - opacity: 0; - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 100%; - overflow: hidden; - pointer-events: none; - z-index: -1; -} -.os-resize-observer-host { - padding: inherit; - border: inherit; - border-color: transparent; - border-style: solid; - box-sizing: border-box; -} -.os-resize-observer-host:after { - content: ''; -} -.os-resize-observer-host > .os-resize-observer, -.os-resize-observer-host:after { - height: 200%; - width: 200%; - padding: inherit; - border: inherit; - margin: 0px; - display: block; - box-sizing: content-box; -} -.os-resize-observer.observed, -object.os-resize-observer { - box-sizing: border-box !important; -} -.os-size-auto-observer { - box-sizing: inherit !important; - height: 100%; - width: inherit; - max-width: 1px; - position: relative; - float: left; - max-height: 1px; - margin-top: -1px; - overflow: hidden; - z-index: -1; - padding: 0; - margin: 0; - pointer-events: none; -} -.os-size-auto-observer > .os-resize-observer { - width: 1000%; - height: 1000%; - min-height: 1px; - min-width: 1px; -} -.os-resize-observer-item { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - overflow: hidden; - z-index: -1; - opacity: 0; - direction: ltr !important; - -webkit-box-flex: 0 !important; - -ms-flex: none !important; - flex: none !important; -} -.os-resize-observer-item-final { - position: absolute; - left: 0; - top: 0; - -webkit-transition: none !important; - transition: none !important; - -webkit-box-flex: 0 !important; - -ms-flex: none !important; - flex: none !important; -} -.os-resize-observer { - -webkit-animation-duration: 0.001s; - animation-duration: 0.001s; - -webkit-animation-name: hs-resize-observer-dummy-animation; - animation-name: hs-resize-observer-dummy-animation; +input[type="text"], textarea { + cursor: text !important; } -/* -CUSTOM SCROLLBARS AND CORNER CORE: -*/ - -.os-host-transition > .os-scrollbar, -.os-host-transition > .os-scrollbar-corner { - -webkit-transition: opacity 0.3s, visibility 0.3s, top 0.3s, right 0.3s, bottom 0.3s, left 0.3s; - transition: opacity 0.3s, visibility 0.3s, top 0.3s, right 0.3s, bottom 0.3s, left 0.3s; -} -html.os-html > .os-host > .os-scrollbar { - position: absolute; /* could be position: fixed; but it causes issues on iOS (-webkit-overflow-scrolling: touch) */ - z-index: 999999; /* highest z-index of the page */ -} -.os-scrollbar, -.os-scrollbar-corner { - position: absolute; - opacity: 1; - -ms-filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=100)'; - z-index: 1; -} -.os-scrollbar-corner { - bottom: 0; - right: 0; -} -.os-scrollbar { - pointer-events: none; -} -.os-scrollbar-track { - pointer-events: auto; - position: relative; - height: 100%; - width: 100%; - padding: 0 !important; - border: none !important; -} -.os-scrollbar-handle { - pointer-events: auto; - position: absolute; - width: 100%; - height: 100%; -} -.os-scrollbar-handle-off, -.os-scrollbar-track-off { - pointer-events: none; -} -.os-scrollbar.os-scrollbar-unusable, -.os-scrollbar.os-scrollbar-unusable * { - pointer-events: none !important; -} -.os-scrollbar.os-scrollbar-unusable .os-scrollbar-handle { - opacity: 0 !important; -} -.os-scrollbar-horizontal { - bottom: 0; - left: 0; -} -.os-scrollbar-vertical { - top: 0; - right: 0; -} -.os-host-rtl > .os-scrollbar-horizontal { - right: 0; -} -.os-host-rtl > .os-scrollbar-vertical { - right: auto; - left: 0; -} -.os-host-rtl > .os-scrollbar-corner { - right: auto; - left: 0; -} -.os-scrollbar-auto-hidden, -.os-padding + .os-scrollbar-corner, -.os-host-resize-disabled.os-host-scrollbar-horizontal-hidden > .os-scrollbar-corner, -.os-host-scrollbar-horizontal-hidden > .os-scrollbar-horizontal, -.os-host-resize-disabled.os-host-scrollbar-vertical-hidden > .os-scrollbar-corner, -.os-host-scrollbar-vertical-hidden > .os-scrollbar-vertical, -.os-scrollbar-horizontal.os-scrollbar-auto-hidden + .os-scrollbar-vertical + .os-scrollbar-corner, -.os-scrollbar-horizontal + .os-scrollbar-vertical.os-scrollbar-auto-hidden + .os-scrollbar-corner, -.os-scrollbar-horizontal.os-scrollbar-auto-hidden + .os-scrollbar-vertical.os-scrollbar-auto-hidden + .os-scrollbar-corner { - opacity: 0; - visibility: hidden; - pointer-events: none; -} -.os-scrollbar-corner-resize-both { - cursor: nwse-resize; -} -.os-host-rtl > .os-scrollbar-corner-resize-both { - cursor: nesw-resize; -} -.os-scrollbar-corner-resize-horizontal { - cursor: ew-resize; -} -.os-scrollbar-corner-resize-vertical { - cursor: ns-resize; -} -.os-dragging .os-scrollbar-corner.os-scrollbar-corner-resize { - cursor: default; -} -.os-host-resize-disabled.os-host-scrollbar-horizontal-hidden > .os-scrollbar-vertical { - top: 0; - bottom: 0; -} -.os-host-resize-disabled.os-host-scrollbar-vertical-hidden > .os-scrollbar-horizontal { - right: 0; - left: 0; -} -.os-scrollbar:hover, -.os-scrollbar-corner.os-scrollbar-corner-resize { - opacity: 1 !important; - visibility: visible !important; -} -.os-scrollbar-corner.os-scrollbar-corner-resize { - background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB3aWR0aD0iMTAiICAgaGVpZ2h0PSIxMCIgICB2ZXJzaW9uPSIxLjEiPiAgPGcgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsLTEwNDIuMzYyMikiICAgICBzdHlsZT0iZGlzcGxheTppbmxpbmUiPiAgICA8cGF0aCAgICAgICBzdHlsZT0iZmlsbDojMDAwMDAwO2ZpbGwtb3BhY2l0eTowLjQ5NDExNzY1O2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTpub25lIiAgICAgICBkPSJtIDcuNDI0MjE4NywxMDQyLjM2MjIgYyAtMC43MjM1NzkyLDAgLTEuMzEwMTU2MiwwLjU4NjYgLTEuMzEwMTU2MiwxLjMxMDIgMCwwLjI5OSAwLjEwNDM0MTksMC41NzEgMC4yNzI5NDkyLDAuNzkxNSAwLjIwOTEwMjQsMC4xNDEzIDAuNDY1NjIwNiwwLjIxODQgMC43MzY5NjI5LDAuMjE4NCAwLjcyMzU3OTMsMCAxLjMxMDE1NjMsLTAuNTg2NiAxLjMxMDE1NjMsLTEuMzEwMiAwLC0wLjI3MTMgLTAuMDc3MDkzLC0wLjUyNzggLTAuMjE4MzU5NCwtMC43MzcgLTAuMjIwNDk0MSwtMC4xNjg2IC0wLjQ5MjU0NDMsLTAuMjcyOSAtMC43OTE1NTI4LC0wLjI3MjkgeiBtIDAsMy4wODQzIGMgLTAuNzIzNTc5MiwwIC0xLjMxMDE1NjIsMC41ODY2IC0xLjMxMDE1NjIsMS4zMTAyIDAsMC4yOTkgMC4xMDQzNDE5LDAuNTcxIDAuMjcyOTQ5MiwwLjc5MTUgMC4yMDkxMDI0LDAuMTQxMyAwLjQ2NTYyMDYsMC4yMTg0IDAuNzM2OTYyOSwwLjIxODQgMC43MjM1NzkzLDAgMS4zMTAxNTYzLC0wLjU4NjYgMS4zMTAxNTYzLC0xLjMxMDIgMCwtMC4yNzEzIC0wLjA3NzA5MywtMC41Mjc4IC0wLjIxODM1OTQsLTAuNzM2OSAtMC4yMjA0OTQxLC0wLjE2ODYgLTAuNDkyNTQ0MywtMC4yNzMgLTAuNzkxNTUyOCwtMC4yNzMgeiBtIC0zLjA4NDMyNjEsMCBjIC0wLjcyMzU3OTMsMCAtMS4zMTAxNTYzLDAuNTg2NiAtMS4zMTAxNTYzLDEuMzEwMiAwLDAuMjk5IDAuMTA0MzQxOSwwLjU3MSAwLjI3Mjk0OTIsMC43OTE1IDAuMjA5MTAyNCwwLjE0MTMgMC40NjU2MjA3LDAuMjE4NCAwLjczNjk2MjksMC4yMTg0IDAuNzIzNTc5MywwIDEuMzEwMTU2MywtMC41ODY2IDEuMzEwMTU2MywtMS4zMTAyIDAsLTAuMjcxMyAtMC4wNzcwOTMsLTAuNTI3OCAtMC4yMTgzNTk0LC0wLjczNjkgLTAuMjIwNDk0LC0wLjE2ODYgLTAuNDkyNTQ0MiwtMC4yNzMgLTAuNzkxNTUyNywtMC4yNzMgeiBtIC0zLjAyOTczNjQsMy4wMjk4IEMgMC41ODY1NzY5MywxMDQ4LjQ3NjMgMCwxMDQ5LjA2MjggMCwxMDQ5Ljc4NjQgYyAwLDAuMjk5IDAuMTA0MzQxOSwwLjU3MTEgMC4yNzI5NDkyMiwwLjc5MTYgMC4yMDkxMDIyOSwwLjE0MTIgMC40NjU2MjA2NSwwLjIxODMgMC43MzY5NjI4OCwwLjIxODMgMC43MjM1NzkzLDAgMS4zMTAxNTYzLC0wLjU4NjUgMS4zMTAxNTYzLC0xLjMxMDEgMCwtMC4yNzE0IC0wLjA3NzA5MywtMC41Mjc5IC0wLjIxODM1OTQsLTAuNzM3IC0wLjIyMDQ5NDEsLTAuMTY4NiAtMC40OTI1NDQzLC0wLjI3MjkgLTAuNzkxNTUyOCwtMC4yNzI5IHogbSAzLjAyOTczNjQsMCBjIC0wLjcyMzU3OTMsMCAtMS4zMTAxNTYzLDAuNTg2NSAtMS4zMTAxNTYzLDEuMzEwMSAwLDAuMjk5IDAuMTA0MzQxOSwwLjU3MTEgMC4yNzI5NDkyLDAuNzkxNiAwLjIwOTEwMjQsMC4xNDEyIDAuNDY1NjIwNywwLjIxODMgMC43MzY5NjI5LDAuMjE4MyAwLjcyMzU3OTMsMCAxLjMxMDE1NjMsLTAuNTg2NSAxLjMxMDE1NjMsLTEuMzEwMSAwLC0wLjI3MTQgLTAuMDc3MDkzLC0wLjUyNzkgLTAuMjE4MzU5NCwtMC43MzcgLTAuMjIwNDk0LC0wLjE2ODYgLTAuNDkyNTQ0MiwtMC4yNzI5IC0wLjc5MTU1MjcsLTAuMjcyOSB6IG0gMy4wODQzMjYxLDAgYyAtMC43MjM1NzkyLDAgLTEuMzEwMTU2MiwwLjU4NjUgLTEuMzEwMTU2MiwxLjMxMDEgMCwwLjI5OSAwLjEwNDM0MTksMC41NzExIDAuMjcyOTQ5MiwwLjc5MTYgMC4yMDkxMDI0LDAuMTQxMiAwLjQ2NTYyMDYsMC4yMTgzIDAuNzM2OTYyOSwwLjIxODMgMC43MjM1NzkzLDAgMS4zMTAxNTYzLC0wLjU4NjUgMS4zMTAxNTYzLC0xLjMxMDEgMCwtMC4yNzE0IC0wLjA3NzA5MywtMC41Mjc5IC0wLjIxODM1OTQsLTAuNzM3IC0wLjIyMDQ5NDEsLTAuMTY4NiAtMC40OTI1NDQzLC0wLjI3MjkgLTAuNzkxNTUyOCwtMC4yNzI5IHoiLz4gIDwvZz4gIDxnICAgICBzdHlsZT0iZGlzcGxheTppbmxpbmUiPiAgICA8cGF0aCAgICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTpub25lIiAgICAgICBkPSJtIDguMjE1NzcxNSwwLjI3Mjk0OTIyIGMgMC4xNDEyNjY3LDAuMjA5MTAyMjkgMC4yMTgzNTk0LDAuNDY1NjIwNjUgMC4yMTgzNTk0LDAuNzM2OTYyODggMCwwLjcyMzU3OTMgLTAuNTg2NTc3LDEuMzEwMTU2MyAtMS4zMTAxNTYzLDEuMzEwMTU2MyAtMC4yNzEzNDIzLDAgLTAuNTI3ODYwNSwtMC4wNzcwOTMgLTAuNzM2OTYyOSwtMC4yMTgzNTk0IDAuMjM5NDEwNCwwLjMxMzA4NTkgMC42MTI2MzYyLDAuNTE4NjAzNSAxLjAzNzIwNywwLjUxODYwMzUgMC43MjM1NzkzLDAgMS4zMTAxNTYzLC0wLjU4NjU3NyAxLjMxMDE1NjMsLTEuMzEwMTU2MyAwLC0wLjQyNDU3MDc2IC0wLjIwNTUxNzYsLTAuNzk3Nzk2NTkgLTAuNTE4NjAzNSwtMS4wMzcyMDY5OCB6IG0gMCwzLjA4NDMyNjE4IGMgMC4xNDEyNjY3LDAuMjA5MTAyMyAwLjIxODM1OTQsMC40NjU2MjA2IDAuMjE4MzU5NCwwLjczNjk2MjkgMCwwLjcyMzU3OTMgLTAuNTg2NTc3LDEuMzEwMTU2MiAtMS4zMTAxNTYzLDEuMzEwMTU2MiAtMC4yNzEzNDIzLDAgLTAuNTI3ODYwNSwtMC4wNzcwOTMgLTAuNzM2OTYyOSwtMC4yMTgzNTkzIDAuMjM5NDEwNCwwLjMxMzA4NTkgMC42MTI2MzYyLDAuNTE4NjAzNSAxLjAzNzIwNywwLjUxODYwMzUgMC43MjM1NzkzLDAgMS4zMTAxNTYzLC0wLjU4NjU3NyAxLjMxMDE1NjMsLTEuMzEwMTU2MyAwLC0wLjQyNDU3MDggLTAuMjA1NTE3NiwtMC43OTc3OTY3IC0wLjUxODYwMzUsLTEuMDM3MjA3IHogbSAtMy4wODQzMjYyLDAgYyAwLjE0MTI2NjcsMC4yMDkxMDIzIDAuMjE4MzU5NCwwLjQ2NTYyMDYgMC4yMTgzNTk0LDAuNzM2OTYyOSAwLDAuNzIzNTc5MyAtMC41ODY1NzcsMS4zMTAxNTYyIC0xLjMxMDE1NjMsMS4zMTAxNTYyIC0wLjI3MTM0MjIsMCAtMC41Mjc4NjA1LC0wLjA3NzA5MyAtMC43MzY5NjI5LC0wLjIxODM1OTMgMC4yMzk0MTA0LDAuMzEzMDg1OSAwLjYxMjYzNjMsMC41MTg2MDM1IDEuMDM3MjA3MSwwLjUxODYwMzUgMC43MjM1NzkzLDAgMS4zMTAxNTYyLC0wLjU4NjU3NyAxLjMxMDE1NjIsLTEuMzEwMTU2MyAwLC0wLjQyNDU3MDggLTAuMjA1NTE3NSwtMC43OTc3OTY3IC0wLjUxODYwMzUsLTEuMDM3MjA3IHogTSAyLjEwMTcwOSw2LjM4NzAxMTcgYyAwLjE0MTI2NjcsMC4yMDkxMDI0IDAuMjE4MzU5NCwwLjQ2NTYyMDYgMC4yMTgzNTk0LDAuNzM2OTYyOSAwLDAuNzIzNTc5MyAtMC41ODY1NzcsMS4zMTAxNTYzIC0xLjMxMDE1NjMsMS4zMTAxNTYzIC0wLjI3MTM0MjIzLDAgLTAuNTI3ODYwNTksLTAuMDc3MDkzIC0wLjczNjk2Mjg4LC0wLjIxODM1OTQgMC4yMzk0MTAzOSwwLjMxMzA4NTkgMC42MTI2MzYyMiwwLjUxODYwMzUgMS4wMzcyMDY5OCwwLjUxODYwMzUgMC43MjM1NzkzLDAgMS4zMTAxNTYzLC0wLjU4NjU3NyAxLjMxMDE1NjMsLTEuMzEwMTU2MyAwLC0wLjQyNDU3MDggLTAuMjA1NTE3NiwtMC43OTc3OTY2IC0wLjUxODYwMzUsLTEuMDM3MjA3IHogbSAzLjAyOTczNjMsMCBjIDAuMTQxMjY2NywwLjIwOTEwMjQgMC4yMTgzNTk0LDAuNDY1NjIwNiAwLjIxODM1OTQsMC43MzY5NjI5IDAsMC43MjM1NzkzIC0wLjU4NjU3NywxLjMxMDE1NjMgLTEuMzEwMTU2MywxLjMxMDE1NjMgLTAuMjcxMzQyMiwwIC0wLjUyNzg2MDUsLTAuMDc3MDkzIC0wLjczNjk2MjksLTAuMjE4MzU5NCAwLjIzOTQxMDQsMC4zMTMwODU5IDAuNjEyNjM2MywwLjUxODYwMzUgMS4wMzcyMDcxLDAuNTE4NjAzNSAwLjcyMzU3OTMsMCAxLjMxMDE1NjIsLTAuNTg2NTc3IDEuMzEwMTU2MiwtMS4zMTAxNTYzIDAsLTAuNDI0NTcwOCAtMC4yMDU1MTc1LC0wLjc5Nzc5NjYgLTAuNTE4NjAzNSwtMS4wMzcyMDcgeiBtIDMuMDg0MzI2MiwwIGMgMC4xNDEyNjY3LDAuMjA5MTAyNCAwLjIxODM1OTQsMC40NjU2MjA2IDAuMjE4MzU5NCwwLjczNjk2MjkgMCwwLjcyMzU3OTMgLTAuNTg2NTc3LDEuMzEwMTU2MyAtMS4zMTAxNTYzLDEuMzEwMTU2MyAtMC4yNzEzNDIzLDAgLTAuNTI3ODYwNSwtMC4wNzcwOTMgLTAuNzM2OTYyOSwtMC4yMTgzNTk0IDAuMjM5NDEwNCwwLjMxMzA4NTkgMC42MTI2MzYyLDAuNTE4NjAzNSAxLjAzNzIwNywwLjUxODYwMzUgMC43MjM1NzkzLDAgMS4zMTAxNTYzLC0wLjU4NjU3NyAxLjMxMDE1NjMsLTEuMzEwMTU2MyAwLC0wLjQyNDU3MDggLTAuMjA1NTE3NiwtMC43OTc3OTY2IC0wLjUxODYwMzUsLTEuMDM3MjA3IHoiIC8+ICA8L2c+PC9zdmc+); - background-repeat: no-repeat; - background-position: 100% 100%; - pointer-events: auto !important; -} -.os-host-rtl > .os-scrollbar-corner.os-scrollbar-corner-resize { - -webkit-transform: scale(-1, 1); - transform: scale(-1, 1); -} -.os-host-overflow { - overflow: hidden !important; -} -.os-host-overflow-x { -} -.os-host-overflow-y { -} -@-webkit-keyframes hs-resize-observer-dummy-animation { - from { - z-index: 0; - } - to { - z-index: -1; - } -} -@keyframes hs-resize-observer-dummy-animation { - from { - z-index: 0; - } - to { - z-index: -1; - } +.x-toggle-component { + justify-content: left; } -/* -THEMES: -*/ - -/* NONE THEME: */ -.os-theme-none > .os-scrollbar-horizontal, -.os-theme-none > .os-scrollbar-vertical, -.os-theme-none > .os-scrollbar-corner { - display: none !important; -} -.os-theme-none > .os-scrollbar-corner-resize { - display: block !important; - min-width: 10px; - min-height: 10px; -} -/* DARK & LIGHT THEME: */ -.os-theme-dark > .os-scrollbar-horizontal, -.os-theme-light > .os-scrollbar-horizontal { - right: 10px; - height: 10px; -} -.os-theme-dark > .os-scrollbar-vertical, -.os-theme-light > .os-scrollbar-vertical { - bottom: 10px; - width: 10px; -} -.os-theme-dark.os-host-rtl > .os-scrollbar-horizontal, -.os-theme-light.os-host-rtl > .os-scrollbar-horizontal { - left: 10px; - right: 0; -} -.os-theme-dark > .os-scrollbar-corner, -.os-theme-light > .os-scrollbar-corner { - height: 10px; - width: 10px; -} -.os-theme-dark > .os-scrollbar-corner, -.os-theme-light > .os-scrollbar-corner { - background-color: transparent; -} -.os-theme-dark > .os-scrollbar, -.os-theme-light > .os-scrollbar { - padding: 2px; - box-sizing: border-box; - background: transparent; -} -.os-theme-dark > .os-scrollbar.os-scrollbar-unusable, -.os-theme-light > .os-scrollbar.os-scrollbar-unusable { - background: transparent; -} -.os-theme-dark > .os-scrollbar > .os-scrollbar-track, -.os-theme-light > .os-scrollbar > .os-scrollbar-track { - background: transparent; -} -.os-theme-dark > .os-scrollbar-horizontal > .os-scrollbar-track > .os-scrollbar-handle, -.os-theme-light > .os-scrollbar-horizontal > .os-scrollbar-track > .os-scrollbar-handle { - min-width: 30px; -} -.os-theme-dark > .os-scrollbar-vertical > .os-scrollbar-track > .os-scrollbar-handle, -.os-theme-light > .os-scrollbar-vertical > .os-scrollbar-track > .os-scrollbar-handle { - min-height: 30px; -} -.os-theme-dark > .os-scrollbar > .os-scrollbar-track > .os-scrollbar-handle, -.os-theme-light > .os-scrollbar > .os-scrollbar-track > .os-scrollbar-handle { - border-radius: 11px; - opacity: .4; -} -.os-theme-dark > .os-scrollbar > .os-scrollbar-track > .os-scrollbar-handle { - background: $color-gray; -} -.os-theme-light > .os-scrollbar > .os-scrollbar-track > .os-scrollbar-handle { - background: $color-off-white; -} -.os-theme-dark.os-host-transition > .os-scrollbar > .os-scrollbar-track > .os-scrollbar-handle, -.os-theme-light.os-host-transition > .os-scrollbar > .os-scrollbar-track > .os-scrollbar-handle { - -webkit-transition: opacity 0.3s; - transition: opacity 0.3s; -} -.os-theme-dark > .os-scrollbar:hover > .os-scrollbar-track > .os-scrollbar-handle, -.os-theme-light > .os-scrollbar:hover > .os-scrollbar-track > .os-scrollbar-handle { - opacity: .55; -} -.os-theme-dark > .os-scrollbar > .os-scrollbar-track > .os-scrollbar-handle.active, -.os-theme-light > .os-scrollbar > .os-scrollbar-track > .os-scrollbar-handle.active { - opacity: .7; -} -.os-theme-dark > .os-scrollbar-horizontal .os-scrollbar-handle:before, -.os-theme-dark > .os-scrollbar-vertical .os-scrollbar-handle:before, -.os-theme-light > .os-scrollbar-horizontal .os-scrollbar-handle:before, -.os-theme-light > .os-scrollbar-vertical .os-scrollbar-handle:before { - content: ''; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - display: block; -} -.os-theme-dark.os-host-scrollbar-horizontal-hidden > .os-scrollbar-horizontal .os-scrollbar-handle:before, -.os-theme-dark.os-host-scrollbar-vertical-hidden > .os-scrollbar-vertical .os-scrollbar-handle:before, -.os-theme-light.os-host-scrollbar-horizontal-hidden > .os-scrollbar-horizontal .os-scrollbar-handle:before, -.os-theme-light.os-host-scrollbar-vertical-hidden > .os-scrollbar-vertical .os-scrollbar-handle:before { - display: none; -} -.os-theme-dark > .os-scrollbar-horizontal .os-scrollbar-handle:before, -.os-theme-light > .os-scrollbar-horizontal .os-scrollbar-handle:before { - top: -6px; - bottom: -2px; -} -.os-theme-dark > .os-scrollbar-vertical .os-scrollbar-handle:before, -.os-theme-light > .os-scrollbar-vertical .os-scrollbar-handle:before { - left: -6px; - right: -2px; -} -.os-host-rtl.os-theme-dark > .os-scrollbar-vertical .os-scrollbar-handle:before, -.os-host-rtl.os-theme-light > .os-scrollbar-vertical .os-scrollbar-handle:before { - right: -6px; - left: -2px; -} - -/* FINISH Overlay Scrollbars */ +/* FINISH x-toggle */ diff --git a/gui/app/styles/widget/widget-list-picker.scss b/gui/app/styles/widget/widget-list-picker.scss index a3ac4db9..ba945570 100644 --- a/gui/app/styles/widget/widget-list-picker.scss +++ b/gui/app/styles/widget/widget-list-picker.scss @@ -7,37 +7,79 @@ padding: 0; > .option { - @include ease-in(); - margin: 0 0 5px 0; + @include border-radius(3px); + margin: 0 0 15px 0; padding: 10px 15px; color: $color-gray; background-color: $color-off-white; + border: 1px solid $color-gray; cursor: pointer; position: relative; list-style-type: none; line-height: 26px; &:hover { - color: $color-black; - // background-color: $color-primary-light; + > .text-header, > .text { + color: $color-off-black; + } + } + + > .text-header { + @include ease-in(); + color: $color-gray; + font-size: 1.3rem; + font-weight: 600; + margin-bottom: 5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 80%; } > .text { + @include ease-in(); + color: $color-gray; + font-size: 1rem; + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; width: 80%; - font-weight: bold; } > .material-icons { position: absolute; top: 10px; right: 10px; - color: $color-white; + color: $color-green; + font-weight: 700; + font-size: 1.3rem; } } > .selected { - color: $color-white !important; - background-color: $color-link !important; + > .text-header, > .text { + color: $color-off-black; + } + + background-color: $color-yellow !important; + border: 1px solid $color-goldy !important; + } + } +} + +.widget-list-picker-horiz { + > .options { + > .option { + display: inline-block; + margin: 15px 15px 0 0; + padding: 10px 15px; + width: 30%; + + @media only screen and (max-width: 1200px) { + display: block; + width: 100%; + } } } } diff --git a/gui/app/templates/components/customize/auth-settings.hbs b/gui/app/templates/components/customize/auth-settings.hbs index 3f1dc4f7..2a4e39b4 100644 --- a/gui/app/templates/components/customize/auth-settings.hbs +++ b/gui/app/templates/components/customize/auth-settings.hbs @@ -9,91 +9,213 @@
-
- -
- {{#ui/ui-radio selected=isDocumizeProvider onClick=(action 'onDocumize')}} Documize — email/password{{/ui/ui-radio}} - {{#ui/ui-radio selected=isKeycloakProvider onClick=(action 'onKeycloak')}} Keycloak — bring your own authentication server{{/ui/ui-radio}} - - External authentication servers, services must be accessible from the server running this Documize instance - -
+
+
    +
  • +
    Documize
    +
    Built-in email/password
    + {{#if isDocumizeProvider}} + check + {{/if}} +
  • +
  • +
    Keycloak
    +
    Via authentication server
    + {{#if isKeycloakProvider}} + check + {{/if}} +
  • +
  • +
    LDAP
    +
    Connect to LDAP/ Active Directory
    + {{#if isLDAPProvider}} + check + {{/if}} +
  • +
{{#if isKeycloakProvider}}
- -
+ +
{{focus-input id="keycloak-url" type="text" value=keycloakConfig.url class=(if KeycloakUrlError 'form-control is-invalid' 'form-control')}} e.g. http://localhost:8888/auth
- -
+ +
{{input id="keycloak-realm" type="text" value=keycloakConfig.realm class=(if KeycloakRealmError 'form-control is-invalid' 'form-control')}} e.g. main
- -
+ +
{{textarea id="keycloak-publicKey" type="text" value=keycloakConfig.publicKey rows=7 class=(if KeycloakPublicKeyError 'form-control is-invalid' 'form-control')}} Copy the RSA Public Key from Realm Settings → Keys
- -
+ +
{{input id="keycloak-clientId" type="text" value=keycloakConfig.clientId class=(if KeycloakClientIdError 'form-control is-invalid' 'form-control')}} e.g. account
- -
+ +
{{input id="keycloak-group" type="text" value=keycloakConfig.group class="form-control"}} If you want to sync users in a particular Group (e.g. 'Documize Users'), provide the Group ID (e.g. 511d8b61-1ec8-45f6-bc8d-5de64d54c9d2)
- -
+ +
{{input id="keycloak-admin-user" type="text" value=keycloakConfig.adminUser class=(if KeycloakAdminUserError 'form-control is-invalid' 'form-control')}} Used to connect with Keycloak and sync users with Documize (create user under Master Realm and assign 'view-users' role against Realm specified above)
- -
+ +
{{input id="keycloak-admin-password" type="password" value=keycloakConfig.adminPassword class=(if KeycloakAdminPasswordError 'form-control is-invalid' 'form-control')}} Used to connect with Keycloak and sync users with Documize
- -
-
- {{input type="checkbox" class="form-check-input" id="keycloak-logout" checked=keycloakConfig.disableLogout}} - -
+ +
+ {{x-toggle value=keycloakConfig.disableLogout size="medium" theme="light" onToggle=(action (mut keycloakConfig.disableLogout))}}
- -
-
- {{input type="checkbox" class="form-check-input" id="keycloak-perm" checked=keycloakConfig.defaultPermissionAddSpace}} - -
+ +
+ {{x-toggle value=keycloakConfig.defaultPermissionAddSpace size="medium" theme="light" onToggle=(action (mut keycloakConfig.defaultPermissionAddSpace))}}
{{/if}} + + {{#if isLDAPProvider}} +
+ +
+ {{focus-input id="ldap-host" type="text" value=ldapConfig.serverHost class=(if ldapErrorServerHost 'form-control is-invalid' 'form-control')}} + IP or host address, e.g. ldap.example.org, 127.0.0.1 +
+
+
+ +
+ {{input id="ldap-port" type="number" value=ldapConfig.serverPort class=(if ldapErrorServerPort 'form-control is-invalid' 'form-control')}} + Port number, e.g. 389 +
+
+
+ +
+ +
+
+
+ +
+ {{input id="ldap-baseDN" type="number" value=ldapConfig.baseDN class='form-control'}} + Starting point for search filters, e.g. ou=users,dc=example,dc=com +
+
+
+ +
+ {{input id="ldap-bindDN" type="text" value=ldapConfig.bindDN class=(if ldapErrorBindDN 'form-control is-invalid' 'form-control')}} + login credentials for LDAP server +
+
+
+ +
+ {{input id="ldap-bindPassword" type="password" value=ldapConfig.bindPassword class=(if ldapErrorBindPassword 'form-control is-invalid' 'form-control')}} + login credentials for LDAP server +
+
+
+ +
+ {{input id="ldap-userFilter" type="text" value=ldapConfig.userFilter class=(if ldapErrorNoFilter 'form-control is-invalid' 'form-control')}} + Search filter for finding users, e.g. (|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson)) + Specify User Filter and/or Group Filter +
+
+
+ +
+ {{input id="ldap-groupFilter" type="text" value=ldapConfig.groupFilter class=(if ldapErrorNoFilter 'form-control is-invalid' 'form-control')}} + Search filter for finding users via groups, e.g. (&(objectClass=group)(|(cn=ship_crew)(cn=admin_staff)) +
+
+ +
+ +
+ {{input id="ldap-attributeUserRDN" type="text" value=ldapConfig.attributeUserRDN class=(if ldapErrorAttributeUserRDN 'form-control is-invalid' 'form-control')}} + Username/login attribute, e.g. uid in LDAP, sAMAccountName in Active Directory + User Attributes used to retreive data when using User Filter +
+
+
+ +
+ {{input id="ldap-attributeUserFirstname" type="text" value=ldapConfig.attributeUserFirstname class=(if ldapErrorAttributeUserFirstname 'form-control is-invalid' 'form-control')}} + Firstname attribute, e.g. givenName +
+
+
+ +
+ {{input id="ldap-attributeUserLastname" type="text" value=ldapConfig.attributeUserLastname class=(if ldapErrorAttributeUserLastname 'form-control is-invalid' 'form-control')}} + Lastname attribute, e.g. sn +
+
+
+ +
+ {{input id="ldap-attributeUserEmail" type="text" value=ldapConfig.attributeUserEmail class=(if ldapErrorAttributeUserEmail 'form-control is-invalid' 'form-control')}} + Email attribute, e.g. mail +
+
+
+ +
+ {{input id="ldap-attributeGroupMember" type="text" value=ldapConfig.attributeGroupMember class=(if ldapErrorAttributeGroupMember 'form-control is-invalid' 'form-control')}} + Attribute that identifies individual group member, e.g. member or uniqueMember + Group Attributes used to retreive data when using Group Filter +
+
+
+ +
+ {{x-toggle value=ldapConfig.disableLogout size="medium" theme="light" onToggle=(action (mut ldapConfig.disableLogout))}} +
+
+
+ +
+ {{x-toggle value=ldapConfig.defaultPermissionAddSpace size="medium" theme="light" onToggle=(action (mut ldapConfig.defaultPermissionAddSpace))}} +
+
+ {{/if}} +
Save
diff --git a/server/routing/routes.go b/server/routing/routes.go index e78aff79..5b33ce08 100644 --- a/server/routing/routes.go +++ b/server/routing/routes.go @@ -214,7 +214,7 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) { AddPrivate(rt, "global/search/status", []string{"GET", "OPTIONS"}, nil, meta.SearchStatus) AddPrivate(rt, "global/search/reindex", []string{"POST", "OPTIONS"}, nil, meta.Reindex) AddPrivate(rt, "global/sync/keycloak", []string{"GET", "OPTIONS"}, nil, keycloak.Sync) - AddPrivate(rt, "global/sync/ldap", []string{"GET", "OPTIONS"}, nil, ldap.Sync) + AddPrivate(rt, "global/sync/ldap/preview", []string{"POST", "OPTIONS"}, nil, ldap.Preview) Add(rt, RoutePrefixRoot, "robots.txt", []string{"GET", "OPTIONS"}, nil, meta.RobotsTxt) Add(rt, RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, meta.Sitemap)