@@ -16,6 +16,7 @@ import (
1616
1717type ItemLinkHandler struct {
1818 db database.Database
19+ permissionService * services.PermissionService
1920 notificationService interface {
2021 EmitEvent (event * services.NotificationEvent )
2122 } // Notification service for async notification processing (optional, can be nil)
@@ -26,13 +27,19 @@ type ItemLinkHandler struct {
2627
2728func NewItemLinkHandler (db database.Database , notificationService interface {
2829 EmitEvent (event * services.NotificationEvent )
29- }) * ItemLinkHandler {
30+ }, permissionService * services. PermissionService ) * ItemLinkHandler {
3031 return & ItemLinkHandler {
3132 db : db ,
3233 notificationService : notificationService ,
34+ permissionService : permissionService ,
3335 }
3436}
3537
38+ // checkItemEditPermission checks if the current user can edit the given item
39+ func (h * ItemLinkHandler ) checkItemEditPermission (w http.ResponseWriter , r * http.Request , itemID int ) bool {
40+ return CheckItemPermission (w , r , h .db , h .permissionService , itemID , models .PermissionItemEdit )
41+ }
42+
3643// SetActionService sets the action service for automation workflows
3744func (h * ItemLinkHandler ) SetActionService (actionService interface {
3845 EmitActionEvent (event * models.ActionEvent )
@@ -49,6 +56,23 @@ func (h *ItemLinkHandler) GetLinksForItem(w http.ResponseWriter, r *http.Request
4956 return
5057 }
5158
59+ user := h .getUserFromContext (r )
60+ if user == nil {
61+ respondUnauthorized (w , r )
62+ return
63+ }
64+
65+ // Check item.view permission if it's a work item
66+ var workspaceID int
67+ isWorkItem := h .db .QueryRow ("SELECT workspace_id FROM items WHERE id = ?" , id ).Scan (& workspaceID ) == nil
68+ if isWorkItem {
69+ hasView , _ := h .permissionService .HasWorkspacePermission (user .ID , workspaceID , models .PermissionItemView )
70+ if ! hasView {
71+ respondNotFound (w , r , "item" )
72+ return
73+ }
74+ }
75+
5276 // Convert URL path to internal type
5377 internalType := "item"
5478 if itemType == "test-cases" {
@@ -69,6 +93,11 @@ func (h *ItemLinkHandler) GetLinksForItem(w http.ResponseWriter, r *http.Request
6993 return
7094 }
7195
96+ // Filter linked items by accessible workspaces
97+ accessibleKeys , _ := GetAccessibleWorkspaceKeys (user , h .db , h .permissionService )
98+ outgoingLinks = filterLinksByAccessibleWorkspaces (outgoingLinks , accessibleKeys )
99+ incomingLinks = filterLinksByAccessibleWorkspaces (incomingLinks , accessibleKeys )
100+
72101 response := map [string ]interface {}{
73102 "outgoing" : outgoingLinks ,
74103 "incoming" : incomingLinks ,
@@ -78,6 +107,21 @@ func (h *ItemLinkHandler) GetLinksForItem(w http.ResponseWriter, r *http.Request
78107 json .NewEncoder (w ).Encode (response )
79108}
80109
110+ // filterLinksByAccessibleWorkspaces removes links pointing to items in inaccessible workspaces
111+ func filterLinksByAccessibleWorkspaces (links []models.ItemLink , accessibleKeys map [string ]bool ) []models.ItemLink {
112+ filtered := make ([]models.ItemLink , 0 , len (links ))
113+ for _ , link := range links {
114+ if link .SourceType == "item" && link .SourceWorkspaceKey != "" && ! accessibleKeys [link .SourceWorkspaceKey ] {
115+ continue
116+ }
117+ if link .TargetType == "item" && link .TargetWorkspaceKey != "" && ! accessibleKeys [link .TargetWorkspaceKey ] {
118+ continue
119+ }
120+ filtered = append (filtered , link )
121+ }
122+ return filtered
123+ }
124+
81125// CreateLink creates a new link between items
82126func (h * ItemLinkHandler ) CreateLink (w http.ResponseWriter , r * http.Request ) {
83127 var link models.ItemLink
@@ -146,6 +190,19 @@ func (h *ItemLinkHandler) CreateLink(w http.ResponseWriter, r *http.Request) {
146190 }
147191 createdBy := currentUser .ID
148192
193+ // Check item.edit on source (authorizes modifying the source by adding a link)
194+ if link .SourceType == "item" {
195+ if ! CheckItemPermission (w , r , h .db , h .permissionService , link .SourceID , models .PermissionItemEdit ) {
196+ return
197+ }
198+ }
199+ // Check item.view on target (verifies user can see it — prevents existence leakage)
200+ if link .TargetType == "item" {
201+ if ! CheckItemPermission (w , r , h .db , h .permissionService , link .TargetID , models .PermissionItemView ) {
202+ return
203+ }
204+ }
205+
149206 // Create link via service (handles link type validation + insert)
150207 linkSvc := services .NewItemLinkService (h .db )
151208 id , err := linkSvc .CreateLink (services.CreateItemLinkParams {
@@ -270,6 +327,13 @@ func (h *ItemLinkHandler) DeleteLink(w http.ResponseWriter, r *http.Request) {
270327 return
271328 }
272329
330+ // Check item.edit permission for item-type source
331+ if sourceType == "item" {
332+ if ! h .checkItemEditPermission (w , r , sourceID ) {
333+ return
334+ }
335+ }
336+
273337 result , err := h .db .ExecWrite ("DELETE FROM item_links WHERE id = ?" , id )
274338 if err != nil {
275339 respondInternalError (w , r , err )
@@ -333,6 +397,10 @@ func (h *ItemLinkHandler) GetLinkedAssets(w http.ResponseWriter, r *http.Request
333397 return
334398 }
335399
400+ if ! CheckItemPermission (w , r , h .db , h .permissionService , id , models .PermissionItemView ) {
401+ return
402+ }
403+
336404 // Get assets where item is the source
337405 outgoingQuery := `
338406 SELECT a.id, a.title, COALESCE(a.description, '') AS description,
@@ -441,6 +509,18 @@ func (h *ItemLinkHandler) GetLinkedAssets(w http.ResponseWriter, r *http.Request
441509
442510// SearchLinkableItems searches for items that can be linked
443511func (h * ItemLinkHandler ) SearchLinkableItems (w http.ResponseWriter , r * http.Request ) {
512+ user := h .getUserFromContext (r )
513+ if user == nil {
514+ respondUnauthorized (w , r )
515+ return
516+ }
517+
518+ accessibleWorkspaceIDs , err := GetAccessibleWorkspaceIDs (user , h .db , h .permissionService )
519+ if err != nil {
520+ respondInternalError (w , r , err )
521+ return
522+ }
523+
444524 query := r .URL .Query ().Get ("q" )
445525 itemType := r .URL .Query ().Get ("type" ) // "item", "test_case", "asset", or empty for all
446526 limit := 20
@@ -455,7 +535,7 @@ func (h *ItemLinkHandler) SearchLinkableItems(w http.ResponseWriter, r *http.Req
455535
456536 // Search work items
457537 if itemType == "" || itemType == "item" {
458- workItems , err := h .searchWorkItems (query , limit )
538+ workItems , err := h .searchWorkItems (query , limit , accessibleWorkspaceIDs )
459539 if err != nil {
460540 respondInternalError (w , r , err )
461541 return
@@ -569,11 +649,16 @@ func (h *ItemLinkHandler) getLinkByID(id int) (*models.ItemLink, error) {
569649 return & links [0 ], nil
570650}
571651
572- func (h * ItemLinkHandler ) searchWorkItems (query string , limit int ) ([]models.LinkableItem , error ) {
573- sqlQuery := `
574- SELECT
575- i.id,
576- i.title,
652+ func (h * ItemLinkHandler ) searchWorkItems (query string , limit int , accessibleWorkspaceIDs []int ) ([]models.LinkableItem , error ) {
653+ if len (accessibleWorkspaceIDs ) == 0 {
654+ return []models.LinkableItem {}, nil
655+ }
656+
657+ placeholders , wsArgs := BuildWorkspaceIDPlaceholders (accessibleWorkspaceIDs )
658+ sqlQuery := fmt .Sprintf (`
659+ SELECT
660+ i.id,
661+ i.title,
577662 COALESCE(i.description, '') AS description,
578663 i.workspace_id,
579664 w.name AS workspace_name,
@@ -583,13 +668,17 @@ func (h *ItemLinkHandler) searchWorkItems(query string, limit int) ([]models.Lin
583668 LEFT JOIN workspaces w ON i.workspace_id = w.id
584669 LEFT JOIN statuses s ON i.status_id = s.id
585670 LEFT JOIN priorities p ON i.priority_id = p.id
586- WHERE i.title LIKE ? OR i.description LIKE ?
671+ WHERE (i.title LIKE ? OR i.description LIKE ?)
672+ AND i.workspace_id IN (%s)
587673 ORDER BY i.title
588674 LIMIT ?
589- `
675+ ` , placeholders )
590676
591677 searchTerm := "%" + query + "%"
592- rows , err := h .db .Query (sqlQuery , searchTerm , searchTerm , limit )
678+ args := []interface {}{searchTerm , searchTerm }
679+ args = append (args , wsArgs ... )
680+ args = append (args , limit )
681+ rows , err := h .db .Query (sqlQuery , args ... )
593682 if err != nil {
594683 return nil , err
595684 }
0 commit comments