strapi - restrict user to fetch only data related to him - koa

Usually, a logged-in user gets all entries of a Content Type.
I created a "snippets" content type (_id,name,content,users<<->>snippets)
<<->> means "has and belongs to many" relation.
I created some test users and make a request:
curl -H 'Authorization: Bearer eyJ...' http://localhost:1337/snippets/
Main Problem: an authenticated user should only see the entries assigned to him. Instead, a logged-in user gets all snippets, which is bad.
How is it possible to modify the fetchAll(ctx.query); query to take that into account so it does something like fetchAll(ctx.state.user.id); at the /-route->find-method ?
The basic find method is here:
find: async (ctx) => {
if (ctx.query._q) {
return strapi.services.snippet.search(ctx.query);
} else {
return strapi.services.snippet.fetchAll(ctx.query);
}
},
Sub-Question: Does strapi even know which user is logged in when I do Bearer-Token Authentication ?

You could set up a /snippets/me route under the snippets config.
That route could call the Snippets.me controller method which would check for the user then query snippets based on the user.
So in api/snippet/config/routes.json there would be something like :
{
"method": "GET",
"path": "/snippets/me",
"handler": "Snippets.me",
"config": {
"policies": []
}
},
Then in the controller (api/snippet/controllers/Snippet.js), you could do something like:
me: async (ctx) => {
const user = ctx.state.user;
if (!user) {
return ctx.badRequest(null, [{ messages: [{ id: 'No authorization header was found' }] }]);
}
const data = await strapi.services.snippet.fetch({user:user.id});
if(!data){
return ctx.notFound();
}
ctx.send(data);
},
Then you would give authenticated users permissions for the me route not for the overall snippets route.

The above is correct, except with newer versions of strapi. Use find and not fetch :)
const data = await strapi.services.snippet.find({ user: user.id });
Strapi v3.0.0-beta.20

A possibility would be to extend the query used by find and findOne in the controllers with a restriction regarding the logged in user. In this case you might also want to adapt the count endpoint to be consistent.
This would result in:
withOwnerQuery: (ctx, ownerPath) => {
const user = ctx.state.user;
if (!user) {
ctx.badRequest(null, [
{ messages: [{ id: "No authorization header was found" }] },
]);
return null;
}
return { ...ctx.query, [ownerPath]: user.id };
};
find: async (ctx) => {
ctx.query = withOwnerQuery(ctx, "owner.id");
if (ctx.query._q) {
return strapi.services.snippet.search(ctx.query);
} else {
return strapi.services.snippet.fetchAll(ctx.query);
}
},
// analogous for for findOne
Depending on your usage of controllers and services you could achieve the same thing via adapting the service methods.
This kind of solution would work with the GraphQL plugin.

Related

Strapi returns 404 for custom route only when deployed to Heroku

I have created a custom route in Strapi v4 called "user-screens". Locally I hit it with my FE code and it returns some data as expected. However when I deploy it to Heroku and attempt to access the endpoint with code also deployed to Heroku it returns a 404. I've tailed the Heroku logs and can see that the endpoint is hit on the server side, but the logs don't give anymore info other than it returned a 404.
I am doing other non custom route api calls and these all work fine on Heroku. I am able to auth, save the token, and hit the api with the JWT token and all other endpoints return data. This is only happening on my custom route when deployed to Heroku. I've set up cors with the appropriate origins, and I am wondering if I need to add something to my policies and middlewares in the custom route. I have verified the permissions and verified the route is accessible to authenticated users in the Strapi admin.
Here is my route:
module.exports = {
routes: [
{
method: "GET",
path: "/user-screens",
handler: "user-screens.getUserScreens",
config: {
policies: [],
middlewares: [],
},
},
],
};
And my controller:
"use strict";
/**
* A set of functions called "actions" for `user-screens`
*/
module.exports = {
getUserScreens: async (ctx) => {
const user = ctx.state.user;
if (!user) {
return ctx.badRequest(null, [
{ messages: [{ id: "No authorization header was found" }] },
]);
}
strapi.entityService
.findMany("api::screen.screen", {
owner: user.id,
populate: ["image"],
})
.then((result) => {
ctx.send(result);
});
},
};
For anyone facing this, the answer was to change how I returned the ctx response from a 'send' to a 'return' from the controller method. I am not sure why this works locally and not on Heroku, but this fixes it:
New controller code:
module.exports = {
getUserScreens: async (ctx) => {
const user = ctx.state.user;
if (!user) {
return ctx.badRequest(null, [
{ messages: [{ id: "No authorization header was found" }] },
]);
}
return strapi.entityService
.findMany("api::screen.screen", {
owner: user.id,
populate: ["image"],
})
.then((result) => {
return result;
})
.catch((error) => {
return error;
});
},
};

Strapi update username from custom controller

