From 467acec3c4bc5fd4ec84f062f5c083e1caedbfc1 Mon Sep 17 00:00:00 2001 From: HarveyKandola Date: Fri, 22 Jun 2018 17:01:26 +0100 Subject: [PATCH] Implement category-based permissioning for search results Only see what you can see. Co-Authored-By: Saul S --- domain/category/mysql/store.go | 48 ++++++++++++++++++++++++++++++++-- domain/document/endpoint.go | 19 +++++++++----- domain/search/mysql/store.go | 2 +- domain/search/search.go | 33 +++++++++++++++++++++++ domain/storer.go | 2 ++ 5 files changed, 95 insertions(+), 9 deletions(-) diff --git a/domain/category/mysql/store.go b/domain/category/mysql/store.go index fdf3cc9e..f47c9447 100644 --- a/domain/category/mysql/store.go +++ b/domain/category/mysql/store.go @@ -77,8 +77,8 @@ func (s Scope) GetAllBySpace(ctx domain.RequestContext, spaceID string) (c []cat WHERE orgid=? AND labelid=? AND labelid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN ( SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view' UNION ALL - SELECT p.refid from permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.who='role' AND p.location='space' - AND p.action='view' AND (r.userid=? OR r.userid='0') + SELECT p.refid from permission p LEFT JOIN rolemember r ON p.whoid=r.roleid + WHERE p.orgid=? AND p.who='role' AND p.location='space' AND p.action='view' AND (r.userid=? OR r.userid='0') )) ORDER BY category`, ctx.OrgID, spaceID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID) @@ -92,6 +92,28 @@ func (s Scope) GetAllBySpace(ctx domain.RequestContext, spaceID string) (c []cat return } +// GetByOrg returns all categories accessible by user for their org. +func (s Scope) GetByOrg(ctx domain.RequestContext, userID string) (c []category.Category, err error) { + err = s.Runtime.Db.Select(&c, ` + SELECT id, refid, orgid, labelid, category, created, revised FROM category + WHERE orgid=? + AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='category' AND refid IN ( + SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='category' UNION ALL + SELECT p.refid from permission p LEFT JOIN rolemember r ON p.whoid=r.roleid + WHERE p.orgid=? AND p.who='role' AND p.location='category' AND (r.userid=? OR r.userid='0') + )) + ORDER BY category`, ctx.OrgID, ctx.OrgID, ctx.OrgID, userID, ctx.OrgID, userID) + + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + err = errors.Wrap(err, fmt.Sprintf("unable to execute select categories for org %s", ctx.OrgID)) + } + + return +} + // Update saves category name change. func (s Scope) Update(ctx domain.RequestContext, c category.Category) (err error) { c.Revised = time.Now().UTC() @@ -255,3 +277,25 @@ func (s Scope) GetSpaceCategoryMembership(ctx domain.RequestContext, spaceID str return } + +// GetOrgCategoryMembership returns category/document associations within organization. +func (s Scope) GetOrgCategoryMembership(ctx domain.RequestContext, userID string) (c []category.Member, err error) { + err = s.Runtime.Db.Select(&c, ` + SELECT id, refid, orgid, labelid, categoryid, documentid, created, revised FROM categorymember + WHERE orgid=? + AND labelid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN ( + SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view' UNION ALL + SELECT p.refid from permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.who='role' AND p.location='space' + AND p.action='view' AND (r.userid=? OR r.userid='0') + )) + ORDER BY documentid`, ctx.OrgID, ctx.OrgID, ctx.OrgID, userID, ctx.OrgID, userID) + + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + err = errors.Wrap(err, fmt.Sprintf("select all category/document membership for organization %s", ctx.OrgID)) + } + + return +} diff --git a/domain/document/endpoint.go b/domain/document/endpoint.go index 29755195..f81b5bc4 100644 --- a/domain/document/endpoint.go +++ b/domain/document/endpoint.go @@ -418,6 +418,7 @@ func (h *Handler) SearchDocuments(w http.ResponseWriter, r *http.Request) { return } + // Get search criteria. options := search.QueryOptions{} err = json.Unmarshal(body, &options) if err != nil { @@ -425,24 +426,30 @@ func (h *Handler) SearchDocuments(w http.ResponseWriter, r *http.Request) { h.Runtime.Log.Error(method, err) return } - options.Keywords = strings.TrimSpace(options.Keywords) + // Get documents for search criteria. results, err := h.Store.Search.Documents(ctx, options) if err != nil { h.Runtime.Log.Error(method, err) } - // Put in slugs for easy UI display of search URL + // Generate slugs for search URL. for key, result := range results { results[key].DocumentSlug = stringutil.MakeSlug(result.Document) results[key].SpaceSlug = stringutil.MakeSlug(result.Space) } - // Record user search history + // Remove documents that cannot be seen due to lack of + // category view/access permission. + cats, err := h.Store.Category.GetByOrg(ctx, ctx.UserID) + members, err := h.Store.Category.GetOrgCategoryMembership(ctx, ctx.UserID) + filtered := indexer.FilterCategoryProtected(results, cats, members) + + // Record user search history. if !options.SkipLog { - if len(results) > 0 { - go h.recordSearchActivity(ctx, results, options.Keywords) + if len(filtered) > 0 { + go h.recordSearchActivity(ctx, filtered, options.Keywords) } else { ctx.Transaction, err = h.Runtime.Db.Beginx() if err != nil { @@ -468,7 +475,7 @@ func (h *Handler) SearchDocuments(w http.ResponseWriter, r *http.Request) { h.Store.Audit.Record(ctx, audit.EventTypeSearch) - response.WriteJSON(w, results) + response.WriteJSON(w, filtered) } // Record search request once per document. diff --git a/domain/search/mysql/store.go b/domain/search/mysql/store.go index e261010e..387f0ff7 100644 --- a/domain/search/mysql/store.go +++ b/domain/search/mysql/store.go @@ -151,7 +151,7 @@ func (s Scope) DeleteContent(ctx domain.RequestContext, pageID string) (err erro } // Documents searches the documents that the client is allowed to see, using the keywords search string, then audits that search. -// Visible documents include both those in the client's own organisation and those that are public, or whose visibility includes the client. +// Visible documents include both those in the client's own organization and those that are public, or whose visibility includes the client. func (s Scope) Documents(ctx domain.RequestContext, q search.QueryOptions) (results []search.QueryResult, err error) { q.Keywords = strings.TrimSpace(q.Keywords) if len(q.Keywords) == 0 { diff --git a/domain/search/search.go b/domain/search/search.go index 0d5dd77f..90cf572f 100644 --- a/domain/search/search.go +++ b/domain/search/search.go @@ -14,8 +14,10 @@ package search import ( "github.com/documize/community/domain" "github.com/documize/community/model/attachment" + "github.com/documize/community/model/category" "github.com/documize/community/model/doc" "github.com/documize/community/model/page" + sm "github.com/documize/community/model/search" ) // IndexDocument adds search indesd entries for document inserting title, tags and attachments as @@ -103,3 +105,34 @@ func (m *Indexer) DeleteContent(ctx domain.RequestContext, pageID string) { ctx.Transaction.Commit() } + +// FilterCategoryProtected removes search results that cannot be seen by user +// due to document cateogory viewing permissions. +func FilterCategoryProtected(results []sm.QueryResult, cats []category.Category, members []category.Member) (filtered []sm.QueryResult) { + filtered = []sm.QueryResult{} + + for _, result := range results { + hasCategory := false + canSeeCategory := false + + OUTER: + + for _, m := range members { + if m.DocumentID == result.DocumentID { + hasCategory = true + for _, cat := range cats { + if cat.RefID == m.CategoryID { + canSeeCategory = true + continue OUTER + } + } + } + } + + if !hasCategory || canSeeCategory { + filtered = append(filtered, result) + } + } + + return +} diff --git a/domain/storer.go b/domain/storer.go index 1cb21a6d..3611ffa3 100644 --- a/domain/storer.go +++ b/domain/storer.go @@ -81,6 +81,8 @@ type CategoryStorer interface { GetSpaceCategoryMembership(ctx RequestContext, spaceID string) (c []category.Member, err error) RemoveDocumentCategories(ctx RequestContext, documentID string) (rows int64, err error) RemoveSpaceCategoryMemberships(ctx RequestContext, spaceID string) (rows int64, err error) + GetByOrg(ctx RequestContext, userID string) (c []category.Category, err error) + GetOrgCategoryMembership(ctx RequestContext, userID string) (c []category.Member, err error) } // PermissionStorer defines required methods for space/document permission management