diff --git a/.jshintignore b/.jshintignore index af18237a..699b2e42 100644 --- a/.jshintignore +++ b/.jshintignore @@ -1,4 +1,3 @@ gui/public/tinymce/** gui/public/tinymce/ -gui/public/tinymce - +gui/public/tinymce \ No newline at end of file diff --git a/domain/permission/endpoint.go b/domain/permission/endpoint.go index 0fb7c808..015f2186 100644 --- a/domain/permission/endpoint.go +++ b/domain/permission/endpoint.go @@ -28,8 +28,10 @@ import ( "github.com/documize/community/domain" "github.com/documize/community/domain/mail" "github.com/documize/community/model/audit" + "github.com/documize/community/model/group" "github.com/documize/community/model/permission" "github.com/documize/community/model/space" + "github.com/documize/community/model/user" ) // Handler contains the runtime information such as logging and database. @@ -122,52 +124,85 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) { hasEveryoneRole := false roleCount := 0 + // Permissions can be assigned to both groups and individual users. + // Pre-fetch users with group membership to help us work out + // if user belongs to a group with permissions. + groupMembers, err := h.Store.Group.GetMembers(ctx) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + for _, perm := range model.Permissions { perm.OrgID = ctx.OrgID perm.SpaceID = id + isGroup := perm.Who == permission.GroupPermission + groupRecords := []group.Record{} + + if isGroup { + // get group records for just this group + groupRecords = group.FilterGroupRecords(groupMembers, perm.WhoID) + } + // Ensure the space owner always has access! - if perm.UserID == ctx.UserID { + if (!isGroup && perm.WhoID == ctx.UserID) || + (isGroup && group.UserHasGroupMembership(groupMembers, perm.WhoID, ctx.UserID)) { me = true } // Only persist if there is a role! if permission.HasAnyPermission(perm) { // identify publically shared spaces - if perm.UserID == "" { - perm.UserID = "0" + if perm.WhoID == "" { + perm.WhoID = user.EveryoneUserID } - - if perm.UserID == "0" { + if perm.WhoID == user.EveryoneUserID { hasEveryoneRole = true } + // Encode group/user permission and save to store. r := permission.EncodeUserPermissions(perm) roleCount++ - for _, p := range r { err = h.Store.Permission.AddPermission(ctx, p) if err != nil { - h.Runtime.Log.Error("set permission", err) + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) } } // We send out space invitation emails to those users // that have *just* been given permissions. - if _, isExisting := previousRoleUsers[perm.UserID]; !isExisting { + if _, isExisting := previousRoleUsers[perm.WhoID]; !isExisting { + // we skip 'everyone' + if perm.WhoID != user.EveryoneUserID { + whoToEmail := []string{} - // we skip 'everyone' (user id != empty string) - if perm.UserID != "0" && perm.UserID != "" { - existingUser, err := h.Store.User.Get(ctx, perm.UserID) - if err != nil { - response.WriteServerError(w, method, err) - h.Runtime.Log.Error(method, err) - break + if isGroup { + // send email to each group member + for i := range groupRecords { + whoToEmail = append(whoToEmail, groupRecords[i].UserID) + } + } else { + // send email to individual user + whoToEmail = append(whoToEmail, perm.WhoID) } - mailer := mail.Mailer{Runtime: h.Runtime, Store: h.Store, Context: ctx} - go mailer.ShareSpaceExistingUser(existingUser.Email, inviter.Fullname(), url, sp.Name, model.Message) - h.Runtime.Log.Info(fmt.Sprintf("%s is sharing space %s with existing user %s", inviter.Email, sp.Name, existingUser.Email)) + for i := range whoToEmail { + existingUser, err := h.Store.User.Get(ctx, whoToEmail[i]) + if err != nil { + h.Runtime.Log.Error(method, err) + continue + } + + mailer := mail.Mailer{Runtime: h.Runtime, Store: h.Store, Context: ctx} + go mailer.ShareSpaceExistingUser(existingUser.Email, inviter.Fullname(), url, sp.Name, model.Message) + h.Runtime.Log.Info(fmt.Sprintf("%s is sharing space %s with existing user %s", inviter.Email, sp.Name, existingUser.Email)) + } } } } @@ -233,6 +268,7 @@ func (h *Handler) GetSpacePermissions(w http.ResponseWriter, r *http.Request) { perms, err := h.Store.Permission.GetSpacePermissions(ctx, spaceID) if err != nil && err != sql.ErrNoRows { response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) return } @@ -246,6 +282,40 @@ func (h *Handler) GetSpacePermissions(w http.ResponseWriter, r *http.Request) { records = append(records, permission.DecodeUserPermissions(up)) } + // populate user/group name for thing that has permission record + groups, err := h.Store.Group.GetAll(ctx) + if err != nil && err != sql.ErrNoRows { + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + for i := range records { + if records[i].Who == permission.GroupPermission { + for j := range groups { + if records[i].WhoID == groups[j].RefID { + records[i].Name = groups[j].Name + break + } + } + } + + if records[i].Who == permission.UserPermission { + if records[i].WhoID == user.EveryoneUserID { + records[i].Name = "Everyone" + } else { + u, err := h.Store.User.Get(ctx, records[i].WhoID) + if err != nil { + h.Runtime.Log.Info(fmt.Sprintf("user not found %s", records[i].WhoID)) + h.Runtime.Log.Error(method, err) + continue + } + + records[i].Name = u.Fullname() + } + } + } + response.WriteJSON(w, records) } @@ -261,7 +331,7 @@ func (h *Handler) GetUserSpacePermissions(w http.ResponseWriter, r *http.Request } perms, err := h.Store.Permission.GetUserSpacePermissions(ctx, spaceID) - if err != nil && err != sql.ErrNoRows { + if err != nil { response.WriteServerError(w, method, err) return } @@ -464,11 +534,6 @@ func (h *Handler) SetDocumentPermissions(w http.ResponseWriter, r *http.Request) return } - // if !HasPermission(ctx, *h.Store, doc.LabelID, permission.SpaceManage, permission.SpaceOwner) { - // response.WriteForbiddenError(w) - // return - // } - defer streamutil.Close(r.Body) body, err := ioutil.ReadAll(r.Body) if err != nil { @@ -528,17 +593,38 @@ func (h *Handler) SetDocumentPermissions(w http.ResponseWriter, r *http.Request) return } - url := ctx.GetAppURL(fmt.Sprintf("s/%s/%s/d/%s/%s", - sp.RefID, stringutil.MakeSlug(sp.Name), doc.RefID, stringutil.MakeSlug(doc.Title))) + url := ctx.GetAppURL(fmt.Sprintf("s/%s/%s/d/%s/%s", sp.RefID, stringutil.MakeSlug(sp.Name), doc.RefID, stringutil.MakeSlug(doc.Title))) + + // Permissions can be assigned to both groups and individual users. + // Pre-fetch users with group membership to help us work out + // if user belongs to a group with permissions. + groupMembers, err := h.Store.Group.GetMembers(ctx) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } for _, perm := range model { perm.OrgID = ctx.OrgID perm.DocumentID = id + isGroup := perm.Who == permission.GroupPermission + groupRecords := []group.Record{} + + if isGroup { + // get group records for just this group + groupRecords = group.FilterGroupRecords(groupMembers, perm.WhoID) + } + // Only persist if there is a role! if permission.HasAnyDocumentPermission(perm) { - r := permission.EncodeUserDocumentPermissions(perm) + if perm.WhoID == "" { + perm.WhoID = user.EveryoneUserID + } + r := permission.EncodeUserDocumentPermissions(perm) for _, p := range r { err = h.Store.Permission.AddPermission(ctx, p) if err != nil { @@ -547,19 +633,32 @@ func (h *Handler) SetDocumentPermissions(w http.ResponseWriter, r *http.Request) } // Send email notification to users who have been given document approver role - if _, isExisting := previousRoleUsers[perm.UserID]; !isExisting { + if _, isExisting := previousRoleUsers[perm.WhoID]; !isExisting { + // we skip 'everyone' as it has no email address! + if perm.WhoID != user.EveryoneUserID && perm.DocumentRoleApprove { + whoToEmail := []string{} - // we skip 'everyone' (user id != empty string) - if perm.UserID != "0" && perm.UserID != "" && perm.DocumentRoleApprove { - existingUser, err := h.Store.User.Get(ctx, perm.UserID) - if err != nil { - response.WriteServerError(w, method, err) - break + if isGroup { + // send email to each group member + for i := range groupRecords { + whoToEmail = append(whoToEmail, groupRecords[i].UserID) + } + } else { + // send email to individual user + whoToEmail = append(whoToEmail, perm.WhoID) } - mailer := mail.Mailer{Runtime: h.Runtime, Store: h.Store, Context: ctx} - go mailer.DocumentApprover(existingUser.Email, inviter.Fullname(), url, doc.Title) - h.Runtime.Log.Info(fmt.Sprintf("%s has made %s document approver for: %s", inviter.Email, existingUser.Email, doc.Title)) + for i := range whoToEmail { + existingUser, err := h.Store.User.Get(ctx, whoToEmail[i]) + if err != nil { + h.Runtime.Log.Error(method, err) + continue + } + + mailer := mail.Mailer{Runtime: h.Runtime, Store: h.Store, Context: ctx} + go mailer.DocumentApprover(existingUser.Email, inviter.Fullname(), url, doc.Title) + h.Runtime.Log.Info(fmt.Sprintf("%s has made %s document approver for: %s", inviter.Email, existingUser.Email, doc.Title)) + } } } } diff --git a/domain/permission/mysql/store.go b/domain/permission/mysql/store.go index f2a08998..2c2a3554 100644 --- a/domain/permission/mysql/store.go +++ b/domain/permission/mysql/store.go @@ -35,7 +35,7 @@ func (s Scope) AddPermission(ctx domain.RequestContext, r permission.Permission) r.Created = time.Now().UTC() _, err = ctx.Transaction.Exec("INSERT INTO permission (orgid, who, whoid, action, scope, location, refid, created) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - r.OrgID, r.Who, r.WhoID, string(r.Action), r.Scope, r.Location, r.RefID, r.Created) + r.OrgID, string(r.Who), r.WhoID, string(r.Action), string(r.Scope), string(r.Location), r.RefID, r.Created) if err != nil { err = errors.Wrap(err, "unable to execute insert permission") @@ -64,7 +64,8 @@ func (s Scope) AddPermissions(ctx domain.RequestContext, r permission.Permission func (s Scope) GetUserSpacePermissions(ctx domain.RequestContext, spaceID string) (r []permission.Permission, err error) { err = s.Runtime.Db.Select(&r, ` SELECT id, orgid, who, whoid, action, scope, location, refid - FROM permission WHERE orgid=? AND location='space' AND refid=? AND who='user' AND (whoid=? OR whoid='0') + FROM permission + WHERE orgid=? AND location='space' AND refid=? AND who='user' AND (whoid=? OR whoid='0') UNION ALL SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid FROM permission p diff --git a/gui/app/components/customize/user-list.js b/gui/app/components/customize/user-list.js index 0803ecb7..b1a53f12 100644 --- a/gui/app/components/customize/user-list.js +++ b/gui/app/components/customize/user-list.js @@ -203,7 +203,6 @@ export default Component.extend(AuthProvider, ModalMixin, { group.set('isMember', false); if (is.undefined(groupId) || is.undefined(userId)) { - console.log(groupId, userId); return; } @@ -218,7 +217,6 @@ export default Component.extend(AuthProvider, ModalMixin, { group.set('isMember', true); if (is.undefined(groupId) || is.undefined(userId)) { - console.log(groupId, userId); return; } diff --git a/gui/app/components/folder/permission-admin.js b/gui/app/components/folder/permission-admin.js index f3165a66..c91010e9 100644 --- a/gui/app/components/folder/permission-admin.js +++ b/gui/app/components/folder/permission-admin.js @@ -9,80 +9,78 @@ // // https://documize.com -import { setProperties } from '@ember/object'; -import Component from '@ember/component'; import { inject as service } from '@ember/service'; +import { A } from "@ember/array" import ModalMixin from '../../mixins/modal'; +import Component from '@ember/component'; export default Component.extend(ModalMixin, { - folderService: service('folder'), - userService: service('user'), - appMeta: service(), + groupSvc: service('group'), + spaceSvc: service('folder'), + userSvc: service('user'), + appMeta: service(), store: service(), + spacePermissions: null, didReceiveAttrs() { - this.get('userService').getSpaceUsers(this.get('folder.id')).then((users) => { - this.set('users', users); + let spacePermissions = A([]); + let constants = this.get('constants'); - // set up users - let folderPermissions = []; + // get groups + this.get('groupSvc').getAll().then((groups) => { + this.set('groups', groups); - users.forEach((user) => { - let u = { - orgId: this.get('folder.orgId'), - folderId: this.get('folder.id'), - userId: user.get('id'), - fullname: user.get('fullname'), - spaceView: false, - spaceManage: false, - spaceOwner: false, - documentAdd: false, - documentEdit: false, - documentDelete: false, - documentMove: false, - documentCopy: false, - documentTemplate: false, - documentApprove: false, - }; - - let data = this.get('store').normalize('space-permission', u) - folderPermissions.pushObject(this.get('store').push(data)); + groups.forEach((g) => { + let pr = this.permissionRecord(constants.WhoType.Group, g.get('id'), g.get('name')); + spacePermissions.pushObject(pr); }); - // set up Everyone user - let u = { - orgId: this.get('folder.orgId'), - folderId: this.get('folder.id'), - userId: '0', - fullname: ' Everyone', - spaceView: false, - spaceManage: false, - spaceOwner: false, - documentAdd: false, - documentEdit: false, - documentDelete: false, - documentMove: false, - documentCopy: false, - documentTemplate: false, - documentApprove: false, - }; - - let data = this.get('store').normalize('space-permission', u) - folderPermissions.pushObject(this.get('store').push(data)); - - this.get('folderService').getPermissions(this.get('folder.id')).then((permissions) => { - permissions.forEach((permission, index) => { // eslint-disable-line no-unused-vars - let record = folderPermissions.findBy('userId', permission.get('userId')); - if (is.not.undefined(record)) { - record = setProperties(record, permission); + // get space permissions + this.get('spaceSvc').getPermissions(this.get('folder.id')).then((permissions) => { + permissions.forEach((perm, index) => { // eslint-disable-line no-unused-vars + // is this permission for group or user? + if (perm.get('who') === constants.WhoType.Group) { + // group permission + spacePermissions.forEach((sp) => { + if (sp.get('whoId') == perm.get('whoId')) { + sp.setProperties(perm); + } + }); + } else { + // user permission + spacePermissions.pushObject(perm); } }); - this.set('permissions', folderPermissions.sortBy('fullname')); + this.set('spacePermissions', spacePermissions.sortBy('who', 'name')); }); }); }, + permissionRecord(who, whoId, name) { + let raw = { + id: whoId, + orgId: this.get('folder.orgId'), + folderId: this.get('folder.id'), + whoId: whoId, + who: who, + name: name, + spaceView: false, + spaceManage: false, + spaceOwner: false, + documentAdd: false, + documentEdit: false, + documentDelete: false, + documentMove: false, + documentCopy: false, + documentTemplate: false, + documentApprove: false, + }; + + let rec = this.get('store').normalize('space-permission', raw); + return this.get('store').push(rec); + }, + getDefaultInvitationMessage() { return "Hey there, I am sharing the " + this.get('folder.name') + " space (in " + this.get("appMeta.title") + ") with you so we can both collaborate on documents."; }, @@ -90,12 +88,13 @@ export default Component.extend(ModalMixin, { actions: { setPermissions() { let message = this.getDefaultInvitationMessage(); - let permissions = this.get('permissions'); + let permissions = this.get('spacePermissions'); let folder = this.get('folder'); let payload = { Message: message, Permissions: permissions }; + let constants = this.get('constants'); - let hasEveryone = _.find(permissions, function (permission) { - return permission.get('userId') === "0" && + let hasEveryone = _.find(permissions, (permission) => { + return permission.get('whoId') === constants.EveryoneUserId && (permission.get('spaceView') || permission.get('documentAdd') || permission.get('documentEdit') || permission.get('documentDelete') || permission.get('documentMove') || permission.get('documentCopy') || permission.get('documentTemplate') || permission.get('documentApprove')); }); @@ -103,7 +102,7 @@ export default Component.extend(ModalMixin, { // see if more than oen user is granted access to space (excluding everyone) let roleCount = 0; permissions.forEach((permission) => { - if (permission.get('userId') !== "0" && + if (permission.get('whoId') !== constants.EveryoneUserId && (permission.get('spaceView') || permission.get('documentAdd') || permission.get('documentEdit') || permission.get('documentDelete') || permission.get('documentMove') || permission.get('documentCopy') || permission.get('documentTemplate') || permission.get('documentApprove'))) { roleCount += 1; @@ -120,7 +119,7 @@ export default Component.extend(ModalMixin, { } } - this.get('folderService').savePermissions(folder.get('id'), payload).then(() => { + this.get('spaceSvc').savePermissions(folder.get('id'), payload).then(() => { this.modalClose('#space-permission-modal'); }); } diff --git a/gui/app/components/toolbar/for-space.js b/gui/app/components/toolbar/for-space.js index 45986935..e17a31f4 100644 --- a/gui/app/components/toolbar/for-space.js +++ b/gui/app/components/toolbar/for-space.js @@ -10,7 +10,6 @@ // https://documize.com import $ from 'jquery'; -import Component from '@ember/component'; import { computed } from '@ember/object'; import { schedule } from '@ember/runloop'; import { inject as service } from '@ember/service'; @@ -18,6 +17,7 @@ import TooltipMixin from '../../mixins/tooltip'; import ModalMixin from '../../mixins/modal'; import AuthMixin from '../../mixins/auth'; import stringUtil from '../../utils/string'; +import Component from '@ember/component'; export default Component.extend(ModalMixin, TooltipMixin, AuthMixin, { spaceService: service('folder'), diff --git a/gui/app/constants/constants.js b/gui/app/constants/constants.js index 6d440867..72e0a3c3 100644 --- a/gui/app/constants/constants.js +++ b/gui/app/constants/constants.js @@ -11,6 +11,9 @@ import EmberObject from "@ember/object"; +// access like so: +// let constants = this.get('constants'); + let constants = EmberObject.extend({ // Document ProtectionType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects @@ -48,7 +51,15 @@ let constants = EmberObject.extend({ PageType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects Tab: 'tab', Section: 'section' - } + }, + + // Who a permission record relates to + WhoType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects + User: 'user', + Group: 'role' + }, + + EveryoneUserId: "0" }); export default { constants } \ No newline at end of file diff --git a/gui/app/models/space-permission.js b/gui/app/models/space-permission.js index 8072f6eb..67b2b269 100644 --- a/gui/app/models/space-permission.js +++ b/gui/app/models/space-permission.js @@ -11,14 +11,12 @@ import Model from 'ember-data/model'; import attr from 'ember-data/attr'; -// import { belongsTo, hasMany } from 'ember-data/relationships'; export default Model.extend({ orgId: attr('string'), folderId: attr('string'), - userId: attr('string'), - fullname: attr('string'), // client-side usage only, not from API - + whoId: attr('string'), + who: attr('string'), spaceView: attr('boolean'), spaceManage: attr('boolean'), spaceOwner: attr('boolean'), @@ -28,5 +26,6 @@ export default Model.extend({ documentMove: attr('boolean'), documentCopy: attr('boolean'), documentTemplate: attr('boolean'), - documentApprove: attr('boolean') + documentApprove: attr('boolean'), + name: attr('string') // read-only }); diff --git a/gui/app/serializers/space-permission.js b/gui/app/serializers/space-permission.js index 2c2733a5..c6542cf8 100644 --- a/gui/app/serializers/space-permission.js +++ b/gui/app/serializers/space-permission.js @@ -4,7 +4,7 @@ export default ApplicationSerializer.extend({ normalize(modelClass, resourceHash) { return { data: { - id: resourceHash.userId ? resourceHash.userId : 0, + id: resourceHash.whoId ? resourceHash.whoId : 0, type: modelClass.modelName, attributes: resourceHash } diff --git a/gui/app/services/folder.js b/gui/app/services/folder.js index e68e67f7..4a344546 100644 --- a/gui/app/services/folder.js +++ b/gui/app/services/folder.js @@ -20,6 +20,7 @@ export default BaseService.extend({ localStorage: service(), store: service(), currentFolder: null, + permissions: null, init() { this._super(...arguments); diff --git a/gui/app/templates/components/folder/permission-admin.hbs b/gui/app/templates/components/folder/permission-admin.hbs index d2610da8..8f60401d 100644 --- a/gui/app/templates/components/folder/permission-admin.hbs +++ b/gui/app/templates/components/folder/permission-admin.hbs @@ -27,38 +27,50 @@
- {{#each permissions as |permission|}} + {{#each spacePermissions as |permission|}}