The member permissions object could not be written back. The read shape emits
permissions null for anyone who is not a manage-all member, but the write models
declared a plain map with serde(default), which only covers a missing field: an
explicit null failed to deserialize and the request was rejected before the
handler ran. Reading a member and sending it straight back therefore failed for
every member except the manage-all case the previous test covered. The field is
optional now, and the smoke test round-trips a plain member as well.
Reinvite was the one member write that could still reach an owner, and it can
change an owner from invited to accepted, so it takes the same guard as the rest.
Group updates log before mutating, so a failure part way through the collection
associations cannot leave an unaudited change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The write endpoints derive the access_all flag from the raw type plus the three
collection permissions, but the member serializer emitted neither the custom
role nor the permissions object. A client that read a manage-all member and
wrote it back unchanged therefore dropped that member from every collection,
because the information needed to round-trip was not in the response.
The member object now reports the custom role and its permissions the same way
the internal serializers do, which is also the shape upstream uses: Permissions
is part of the shared member model and is returned on reads as well as accepted
on writes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adversarial review of the new endpoints turned up several defects.
Ownership is now out of reach of a Public API client. The internal endpoints
only let an Owner grant, change or remove Owner, and that check cannot be
applied here because there is no user behind the request. Since the
organization API key can be created by an Admin, a client could previously
promote itself to Owner and take over the organization. Granting the Owner role
and acting on an existing owner are both refused now, which subsumes the
narrower last confirmed owner guard.
An omitted groups list on a member update no longer unassigns every group.
Upstream leaves group access untouched when the field is absent and only
collections reset on omission, so groups is optional now.
An omitted accessAll on a group update no longer clears the flag, and an
omitted externalId no longer clears the directory matching key that
/public/organization/import relies on to match members and groups.
Revoked members are reported with the upstream status of -1 instead of the
internal offset encoding, matching every other serializer in the codebase.
Restore refuses a member who is not revoked, rather than saving nothing and
logging a restore that did not happen, and group creation is logged before its
collection associations so a failure cannot leave an unaudited group.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Boots a throwaway instance against a seeded SQLite database and exercises every
member and group write endpoint end to end, asserting on the resulting state
rather than just the status code.
Covers the guards that protect organization integrity: the last confirmed owner
cannot be demoted, revoked or deleted; collections, groups and members from
another organization are rejected; ids belonging to another organization return
404; and writes require a token. It also pins two behaviours that are easy to
regress: a group update leaves member assignments alone, and every write is
recorded in the event log with no acting user and no device type.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Boots a throwaway instance against a temporary SQLite database seeded
with two organizations (each with members, groups, collections and
their access associations), mints an organization API token, and
asserts on every read endpoint: member/group/collection lists and
details, the direct member-to-collection grant on member detail, the
group-to-collection grant on group and collection detail, and that
collection responses omit the encrypted name.
It also asserts the organization scoping boundary (ids owned by the
second organization return 404 through the first org's token) and that
an unauthenticated request returns 401. The script exits non-zero if any
assertion fails, so it can be run as a check. It builds the binary when
one is not supplied via VW_BIN, uses a throwaway port and temp directory,
and cleans up on exit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>