Use document data in Access Control function

default discord avatar
Twoxiclast year
25

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?

  • default discord avatar
    TheDuncolast year

    The

    req

    object should also contain the

    payload

    object which you can use to query for any other documents!

  • default discord avatar
    Twoxiclast year

    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

    )

  • default discord avatar
    TheDuncolast year

    I think you would just need to do this with access functions in each of the

    individual collections

    that 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 to

    you 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!

  • default discord avatar
    Twoxiclast year

    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.

  • default discord avatar
    Jarrodlast year

    @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?

  • default discord avatar
    Twoxiclast year

    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-duckying

    myself 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!

  • default discord avatar
    Jarrodlast year

    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?

  • default discord avatar
    Twoxiclast year

    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

    analyses

    and

    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]
      },
  • default discord avatar
    Jarrodlast year

    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)

  • default discord avatar
    Twoxiclast year

    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]
  • default discord avatar
    Jarrodlast year

    you are logging these by hitting the API? or by loading the admin panel?

  • default discord avatar
    Twoxiclast year

    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;
        })),
      };
  • default discord avatar
    Jarrodlast year

    yep yep

  • default discord avatar
    Twoxiclast year

    I could see usecases for a

    afterOperation

    (this one!). Do you think that would be a welcome addition? I could make a PR.

  • default discord avatar
    Jarrodlast year

    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

  • default discord avatar
    Twoxiclast year

    Cool. Let me know if I can do some work on it.

Star on GitHub

Star

Chat on Discord

Discord

online

Can't find what you're looking for?

Get help straight from the Payload team with an Enterprise License.