From 53d8124b8f7c6667c00613397c257497fa7996b5 Mon Sep 17 00:00:00 2001 From: Despina Adamopoulou <16343312+despadam@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:09:32 +0200 Subject: [PATCH] Jobs: implement fullfacet (#1436) * implement fullfacet * tests --- src/jobs/interfaces/job-filters.interface.ts | 28 ++++ src/jobs/jobs.controller.ts | 83 +++++------ src/jobs/jobs.service.ts | 7 +- test/Jobs.js | 144 ++++++++++++++++++- 4 files changed, 212 insertions(+), 50 deletions(-) create mode 100644 src/jobs/interfaces/job-filters.interface.ts diff --git a/src/jobs/interfaces/job-filters.interface.ts b/src/jobs/interfaces/job-filters.interface.ts new file mode 100644 index 000000000..545d8f86e --- /dev/null +++ b/src/jobs/interfaces/job-filters.interface.ts @@ -0,0 +1,28 @@ +interface IDateRange { + begin: string; + end: string; +} + +interface IJobFieldObject { + $regex: string; + $options: string; +} + +interface IJobIdsFieldObject { + $in: string[]; +} + +export interface IJobFields { + mode?: Record; + text?: string; + createdAt?: IDateRange; + id?: IJobFieldObject; + _id?: IJobIdsFieldObject; + type?: IJobFieldObject; + statusCode?: IJobFieldObject; + jobParams?: IJobFieldObject; + jobResultObject?: IJobFieldObject; + ownerUser?: IJobFieldObject; + ownerGroup?: string[]; + accessGroups?: string[]; +} diff --git a/src/jobs/jobs.controller.ts b/src/jobs/jobs.controller.ts index 29f8aba5b..40eff44fc 100644 --- a/src/jobs/jobs.controller.ts +++ b/src/jobs/jobs.controller.ts @@ -52,6 +52,7 @@ import { } from "src/common/utils"; import { JobCreateInterceptor } from "./interceptors/job-create.interceptor"; import { JobAction } from "./config/jobconfig"; +import { IJobFields } from "./interfaces/job-filters.interface"; @ApiBearerAuth() @ApiTags("jobs") @@ -575,8 +576,6 @@ export class JobsController { async performJobStatusUpdateAction(jobInstance: JobClass): Promise { const jobConfig = this.getJobTypeConfiguration(jobInstance.type); - - // TODO - what shall we do when configVersion does not match? if (jobConfig.configVersion !== jobInstance.configVersion) { Logger.log( ` @@ -586,7 +585,6 @@ export class JobsController { "JobStatusUpdate", ); } - for (const action of jobConfig.statusUpdate.actions) { await this.performJobAction(jobInstance, action); } @@ -688,7 +686,6 @@ export class JobsController { if (!canUpdateStatus) { throw new ForbiddenException("Unauthorized to update this job."); } - // Update job in database const updatedJob = await this.jobsService.statusUpdate( id, @@ -762,10 +759,10 @@ export class JobsController { const jobInstance = await this.generateJobInstanceForPermissions( jobsFound[i], ); - const canCreate = + const canRead = ability.can(Action.JobReadAny, JobClass) || ability.can(Action.JobReadAccess, jobInstance); - if (canCreate) { + if (canRead) { jobsAccessible.push(jobsFound[i]); } } @@ -792,11 +789,8 @@ export class JobsController { @Get("/fullfacet") @ApiOperation({ summary: "It returns a list of job facets matching the filter provided.", - description: ` - This endpoint was added for completeness reasons, - so that the frontend can work with the new backend version. - For now, it always returns an empty array. - `, + description: + "It returns a list of job facets matching the filter provided.", }) @ApiQuery({ name: "fields", @@ -820,36 +814,44 @@ export class JobsController { async fullFacet( @Req() request: Request, @Query() filters: { fields?: string; facets?: string }, - ): Promise { + ): Promise[]> { try { - // const parsedFilters: IFacets> = { - // fields: JSON.parse(filters.fields ?? "{}" as string), - // facets: JSON.parse(filters.facets ?? "[]" as string), - // }; - // const jobsFound = await this.jobsService.fullfacet(parsedFilters); - const jobsAccessible: JobClass[] = []; + const fields: IJobFields = JSON.parse(filters.fields ?? ("{}" as string)); + const queryFilters: IFilters> = { + fields: fields, + limits: JSON.parse("{}" as string), + }; + const jobsFound = await this.jobsService.fullquery(queryFilters); + const jobIdsAccessible: string[] = []; // for each job run a casl JobReadOwner on a jobInstance - // for (const i in jobsFound) { - // const jobConfiguration = this.getJobTypeConfiguration( - // jobsFound[i].type, - // ); - // const ability = this.caslAbilityFactory.jobsInstanceAccess( - // request.user as JWTUser, - // jobConfiguration, - // ); - // // check if the user can get this job - // const jobInstance = await this.generateJobInstanceForPermissions( - // jobsFound[i], - // ); - // const canCreate = - // ability.can(Action.JobReadAny, JobClass) || - // ability.can(Action.JobReadAccess, jobInstance); - // if (canCreate) { - // jobsAccessible.push(jobsFound[i]); - // } - // } - return jobsAccessible; + if (jobsFound != null) { + for (const i in jobsFound) { + const jobConfiguration = this.getJobTypeConfiguration( + jobsFound[i].type, + ); + const ability = this.caslAbilityFactory.jobsInstanceAccess( + request.user as JWTUser, + jobConfiguration, + ); + // check if the user can get this job + const jobInstance = await this.generateJobInstanceForPermissions( + jobsFound[i], + ); + const canRead = + ability.can(Action.JobReadAny, JobClass) || + ability.can(Action.JobReadAccess, jobInstance); + if (canRead) { + jobIdsAccessible.push(jobsFound[i]._id); + } + } + } + fields._id = { $in: jobIdsAccessible }; + const facetFilters: IFacets = { + fields: fields, + facets: JSON.parse(filters.facets ?? ("[]" as string)), + }; + return await this.jobsService.fullfacet(facetFilters); } catch (e) { throw new HttpException( { @@ -967,10 +969,10 @@ export class JobsController { const jobInstance = await this.generateJobInstanceForPermissions( jobsFound[i], ); - const canCreate = + const canRead = ability.can(Action.JobReadAny, JobClass) || ability.can(Action.JobReadAccess, jobInstance); - if (canCreate) { + if (canRead) { jobsAccessible.push(jobsFound[i]); } } @@ -1017,7 +1019,6 @@ export class JobsController { HttpStatus.BAD_REQUEST, ); } - Logger.log(`Deleting job with id ${id}!`); return this.jobsService.remove({ _id: id }); } diff --git a/src/jobs/jobs.service.ts b/src/jobs/jobs.service.ts index fab874ed9..bce201ad3 100644 --- a/src/jobs/jobs.service.ts +++ b/src/jobs/jobs.service.ts @@ -22,6 +22,7 @@ import { import { CreateJobDto } from "./dto/create-job.dto"; import { StatusUpdateJobDto } from "./dto/status-update-job.dto"; import { JobClass, JobDocument } from "./schemas/job.schema"; +import { IJobFields } from "./interfaces/job-filters.interface"; @Injectable({ scope: Scope.REQUEST }) export class JobsService { @@ -76,14 +77,14 @@ export class JobsService { } async fullfacet( - filters: IFacets>, - ): Promise { + filters: IFacets, + ): Promise[]> { const fields = filters.fields ?? {}; const facets = filters.facets ?? []; const pipeline: PipelineStage[] = createFullfacetPipeline< JobDocument, - FilterQuery + IJobFields >(this.jobModel, "id", fields, facets); return await this.jobModel.aggregate(pipeline).exec(); diff --git a/test/Jobs.js b/test/Jobs.js index a9a4d5ed9..d62529bc9 100644 --- a/test/Jobs.js +++ b/test/Jobs.js @@ -3877,7 +3877,7 @@ describe("1100: Jobs: Test New Job Model", () => { }); }); - it("2000: Fullquery jobs as a user from ADMIN_GROUPS that were created by User5.1", async () => { + it("2000: Fullquery jobs as a user from ADMIN_GROUPS that were created by User5.1, limited by 5", async () => { const queryFields = { createdBy: "user5.1" }; const queryLimits = { limit: 5 }; return request(appUrl) @@ -4004,26 +4004,158 @@ describe("1100: Jobs: Test New Job Model", () => { .expect("Content-Type", /json/); }); - it("3000: Fullfacet jobs as a user from ADMIN_GROUPS, which should always return an empty array", async () => { + it("3010: Fullfacet jobs as unauthenticated user, which should be forbidden", async () => { return request(appUrl) .get(`/api/v3/Jobs/fullfacet`) .send({}) .set("Accept", "application/json") + .expect(TestData.AccessForbiddenStatusCode) + .expect("Content-Type", /json/); + }); + + it("3020: Fullfacet jobs as a user from ADMIN_GROUPS that were created by admin", async () => { + const query = { createdBy: "admin" }; + return request(appUrl) + .get(`/api/v3/Jobs/fullfacet`) + .send({}) + .query("fields=" + encodeURIComponent(JSON.stringify(query))) + .set("Accept", "application/json") .set({ Authorization: `Bearer ${accessTokenAdmin}` }) .expect(TestData.SuccessfulGetStatusCode) .expect("Content-Type", /json/) .then((res) => { - res.body.should.be.an("array").to.have.lengthOf(0); + res.body.should.be.an("array").that.deep.contains({ all: [{ totalSets: 36 }] }); }); }); - it("3010: Fullfacet jobs as unauthenticated user, which should be forbidden", async () => { + it("3030: Fullfacet jobs as a user from ADMIN_GROUPS that were created by User1", async () => { + const query = { createdBy: "user1" }; return request(appUrl) .get(`/api/v3/Jobs/fullfacet`) .send({}) + .query("fields=" + encodeURIComponent(JSON.stringify(query))) .set("Accept", "application/json") - .expect(TestData.AccessForbiddenStatusCode) - .expect("Content-Type", /json/); + .set({ Authorization: `Bearer ${accessTokenAdmin}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.be.an("array").that.deep.contains({ all: [{ totalSets: 11 }] }); + }); + }); + + it("3040: Fullfacet jobs as a user from ADMIN_GROUPS that were created by User5.1", async () => { + const queryFields = { createdBy: "user5.1" }; + return request(appUrl) + .get(`/api/v3/Jobs/fullfacet`) + .send({}) + .query("fields=" + encodeURIComponent(JSON.stringify(queryFields))) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenAdmin}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.be.an("array").that.deep.contains({ all: [{ totalSets: 10 }] }); + }); + }); + + it("3050: Fullfacet jobs as a user from ADMIN_GROUPS that were created by User5.2", async () => { + const query = { createdBy: "user5.2" }; + return request(appUrl) + .get(`/api/v3/Jobs/fullfacet`) + .send({}) + .query("fields=" + encodeURIComponent(JSON.stringify(query))) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenAdmin}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.be.an("array").that.deep.contains({ all: [{ totalSets: 1 }] }); + }); + }); + + it("3060: Fullfacet jobs as a user from ADMIN_GROUPS that were created by anonymous user", async () => { + const query = { createdBy: "anonymous" }; + return request(appUrl) + .get(`/api/v3/Jobs/fullfacet`) + .send({}) + .query("fields=" + encodeURIComponent(JSON.stringify(query))) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenAdmin}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.be.an("array").that.deep.contains({ all: [{ totalSets: 2 }] }); + }); }); + it("3070: Fullfacet jobs as a user from CREATE_JOB_GROUPS that were created by admin", async () => { + const query = { createdBy: "admin" }; + return request(appUrl) + .get(`/api/v3/Jobs/fullfacet`) + .send({}) + .query("fields=" + encodeURIComponent(JSON.stringify(query))) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenUser1}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.be.an("array").that.deep.contains({ all: [{ totalSets: 14 }] }); + }); + }); + + it("3080: Fullfacet jobs as a user from CREATE_JOB_GROUPS that were created by User1", async () => { + const query = { createdBy: "user1" }; + return request(appUrl) + .get(`/api/v3/Jobs/fullfacet`) + .send({}) + .query("fields=" + encodeURIComponent(JSON.stringify(query))) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenUser1}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.be.an("array").that.deep.contains({ all: [{ totalSets: 11 }] }); + }); + }); + + it("3090: Fullfacet jobs as a normal user", async () => { + return request(appUrl) + .get(`/api/v3/Jobs/fullfacet`) + .send({}) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenUser51}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.be.an("array").that.deep.contains({ all: [{ totalSets: 10 }] }); + }); + }); + + it("3100: Fullfacet jobs as a normal user (user5.1) that were created by admin", async () => { + const query = { createdBy: "admin" }; + return request(appUrl) + .get(`/api/v3/Jobs/fullfacet`) + .send({}) + .query("fields=" + encodeURIComponent(JSON.stringify(query))) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenUser51}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.be.an("array").that.deep.contains({ all: [] }); + }); + }); + + it("3110: Fullfacet jobs as another normal user (user5.2)", async () => { + return request(appUrl) + .get(`/api/v3/Jobs/fullfacet`) + .send({}) + .set("Accept", "application/json") + .set({ Authorization: `Bearer ${accessTokenUser52}` }) + .expect(TestData.SuccessfulGetStatusCode) + .expect("Content-Type", /json/) + .then((res) => { + res.body.should.be.an("array").that.deep.contains({ all: [{ totalSets: 2 }] }); + }); + }); });