Skip to content

Commit

Permalink
Merge branch '944-unavailable-workflowitem-documents-appear' of https…
Browse files Browse the repository at this point in the history
…://github.com/openkfw/TruBudget into 944-unavailable-workflowitem-documents-appear
  • Loading branch information
laurenzhonauer committed Aug 25, 2021
2 parents 8cec978 + e5e7960 commit 3eec3eb
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 46 deletions.
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);
});
});
52 changes: 7 additions & 45 deletions api/src/service/domain/workflow/workflowitem_get_details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import { Ctx } from "../../../lib/ctx";
import * as Result from "../../../result";
import { NotAuthorized } from "../errors/not_authorized";
import { NotFound } from "../errors/not_found";
import { canAssumeIdentity } from "../organization/auth_token";
import { ServiceUser } from "../organization/service_user";
import * as Workflowitem from "./workflowitem";
import { WorkflowitemTraceEvent } from "./workflowitem_trace_event";
import * as WorkflowitemDocument from "../document/document";

interface Repository {
Expand All @@ -20,7 +18,7 @@ export async function getWorkflowitemDetails(
workflowitemId: string,
repository: Repository,
): Promise<Result.Type<Workflowitem.Workflowitem>> {
let workflowitem = await repository.getWorkflowitem();
const workflowitem = await repository.getWorkflowitem();

if (Result.isErr(workflowitem)) {
return new NotFound(ctx, "workflowitem", workflowitemId);
Expand All @@ -33,45 +31,11 @@ export async function getWorkflowitemDetails(
}
}

workflowitem.documents = await setDocumentAvailability(workflowitem.documents, repository);

return dropHiddenHistoryEvents(workflowitem, user);
}

type EventType = string;
const requiredPermissions = new Map<EventType, Intent[]>([
["workflowitem_created", ["workflowitem.view"]],
["workflowitem_permission_granted", ["workflowitem.intent.listPermissions"]],
["workflowitem_permission_revoked", ["workflowitem.intent.listPermissions"]],
["workflowitem_assigned", ["workflowitem.view"]],
["workflowitem_updated", ["workflowitem.view"]],
["workflowitem_closed", ["workflowitem.view"]],
["workflowitem_projected_budget_updated", ["workflowitem.view"]],
["workflowitem_projected_budget_deleted", ["workflowitem.view"]],
]);

function dropHiddenHistoryEvents(
workflowitem: Workflowitem.Workflowitem,
actingUser: ServiceUser,
): Workflowitem.Workflowitem {
const isEventVisible =
actingUser.id === "root"
? () => true
: (event: WorkflowitemTraceEvent) => {
const allowed = requiredPermissions.get(event.businessEvent.type);
if (!allowed) return false;
for (const intent of allowed) {
for (const identity of workflowitem.permissions[intent] || []) {
if (canAssumeIdentity(actingUser, identity)) return true;
}
}
return false;
};

return {
...workflowitem,
log: (workflowitem.log || []).filter(isEventVisible),
};
const documentsWithAvailability = await setDocumentAvailability(
workflowitem.documents,
repository,
);
return { ...workflowitem, documents: documentsWithAvailability };
}

async function setDocumentAvailability(
Expand All @@ -82,9 +46,7 @@ async function setDocumentAvailability(

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

return docsWithAvailability;
Expand Down
7 changes: 6 additions & 1 deletion api/src/service/workflowitem_get_details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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";

Expand All @@ -23,7 +24,11 @@ export async function getWorkflowitemDetails(
const workflowitemResult = await Cache.withCache(conn, ctx, async (cache) =>
WorkflowitemGetDetails.getWorkflowitemDetails(ctx, serviceUser, workflowitemId, {
getWorkflowitem: async () => {
return cache.getWorkflowitem(projectId, subprojectId, workflowitemId);
return WorkflowitemGet.getWorkflowitem(ctx, serviceUser, workflowitemId, {
getWorkflowitem: async () => {
return cache.getWorkflowitem(projectId, subprojectId, workflowitemId);
},
});
},
downloadDocument: async (docId) => {
return WorkflowitemDocumentDownloadService.getDocument(
Expand Down

0 comments on commit 3eec3eb

Please sign in to comment.