Commit Graph
4 Commits
Author SHA1 Message Date
Chris Lu cc2775d9f2 s3: register an identity's inline account instead of collapsing it into admin (#10548)
* s3: register an identity's inline account instead of collapsing it into admin

Credential stores persist an account inline on the identity and never
emit a top-level accounts list, so every user created through the IAM
API or the admin UI with an email hit the "non exist account ID" branch
and was given the shared admin account. Distinct users then presented
the same owner id, so ownership checks could not tell them apart and
each passed for the others' buckets.

Treat an id missing from the account map as undeclared rather than
invalid: register it, keeping an email another account already claimed.
Both load paths now resolve the account through one helper.

* s3: refresh an undeclared account from the identity that carries it

The merge path starts from the live account cache, so an identity
upserted with the same account id but a new email or display name kept
the cached copy: the new address never reached the email index and the
replaced one still resolved. Changing a user's email through the admin
UI takes exactly that path.

An account registered from an inline block is only described by the
identity carrying it, so refresh it and move its email claim. Accounts
from a top-level list and the predefined defaults are marked declared
and stay authoritative.

* s3: let an account reclaim an email once its holder moves away

Two identities can carry the same email, and the second to load leaves
the lookup with the first. Returning early when the incoming metadata
matches the cached account meant the loser never re-ran the claim, so an
address freed by the holder's update resolved to nobody until the loser
itself changed. Re-index on the unchanged path, which is a no-op while
another account still holds the address.
2026-08-03 12:45:42 -07:00
Chris Lu c4f0b12a9a s3: lazy, bounded, entry-free per-bucket caches (#10213)
* s3: stop warming every bucket's config at startup

Listing all buckets in BucketRegistry.init() made S3 gateway startup
O(buckets) and pinned every bucket's metadata and config resident, which
does not scale past a few hundred thousand buckets. Both caches already
have lazy miss paths, so load on first access instead and let the
metadata subscription refresh only entries already resident; cold
buckets cost one filer round-trip on their first request.

* s3: bound the per-bucket caches with LRU eviction

The bucket config cache, bucket registry, and their negative caches were
plain maps that only ever grew: the config cache TTL made Get miss but
never evicted the entry, and the not-found sets grew on every probe of a
nonexistent bucket name. Cap all four at 65536 entries with LRU eviction
so a gateway keeps its hot working set and evicted buckets reload from
the filer on next access.

* s3: cache parsed bucket config instead of the full filer entry

Each cached BucketConfig retained the whole bucket entry (extended
attribute map plus raw content bytes) alongside the fields parsed from
it, roughly doubling per-bucket cache cost and keeping data the read
path never looks at. Parse everything up front in
newBucketConfigFromEntry - now also the creator identity, tags,
encryption config, and stored lifecycle XML - and drop the entry.

updateBucketConfig now reads the entry fresh from the filer and diffs
the mapped extended attributes against it, so the patch is computed
against current state instead of a cached copy; the config clone
helpers that existed for that path go away.

* s3: dedup cold bucket-registry loads per bucket

The registry's notFound lock doubled as the load serializer, holding one
global mutex across the filer round-trip so first-touch requests for
different buckets queued behind each other; the cache fill also happened
after the lock was released, so two concurrent misses for the same
bucket could both reach the filer. Replace it with a singleflight per
bucket that fills the cache inside the flight: different buckets load
concurrently, the same bucket loads once.
2026-07-02 21:11:34 -07:00
Chris Lu 14d247703a s3: register account-less identities' synthesized account so ACL/owner ids resolve (#9971)
* s3: register account-less identities' synthesized account in the lookup

#9962 gave each account-less identity a distinct account id derived from
its name (instead of collapsing into admin), but never registered that
account in the id->account map. GetAccountNameById then returned empty
for such ids, so ACL grantee validation rejected canonical grants to the
caller's own account with InvalidRequest, and bucket/object owner display
was dropped as 'owner is invalid'.

This broke a canned PutObjectAcl by an account-less identity (e.g.
TestVersionedObjectAcl with the default 'some_admin_user' identity):
ValidateAndTransferGrants -> GetAccountNameById -> 'account id is not
exists' -> 400 InvalidRequest.

Register the synthesized account at config load so its id resolves to a
display name. Add a regression test.

* s3: reuse explicitly-configured account for account-less identity

Address review: if an account with the same id as an account-less
identity's synthesized account is explicitly configured (custom display
name/email), reuse it instead of the synthesized one. Add a test.
2026-06-14 21:42:23 -07:00
Chris Lu e64c821139 s3: give account-less identities a distinct owner instead of admin (#9962)
* s3: stop collapsing account-less identities into the admin account

Identities configured without an account block all defaulted to the
shared admin account, so distinct users got the same owner id and
ownership checks could not tell them apart. checkAccessByOwnership also
treated that id as an admin bypass, so any account-less caller passed
ownership for any bucket. Give such identities a distinct account id from
their name, and decide the ownership admin bypass by Admin capability
rather than by the account id. isUserAdmin is now nil-safe.

* s3: use the context identity in isUserAdmin before re-authenticating

The Auth middleware already verifies and stores the identity in the
request context. Read it there first so the ownership/admin checks don't
re-run signature verification, which is redundant and fails once the
request body has been consumed.

* s3: nil-guard the context identity in isUserAdmin

A non-nil interface wrapping a typed-nil *Identity passes the type
assertion; guard against it before calling isAdmin().

* s3: trim verbose comments
2026-06-14 13:54:49 -07:00