Skip to content

Commit

Permalink
Merge pull request #765 from SciCatProject/SWAP-3335-scicat-be-implem…
Browse files Browse the repository at this point in the history
…ent-authorization-on-origdatabloc

fix: implement authorization on origDatablock
  • Loading branch information
nitrosx authored Oct 18, 2023
2 parents 8f62192 + 8b1eafb commit ee2f60f
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 149 deletions.
4 changes: 2 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier"
],
],
"root": true,
"env": {
"node": true,
Expand All @@ -33,7 +33,7 @@
"double",
{
"allowTemplateLiterals": true,
"avoidEscape": true
"avoidEscape": true
}
],
"no-constant-condition": ["error", { "checkLoops": false }],
Expand Down
15 changes: 13 additions & 2 deletions src/casl/casl-ability.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,12 @@ export class CaslAbilityFactory {
// -------------------------------------
// policies
can(Action.Update, Policy);
// -------------------------------------
// origdatablocks
can(Action.Manage, OrigDatablock);
} else {
//
// non admin users

can(Action.ListOwn, ProposalClass);
can(Action.ListOwn, DatasetClass);
if (
Expand Down Expand Up @@ -128,6 +130,7 @@ export class CaslAbilityFactory {
cannot(Action.InstrumentUpdate, Instrument);
cannot(Action.InstrumentDelete, Instrument);
}

can(Action.Read, DatasetClass, { isPublished: true });
can(Action.Read, DatasetClass, {
isPublished: false,
Expand Down Expand Up @@ -181,7 +184,15 @@ export class CaslAbilityFactory {

can(Action.Manage, Attachment, { ownerGroup: { $in: user.currentGroups } });
can(Action.Manage, Datablock, { ownerGroup: { $in: user.currentGroups } });
can(Action.Manage, OrigDatablock, {

can(Action.Create, OrigDatablock);
can(Action.Read, OrigDatablock, {
accessGroups: { $in: user.currentGroups },
});
can(Action.Read, OrigDatablock, {
ownerGroup: { $in: user.currentGroups },
});
can(Action.Update, OrigDatablock, {
ownerGroup: { $in: user.currentGroups },
});

Expand Down
1 change: 1 addition & 0 deletions src/origdatablocks/interfaces/origdatablocks.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface IOrigDatablockFields {
max?: string;
};
dataFilelist: IDatafileFilter[];
userGroups?: string[];
}
168 changes: 154 additions & 14 deletions src/origdatablocks/origdatablocks.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import {
HttpCode,
HttpStatus,
BadRequestException,
Req,
ForbiddenException,
NotFoundException,
} from "@nestjs/common";
import { Request } from "express";
import { OrigDatablocksService } from "./origdatablocks.service";
import { CreateOrigDatablockDto } from "./dto/create-origdatablock.dto";
import { UpdateOrigDatablockDto } from "./dto/update-origdatablock.dto";
Expand All @@ -27,7 +31,7 @@ import {
} from "@nestjs/swagger";
import { PoliciesGuard } from "src/casl/guards/policies.guard";
import { CheckPolicies } from "src/casl/decorators/check-policies.decorator";
import { AppAbility } from "src/casl/casl-ability.factory";
import { AppAbility, CaslAbilityFactory } from "src/casl/casl-ability.factory";
import { Action } from "src/casl/action.enum";
import {
OrigDatablock,
Expand All @@ -41,6 +45,8 @@ import { validate, ValidationError } from "class-validator";
import { DatasetsService } from "src/datasets/datasets.service";
import { PartialUpdateDatasetDto } from "src/datasets/dto/update-dataset.dto";
import { filterDescription, filterExample } from "src/common/utils";
import { JWTUser } from "src/auth/interfaces/jwt-user.interface";
import { DatasetClass } from "src/datasets/schemas/dataset.schema";

@ApiBearerAuth()
@ApiTags("origdatablocks")
Expand All @@ -49,7 +55,44 @@ export class OrigDatablocksController {
constructor(
private readonly origDatablocksService: OrigDatablocksService,
private readonly datasetsService: DatasetsService,
private caslAbilityFactory: CaslAbilityFactory,
) {}
async checkPermissionsForOrigDatablock(request: Request, id: string) {
const origDatablock = await this.origDatablocksService.findOne({ _id: id });
if (!origDatablock) {
throw new BadRequestException("Invalid origDatablock Id");
}

return await this.checkPermissionsForDataset(
request,
origDatablock.datasetId,
);
}

async checkPermissionsForDataset(request: Request, id: string) {
const user: JWTUser = request.user as JWTUser;
const dataset = await this.datasetsService.findOne({
where: { pid: id },
});
if (dataset) {
// NOTE: We need DatasetClass instance because casl module
// can not recognize the type from dataset mongo database model.
// If other fields are needed can be added later.
const newDatasetClass = new DatasetClass();
newDatasetClass.ownerGroup = dataset.ownerGroup;

if (user) {
const ability = this.caslAbilityFactory.createForUser(user);
const canUpdate = ability.can(Action.Update, newDatasetClass);
if (!canUpdate) {
throw new ForbiddenException("Unauthorized access");
}
}
} else {
throw new BadRequestException("Invalid datasetId");
}
return dataset;
}

// POST /origdatablocks
@UseGuards(PoliciesGuard)
Expand All @@ -75,14 +118,13 @@ export class OrigDatablocksController {
"Create a new origdataset and return its representation in SciCat",
})
async create(
@Req() request: Request,
@Body() createOrigDatablockDto: CreateOrigDatablockDto,
): Promise<OrigDatablock> {
const dataset = await this.datasetsService.findOne({
where: { pid: createOrigDatablockDto.datasetId },
});
if (!dataset) {
throw new BadRequestException("Invalid datasetId");
}
const dataset = await this.checkPermissionsForDataset(
request,
createOrigDatablockDto.datasetId,
);

createOrigDatablockDto = {
...createOrigDatablockDto,
Expand Down Expand Up @@ -186,9 +228,24 @@ export class OrigDatablocksController {
isArray: true,
description: "Return the orig datablocks requested",
})
async findAll(@Query("filter") filter?: string): Promise<OrigDatablock[]> {
async findAll(
@Req() request: Request,
@Query("filter") filter?: string,
): Promise<OrigDatablock[]> {
const user: JWTUser = request.user as JWTUser;
const parsedFilters: IFilters<OrigDatablockDocument, IOrigDatablockFields> =
JSON.parse(filter ?? "{}");
if (user) {
const ability = this.caslAbilityFactory.createForUser(user);
const canViewAll = ability.can(Action.ListAll, OrigDatablock);

if (!canViewAll) {
parsedFilters.where = parsedFilters.where ?? {};
parsedFilters.where.userGroups = parsedFilters.where.userGroups ?? [];
parsedFilters.where.userGroups.push(...user.currentGroups);
}
}

return this.origDatablocksService.findAll(parsedFilters);
}

Expand All @@ -214,10 +271,24 @@ export class OrigDatablocksController {
example: '{ "skip": 0, "limit": 25, "order": "creationTime:desc" }',
})
async fullquery(
@Req() request: Request,
@Query() filters: { fields?: string; limits?: string },
): Promise<OrigDatablock[] | null> {
const user: JWTUser = request.user as JWTUser;
const fields: IOrigDatablockFields = JSON.parse(filters.fields ?? "{}");

if (user) {
const ability = this.caslAbilityFactory.createForUser(user);
const canViewAll = ability.can(Action.ListAll, OrigDatablock);

if (!canViewAll) {
fields.userGroups = fields.userGroups ?? [];
fields.userGroups.push(...user.currentGroups);
}
}

const parsedFilters = {
fields: JSON.parse(filters.fields ?? "{}"),
fields: fields,
limits: JSON.parse(filters.limits ?? "{}"),
};

Expand Down Expand Up @@ -246,10 +317,23 @@ export class OrigDatablocksController {
example: '{ "skip": 0, "limit": 25, "order": "creationTime:desc" }',
})
async fullqueryFiles(
@Req() request: Request,
@Query() filters: { fields?: string; limits?: string },
): Promise<OrigDatablock[] | null> {
const user: JWTUser = request.user as JWTUser;
const fields: IOrigDatablockFields = JSON.parse(filters.fields ?? "{}");

if (user) {
const ability = this.caslAbilityFactory.createForUser(user);
const canViewAll = ability.can(Action.Manage, OrigDatablock);

if (!canViewAll) {
fields.userGroups = fields.userGroups ?? [];
fields.userGroups.push(...user.currentGroups);
}
}
const parsedFilters = {
fields: JSON.parse(filters.fields ?? "{}"),
fields: fields,
limits: JSON.parse(filters.limits ?? "{}"),
};

Expand All @@ -263,12 +347,26 @@ export class OrigDatablocksController {
)
@Get("/fullfacet")
async fullfacet(
@Req() request: Request,
@Query() filters: { fields?: string; facets?: string },
): Promise<Record<string, unknown>[]> {
const user: JWTUser = request.user as JWTUser;
const fields: IOrigDatablockFields = JSON.parse(filters.fields ?? "{}");

if (user) {
const ability = this.caslAbilityFactory.createForUser(user);
const canViewAll = ability.can(Action.ListAll, OrigDatablock);

if (!canViewAll) {
fields.userGroups = fields.userGroups ?? [];
fields.userGroups.push(...user.currentGroups);
}
}
const parsedFilters = {
fields: JSON.parse(filters.fields ?? "{}"),
fields: fields,
limits: JSON.parse(filters.facets ?? "{}"),
};

return this.origDatablocksService.fullfacet(parsedFilters);
}

Expand All @@ -279,10 +377,23 @@ export class OrigDatablocksController {
)
@Get("/fullfacet/files")
async fullfacetFiles(
@Req() request: Request,
@Query() filters: { fields?: string; facets?: string },
): Promise<Record<string, unknown>[]> {
const user: JWTUser = request.user as JWTUser;
const fields: IOrigDatablockFields = JSON.parse(filters.fields ?? "{}");

if (user) {
const ability = this.caslAbilityFactory.createForUser(user);
const canViewAll = ability.can(Action.ListAll, OrigDatablock);

if (!canViewAll) {
fields.userGroups = fields.userGroups ?? [];
fields.userGroups.push(...user.currentGroups);
}
}
const parsedFilters = {
fields: JSON.parse(filters.fields ?? "{}"),
fields: fields,
limits: JSON.parse(filters.facets ?? "{}"),
};
const getSubFieldCount = "dataFileList";
Expand Down Expand Up @@ -313,8 +424,34 @@ export class OrigDatablocksController {
description: "The origdatablock requested",
type: OrigDatablock,
})
async findById(@Param("id") id: string): Promise<OrigDatablock | null> {
return this.origDatablocksService.findOne({ _id: id });
async findById(
@Req() request: Request,
@Param("id")
id: string,
): Promise<OrigDatablock | null> {
const user = request.user as JWTUser;
const filter = {
_id: id,
userGroups: [] as string[],
};
if (user) {
const ability = this.caslAbilityFactory.createForUser(user);
const canViewAll = ability.can(Action.ListAll, OrigDatablock);

if (!canViewAll) {
filter.userGroups.push(...user.currentGroups);
}
}

try {
const data = await this.origDatablocksService.findOne(filter);
return data;
} catch (error) {
if (error instanceof ForbiddenException) {
throw new ForbiddenException(error.message);
}
throw new NotFoundException(error);
}
}

// PATCH /origdatablocks/:id
Expand Down Expand Up @@ -344,9 +481,12 @@ export class OrigDatablocksController {
type: OrigDatablock,
})
async update(
@Req() request: Request,
@Param("id") id: string,
@Body() updateOrigDatablockDto: UpdateOrigDatablockDto,
): Promise<OrigDatablock | null> {
await this.checkPermissionsForOrigDatablock(request, id);

const origdatablock = (await this.origDatablocksService.update(
{ _id: id },
updateOrigDatablockDto,
Expand Down
25 changes: 22 additions & 3 deletions src/origdatablocks/origdatablocks.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable, Inject, Scope } from "@nestjs/common";
import { Injectable, Inject, Scope, ForbiddenException } from "@nestjs/common";
import { REQUEST } from "@nestjs/core";
import { Request } from "express";
import { InjectModel } from "@nestjs/mongoose";
Expand Down Expand Up @@ -42,7 +42,13 @@ export class OrigDatablocksService {
async findAll(
filter: FilterQuery<OrigDatablockDocument>,
): Promise<OrigDatablock[]> {
const whereFilter: FilterQuery<OrigDatablockDocument> = filter.where ?? {};
const whereFilter: FilterQuery<OrigDatablockDocument> =
createFullqueryFilter<OrigDatablockDocument>(
this.origDatablockModel,
"_id",
filter.where as FilterQuery<OrigDatablockDocument>,
);

const fieldsProjection: FilterQuery<OrigDatablockDocument> =
filter.fields ?? {};
const { limit, skip, sort } = parseLimitFilters(filter.limits);
Expand All @@ -61,7 +67,20 @@ export class OrigDatablocksService {
async findOne(
filter: FilterQuery<OrigDatablockDocument>,
): Promise<OrigDatablock | null> {
return this.origDatablockModel.findOne(filter).exec();
const whereFilter: FilterQuery<OrigDatablockDocument> =
createFullqueryFilter<OrigDatablockDocument>(
this.origDatablockModel,
"_id",
filter as FilterQuery<OrigDatablockDocument>,
);

const origdatablock = await this.origDatablockModel.findOne(whereFilter);

if (!origdatablock) {
throw new ForbiddenException("Unauthorized access");
}

return origdatablock;
}

async fullquery(
Expand Down
Loading

0 comments on commit ee2f60f

Please sign in to comment.