Rest API should not return id
s for read denied records
#9524
Replies: 4 comments 7 replies
-
For anyone else running into this issue, here is function and type that you can use with the generated types to remove numbers from the array responses. This will remove all numbers from all array fields, so it should not be used if you know your query depth will not be deep enough, or you expect your response to include an array of numbers. type RemoveNumberFromMixedArrays<T> = T extends (number | infer U)[] // Check if T is an array of number or some other type
? U[] // Transform it to an array of the other type, removing `number`
: T extends object // If T is an object, recurse into its fields
? { [K in keyof T]: RemoveNumberFromMixedArrays<T[K]> }
: T; // For all other types, leave them as-is
function removeNumbersFromMixedArrays<T>(
value: T,
): RemoveNumberFromMixedArrays<T> {
// recursively remove numbers from arrays
if (Array.isArray(value)) {
return value.filter(
(v) => typeof v !== "number",
) as RemoveNumberFromMixedArrays<T>;
}
// recursively filter the values of an object
if (typeof value === "object" && value !== null) {
return Object.fromEntries(
Object.entries(value).map(([key, value]) => [
key,
removeNumbersFromMixedArrays(value),
]),
) as RemoveNumberFromMixedArrays<T>;
}
return value as RemoveNumberFromMixedArrays<T>;
} an example usage for me, using tanstack query, with a global useSuspenseQuery({
queryKey: ["homepage"],
queryFn: async () => {
const res = await fetch(
`${process.env.PAYLOAD_URL}/api/globals/homepage?depth=1`,
);
if (res.ok) {
const body = (await res.json()) as Homepage;
return removeNumbersFromMixedArrays(body);
} else {
throw (await res.json()) as { errors: { message: string }[] };
}
},
}); |
Beta Was this translation helpful? Give feedback.
-
I appreciate the code examples, but you are still making the fetch and removing the data on the client side, so this is not really any more secure if that is the goal. A little context for why Payload works this the way it does, serving the You can always add read access to the relationship field itself in order to enforce that relationship IDs should not come through by config. That would be the payload preferred way to do this to not result in any data loss when saving content. |
Beta Was this translation helpful? Give feedback.
-
Here is a better type for recursively removing export type Deep<T> = unknown extends T
? T // don't modify T if it's any or unknown; doing so will cause infinite recursion
: number extends T // if T can be a number
? Exclude<T, number | undefined | null> extends never // and can only be a number, null, or undefined
? T // Then don't modify it. This number does not represent an ID from payload.
: Deep<Exclude<T, number>> // Otherwise, remove the number (ID) option and recurse
: T extends object // if T can't be a number and is an object,
? { [K in keyof T]: Deep<T[K]> } // recurse
: T extends (infer U)[] // if T can't be a number and is an array
? Deep<U>[] // recurse
: T; // otherwise, return T Usage (assuming HomePage has 1 nested level of relationships only. API depth would have to be set explicitly for other queries): import { useQuery } from "@tanstack/react-query";
import { HomePage } from "path/to/payload-types";
import type { Deep } from "path/to/deep";
/**
* Gets the homepage global from the API
*/
export default function useHomepage() {
return useQuery<Deep<HomePage>>({
queryKey: ["homepage"],
queryFn: async () => {
const res = await fetch(
`${process.env.PAYLOAD_URL}/api/globals/homePage`,
);
if (res.ok) {
return await res.json();
} else {
throw {
...(await res.json()),
code: res.status,
};
}
},
});
} |
Beta Was this translation helpful? Give feedback.
-
I agree with @6TELOIV's proposal (not the original, which would omit data) to return either an object containing the ID; or the full document. I'd like to shed some light on the current approaches I have seen, and some considerations in consistency. Type narrowingA union type between a primitive and an object is quite bad, it is a hassle to have to resolve. This makes UI code more difficult to read and business logic more prone to errors. I have seen some wonky checks and messy conditional trees arise. This is no longer a problem when implementing the proposal: a simple null check would suffice, which can easily be chained in-line and results in better readability. API interchangeabilityConsistency in the API responses across the various solutions that are supported would be a big advantage. I can think of some more minor arguments, but a concrete case I have experienced is perhaps the most useful. With the release of Payload 3, it has become incredibly easy to fetch data server side through the Payload Local API. However, to reduce round trip delays in client heavy applications, it becomes interesting to also fetch client side, but only to revalidate. A NextJS example:
Determining partial documentOf course, this proposal has downsides. Say you wish to render a card for every result received, but not when access is denied. When filtering these results, it is no longer good enough to check if the result is of the object type. One way would be to check some arbitrary, hopefully always present, property like Observed approaches
Suggested approachImplementing the proposal ;). But I can see that this is a major change anyways, so for now I am personally doing it using some utility functions (assuming uuid, not numeric): // Overload to preserve nullability
export function entityId(entity: string | { id: string }): string
export function entityId(entity?: string | { id: string } | null): string | null
export function entityId(entity?: string | { id: string } | null): string | null {
if (entity == null) return null
return typeof entity === 'string' ? entity : entity.id
}
export function relation<T extends { id: string }>(relation?: T | string | null) {
if (relation == null || typeof relation === 'string') return null
return relation as T
} Which would then be used as follows: const post: Post = await fetchPost(postId);
const groupId: string = entityId(post.group); // or null, if group is optional
const group: Group | null = relation(post.group); |
Beta Was this translation helpful? Give feedback.
-
When using the Rest API, relationship fields can return a mix of numbers and objects when the field contains records to which the user doesn't have read access.
To me, most of the time there is no use for an id of a record that the user cannot access (exceptions exist). Additionally, this could be a leak of sensitive data, depending on what the ID is used for.
All in all, this behavior results in
I would propose that the Rest API should return only the fields to which the user has read access by default, with an option in configuration to opt-in to the current behavior (possibly
allowUnreadableIds
on the relationship field as a config option would cover it?)Beta Was this translation helpful? Give feedback.
All reactions