Skip to content

Commit 1453c52

Browse files
AnnKasianAnna KasianFjortis
authored
feat: Adjust project permissions gf-351 (#401)
* feat: add the migrations gf-351 * feat: add possibility to processa a project permissions gf-351 * feat: add protected access to backend gf-351 * feat: add protected access to the frontend gf-351 * feat: modify permission access gf-351 * fix: improve access by manage and edit project gf-351 * fix: update access to analytic button gf-351 * fix: update access to key generation gf-351 * fix: improve consistency gf-351 * fix: update frontend routes gf-351 * fix: updated project Id check for permissions gf-351 * feat: add permision protection on backend routes gf-351 * fix: improve consistency gf-351 * fix: create a separate helper for project permissions gf-351 * fix: update permissions for selector on analitic page gf-351 * fix: improve consistency gf-351 * fix: update check user permissions hook gf-351 * fix: improve consistency gf-351 * fix: apdate project permission key type gf-351 * fix: update project permission key type gf-351 * fix: improve consistency gf-351 * fix: move method to project service gf-351 * fix: improve consistency gf-351 * fix: refactored projectdetailsmenu to take permissions as props gf-351 * fix: adjusted projectID mapping to number gf-351 * fix: adjusted projectID mapping tor gf-351 * fix: adjusted getpermittednavigationitem to use single filter gf-351 * fix: getpermittednavigationitemshelper bug with pageprojectpermission adjustment gf-351 * fix: getpermittednavigationitemshelper bug with pageprojectpermission adjustment gf-351 * fix: refactored getpermittednavigationitem to make use of both dtos gf-351 * fix: adjusted projects prop sent to projectdetailsmenu to fix all functionality gf-351 * fix: improve consistency gf-351 * fix: improve project details menu permissions gf-351 * fix: improve project details menu root permissions gf-351 * fix: update get all contributors gf-351 * fix: bug with search gf-351 * fix: improve consistency gf-351 * fix: bug with setup analitic button gf-351 * fix: improve consistency gf-351 --------- Co-authored-by: Anna Kasian <[email protected]> Co-authored-by: Joel Strid <[email protected]>
1 parent 4c5ad51 commit 1453c52

File tree

43 files changed

+823
-165
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+823
-165
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { type Knex } from "knex";
2+
3+
const TABLE_NAME = "project_permissions";
4+
5+
const PermissionKey = {
6+
EDIT_PROJECT: "edit_project",
7+
} as const;
8+
9+
function up(knex: Knex): Promise<void> {
10+
return knex(TABLE_NAME).insert({
11+
key: PermissionKey.EDIT_PROJECT,
12+
name: "Edit Project",
13+
});
14+
}
15+
16+
function down(knex: Knex): Promise<void> {
17+
return knex(TABLE_NAME)
18+
.where({
19+
key: PermissionKey.EDIT_PROJECT,
20+
})
21+
.del();
22+
}
23+
24+
export { down, up };
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { type Knex } from "knex";
2+
3+
const TABLE_NAME = "project_permissions";
4+
5+
const PermissionKey = {
6+
MANAGE_PROJECT: "manage_project",
7+
} as const;
8+
9+
function up(knex: Knex): Promise<void> {
10+
return knex(TABLE_NAME).insert({
11+
key: PermissionKey.MANAGE_PROJECT,
12+
name: "Manage Project",
13+
});
14+
}
15+
16+
function down(knex: Knex): Promise<void> {
17+
return knex(TABLE_NAME)
18+
.where({
19+
key: PermissionKey.MANAGE_PROJECT,
20+
})
21+
.del();
22+
}
23+
24+
export { down, up };

apps/backend/src/libs/hooks/check-user-permissions.hook.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,45 @@
1-
import { type FastifyRequest } from "fastify";
2-
3-
import { ExceptionMessage } from "~/libs/enums/enums.js";
1+
import {
2+
ExceptionMessage,
3+
type PermissionKey,
4+
type ProjectPermissionKey,
5+
} from "~/libs/enums/enums.js";
46
import { checkHasPermission } from "~/libs/helpers/helpers.js";
5-
import { type APIPreHandler } from "~/libs/modules/controller/controller.js";
7+
import {
8+
type APIHandlerOptions,
9+
type APIPreHandler,
10+
} from "~/libs/modules/controller/controller.js";
611
import { HTTPCode, HTTPError } from "~/libs/modules/http/http.js";
12+
import { type UserAuthResponseDto } from "~/modules/users/users.js";
713

8-
const checkUserPermissions = (routePermissions: string[]): APIPreHandler => {
9-
return (request: FastifyRequest, _, done): void => {
10-
const { user } = request;
14+
import { type ValueOf } from "../types/types.js";
1115

12-
if (!user) {
13-
throw new HTTPError({
14-
message: ExceptionMessage.USER_NOT_FOUND,
15-
status: HTTPCode.UNAUTHORIZED,
16-
});
17-
}
16+
const checkUserPermissions = (
17+
permissions: ValueOf<typeof PermissionKey>[],
18+
projectsPermissions?: ValueOf<typeof ProjectPermissionKey>[],
19+
getProjectId?: (options: APIHandlerOptions) => number | undefined,
20+
): APIPreHandler => {
21+
return (options, done): void => {
22+
const user = options.user as UserAuthResponseDto;
23+
const projectId = getProjectId?.(options);
24+
25+
const userPermissions = user.groups.flatMap((group) => group.permissions);
26+
const projectPermissions = user.projectGroups
27+
.filter((group) => projectId && group.projectId === projectId)
28+
.flatMap((projectGroup) => projectGroup.permissions);
1829

19-
const hasPermission = checkHasPermission(
20-
routePermissions,
21-
user.groups.flatMap((group) => group.permissions),
30+
const hasGlobalPermission = checkHasPermission(
31+
permissions,
32+
userPermissions,
2233
);
2334

24-
if (!hasPermission) {
35+
const hasProjectPermission = projectId
36+
? checkHasPermission(
37+
projectsPermissions as ValueOf<typeof ProjectPermissionKey>[],
38+
projectPermissions,
39+
)
40+
: true;
41+
42+
if (!hasGlobalPermission && !hasProjectPermission) {
2543
throw new HTTPError({
2644
message: ExceptionMessage.NO_PERMISSION,
2745
status: HTTPCode.FORBIDDEN,

apps/backend/src/libs/modules/controller/base-controller.module.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type ServerApplicationRouteParameters } from "~/libs/modules/server-app
44
import {
55
type APIHandler,
66
type APIHandlerOptions,
7+
type APIPreHandler,
78
type Controller,
89
type ControllerRouteParameters,
910
} from "./libs/types/types.js";
@@ -34,6 +35,20 @@ class BaseController implements Controller {
3435
return await reply.status(status).send(payload);
3536
}
3637

38+
private mapPreHandler(
39+
preHandler: APIPreHandler,
40+
request: Parameters<
41+
NonNullable<ServerApplicationRouteParameters["preHandlers"]>[number]
42+
>[0],
43+
done: Parameters<
44+
NonNullable<ServerApplicationRouteParameters["preHandlers"]>[number]
45+
>[2],
46+
): void {
47+
const handlerOptions = this.mapRequest(request);
48+
49+
preHandler(handlerOptions, done);
50+
}
51+
3752
private mapRequest(
3853
request: Parameters<ServerApplicationRouteParameters["handler"]>[0],
3954
): APIHandlerOptions {
@@ -49,13 +64,18 @@ class BaseController implements Controller {
4964
}
5065

5166
public addRoute(options: ControllerRouteParameters): void {
52-
const { handler, path } = options;
67+
const { handler, path, preHandlers = [] } = options;
5368
const fullPath = this.apiUrl + path;
5469

5570
this.routes.push({
5671
...options,
5772
handler: (request, reply) => this.mapHandler(handler, request, reply),
5873
path: fullPath,
74+
preHandlers: preHandlers.map((preHandler) => {
75+
return (request, _, done): void => {
76+
this.mapPreHandler(preHandler, request, done);
77+
};
78+
}),
5979
});
6080
}
6181
}
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
import { type FastifyReply, type FastifyRequest } from "fastify";
1+
import { type APIHandlerOptions } from "./api-handler-options.type.js";
22

3-
type APIPreHandler = (
4-
request: FastifyRequest,
5-
reply: FastifyReply,
6-
done: () => void,
7-
) => void;
3+
type APIPreHandler = (options: APIHandlerOptions, done: () => void) => void;
84

95
export { type APIPreHandler };

apps/backend/src/libs/modules/server-application/libs/types/server-application-route-parameters.type.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { type FastifyReply, type FastifyRequest } from "fastify";
22

3-
import { type APIPreHandler } from "~/libs/modules/controller/controller.js";
43
import { type HTTPMethod } from "~/libs/modules/http/http.js";
54
import { type ValidationSchema } from "~/libs/types/types.js";
65

@@ -11,7 +10,11 @@ type ServerApplicationRouteParameters = {
1110
) => Promise<void> | void;
1211
method: HTTPMethod;
1312
path: string;
14-
preHandlers?: APIPreHandler[];
13+
preHandlers?: ((
14+
request: FastifyRequest,
15+
reply: FastifyReply,
16+
done: () => void,
17+
) => void)[];
1518
validation?: {
1619
body?: ValidationSchema;
1720
query?: ValidationSchema;

apps/backend/src/modules/activity-logs/activity-log.controller.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1-
import { APIPath } from "~/libs/enums/enums.js";
1+
import {
2+
APIPath,
3+
PermissionKey,
4+
ProjectPermissionKey,
5+
} from "~/libs/enums/enums.js";
6+
import { checkHasPermission } from "~/libs/helpers/helpers.js";
7+
import { checkUserPermissions } from "~/libs/hooks/hooks.js";
28
import {
39
type APIHandlerOptions,
410
type APIHandlerResponse,
511
BaseController,
612
} from "~/libs/modules/controller/controller.js";
713
import { HTTPCode } from "~/libs/modules/http/http.js";
814
import { type Logger } from "~/libs/modules/logger/logger.js";
15+
import { type ProjectGroupService } from "~/modules/project-groups/project-groups.js";
16+
import { type UserAuthResponseDto } from "~/modules/users/users.js";
917

18+
import { type PermissionGetAllItemResponseDto } from "../permissions/libs/types/types.js";
1019
import { type ActivityLogService } from "./activity-log.service.js";
1120
import { ActivityLogsApiPath } from "./libs/enums/enums.js";
1221
import {
@@ -53,11 +62,17 @@ import {
5362

5463
class ActivityLogController extends BaseController {
5564
private activityLogService: ActivityLogService;
65+
private projectGroupService: ProjectGroupService;
5666

57-
public constructor(logger: Logger, activityLogService: ActivityLogService) {
67+
public constructor(
68+
logger: Logger,
69+
activityLogService: ActivityLogService,
70+
projectGroupService: ProjectGroupService,
71+
) {
5872
super(logger, APIPath.ACTIVITY_LOGS);
5973

6074
this.activityLogService = activityLogService;
75+
this.projectGroupService = projectGroupService;
6176

6277
this.addRoute({
6378
handler: (options) =>
@@ -79,10 +94,21 @@ class ActivityLogController extends BaseController {
7994
this.findAll(
8095
options as APIHandlerOptions<{
8196
query: ActivityLogQueryParameters;
97+
user: UserAuthResponseDto;
8298
}>,
8399
),
84100
method: "GET",
85101
path: ActivityLogsApiPath.ROOT,
102+
preHandlers: [
103+
checkUserPermissions(
104+
[PermissionKey.VIEW_ALL_PROJECTS, PermissionKey.MANAGE_ALL_PROJECTS],
105+
[
106+
ProjectPermissionKey.VIEW_PROJECT,
107+
ProjectPermissionKey.EDIT_PROJECT,
108+
ProjectPermissionKey.MANAGE_PROJECT,
109+
],
110+
),
111+
],
86112
validation: {
87113
query: activityLogGetValidationSchema,
88114
},
@@ -166,10 +192,37 @@ class ActivityLogController extends BaseController {
166192
private async findAll(
167193
options: APIHandlerOptions<{
168194
query: ActivityLogQueryParameters;
195+
user: UserAuthResponseDto;
169196
}>,
170197
): Promise<APIHandlerResponse> {
198+
const { contributorName, endDate, projectId, startDate } = options.query;
199+
const { user } = options;
200+
201+
const groups = await this.projectGroupService.findAllByUserId(user.id);
202+
const rootPermissions: PermissionGetAllItemResponseDto[] =
203+
user.groups.flatMap((group) =>
204+
group.permissions.map((permission) => ({
205+
id: permission.id,
206+
key: permission.key,
207+
name: permission.name,
208+
})),
209+
);
210+
211+
const hasRootPermission = checkHasPermission(
212+
[PermissionKey.MANAGE_ALL_PROJECTS, PermissionKey.VIEW_ALL_PROJECTS],
213+
rootPermissions,
214+
);
215+
const userProjectIds = groups.map(({ projectId }) => projectId.id);
216+
171217
return {
172-
payload: await this.activityLogService.findAll(options.query),
218+
payload: await this.activityLogService.findAll({
219+
contributorName,
220+
endDate,
221+
hasRootPermission,
222+
projectId,
223+
startDate,
224+
userProjectIds,
225+
}),
173226
status: HTTPCode.OK,
174227
};
175228
}

apps/backend/src/modules/activity-logs/activity-log.repository.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { EMPTY_LENGTH } from "~/libs/constants/constants.js";
12
import { type Repository } from "~/libs/types/types.js";
23

34
import { ActivityLogEntity } from "./activity-log.entity.js";
@@ -43,9 +44,12 @@ class ActivityLogRepository implements Repository {
4344
public async findAll({
4445
contributorName,
4546
endDate,
47+
permittedProjectIds,
4648
projectId,
4749
startDate,
48-
}: ActivityLogQueryParameters): Promise<{ items: ActivityLogEntity[] }> {
50+
}: {
51+
permittedProjectIds: number[] | undefined;
52+
} & ActivityLogQueryParameters): Promise<{ items: ActivityLogEntity[] }> {
4953
const query = this.activityLogModel
5054
.query()
5155
.withGraphFetched("[project, createdByUser]")
@@ -61,11 +65,16 @@ class ActivityLogRepository implements Repository {
6165
query.whereILike("gitEmail:contributor.name", `%${contributorName}%`);
6266
}
6367

68+
const hasPermissionedProjects =
69+
permittedProjectIds && permittedProjectIds.length !== EMPTY_LENGTH;
70+
6471
if (projectId) {
6572
query.where("project_id", projectId);
73+
} else if (hasPermissionedProjects) {
74+
query.whereIn("project_id", permittedProjectIds);
6675
}
6776

68-
const activityLogs = await query.execute();
77+
const activityLogs = await query.orderBy("date");
6978

7079
return {
7180
items: activityLogs.map((activityLog) =>

apps/backend/src/modules/activity-logs/activity-log.service.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,33 @@ class ActivityLogService implements Service {
139139
public async findAll({
140140
contributorName,
141141
endDate,
142+
hasRootPermission,
142143
projectId,
143144
startDate,
144-
}: ActivityLogQueryParameters): Promise<ActivityLogGetAllAnalyticsResponseDto> {
145+
userProjectIds,
146+
}: {
147+
hasRootPermission: boolean;
148+
userProjectIds: number[];
149+
} & ActivityLogQueryParameters): Promise<ActivityLogGetAllAnalyticsResponseDto> {
150+
const projectIdParsed = projectId ? Number(projectId) : undefined;
151+
152+
let permittedProjectIds: number[];
153+
154+
if (projectIdParsed) {
155+
if (!hasRootPermission && !userProjectIds.includes(projectIdParsed)) {
156+
throw new ActivityLogError({
157+
message: ExceptionMessage.NO_PERMISSION,
158+
status: HTTPCode.FORBIDDEN,
159+
});
160+
}
161+
162+
permittedProjectIds = [projectIdParsed];
163+
} else if (hasRootPermission) {
164+
permittedProjectIds = [];
165+
} else {
166+
permittedProjectIds = userProjectIds;
167+
}
168+
145169
const formattedStartDate = formatDate(
146170
getStartOfDay(new Date(startDate)),
147171
"yyyy-MM-dd",
@@ -155,6 +179,7 @@ class ActivityLogService implements Service {
155179
const activityLogsEntities = await this.activityLogRepository.findAll({
156180
contributorName,
157181
endDate: formattedEndDate,
182+
permittedProjectIds,
158183
projectId,
159184
startDate: formattedStartDate,
160185
});
@@ -167,11 +192,13 @@ class ActivityLogService implements Service {
167192
? this.contributorService.findAllByProjectId({
168193
contributorName: contributorName ?? "",
169194
hasHidden: false,
195+
permittedProjectIds,
170196
projectId: Number(projectId),
171197
})
172198
: this.contributorService.findAllWithoutPagination({
173199
contributorName: contributorName ?? "",
174200
hasHidden: false,
201+
permittedProjectIds,
175202
}));
176203

177204
const dateRange = getDateRange(formattedStartDate, formattedEndDate);

0 commit comments

Comments
 (0)