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
History
and 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
req
object should also contain the
payload
object 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
History
collection will control if they can read the ENTIRE
History
collection, 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=2
query 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"
history
items.
So ideally I'd make an
access
function on the
history
collection.
@Twoxic 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=4
which 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
limit
query in a
beforeRead
hook 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
history
collection 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=2
gives 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
access
hook on the
history
collection 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
limit
query on
beforeOperation
like:
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
history
docs. 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#afterread
also, 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
afterRead
hooks.
export const filterUnaccessibleEvent: CollectionAfterReadHook = async ({
doc // full document data
}) => {
console.log('afterRead');
return doc;
};
And one function to
beforeOperation
which 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 #core-dev channel
Cool. Let me know if I can do some work on it.
Star
Discord
online
Get help straight from the Payload team with an Enterprise License.