Skip to content

Commit

Permalink
Merge pull request #947 from openkfw/944-unavailable-workflowitem-doc…
Browse files Browse the repository at this point in the history
…uments-appear

Add workflowitem.viewDetails endpoint
  • Loading branch information
Stezido committed Aug 31, 2021
2 parents 33a8a49 + 668a212 commit 59855d0
Show file tree
Hide file tree
Showing 22 changed files with 754 additions and 239 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

- Add a bash script for a quick and easy TruBudget setup [#905](https://github.com/openkfw/TruBudget/issues/905)
- Add possibility to reject a workflowitem [#845](https://github.com/openkfw/TruBudget/issues/845)
- Disable download option for documents that are not available anymore [#944](https://github.com/openkfw/TruBudget/issues/944)

<!-- ### Changed -->

Expand Down
15 changes: 15 additions & 0 deletions api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import * as WorkflowitemDocumentDownloadService from "./service/workflowitem_doc
import * as WorkflowitemGetService from "./service/workflowitem_get";
import * as WorkflowitemViewHistoryService from "./service/workflowitem_history_get";
import * as WorkflowitemListService from "./service/workflowitem_list";
import * as WorkflowitemGetDetailsService from "./service/workflowitem_get_details";
import * as WorkflowitemPermissionsListService from "./service/workflowitem_permissions_list";
import * as WorkflowitemPermissionGrantService from "./service/workflowitem_permission_grant";
import * as WorkflowitemPermissionRevokeService from "./service/workflowitem_permission_revoke";
Expand Down Expand Up @@ -133,6 +134,7 @@ import * as WorkflowitemCloseAPI from "./workflowitem_close";
import * as WorkflowitemCreateAPI from "./workflowitem_create";
import * as WorkflowitemsDocumentDownloadAPI from "./workflowitem_download_document";
import * as WorkflowitemListAPI from "./workflowitem_list";
import * as WorkflowitemViewDetailsAPI from "./workflowitem_view_details";
import * as WorkflowitemPermissionsListAPI from "./workflowitem_permissions_list";
import * as WorkflowitemPermissionGrantAPI from "./workflowitem_permission_grant";
import * as WorkflowitemPermissionRevokeAPI from "./workflowitem_permission_revoke";
Expand Down Expand Up @@ -649,6 +651,19 @@ WorkflowitemListAPI.addHttpHandler(server, URL_PREFIX, {
WorkflowitemListService.listWorkflowitems(db, ctx, user, projectId, subprojectId),
});

WorkflowitemViewDetailsAPI.addHttpHandler(server, URL_PREFIX, {
getWorkflowitemDetails: (ctx, user, projectId, subprojectId, workflowitemId) =>
WorkflowitemGetDetailsService.getWorkflowitemDetails(
db,
storageServiceClient,
ctx,
user,
projectId,
subprojectId,
workflowitemId,
),
});

WorkflowitemViewHistoryAPI.addHttpHandler(server, URL_PREFIX, {
getWorkflowitemHistory: (ctx, user, projectId, subprojectId, workflowitemId, filter) =>
WorkflowitemViewHistoryService.getWorkflowitemHistory(
Expand Down
2 changes: 2 additions & 0 deletions api/src/service/domain/document/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface StoredDocument {
hash: string;
// new document feature properties
fileName?: string;
available?: boolean;
organization?: string;
organizationUrl?: string;
}
Expand All @@ -17,6 +18,7 @@ export const storedDocumentSchema = Joi.object({
id: Joi.string().required(),
hash: Joi.string().allow("").required(),
fileName: Joi.string(),
available: Joi.boolean(),
organization: Joi.string(),
organizationUrl: Joi.string(),
});
Expand Down
125 changes: 125 additions & 0 deletions api/src/service/domain/workflow/workflowitem_get_details.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { assert } from "chai";

import { Ctx } from "../../../lib/ctx";
import * as Result from "../../../result";
import { NotAuthorized } from "../errors/not_authorized";
import { NotFound } from "../errors/not_found";
import { ServiceUser } from "../organization/service_user";
import { Permissions } from "../permissions";
import { Workflowitem } from "./workflowitem";
import { getWorkflowitemDetails } from "./workflowitem_get_details";
import { StoredDocument, UploadedDocument } from "../document/document";

const ctx: Ctx = { requestId: "", source: "test" };
const root: ServiceUser = { id: "root", groups: [] };
const alice: ServiceUser = { id: "alice", groups: [] };
const subprojectId = "dummy-subproject";
const workflowitemId = "dummy-workflowitem";

const permissions: Permissions = {
"workflowitem.view": ["alice"],
};

const baseWorkflowitem: Workflowitem = {
isRedacted: false,
id: workflowitemId,
subprojectId,
createdAt: new Date().toISOString(),
dueDate: new Date().toISOString(),
status: "open",
assignee: alice.id,
displayName: "dummy",
description: "dummy",
amountType: "N/A",
documents: [],
permissions,
log: [],
additionalData: {},
workflowitemType: "general",
};

const documentStoredInWorkflowitem: StoredDocument = {
id: "documentIdOffchain",
hash: "lakjflaksdjf",
fileName: "offchainFile",
};
const uploadedDocument: UploadedDocument = {
id: "documentIdStorage",
base64: "lakjflaksdjf",
fileName: "storageFile",
};

const baseRepository = {
getWorkflowitem: async () => baseWorkflowitem,
downloadDocument: async (docId: string) => uploadedDocument,
};

describe("get workflowitems: authorization", () => {
it("Without the required permissions, a user cannot get a workflowitem's details.", async () => {
const notPermittedWorkflowitem: Workflowitem = {
...baseWorkflowitem,
permissions: {},
};
const result = await getWorkflowitemDetails(ctx, alice, workflowitemId, {
...baseRepository,
getWorkflowitem: async () => notPermittedWorkflowitem,
});

assert.instanceOf(result, NotAuthorized);
});

it("With the required permissions, a user can get the workflowitem details.", async () => {
const result = await getWorkflowitemDetails(ctx, alice, workflowitemId, baseRepository);

assert.isTrue(Result.isOk(result), (result as Error).message);
assert.equal(Result.unwrap(result).id, workflowitemId);
});

it("The root user doesn't need permission to get the workflowitem details.", async () => {
const result = await getWorkflowitemDetails(ctx, root, workflowitemId, baseRepository);

// No errors, despite the missing permissions:
assert.isTrue(Result.isOk(result), (result as Error).message);
assert.equal(Result.unwrap(result).id, workflowitemId);
});
});
describe("get workflowitem details: preconditions", () => {
it("Getting a workflowitem fails if the workflowitem cannot be found", async () => {
const result = await getWorkflowitemDetails(ctx, alice, workflowitemId, {
...baseRepository,
getWorkflowitem: async () => new Error("some error"),
});
assert.isTrue(Result.isErr(result));
assert.instanceOf(result, NotFound);
});
});
describe("get workflowitem details: test correct availability", () => {
it("Unavailable documents are marked as unavailable", async () => {
const workflowitemWithDocs: Workflowitem = {
...baseWorkflowitem,
documents: [documentStoredInWorkflowitem],
};
const result = await getWorkflowitemDetails(ctx, alice, workflowitemId, {
getWorkflowitem: async () => workflowitemWithDocs,
downloadDocument: async () => new Error("some error"),
});
assert.isTrue(Result.isOk(result));
const documents = Result.unwrap(result).documents;
assert.isNotEmpty(documents);
assert.isFalse(documents[0].available);
});
it("Available documents are marked as available", async () => {
const workflowitemWithDocs: Workflowitem = {
...baseWorkflowitem,
documents: [documentStoredInWorkflowitem],
};
const result = await getWorkflowitemDetails(ctx, alice, workflowitemId, {
getWorkflowitem: async () => workflowitemWithDocs,
downloadDocument: async () => uploadedDocument,
});
assert.isTrue(Result.isOk(result));
const documents = Result.unwrap(result).documents;
assert.isNotEmpty(documents);
assert.isTrue(documents[0].available);
});
});
53 changes: 53 additions & 0 deletions api/src/service/domain/workflow/workflowitem_get_details.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Intent from "../../../authz/intents";
import { Ctx } from "../../../lib/ctx";
import * as Result from "../../../result";
import { NotAuthorized } from "../errors/not_authorized";
import { NotFound } from "../errors/not_found";
import { ServiceUser } from "../organization/service_user";
import * as Workflowitem from "./workflowitem";
import * as WorkflowitemDocument from "../document/document";

interface Repository {
getWorkflowitem(): Promise<Result.Type<Workflowitem.Workflowitem>>;
downloadDocument(docId: string): Promise<Result.Type<WorkflowitemDocument.UploadedDocument>>;
}

export async function getWorkflowitemDetails(
ctx: Ctx,
user: ServiceUser,
workflowitemId: string,
repository: Repository,
): Promise<Result.Type<Workflowitem.Workflowitem>> {
const workflowitem = await repository.getWorkflowitem();

if (Result.isErr(workflowitem)) {
return new NotFound(ctx, "workflowitem", workflowitemId);
}

if (user.id !== "root") {
const intent = "workflowitem.view";
if (!Workflowitem.permits(workflowitem, user, [intent])) {
return new NotAuthorized({ ctx, userId: user.id, intent, target: workflowitem });
}
}

const documentsWithAvailability = await setDocumentAvailability(
workflowitem.documents,
repository,
);
return { ...workflowitem, documents: documentsWithAvailability };
}

async function setDocumentAvailability(
documents: WorkflowitemDocument.StoredDocument[],
repository: Repository,
): Promise<WorkflowitemDocument.StoredDocument[]> {
const docsWithAvailability: WorkflowitemDocument.StoredDocument[] = [];

for (const doc of documents) {
const result = await repository.downloadDocument(doc.id);
docsWithAvailability.push({ ...doc, available: Result.isOk(result) });
}

return docsWithAvailability;
}
51 changes: 51 additions & 0 deletions api/src/service/workflowitem_get_details.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { VError } from "verror";
import { Ctx } from "../lib/ctx";
import * as Result from "../result";
import * as Cache from "./cache2";
import { ConnToken } from "./conn";
import { ServiceUser } from "./domain/organization/service_user";
import * as Project from "./domain/workflow/project";
import * as Subproject from "./domain/workflow/subproject";
import * as Workflowitem from "./domain/workflow/workflowitem";
import * as WorkflowitemGetDetails from "./domain/workflow/workflowitem_get_details";
import * as WorkflowitemGet from "./domain/workflow/workflowitem_get";
import * as WorkflowitemDocumentDownloadService from "./workflowitem_document_download";
import { StorageServiceClientI } from "./Client_storage_service.h";

export async function getWorkflowitemDetails(
conn: ConnToken,
storageServiceClient: StorageServiceClientI,
ctx: Ctx,
serviceUser: ServiceUser,
projectId: Project.Id,
subprojectId: Subproject.Id,
workflowitemId: Workflowitem.Id,
): Promise<Result.Type<Workflowitem.Workflowitem>> {
const workflowitemResult = await Cache.withCache(conn, ctx, async (cache) =>
WorkflowitemGetDetails.getWorkflowitemDetails(ctx, serviceUser, workflowitemId, {
getWorkflowitem: async () => {
return WorkflowitemGet.getWorkflowitem(ctx, serviceUser, workflowitemId, {
getWorkflowitem: async () => {
return cache.getWorkflowitem(projectId, subprojectId, workflowitemId);
},
});
},
downloadDocument: async (docId) => {
return WorkflowitemDocumentDownloadService.getDocument(
conn,
storageServiceClient,
ctx,
serviceUser,
projectId,
subprojectId,
workflowitemId,
docId,
);
},
}),
);
return Result.mapErr(
workflowitemResult,
(err) => new VError(err, `could not fetch workflowitem ${workflowitemId}`),
);
}
1 change: 0 additions & 1 deletion api/src/workflowitem_list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ export function addHttpHandler(server: FastifyInstance, urlPrefix: string, servi
const workflowitems = workflowitemsResult;

return workflowitems.map((workflowitem) => {
const d = workflowitem.documents;
const exposedWorkflowitem: ExposedWorkflowitem = {
allowedIntents: workflowitem.isRedacted
? []
Expand Down
Loading

0 comments on commit 59855d0

Please sign in to comment.