checkItemVisible is the context-free visibility decision. Returns (true, nil) when the (user, role) pair can see `item` under the same rules `requireItemVisible` enforces. Centralizes the rule set so the resolver route (IDEA-1492) and the middleware-gated handlers can't drift. Inputs: - workspaceI
(workspaceID string, item *models.Item, user *models.User, role string, isBearer bool)
| 2129 | // because the item-grants branch only checked direct grants + the |
| 2130 | // member's explicit collection-access list). |
| 2131 | func (s *Server) checkItemVisible(workspaceID string, item *models.Item, user *models.User, role string, isBearer bool) (bool, error) { |
| 2132 | // Tokenized-nil-user bypass. RequireWorkspaceAccess synthesizes |
| 2133 | // "owner" on fresh installs (UserCount == 0, currentUser == nil) and |
| 2134 | // "editor" for legacy workspace-scoped API tokens (currentUser == |
| 2135 | // nil but tokenWorkspaceID matches). Both are authorized by the |
| 2136 | // middleware already. Real authenticated users with these roles |
| 2137 | // (workspace owners, member.Role=="editor", …) must NOT short-circuit |
| 2138 | // here — they have to walk the per-collection filter so |
| 2139 | // collection_access="specific" + member_collection_access actually |
| 2140 | // gates them (Codex round-3 — the round-2 fix dropped the |
| 2141 | // `user == nil` qualifier and accidentally disabled the gate for |
| 2142 | // every real editor too). |
| 2143 | if user == nil && (role == "owner" || role == "editor") { |
| 2144 | return true, nil |
| 2145 | } |
| 2146 | if user == nil { |
| 2147 | return false, nil |
| 2148 | } |
| 2149 | // Admin sees everything, but only for cookie-session auth (matches |
| 2150 | // visibleCollectionIDs's nil-filter shape). Bearer-borne admins |
| 2151 | // (BUG-1918) fall through to the same per-collection filter every |
| 2152 | // other member faces. |
| 2153 | if user.Role == "admin" && !isBearer { |
| 2154 | return true, nil |
| 2155 | } |
| 2156 | |
| 2157 | // Visibility filter: nil = unrestricted; non-nil = restricted to the slice. |
| 2158 | visibleIDs, err := s.store.VisibleCollectionIDs(workspaceID, user.ID) |
| 2159 | if err != nil { |
| 2160 | return false, err |
| 2161 | } |
| 2162 | if !isCollectionVisible(item.CollectionID, visibleIDs) { |
| 2163 | return false, nil |
| 2164 | } |
| 2165 | |
| 2166 | // Replay guestResourceFilterCore's logic without the *http.Request |
| 2167 | // dependency. Member-with-all-access short-circuits to "no item-level |
| 2168 | // filter"; guests + restricted members get the grant filter. |
| 2169 | if role != "guest" { |
| 2170 | member, err := s.store.GetWorkspaceMember(workspaceID, user.ID) |
| 2171 | if err != nil { |
| 2172 | return false, err |
| 2173 | } |
| 2174 | if member != nil && (member.CollectionAccess == "all" || member.CollectionAccess == "") { |
| 2175 | // Full collection access — visibleIDs filter already passed. |
| 2176 | return true, nil |
| 2177 | } |
| 2178 | } |
| 2179 | |
| 2180 | grantCollIDs, grantedItemIDs, err := s.store.GuestVisibleResources(workspaceID, user.ID) |
| 2181 | if err != nil { |
| 2182 | return false, err |
| 2183 | } |
| 2184 | if len(grantedItemIDs) == 0 { |
| 2185 | // No item-level grants in play. visibleCollectionIDs already |
| 2186 | // determined the collection is reachable; visibility stands. |
| 2187 | return true, nil |
| 2188 | } |