How to use document data in the access control function?
I've got a collection that I use to store recently added entries to a couple other collections. It's called
Historyand looks like this:
const History: CollectionConfig = {
slug: 'history',
admin: {
useAsTitle: 'id'
},
access: {
read: isAdminOrHasAccessToResourceInHistory,
create: () => true,
update: () => true,
delete: () => true
},
fields: [
{
name: 'event',
type: 'relationship',
relationTo: ['analyses', 'orthophotos', 'locations', 'species'],
hasMany: false,
required: true
}
],
timestamps: true
};
export default History;The
accessFunction(not working) looks like this:
export const isAdminOrHasAccessToResourceInHistory: Access<any, User> = ({
req: { user }
}) => {
if (!user) {
return false;
}
if (user.roles.includes('admin')) return true;
if (user.roles.includes('user') && user[collectionSlug]) {
return {
['id']: {
['in']: user[collectionSlug]
}
};
}
};I want to make it so that users only retrieve "history" of documents that they have reading-access to. So I've got to figure out the
collectionSlug(analyses, orthophotos, locations, species)
based on the document, however I do not see a way to access this document. Is this possible and if not, how do you think I can build this?
The
reqobject should also contain the
payloadobject which you can use to query for any other documents!
I don't think this will help me since I need to know the ID of the element first. According to the docs (
https://payloadcms.com/docs/access-control/collections#read) this is only available on findByID operations, and not on "find all" operations
(
id of document requested, if within findByID)
I think you would just need to do this with access functions in each of the
individual collectionsthat are included in that relationship field (analyses, orthophotos, locations, species). The
access: { read: ...}in the
Historycollection will control if they can read the ENTIRE
Historycollection, not the individual related ones. It seems to me like
if you're trying to prevent them from viewing items in the history that they don't have access toyou need to provide access functions on the individual collections for analyses, orthophotos, locations, and species.
If I'm misunderstanding what you're trying to do I'll need a better explanation of the whole setup and what exactly you're trying to do. Hope this helps!
You're right, that is how I'm doing it now and that does sort of work. However in that case the result of the
/history?limit=2query would be this:
{
"docs": [
{
"id": "644b82d6fedb175d3f3347d9",
"event": {
"relationTo": "analyses",
"value": {
...documentWhichUserHasAccessTo
}
},
"createdAt": "2023-04-28T08:24:54.871Z",
"updatedAt": "2023-04-28T08:24:54.871Z"
},
{
"id": "644b8299fedb175d3f3347b8",
"event": {
"relationTo": "analyses",
"value": "644b8292fedb175d3f334713"
},
"createdAt": "2023-04-28T08:23:53.049Z",
"updatedAt": "2023-04-28T08:23:53.049Z"
}
],
"totalDocs": 55,
"limit": 2,
"totalPages": 14,
"page": 1,
"pagingCounter": 1,
"hasPrevPage": false,
"hasNextPage": true,
"prevPage": null,
"nextPage": 2
}However, I want to show the latest 2 relevant (cases in which the user actually has access to the "event"
historyitems.
So ideally I'd make an
accessfunction on the
historycollection.
I believe you could use an afterRead hook to filter out the docs that the user does not have access to. If the user does not have access to the doc it should only return the string id, else it should return the entire doc. Can you give that a shot and see if that does what you need?
I've tried that and that does work partially. The only problem I see is I don't want to fetch the entire history (collection) every time.
Say I just want the 4 latest items in the collection that the user has access to. I'd use
?limit=4which could give me no records (if I don't have access to the latest 4 history-items.).
While I'm sort of
rubber-duckyingmyself I get the thought of stripping the
limitquery in a
beforeReadhook and manually slicing the filtered docs in an
afterRead. I'll try that later!
I am trying to align myself with your issue.
- You have a history collection, with access control.
- You query the history collection with limit=4
- You do not get docs back if your user does not have access to the first 4 docs?
So I have a
historycollection which stores documents
("events")which can be of multiple types(e.g.
analyses,
orthophoto). Users have access to specific
analysesand
orthophotos, but not to all of them. The access control for this is setup and working.
The userstory I have for this usecase is
As a user I want to retrieve the 2 (or insert any other number) latest documents in the history collection which references a document which the user has access to.At the moment, querying history like
/history?limit=2gives back:
"docs": [
{
"id": "644b82d6fedb175d3f3347d9",
"event": {
"relationTo": "analyses",
"value": {
...documentWhichUserHasAccessTo
}
},
"createdAt": "2023-04-28T08:24:54.871Z",
"updatedAt": "2023-04-28T08:24:54.871Z"
},
{
"id": "644b8299fedb175d3f3347b8",
"event": {
"relationTo": "analyses",
"value": "644b8292fedb175d3f334713"
},
"createdAt": "2023-04-28T08:23:53.049Z",
"updatedAt": "2023-04-28T08:23:53.049Z"
}
],In which
docs[0]the user has access to and
docs[1]the user doesn't. So that doesn't give me back
2 (or insert any other number) latest documents in the history collection which references a document which the user has access to.To solve this I would need to write an
accesshook on the
historycollection itself. But I'd need to access the individual document in this function to check of which type (analyses, orthophoto) it is before I can check if the user has access to it.
One step closer! I've removed the
limitquery on
beforeOperationlike:
export const removeLimitFromArgs: CollectionBeforeOperationHook = async ({
args,
operation
}) => {
if (operation !== 'read') {
return args;
}
if (args.limit) {
args._limit = args.limit;
args.limit = 999999; // Remove limit for now.
}
return args;
};This way payload grabs all
historydocs. Next step is filtering the docs and apply the limit. One problem: There is no hook in which you can access the list of retrieved documents, like:
afterOperation. Why isn't there one? How to fix it without this hook?
So the hooks would become like this:
hooks: {
beforeOperation: [removeLimitFromArgs],
afterOperation: [filterUnaccessibleEvents, addPreviouslyRemovedLimit]
},I think you are looking for the beforeRead and afterRead hooks:
https://payloadcms.com/docs/hooks/collections#afterreadalso, you can set the limit to 0 and that will retrieve all docs. (instead of 99999)
afterRead seems to fire for every document which is read, I've added this function to the
afterReadhooks.
export const filterUnaccessibleEvent: CollectionAfterReadHook = async ({
doc // full document data
}) => {
console.log('afterRead');
return doc;
};And one function to
beforeOperationwhich does
console.log('beforeOperation'). This results in the following output:
beforeOperation
afterRead
afterRead
afterRead
afterRead
afterRead
afterRead
afterRead
afterRead
afterRead
afterRead
afterRead
[many more here]you are logging these by hitting the API? or by loading the admin panel?
Hitting the API
I've looked in the source and it is clear that afterRead runs for every document:
https://github.com/payloadcms/payload/blob/master/src/collections/operations/find.ts result = {
...result,
docs: await Promise.all(result.docs.map(async (doc) => {
let docRef = doc;
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook;
docRef = await hook({ req, query, doc: docRef, findMany: true }) || doc;
}, Promise.resolve());
return docRef;
})),
};yep yep
I could see usecases for a
afterOperation(this one!). Do you think that would be a welcome addition? I could make a PR.
I am following
I think what you are saying makes sense
and sounds like a good feature
I am going to link to this convo in the
#1102950643259424828channel
Cool. Let me know if I can do some work on it.
Star
Discord
online
Get dedicated engineering support directly from the Payload team.