I am trying to create a custom controller to update the user profile.
I created the routing file and the corresponding controller.
Routing file: server/src/api/profile/routes/profile.js
module.exports = {
routes: [
{
method: 'GET',
path: '/profile',
handler: 'profile.getProfile',
},
{
method: 'PUT',
path: '/profile',
handler: 'profile.updateProfile',
},
]
}
Controller: src/api/profile/controllers/profile.js
async updateProfile(ctx) {
try {
const { id } = ctx.state?.user;
const user = strapi.query('admin::user').update({
where: { id },
data: {
username: "testUsername"
}
})
ctx.body = "User updated"
} catch(error) {
ctx.badRequest("Something went wrong", { error })
}
},
The above code returns "User updated", but the username does not update. I am executing the PUT call with a correct Bearer authorisation token and the user permissions for that user are set to enable "updateProfile".
Oddly enough, the same code, when changed to update a different API item, works perfectly fine:
async updateArticle(ctx) {
try {
const { id } = ctx.state?.user;
const article = strapi.query('api::article.article').update({
where: { author: id },
data: {
title: "New title"
}
})
ctx.body = article
} catch(error) {
ctx.badRequest("Something went wrong", { error })
}
},
I am also confused by different syntaxes appearing in the official Strapi documentation, for example some docs mention:
strapi.query('admin::user').update({ id }, data)
But in other places in the documentation its:
strapi.plugins['users-permissions'].services.user.update({ id });
And then elsewhere:
strapi.query('user', 'users-permissions').update(params, values);
Another question is: do I need to sanitise the input / output in any way? If yes, how? Importing sanitizeEntity from "Strapi-utils" doesn't work, but it's mentioned in several places on the internet.
Additionally, I cannot find a list of all ctx properties. Where can I read what is the difference between ctx.body and ctx.send?
The lack of good documentation is really hindering my development. Any help with this will be greatly appreciated.

Supabase middleware for business logic

New to the supabase universe. Simple questions
Is there a way to setup middleware in supabase?. Can Supabase fulfill this?
Add business logic middleware when creating an entity
Add special validations (ie: validate a product has stock before purchase)
Restrict information depending on user roles (ie: admins can read additional entity attributes, but not common users).
Thanks
this is now solvable using Supabase's Edge Functions: https://supabase.com/docs/guides/functions
There is an example here to solve "Restrict information depending on user roles" using Postgres' Row Level Security:
https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts
/ Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.
import { serve } from 'https://deno.land/std#0.131.0/http/server.ts'
import { supabaseClient } from '../_shared/supabaseClient.ts'
import { corsHeaders } from '../_shared/cors.ts'
console.log(`Function "select-from-table-with-auth-rls" up and running!`)
serve(async (req: Request) => {
// This is needed if you're planning to invoke your function from a browser.
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
// Set the Auth context of the user that called the function.
// This way your row-level-security (RLS) policies are applied.
supabaseClient.auth.setAuth(req.headers.get('Authorization')!.replace('Bearer ', ''))
const { data, error } = await supabaseClient.from('users').select('*')
console.log({ data, error })
return new Response(JSON.stringify({ data, error }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 200,
})
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
})
}
})

Apollo cache redirect for field with no arguments

I have a login mutation tha looks similiar to this:
mutation login($password: String!, $email: String) {
login(password: $password, email: $email) {
jwt
user {
account {
id
email
}
}
}
}
On the other hand, I have a query for getting the account details. The backend verifies which user it is by means of the JWT token that is send with the request, so no need for sending the account id as an argument.
query GetUser {
user {
account {
id
email
}
}
}
The issue I am facing is now: Apollo is making a network request every time as GetUser has no argument. I would prever to query from cache first. So I thought, I could redirect as described here.
First, I was facing the issue that user field does not return an id directly so I have defined a type policy as such:
const typePolicies: TypePolicies = {
User: {
keyFields: ["account", ["id"]],
},
}
So regarding the redirect I have add the following to the type policy:
const typePolicies: TypePolicies = {
User: {
keyFields: ["account", ["id"]],
},
Query: {
fields: {
user(_, { toReference }) {
return toReference({
__typename: "User",
account: {
id: "1234",
},
})
},
},
},
}
This works, however there is a fixed id of course. Is there any way to solve this issue by always redirecting to the user object that was queried during login?
Or is it better to add the id as argument to the GetUser query?
I have solve this by means of the readField function:
Query: {
fields: {
user(_, { toReference, readField }) {
return readField({
fieldName: "user",
from: toReference({
__typename: "LoginMutationResponse",
errors: null,
}),
})
},
},
},
What happens if the reference cannot be found? It seems like apollo is not making a request then. Is that right? How can this be solved?
I have noticed this in my tests as the login query is not executed.

How to limit User access to a User API to it's own :ID only?

I have created an API that contains many pieces of information related to each "User". By default "User" only have a model containing e-mail, password, username and lang.
So I have created a "UserInfo" API/Model that contains firstName, lastName, age, address and so on.
I want to give non-admin users access to it's own "UserInfo" data, which means the user would be able to access the api using localhost/userinfo/:id. But each user will only get authorized to call it's own ID and not other users IDs.
How can I create such a "policy"/ restriction for the final users "role"?
Hope I have been clear enough. My questions is probably quite trivial.
You will have to play with role's permissions or policies.
I think this issue is really close to what you want: https://github.com/strapi/strapi/issues/624
In strapi after the authentication the ctx.state.user.id is assigned with the user entity so you can use it to validate.
you can either create a policy
module.exports = async (ctx, next) => {
const { params } = ctx;
if (ctx.state.user.id === params.id) {
return next();
} else {
return ctx.badRequest(
'Not Authorized'
);
}
};
and set policy in the route file
{
"method": "GET",
"path": "/userinfo/:id",
"handler": "userinfo.findOne",
"config": {
"policies": [
"admin::hasPermissions",
"isOwner"]
}
}
Or custom write the findOne method
async findOne(ctx) {
const { query, params } = ctx;
if (ctx.state.user.id === params.id) {
const entity = await service.findOne({ ...query, id: params.id });
}
else {
return ctx.badRequest(
'Not Authorized'
)
}
return sanitize(entity);
},

Resources