Skip to content
This repository was archived by the owner on Feb 24, 2026. It is now read-only.

Commit 0152b94

Browse files
authored
GZY-437: Restrict task visibility to employee’s own projects in ERP (#117)
* feat: enforce consistent project visibility rules across roles - This change enforces a consistent project-visibility rule across the system, ensuring that both Managers and Employees follow the same assigned-projects restriction. - The previous behavior allowed Managers to see all projects by default, which conflicted with the intended access-control model and caused inconsistencies in how visibility rules were applied across different endpoints. - To maintain coherence with the platform's permission model and avoid privilege gaps between roles, the assigned-projects rule was formally applied to Employees as well. - Backend services that return projects or project-related tasks were updated to ensure permission checks are applied uniformly, preventing accidental exposure of unassigned projects. - A migration was added to guarantee that roles have the correct permission associations, avoiding environments where outdated role-permission links could break the new visibility logic. - Minor frontend adjustments were made to fix the employee filter in the project-management dashboard, since incorrect parameter mapping caused the API to receive invalid filter values and could misapply visibility rules. * revert: changes in TenantAwareCrudService This change was reverted because it was outside the scope of the current task. * chore: revert task service changes - Reverts changes in task service because it are out of scope for this PR. * fix: handle null organizationSprintId in task filtering - Updated the task service to treat 'null' as a valid case for setting organizationSprintId to null in the filter options.
1 parent c144575 commit 0152b94

19 files changed

Lines changed: 468 additions & 231 deletions

File tree

apps/gauzy/src/app/pages/dashboard/project-management/project-management-details/project-management-details.component.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ import { MyTaskDialogComponent } from '../../../tasks/components/my-task-dialog/
2323

2424
@UntilDestroy({ checkProperties: true })
2525
@Component({
26-
selector: 'gauzy-project-management-details',
27-
templateUrl: './project-management-details.component.html',
28-
styleUrls: ['./project-management-details.component.scss'],
29-
standalone: false
26+
selector: 'gauzy-project-management-details',
27+
templateUrl: './project-management-details.component.html',
28+
styleUrls: ['./project-management-details.component.scss'],
29+
standalone: false
3030
})
3131
export class ProjectManagementDetailsComponent extends PaginationFilterBaseComponent implements OnInit, OnDestroy {
3232
private _smartTableSource: ServerDataSource;
@@ -109,7 +109,7 @@ export class ProjectManagementDetailsComponent extends PaginationFilterBaseCompo
109109
where: {
110110
organizationId,
111111
tenantId,
112-
...(this.selectedEmployeeId ? { employeeId: this.selectedEmployeeId } : {}),
112+
...(this.selectedEmployeeId ? { members: { id: this.selectedEmployeeId } } : {}),
113113
...(this.selectedProjectId ? { projectId: this.selectedProjectId } : {}),
114114
...(this.filters.where ? this.filters.where : {})
115115
}
@@ -237,5 +237,7 @@ export class ProjectManagementDetailsComponent extends PaginationFilterBaseCompo
237237
this._router.navigate(['/pages/tasks/me']);
238238
}
239239

240-
ngOnDestroy(): void {}
240+
ngOnDestroy(): void {
241+
// No cleanup needed - subscriptions are handled by async pipe
242+
}
241243
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { MigrationInterface, QueryRunner } from 'typeorm';
2+
import * as chalk from 'chalk';
3+
import { DatabaseTypeEnum } from '@gauzy/config';
4+
import { RolePermissionUtils } from '../../role-permission/utils';
5+
6+
export class MigrateRolePermissions1763105402336 implements MigrationInterface {
7+
name = 'MigrateRolePermissions1763105402336';
8+
9+
public async up(queryRunner: QueryRunner): Promise<void> {
10+
console.log(chalk.yellow(`${this.constructor.name} start running!`));
11+
12+
switch (queryRunner.connection.options.type) {
13+
case DatabaseTypeEnum.sqlite:
14+
case DatabaseTypeEnum.betterSqlite3:
15+
case DatabaseTypeEnum.postgres:
16+
case DatabaseTypeEnum.mysql:
17+
try {
18+
await RolePermissionUtils.migrateRolePermissions(queryRunner);
19+
} catch (error) {
20+
console.log(chalk.red(`Error while migrating missing role permisions: ${error}`));
21+
}
22+
break;
23+
default:
24+
throw Error(`Unsupported database: ${queryRunner.connection.options.type}`);
25+
}
26+
}
27+
28+
public async down(queryRunner: QueryRunner): Promise<void> {
29+
// This migration cannot be reverted - permission changes are data-dependent
30+
}
31+
}

packages/core/src/lib/organization-project/organization-project.service.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
IPagination,
1717
RolesEnum,
1818
EntitySubscriptionTypeEnum,
19-
ITimeLog
19+
ITimeLog,
20+
PermissionsEnum
2021
} from '@gauzy/contracts';
2122
import { getConfig } from '@gauzy/config';
2223
import { CustomEmbeddedFieldConfig } from '@gauzy/common';
@@ -445,6 +446,37 @@ export class OrganizationProjectService extends TenantAwareCrudService<Organizat
445446
query.andWhere(`project_team.id = :organizationTeamId`, { organizationTeamId });
446447
}
447448

