diff --git a/core/database/scripts/autobuild/db_00016.sql b/core/database/scripts/autobuild/db_00016.sql index f8a9b814..5a00f7a9 100644 --- a/core/database/scripts/autobuild/db_00016.sql +++ b/core/database/scripts/autobuild/db_00016.sql @@ -24,7 +24,7 @@ CREATE TABLE IF NOT EXISTS `permission` ( DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = MyISAM; --- category represents "folder/label" assignment to document (1:M) +-- category represents "folder/label/category" assignment to document (1:M) DROP TABLE IF EXISTS `category`; CREATE TABLE IF NOT EXISTS `category` ( @@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS `category` ( `refid` CHAR(16) NOT NULL COLLATE utf8_bin, `orgid` CHAR(16) NOT NULL COLLATE utf8_bin, `labelid` CHAR(16) NOT NULL COLLATE utf8_bin, - `label` VARCHAR(30) NOT NULL, + `category` VARCHAR(30) NOT NULL, `created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE INDEX `idx_category_id` (`id` ASC), INDEX `idx_category_refid` (`refid` ASC), @@ -45,6 +45,7 @@ DROP TABLE IF EXISTS `categorymember`; CREATE TABLE IF NOT EXISTS `categorymember` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `refid` CHAR(16) NOT NULL COLLATE utf8_bin, `orgid` CHAR(16) NOT NULL COLLATE utf8_bin, `categoryid` CHAR(16) NOT NULL COLLATE utf8_bin, `documentid` CHAR(16) NOT NULL COLLATE utf8_bin, diff --git a/domain/attachment/endpoint.go b/domain/attachment/endpoint.go index 476d0ebb..5237b1a5 100644 --- a/domain/attachment/endpoint.go +++ b/domain/attachment/endpoint.go @@ -25,8 +25,8 @@ import ( "github.com/documize/community/core/secrets" "github.com/documize/community/core/uniqueid" "github.com/documize/community/domain" - "github.com/documize/community/domain/document" "github.com/documize/community/domain/organization" + "github.com/documize/community/domain/permission" indexer "github.com/documize/community/domain/search" "github.com/documize/community/model/attachment" "github.com/documize/community/model/audit" @@ -89,7 +89,7 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) { return } - if !document.CanViewDocument(ctx, *h.Store, documentID) { + if !permission.CanViewDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -125,7 +125,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { return } - if !document.CanChangeDocument(ctx, *h.Store, documentID) { + if !permission.CanChangeDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -177,7 +177,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { return } - if !document.CanChangeDocument(ctx, *h.Store, documentID) { + if !permission.CanChangeDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } diff --git a/domain/block/endpoint.go b/domain/block/endpoint.go index 92b3f41c..82e9e076 100644 --- a/domain/block/endpoint.go +++ b/domain/block/endpoint.go @@ -22,7 +22,7 @@ import ( "github.com/documize/community/core/streamutil" "github.com/documize/community/core/uniqueid" "github.com/documize/community/domain" - "github.com/documize/community/domain/document" + "github.com/documize/community/domain/permission" "github.com/documize/community/model/audit" "github.com/documize/community/model/block" ) @@ -57,7 +57,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { return } - if !document.CanUploadDocument(ctx, *h.Store, b.LabelID) { + if !permission.CanUploadDocument(ctx, *h.Store, b.LabelID) { response.WriteForbiddenError(w) return } @@ -165,7 +165,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { b.RefID = blockID - if !document.CanUploadDocument(ctx, *h.Store, b.LabelID) { + if !permission.CanUploadDocument(ctx, *h.Store, b.LabelID) { response.WriteForbiddenError(w) return } diff --git a/domain/conversion/conversion.go b/domain/conversion/conversion.go index 77ba6c13..55e176aa 100644 --- a/domain/conversion/conversion.go +++ b/domain/conversion/conversion.go @@ -28,7 +28,7 @@ import ( "github.com/documize/community/core/uniqueid" "github.com/documize/community/domain" "github.com/documize/community/domain/conversion/store" - "github.com/documize/community/domain/document" + "github.com/documize/community/domain/permission" "github.com/documize/community/model/activity" "github.com/documize/community/model/attachment" "github.com/documize/community/model/audit" @@ -50,7 +50,7 @@ func (h *Handler) upload(w http.ResponseWriter, r *http.Request) (string, string folderID := request.Param(r, "folderID") - if !document.CanUploadDocument(ctx, *h.Store, folderID) { + if !permission.CanUploadDocument(ctx, *h.Store, folderID) { response.WriteForbiddenError(w) return "", "", "" } diff --git a/domain/document/endpoint.go b/domain/document/endpoint.go index c1f678e0..aabd5877 100644 --- a/domain/document/endpoint.go +++ b/domain/document/endpoint.go @@ -23,8 +23,8 @@ import ( "github.com/documize/community/core/streamutil" "github.com/documize/community/core/stringutil" "github.com/documize/community/domain" + "github.com/documize/community/domain/permission" indexer "github.com/documize/community/domain/search" - "github.com/documize/community/domain/space" "github.com/documize/community/model/activity" "github.com/documize/community/model/audit" "github.com/documize/community/model/doc" @@ -61,7 +61,7 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) { return } - if !CanViewDocumentInFolder(ctx, *h.Store, document.LabelID) { + if !permission.CanViewSpaceDocument(ctx, *h.Store, document.LabelID) { response.WriteForbiddenError(w) return } @@ -147,7 +147,7 @@ func (h *Handler) BySpace(w http.ResponseWriter, r *http.Request) { return } - if !space.CanViewSpace(ctx, *h.Store, spaceID) { + if !permission.CanViewSpace(ctx, *h.Store, spaceID) { response.WriteForbiddenError(w) return } @@ -210,7 +210,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { // return // } - if !CanChangeDocument(ctx, *h.Store, documentID) { + if !permission.CanChangeDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -269,7 +269,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { return } - if !CanChangeDocument(ctx, *h.Store, documentID) { + if !permission.CanDeleteDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } diff --git a/domain/document/permission.go b/domain/document/permission.go deleted file mode 100644 index d39c5f9e..00000000 --- a/domain/document/permission.go +++ /dev/null @@ -1,117 +0,0 @@ -// 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 document - -import ( - "database/sql" - - "github.com/documize/community/domain" - sp "github.com/documize/community/model/space" -) - -// CanViewDocumentInFolder returns if the user has permission to view a document within the specified folder. -func CanViewDocumentInFolder(ctx domain.RequestContext, s domain.Store, labelID string) bool { - roles, err := s.Space.GetUserPermissions(ctx, labelID) - if err == sql.ErrNoRows { - err = nil - } - if err != nil { - return false - } - - for _, role := range roles { - if role.RefID == labelID && role.Location == "space" && role.Scope == "object" && - sp.HasPermission(role.Action, sp.SpaceView, sp.SpaceManage, sp.SpaceOwner) { - return true - } - } - - return false -} - -// CanViewDocument returns if the client has permission to view a given document. -func CanViewDocument(ctx domain.RequestContext, s domain.Store, documentID string) bool { - document, err := s.Document.Get(ctx, documentID) - if err == sql.ErrNoRows { - err = nil - } - if err != nil { - return false - } - - roles, err := s.Space.GetUserPermissions(ctx, document.LabelID) - if err == sql.ErrNoRows { - err = nil - } - if err != nil { - return false - } - - for _, role := range roles { - if role.RefID == document.LabelID && role.Location == "space" && role.Scope == "object" && - sp.HasPermission(role.Action, sp.SpaceView, sp.SpaceManage, sp.SpaceOwner) { - return true - } - } - - return false -} - -// CanChangeDocument returns if the clinet has permission to change a given document. -func CanChangeDocument(ctx domain.RequestContext, s domain.Store, documentID string) bool { - document, err := s.Document.Get(ctx, documentID) - - if err == sql.ErrNoRows { - err = nil - } - if err != nil { - return false - } - - roles, err := s.Space.GetUserPermissions(ctx, document.LabelID) - - if err == sql.ErrNoRows { - err = nil - } - if err != nil { - return false - } - - for _, role := range roles { - if role.RefID == document.LabelID && role.Location == "space" && role.Scope == "object" && - sp.HasPermission(role.Action, sp.DocumentEdit) { - return true - } - } - - return false -} - -// CanUploadDocument returns if the client has permission to upload documents to the given space. -func CanUploadDocument(ctx domain.RequestContext, s domain.Store, spaceID string) bool { - roles, err := s.Space.GetUserPermissions(ctx, spaceID) - if err == sql.ErrNoRows { - err = nil - } - if err != nil { - return false - } - - for _, role := range roles { - if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" && - sp.HasPermission(role.Action, sp.DocumentAdd) { - return true - } - } - - return false -} diff --git a/domain/link/endpoint.go b/domain/link/endpoint.go index ffc82243..0006491d 100644 --- a/domain/link/endpoint.go +++ b/domain/link/endpoint.go @@ -21,7 +21,7 @@ import ( "github.com/documize/community/core/response" "github.com/documize/community/core/uniqueid" "github.com/documize/community/domain" - "github.com/documize/community/domain/document" + "github.com/documize/community/domain/permission" "github.com/documize/community/model/attachment" "github.com/documize/community/model/link" "github.com/documize/community/model/page" @@ -57,7 +57,7 @@ func (h *Handler) GetLinkCandidates(w http.ResponseWriter, r *http.Request) { } // permission check - if !document.CanViewDocument(ctx, *h.Store, documentID) { + if !permission.CanViewDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } diff --git a/domain/page/endpoint.go b/domain/page/endpoint.go index a02094db..589f90c1 100644 --- a/domain/page/endpoint.go +++ b/domain/page/endpoint.go @@ -25,8 +25,8 @@ import ( "github.com/documize/community/core/streamutil" "github.com/documize/community/core/uniqueid" "github.com/documize/community/domain" - "github.com/documize/community/domain/document" "github.com/documize/community/domain/link" + "github.com/documize/community/domain/permission" indexer "github.com/documize/community/domain/search" "github.com/documize/community/domain/section/provider" "github.com/documize/community/model/activity" @@ -59,7 +59,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { return } - if !document.CanChangeDocument(ctx, *h.Store, documentID) { + if !permission.CanChangeDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -165,7 +165,7 @@ func (h *Handler) GetPage(w http.ResponseWriter, r *http.Request) { return } - if !document.CanViewDocument(ctx, *h.Store, documentID) { + if !permission.CanViewDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -200,7 +200,7 @@ func (h *Handler) GetPages(w http.ResponseWriter, r *http.Request) { return } - if !document.CanViewDocument(ctx, *h.Store, documentID) { + if !permission.CanViewDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -238,7 +238,7 @@ func (h *Handler) GetPagesBatch(w http.ResponseWriter, r *http.Request) { return } - if !document.CanViewDocument(ctx, *h.Store, documentID) { + if !permission.CanViewDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -290,7 +290,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { return } - if !document.CanChangeDocument(ctx, *h.Store, documentID) { + if !permission.CanChangeDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -366,7 +366,7 @@ func (h *Handler) DeletePages(w http.ResponseWriter, r *http.Request) { return } - if !document.CanChangeDocument(ctx, *h.Store, documentID) { + if !permission.CanChangeDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -471,7 +471,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { return } - if !document.CanChangeDocument(ctx, *h.Store, documentID) { + if !permission.CanChangeDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -617,7 +617,7 @@ func (h *Handler) ChangePageSequence(w http.ResponseWriter, r *http.Request) { return } - if !document.CanChangeDocument(ctx, *h.Store, documentID) { + if !permission.CanChangeDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -678,7 +678,7 @@ func (h *Handler) ChangePageLevel(w http.ResponseWriter, r *http.Request) { return } - if !document.CanChangeDocument(ctx, *h.Store, documentID) { + if !permission.CanChangeDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -740,7 +740,7 @@ func (h *Handler) GetMeta(w http.ResponseWriter, r *http.Request) { return } - if !document.CanViewDocument(ctx, *h.Store, documentID) { + if !permission.CanViewDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -816,7 +816,7 @@ func (h *Handler) Copy(w http.ResponseWriter, r *http.Request) { } // permission - if !document.CanViewDocument(ctx, *h.Store, documentID) { + if !permission.CanViewDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -922,7 +922,7 @@ func (h *Handler) GetDocumentRevisions(w http.ResponseWriter, r *http.Request) { return } - if !document.CanViewDocument(ctx, *h.Store, documentID) { + if !permission.CanViewDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -953,7 +953,7 @@ func (h *Handler) GetRevisions(w http.ResponseWriter, r *http.Request) { return } - if !document.CanViewDocument(ctx, *h.Store, documentID) { + if !permission.CanViewDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -1000,7 +1000,7 @@ func (h *Handler) GetDiff(w http.ResponseWriter, r *http.Request) { return } - if !document.CanViewDocument(ctx, *h.Store, documentID) { + if !permission.CanViewDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -1064,7 +1064,7 @@ func (h *Handler) Rollback(w http.ResponseWriter, r *http.Request) { return } - if !document.CanChangeDocument(ctx, *h.Store, documentID) { + if !permission.CanChangeDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } diff --git a/domain/permission/endpoint.go b/domain/permission/endpoint.go new file mode 100644 index 00000000..f711b733 --- /dev/null +++ b/domain/permission/endpoint.go @@ -0,0 +1,278 @@ +// 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 handles API calls and persistence for spaces. +// Spaces in Documize contain documents. +package permission + +import ( + "database/sql" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/documize/community/core/env" + "github.com/documize/community/core/request" + "github.com/documize/community/core/response" + "github.com/documize/community/core/streamutil" + "github.com/documize/community/core/stringutil" + "github.com/documize/community/domain" + "github.com/documize/community/domain/mail" + "github.com/documize/community/model/audit" + "github.com/documize/community/model/permission" + "github.com/documize/community/model/space" +) + +// Handler contains the runtime information such as logging and database. +type Handler struct { + Runtime *env.Runtime + Store *domain.Store +} + +// SetSpacePermissions persists specified space permissions +func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) { + method := "space.SetPermissions" + ctx := domain.GetRequestContext(r) + + if !ctx.Editor { + response.WriteForbiddenError(w) + return + } + + id := request.Param(r, "spaceID") + if len(id) == 0 { + response.WriteMissingDataError(w, method, "spaceID") + return + } + + sp, err := h.Store.Space.Get(ctx, id) + if err != nil { + response.WriteNotFoundError(w, method, "space not found") + return + } + + if sp.UserID != ctx.UserID { + 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.PermissionsModel{} + 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.GetSpacePermissions(ctx, id) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + // Store all previous roles as map for easy querying + previousRoleUsers := make(map[string]bool) + for _, v := range previousRoles { + previousRoleUsers[v.WhoID] = true + } + + // Who is sharing this space? + 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 space + _, err = h.Store.Permission.DeleteSpacePermissions(ctx, id) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + me := false + hasEveryoneRole := false + roleCount := 0 + + url := ctx.GetAppURL(fmt.Sprintf("s/%s/%s", sp.RefID, stringutil.MakeSlug(sp.Name))) + + for _, perm := range model.Permissions { + perm.OrgID = ctx.OrgID + perm.SpaceID = id + + // Ensure the space owner always has access! + if perm.UserID == ctx.UserID { + me = true + } + + // Only persist if there is a role! + if permission.HasAnyPermission(perm) { + // identify publically shared spaces + if len(perm.UserID) == 0 { + hasEveryoneRole = true + } + + r := permission.EncodeUserPermissions(perm) + + for _, p := range r { + err = h.Store.Permission.AddPermission(ctx, p) + if err != nil { + h.Runtime.Log.Error("set permission", err) + } + + roleCount++ + } + + // We send out space invitation emails to those users + // that have *just* been given permissions. + if _, isExisting := previousRoleUsers[perm.UserID]; !isExisting { + + // we skip 'everyone' (user id != empty string) + if len(perm.UserID) > 0 { + 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.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)) + } + } + } + } + + // Do we need to ensure permissions for space owner when shared? + if !me { + perm := permission.Permission{} + perm.OrgID = ctx.OrgID + perm.Who = "user" + perm.WhoID = ctx.UserID + perm.Scope = "object" + perm.Location = "space" + perm.RefID = id + perm.Action = "" // we send array for actions below + + err = h.Store.Permission.AddPermissions(ctx, perm, permission.SpaceView, permission.SpaceManage) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + return + } + } + + // Mark up space type as either public, private or restricted access. + if hasEveryoneRole { + sp.Type = space.ScopePublic + } else { + if roleCount > 1 { + sp.Type = space.ScopeRestricted + } else { + sp.Type = space.ScopePrivate + } + } + + err = h.Store.Space.Update(ctx, sp) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + h.Store.Audit.Record(ctx, audit.EventTypeSpacePermission) + + ctx.Transaction.Commit() + + response.WriteEmpty(w) +} + +// GetSpacePermissions returns permissions for alll users for given space. +func (h *Handler) GetSpacePermissions(w http.ResponseWriter, r *http.Request) { + method := "space.GetPermissions" + ctx := domain.GetRequestContext(r) + + spaceID := request.Param(r, "spaceID") + if len(spaceID) == 0 { + response.WriteMissingDataError(w, method, "spaceID") + return + } + + perms, err := h.Store.Permission.GetSpacePermissions(ctx, spaceID) + 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.Record{} + for _, up := range userPerms { + records = append(records, permission.DecodeUserPermissions(up)) + } + + response.WriteJSON(w, records) +} + +// GetUserSpacePermissions returns permissions for the requested space, for current user. +func (h *Handler) GetUserSpacePermissions(w http.ResponseWriter, r *http.Request) { + method := "space.GetUserSpacePermissions" + ctx := domain.GetRequestContext(r) + + spaceID := request.Param(r, "spaceID") + if len(spaceID) == 0 { + response.WriteMissingDataError(w, method, "spaceID") + return + } + + perms, err := h.Store.Permission.GetUserSpacePermissions(ctx, spaceID) + if err != nil && err != sql.ErrNoRows { + response.WriteServerError(w, method, err) + return + } + if len(perms) == 0 { + perms = []permission.Permission{} + } + + record := permission.DecodeUserPermissions(perms) + response.WriteJSON(w, record) +} diff --git a/domain/permission/mysql/store.go b/domain/permission/mysql/store.go new file mode 100644 index 00000000..22431665 --- /dev/null +++ b/domain/permission/mysql/store.go @@ -0,0 +1,136 @@ +// 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 mysql handles data persistence for space and document permissions. +package mysql + +import ( + "database/sql" + "fmt" + "time" + + "github.com/documize/community/core/env" + "github.com/documize/community/core/streamutil" + "github.com/documize/community/domain" + "github.com/documize/community/domain/store/mysql" + "github.com/documize/community/model/permission" + "github.com/pkg/errors" +) + +// Scope provides data access to MySQL. +type Scope struct { + Runtime *env.Runtime +} + +// AddPermission inserts the given record into the permisssion table. +func (s Scope) AddPermission(ctx domain.RequestContext, r permission.Permission) (err error) { + r.Created = time.Now().UTC() + + stmt, err := ctx.Transaction.Preparex("INSERT INTO permission (orgid, who, whoid, action, scope, location, refid, created) VALUES (?, ?, ?, ?, ?, ?, ?, ?)") + defer streamutil.Close(stmt) + + if err != nil { + err = errors.Wrap(err, "unable to prepare insert permission") + return + } + + _, err = stmt.Exec(r.OrgID, r.Who, r.WhoID, string(r.Action), r.Scope, r.Location, r.RefID, r.Created) + if err != nil { + err = errors.Wrap(err, "unable to execute insert permission") + return + } + + return +} + +// AddPermissions inserts records into permission database table, one per action. +func (s Scope) AddPermissions(ctx domain.RequestContext, r permission.Permission, actions ...permission.Action) (err error) { + for _, a := range actions { + r.Action = a + s.AddPermission(ctx, r) + } + + return +} + +// GetUserSpacePermissions returns space permissions for user. +// Context is used to for user ID. +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='') + 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='space' AND refid=? + AND p.who='role' AND (r.userid=? OR r.userid='')`, + ctx.OrgID, spaceID, ctx.UserID, ctx.OrgID, spaceID, ctx.OrgID) + + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + err = errors.Wrap(err, fmt.Sprintf("unable to execute select user permissions %s", ctx.UserID)) + return + } + + return +} + +// GetSpacePermissions returns space permissions for all users. +func (s Scope) GetSpacePermissions(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' + 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='space' AND p.refid=? + AND p.who='role'`, + ctx.OrgID, spaceID, ctx.OrgID, spaceID) + + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + err = errors.Wrap(err, fmt.Sprintf("unable to execute select space permissions %s", ctx.UserID)) + return + } + + return +} + +// DeleteSpacePermissions removes records from permissions table for given space ID. +func (s Scope) DeleteSpacePermissions(ctx domain.RequestContext, spaceID string) (rows int64, err error) { + b := mysql.BaseQuery{} + + sql := fmt.Sprintf("DELETE FROM permission WHERE orgid='%s' AND location='space' AND refid='%s'", ctx.OrgID, spaceID) + + return b.DeleteWhere(ctx.Transaction, sql) +} + +// DeleteUserSpacePermissions removes all roles for the specified user, for the specified space. +func (s Scope) DeleteUserSpacePermissions(ctx domain.RequestContext, spaceID, userID string) (rows int64, err error) { + b := mysql.BaseQuery{} + + sql := fmt.Sprintf("DELETE FROM permission WHERE orgid='%s' AND location='space' AND refid='%s' who='user' AND whoid='%s'", + ctx.OrgID, spaceID, userID) + + return b.DeleteWhere(ctx.Transaction, sql) +} + +// DeleteUserPermissions removes all roles for the specified user, for the specified space. +func (s Scope) DeleteUserPermissions(ctx domain.RequestContext, userID string) (rows int64, err error) { + b := mysql.BaseQuery{} + + sql := fmt.Sprintf("DELETE FROM permission WHERE orgid='%s' AND who='user' AND whoid='%s'", + ctx.OrgID, userID) + + return b.DeleteWhere(ctx.Transaction, sql) +} diff --git a/domain/permission/permission.go b/domain/permission/permission.go new file mode 100644 index 00000000..ef880711 --- /dev/null +++ b/domain/permission/permission.go @@ -0,0 +1,194 @@ +// 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 ( + "database/sql" + + "github.com/documize/community/domain" + pm "github.com/documize/community/model/permission" +) + +// CanViewSpaceDocument returns if the user has permission to view a document within the specified folder. +func CanViewSpaceDocument(ctx domain.RequestContext, s domain.Store, labelID string) bool { + roles, err := s.Permission.GetUserSpacePermissions(ctx, labelID) + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + return false + } + + for _, role := range roles { + if role.RefID == labelID && role.Location == "space" && role.Scope == "object" && + pm.HasPermission(role.Action, pm.SpaceView, pm.SpaceManage, pm.SpaceOwner) { + return true + } + } + + return false +} + +// CanViewDocument returns if the client has permission to view a given document. +func CanViewDocument(ctx domain.RequestContext, s domain.Store, documentID string) bool { + document, err := s.Document.Get(ctx, documentID) + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + return false + } + + roles, err := s.Permission.GetUserSpacePermissions(ctx, document.LabelID) + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + return false + } + + for _, role := range roles { + if role.RefID == document.LabelID && role.Location == "space" && role.Scope == "object" && + pm.HasPermission(role.Action, pm.SpaceView, pm.SpaceManage, pm.SpaceOwner) { + return true + } + } + + return false +} + +// CanChangeDocument returns if the clinet has permission to change a given document. +func CanChangeDocument(ctx domain.RequestContext, s domain.Store, documentID string) bool { + document, err := s.Document.Get(ctx, documentID) + + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + return false + } + + roles, err := s.Permission.GetUserSpacePermissions(ctx, document.LabelID) + + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + return false + } + + for _, role := range roles { + if role.RefID == document.LabelID && role.Location == "space" && role.Scope == "object" && role.Action == pm.DocumentEdit { + return true + } + } + + return false +} + +// CanDeleteDocument returns if the clinet has permission to change a given document. +func CanDeleteDocument(ctx domain.RequestContext, s domain.Store, documentID string) bool { + document, err := s.Document.Get(ctx, documentID) + + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + return false + } + + roles, err := s.Permission.GetUserSpacePermissions(ctx, document.LabelID) + + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + return false + } + + for _, role := range roles { + if role.RefID == document.LabelID && role.Location == "space" && role.Scope == "object" && role.Action == pm.DocumentDelete { + return true + } + } + + return false +} + +// CanUploadDocument returns if the client has permission to upload documents to the given space. +func CanUploadDocument(ctx domain.RequestContext, s domain.Store, spaceID string) bool { + roles, err := s.Permission.GetUserSpacePermissions(ctx, spaceID) + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + return false + } + + for _, role := range roles { + if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" && + pm.HasPermission(role.Action, pm.DocumentAdd) { + return true + } + } + + return false +} + +// CanViewSpace returns if the user has permission to view the given spaceID. +func CanViewSpace(ctx domain.RequestContext, s domain.Store, spaceID string) bool { + roles, err := s.Permission.GetUserSpacePermissions(ctx, spaceID) + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + return false + } + + for _, role := range roles { + if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" && + pm.HasPermission(role.Action, pm.SpaceView, pm.SpaceManage, pm.SpaceOwner) { + return true + } + } + + return false +} + +// HasDocumentAction returns if user can perform specified action. +func HasDocumentAction(ctx domain.RequestContext, s domain.Store, documentID string, a pm.Action) bool { + document, err := s.Document.Get(ctx, documentID) + + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + return false + } + + roles, err := s.Permission.GetUserSpacePermissions(ctx, document.LabelID) + + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + return false + } + + for _, role := range roles { + if role.RefID == document.LabelID && role.Location == "space" && role.Scope == "object" && role.Action == a { + return true + } + } + + return false +} diff --git a/domain/section/endpoint.go b/domain/section/endpoint.go index be4f8989..0cc30603 100644 --- a/domain/section/endpoint.go +++ b/domain/section/endpoint.go @@ -20,7 +20,7 @@ import ( "github.com/documize/community/core/response" "github.com/documize/community/core/uniqueid" "github.com/documize/community/domain" - "github.com/documize/community/domain/document" + "github.com/documize/community/domain/permission" "github.com/documize/community/domain/section/provider" "github.com/documize/community/model/page" ) @@ -59,7 +59,7 @@ func (h *Handler) RunSectionCommand(w http.ResponseWriter, r *http.Request) { // it's up to the section handler to parse if required. // Permission checks - if !document.CanChangeDocument(ctx, *h.Store, documentID) { + if !permission.CanChangeDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } @@ -86,7 +86,7 @@ func (h *Handler) RefreshSections(w http.ResponseWriter, r *http.Request) { return } - if !document.CanViewDocument(ctx, *h.Store, documentID) { + if !permission.CanViewDocument(ctx, *h.Store, documentID) { response.WriteForbiddenError(w) return } diff --git a/domain/space/endpoint.go b/domain/space/endpoint.go index a77c3b25..e47a2856 100644 --- a/domain/space/endpoint.go +++ b/domain/space/endpoint.go @@ -35,6 +35,7 @@ import ( "github.com/documize/community/model/audit" "github.com/documize/community/model/doc" "github.com/documize/community/model/page" + "github.com/documize/community/model/permission" "github.com/documize/community/model/space" uuid "github.com/nu7hatch/gouuid" ) @@ -105,7 +106,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { return } - perm := space.Permission{} + perm := permission.Permission{} perm.OrgID = sp.OrgID perm.Who = "user" perm.WhoID = ctx.UserID @@ -114,7 +115,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { perm.RefID = sp.RefID perm.Action = "" // we send array for actions below - err = h.Store.Space.AddPermissions(ctx, perm, space.SpaceOwner, space.SpaceManage, space.SpaceView) + err = h.Store.Permission.AddPermissions(ctx, perm, permission.SpaceOwner, permission.SpaceManage, permission.SpaceView) if err != nil { ctx.Transaction.Rollback() response.WriteServerError(w, method, err) @@ -138,7 +139,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { return } - spCloneRoles, err := h.Store.Space.GetPermissions(ctx, model.CloneID) + spCloneRoles, err := h.Store.Permission.GetSpacePermissions(ctx, model.CloneID) if err != nil { response.WriteServerError(w, method, err) h.Runtime.Log.Error(method, err) @@ -149,7 +150,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { for _, r := range spCloneRoles { r.RefID = sp.RefID - err = h.Store.Space.AddPermission(ctx, r) + err = h.Store.Permission.AddPermission(ctx, r) if err != nil { ctx.Transaction.Rollback() response.WriteServerError(w, method, err) @@ -451,7 +452,7 @@ func (h *Handler) Remove(w http.ResponseWriter, r *http.Request) { return } - _, err = h.Store.Space.DeletePermissions(ctx, id) + _, err = h.Store.Permission.DeleteSpacePermissions(ctx, id) if err != nil { ctx.Transaction.Rollback() response.WriteServerError(w, method, err) @@ -519,7 +520,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { return } - _, err = h.Store.Space.DeletePermissions(ctx, id) + _, err = h.Store.Permission.DeleteSpacePermissions(ctx, id) if err != nil { ctx.Transaction.Rollback() response.WriteServerError(w, method, err) @@ -542,245 +543,6 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { response.WriteEmpty(w) } -// SetPermissions persists specified spac3 permissions -func (h *Handler) SetPermissions(w http.ResponseWriter, r *http.Request) { - method := "space.SetPermissions" - ctx := domain.GetRequestContext(r) - - if !ctx.Editor { - response.WriteForbiddenError(w) - return - } - - id := request.Param(r, "spaceID") - if len(id) == 0 { - response.WriteMissingDataError(w, method, "spaceID") - return - } - - sp, err := h.Store.Space.Get(ctx, id) - if err != nil { - response.WriteNotFoundError(w, method, "space not found") - return - } - - if sp.UserID != ctx.UserID { - 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 = space.PermissionsModel{} - 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.Space.GetPermissions(ctx, id) - if err != nil { - ctx.Transaction.Rollback() - response.WriteServerError(w, method, err) - h.Runtime.Log.Error(method, err) - return - } - - // Store all previous roles as map for easy querying - previousRoleUsers := make(map[string]bool) - for _, v := range previousRoles { - previousRoleUsers[v.WhoID] = true - } - - // Who is sharing this space? - 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 space - _, err = h.Store.Space.DeletePermissions(ctx, id) - if err != nil { - ctx.Transaction.Rollback() - response.WriteServerError(w, method, err) - h.Runtime.Log.Error(method, err) - return - } - - me := false - hasEveryoneRole := false - roleCount := 0 - - url := ctx.GetAppURL(fmt.Sprintf("s/%s/%s", sp.RefID, stringutil.MakeSlug(sp.Name))) - - for _, perm := range model.Permissions { - perm.OrgID = ctx.OrgID - perm.SpaceID = id - - // Ensure the space owner always has access! - if perm.UserID == ctx.UserID { - me = true - } - - // Only persist if there is a role! - if space.HasAnyPermission(perm) { - // identify publically shared spaces - if len(perm.UserID) == 0 { - hasEveryoneRole = true - } - - r := space.EncodeUserPermissions(perm) - - for _, p := range r { - err = h.Store.Space.AddPermission(ctx, p) - if err != nil { - h.Runtime.Log.Error("set permission", err) - } - - roleCount++ - } - - // We send out space invitation emails to those users - // that have *just* been given permissions. - if _, isExisting := previousRoleUsers[perm.UserID]; !isExisting { - - // we skip 'everyone' (user id != empty string) - if len(perm.UserID) > 0 { - 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.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)) - } - } - } - } - - // Do we need to ensure permissions for space owner when shared? - if !me { - perm := space.Permission{} - perm.OrgID = ctx.OrgID - perm.Who = "user" - perm.WhoID = ctx.UserID - perm.Scope = "object" - perm.Location = "space" - perm.RefID = id - perm.Action = "" // we send array for actions below - - err = h.Store.Space.AddPermissions(ctx, perm, space.SpaceView, space.SpaceManage) - if err != nil { - ctx.Transaction.Rollback() - response.WriteServerError(w, method, err) - return - } - } - - // Mark up space type as either public, private or restricted access. - if hasEveryoneRole { - sp.Type = space.ScopePublic - } else { - if roleCount > 1 { - sp.Type = space.ScopeRestricted - } else { - sp.Type = space.ScopePrivate - } - } - - err = h.Store.Space.Update(ctx, sp) - if err != nil { - ctx.Transaction.Rollback() - response.WriteServerError(w, method, err) - h.Runtime.Log.Error(method, err) - return - } - - h.Store.Audit.Record(ctx, audit.EventTypeSpacePermission) - - ctx.Transaction.Commit() - - response.WriteEmpty(w) -} - -// GetPermissions returns permissions for alll users for given space. -func (h *Handler) GetPermissions(w http.ResponseWriter, r *http.Request) { - method := "space.GetPermissions" - ctx := domain.GetRequestContext(r) - - spaceID := request.Param(r, "spaceID") - if len(spaceID) == 0 { - response.WriteMissingDataError(w, method, "spaceID") - return - } - - perms, err := h.Store.Space.GetPermissions(ctx, spaceID) - if err != nil && err != sql.ErrNoRows { - response.WriteServerError(w, method, err) - return - } - if len(perms) == 0 { - perms = []space.Permission{} - } - - userPerms := make(map[string][]space.Permission) - for _, p := range perms { - userPerms[p.WhoID] = append(userPerms[p.WhoID], p) - } - - records := []space.PermissionRecord{} - for _, up := range userPerms { - records = append(records, space.DecodeUserPermissions(up)) - } - - response.WriteJSON(w, records) -} - -// GetUserPermissions returns permissions for the requested space, for current user. -func (h *Handler) GetUserPermissions(w http.ResponseWriter, r *http.Request) { - method := "space.GetUserPermissions" - ctx := domain.GetRequestContext(r) - - spaceID := request.Param(r, "spaceID") - if len(spaceID) == 0 { - response.WriteMissingDataError(w, method, "spaceID") - return - } - - perms, err := h.Store.Space.GetUserPermissions(ctx, spaceID) - if err != nil && err != sql.ErrNoRows { - response.WriteServerError(w, method, err) - return - } - if len(perms) == 0 { - perms = []space.Permission{} - } - - record := space.DecodeUserPermissions(perms) - response.WriteJSON(w, record) -} - // AcceptInvitation records the fact that a user has completed space onboard process. func (h *Handler) AcceptInvitation(w http.ResponseWriter, r *http.Request) { method := "space.AcceptInvitation" @@ -971,9 +733,9 @@ func (h *Handler) Invite(w http.ResponseWriter, r *http.Request) { } // Ensure they have space roles - h.Store.Space.DeleteUserPermissions(ctx, sp.RefID, u.RefID) + h.Store.Permission.DeleteUserSpacePermissions(ctx, sp.RefID, u.RefID) - perm := space.Permission{} + perm := permission.Permission{} perm.OrgID = sp.OrgID perm.Who = "user" perm.WhoID = u.RefID @@ -982,7 +744,7 @@ func (h *Handler) Invite(w http.ResponseWriter, r *http.Request) { perm.RefID = sp.RefID perm.Action = "" // we send array for actions below - err = h.Store.Space.AddPermissions(ctx, perm, space.SpaceView) + err = h.Store.Permission.AddPermissions(ctx, perm, permission.SpaceView) if err != nil { ctx.Transaction.Rollback() response.WriteServerError(w, method, err) diff --git a/domain/space/mysql/store.go b/domain/space/mysql/store.go index 34f571fb..79499864 100644 --- a/domain/space/mysql/store.go +++ b/domain/space/mysql/store.go @@ -13,7 +13,6 @@ package mysql import ( - "database/sql" "fmt" "time" @@ -163,108 +162,3 @@ func (s Scope) Delete(ctx domain.RequestContext, id string) (rows int64, err err b := mysql.BaseQuery{} return b.DeleteConstrained(ctx.Transaction, "label", ctx.OrgID, id) } - -// AddPermission inserts the given record into the permisssion table. -func (s Scope) AddPermission(ctx domain.RequestContext, r space.Permission) (err error) { - r.Created = time.Now().UTC() - - stmt, err := ctx.Transaction.Preparex("INSERT INTO permission (orgid, who, whoid, action, scope, location, refid, created) VALUES (?, ?, ?, ?, ?, ?, ?, ?)") - defer streamutil.Close(stmt) - - if err != nil { - err = errors.Wrap(err, "unable to prepare insert for space permission") - return - } - - _, err = stmt.Exec(r.OrgID, r.Who, r.WhoID, string(r.Action), r.Scope, r.Location, r.RefID, r.Created) - if err != nil { - err = errors.Wrap(err, "unable to execute insert for space permission") - return - } - - return -} - -// AddPermissions inserts records into permission database table, one per action. -func (s Scope) AddPermissions(ctx domain.RequestContext, r space.Permission, actions ...space.PermissionAction) (err error) { - for _, a := range actions { - r.Action = a - s.AddPermission(ctx, r) - } - - return -} - -// GetUserPermissions returns space permissions for user. -// Context is used to for user ID. -func (s Scope) GetUserPermissions(ctx domain.RequestContext, spaceID string) (r []space.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='') - 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='space' AND refid=? - AND p.who='role' AND (r.userid=? OR r.userid='')`, - ctx.OrgID, spaceID, ctx.UserID, ctx.OrgID, spaceID, ctx.OrgID) - - if err == sql.ErrNoRows { - err = nil - } - if err != nil { - err = errors.Wrap(err, fmt.Sprintf("unable to execute select user permissions %s", ctx.UserID)) - return - } - - return -} - -// GetPermissions returns space permissions for all users. -func (s Scope) GetPermissions(ctx domain.RequestContext, spaceID string) (r []space.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' - 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='space' AND p.refid=? - AND p.who='role'`, - ctx.OrgID, spaceID, ctx.OrgID, spaceID) - - if err == sql.ErrNoRows { - err = nil - } - if err != nil { - err = errors.Wrap(err, fmt.Sprintf("unable to execute select space permissions %s", ctx.UserID)) - return - } - - return -} - -// DeletePermissions removes records from permissions table for given space ID. -func (s Scope) DeletePermissions(ctx domain.RequestContext, spaceID string) (rows int64, err error) { - b := mysql.BaseQuery{} - - sql := fmt.Sprintf("DELETE FROM permission WHERE orgid='%s' AND location='space' AND refid='%s'", ctx.OrgID, spaceID) - - return b.DeleteWhere(ctx.Transaction, sql) -} - -// DeleteUserPermissions removes all roles for the specified user, for the specified space. -func (s Scope) DeleteUserPermissions(ctx domain.RequestContext, spaceID, userID string) (rows int64, err error) { - b := mysql.BaseQuery{} - - sql := fmt.Sprintf("DELETE FROM permission WHERE orgid='%s' AND location='space' AND refid='%s' who='user' AND whoid='%s'", - ctx.OrgID, spaceID, userID) - - return b.DeleteWhere(ctx.Transaction, sql) -} - -// DeleteAllUserPermissions removes all roles for the specified user, for the specified space. -func (s Scope) DeleteAllUserPermissions(ctx domain.RequestContext, userID string) (rows int64, err error) { - b := mysql.BaseQuery{} - - sql := fmt.Sprintf("DELETE FROM permission WHERE orgid='%s' AND who='user' AND whoid='%s'", - ctx.OrgID, userID) - - return b.DeleteWhere(ctx.Transaction, sql) -} diff --git a/domain/space/permission.go b/domain/space/permission.go deleted file mode 100644 index 358b1f94..00000000 --- a/domain/space/permission.go +++ /dev/null @@ -1,41 +0,0 @@ -// 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 space handles API calls and persistence for spaces. -// Spaces in Documize contain documents. -package space - -import ( - "database/sql" - - "github.com/documize/community/domain" - "github.com/documize/community/model/space" -) - -// CanViewSpace returns if the user has permission to view the given spaceID. -func CanViewSpace(ctx domain.RequestContext, s domain.Store, spaceID string) bool { - roles, err := s.Space.GetUserPermissions(ctx, spaceID) - if err == sql.ErrNoRows { - err = nil - } - if err != nil { - return false - } - - for _, role := range roles { - if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" && - space.HasPermission(role.Action, space.SpaceView, space.SpaceManage, space.SpaceOwner) { - return true - } - } - - return false -} diff --git a/domain/space/space.go b/domain/space/space.go index a48ab4ee..676a53e3 100644 --- a/domain/space/space.go +++ b/domain/space/space.go @@ -20,6 +20,7 @@ import ( "github.com/documize/community/domain" "github.com/documize/community/domain/mail" "github.com/documize/community/model/account" + "github.com/documize/community/model/permission" "github.com/documize/community/model/space" "github.com/documize/community/model/user" ) @@ -61,7 +62,7 @@ func inviteNewUserToSharedSpace(ctx domain.RequestContext, rt *env.Runtime, s *d return } - perm := space.Permission{} + perm := permission.Permission{} perm.OrgID = sp.OrgID perm.Who = "user" perm.WhoID = userID @@ -70,7 +71,7 @@ func inviteNewUserToSharedSpace(ctx domain.RequestContext, rt *env.Runtime, s *d perm.RefID = sp.RefID perm.Action = "" // we send array for actions below - err = s.Space.AddPermissions(ctx, perm, space.SpaceView) + err = s.Permission.AddPermissions(ctx, perm, permission.SpaceView) if err != nil { return } diff --git a/domain/storer.go b/domain/storer.go index 47218885..8dcb67e2 100644 --- a/domain/storer.go +++ b/domain/storer.go @@ -22,6 +22,7 @@ import ( "github.com/documize/community/model/link" "github.com/documize/community/model/org" "github.com/documize/community/model/page" + "github.com/documize/community/model/permission" "github.com/documize/community/model/pin" "github.com/documize/community/model/search" "github.com/documize/community/model/space" @@ -40,6 +41,7 @@ type Store struct { Organization OrganizationStorer Page PageStorer Pin PinStorer + Permission PermissionStorer Search SearchStorer Setting SettingStorer Space SpaceStorer @@ -55,14 +57,17 @@ type SpaceStorer interface { Update(ctx RequestContext, sp space.Space) (err error) Viewers(ctx RequestContext) (v []space.Viewer, err error) Delete(ctx RequestContext, id string) (rows int64, err error) +} - AddPermission(ctx RequestContext, r space.Permission) (err error) - AddPermissions(ctx RequestContext, r space.Permission, actions ...space.PermissionAction) (err error) - GetUserPermissions(ctx RequestContext, spaceID string) (r []space.Permission, err error) - GetPermissions(ctx RequestContext, spaceID string) (r []space.Permission, err error) - DeletePermissions(ctx RequestContext, spaceID string) (rows int64, err error) - DeleteUserPermissions(ctx RequestContext, spaceID, userID string) (rows int64, err error) - DeleteAllUserPermissions(ctx RequestContext, userID string) (rows int64, err error) +// PermissionStorer defines required methods for space/document permission management +type PermissionStorer interface { + AddPermission(ctx RequestContext, r permission.Permission) (err error) + AddPermissions(ctx RequestContext, r permission.Permission, actions ...permission.Action) (err error) + GetUserSpacePermissions(ctx RequestContext, spaceID string) (r []permission.Permission, err error) + GetSpacePermissions(ctx RequestContext, spaceID string) (r []permission.Permission, err error) + DeleteSpacePermissions(ctx RequestContext, spaceID string) (rows int64, err error) + DeleteUserSpacePermissions(ctx RequestContext, spaceID, userID string) (rows int64, err error) + DeleteUserPermissions(ctx RequestContext, userID string) (rows int64, err error) } // UserStorer defines required methods for user management diff --git a/domain/template/endpoint.go b/domain/template/endpoint.go index 4f3e1fc4..f1a617b5 100644 --- a/domain/template/endpoint.go +++ b/domain/template/endpoint.go @@ -27,12 +27,13 @@ import ( "github.com/documize/community/core/stringutil" "github.com/documize/community/core/uniqueid" "github.com/documize/community/domain" - "github.com/documize/community/domain/document" + "github.com/documize/community/domain/permission" indexer "github.com/documize/community/domain/search" "github.com/documize/community/model/attachment" "github.com/documize/community/model/audit" "github.com/documize/community/model/doc" "github.com/documize/community/model/page" + pm "github.com/documize/community/model/permission" "github.com/documize/community/model/template" uuid "github.com/nu7hatch/gouuid" ) @@ -112,7 +113,7 @@ func (h *Handler) SaveAs(w http.ResponseWriter, r *http.Request) { return } - if !document.CanChangeDocument(ctx, *h.Store, model.DocumentID) { + if !permission.HasDocumentAction(ctx, *h.Store, model.DocumentID, pm.DocumentTemplate) { response.WriteForbiddenError(w) return } diff --git a/domain/user/endpoint.go b/domain/user/endpoint.go index c8e0b976..501d1a0f 100644 --- a/domain/user/endpoint.go +++ b/domain/user/endpoint.go @@ -378,7 +378,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { } // remove all associated roles for this user - _, err = h.Store.Space.DeleteAllUserPermissions(ctx, userID) + _, err = h.Store.Permission.DeleteUserPermissions(ctx, userID) if err != nil { ctx.Transaction.Rollback() response.WriteServerError(w, method, err) diff --git a/edition/boot/mysql.go b/edition/boot/mysql.go index fa5dc92d..04afae13 100644 --- a/edition/boot/mysql.go +++ b/edition/boot/mysql.go @@ -24,6 +24,7 @@ import ( link "github.com/documize/community/domain/link/mysql" org "github.com/documize/community/domain/organization/mysql" page "github.com/documize/community/domain/page/mysql" + permission "github.com/documize/community/domain/permission/mysql" pin "github.com/documize/community/domain/pin/mysql" search "github.com/documize/community/domain/search/mysql" setting "github.com/documize/community/domain/setting/mysql" @@ -43,6 +44,7 @@ func StoreMySQL(r *env.Runtime, s *domain.Store) { s.Organization = org.Scope{Runtime: r} s.Page = page.Scope{Runtime: r} s.Pin = pin.Scope{Runtime: r} + s.Permission = permission.Scope{Runtime: r} s.Search = search.Scope{Runtime: r} s.Setting = setting.Scope{Runtime: r} s.Space = space.Scope{Runtime: r} diff --git a/gui/app/templates/components/document/tab-heading.hbs b/gui/app/templates/components/document/tab-heading.hbs index 581bc855..e145c0fb 100644 --- a/gui/app/templates/components/document/tab-heading.hbs +++ b/gui/app/templates/components/document/tab-heading.hbs @@ -19,6 +19,12 @@ + {{/if}} +
+ expand_less +
+ + {{#if hasMenuPermissions}} {{#dropdown-menu target=menuTarget position="top right" open="click" onOpenCallback=(action 'onMenuOpen') onCloseCallback=(action 'onMenuOpen')}} {{/dropdown-menu}} {{/if}} -
- expand_less -
{{#if menuOpen}} {{#if permissions.documentDelete}} diff --git a/model/space/permissions.go b/model/permission/permissions.go similarity index 57% rename from model/space/permissions.go rename to model/permission/permissions.go index 30debe8d..c67ed926 100644 --- a/model/space/permissions.go +++ b/model/permission/permissions.go @@ -9,12 +9,52 @@ // // https://documize.com -package space +package permission -// PermissionRecord represents space permissions for a user on a space. +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:"-"` + 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" +) + +// 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). -type PermissionRecord struct { +type Record struct { OrgID string `json:"orgId"` SpaceID string `json:"folderId"` UserID string `json:"userId"` @@ -31,8 +71,8 @@ type PermissionRecord struct { // DecodeUserPermissions returns a flat, usable permission summary record // from multiple user permission records for a given space. -func DecodeUserPermissions(perm []Permission) (r PermissionRecord) { - r = PermissionRecord{} +func DecodeUserPermissions(perm []Permission) (r Record) { + r = Record{} if len(perm) > 0 { r.OrgID = perm[0].OrgID @@ -67,9 +107,26 @@ func DecodeUserPermissions(perm []Permission) (r PermissionRecord) { return } +// PermissionsModel details which users have what permissions on a given space. +type PermissionsModel struct { + Message string + Permissions []Record +} + +// HasPermission checks if action matches one of the required actions? +func HasPermission(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 PermissionRecord) (perm []Permission) { +func EncodeUserPermissions(r Record) (perm []Permission) { if r.SpaceView { perm = append(perm, EncodeRecord(r, SpaceView)) } @@ -103,13 +160,13 @@ func EncodeUserPermissions(r PermissionRecord) (perm []Permission) { } // HasAnyPermission returns true if user has at least one permission. -func HasAnyPermission(p PermissionRecord) bool { +func HasAnyPermission(p Record) bool { return p.SpaceView || p.SpaceManage || p.SpaceOwner || p.DocumentAdd || p.DocumentEdit || p.DocumentDelete || p.DocumentMove || p.DocumentCopy || p.DocumentTemplate } // EncodeRecord creates standard permission record representing user permissions for a space. -func EncodeRecord(r PermissionRecord, a PermissionAction) (p Permission) { +func EncodeRecord(r Record, a Action) (p Permission) { p = Permission{} p.OrgID = r.OrgID p.Who = "user" diff --git a/model/space/space.go b/model/space/space.go index 4050d2b5..b25b6d60 100644 --- a/model/space/space.go +++ b/model/space/space.go @@ -12,8 +12,6 @@ package space import ( - "time" - "github.com/documize/community/model" ) @@ -55,44 +53,6 @@ func (l *Space) IsRestricted() bool { return l.Type == ScopeRestricted } -// Permission represents a permission for a space and is persisted to the database. -type Permission struct { - ID uint64 `json:"id"` - OrgID string `json:"-"` - Who string `json:"who"` // user, role - WhoID string `json:"whoId"` // either a user or role ID - Action PermissionAction `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"` -} - -// PermissionAction details type of action -type PermissionAction string - -const ( - // SpaceView action means you can view a space and documents therein - SpaceView PermissionAction = "view" - // SpaceManage action means you can add, remove users, set permissions, but not delete that space - SpaceManage PermissionAction = "manage" - // SpaceOwner action means you can delete a space and do all SpaceManage functions - SpaceOwner PermissionAction = "own" - - // DocumentAdd action means you can create/upload documents to a space - DocumentAdd PermissionAction = "doc-add" - // DocumentEdit action means you can edit documents in a space - DocumentEdit PermissionAction = "doc-edit" - // DocumentDelete means you can delete documents in a space - DocumentDelete PermissionAction = "doc-delete" - // DocumentMove means you can move documents between spaces - DocumentMove PermissionAction = "doc-move" - // DocumentCopy means you can copy documents within and between spaces - DocumentCopy PermissionAction = "doc-copy" - // DocumentTemplate means you can create, edit and delete document templates and content blocks - DocumentTemplate PermissionAction = "doc-template" -) - // Viewer details who can see a particular space type Viewer struct { Name string `json:"name"` @@ -104,12 +64,6 @@ type Viewer struct { Email string `json:"email"` } -// PermissionsModel details which users have what permissions on a given space. -type PermissionsModel struct { - Message string - Permissions []PermissionRecord -} - // AcceptShareModel is used to setup a user who has accepted a shared space. type AcceptShareModel struct { Serial string `json:"serial"` @@ -132,14 +86,3 @@ type NewSpaceRequest struct { CopyPermission bool `json:"copyPermission"` // copy uer permissions CopyDocument bool `json:"copyDocument"` // copy all documents! } - -// HasPermission checks if action matches one of the required actions? -func HasPermission(action PermissionAction, actions ...PermissionAction) bool { - for _, a := range actions { - if action == a { - return true - } - } - - return false -} diff --git a/server/routing/routes.go b/server/routing/routes.go index f06024e6..1fef8d7d 100644 --- a/server/routing/routes.go +++ b/server/routing/routes.go @@ -26,6 +26,7 @@ import ( "github.com/documize/community/domain/meta" "github.com/documize/community/domain/organization" "github.com/documize/community/domain/page" + "github.com/documize/community/domain/permission" "github.com/documize/community/domain/pin" "github.com/documize/community/domain/search" "github.com/documize/community/domain/section" @@ -58,6 +59,7 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) { document := document.Handler{Runtime: rt, Store: s, Indexer: indexer} attachment := attachment.Handler{Runtime: rt, Store: s, Indexer: indexer} conversion := conversion.Handler{Runtime: rt, Store: s, Indexer: indexer} + permission := permission.Handler{Runtime: rt, Store: s} organization := organization.Handler{Runtime: rt, Store: s} //************************************************** @@ -113,9 +115,9 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) { Add(rt, RoutePrefixPrivate, "space/{spaceID}", []string{"DELETE", "OPTIONS"}, nil, space.Delete) Add(rt, RoutePrefixPrivate, "space/{spaceID}/move/{moveToId}", []string{"DELETE", "OPTIONS"}, nil, space.Remove) - Add(rt, RoutePrefixPrivate, "space/{spaceID}/permissions", []string{"PUT", "OPTIONS"}, nil, space.SetPermissions) - Add(rt, RoutePrefixPrivate, "space/{spaceID}/permissions/user", []string{"GET", "OPTIONS"}, nil, space.GetUserPermissions) - Add(rt, RoutePrefixPrivate, "space/{spaceID}/permissions", []string{"GET", "OPTIONS"}, nil, space.GetPermissions) + Add(rt, RoutePrefixPrivate, "space/{spaceID}/permissions", []string{"PUT", "OPTIONS"}, nil, permission.SetSpacePermissions) + Add(rt, RoutePrefixPrivate, "space/{spaceID}/permissions/user", []string{"GET", "OPTIONS"}, nil, permission.GetUserSpacePermissions) + Add(rt, RoutePrefixPrivate, "space/{spaceID}/permissions", []string{"GET", "OPTIONS"}, nil, permission.GetSpacePermissions) Add(rt, RoutePrefixPrivate, "space/{spaceID}/invitation", []string{"POST", "OPTIONS"}, nil, space.Invite) Add(rt, RoutePrefixPrivate, "space", []string{"GET", "OPTIONS"}, []string{"filter", "viewers"}, space.GetSpaceViewers) Add(rt, RoutePrefixPrivate, "space", []string{"POST", "OPTIONS"}, nil, space.Add)