diff --git a/domain/auth/add.go b/domain/auth/add.go new file mode 100644 index 00000000..e337a746 --- /dev/null +++ b/domain/auth/add.go @@ -0,0 +1,90 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +package auth + +import ( + "database/sql" + + "github.com/documize/community/core/env" + "github.com/documize/community/core/uniqueid" + "github.com/documize/community/domain" + usr "github.com/documize/community/domain/user" + "github.com/documize/community/model/account" + "github.com/documize/community/model/user" +) + +// AddExternalUser method to setup user account in Documize using Keycloak/LDAP provided user data. +func AddExternalUser(ctx domain.RequestContext, rt *env.Runtime, store *domain.Store, u user.User, addSpace bool) (nu user.User, err error) { + // only create account if not dupe + addUser := true + addAccount := true + var userID string + + userDupe, err := store.User.GetByEmail(ctx, u.Email) + if err != nil && err != sql.ErrNoRows { + return + } + + if u.Email == userDupe.Email { + addUser = false + userID = userDupe.RefID + } + + ctx.Transaction, err = rt.Db.Beginx() + if err != nil { + return + } + + if addUser { + userID = uniqueid.Generate() + u.RefID = userID + + err = store.User.Add(ctx, u) + if err != nil { + ctx.Transaction.Rollback() + return + } + } else { + usr.AttachUserAccounts(ctx, *store, ctx.OrgID, &userDupe) + + for _, a := range userDupe.Accounts { + if a.OrgID == ctx.OrgID { + addAccount = false + break + } + } + } + + // set up user account for the org + if addAccount { + var a account.Account + a.UserID = userID + a.OrgID = ctx.OrgID + a.Editor = addSpace + a.Admin = false + accountID := uniqueid.Generate() + a.RefID = accountID + a.Active = true + + err = store.Account.Add(ctx, a) + if err != nil { + ctx.Transaction.Rollback() + return + } + } + + ctx.Transaction.Commit() + + nu, err = store.User.Get(ctx, userID) + + return +} diff --git a/domain/auth/keycloak/endpoint.go b/domain/auth/keycloak/endpoint.go index cb90e10c..0068475b 100644 --- a/domain/auth/keycloak/endpoint.go +++ b/domain/auth/keycloak/endpoint.go @@ -130,7 +130,7 @@ func (h *Handler) Sync(w http.ResponseWriter, r *http.Request) { if len(u.Email) == 0 { missing++ } else { - err = addUser(ctx, h.Runtime, h.Store, u, c.DefaultPermissionAddSpace) + _, err = auth.AddExternalUser(ctx, h.Runtime, h.Store, u, c.DefaultPermissionAddSpace) } } @@ -240,7 +240,7 @@ func (h *Handler) Authenticate(w http.ResponseWriter, r *http.Request) { u.Salt = secrets.GenerateSalt() u.Password = secrets.GeneratePassword(secrets.GenerateRandomPassword(), u.Salt) - err = addUser(ctx, h.Runtime, h.Store, u, ac.DefaultPermissionAddSpace) + u, err = auth.AddExternalUser(ctx, h.Runtime, h.Store, u, ac.DefaultPermissionAddSpace) if err != nil { response.WriteServerError(w, method, err) h.Runtime.Log.Error(method, err) diff --git a/domain/auth/keycloak/keycloak.go b/domain/auth/keycloak/keycloak.go index cb8389ee..1d538825 100644 --- a/domain/auth/keycloak/keycloak.go +++ b/domain/auth/keycloak/keycloak.go @@ -13,7 +13,6 @@ package keycloak import ( "bytes" - "database/sql" "encoding/json" "fmt" "io/ioutil" @@ -22,12 +21,7 @@ import ( "strconv" "strings" - "github.com/documize/community/core/env" "github.com/documize/community/core/stringutil" - "github.com/documize/community/core/uniqueid" - "github.com/documize/community/domain" - usr "github.com/documize/community/domain/user" - "github.com/documize/community/model/account" "github.com/documize/community/model/auth" "github.com/documize/community/model/user" "github.com/pkg/errors" @@ -136,70 +130,3 @@ func Fetch(c auth.KeycloakConfig) (users []user.User, err error) { return users, nil } - -// Helper method to setup user account in Documize using Keycloak provided user data. -func addUser(ctx domain.RequestContext, rt *env.Runtime, store *domain.Store, u user.User, addSpace bool) (err error) { - // only create account if not dupe - addUser := true - addAccount := true - var userID string - - userDupe, err := store.User.GetByEmail(ctx, u.Email) - if err != nil && err != sql.ErrNoRows { - return err - } - - if u.Email == userDupe.Email { - addUser = false - userID = userDupe.RefID - } - - ctx.Transaction, err = rt.Db.Beginx() - if err != nil { - return err - } - - if addUser { - userID = uniqueid.Generate() - u.RefID = userID - - err = store.User.Add(ctx, u) - if err != nil { - ctx.Transaction.Rollback() - return err - } - } else { - usr.AttachUserAccounts(ctx, *store, ctx.OrgID, &userDupe) - - for _, a := range userDupe.Accounts { - if a.OrgID == ctx.OrgID { - addAccount = false - break - } - } - } - - // set up user account for the org - if addAccount { - var a account.Account - a.UserID = userID - a.OrgID = ctx.OrgID - a.Editor = addSpace - a.Admin = false - accountID := uniqueid.Generate() - a.RefID = accountID - a.Active = true - - err = store.Account.Add(ctx, a) - if err != nil { - ctx.Transaction.Rollback() - return err - } - } - - ctx.Transaction.Commit() - - u, err = store.User.Get(ctx, userID) - - return err -} diff --git a/domain/auth/ldap/ad_test.go b/domain/auth/ldap/ad_test.go index 9f4aaf94..ae2aa9a0 100644 --- a/domain/auth/ldap/ad_test.go +++ b/domain/auth/ldap/ad_test.go @@ -88,16 +88,17 @@ func TestAuthenticate_PublicAD(t *testing.T) { } defer l.Close() - ok, err := authenticate(l, testConfigPublicAD, "bob.johnson", "Pass@word1!") + user, ok, err := authenticate(l, testConfigPublicAD, "bob.johnson", "Pass@word1!") if err != nil { t.Error("error during LDAP authentication: ", err.Error()) return } if !ok { t.Error("failed LDAP authentication") + return } - t.Log("Authenticated") + t.Log("Authenticated", user.Email) } func TestNotAuthenticate_PublicAD(t *testing.T) { @@ -108,13 +109,14 @@ func TestNotAuthenticate_PublicAD(t *testing.T) { } defer l.Close() - ok, err := authenticate(l, testConfigPublicAD, "junk", "junk") + _, ok, err := authenticate(l, testConfigPublicAD, "junk", "junk") if err != nil { t.Error("error during LDAP authentication: ", err.Error()) return } if ok { t.Error("incorrect LDAP authentication") + return } t.Log("Not authenticated") diff --git a/domain/auth/ldap/endpoint.go b/domain/auth/ldap/endpoint.go index e3dfaacb..b1e68d8a 100644 --- a/domain/auth/ldap/endpoint.go +++ b/domain/auth/ldap/endpoint.go @@ -12,27 +12,24 @@ package ldap import ( - "crypto/tls" - // "database/sql" + "database/sql" "encoding/json" "fmt" "io/ioutil" "net/http" - // "sort" - // "strings" + "sort" + "strings" "github.com/documize/community/core/env" "github.com/documize/community/core/response" - // "github.com/documize/community/core/secrets" + "github.com/documize/community/core/secrets" "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" + "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" ) // Handler contains the runtime information such as logging and database. @@ -41,9 +38,7 @@ 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. +// Preview connects to LDAP using paylaod and returns first 50 users. func (h *Handler) Preview(w http.ResponseWriter, r *http.Request) { h.Runtime.Log.Info("Sync'ing with LDAP") @@ -57,6 +52,7 @@ func (h *Handler) Preview(w http.ResponseWriter, r *http.Request) { Message string `json:"message"` IsError bool `json:"isError"` Users []user.User `json:"users"` + Count int `json:"count"` } result.Users = []user.User{} @@ -95,10 +91,12 @@ func (h *Handler) Preview(w http.ResponseWriter, r *http.Request) { 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 + result.Count = len(users) + result.Users = users + + // Preview does not require more than 50 users. + if len(users) > 50 { + result.Users = users[:50] } h.Runtime.Log.Info(result.Message) @@ -133,310 +131,219 @@ func (h *Handler) Sync(w http.ResponseWriter, r *http.Request) { // Exit if not using LDAP if org.AuthProvider != ath.AuthProviderLDAP { - // result.Message = "Error: skipping user sync with LDAP as it is not the configured option" - // result.IsError = true - // response.WriteJSON(w, result) - // h.Runtime.Log.Info(result.Message) - // return + result.Message = "Error: skipping user sync with LDAP as it is not the configured option" + result.IsError = true + response.WriteJSON(w, result) + h.Runtime.Log.Info(result.Message) + return } - // Make Keycloak auth provider config + // Get auth provider config c := lm.LDAPConfig{} - // err = json.Unmarshal([]byte(org.AuthConfig), &c) - // if err != nil { - // result.Message = "Error: unable read LDAP configuration data" - // result.IsError = true - // response.WriteJSON(w, result) - // h.Runtime.Log.Error(result.Message, err) - // return - // } - - c.ServerHost = "ldap.forumsys.com" - c.ServerPort = 389 - c.EncryptionType = "none" - c.BaseDN = "dc=example,dc=com" - c.BindDN = "cn=read-only-admin,dc=example,dc=com" - c.BindPassword = "password" - c.UserFilter = "" - c.GroupFilter = "" - c.DisableLogout = false - c.DefaultPermissionAddSpace = false - - address := fmt.Sprintf("%s:%d", c.ServerHost, c.ServerPort) - - h.Runtime.Log.Info("Connecting to LDAP server") - - l, err := ld.Dial("tcp", address) + err = json.Unmarshal([]byte(org.AuthConfig), &c) if err != nil { - result.Message = "Error: unable to dial LDAP server: " + err.Error() + result.Message = "Error: unable read LDAP configuration data" result.IsError = true response.WriteJSON(w, result) h.Runtime.Log.Error(result.Message, err) return } + + // Get user list from LDAP. + ldapUsers, err := fetchUsers(c) + if err != nil { + result.Message = "Error: unable to fetch LDAP users: " + err.Error() + result.IsError = true + response.WriteJSON(w, result) + h.Runtime.Log.Error(result.Message, err) + return + } + + // Get user list from Documize + dmzUsers, err := h.Store.User.GetUsersForOrganization(ctx, "", 99999) + if err != nil { + result.Message = "Error: unable to fetch Documize users" + result.IsError = true + response.WriteJSON(w, result) + h.Runtime.Log.Error(result.Message, err) + return + } + + sort.Slice(ldapUsers, func(i, j int) bool { return ldapUsers[i].Email < ldapUsers[j].Email }) + sort.Slice(dmzUsers, func(i, j int) bool { return dmzUsers[i].Email < dmzUsers[j].Email }) + + insert := []user.User{} + + for _, k := range ldapUsers { + exists := false + for _, d := range dmzUsers { + if k.Email == d.Email { + exists = true + } + } + if !exists { + insert = append(insert, k) + } + } + + // Track the number of LDAP users with missing data. + missing := 0 + + // Insert new users into Documize + for _, u := range insert { + if len(u.Email) == 0 { + missing++ + } else { + _, err = auth.AddExternalUser(ctx, h.Runtime, h.Store, u, c.DefaultPermissionAddSpace) + } + } + + result.IsError = false + result.Message = "Sync complete with LDAP server" + result.Message = fmt.Sprintf( + "LDAP sync found %d users, %d new users added, %d users with missing data ignored", + len(ldapUsers), len(insert), missing) + + h.Runtime.Log.Info(result.Message) + + response.WriteJSON(w, result) +} + +// Authenticate checks LDAP authentication credentials. +func (h *Handler) Authenticate(w http.ResponseWriter, r *http.Request) { + method := "ldap.authenticate" + ctx := domain.GetRequestContext(r) + + // check for http header + authHeader := r.Header.Get("Authorization") + if len(authHeader) == 0 { + response.WriteBadRequestError(w, method, "Missing Authorization header") + return + } + + // decode what we received + data := strings.Replace(authHeader, "Basic ", "", 1) + + decodedBytes, err := secrets.DecodeBase64([]byte(data)) + if err != nil { + response.WriteBadRequestError(w, method, "Unable to decode authentication token") + h.Runtime.Log.Error("decode auth header", err) + return + } + + decoded := string(decodedBytes) + + // check that we have domain:username:password (but allow for : in password field!) + credentials := strings.SplitN(decoded, ":", 3) + if len(credentials) != 3 { + response.WriteBadRequestError(w, method, "Bad authentication token, expecting domain:username:password") + h.Runtime.Log.Error("bad auth token", err) + return + } + + dom := strings.TrimSpace(strings.ToLower(credentials[0])) + username := strings.TrimSpace(strings.ToLower(credentials[1])) + password := credentials[2] + + // Check for required fields. + if len(username) == 0 || len(password) == 0 { + response.WriteUnauthorizedError(w) + return + } + + dom = h.Store.Organization.CheckDomain(ctx, dom) // TODO optimize by removing this once js allows empty domains + + h.Runtime.Log.Info("LDAP login request " + username + " @ " + dom) + + // Get the org and it's associated LDAP config. + org, err := h.Store.Organization.GetOrganizationByDomain(dom) + if err != nil { + response.WriteUnauthorizedError(w) + h.Runtime.Log.Error("bad auth organization", err) + return + } + + lc := lm.LDAPConfig{} + err = json.Unmarshal([]byte(org.AuthConfig), &lc) + if err != nil { + response.WriteBadRequestError(w, method, "unable to read LDAP config during authorization") + h.Runtime.Log.Error(method, err) + return + } + + ctx.OrgID = org.RefID + + l, err := connect(lc) + if err != nil { + response.WriteBadRequestError(w, method, "unable to dial LDAP server") + h.Runtime.Log.Error(method, err) + return + } defer l.Close() - if c.EncryptionType == "starttls" { - h.Runtime.Log.Info("Using StartTLS with LDAP server") - err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) + lu, ok, err := authenticate(l, lc, username, password) + if err != nil { + response.WriteBadRequestError(w, method, "error during LDAP authentication") + h.Runtime.Log.Error(method, err) + return + } + if !ok { + response.WriteUnauthorizedError(w) + return + } + + h.Runtime.Log.Info("LDAP logon completed " + lu.Email) + + u, err := h.Store.User.GetByDomain(ctx, dom, lu.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("Adding new LDAP user " + lu.Email + " @ " + dom) + + u = convertUser(lc, lu) + u.Salt = secrets.GenerateSalt() + u.Password = secrets.GeneratePassword(secrets.GenerateRandomPassword(), u.Salt) + + u, err = auth.AddExternalUser(ctx, h.Runtime, h.Store, u, lc.DefaultPermissionAddSpace) if err != nil { - result.Message = "Error: unable to startTLS with LDAP server: " + err.Error() - result.IsError = true - response.WriteJSON(w, result) - h.Runtime.Log.Error(result.Message, err) + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) return } } - // Authenticate with LDAP server using admin credentials. - h.Runtime.Log.Info("Binding LDAP admin user") - err = l.Bind(c.BindDN, c.BindPassword) - if err != nil { - result.Message = "Error: unable to bind specified admin user to LDAP: " + err.Error() - result.IsError = true - response.WriteJSON(w, result) - h.Runtime.Log.Error(result.Message, err) + // 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 } - // Get users from LDAP server by using filter - filter := "" - attrs := []string{} - if len(c.GroupFilter) > 0 { - filter = fmt.Sprintf("(&(objectClass=group)(cn=%s))", c.GroupFilter) - attrs = []string{"cn"} - } else { - filter = "(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))" - attrs = []string{"dn", "cn", "givenName", "sn", "mail", "uid"} + // 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 + } } - searchRequest := ld.NewSearchRequest( - c.BaseDN, - ld.ScopeWholeSubtree, ld.NeverDerefAliases, 0, 0, false, - filter, - attrs, - nil, - ) + // Generate JWT token + authModel := ath.AuthenticationModel{} + authModel.Token = auth.GenerateJWT(h.Runtime, u.RefID, org.RefID, dom) + authModel.User = u - sr, err := l.Search(searchRequest) - if err != nil { - result.Message = "Error: unable to bind specified admin user to LDAP: " + err.Error() - result.IsError = true - response.WriteJSON(w, result) - h.Runtime.Log.Error(result.Message, err) - return - } - fmt.Printf("entries found: %d", len(sr.Entries)) - - for _, entry := range sr.Entries { - fmt.Printf("[%s] %s (%s %s) @ %s\n", - entry.GetAttributeValue("uid"), - entry.GetAttributeValue("cn"), - entry.GetAttributeValue("givenName"), - entry.GetAttributeValue("sn"), - entry.GetAttributeValue("mail")) - } - // // User list from LDAP - // kcUsers, err := Fetch(c) - // if err != nil { - // result.Message = "Error: unable to fetch Keycloak users: " + err.Error() - // result.IsError = true - // response.WriteJSON(w, result) - // h.Runtime.Log.Error(result.Message, err) - // return - // } - - // // User list from Documize - // dmzUsers, err := h.Store.User.GetUsersForOrganization(ctx, "", 99999) - // if err != nil { - // result.Message = "Error: unable to fetch Documize users" - // result.IsError = true - // response.WriteJSON(w, result) - // h.Runtime.Log.Error(result.Message, err) - // return - // } - - // sort.Slice(kcUsers, func(i, j int) bool { return kcUsers[i].Email < kcUsers[j].Email }) - // sort.Slice(dmzUsers, func(i, j int) bool { return dmzUsers[i].Email < dmzUsers[j].Email }) - - // insert := []user.User{} - - // for _, k := range kcUsers { - // exists := false - - // for _, d := range dmzUsers { - // if k.Email == d.Email { - // exists = true - // } - // } - - // if !exists { - // insert = append(insert, k) - // } - // } - - // // Track the number of Keycloak users with missing data. - // missing := 0 - - // // Insert new users into Documize - // for _, u := range insert { - // if len(u.Email) == 0 { - // missing++ - // } else { - // err = addUser(ctx, h.Runtime, h.Store, u, c.DefaultPermissionAddSpace) - // } - // } - - // result.Message = fmt.Sprintf("LDAP sync found %d users, %d new users added, %d users with missing data ignored", - // len(kcUsers), len(insert), missing) - - result.IsError = false - result.Message = "Sync complete with LDAP server" - - response.WriteJSON(w, result) - h.Runtime.Log.Info(result.Message) -} - -// Authenticate checks Keycloak authentication credentials. -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 - // } - - // 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)) - - // // 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 - // } - - // 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 - // } - - // // 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 - // } - - // // 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) - - // 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) - - // 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 - // } - // } - - // // 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) - - // // 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 - // } - // } - - // // 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 f46e85b9..268d6db6 100644 --- a/domain/auth/ldap/ldap.go +++ b/domain/auth/ldap/ldap.go @@ -48,8 +48,10 @@ func connect(c lm.LDAPConfig) (l *ld.Conn, err error) { } // Authenticate user against LDAP provider. -func authenticate(l *ld.Conn, c lm.LDAPConfig, username, pwd string) (success bool, err error) { +func authenticate(l *ld.Conn, c lm.LDAPConfig, username, pwd string) (lu lm.LDAPUser, success bool, err error) { success = false + err = nil + userAttrs := c.GetUserFilterAttributes() filter := fmt.Sprintf("(%s=%s)", c.AttributeUserRDN, username) @@ -87,10 +89,13 @@ func authenticate(l *ld.Conn, c lm.LDAPConfig, username, pwd string) (success bo // Bind as the user to verify their password err = l.Bind(userdn, pwd) if err != nil { - return false, nil + return } - return true, nil + lu = extractUser(c, sr.Entries[0]) + success = true + + return } // ExecuteUserFilter returns all matching LDAP users. @@ -212,10 +217,10 @@ func extractUser(c lm.LDAPConfig, e *ld.Entry) (u lm.LDAPUser) { } if len(u.Firstname) == 0 { - u.Firstname = "Empty" + u.Firstname = "LDAP" } if len(u.Lastname) == 0 { - u.Lastname = "Empty" + u.Lastname = "User" } return @@ -235,25 +240,30 @@ func convertUsers(c lm.LDAPConfig, lu []lm.LDAPUser) (du []user.User) { // 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) + du = append(du, convertUser(c, i)) } } return } +// ConvertUser turns LDAP user into Documize user. +func convertUser(c lm.LDAPConfig, lu lm.LDAPUser) (du user.User) { + du = user.User{} + du.Editor = c.DefaultPermissionAddSpace + du.Active = true + du.Email = lu.Email + du.ViewUsers = false + du.Analytics = false + du.Admin = false + du.Global = false + du.Firstname = lu.Firstname + du.Lastname = lu.Lastname + du.Initials = stringutil.MakeInitials(lu.Firstname, lu.Lastname) + + return +} + // FetchUsers from LDAP server using both User and Group filters. func fetchUsers(c lm.LDAPConfig) (du []user.User, err error) { du = []user.User{} diff --git a/domain/auth/ldap/local_test.go b/domain/auth/ldap/local_test.go index bbc5d1e8..4ed5142b 100644 --- a/domain/auth/ldap/local_test.go +++ b/domain/auth/ldap/local_test.go @@ -104,16 +104,17 @@ func TestAuthenticate_LocalLDAP(t *testing.T) { } defer l.Close() - ok, err := authenticate(l, testConfigLocalLDAP, "professor", "professor") + user, ok, err := authenticate(l, testConfigLocalLDAP, "professor", "professor") if err != nil { t.Error("error during LDAP authentication: ", err.Error()) return } if !ok { t.Error("failed LDAP authentication") + return } - t.Log("Authenticated") + t.Log("Authenticated", user.Email) } func TestNotAuthenticate_LocalLDAP(t *testing.T) { @@ -124,13 +125,14 @@ func TestNotAuthenticate_LocalLDAP(t *testing.T) { } defer l.Close() - ok, err := authenticate(l, testConfigLocalLDAP, "junk", "junk") + _, ok, err := authenticate(l, testConfigLocalLDAP, "junk", "junk") if err != nil { t.Error("error during LDAP authentication: ", err.Error()) return } if ok { t.Error("incorrect LDAP authentication") + return } t.Log("Not authenticated") diff --git a/domain/auth/ldap/public_test.go b/domain/auth/ldap/public_test.go index 2d36c307..5795ecd0 100644 --- a/domain/auth/ldap/public_test.go +++ b/domain/auth/ldap/public_test.go @@ -88,14 +88,17 @@ func TestAuthenticate_PublicLDAP(t *testing.T) { } defer l.Close() - ok, err := authenticate(l, testConfigPublicLDAP, "newton", "password") + user, ok, err := authenticate(l, testConfigPublicLDAP, "newton", "password") if err != nil { t.Error("error during LDAP authentication: ", err.Error()) return } if !ok { t.Error("failed LDAP authentication") + return } + + t.Log("Authenticated", user.Email) } func TestNotAuthenticate_PublicLDAP(t *testing.T) { @@ -106,13 +109,14 @@ func TestNotAuthenticate_PublicLDAP(t *testing.T) { } defer l.Close() - ok, err := authenticate(l, testConfigPublicLDAP, "junk", "junk") + _, ok, err := authenticate(l, testConfigPublicLDAP, "junk", "junk") if err != nil { t.Error("error during LDAP authentication: ", err.Error()) return } if ok { t.Error("incorrect LDAP authentication") + return } t.Log("Not authenticated") diff --git a/gui/app/authenticators/anonymous.js b/gui/app/authenticators/anonymous.js index 1243358e..c8cd6ec9 100644 --- a/gui/app/authenticators/anonymous.js +++ b/gui/app/authenticators/anonymous.js @@ -1,16 +1,15 @@ // Copyright 2016 Documize Inc. . All rights reserved. // -// This software (Documize Community Edition) is licensed under +// This software (Documize Community Edition) is licensed under // GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html // // You can operate outside the AGPL restrictions by purchasing // Documize Enterprise Edition and obtaining a commercial license -// by contacting . +// by contacting . // // https://documize.com import { resolve } from 'rsvp'; - import Base from 'ember-simple-auth/authenticators/base'; export default Base.extend({ @@ -20,4 +19,4 @@ export default Base.extend({ authenticate(data) { return resolve(data); } -}); \ No newline at end of file +}); diff --git a/gui/app/authenticators/documize.js b/gui/app/authenticators/documize.js index df7fa74d..3d6c67de 100644 --- a/gui/app/authenticators/documize.js +++ b/gui/app/authenticators/documize.js @@ -1,21 +1,20 @@ // Copyright 2016 Documize Inc. . All rights reserved. // -// This software (Documize Community Edition) is licensed under +// This software (Documize Community Edition) is licensed under // GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html // // You can operate outside the AGPL restrictions by purchasing // Documize Enterprise Edition and obtaining a commercial license -// by contacting . +// by contacting . // // https://documize.com import { isPresent } from '@ember/utils'; - import { reject, resolve } from 'rsvp'; import { inject as service } from '@ember/service'; -import Base from 'ember-simple-auth/authenticators/base'; import encodingUtil from '../utils/encoding'; import netUtil from '../utils/net'; +import Base from 'ember-simple-auth/authenticators/base'; export default Base.extend({ ajax: service(), @@ -27,7 +26,7 @@ export default Base.extend({ if (data) { return resolve(data); } - + return reject(); }, @@ -55,7 +54,7 @@ export default Base.extend({ }, invalidate() { - this.get('localStorage').clearAll(); + this.get('localStorage').clearAll(); return resolve(); } -}); \ No newline at end of file +}); diff --git a/gui/app/authenticators/keycloak.js b/gui/app/authenticators/keycloak.js index 67121e4e..335b8664 100644 --- a/gui/app/authenticators/keycloak.js +++ b/gui/app/authenticators/keycloak.js @@ -10,11 +10,10 @@ // https://documize.com import { isPresent } from '@ember/utils'; - import { reject, resolve } from 'rsvp'; import { inject as service } from '@ember/service'; -import Base from 'ember-simple-auth/authenticators/base'; import netUtil from '../utils/net'; +import Base from 'ember-simple-auth/authenticators/base'; export default Base.extend({ ajax: service(), diff --git a/gui/app/authenticators/ldap.js b/gui/app/authenticators/ldap.js new file mode 100644 index 00000000..e9d49473 --- /dev/null +++ b/gui/app/authenticators/ldap.js @@ -0,0 +1,60 @@ +// Copyright 2016 Documize Inc. . All rights reserved. +// +// This software (Documize Community Edition) is licensed under +// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html +// +// You can operate outside the AGPL restrictions by purchasing +// Documize Enterprise Edition and obtaining a commercial license +// by contacting . +// +// https://documize.com + +import { isPresent } from '@ember/utils'; +import { reject, resolve } from 'rsvp'; +import { inject as service } from '@ember/service'; +import encodingUtil from '../utils/encoding'; +import netUtil from '../utils/net'; +import Base from 'ember-simple-auth/authenticators/base'; + +export default Base.extend({ + ajax: service(), + appMeta: service(), + localStorage: service(), + + restore(data) { + // TODO: verify authentication data + if (data) { + return resolve(data); + } + + return reject(); + }, + + authenticate(credentials) { + let domain = netUtil.getSubdomain(); + let encoded; + + if (typeof credentials === 'object') { + let { password, username } = credentials; + + if (!isPresent(password) || !isPresent(username)) { + return reject("invalid"); + } + + encoded = encodingUtil.Base64.encode(`${domain}:${username}:${password}`); + } else if (typeof credentials === 'string') { + encoded = credentials; + } else { + return reject("invalid"); + } + + let headers = { 'Authorization': 'Basic ' + encoded }; + + return this.get('ajax').post('public/authenticate/ldap', { headers }); + }, + + invalidate() { + this.get('localStorage').clearAll(); + return resolve(); + } +}); diff --git a/gui/app/components/customize/auth-settings.js b/gui/app/components/customize/auth-settings.js index 022a531f..f655db47 100644 --- a/gui/app/components/customize/auth-settings.js +++ b/gui/app/components/customize/auth-settings.js @@ -129,6 +129,7 @@ export default Component.extend(ModalMixin, Notifier, { if (is.undefined(ldapConfig) || is.null(ldapConfig) || is.empty(ldapConfig) ) { ldapConfig = {}; } else { + ldapConfig = JSON.parse(ldapConfig); ldapConfig.defaultPermissionAddSpace = ldapConfig.hasOwnProperty('defaultPermissionAddSpace') ? ldapConfig.defaultPermissionAddSpace : false; ldapConfig.disableLogout = ldapConfig.hasOwnProperty('disableLogout') ? ldapConfig.disableLogout : true; } @@ -240,19 +241,43 @@ export default Component.extend(ModalMixin, Notifier, { config = copy(this.get('ldapConfig')); config.serverHost = config.serverHost.trim(); config.serverPort = parseInt(this.get('ldapConfig.serverPort')); + + if (is.not.empty(config.groupFilter) && is.empty(config.attributeGroupMember)) { + this.$('#ldap-attributeGroupMember').focus(); + return; + } + break; } - debugger; - this.showWait(); let data = { authProvider: provider, authConfig: JSON.stringify(config) }; this.get('onSave')(data).then(() => { // Without sync we cannot log in + + // Keycloak sync process if (data.authProvider === constants.AuthProvider.Keycloak) { - this.get('onSync')().then((response) => { + this.get('onSyncKeycloak')().then((response) => { + if (response.isError) { + this.set('keycloakFailure', response.message); + console.log(response.message); // eslint-disable-line no-console + data.authProvider = constants.AuthProvider.Documize; + this.get('onSave')(data).then(() => {}); + } else { + if (data.authProvider === this.get('appMeta.authProvider')) { + console.log(response.message); // eslint-disable-line no-console + } else { + this.get('onChange')(data); + } + } + }); + } + + // LDAP sync process + if (data.authProvider === constants.AuthProvider.LDAP) { + this.get('onSyncLDAP')().then((response) => { if (response.isError) { this.set('keycloakFailure', response.message); console.log(response.message); // eslint-disable-line no-console diff --git a/gui/app/components/customize/user-list.js b/gui/app/components/customize/user-list.js index e41214cc..7bb89f5d 100644 --- a/gui/app/components/customize/user-list.js +++ b/gui/app/components/customize/user-list.js @@ -240,8 +240,12 @@ export default Component.extend(AuthProvider, ModalMixin, TooltipMixin, { }); }, - onSync() { - this.get('onSync')(); + onSyncKeycloak() { + this.get('onSyncKeycloak')(); + }, + + onSyncLDAP() { + this.get('onSyncLDAP')(); }, onLimit(limit) { diff --git a/gui/app/components/forgot-password.js b/gui/app/components/forgot-password.js index 2657cf4e..74af0db5 100644 --- a/gui/app/components/forgot-password.js +++ b/gui/app/components/forgot-password.js @@ -12,8 +12,8 @@ import $ from 'jquery'; import { empty, and } from '@ember/object/computed'; import { set } from '@ember/object'; -import Component from '@ember/component'; import { isEmpty } from '@ember/utils'; +import Component from '@ember/component'; export default Component.extend({ email: "", diff --git a/gui/app/components/layout/top-bar.js b/gui/app/components/layout/top-bar.js index 69a932fa..ce30b063 100644 --- a/gui/app/components/layout/top-bar.js +++ b/gui/app/components/layout/top-bar.js @@ -36,7 +36,7 @@ export default Component.extend(ModalMixin, { this.pins = []; - if (this.get('appMeta.authProvider') === constants.AuthProvider.Keycloak) { + if (this.get('appMeta.authProvider') !== constants.AuthProvider.Documize) { let config = this.get('appMeta.authConfig'); config = JSON.parse(config); this.set('enableLogout', !config.disableLogout); diff --git a/gui/app/pods/auth/forgot/controller.js b/gui/app/pods/auth/forgot/controller.js index fa86b74e..7ae479fd 100644 --- a/gui/app/pods/auth/forgot/controller.js +++ b/gui/app/pods/auth/forgot/controller.js @@ -10,11 +10,11 @@ // https://documize.com import { inject as service } from '@ember/service'; - import Controller from '@ember/controller'; export default Controller.extend({ userService: service('user'), + appMeta: service('app-meta'), actions: { forgot: function (email) { diff --git a/gui/app/pods/auth/forgot/route.js b/gui/app/pods/auth/forgot/route.js index 0657f548..47806f38 100644 --- a/gui/app/pods/auth/forgot/route.js +++ b/gui/app/pods/auth/forgot/route.js @@ -19,7 +19,7 @@ export default Route.extend({ beforeModel() { let constants = this.get('constants'); - if (this.get('appMeta.authProvider') === constants.AuthProvider.Keycloak) { + if (this.get('appMeta.authProvider') !== constants.AuthProvider.Documize) { this.transitionTo('auth.login'); } }, diff --git a/gui/app/pods/auth/forgot/template.hbs b/gui/app/pods/auth/forgot/template.hbs index 4ede3219..08f0c9fe 100644 --- a/gui/app/pods/auth/forgot/template.hbs +++ b/gui/app/pods/auth/forgot/template.hbs @@ -2,6 +2,7 @@