449+
const userEmployeeId = RequestContext.currentUser().employeeId;
450+
if (
451+
employeeId !== userEmployeeId &&
452+
RequestContext.hasPermission(PermissionsEnum.VIEW_ASSIGNED_PROJECTS_ONLY)
453+
) {
454+
query.andWhere((qb: SelectQueryBuilder<OrganizationProject>) => {
455+
const subQuery = qb.subQuery();
456+
subQuery
457+
.select(p('"project"."id" AS "id"'))
458+
.from('organization_project', 'project')
459+
.innerJoin('project.members', 'project_members');
460+
461+
subQuery
462+
.where(p('"project_members"."employeeId" = :userEmployeeId'), { userEmployeeId })
463+
.andWhere(p('"project"."tenantId" = :tenantId'), { tenantId })
464+
.andWhere(p('"project"."organizationId" = :organizationId'), { organizationId });
465+
466+
if (isNotEmpty(organizationContactId)) {
467+
subQuery.andWhere(p('"project"."organizationContactId" = :organizationContactId'), {
468+
organizationContactId
469+
});
470+
}
471+
472+
if (isNotEmpty(organizationTeamId)) {
473+
subQuery.andWhere(p('"project_team"."id" = :organizationTeamId'), { organizationTeamId });
474+
}
475+
476+
return p(`"${query.alias}"."id" IN `) + subQuery.distinct(true).getQuery();
477+
});
478+
}
479+
448480
// Get the results
449481
return query.getMany();
450482
}
@@ -461,6 +493,15 @@ export class OrganizationProjectService extends TenantAwareCrudService<Organizat
461493
options.where.organizationContactId = IsNull();
462494
}
463495

496+
const userEmployeeId = RequestContext.currentUser().employeeId;
497+
if (isNotEmpty(userEmployeeId) && RequestContext.hasPermission(PermissionsEnum.VIEW_ASSIGNED_PROJECTS_ONLY)) {
498+
options.where.members = {
499+
employee: {
500+
id: userEmployeeId
501+
}
502+
};
503+
}
504+
464505
// Call the parent class's findAll method with the modified options
465506
return super.findAll(options);
466507
}
@@ -527,6 +568,17 @@ export class OrganizationProjectService extends TenantAwareCrudService<Organizat
527568
}
528569
}
529570

571+
if (RequestContext.hasPermission(PermissionsEnum.VIEW_ASSIGNED_PROJECTS_ONLY)) {
572+
const employeeAssignedProjects = await this.typeOrmOrganizationProjectEmployeeRepository.find({
573+
where: {
574+
employeeId: RequestContext.currentUser().employeeId,
575+
organizationId: options.where.organizationId
576+
}
577+
});
578+
579+
options.where.id = In(employeeAssignedProjects.map((employee) => employee.organizationProjectId));
580+
}
581+
530582
// Call the parent class's paginate method with the modified options
531583
return super.paginate(options);
532584
}

