diff --git a/core/database/scripts/autobuild/db_00017.sql b/core/database/scripts/autobuild/db_00017.sql index d66fc555..54353da2 100644 --- a/core/database/scripts/autobuild/db_00017.sql +++ b/core/database/scripts/autobuild/db_00017.sql @@ -4,10 +4,6 @@ ALTER TABLE document ADD COLUMN `protection` INT NOT NULL DEFAULT 0 AFTER `template`; ALTER TABLE document ADD COLUMN `approval` INT NOT NULL DEFAULT 0 AFTER `protection`; --- page needs proection and approval columns -ALTER TABLE page ADD COLUMN `protection` INT NOT NULL DEFAULT 0 AFTER `revisions`; -ALTER TABLE page ADD COLUMN `approval` INT NOT NULL DEFAULT 0 AFTER `protection`; - -- data migration clean up from previous releases DROP TABLE IF EXISTS `audit`; DROP TABLE IF EXISTS `search_old`; diff --git a/domain/document/endpoint.go b/domain/document/endpoint.go index 165a3515..7b628049 100644 --- a/domain/document/endpoint.go +++ b/domain/document/endpoint.go @@ -414,6 +414,17 @@ func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) { record := pm.DecodeUserPermissions(perms) + roles, err := h.Store.Permission.GetUserDocumentPermissions(ctx, document.RefID) + if err != nil && err != sql.ErrNoRows { + response.WriteServerError(w, method, err) + return + } + if len(roles) == 0 { + roles = []pm.Permission{} + } + + rolesRecord := pm.DecodeUserDocumentPermissions(roles) + // links l, err := h.Store.Link.GetDocumentOutboundLinks(ctx, id) if len(l) == 0 { @@ -439,6 +450,7 @@ func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) { data := documentData{} data.Document = document data.Permissions = record + data.Roles = rolesRecord data.Links = l data.Spaces = sp @@ -469,8 +481,9 @@ func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) { // documentData represents all data associated for a single document. // Used by FetchDocumentData() bulk data load call. type documentData struct { - Document doc.Document `json:"document"` - Permissions pm.Record `json:"permissions"` - Spaces []space.Space `json:"folders"` - Links []link.Link `json:"link"` + Document doc.Document `json:"document"` + Permissions pm.Record `json:"permissions"` + Roles pm.DocumentRecord `json:"roles"` + Spaces []space.Space `json:"folders"` + Links []link.Link `json:"link"` } diff --git a/domain/mail/document-approver.html b/domain/mail/document-approver.html new file mode 100644 index 00000000..9601892a --- /dev/null +++ b/domain/mail/document-approver.html @@ -0,0 +1,98 @@ + + + + +{{.Subject}} + + + + + + + + + + + +
+
+ + + + + + + +
+ Document Approval Role Granted +
+ + + + + + + + + + +
+

You are requested to approve all changes to the following document:

+

{{.Document}}

+

{{.Inviter}}

+
+ View document +
+ Have any questions? Contact Documize +
+
+
+
+ + + diff --git a/domain/mail/document.go b/domain/mail/document.go new file mode 100644 index 00000000..1089a49d --- /dev/null +++ b/domain/mail/document.go @@ -0,0 +1,70 @@ +// 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 + +// jshint ignore:start + +package mail + +import ( + "bytes" + "fmt" + "html/template" + + "github.com/documize/community/server/web" +) + +// DocumentApprover notifies user who has just been granted document approval rights. +func (m *Mailer) DocumentApprover(recipient, inviter, url, document string) { + method := "DocumentApprover" + m.LoadCredentials() + + file, err := web.ReadFile("mail/document-approver.html") + if err != nil { + m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err) + return + } + + emailTemplate := string(file) + + // check inviter name + if inviter == "Hello You" || len(inviter) == 0 { + inviter = "Your colleague" + } + + subject := fmt.Sprintf("%s has granted you document approval", inviter) + + e := NewEmail() + e.From = m.Credentials.SMTPsender + e.To = []string{recipient} + e.Subject = subject + + parameters := struct { + Subject string + Inviter string + Url string + Document string + }{ + subject, + inviter, + url, + document, + } + + buffer := new(bytes.Buffer) + t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) + t.Execute(buffer, ¶meters) + e.HTML = buffer.Bytes() + + err = e.Send(m.GetHost(), m.GetAuth()) + if err != nil { + m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) + } +} diff --git a/domain/mail/mailer.go b/domain/mail/mailer.go index 3377d6dc..47e742b2 100644 --- a/domain/mail/mailer.go +++ b/domain/mail/mailer.go @@ -14,15 +14,11 @@ package mail import ( - "bytes" - "fmt" - "html/template" "net/smtp" "strings" "github.com/documize/community/core/env" "github.com/documize/community/domain" - "github.com/documize/community/server/web" ) // Mailer provides emailing facilities @@ -33,241 +29,6 @@ type Mailer struct { Credentials Credentials } -// InviteNewUser invites someone new providing credentials, explaining the product and stating who is inviting them. -func (m *Mailer) InviteNewUser(recipient, inviter, url, username, password string) { - method := "InviteNewUser" - m.LoadCredentials() - - file, err := web.ReadFile("mail/invite-new-user.html") - if err != nil { - m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err) - return - } - - emailTemplate := string(file) - - // check inviter name - if inviter == "Hello You" || len(inviter) == 0 { - inviter = "Your colleague" - } - - subject := fmt.Sprintf("%s has invited you to Documize", inviter) - - e := NewEmail() - e.From = m.Credentials.SMTPsender - e.To = []string{recipient} - e.Subject = subject - - parameters := struct { - Subject string - Inviter string - Url string - Username string - Password string - }{ - subject, - inviter, - url, - recipient, - password, - } - - buffer := new(bytes.Buffer) - t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) - t.Execute(buffer, ¶meters) - e.HTML = buffer.Bytes() - - err = e.Send(m.GetHost(), m.GetAuth()) - if err != nil { - m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) - } -} - -// InviteExistingUser invites a known user to an organization. -func (m *Mailer) InviteExistingUser(recipient, inviter, url string) { - method := "InviteExistingUser" - m.LoadCredentials() - - file, err := web.ReadFile("mail/invite-existing-user.html") - if err != nil { - m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err) - return - } - - emailTemplate := string(file) - - // check inviter name - if inviter == "Hello You" || len(inviter) == 0 { - inviter = "Your colleague" - } - - subject := fmt.Sprintf("%s has invited you to their Documize account", inviter) - - e := NewEmail() - e.From = m.Credentials.SMTPsender - e.To = []string{recipient} - e.Subject = subject - - parameters := struct { - Subject string - Inviter string - Url string - }{ - subject, - inviter, - url, - } - - buffer := new(bytes.Buffer) - t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) - t.Execute(buffer, ¶meters) - e.HTML = buffer.Bytes() - - err = e.Send(m.GetHost(), m.GetAuth()) - if err != nil { - m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) - } -} - -// PasswordReset sends a reset email with an embedded token. -func (m *Mailer) PasswordReset(recipient, url string) { - method := "PasswordReset" - m.LoadCredentials() - - file, err := web.ReadFile("mail/password-reset.html") - if err != nil { - m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err) - return - } - - emailTemplate := string(file) - - subject := "Documize password reset request" - - e := NewEmail() - e.From = m.Credentials.SMTPsender //e.g. "Documize " - e.To = []string{recipient} - e.Subject = subject - - parameters := struct { - Subject string - Url string - }{ - subject, - url, - } - - buffer := new(bytes.Buffer) - t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) - t.Execute(buffer, ¶meters) - e.HTML = buffer.Bytes() - - err = e.Send(m.GetHost(), m.GetAuth()) - if err != nil { - m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) - } -} - -// ShareSpaceExistingUser provides an existing user with a link to a newly shared space. -func (m *Mailer) ShareSpaceExistingUser(recipient, inviter, url, folder, intro string) { - method := "ShareSpaceExistingUser" - m.LoadCredentials() - - file, err := web.ReadFile("mail/share-space-existing-user.html") - if err != nil { - m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err) - return - } - - emailTemplate := string(file) - - // check inviter name - if inviter == "Hello You" || len(inviter) == 0 { - inviter = "Your colleague" - } - - subject := fmt.Sprintf("%s has shared %s with you", inviter, folder) - - e := NewEmail() - e.From = m.Credentials.SMTPsender - e.To = []string{recipient} - e.Subject = subject - - parameters := struct { - Subject string - Inviter string - Url string - Folder string - Intro string - }{ - subject, - inviter, - url, - folder, - intro, - } - - buffer := new(bytes.Buffer) - t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) - t.Execute(buffer, ¶meters) - e.HTML = buffer.Bytes() - - err = e.Send(m.GetHost(), m.GetAuth()) - if err != nil { - m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) - } -} - -// ShareSpaceNewUser invites new user providing Credentials, explaining the product and stating who is inviting them. -func (m *Mailer) ShareSpaceNewUser(recipient, inviter, url, space, invitationMessage string) { - method := "ShareSpaceNewUser" - m.LoadCredentials() - - file, err := web.ReadFile("mail/share-space-new-user.html") - if err != nil { - m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err) - return - } - - emailTemplate := string(file) - - // check inviter name - if inviter == "Hello You" || len(inviter) == 0 { - inviter = "Your colleague" - } - - subject := fmt.Sprintf("%s has shared %s with you on Documize", inviter, space) - - e := NewEmail() - e.From = m.Credentials.SMTPsender - e.To = []string{recipient} - e.Subject = subject - - parameters := struct { - Subject string - Inviter string - Url string - Invitation string - Folder string - }{ - subject, - inviter, - url, - invitationMessage, - space, - } - - buffer := new(bytes.Buffer) - t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) - t.Execute(buffer, ¶meters) - e.HTML = buffer.Bytes() - - err = e.Send(m.GetHost(), m.GetAuth()) - if err != nil { - m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) - } -} - // Credentials holds SMTP endpoint and authentication methods type Credentials struct { SMTPuserid string diff --git a/domain/mail/space.go b/domain/mail/space.go new file mode 100644 index 00000000..7921e15a --- /dev/null +++ b/domain/mail/space.go @@ -0,0 +1,122 @@ +// 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 + +// jshint ignore:start + +package mail + +import ( + "bytes" + "fmt" + "html/template" + + "github.com/documize/community/server/web" +) + +// ShareSpaceExistingUser provides an existing user with a link to a newly shared space. +func (m *Mailer) ShareSpaceExistingUser(recipient, inviter, url, folder, intro string) { + method := "ShareSpaceExistingUser" + m.LoadCredentials() + + file, err := web.ReadFile("mail/share-space-existing-user.html") + if err != nil { + m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err) + return + } + + emailTemplate := string(file) + + // check inviter name + if inviter == "Hello You" || len(inviter) == 0 { + inviter = "Your colleague" + } + + subject := fmt.Sprintf("%s has shared %s with you", inviter, folder) + + e := NewEmail() + e.From = m.Credentials.SMTPsender + e.To = []string{recipient} + e.Subject = subject + + parameters := struct { + Subject string + Inviter string + Url string + Folder string + Intro string + }{ + subject, + inviter, + url, + folder, + intro, + } + + buffer := new(bytes.Buffer) + t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) + t.Execute(buffer, ¶meters) + e.HTML = buffer.Bytes() + + err = e.Send(m.GetHost(), m.GetAuth()) + if err != nil { + m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) + } +} + +// ShareSpaceNewUser invites new user providing Credentials, explaining the product and stating who is inviting them. +func (m *Mailer) ShareSpaceNewUser(recipient, inviter, url, space, invitationMessage string) { + method := "ShareSpaceNewUser" + m.LoadCredentials() + + file, err := web.ReadFile("mail/share-space-new-user.html") + if err != nil { + m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err) + return + } + + emailTemplate := string(file) + + // check inviter name + if inviter == "Hello You" || len(inviter) == 0 { + inviter = "Your colleague" + } + + subject := fmt.Sprintf("%s has shared %s with you on Documize", inviter, space) + + e := NewEmail() + e.From = m.Credentials.SMTPsender + e.To = []string{recipient} + e.Subject = subject + + parameters := struct { + Subject string + Inviter string + Url string + Invitation string + Folder string + }{ + subject, + inviter, + url, + invitationMessage, + space, + } + + buffer := new(bytes.Buffer) + t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) + t.Execute(buffer, ¶meters) + e.HTML = buffer.Bytes() + + err = e.Send(m.GetHost(), m.GetAuth()) + if err != nil { + m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) + } +} diff --git a/domain/mail/user.go b/domain/mail/user.go new file mode 100644 index 00000000..8d0d45db --- /dev/null +++ b/domain/mail/user.go @@ -0,0 +1,157 @@ +// 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 + +// jshint ignore:start + +package mail + +import ( + "bytes" + "fmt" + "html/template" + + "github.com/documize/community/server/web" +) + +// InviteNewUser invites someone new providing credentials, explaining the product and stating who is inviting them. +func (m *Mailer) InviteNewUser(recipient, inviter, url, username, password string) { + method := "InviteNewUser" + m.LoadCredentials() + + file, err := web.ReadFile("mail/invite-new-user.html") + if err != nil { + m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err) + return + } + + emailTemplate := string(file) + + // check inviter name + if inviter == "Hello You" || len(inviter) == 0 { + inviter = "Your colleague" + } + + subject := fmt.Sprintf("%s has invited you to Documize", inviter) + + e := NewEmail() + e.From = m.Credentials.SMTPsender + e.To = []string{recipient} + e.Subject = subject + + parameters := struct { + Subject string + Inviter string + Url string + Username string + Password string + }{ + subject, + inviter, + url, + recipient, + password, + } + + buffer := new(bytes.Buffer) + t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) + t.Execute(buffer, ¶meters) + e.HTML = buffer.Bytes() + + err = e.Send(m.GetHost(), m.GetAuth()) + if err != nil { + m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) + } +} + +// InviteExistingUser invites a known user to an organization. +func (m *Mailer) InviteExistingUser(recipient, inviter, url string) { + method := "InviteExistingUser" + m.LoadCredentials() + + file, err := web.ReadFile("mail/invite-existing-user.html") + if err != nil { + m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err) + return + } + + emailTemplate := string(file) + + // check inviter name + if inviter == "Hello You" || len(inviter) == 0 { + inviter = "Your colleague" + } + + subject := fmt.Sprintf("%s has invited you to their Documize account", inviter) + + e := NewEmail() + e.From = m.Credentials.SMTPsender + e.To = []string{recipient} + e.Subject = subject + + parameters := struct { + Subject string + Inviter string + Url string + }{ + subject, + inviter, + url, + } + + buffer := new(bytes.Buffer) + t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) + t.Execute(buffer, ¶meters) + e.HTML = buffer.Bytes() + + err = e.Send(m.GetHost(), m.GetAuth()) + if err != nil { + m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) + } +} + +// PasswordReset sends a reset email with an embedded token. +func (m *Mailer) PasswordReset(recipient, url string) { + method := "PasswordReset" + m.LoadCredentials() + + file, err := web.ReadFile("mail/password-reset.html") + if err != nil { + m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err) + return + } + + emailTemplate := string(file) + + subject := "Documize password reset request" + + e := NewEmail() + e.From = m.Credentials.SMTPsender //e.g. "Documize " + e.To = []string{recipient} + e.Subject = subject + + parameters := struct { + Subject string + Url string + }{ + subject, + url, + } + + buffer := new(bytes.Buffer) + t := template.Must(template.New("emailTemplate").Parse(emailTemplate)) + t.Execute(buffer, ¶meters) + e.HTML = buffer.Bytes() + + err = e.Send(m.GetHost(), m.GetAuth()) + if err != nil { + m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err) + } +} diff --git a/domain/page/mysql/store.go b/domain/page/mysql/store.go index c30ed97a..e9aa035f 100644 --- a/domain/page/mysql/store.go +++ b/domain/page/mysql/store.go @@ -54,8 +54,8 @@ func (s Scope) Add(ctx domain.RequestContext, model page.NewPage) (err error) { model.Page.Sequence = maxSeq * 2 } - _, err = ctx.Transaction.Exec("INSERT INTO page (refid, orgid, documentid, userid, contenttype, pagetype, level, title, body, revisions, sequence, blockid, protected, approval, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - model.Page.RefID, model.Page.OrgID, model.Page.DocumentID, model.Page.UserID, model.Page.ContentType, model.Page.PageType, model.Page.Level, model.Page.Title, model.Page.Body, model.Page.Revisions, model.Page.Sequence, model.Page.BlockID, model.Page.Protection, model.Page.Approval, model.Page.Created, model.Page.Revised) + _, err = ctx.Transaction.Exec("INSERT INTO page (refid, orgid, documentid, userid, contenttype, pagetype, level, title, body, revisions, sequence, blockid, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + model.Page.RefID, model.Page.OrgID, model.Page.DocumentID, model.Page.UserID, model.Page.ContentType, model.Page.PageType, model.Page.Level, model.Page.Title, model.Page.Body, model.Page.Revisions, model.Page.Sequence, model.Page.BlockID, model.Page.Created, model.Page.Revised) _, err = ctx.Transaction.Exec("INSERT INTO pagemeta (pageid, orgid, userid, documentid, rawbody, config, externalsource, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", model.Meta.PageID, model.Meta.OrgID, model.Meta.UserID, model.Meta.DocumentID, model.Meta.RawBody, model.Meta.Config, model.Meta.ExternalSource, model.Meta.Created, model.Meta.Revised) @@ -69,7 +69,7 @@ func (s Scope) Add(ctx domain.RequestContext, model page.NewPage) (err error) { // Get returns the pageID page record from the page table. func (s Scope) Get(ctx domain.RequestContext, pageID string) (p page.Page, err error) { - err = s.Runtime.Db.Get(&p, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.protection, a.approval, a.created, a.revised FROM page a WHERE a.orgid=? AND a.refid=?", + err = s.Runtime.Db.Get(&p, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.refid=?", ctx.OrgID, pageID) if err != nil { @@ -81,7 +81,7 @@ func (s Scope) Get(ctx domain.RequestContext, pageID string) (p page.Page, err e // GetPages returns a slice containing all the page records for a given documentID, in presentation sequence. func (s Scope) GetPages(ctx domain.RequestContext, documentID string) (p []page.Page, err error) { - err = s.Runtime.Db.Select(&p, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.protection, a.approval, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? ORDER BY a.sequence", ctx.OrgID, documentID) + err = s.Runtime.Db.Select(&p, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? ORDER BY a.sequence", ctx.OrgID, documentID) if err != nil { err = errors.Wrap(err, "execute get pages") @@ -93,7 +93,7 @@ func (s Scope) GetPages(ctx domain.RequestContext, documentID string) (p []page. // GetPagesWithoutContent returns a slice containing all the page records for a given documentID, in presentation sequence, // but without the body field (which holds the HTML content). func (s Scope) GetPagesWithoutContent(ctx domain.RequestContext, documentID string) (pages []page.Page, err error) { - err = s.Runtime.Db.Select(&pages, "SELECT id, refid, orgid, documentid, userid, contenttype, pagetype, sequence, level, title, revisions, blockid, protection, approval, created, revised FROM page WHERE orgid=? AND documentid=? ORDER BY sequence", ctx.OrgID, documentID) + err = s.Runtime.Db.Select(&pages, "SELECT id, refid, orgid, documentid, userid, contenttype, pagetype, sequence, level, title, revisions, blockid, created, revised FROM page WHERE orgid=? AND documentid=? ORDER BY sequence", ctx.OrgID, documentID) if err != nil { err = errors.Wrap(err, fmt.Sprintf("Unable to execute select pages for org %s and document %s", ctx.OrgID, documentID)) @@ -119,7 +119,7 @@ func (s Scope) Update(ctx domain.RequestContext, page page.Page, refID, userID s } // Update page - _, err = ctx.Transaction.NamedExec("UPDATE page SET documentid=:documentid, level=:level, title=:title, body=:body, revisions=:revisions, sequence=:sequence, protection=:protection, approval=:approval, revised=:revised WHERE orgid=:orgid AND refid=:refid", + _, err = ctx.Transaction.NamedExec("UPDATE page SET documentid=:documentid, level=:level, title=:title, body=:body, revisions=:revisions, sequence=:sequence, revised=:revised WHERE orgid=:orgid AND refid=:refid", &page) if err != nil { diff --git a/domain/permission/endpoint.go b/domain/permission/endpoint.go index 2dba99a1..5e705d8c 100644 --- a/domain/permission/endpoint.go +++ b/domain/permission/endpoint.go @@ -69,7 +69,7 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) { return } - var model = permission.PermissionsModel{} + var model = permission.SpaceRequestModel{} err = json.Unmarshal(body, &model) if err != nil { response.WriteServerError(w, method, err) @@ -398,3 +398,192 @@ func (h *Handler) SetCategoryPermissions(w http.ResponseWriter, r *http.Request) response.WriteEmpty(w) } + +// GetDocumentPermissions returns permissions for all users for given document. +func (h *Handler) GetDocumentPermissions(w http.ResponseWriter, r *http.Request) { + method := "space.GetDocumentPermissions" + ctx := domain.GetRequestContext(r) + + documentID := request.Param(r, "documentID") + if len(documentID) == 0 { + response.WriteMissingDataError(w, method, "documentID") + return + } + + perms, err := h.Store.Permission.GetDocumentPermissions(ctx, documentID) + if err != nil && err != sql.ErrNoRows { + response.WriteServerError(w, method, err) + return + } + if len(perms) == 0 { + perms = []permission.Permission{} + } + + userPerms := make(map[string][]permission.Permission) + for _, p := range perms { + userPerms[p.WhoID] = append(userPerms[p.WhoID], p) + } + + records := []permission.DocumentRecord{} + for _, up := range userPerms { + records = append(records, permission.DecodeUserDocumentPermissions(up)) + } + + response.WriteJSON(w, records) +} + +// GetUserDocumentPermissions returns permissions for the requested space, for current user. +func (h *Handler) GetUserDocumentPermissions(w http.ResponseWriter, r *http.Request) { + method := "space.GetUserDocumentPermissions" + ctx := domain.GetRequestContext(r) + + documentID := request.Param(r, "documentID") + if len(documentID) == 0 { + response.WriteMissingDataError(w, method, "documentID") + return + } + + perms, err := h.Store.Permission.GetUserDocumentPermissions(ctx, documentID) + if err != nil && err != sql.ErrNoRows { + response.WriteServerError(w, method, err) + return + } + if len(perms) == 0 { + perms = []permission.Permission{} + } + + record := permission.DecodeUserDocumentPermissions(perms) + response.WriteJSON(w, record) +} + +// SetDocumentPermissions persists specified document permissions +// These permissions override document permissions +func (h *Handler) SetDocumentPermissions(w http.ResponseWriter, r *http.Request) { + method := "space.SetDocumentPermissions" + ctx := domain.GetRequestContext(r) + + id := request.Param(r, "documentID") + if len(id) == 0 { + response.WriteMissingDataError(w, method, "documentID") + return + } + + doc, err := h.Store.Document.Get(ctx, id) + if err != nil { + response.WriteNotFoundError(w, method, "document not found") + return + } + + sp, err := h.Store.Space.Get(ctx, doc.LabelID) + if err != nil { + response.WriteNotFoundError(w, method, "space not found") + 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 { + response.WriteBadRequestError(w, method, err.Error()) + h.Runtime.Log.Error(method, err) + return + } + + var model = []permission.DocumentRecord{} + err = json.Unmarshal(body, &model) + if err != nil { + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + ctx.Transaction, err = h.Runtime.Db.Beginx() + if err != nil { + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + // We compare new permisions to what we had before. + // Why? So we can send out space invitation emails. + previousRoles, err := h.Store.Permission.GetDocumentPermissions(ctx, id) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + // Store all previous approval roles as map for easy querying + previousRoleUsers := make(map[string]bool) + for _, v := range previousRoles { + if v.Action == permission.DocumentApprove { + previousRoleUsers[v.WhoID] = true + } + } + + // Get user who is setting document permissions so we can send out emails with context + inviter, err := h.Store.User.Get(ctx, ctx.UserID) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + // Nuke all previous permissions for this document + _, err = h.Store.Permission.DeleteDocumentPermissions(ctx, id) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + url := ctx.GetAppURL(fmt.Sprintf("s/%s/%s/d/%s/%s", + sp.RefID, stringutil.MakeSlug(sp.Name), doc.RefID, stringutil.MakeSlug(doc.Title))) + + for _, perm := range model { + perm.OrgID = ctx.OrgID + perm.DocumentID = id + + // Only persist if there is a role! + if permission.HasAnyDocumentPermission(perm) { + r := permission.EncodeUserDocumentPermissions(perm) + + for _, p := range r { + err = h.Store.Permission.AddPermission(ctx, p) + if err != nil { + h.Runtime.Log.Error("set document permission", err) + } + } + + // Send email notification to users who have been given document approver role + if _, isExisting := previousRoleUsers[perm.UserID]; !isExisting { + + // 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 + } + + 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)) + } + } + } + } + + h.Store.Audit.Record(ctx, audit.EventTypeDocumentPermission) + + ctx.Transaction.Commit() + + response.WriteEmpty(w) +} diff --git a/domain/permission/mysql/store.go b/domain/permission/mysql/store.go index 1e2d3dfc..6569ed08 100644 --- a/domain/permission/mysql/store.go +++ b/domain/permission/mysql/store.go @@ -212,3 +212,55 @@ func (s Scope) GetUserCategoryPermissions(ctx domain.RequestContext, userID stri return } + +// GetUserDocumentPermissions returns document permissions for user. +// Context is used to for user ID. +func (s Scope) GetUserDocumentPermissions(ctx domain.RequestContext, documentID 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='document' 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 LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.location='document' AND refid=? + AND p.who='role' AND (r.userid=? OR r.userid='0')`, + ctx.OrgID, documentID, ctx.UserID, ctx.OrgID, documentID, ctx.OrgID) + + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + err = errors.Wrap(err, fmt.Sprintf("unable to execute select user document permissions %s", ctx.UserID)) + } + + return +} + +// GetDocumentPermissions returns documents permissions for all users. +func (s Scope) GetDocumentPermissions(ctx domain.RequestContext, documentID 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='document' AND refid=? AND who='user' + UNION ALL + SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid + FROM permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.location='document' AND p.refid=? + AND p.who='role'`, + ctx.OrgID, documentID, ctx.OrgID, documentID) + + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + err = errors.Wrap(err, fmt.Sprintf("unable to execute select document permissions %s", ctx.UserID)) + } + + return +} + +// DeleteDocumentPermissions removes records from permissions table for given document. +func (s Scope) DeleteDocumentPermissions(ctx domain.RequestContext, documentID string) (rows int64, err error) { + b := mysql.BaseQuery{} + + sql := fmt.Sprintf("DELETE FROM permission WHERE orgid='%s' AND location='document' AND refid='%s'", ctx.OrgID, documentID) + + return b.DeleteWhere(ctx.Transaction, sql) +} diff --git a/domain/storer.go b/domain/storer.go index a12cb6ac..7adffedb 100644 --- a/domain/storer.go +++ b/domain/storer.go @@ -94,6 +94,9 @@ type PermissionStorer interface { GetCategoryPermissions(ctx RequestContext, catID string) (r []permission.Permission, err error) GetCategoryUsers(ctx RequestContext, catID string) (u []user.User, err error) GetUserCategoryPermissions(ctx RequestContext, userID string) (r []permission.Permission, err error) + GetUserDocumentPermissions(ctx RequestContext, documentID string) (r []permission.Permission, err error) + GetDocumentPermissions(ctx RequestContext, documentID string) (r []permission.Permission, err error) + DeleteDocumentPermissions(ctx RequestContext, documentID string) (rows int64, err error) } // UserStorer defines required methods for user management diff --git a/gui/app/components/folder/permission-admin.js b/gui/app/components/folder/permission-admin.js index 13752fb2..f3165a66 100644 --- a/gui/app/components/folder/permission-admin.js +++ b/gui/app/components/folder/permission-admin.js @@ -12,8 +12,9 @@ import { setProperties } from '@ember/object'; import Component from '@ember/component'; import { inject as service } from '@ember/service'; +import ModalMixin from '../../mixins/modal'; -export default Component.extend({ +export default Component.extend(ModalMixin, { folderService: service('folder'), userService: service('user'), appMeta: service(), @@ -120,8 +121,7 @@ export default Component.extend({ } this.get('folderService').savePermissions(folder.get('id'), payload).then(() => { - $('#space-permission-modal').modal('hide'); - $('#space-permission-modal').modal('dispose'); + this.modalClose('#space-permission-modal'); }); } } diff --git a/gui/app/models/document-role.js b/gui/app/models/document-role.js new file mode 100644 index 00000000..74361688 --- /dev/null +++ b/gui/app/models/document-role.js @@ -0,0 +1,25 @@ +// 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 Model from 'ember-data/model'; +import attr from 'ember-data/attr'; + +export default Model.extend({ + orgId: attr('string'), + documentId: attr('string'), + userId: attr('string'), + fullname: attr('string'), // client-side usage only, not from API + + documentEdit: attr('boolean'), // space level setting + documentApprove: attr('boolean'), // space level setting + documentRoleEdit: attr('boolean'), // document level setting + documentRoleApprove: attr('boolean') // document level setting +}); diff --git a/gui/app/models/page.js b/gui/app/models/page.js index f3df8f41..46f98d93 100644 --- a/gui/app/models/page.js +++ b/gui/app/models/page.js @@ -28,8 +28,6 @@ export default Model.extend({ body: attr('string'), rawBody: attr('string'), meta: attr(), - protection: attr('number', { defaultValue: 0 }), - approval: attr('number', { defaultValue: 0 }), tagName: computed('level', function () { return "h2"; diff --git a/gui/app/pods/document/index/route.js b/gui/app/pods/document/index/route.js index 6d9b3b35..a095ea9d 100644 --- a/gui/app/pods/document/index/route.js +++ b/gui/app/pods/document/index/route.js @@ -32,7 +32,8 @@ export default Route.extend(AuthenticatedRouteMixin, { pages: this.get('documentService').getPages(this.modelFor('document').document.get('id')), links: this.modelFor('document').links, sections: this.modelFor('document').sections, - permissions: this.modelFor('document').permissions + permissions: this.modelFor('document').permissions, + roles: this.modelFor('document').roles }); }, @@ -44,5 +45,6 @@ export default Route.extend(AuthenticatedRouteMixin, { controller.set('links', model.links); controller.set('sections', model.sections); controller.set('permissions', model.permissions); + controller.set('roles', model.roles); } }); diff --git a/gui/app/pods/document/index/template.hbs b/gui/app/pods/document/index/template.hbs index c6e57f7d..7153a4e4 100644 --- a/gui/app/pods/document/index/template.hbs +++ b/gui/app/pods/document/index/template.hbs @@ -1,7 +1,9 @@ {{toolbar/nav-bar}} -{{toolbar/for-document document=document spaces=folders space=folder permissions=permissions - onDocumentDelete=(action 'onDocumentDelete') onSaveTemplate=(action 'onSaveTemplate')}} +{{toolbar/for-document document=document spaces=folders space=folder permissions=permissions roles=roles + onDocumentDelete=(action 'onDocumentDelete') + onSaveTemplate=(action 'onSaveTemplate') + onSaveDocument=(action 'onSaveDocument')}}
@@ -15,7 +17,8 @@
- {{document/document-toc document=document folder=folder pages=pages page=page permissions=permissions currentPageId=pageId tab=tab + {{document/document-toc document=document folder=folder pages=pages page=page + permissions=permissions roles=roles currentPageId=pageId tab=tab onPageSequenceChange=(action 'onPageSequenceChange') onPageLevelChange=(action 'onPageLevelChange') onGotoPage=(action 'onGotoPage')}}
diff --git a/gui/app/pods/document/route.js b/gui/app/pods/document/route.js index 7508ebf8..1e6b55d1 100644 --- a/gui/app/pods/document/route.js +++ b/gui/app/pods/document/route.js @@ -31,6 +31,7 @@ export default Route.extend(AuthenticatedRouteMixin, { this.set('folders', data.folders); this.set('folder', data.folder); this.set('permissions', data.permissions); + this.set('roles', data.roles); this.set('links', data.links); resolve(); }); @@ -44,6 +45,7 @@ export default Route.extend(AuthenticatedRouteMixin, { document: this.get('document'), page: this.get('pageId'), permissions: this.get('permissions'), + roles: this.get('roles'), links: this.get('links'), sections: this.get('sectionService').getAll() }); diff --git a/gui/app/serializers/document-role.js b/gui/app/serializers/document-role.js new file mode 100644 index 00000000..2c2733a5 --- /dev/null +++ b/gui/app/serializers/document-role.js @@ -0,0 +1,13 @@ +import ApplicationSerializer from './application'; + +export default ApplicationSerializer.extend({ + normalize(modelClass, resourceHash) { + return { + data: { + id: resourceHash.userId ? resourceHash.userId : 0, + type: modelClass.modelName, + attributes: resourceHash + } + }; + } +}); diff --git a/gui/app/services/document.js b/gui/app/services/document.js index 515d4237..5bc1d3d8 100644 --- a/gui/app/services/document.js +++ b/gui/app/services/document.js @@ -328,6 +328,7 @@ export default Service.extend({ let data = { document: {}, permissions: {}, + roles: {}, folders: [], folder: {}, links: [], @@ -337,9 +338,12 @@ export default Service.extend({ doc = this.get('store').push(doc); let perms = this.get('store').normalize('space-permission', response.permissions); - perms= this.get('store').push(perms); + perms = this.get('store').push(perms); this.get('folderService').set('permissions', perms); + let roles = this.get('store').normalize('document-role', response.roles); + roles = this.get('store').push(roles); + let folders = response.folders.map((obj) => { let data = this.get('store').normalize('folder', obj); return this.get('store').push(data); @@ -347,6 +351,7 @@ export default Service.extend({ data.document = doc; data.permissions = perms; + data.roles = roles; data.folders = folders; data.folder = folders.findBy('id', doc.get('folderId')); data.links = response.links; diff --git a/gui/app/styles/view/document/doc-structure.scss b/gui/app/styles/view/document/doc-structure.scss index 44bc59a0..9517824d 100644 --- a/gui/app/styles/view/document/doc-structure.scss +++ b/gui/app/styles/view/document/doc-structure.scss @@ -23,6 +23,36 @@ color: $color-dark; } } + + > .protection-table { + > tbody, > thead { + > tr, > th { + > td, > th { + margin: 0; + padding: 10px 15px; + text-align: center; + } + + > td:first-child { + text-align: left; + } + } + } + + > thead { + > tr { + > th { + background-color: $color-off-white; + color: $color-gray; + } + + > th:first-child { + background-color: $color-white !important; + border: none !important; + } + } + } + } } .section-divider { diff --git a/gui/app/templates/components/document/document-meta.hbs b/gui/app/templates/components/document/document-meta.hbs index cc021103..a8402ca7 100644 --- a/gui/app/templates/components/document/document-meta.hbs +++ b/gui/app/templates/components/document/document-meta.hbs @@ -2,8 +2,8 @@
-
Categories
-
+
Categories
+
{{#each selectedCategories as |cat|}} {{#link-to 'folder' folder.id folder.slug (query-params category=cat.id)}} {{cat.category}} @@ -27,8 +27,8 @@
-
Tags
-
+
Tags
+
{{#each tagz as |t index|}} {{#link-to 'search' (query-params filter=t matchTag=true)}} {{concat '#' t}} diff --git a/gui/app/utils/model.js b/gui/app/utils/model.js index 35ce61f4..9c69273a 100644 --- a/gui/app/utils/model.js +++ b/gui/app/utils/model.js @@ -164,8 +164,6 @@ let PageModel = BaseModel.extend({ title: "", body: "", rawBody: "", - protection: constants.ProtectionType.None, - approval: constants.ApprovalType.None, meta: {}, tagName: computed('level', function () { diff --git a/model/audit/audit.go b/model/audit/audit.go index b3671a17..d754e448 100644 --- a/model/audit/audit.go +++ b/model/audit/audit.go @@ -34,6 +34,7 @@ const ( EventTypeDocumentUpdate EventType = "updated-document" EventTypeDocumentDelete EventType = "removed-document" EventTypeDocumentRevisions EventType = "viewed-document-revisions" + EventTypeDocumentPermission EventType = "changed-document-permissions" EventTypeSpaceAdd EventType = "added-space" EventTypeSpaceUpdate EventType = "updated-space" EventTypeSpaceDelete EventType = "removed-space" diff --git a/model/page/page.go b/model/page/page.go index 8f6dfedd..e5168650 100644 --- a/model/page/page.go +++ b/model/page/page.go @@ -16,26 +16,23 @@ import ( "time" "github.com/documize/community/model" - "github.com/documize/community/model/workflow" ) // Page represents a section within a document. type Page struct { model.BaseEntity - OrgID string `json:"orgId"` - DocumentID string `json:"documentId"` - UserID string `json:"userId"` - ContentType string `json:"contentType"` - PageType string `json:"pageType"` - BlockID string `json:"blockId"` - Level uint64 `json:"level"` - Sequence float64 `json:"sequence"` - Numbering string `json:"numbering"` - Title string `json:"title"` - Body string `json:"body"` - Revisions uint64 `json:"revisions"` - Protection workflow.Protection `json:"protection"` - Approval workflow.Approval `json:"approval"` + OrgID string `json:"orgId"` + DocumentID string `json:"documentId"` + UserID string `json:"userId"` + ContentType string `json:"contentType"` + PageType string `json:"pageType"` + BlockID string `json:"blockId"` + Level uint64 `json:"level"` + Sequence float64 `json:"sequence"` + Numbering string `json:"numbering"` + Title string `json:"title"` + Body string `json:"body"` + Revisions uint64 `json:"revisions"` } // SetDefaults ensures no blank values. diff --git a/model/permission/common.go b/model/permission/common.go new file mode 100644 index 00000000..2b868d8c --- /dev/null +++ b/model/permission/common.go @@ -0,0 +1,76 @@ +// 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 permission + +import "time" + +// Permission represents a permission for a space and is persisted to the database. +type Permission struct { + ID uint64 `json:"id"` + OrgID string `json:"orgId"` + Who string `json:"who"` // user, role + WhoID string `json:"whoId"` // either a user or role ID + Action Action `json:"action"` // view, edit, delete + Scope string `json:"scope"` // object, table + Location string `json:"location"` // table name + RefID string `json:"refId"` // id of row in table / blank when scope=table + Created time.Time `json:"created"` +} + +// Action details type of action +type Action string + +const ( + // SpaceView action means you can view a space and documents therein + SpaceView Action = "view" + + // SpaceManage action means you can add, remove users, set permissions, but not delete that space + SpaceManage Action = "manage" + + // SpaceOwner action means you can delete a space and do all SpaceManage functions + SpaceOwner Action = "own" + + // DocumentAdd action means you can create/upload documents to a space + DocumentAdd Action = "doc-add" + + // DocumentEdit action means you can edit documents in a space + DocumentEdit Action = "doc-edit" + + // DocumentDelete means you can delete documents in a space + DocumentDelete Action = "doc-delete" + + // DocumentMove means you can move documents between spaces + DocumentMove Action = "doc-move" + + // DocumentCopy means you can copy documents within and between spaces + DocumentCopy Action = "doc-copy" + + // DocumentTemplate means you can create, edit and delete document templates and content blocks + DocumentTemplate Action = "doc-template" + + // DocumentApprove means you can approve a change to a document + DocumentApprove Action = "doc-approve" + + // CategoryView action means you can view a category and documents therein + CategoryView Action = "view" +) + +// ContainsPermission checks if action matches one of the required actions? +func ContainsPermission(action Action, actions ...Action) bool { + for _, a := range actions { + if action == a { + return true + } + } + + return false +} diff --git a/model/permission/document.go b/model/permission/document.go new file mode 100644 index 00000000..2083bca2 --- /dev/null +++ b/model/permission/document.go @@ -0,0 +1,78 @@ +// 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 permission + +// DocumentRecord represents space permissions for a user on a document. +// This data structure is made from database permission records for the document, +// and it is designed to be sent to HTTP clients (web, mobile). +type DocumentRecord struct { + OrgID string `json:"orgId"` + DocumentID string `json:"documentId"` + UserID string `json:"userId"` + DocumentRoleEdit bool `json:"documentRoleEdit"` + DocumentRoleApprove bool `json:"documentRoleApprove"` +} + +// DecodeUserDocumentPermissions returns a flat, usable permission summary record +// from multiple user permission records for a given document. +func DecodeUserDocumentPermissions(perm []Permission) (r DocumentRecord) { + r = DocumentRecord{} + + if len(perm) > 0 { + r.OrgID = perm[0].OrgID + r.UserID = perm[0].WhoID + r.DocumentID = perm[0].RefID + } + + for _, p := range perm { + switch p.Action { + case DocumentEdit: + r.DocumentRoleEdit = true + case DocumentApprove: + r.DocumentRoleApprove = true + } + } + + return +} + +// EncodeUserDocumentPermissions returns multiple user permission records +// for a given document, using flat permission summary record. +func EncodeUserDocumentPermissions(r DocumentRecord) (perm []Permission) { + if r.DocumentRoleEdit { + perm = append(perm, EncodeDocumentRecord(r, DocumentEdit)) + } + if r.DocumentRoleApprove { + perm = append(perm, EncodeDocumentRecord(r, DocumentApprove)) + } + + return +} + +// HasAnyDocumentPermission returns true if user has at least one permission. +func HasAnyDocumentPermission(p DocumentRecord) bool { + return p.DocumentRoleEdit || p.DocumentRoleApprove +} + +// EncodeDocumentRecord creates standard permission record representing user permissions for a document. +func EncodeDocumentRecord(r DocumentRecord, a Action) (p Permission) { + p = Permission{} + p.OrgID = r.OrgID + p.Who = "user" + p.WhoID = r.UserID + p.Location = "document" + p.RefID = r.DocumentID + p.Action = a + p.Scope = "object" // default to row level permission + + return +} diff --git a/model/permission/permission.go b/model/permission/space.go similarity index 64% rename from model/permission/permission.go rename to model/permission/space.go index 876b1799..6d02ef0f 100644 --- a/model/permission/permission.go +++ b/model/permission/space.go @@ -11,59 +11,6 @@ package permission -import "time" - -// Permission represents a permission for a space and is persisted to the database. -type Permission struct { - ID uint64 `json:"id"` - OrgID string `json:"orgId"` - Who string `json:"who"` // user, role - WhoID string `json:"whoId"` // either a user or role ID - Action Action `json:"action"` // view, edit, delete - Scope string `json:"scope"` // object, table - Location string `json:"location"` // table name - RefID string `json:"refId"` // id of row in table / blank when scope=table - Created time.Time `json:"created"` -} - -// Action details type of action -type Action string - -const ( - // SpaceView action means you can view a space and documents therein - SpaceView Action = "view" - - // SpaceManage action means you can add, remove users, set permissions, but not delete that space - SpaceManage Action = "manage" - - // SpaceOwner action means you can delete a space and do all SpaceManage functions - SpaceOwner Action = "own" - - // DocumentAdd action means you can create/upload documents to a space - DocumentAdd Action = "doc-add" - - // DocumentEdit action means you can edit documents in a space - DocumentEdit Action = "doc-edit" - - // DocumentDelete means you can delete documents in a space - DocumentDelete Action = "doc-delete" - - // DocumentMove means you can move documents between spaces - DocumentMove Action = "doc-move" - - // DocumentCopy means you can copy documents within and between spaces - DocumentCopy Action = "doc-copy" - - // DocumentTemplate means you can create, edit and delete document templates and content blocks - DocumentTemplate Action = "doc-template" - - // DocumentApprove means you can approve a change to a document - DocumentApprove Action = "doc-approve" - - // CategoryView action means you can view a category and documents therein - CategoryView Action = "view" -) - // Record represents space permissions for a user on a space. // This data structure is made from database permission records for the space, // and it is designed to be sent to HTTP clients (web, mobile). @@ -123,23 +70,6 @@ func DecodeUserPermissions(perm []Permission) (r Record) { return } -// PermissionsModel details which users have what permissions on a given space. -type PermissionsModel struct { - Message string - Permissions []Record -} - -// ContainsPermission checks if action matches one of the required actions? -func ContainsPermission(action Action, actions ...Action) bool { - for _, a := range actions { - if action == a { - return true - } - } - - return false -} - // EncodeUserPermissions returns multiple user permission records // for a given space, using flat permission summary record. func EncodeUserPermissions(r Record) (perm []Permission) { @@ -205,3 +135,9 @@ type CategoryViewRequestModel struct { CategoryID string `json:"categoryID"` UserID string `json:"userId"` } + +// SpaceRequestModel details which users have what permissions on a given space. +type SpaceRequestModel struct { + Message string + Permissions []Record +} diff --git a/server/routing/routes.go b/server/routing/routes.go index 10dbd68f..fe679d1f 100644 --- a/server/routing/routes.go +++ b/server/routing/routes.go @@ -89,6 +89,9 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) { Add(rt, RoutePrefixPrivate, "documents/{documentID}", []string{"GET", "OPTIONS"}, nil, document.Get) Add(rt, RoutePrefixPrivate, "documents/{documentID}", []string{"PUT", "OPTIONS"}, nil, document.Update) Add(rt, RoutePrefixPrivate, "documents/{documentID}", []string{"DELETE", "OPTIONS"}, nil, document.Delete) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/permissions", []string{"GET", "OPTIONS"}, nil, permission.GetDocumentPermissions) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/permissions", []string{"PUT", "OPTIONS"}, nil, permission.SetDocumentPermissions) + Add(rt, RoutePrefixPrivate, "documents/{documentID}/permissions/user", []string{"GET", "OPTIONS"}, nil, permission.GetUserDocumentPermissions) Add(rt, RoutePrefixPrivate, "documents/{documentID}/activity", []string{"GET", "OPTIONS"}, nil, document.Activity) Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/level", []string{"POST", "OPTIONS"}, nil, page.ChangePageLevel)