Browse Source

Expose member permissions so manage-all survives a write

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>
pull/7569/head
Rune Darrud 5 days ago
parent
commit
7f2911231a
  1. 33
      scripts/smoke_public_api_write.sh
  2. 29
      src/api/core/public.rs

33
scripts/smoke_public_api_write.sh

@ -395,6 +395,39 @@ check_eq "restoring an active member -> 400" "$HTTP_CODE" "400"
req POST "/api/public/members/$NEWMEMBER/restore" "$TOKEN" req POST "/api/public/members/$NEWMEMBER/restore" "$TOKEN"
check_eq "restoring an invited member -> 400" "$HTTP_CODE" "400" check_eq "restoring an invited member -> 400" "$HTTP_CODE" "400"
echo ""
echo "== A manage-all member survives a read-modify-write =="
# Vaultwarden stores manage-all as the access_all flag on a Manager. It is exposed as the
# custom role plus its three collection permissions, which is the only shape a client can
# read and send back without silently dropping the access.
PERMS='{"accessEventLogs":false,"accessImportExport":false,"accessReports":false,"createNewCollections":true,"editAnyCollection":true,"deleteAnyCollection":true,"manageGroups":false,"managePolicies":false,"manageSso":false,"manageUsers":false,"manageResetPassword":false,"manageScim":false}'
reqj POST "/api/public/members" "$TOKEN" \
"{\"email\":\"manageall@example.com\",\"type\":4,\"permissions\":$PERMS}"
check_eq "create a manage-all member -> 200" "$HTTP_CODE" "200"
MANAGEALL=$(jqval '.id')
jqcheck "manage-all member reports the custom role" '.type' "4"
jqcheck "manage-all member reports its permissions" '.permissions.editAnyCollection' "true"
# Read it back and send exactly that back again, which is what a sync client does.
req GET "/api/public/members/$MANAGEALL" "$TOKEN"
jqcheck "manage-all member still reports the custom role" '.type' "4"
ROUNDTRIP=$(jq -c '{type: .type, externalId: .externalId, permissions: .permissions}' "$TMP/body")
reqj PUT "/api/public/members/$MANAGEALL" "$TOKEN" "$ROUNDTRIP"
check_eq "write the member back unchanged -> 200" "$HTTP_CODE" "200"
req GET "/api/public/members/$MANAGEALL" "$TOKEN"
jqcheck "round-trip kept the custom role" '.type' "4"
jqcheck "round-trip kept manage-all" '.permissions.editAnyCollection' "true"
# A plain member carries no permissions object.
req GET "/api/public/members/$MEMBER3" "$TOKEN"
jqcheck "a plain member has null permissions" '.permissions' "null"
req DELETE "/api/public/members/$MANAGEALL" "$TOKEN"
check_eq "clean up the manage-all member -> 200" "$HTTP_CODE" "200"
echo "" echo ""
echo "== Ownership is out of reach for a Public API client ==" echo "== Ownership is out of reach for a Public API client =="

29
src/api/core/public.rs

@ -252,16 +252,43 @@ async fn member_to_json(member: &Membership, conn: &DbConn) -> Value {
member.status member.status
}; };
// HACK: Convert the manager type to a custom type, the same way the internal
// serializers do. Vaultwarden has no real custom role, it links the three collection
// permissions to the access_all flag instead, and the write endpoints read that flag
// back out of exactly this shape. Emitting both is what lets a client read a member,
// send it back unchanged, and keep its access.
let membership_type = member.type_manager_as_custom();
let permissions = if membership_type == 4 && member.access_all {
json!({
"accessEventLogs": false,
"accessImportExport": false,
"accessReports": false,
// If the following 3 Collection roles are set to true a custom user has access all permission
"createNewCollections": true,
"editAnyCollection": true,
"deleteAnyCollection": true,
"manageGroups": false,
"managePolicies": false,
"manageSso": false, // Not supported
"manageUsers": false,
"manageResetPassword": false,
"manageScim": false // Not supported (Not AGPLv3 Licensed)
})
} else {
json!(null)
};
json!({ json!({
"object": "member", "object": "member",
"id": member.uuid, "id": member.uuid,
"userId": member.user_uuid, "userId": member.user_uuid,
"name": name, "name": name,
"email": email, "email": email,
"type": member.atype, "type": membership_type,
"externalId": member.external_id, "externalId": member.external_id,
"resetPasswordEnrolled": member.reset_password_key.is_some(), "resetPasswordEnrolled": member.reset_password_key.is_some(),
"status": status, "status": status,
"permissions": permissions,
}) })
} }

Loading…
Cancel
Save