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.

3 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()
}

Is there a version for Strapi 4?

I just implemented the v4 version of this global policy.

File located at ./src/policies/is-owner.js

// the content type must have field named "author" that is a relation N-1 to users-permission
module.exports = async (policyContext, config, {
  strapi
}) => {
  const ctx = policyContext
  if (!ctx.state.isAuthenticated)
    return false
  const api = ctx.state.route.info.apiName 
  const controller = api // assume controller same as api name
  const service = strapi.service(`api::${api}.${controller}`)
  if (!service)
    return false
  if (!ctx.params.id) return true
  const {
    results: [content]
  } = await service.find({
    filters: {
      id: {
        $eq: ctx.params.id
      },
      author: {
        id: {
          $eq: ctx.state.user.id
        }
      }
    },
    publicationState: 'preview'
  })

  return !!content
};
3 Likes

Content type controller v4 can auto assign author with this custom controller wrapper:

/src/api/article/controller/article.js

'use strict';
const {  createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::article.article', () => ({
  // Wrapping a core action (leaves core logic in place)
  async create(ctx) {
    ctx.request.body.data.author = ctx.state.user ? ctx.state.user.id : null
    try {
      return await super.create(ctx);
    } catch (err) {
      ctx.badRequest(err)
    }
  }
}));
3 Likes

I implemented a middleware to assign author of any content.

./src/middlewares/assign-owner.js

'use strict';
/**
 * `assign-owner` middleware.
 */
module.exports = (config, { strapi }) => {
  return async (ctx, next) => {
    // it is required that user must be authenticated
    ctx.request.body.data.author = ctx.state.user.id
    await next();
  };
};

We can put these config in routes of any content type:

./src/api/article/routes/article.js

'use strict';
/**
 * article router.
 */
const { createCoreRouter } = require('@strapi/strapi').factories;
module.exports = createCoreRouter('api::article.article', {
    config: {
      create: {
        middleware: ['global::assign-owner']
      },
      update: {
        policies: ['global::is-owner']
      },
      delete: {
        policies: ['global::is-owner']
      }
    }
  });

So the article controller don’t need to do anything.

4 Likes

Thanks for that … the procedure works for findOne, create, update and delete. But it does not work for a find. As soon as a document matches the author, the whole collection is delivered.

hello the middleware won’t work if the ctx is multipart form data

1 Like

thanks for updating your policy to v4. very much appreciated. everything works fine so far except “find” like mchotti mentioned

I tried to use for the “find” an controller like this:

module.exports = createCoreController('api::favorite.favorite', ({ strapi }) => ({ async find(ctx) { ctx.query = { ...ctx.query, 'populate':'*', 'Owner.id':ctx.state.user.id} return await super.find(ctx); } }));

But I get still all documents and the field with the relation is never visible by the API: All the other relations work as aspected.

Yes, you must use controller to filter results to only ownered content, like you did.

Inspired by you, I tried this code, it seems to work well:

./src/api/article/controllers/article.js

module.exports = createCoreController('api::article.article',
  ({
    strapi
  }) => ({
    async find(ctx) {
      const {
        filters
      } = ctx.query
      ctx.query = {
        ...ctx.query,
        filters: {
          ...filters,
          author: {
            id: ctx.state.user.id
          }
        }
      }
      return await super.find(ctx);
    }
  }));

(I updated the above is-owner policy code to admit no id params)

1 Like

I created github repo with all of these concepts, so you can check there all explanations:

5 Likes

thx! works for me.

Hey @msoler75 that repo is super helpful, thanks for putting that together!

@msoler75 thank you for great content here.

Can you please tell me how implement policy/middleware to protect removing images uploaded by another users? I tried to write extension for upload plugin with no luck(

const { parseMultipartData, sanitizeEntity } = require('strapi-utils');
const { data, files } = parseMultipartData(ctx);
data.user = Number(ctx.state.user.id)
let response = await strapi.service('api::restaurent.restaurent').create({
                        data,
                        files
                    })

You can extend your controller like this and hopefully it will work