packages/core/src/lib/role-permission/default-role-permissions.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,7 @@ export const DEFAULT_ROLE_PERMISSIONS = [
500500
PermissionsEnum.PROJECT_MODULE_READ,
501501
PermissionsEnum.PROJECT_MODULE_UPDATE,
502502
PermissionsEnum.PROJECT_MODULE_DELETE,
503+
PermissionsEnum.VIEW_ASSIGNED_PROJECTS_ONLY,
503504
/** Dashboard */
504505
PermissionsEnum.DASHBOARD_CREATE,
505506
PermissionsEnum.DASHBOARD_READ,
@@ -569,7 +570,83 @@ export const DEFAULT_ROLE_PERMISSIONS = [
569570
},
570571
{
571572
role: RolesEnum.MANAGER,
572-
defaultEnabledPermissions: [PermissionsEnum.VIEW_ASSIGNED_PROJECTS_ONLY]
573+
defaultEnabledPermissions: [
574+
/** Dashboards */
575+
PermissionsEnum.TEAM_DASHBOARD,
576+
PermissionsEnum.PROJECT_MANAGEMENT_DASHBOARD,
577+
PermissionsEnum.TIME_TRACKING_DASHBOARD,
578+
PermissionsEnum.DASHBOARD_READ,
579+
/** Employee Selection */
580+
PermissionsEnum.SELECT_EMPLOYEE,
581+
PermissionsEnum.CHANGE_SELECTED_EMPLOYEE,
582+
PermissionsEnum.CHANGE_SELECTED_ORGANIZATION,
583+
PermissionsEnum.ORG_EMPLOYEES_VIEW,
584+
/** Tasks */
585+
PermissionsEnum.ORG_TASK_ADD,
586+
PermissionsEnum.ORG_TASK_VIEW,
587+
PermissionsEnum.ORG_TASK_EDIT,
588+
PermissionsEnum.ORG_TASK_DELETE,
589+
PermissionsEnum.ORG_TASK_SETTING,
590+
PermissionsEnum.ORG_TEAM_EDIT_ACTIVE_TASK,
591+
/** Time Off */
592+
PermissionsEnum.TIME_OFF_ADD,
593+
PermissionsEnum.TIME_OFF_VIEW,
594+
PermissionsEnum.TIME_OFF_EDIT,
595+
PermissionsEnum.TIME_OFF_DELETE,
596+
/** Approvals */
597+
PermissionsEnum.APPROVAL_POLICY_VIEW,
598+
PermissionsEnum.REQUEST_APPROVAL_EDIT,
599+
PermissionsEnum.REQUEST_APPROVAL_VIEW,
600+
/** Projects */
601+
PermissionsEnum.ACCESS_PRIVATE_PROJECTS,
602+
PermissionsEnum.ORG_PROJECT_VIEW,
603+
PermissionsEnum.ORG_PROJECT_EDIT,
604+
PermissionsEnum.VIEW_ASSIGNED_PROJECTS_ONLY,
605+
/** Timesheet */
606+
PermissionsEnum.TIMESHEET_EDIT_TIME,
607+
PermissionsEnum.CAN_APPROVE_TIMESHEET,
608+
/** Invoices */
609+
PermissionsEnum.INVOICES_VIEW,
610+
PermissionsEnum.INVOICES_EDIT,
611+
/** Tags */
612+
PermissionsEnum.ORG_TAGS_ADD,
613+
PermissionsEnum.ORG_TAGS_VIEW,
614+
PermissionsEnum.ORG_TAGS_EDIT,
615+
PermissionsEnum.ORG_TAGS_DELETE,
616+
/** Sprints */
617+
PermissionsEnum.ORG_SPRINT_ADD,
618+
PermissionsEnum.ORG_SPRINT_EDIT,
619+
PermissionsEnum.ORG_SPRINT_VIEW,
620+
PermissionsEnum.ORG_SPRINT_DELETE,
621+
/** Contacts */
622+
PermissionsEnum.ORG_CONTACT_VIEW,
623+
/** Daily Plans */
624+
PermissionsEnum.DAILY_PLAN_CREATE,
625+
PermissionsEnum.DAILY_PLAN_READ,
626+
PermissionsEnum.DAILY_PLAN_UPDATE,
627+
PermissionsEnum.DAILY_PLAN_DELETE,
628+
/** Project Modules */
629+
PermissionsEnum.PROJECT_MODULE_CREATE,
630+
PermissionsEnum.PROJECT_MODULE_READ,
631+
PermissionsEnum.PROJECT_MODULE_UPDATE,
632+
PermissionsEnum.PROJECT_MODULE_DELETE,
633+
/** Teams */
634+
PermissionsEnum.ORG_TEAM_ADD,
635+
PermissionsEnum.ORG_TEAM_VIEW,
636+
PermissionsEnum.ORG_TEAM_EDIT,
637+
PermissionsEnum.ORG_TEAM_JOIN_REQUEST_VIEW,
638+
PermissionsEnum.ORG_TEAM_JOIN_REQUEST_EDIT,
639+
/** Other */
640+
PermissionsEnum.EVENT_TYPES_VIEW,
641+
PermissionsEnum.TIME_TRACKER,
642+
PermissionsEnum.MEDIA_GALLERY_VIEW,
643+
PermissionsEnum.EQUIPMENT_APPROVE_REQUEST,
644+
/** Time Management */
645+
PermissionsEnum.ALLOW_DELETE_TIME,
646+
PermissionsEnum.ALLOW_MODIFY_TIME,
647+
PermissionsEnum.ALLOW_MANUAL_TIME,
648+
PermissionsEnum.DELETE_SCREENSHOTS
649+
]
573650
},
574651
{
575652
role: RolesEnum.VIEWER,

0 commit comments

Comments
 (0)