My IsOwner global policy version

Hi to all. I would like to tell my own version of IsOwner (global) Policy.

To get it work, the collection or content type must have a field named “author” that is a relation N-1 to users-permission

/config/policies/is-owner.js:

// the content type must have field named "author" that is a relation N-1 to users-permission

module.exports = async (ctx, next) => {
  // must be authenticated user
  if (!ctx.state.user) {
    return ctx.unauthorized(`Forbidden`)
  }
  const url = ctx.request.url
  // I get the collection or content type from url. Sure there is a better way
  const parts = url.split('/')
  const last = parts[parts.length - 1]
  const collection = parts[parts.length - (last.match(/^\d+$/) ? 2 : 1)]
  if (!strapi.services[collection])
    return ctx.unauthorized(`Collection ${collection} not found`)
  const [content] = await strapi.services[collection].find({
    id: ctx.params.id,
    'author.id': ctx.state.user.id
  })
  if (!content) {
    return ctx.unauthorized(`Only the author can do this`)
  }
  return await next()
}

Then, we can use the global policy in routes.json for update or delete a content. For example:

/api/articles/config/routes.json:

...
      {
      "method": "PUT",
      "path": "/articles/:id",
      "handler": "articles.update",
      "config": {
        "policies": ["global::is-owner"]
      },
      {
      "method": "DELETE",
      "path": "/articles/:id",
      "handler": "articles.delete",
      "config": {
        "policies": ["global::is-owner"]
      }
  ...

We also can use a custom controller that binds automatically author with the content when is created.

/api/articles/controllers/articles.js:

const { parseMultipartData, sanitizeEntity } = require('strapi-utils');

module.exports = {
// https://strapi.io/documentation/developer-docs/latest/development/backend-customization.html#controllers
      async create(ctx) {
        let entity;
        if (ctx.is('multipart')) {
            const { data, files } = parseMultipartData(ctx);
            data.author = ctx.state.user.id;
            entity = await strapi.services.articles.create(data, { files });
        } else {
            ctx.request.body.author = ctx.state.user.id;
            entity = await strapi.services.articles.create(ctx.request.body);
        }
        return sanitizeEntity(entity, { model: strapi.models.articles});
      }
   }

If you have more than a content type with author ownerness policy, you must replicate last two steps in each content type (routes.json and custom ‘create’ controller).

If anyone knows another way to get same or better result to get global IsOwner policy to work, please tell.

2 Likes

There is indeed a better way :slight_smile:

1 Like

Thanks, I modified the original post to reflect this better code :smiley:

/config/policies/is-owner.js:

// the content type must have field named "author" that is a relation N-1 to users-permission

module.exports = async (ctx, next) => {
  // must be authenticated user
  if (!ctx.state.user) {
    return ctx.unauthorized(`Forbidden`)
  }
  const collection = ctx.request.route.controller
  if (!strapi.services[collection])
    return ctx.unauthorized(`Collection ${collection} not found`)
  const [content] = await strapi.services[collection].find({
    id: ctx.params.id,
    'author.id': ctx.state.user.id
  })
  if (!content) {
    return ctx.unauthorized(`Only the author can do this`)
  }
  return await next()
}

If it were me I would return a 404 or 400 here and not a 401 as it’s not because they aren’t authorized but what they are trying to do doesn’t exist.

Your also sending a bit of mixed message here:

401 is unauthorized, 403 is forbidden:

And you have all these method available to you (it’s where those return ctx.* come from):

Thanks.

I was searching in documentation about the other response errors but I never found and I used ‘unauthorized’ as generic error.

So the code could be:

/config/policies/is-owner.js:

// the content type must have field named "author" that is a relation N-1 to users-permission

module.exports = async (ctx, next) => {
  // must be authenticated user
  if (!ctx.state.user) {
    return ctx.unauthorized()
  }
  const collection = ctx.request.route.controller
  if (!strapi.services[collection])
    return ctx.notFound(`Collection ${collection} not found`)
  const [content] = await strapi.services[collection].find({
    id: ctx.params.id,
    'author.id': ctx.state.user.id
  })
  if (!content) {
    return ctx.forbidden(`Only the author can do this`)
  }
  return await next()
}