Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: Fix new bugs from disable/enable feature #537

Merged
merged 4 commits into from
Sep 16, 2020
Merged
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
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

<!-- ## Unreleased -->

<!-- ### Added -->
### Added

- Added confirmation dialog for disabling and enabling users [#537](https://github.com/openkfw/TruBudget/pull/537)
- Added table of user assignments for disable users dialog [#537](https://github.com/openkfw/TruBudget/pull/537)

<!-- ### Changed -->

<!-- ### Deprecated -->

<!-- ### Removed -->

<!-- ### Fixed -->
### Fixed

- Fixed the global permission list to set permissions to disable or enable users [#537](https://github.com/openkfw/TruBudget/pull/537)

<!-- ### Security -->

Expand Down
4 changes: 4 additions & 0 deletions api/src/authz/intents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type Intent =
| "global.createUser"
| "global.enableUser"
| "global.disableUser"
| "global.listAssignments"
| "global.createGroup"
| "user.authenticate"
| "user.changePassword"
Expand Down Expand Up @@ -70,6 +71,7 @@ export const globalIntents: Intent[] = [
"global.createUser",
"global.enableUser",
"global.disableUser",
"global.listAssignments",
"global.createGroup",
"network.registerNode",
"network.list",
Expand All @@ -88,6 +90,7 @@ export const userAssignableIntents: Intent[] = [
"global.createUser",
"global.enableUser",
"global.disableUser",
"global.listAssignments",
"global.createGroup",
"group.addUser",
"group.removeUser",
Expand Down Expand Up @@ -166,6 +169,7 @@ export const allIntents: Intent[] = [
"global.createUser",
"global.enableUser",
"global.disableUser",
"global.listAssignments",
"global.createGroup",
"user.authenticate",
"user.changePassword",
Expand Down
7 changes: 7 additions & 0 deletions api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import * as UserAuthenticateService from "./service/user_authenticate";
import * as UserCreateService from "./service/user_create";
import * as UserEnableService from "./service/user_enable";
import * as UserDisableService from "./service/user_disable";
import * as UserAssignmentsService from "./service/user_assignments_get";
import * as UserPasswordChangeService from "./service/user_password_change";
import * as UserPermissionGrantService from "./service/user_permission_grant";
import * as UserPermissionRevokeService from "./service/user_permission_revoke";
Expand Down Expand Up @@ -106,6 +107,7 @@ import * as UserAuthenticateAPI from "./user_authenticate";
import * as UserCreateAPI from "./user_create";
import * as UserEnableAPI from "./user_enable";
import * as UserDisableAPI from "./user_disable";
import * as UserAssignmentsAPI from "./user_listAssignments";
import * as UserListAPI from "./user_list";
import * as UserPasswordChangeAPI from "./user_password_change";
import * as UserPermissionGrantAPI from "./user_permission_grant";
Expand Down Expand Up @@ -310,6 +312,11 @@ UserDisableAPI.addHttpHandler(server, URL_PREFIX, {
UserDisableService.disableUser(db, ctx, issuer, orga, reqData),
});

UserAssignmentsAPI.addHttpHandler(server, URL_PREFIX, {
getUserAssignments: (ctx, issuer, orga, reqData) =>
UserAssignmentsService.getUserAssignments(db, ctx, issuer, orga, reqData),
});

UserListAPI.addHttpHandler(server, URL_PREFIX, {
listUsers: (ctx, issuer) => UserQueryService.getUsers(db, ctx, issuer),
listGroups: (ctx, issuer) => GroupQueryService.getGroups(db, ctx, issuer),
Expand Down
255 changes: 255 additions & 0 deletions api/src/service/domain/workflow/user_assignment_get.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import { assert } from "chai";

import Intent from "../../../authz/intents";
import { Ctx } from "../../../lib/ctx";
import * as Result from "../../../result";
import { NotAuthorized } from "../errors/not_authorized";
import { Identity } from "../organization/identity";
import { ServiceUser } from "../organization/service_user";
import * as UserRecord from "../organization/user_record";
import { getUserAssignments } from "./user_assignments_get";
import * as GlobalPermissions from "./global_permissions";
import * as UserAssignments from "../workflow/user_assignments";
import * as Project from "../workflow/project";
import * as Subproject from "../workflow/subproject";
import * as Workflowitem from "../workflow/workflowitem";
import { HiddenAssignments } from "./user_assignments";
import { VError } from "verror";

const ctx: Ctx = { requestId: "", source: "test" };
const root: ServiceUser = { id: "root", groups: [] };
const admin: ServiceUser = { id: "admin", groups: [] };
const member: ServiceUser = { id: "member", groups: [] };
const orgaA = "orgaA";
const otherOrganization = "otherOrganization";

const baseUser: UserRecord.UserRecord = {
id: "dummy",
createdAt: new Date().toISOString(),
displayName: "dummy",
organization: orgaA,
passwordHash: "12345",
address: "12345",
encryptedPrivKey: "12345",
permissions: {},
log: [],
additionalData: {},
};

const baseProject: Project.Project[] = [
{
assignee: member.id,
id: "projectId",
createdAt: new Date().toISOString(),
status: "open",
displayName: "baseUser",
description: "baseUser",
projectedBudgets: [],
permissions: {},
log: [],
additionalData: {},
tags: [],
},
];

const baseSubproject: Subproject.Subproject[] = [
{
assignee: member.id,
projectId: "projectId",
id: "subprojectId",
createdAt: new Date().toISOString(),
status: "open",
displayName: "baseUser",
description: "baseUser",
currency: "EUR",
projectedBudgets: [],
workflowitemOrdering: [],
permissions: {},
log: [],
additionalData: {},
},
];

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

const baseRepository = {
getAllProjects: async () => baseProject,
getSubprojects: async () => baseSubproject,
getWorkflowitems: async () => baseWorkflowitem,
getUser: async () => baseUser,
};

describe("Get user assignments: authorization and conditions", () => {
it("The root user can always view user assignments within the same organization", async () => {
const result = await getUserAssignments(ctx, member.id, root, orgaA, { ...baseRepository });

assert.isTrue(Result.isOk(result));
});

it("A user (including root) cannot view user assignments to users from other organizations", async () => {
const result = await getUserAssignments(ctx, member.id, root, otherOrganization, {
...baseRepository,
});

assert.isTrue(Result.isErr(result));
assert.instanceOf(result, NotAuthorized);
});

it("A user can always view user assignments of projects (returned as hidden)", async () => {
const result = await getUserAssignments(ctx, member.id, admin, orgaA, {
...baseRepository,
getAllProjects: async () => {
return [{ ...baseProject[0] }];
},
});

assert.isTrue(Result.isOk(result));
if (Result.isErr(result)) {
throw new VError(result, "global.listAssignments failed");
}
const userAssignments = result;
assert.isTrue(
userAssignments.hiddenAssignments !== undefined &&
userAssignments.hiddenAssignments.hasHiddenProjects === true,
);
});

it("A user can always view user assignments of subprojects (returned as hidden)", async () => {
const result = await getUserAssignments(ctx, member.id, admin, orgaA, {
...baseRepository,
getSubprojects: async () => {
return [{ ...baseSubproject[0] }];
},
});

assert.isTrue(Result.isOk(result));
if (Result.isErr(result)) {
throw new VError(result, "global.listAssignments failed");
}
const userAssignments = result;
assert.isTrue(
userAssignments.hiddenAssignments !== undefined &&
userAssignments.hiddenAssignments.hasHiddenSubprojects === true,
);
});

it("A user can always view user assignments of workflowitems (returned as hidden)", async () => {
const result = await getUserAssignments(ctx, member.id, admin, orgaA, {
...baseRepository,
getWorkflowitems: async () => {
return [{ ...baseWorkflowitem[0] }];
},
});

assert.isTrue(Result.isOk(result));
if (Result.isErr(result)) {
throw new VError(result, "global.listAssignments failed");
}
const userAssignments = result;
assert.isTrue(
userAssignments.hiddenAssignments !== undefined &&
userAssignments.hiddenAssignments.hasHiddenWorkflowitems === true,
);
});

it("A user can view user assignments of projects with view permissions", async () => {
const result = await getUserAssignments(ctx, member.id, admin, orgaA, {
...baseRepository,
getAllProjects: async () => {
return [
{
...baseProject[0],
permissions: {
"project.viewSummary": [admin.id],
"project.viewDetails": [admin.id],
},
},
];
},
});

assert.isTrue(Result.isOk(result));
if (Result.isErr(result)) {
throw new VError(result, "global.listAssignments failed");
}
const userAssignments = result;
assert.isTrue(
userAssignments.hiddenAssignments !== undefined &&
userAssignments.hiddenAssignments.hasHiddenProjects === false &&
userAssignments.projects !== undefined,
);
});

it("A user can view user assignments of subprojects with view permissions", async () => {
const result = await getUserAssignments(ctx, member.id, admin, orgaA, {
...baseRepository,
getSubprojects: async () => {
return [
{
...baseSubproject[0],
permissions: {
"subproject.viewSummary": [admin.id],
"subproject.viewDetails": [admin.id],
},
},
];
},
});

assert.isTrue(Result.isOk(result));
if (Result.isErr(result)) {
throw new VError(result, "global.listAssignments failed");
}
const userAssignments = result;
assert.isTrue(
userAssignments.hiddenAssignments !== undefined &&
userAssignments.hiddenAssignments.hasHiddenSubprojects === false &&
userAssignments.subprojects !== undefined,
);
});

it("A user can view user assignments of workflowitems with view permissions", async () => {
const result = await getUserAssignments(ctx, member.id, admin, orgaA, {
...baseRepository,
getWorkflowitems: async () => {
return [
{
...baseWorkflowitem[0],
permissions: {
"workflowitem.view": [admin.id],
},
},
];
},
});

assert.isTrue(Result.isOk(result));
if (Result.isErr(result)) {
throw new VError(result, "global.listAssignments failed");
}
const userAssignments = result;
assert.isTrue(
userAssignments.hiddenAssignments !== undefined &&
userAssignments.hiddenAssignments.hasHiddenWorkflowitems === false &&
userAssignments.workflowitems !== undefined,
);
});
});
9 changes: 9 additions & 0 deletions api/src/service/domain/workflow/user_assignments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@ import * as Project from "./project";
import * as Subproject from "./subproject";
import * as Workflowitem from "./workflowitem";

// HiddenAssignments gives information if the requested user has assignments,
// but the issuer has no permission to view the project, subproject or workflowitem
// where the requested users is assigned
export interface HiddenAssignments {
hasHiddenProjects: boolean;
hasHiddenSubprojects: boolean;
hasHiddenWorkflowitems: boolean;
}
export interface UserAssignments {
userId: string;
projects?: Project.Project[];
subprojects?: Subproject.Subproject[];
workflowitems?: Workflowitem.Workflowitem[];
hiddenAssignments?: HiddenAssignments;
}
Loading