Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 42 additions & 43 deletions src/attachments/attachments.v4.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,27 +109,18 @@ export class AttachmentsV4Controller {
this.generateAttachmentInstanceForPermissions(attachment);

const user: JWTUser = request.user as JWTUser;
const ability = this.caslAbilityFactory.attachmentInstanceAccess(user);
const ability = this.caslAbilityFactory.attachmentAccess(user);

try {
switch (group) {
case Action.AttachmentCreateEndpoint:
return ability.can(
Action.AttachmentCreateInstance,
attachmentInstance,
);
case Action.AttachmentReadEndpoint:
return ability.can(Action.AttachmentReadInstance, attachmentInstance);
case Action.AttachmentUpdateEndpoint:
return ability.can(
Action.AttachmentUpdateInstance,
attachmentInstance,
);
case Action.AttachmentDeleteEndpoint:
return ability.can(
Action.AttachmentDeleteInstance,
attachmentInstance,
);
case Action.AttachmentCreate:
return ability.can(Action.AttachmentCreate, attachmentInstance);
case Action.AttachmentRead:
return ability.can(Action.AttachmentRead, attachmentInstance);
case Action.AttachmentUpdate:
return ability.can(Action.AttachmentUpdate, attachmentInstance);
case Action.AttachmentDelete:
return ability.can(Action.AttachmentDelete, attachmentInstance);
default:
throw new InternalServerErrorException(
"Permission for the action is not specified",
Expand All @@ -144,29 +135,37 @@ export class AttachmentsV4Controller {
user: JWTUser,
filter: IAttachmentFiltersV4<AttachmentDocument, IAttachmentFields>,
): IAttachmentFiltersV4<AttachmentDocument, IAttachmentFields> {
if (!filter.where) {
filter.where = {};
}
const ability = this.caslAbilityFactory.attachmentInstanceAccess(user);
const canAccessAny = ability.can(Action.AccessAny, Attachment);
const ability = this.caslAbilityFactory.attachmentAccess(user);
const canViewAny = ability.can(Action.AccessAny, Attachment);
const canView = ability.can(Action.AttachmentRead, Attachment);

if (!canAccessAny) {
filter.where = filter.where ?? {};

if (!user) {
if (filter.where["$and"]) {
filter.where["$and"].push({
isPublished: true,
});
} else {
filter.where["$and"] = [{ isPublished: true }];
}
} else if (!canViewAny && canView) {
if (filter.where["$and"]) {
filter.where["$and"].push({
$or: [
{ ownerGroup: { $in: user?.currentGroups || [] } },
{ accessGroups: { $in: user?.currentGroups || [] } },
{ sharedWith: { $in: [user?.email || ""] } },
{ ownerGroup: { $in: user.currentGroups } },
{ accessGroups: { $in: user.currentGroups } },
{ sharedWith: { $in: [user.email] } },
{ isPublished: true },
],
});
} else {
filter.where["$and"] = [
{
$or: [
{ ownerGroup: { $in: user?.currentGroups || [] } },
{ accessGroups: { $in: user?.currentGroups || [] } },
{ sharedWith: { $in: [user?.email || ""] } },
{ ownerGroup: { $in: user.currentGroups } },
{ accessGroups: { $in: user.currentGroups } },
{ sharedWith: { $in: [user.email] } },
{ isPublished: true },
],
},
Expand Down Expand Up @@ -215,7 +214,7 @@ export class AttachmentsV4Controller {
// GET /attachments
@UseGuards(PoliciesGuard)
@CheckPolicies("attachments", (ability: AppAbility) =>
ability.can(Action.AttachmentReadEndpoint, Attachment),
ability.can(Action.AttachmentRead, Attachment),
Comment on lines 216 to +217

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Using AttachmentRead with a class subject while the rule has instance-level conditions may cause unexpected guard behavior for unauthenticated requests.

In attachmentAccess, unauthenticated users are granted can(Action.AttachmentRead, Attachment, { isPublished: true }), but CheckPolicies is now calling ability.can(Action.AttachmentRead, Attachment) with the class as subject. Depending on how PoliciesGuard/CASL treat class subjects, the isPublished condition may never be applied, which could either unintentionally block previously allowed unauthenticated reads or, worse, bypass the condition and allow more than intended. To avoid changing behavior, either:

  • Add an unconditional can(Action.AttachmentRead, Attachment) rule specifically for endpoint-level checks, and keep { isPublished: true } for instance-level filtering, or
  • Update PoliciesGuard to evaluate this permission against a representative Attachment instance instead of the class.

)
@ApiOperation({
summary: "It returns a list of attachments.",
Expand Down Expand Up @@ -309,7 +308,7 @@ export class AttachmentsV4Controller {
// GET /attachments/:aid
@UseGuards(PoliciesGuard)
@CheckPolicies("attachments", (ability: AppAbility) =>
ability.can(Action.AttachmentReadEndpoint, Attachment),
ability.can(Action.AttachmentRead, Attachment),
)
@ApiOperation({
summary: "It returns the attachment requested.",
Expand All @@ -334,15 +333,15 @@ export class AttachmentsV4Controller {
await this.checkPermissionsForAttachment(
request,
aid,
Action.AttachmentReadEndpoint,
Action.AttachmentRead,
);
return this.attachmentsService.findOne({ aid });
}

// PATCH /attachments/:aid
@UseGuards(PoliciesGuard)
@CheckPolicies("attachments", (ability: AppAbility) =>
ability.can(Action.AttachmentUpdateEndpoint, Attachment),
ability.can(Action.AttachmentUpdate, Attachment),
)
@ApiOperation({
summary: "It updates the attachment.",
Expand Down Expand Up @@ -378,7 +377,7 @@ Set \`content-type\` header to \`application/merge-patch+json\` if you would lik
const foundAttachment = await this.checkPermissionsForAttachment(
request,
aid,
Action.AttachmentUpdateEndpoint,
Action.AttachmentUpdate,
);
const updateAttachmentDtoForservice =
request.headers["content-type"] === "application/merge-patch+json"
Expand All @@ -396,7 +395,7 @@ Set \`content-type\` header to \`application/merge-patch+json\` if you would lik
// PUT /attachments/:aid
@UseGuards(PoliciesGuard)
@CheckPolicies("attachments", (ability: AppAbility) =>
ability.can(Action.AttachmentUpdateEndpoint, Attachment),
ability.can(Action.AttachmentUpdate, Attachment),
)
@ApiOperation({
summary: "It updates the attachment.",
Expand Down Expand Up @@ -428,7 +427,7 @@ Set \`content-type\` header to \`application/merge-patch+json\` if you would lik
await this.checkPermissionsForAttachment(
request,
aid,
Action.AttachmentUpdateEndpoint,
Action.AttachmentUpdate,
);
return this.attachmentsService.findOneAndReplace(
{ _id: aid },
Expand All @@ -439,7 +438,7 @@ Set \`content-type\` header to \`application/merge-patch+json\` if you would lik
// POST /attachments
@UseGuards(PoliciesGuard)
@CheckPolicies("attachments", (ability: AppAbility) =>
ability.can(Action.AttachmentCreateEndpoint, Attachment),
ability.can(Action.AttachmentCreate, Attachment),
)
@ApiOperation({
summary: "It creates a new attachment.",
Expand All @@ -463,14 +462,14 @@ Set \`content-type\` header to \`application/merge-patch+json\` if you would lik
this.checkPermissionsForAttachmentCreate(
request,
createAttachmentDto,
Action.AttachmentCreateEndpoint,
Action.AttachmentCreate,
);
return this.attachmentsService.create(createAttachmentDto);
}

@UseGuards(PoliciesGuard)
@CheckPolicies("attachments", (ability: AppAbility) =>
ability.can(Action.AttachmentCreateEndpoint, Attachment),
ability.can(Action.AttachmentCreate, Attachment),
)
@Post("/isValid")
@HttpCode(HttpStatus.OK)
Expand Down Expand Up @@ -504,7 +503,7 @@ Set \`content-type\` header to \`application/merge-patch+json\` if you would lik
this.checkPermissionsForAttachmentCreate(
request,
CreateAttachmentDtoInstance,
Action.AttachmentCreateEndpoint,
Action.AttachmentCreate,
);
const errorsAttachment = await validate(
CreateAttachmentDtoInstance,
Expand All @@ -519,7 +518,7 @@ Set \`content-type\` header to \`application/merge-patch+json\` if you would lik
// DELETE /attachments/:aid
@UseGuards(PoliciesGuard)
@CheckPolicies("attachments", (ability: AppAbility) =>
ability.can(Action.AttachmentDeleteEndpoint, Attachment),
ability.can(Action.AttachmentDelete, Attachment),
)
@ApiOperation({
summary: "It deletes the attachment.",
Expand All @@ -542,7 +541,7 @@ Set \`content-type\` header to \`application/merge-patch+json\` if you would lik
await this.checkPermissionsForAttachment(
request,
aid,
Action.AttachmentDeleteEndpoint,
Action.AttachmentDelete,
);
return this.attachmentsService.findOneAndDelete({ aid });
}
Expand Down
106 changes: 106 additions & 0 deletions src/casl/abilities/attachments.ability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
AbilityBuilder,
ExtractSubjectType,
MongoAbility,
createMongoAbility,
} from "@casl/ability";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { AccessGroupsType } from "src/config/configuration";
import { Action } from "../action.enum";
import {
Subjects,
PossibleAbilities,
Conditions,
} from "../types/casl-subjects";
import { JWTUser } from "src/auth/interfaces/jwt-user.interface";
import { Attachment } from "src/attachments/schemas/attachment.schema";

@Injectable()
export class AttachmentAbility {
constructor(private configService: ConfigService) {
this.accessGroups =
this.configService.get<AccessGroupsType>("accessGroups");
}
private accessGroups;

buildAbility(user: JWTUser): MongoAbility<PossibleAbilities, Conditions> {
const { can, build } = new AbilityBuilder(
createMongoAbility<PossibleAbilities, Conditions>,
);
const ifPublished = { isPublished: true };

/**
* Unauthenticated user
*/
can(Action.AttachmentRead, Attachment, ifPublished);

if (!user) {
return build({
detectSubjectType: (item) =>
item.constructor as ExtractSubjectType<Subjects>,
});
}

const ifOwner = { ownerGroup: { $in: user.currentGroups } };
const ifAccess = { accessGroups: { $in: user.currentGroups } };

/**
* Authenticated user
*/
can(Action.AttachmentRead, Attachment, ifOwner);
can(Action.AttachmentRead, Attachment, ifAccess);
can(Action.AttachmentRead, Attachment, ifPublished);

if (
user.currentGroups.some((g) =>
this.accessGroups?.attachment.includes(g),
) ||
this.accessGroups?.attachment.includes("#all")
) {
/**
* User belonging to ATTACHMENT_GROUPS
*/
can(Action.AttachmentCreate, Attachment, ifOwner);
can(Action.AttachmentUpdate, Attachment, ifOwner);
can(Action.AttachmentDelete, Attachment, ifOwner);
}

if (
user.currentGroups.some((g) =>
this.accessGroups?.attachmentPrivileged.includes(g),
)
) {
/**
* User belonging to ATTACHMENT_PRIVILEGED_GROUPS
*/
can(Action.AttachmentCreate, Attachment);
can(Action.AttachmentUpdate, Attachment, ifOwner);
can(Action.AttachmentDelete, Attachment, ifOwner);
}

if (user.currentGroups.some((g) => this.accessGroups?.admin.includes(g))) {
/**
* User belonging to ADMIN_GROUPS
*/
can(Action.AccessAny, Attachment);

can(Action.AttachmentCreate, Attachment);
can(Action.AttachmentRead, Attachment);
can(Action.AttachmentUpdate, Attachment);
can(Action.AttachmentDelete, Attachment);
}

if (user.currentGroups.some((g) => this.accessGroups?.delete.includes(g))) {
/**
* User belonging to DELETE_GROUPS
*/
can(Action.AttachmentDelete, Attachment);
}

return build({
detectSubjectType: (item) =>
item.constructor as ExtractSubjectType<Subjects>,
});
}
}
21 changes: 6 additions & 15 deletions src/casl/action.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export enum Action {
// Currently used by addAccessBasedFilters for admin/special group users
AccessAny = "access_any",

// Attachments
AttachmentCreate = "attachment_create",
AttachmentRead = "attachment_read",
AttachmentUpdate = "attachment_update",
AttachmentDelete = "attachment_delete",

// ---------------
// Datasets
// endpoint authorization actions
Expand Down Expand Up @@ -236,21 +242,6 @@ export enum Action {
InstrumentCreate = "instrument_create",
InstrumentDelete = "instrument_delete",

// -------------------------------------
// Attachment
// -------------------------------------
// attachment endpoint authorization
AttachmentCreateEndpoint = "attachment_create_endpoint",
AttachmentReadEndpoint = "attachment_read_endpoint",
AttachmentUpdateEndpoint = "attachment_update_endpoint",
AttachmentDeleteEndpoint = "attachment_delete_endpoint",
// -------------------------------------
// attachment data instance authorization
AttachmentCreateInstance = "attachment_create_instance",
AttachmentReadInstance = "attachment_read_instance",
AttachmentUpdateInstance = "attachment_update_instance",
AttachmentDeleteInstance = "attachment_delete_instance",

// -------------------------------------
// History
// -------------------------------------
Expand Down
Loading