diff --git a/app/app/components/auth-settings.js b/app/app/components/auth-settings.js index e2634b1f..3703cf5c 100644 --- a/app/app/components/auth-settings.js +++ b/app/app/components/auth-settings.js @@ -111,5 +111,9 @@ export default Ember.Component.extend({ this.get('onSave')(provider, config).then(() => { }); }, + + onSync() { + this.get('onSync')(); + } } }); diff --git a/app/app/pods/customize/auth/controller.js b/app/app/pods/customize/auth/controller.js index 6c965c4e..81d7f93d 100644 --- a/app/app/pods/customize/auth/controller.js +++ b/app/app/pods/customize/auth/controller.js @@ -27,6 +27,12 @@ export default Ember.Controller.extend(NotifierMixin, { this.set('appMeta.authConfig', config); }); } + }, + + onSync() { + return this.get('global').syncExternalUsers().then((response) => { + this.showNotification(response.message); + }); } } }); diff --git a/app/app/pods/customize/auth/template.hbs b/app/app/pods/customize/auth/template.hbs index 829cc8aa..4a9fff03 100644 --- a/app/app/pods/customize/auth/template.hbs +++ b/app/app/pods/customize/auth/template.hbs @@ -1 +1 @@ -{{auth-settings authProvider=model.authProvider authConfig=model.authConfig onSave=(action 'onSave')}} +{{auth-settings authProvider=model.authProvider authConfig=model.authConfig onSave=(action 'onSave') onSync=(action 'onSync')}} diff --git a/app/app/pods/customize/users/route.js b/app/app/pods/customize/users/route.js index 913be226..c3d66f8c 100644 --- a/app/app/pods/customize/users/route.js +++ b/app/app/pods/customize/users/route.js @@ -14,6 +14,7 @@ import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-rout export default Ember.Route.extend(AuthenticatedRouteMixin, { userService: Ember.inject.service('user'), + global: Ember.inject.service('global'), beforeModel: function () { if (!this.session.isAdmin) { @@ -21,8 +22,14 @@ export default Ember.Route.extend(AuthenticatedRouteMixin, { } }, - model: function () { - return this.get('userService').getAll(); + model() { + return new Ember.RSVP.Promise((resolve) => { + this.get('global').syncExternalUsers().then(() => { + this.get('userService').getAll().then((users) =>{ + resolve(users); + }); + }); + }); }, activate: function () { diff --git a/app/app/services/global.js b/app/app/services/global.js index 4b659818..4e69a23e 100644 --- a/app/app/services/global.js +++ b/app/app/services/global.js @@ -73,5 +73,15 @@ export default Ember.Service.extend({ data: JSON.stringify(config) }); } - } + }, + + syncExternalUsers() { + if(this.get('sessionService.isGlobalAdmin')) { + return this.get('ajax').request(`users/sync`, { + method: 'GET' + }).then((response) => { + return response; + }); + } + }, }); diff --git a/app/app/templates/components/auth-settings.hbs b/app/app/templates/components/auth-settings.hbs index dfca4b7c..a52dc437 100644 --- a/app/app/templates/components/auth-settings.hbs +++ b/app/app/templates/components/auth-settings.hbs @@ -48,4 +48,6 @@ {{/if}}
+ + diff --git a/core/api/endpoint/keycloak.go b/core/api/endpoint/keycloak.go index bfdbc359..e1e442f2 100644 --- a/core/api/endpoint/keycloak.go +++ b/core/api/endpoint/keycloak.go @@ -28,6 +28,7 @@ import ( "github.com/documize/community/core/api/util" "github.com/documize/community/core/log" "github.com/documize/community/core/utility" + "sort" "strconv" ) @@ -117,7 +118,15 @@ func AuthenticateKeycloak(w http.ResponseWriter, r *http.Request) { return } - user, err = addUser(p, a) + user = entity.User{} + user.Firstname = a.Firstname + user.Lastname = a.Lastname + user.Email = a.Email + user.Initials = utility.MakeInitials(user.Firstname, user.Lastname) + user.Salt = util.GenerateSalt() + user.Password = util.GeneratePassword(util.GenerateRandomPassword(), user.Salt) + + err = addUser(p, &user) if err != nil { writeServerError(w, method, err) return @@ -162,32 +171,100 @@ func AuthenticateKeycloak(w http.ResponseWriter, r *http.Request) { return } - err = SyncUsers(ac) - if err != nil { - log.Error("su", err) - } - writeSuccessBytes(w, json) } -// Helper method to setup user account in Documize using Keycloak provided user data. -func addUser(p request.Persister, a keycloakAuthRequest) (u entity.User, err error) { - u.Firstname = a.Firstname - u.Lastname = a.Lastname - u.Email = a.Email - u.Initials = utility.MakeInitials(a.Firstname, a.Lastname) - u.Salt = util.GenerateSalt() - u.Password = util.GeneratePassword(util.GenerateRandomPassword(), u.Salt) +// SyncKeycloak gets list of Keycloak users and inserts new users into Documize +// and marks Keycloak disabled users as inactive. +func SyncKeycloak(w http.ResponseWriter, r *http.Request) { + p := request.GetPersister(r) + if !p.Context.Administrator { + writeForbiddenError(w) + return + } + + var result struct { + Message string `json:"message"` + } + + // Org contains raw auth provider config + org, err := p.GetOrganization(p.Context.OrgID) + if err != nil { + result.Message = "Unable to get organization record" + log.Error(result.Message, err) + util.WriteJSON(w, result) + return + } + + // Make Keycloak auth provider config + c := keycloakConfig{} + err = json.Unmarshal([]byte(org.AuthConfig), &c) + if err != nil { + result.Message = "Unable process Keycloak public key" + log.Error(result.Message, err) + util.WriteJSON(w, result) + return + } + + // User list from Keycloak + kcUsers, err := KeycloakUsers(c) + if err != nil { + result.Message = "Unable to fetch Keycloak users: " + err.Error() + log.Error(result.Message, err) + util.WriteJSON(w, result) + return + } + + // User list from Documize + dmzUsers, err := p.GetUsersForOrganization() + if err != nil { + result.Message = "Unable to fetch Documize users" + log.Error(result.Message, err) + util.WriteJSON(w, result) + 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 := []entity.User{} + + for _, k := range kcUsers { + exists := false + + for _, d := range dmzUsers { + if k.Email == d.Email { + exists = true + } + } + + if !exists { + insert = append(insert, k) + } + } + + // Insert new users into Documize + for _, u := range insert { + err = addUser(p, &u) + } + + result.Message = fmt.Sprintf("Keycloak sync'ed %d users, %d new additions", len(kcUsers), len(insert)) + log.Info(result.Message) + util.WriteJSON(w, result) +} + +// Helper method to setup user account in Documize using Keycloak provided user data. +func addUser(p request.Persister, u *entity.User) (err error) { // only create account if not dupe addUser := true addAccount := true var userID string - userDupe, err := p.GetUserByEmail(a.Email) + userDupe, err := p.GetUserByEmail(u.Email) if err != nil && err != sql.ErrNoRows { - return u, err + return err } if u.Email == userDupe.Email { @@ -197,17 +274,17 @@ func addUser(p request.Persister, a keycloakAuthRequest) (u entity.User, err err p.Context.Transaction, err = request.Db.Beginx() if err != nil { - return u, err + return err } if addUser { userID = util.UniqueID() u.RefID = userID - err = p.AddUser(u) + err = p.AddUser(*u) if err != nil { log.IfErr(p.Context.Transaction.Rollback()) - return u, err + return err } } else { attachUserAccounts(p, p.Context.OrgID, &userDupe) @@ -234,23 +311,22 @@ func addUser(p request.Persister, a keycloakAuthRequest) (u entity.User, err err err = p.AddAccount(a) if err != nil { log.IfErr(p.Context.Transaction.Rollback()) - return u, err + return err } } log.IfErr(p.Context.Transaction.Commit()) - // If we did not add user or give them access (account) then we error back - if !addUser && !addAccount { - log.IfErr(p.Context.Transaction.Rollback()) - return u, err - } + nu, err := p.GetUser(userID) + u = &nu - return p.GetUser(userID) + return err } -// SyncUsers gets list of Keycloak users for specified Realm, Client Id -func SyncUsers(c keycloakConfig) (err error) { +// KeycloakUsers gets list of Keycloak users for specified Realm, Client Id +func KeycloakUsers(c keycloakConfig) (users []entity.User, err error) { + users = []entity.User{} + form := url.Values{} form.Add("username", c.AdminUser) form.Add("password", c.AdminPassword) @@ -267,59 +343,65 @@ func SyncUsers(c keycloakConfig) (err error) { client := &http.Client{} res, err := client.Do(req) if err != nil { - return err + return users, err } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { - return err + return users, err } ka := keycloakAPIAuth{} err = json.Unmarshal(body, &ka) if err != nil { - return err + return users, err } if res.StatusCode != http.StatusOK { - return errors.New("Keycloak authentication failed " + res.Status) + return users, errors.New("Keycloak authentication failed " + res.Status) } - req, err = http.NewRequest("GET", - fmt.Sprintf("%s/admin/realms/%s/users?max=500", c.URL, c.Realm), - nil) + req, err = http.NewRequest("GET", fmt.Sprintf("%s/admin/realms/%s/users?max=500", c.URL, c.Realm), nil) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", ka.AccessToken)) client = &http.Client{} res, err = client.Do(req) if err != nil { - return err + return users, err + } defer res.Body.Close() body, err = ioutil.ReadAll(res.Body) if err != nil { - return err + return users, err } - u := []keycloakUser{} - err = json.Unmarshal(body, &u) + kcUsers := []keycloakUser{} + err = json.Unmarshal(body, &kcUsers) if err != nil { - return err + return users, err } if res.StatusCode != http.StatusOK { - return errors.New("Keycloak /users call failed " + res.Status) + return users, errors.New("Keycloak /users call failed " + res.Status) } - log.Info(fmt.Sprintf("%d", res.StatusCode)) + for _, kc := range kcUsers { + u := entity.User{} + u.Email = kc.Email + u.Firstname = kc.Firstname + u.Lastname = kc.Lastname + u.Initials = utility.MakeInitials(u.Firstname, u.Lastname) + u.Active = kc.Enabled + u.Editor = false - fmt.Println(fmt.Sprintf("%d len", len(u))) - fmt.Println(u[0].Email) + users = append(users, u) + } - return nil + return users, nil } // Data received via Keycloak client library diff --git a/core/api/endpoint/router.go b/core/api/endpoint/router.go index 8d02c036..e1ec83be 100644 --- a/core/api/endpoint/router.go +++ b/core/api/endpoint/router.go @@ -202,6 +202,7 @@ func init() { log.IfErr(Add(RoutePrefixPrivate, "users/{userID}", []string{"GET", "OPTIONS"}, nil, GetUser)) log.IfErr(Add(RoutePrefixPrivate, "users/{userID}", []string{"PUT", "OPTIONS"}, nil, UpdateUser)) log.IfErr(Add(RoutePrefixPrivate, "users/{userID}", []string{"DELETE", "OPTIONS"}, nil, DeleteUser)) + log.IfErr(Add(RoutePrefixPrivate, "users/sync", []string{"GET", "OPTIONS"}, nil, SyncKeycloak)) // Search log.IfErr(Add(RoutePrefixPrivate, "search", []string{"GET", "OPTIONS"}, nil, SearchDocuments)) diff --git a/core/api/entity/objects.go b/core/api/entity/objects.go index 4825c8d4..27661e80 100644 --- a/core/api/entity/objects.go +++ b/core/api/entity/objects.go @@ -64,6 +64,17 @@ func (user *User) Fullname() string { return fmt.Sprintf("%s %s", user.Firstname, user.Lastname) } +// GetAccount returns matching org account using orgID +func (user *User) GetAccount(orgID string) (a Account, found bool) { + for _, a := range user.Accounts { + if a.OrgID == orgID { + return a, true + } + } + + return a, false +} + // Organization defines a company that uses this app. type Organization struct { BaseEntity diff --git a/core/database/check.go b/core/database/check.go index f7db93ab..3fc83f29 100644 --- a/core/database/check.go +++ b/core/database/check.go @@ -38,7 +38,7 @@ var dbPtr **sqlx.DB func Check(Db *sqlx.DB, connectionString string) bool { dbPtr = &Db - log.Info("Running database checks, this may take a while...") + log.Info("Database checks: started") csBits := strings.Split(connectionString, "/") if len(csBits) > 1 { @@ -73,8 +73,8 @@ func Check(Db *sqlx.DB, connectionString string) bool { // MySQL and Percona share same version scheme (e..g 5.7.10). // MariaDB starts at 10.2.x sqlVariant := GetSQLVariant(dbComment) - log.Info("SQL variant: " + sqlVariant) - log.Info("SQL version: " + version) + log.Info("Database checks: SQL variant " + sqlVariant) + log.Info("Database checks: SQL version " + version) verNums, err := GetSQLVersion(version) if err != nil { diff --git a/core/database/migrate.go b/core/database/migrate.go index 2d97976c..d80bce2a 100644 --- a/core/database/migrate.go +++ b/core/database/migrate.go @@ -211,7 +211,7 @@ func Migrate(ConfigTableExists bool) error { if err != nil { return migrateEnd(tx, err, amLeader) } - log.Info("Database checks: last previously applied file was " + lastMigration) + log.Info("Database checks: last applied " + lastMigration) } mig, err := migrations(lastMigration) @@ -220,7 +220,7 @@ func Migrate(ConfigTableExists bool) error { } if len(mig) == 0 { - log.Info("Database checks: no updates to perform") + log.Info("Database checks: no updates required") return migrateEnd(tx, nil, amLeader) // no migrations to perform } @@ -233,7 +233,7 @@ func Migrate(ConfigTableExists bool) error { targetMigration := string(mig[len(mig)-1]) for targetMigration != lastMigration { time.Sleep(time.Second) - log.Info("Waiting for database migration process to complete") + log.Info("Waiting for database migration completion") tx.Rollback() // ignore error tx, err := (*dbPtr).Beginx() // need this in order to see the changed situation since last tx if err != nil {