fix(publicshare): [OCISDEV-861] bound the ListGrants stat fan-out - #712
Open
LukasHirt wants to merge 2 commits into
Open
fix(publicshare): [OCISDEV-861] bound the ListGrants stat fan-out#712LukasHirt wants to merge 2 commits into
LukasHirt wants to merge 2 commits into
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
LukasHirt
force-pushed
the
fix/ocisdev-861-publicshare-list
branch
2 times, most recently
from
August 17, 2026 16:11
81193f1 to
c63c328
Compare
ListPublicShares issued one gateway Stat per public link not created by the calling user, purely to read the ListGrants permission bit. On tenants with a few thousand links this exceeded the request deadline, so the shares list came back empty and the Web UI span forever. The permission check itself is unchanged, so every user sees exactly the same links as before. What changed is how often it runs: the listing is split into an in-memory pass that needs no RPCs at all, followed by a pass that stats each distinct resource at most once. Allowed and denied answers are both cached, where previously only allowed ones were, so a denied resource is no longer re-stated once per link pointing at it. Those stats run through a bounded worker pool instead of one after another, and a caller who created every link issues no RPC at all. When the caller's own deadline is about to expire the remaining stats are abandoned and a partial list is returned with a warning, rather than the whole request failing. A resource left undecided is absent from the permitted set and therefore treated as not permitted, so the degraded path can only ever return fewer links, never more. No new configuration is introduced: this is a mitigation until an indexed public-share backend replaces this manager. Also hardens MatchesFilter, whose StorageIDFilterType branch dereferenced share.ResourceId without a nil check. A persisted row with a nil resource_id panicked there, reachable because MatchesFilters runs ahead of the manager's own nil guard while the gateway's space purge passes a StorageIDFilter. Cache keys now include the space id, which also closes a latent cross-space collision in the old key, and the Stat carries a field mask limiting the response to permissions.
LukasHirt
force-pushed
the
fix/ocisdev-861-publicshare-list
branch
from
August 17, 2026 17:48
5588da0 to
86f21bb
Compare
Signed-off-by: Lukas Hirt <info@hirt.cz>
LukasHirt
force-pushed
the
fix/ocisdev-861-publicshare-list
branch
from
August 19, 2026 20:29
4359052 to
18b94ff
Compare
LukasHirt
marked this pull request as ready for review
August 19, 2026 20:45
| func (m *manager) userCanListGrants(ctx context.Context, client gateway.GatewayAPIClient, cache *statCache, rid *provider.ResourceId) bool { | ||
| log := appctx.GetLogger(ctx) | ||
| key := storagespace.FormatResourceID(rid) | ||
| if allowed, hit := cache.get(key); hit { |
There was a problem hiding this comment.
Why do we check cache here? don't we deduplicate the resources already early in memory? And we initialize the cache from scratch before, so it's not kept between user requests, correct? So is a cache hit even possible here?
There was a problem hiding this comment.
If the above is correct, I'd also rename it from cache to something like resultMap. Since it wouldn't really be caching, just collecting the result, no?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes OCISDEV-861:
ListPublicSharesissued one gatewayStatRPC per public link not created by the calling user, exceeding the request deadline on tenants with many links. The permission check is unchanged — every user sees exactly the same links as before — but the cost of performing it is now bounded.Acceptance-test counterpart: owncloud/ocis#12783 (merged;
APITESTS_COMMITIDbumped to00aeefac3b23efbb4b7fe67225e74a8a6e6b71f9).The bug
pkg/publicshare/manager/json/json.go'sListPublicSharesiterated the entire tenant public-link DB and, for every foreign link, issued a synchronous gatewayStatpurely to readPermissionSet.ListGrants— discarding the rest of the response.Observed in production on a tenant with ~3000 links: ~3000 serial RPCs in one request, deadline exceeded, 3091x
listshares: an error occurred during stat on the resourcewithcode = Canceled, infinite spinner in the Web "Shares" page. Independently corroborated by OCISDEV-1196:protojson.Unmarshalon this path accounted for 5.4% of storage-process CPU across 12,937 calls in 5 minutes.This is the production default path —
init()registers the driver namejsoncs3onto this same type, and ocis defaults tojsoncs3. There is no separatemanager/jsoncs3package.The approach
The
Statcheck is kept, not replaced. Four earlier attempts tried to substitute a cheaper precomputed lookup (space membership,+grantspaces,ListReceivedShares, a tenant-wide admin capability check). All of them changed who could see what, and two introduced a privilege escalation. The reason is structural:ListGrantson a resource is the OR of every ACE from that resource up to the space root (assemblePermissions, which also short-circuits on deny grants), so it cannot be derived from any set of space or resource ids. Keeping the original check is what makes visibility parity provable rather than argued.ListPublicSharesis now two passes:resource_idguard behave exactly as before. Split survivors into own shares (no permission check needed) and foreign shares, collecting the set of distinct resource ids the foreign ones point at.Three consequences:
Cache keys use
storagespace.FormatResourceID(the old key omittedSpaceIdwhileutils.ResourceIDEqualcompares it — a cross-space collision), and theStatcarriesFieldMask{Paths: ["permissions"]}so the gateway stops marshalling full resource info to read one bool.Deadline handling
If the caller's context has a deadline, the stat phase stops shortly before it expires and returns a partial list with a single warning, rather than the whole request dying with
code = Canceled. If the caller sets no deadline, no bound is imposed.Deliberately no new configuration: this is a mitigation until an indexed public-share backend replaces this manager, and there is no value in teaching operators knobs that will be removed again. Concurrency is an internal constant; the deadline is derived from the caller and never capped by an invented value.
Measurements
Real oCIS in Docker — two images built from source (unpatched vs this branch), 10,000 real public links created through the API by one user, listed by a different user who is a space Manager. Median of 3:
At 10,000 links the unpatched build took 15.5 s on a cold cache and ~9.8 s warm — straddling a 10 s client deadline, and failing outright under
curl --max-time 10on first call. This branch: 6.45 s cold, ~5.7 s warm, comfortably under.Every cell returned byte-identical response bodies between the two builds (16,346,826 bytes at 10,000 links) — the strongest available evidence that no share is lost.
Two caveats stated plainly. First, that fixture puts every link on its own file, which is the worst case for per-resource dedup — so this 1.4–1.6× reflects bounded concurrency alone. A synthetic run where 3,000 links sat on 50 shared folders showed the dedup and negative-caching paths reducing 2,999 stats to 50 (~109×), which is closer to how a real tenant looks but has not been measured against live oCIS. Second, loopback gRPC costs ~1 ms per
Statversus tens of ms in production, so absolute timings here understate the real-world gap.Testing
15 specs, green under
-race:Statcalls for own shares, written first and failing with a measured 500 stats.resource_idguards for both filter types.Several are mutation-verified — deleting the
ListGrantscheck, keying onSpaceIdalone, or dropping negative caching each fail exactly the intended spec and nothing else.Also fixed here
A nil-pointer panic in
pkg/publicshare/publicshare.go:MatchesFilter'sStorageIDFilterTypebranch dereferencedshare.ResourceId.StorageIdunguarded. A persisted row with nilresource_id(real — OCISDEV-877 shipped a repair CLI for it) panicked, reachable becauseMatchesFiltersruns before the manager's own nil guard while the gateway's space-purge passes aStorageIDFilter. Pre-existing; fixed here because it sits inside the code path under change, and regression-tested.