Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(permissions): Restrict user and profile details based on allowed fields #196

Merged
merged 1 commit into from
Nov 12, 2024

Conversation

becevka
Copy link
Contributor

@becevka becevka commented Jul 9, 2024

Addresses #179
Please ignore commits from already squashed branch, I needed those while was waiting on PR approval.
Any suggestions how we can simplify this middleware are welcome.

if (modelName === "api::profile.profile" && action === 'findOne' && response?.data?.attributes?.user?.data?.id === user.id) {
return response;
}
return this.findAndRestrictUserDetails(response, modelName);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main algorithm here: take the response and introspect all attributes, if any attribute is a relation, we take that object and introspect its attributes. When relation points to user or profile, we replace that object with shrunk version, including only allowed fields.

if (modelName === "user" && action === 'findOne' && response?.id === user.id) {
return response;
}
if (modelName === "api::profile.profile" && action === 'findOne' && response?.data?.attributes?.user?.data?.id === user.id) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If profile was requested with additionally populated user, we can compare it with current user and return full profile, if current user is an owner of the profile. I noticed, that frontend doesn't call profiles API directly, maybe we can skip this check and always return limited version of the profile data?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the profile API is not called directly, should we remove the router?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (modelName === "user") {
return this.restrictNonWrappedItem(item, this.config.allowedUserFields);
}
if (modelName === "plugin::users-permissions.user") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When will we see this modelName instead of just user?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we call /api/users/1 we will get handler user.findOne, but let say we call api/idea-cards/1?populate=ideaOwner, that ideaOwner will be a relation type and will target not user but plugin::users-permissions.user. Other modelNames are more consistent api::profile.profile is the same in both handler and relation target type.

const idx = handler.lastIndexOf(".");
const modelName = handler.substring(0, idx);
const action = handler.substring(idx + 1);
const metadata = strapi.db.metadata;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are examples of metadata?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metadata is a Map-alike object where key is a modelName and value is MetaModel, which contains almost all the same info you can find under content-types/schema.json See https://github.com/strapi/strapi/blob/develop/packages/core/database/src/types/index.ts#L216 and https://github.com/strapi/strapi/blob/develop/packages/core/database/src/metadata/metadata.ts

return restrictedItem;
}

restrictNonWrappedItem(item, allowedFields) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Wolfgang, the code looks really good. So a short refact comment about this conditional.
You are checking item[field] twice. Is there a way to check it just once and then check field === "profile" ?

@j-peace
Copy link
Contributor

j-peace commented Sep 18, 2024

@chungthuang just to I keep in touch, why isn't this pr merged yet?

@dbradham
Copy link
Contributor

@nikita-pardeshi-github @chungthuang @tiagopazhs @oriyomibadmus is anyone here familiar with the file that has a conflict? I'd like to get this merged, as Yacine has fixed some breaking changes on the frontend side, so we can now merge and deploy this change.

@dbradham
Copy link
Contributor

@chungthuang just to I keep in touch, why isn't this pr merged yet?

this was going to break some frontend interfaces that exposed PII (personally identifiable information) so we were waiting until those got updated.

@chungthuang chungthuang force-pushed the becevka/179-disable-user-details branch from efdfe9e to 1b51b68 Compare November 11, 2024 01:36
await next();
try {
// Only need to filter if there is a response body
if (ctx.request.method === "GET" && ctx.request.url.includes("/api/") && ctx.response.body) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@becevka I added a check for non empty ctx.response.body, otherwise https://github.com/dev-launchers/strapiv4/pull/196/files#diff-020f47be16701da9ac1d16017015d4d6d586e314e61850aa215c4c801450563eR27 will set it to an empty body and change the status code from 404 to 204.

@chungthuang chungthuang force-pushed the becevka/179-disable-user-details branch from 1b51b68 to 75fd97c Compare November 12, 2024 01:16
Copy link

Code Coverage

Package Line Rate Branch Rate Health
src 65% 50%
src.api.applicant.content-types.applicant 25% 0%
src.api.applicant.controllers 100% 100%
src.api.applicant.routes 100% 100%
src.api.applicant.services 100% 100%
src.api.category.controllers 100% 100%
src.api.category.routes 100% 100%
src.api.category.services 100% 100%
src.api.comment.content-types.comment 100% 100%
src.api.comment.controllers 100% 100%
src.api.comment.routes 100% 100%
src.api.comment.services 100% 100%
src.api.dl-tal-community.controllers 100% 100%
src.api.dl-tal-community.routes 100% 100%
src.api.dl-tal-community.services 100% 100%
src.api.event.content-types.event 88% 100%
src.api.event.controllers 100% 58%
src.api.event.routes 100% 100%
src.api.event.services 100% 100%
src.api.github-repo.controllers 20% 100%
src.api.github-repo.routes 100% 100%
src.api.github-repo.services 15% 0%
src.api.google-meet.content-types.google-meet 33% 100%
src.api.google-meet.controllers 100% 100%
src.api.google-meet.routes 100% 100%
src.api.google-meet.services 100% 100%
src.api.idea-card.content-types.idea-card 100% 100%
src.api.idea-card.controllers 100% 100%
src.api.idea-card.routes 100% 100%
src.api.idea-card.services 100% 100%
src.api.interest.controllers 100% 100%
src.api.interest.routes 100% 100%
src.api.interest.services 100% 100%
src.api.like.controllers 100% 100%
src.api.like.routes 100% 100%
src.api.like.services 100% 100%
src.api.newsletter.controllers 100% 100%
src.api.newsletter.routes 100% 100%
src.api.newsletter.services 100% 100%
src.api.notification.controllers 100% 100%
src.api.notification.policies 100% 88%
src.api.notification.routes 100% 100%
src.api.notification.services 100% 100%
src.api.opportunity.controllers 100% 100%
src.api.opportunity.routes 100% 100%
src.api.opportunity.services 100% 100%
src.api.profile.content-types.profile 12% 0%
src.api.profile.controllers 100% 100%
src.api.profile.routes 100% 100%
src.api.profile.services 100% 100%
src.api.project.controllers 100% 100%
src.api.project.routes 100% 100%
src.api.project.services 100% 100%
src.api.save-idea.controllers 100% 100%
src.api.save-idea.routes 100% 100%
src.api.save-idea.services 100% 100%
src.api.sendmail.controllers 100% 100%
src.api.sendmail.routes 100% 100%
src.api.sendmail.services 67% 100%
src.api.subscription.controllers 100% 100%
src.api.subscription.routes 100% 100%
src.api.subscription.services 100% 100%
src.api.team-membership.controllers 100% 100%
src.api.team-membership.routes 100% 100%
src.api.team-membership.services 100% 100%
src.api.user-recommendations.controllers 10% 0%
src.api.user-recommendations.routes 100% 100%
src.extensions.users-permissions 100% 100%
src.extensions.users-permissions.controllers 33% 100%
src.extensions.users-permissions.routes.content-api 100% 100%
src.extensions.users-permissions.server.bootstrap 100% 62%
src.extensions.users-permissions.server.middlewares 70% 59%
src.extensions.users-permissions.server.services 11% 0%
src.migrators 60% 50%
Summary 57% (295 / 519) 35% (48 / 139)

@chungthuang chungthuang merged commit f40f662 into main Nov 12, 2024
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants