diff --git a/scripts/smoke_public_api_write.sh b/scripts/smoke_public_api_write.sh index 47db57a4..18540d5a 100755 --- a/scripts/smoke_public_api_write.sh +++ b/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" 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 "== Ownership is out of reach for a Public API client ==" diff --git a/src/api/core/public.rs b/src/api/core/public.rs index d790274f..310d6391 100644 --- a/src/api/core/public.rs +++ b/src/api/core/public.rs @@ -252,16 +252,43 @@ async fn member_to_json(member: &Membership, conn: &DbConn) -> Value { 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!({ "object": "member", "id": member.uuid, "userId": member.user_uuid, "name": name, "email": email, - "type": member.atype, + "type": membership_type, "externalId": member.external_id, "resetPasswordEnrolled": member.reset_password_key.is_some(), "status": status, + "permissions": permissions, }) }