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

[Fix] Correct project manager permission validation logic #8794

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 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
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ export class ProjectEditMutationComponent extends TranslationBaseComponent imple
*/
private _getGithubIntegrationTenant() {
this.integration$ = this._activatedRoute.data.pipe(
map(({ integration }) => integration),
map(({ integration }) => {
console.log(integration);
return integration;
}),
untilDestroyed(this) // Automatically unsubscribes when the component is destroyed
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
TagsOnlyComponent,
VisibilityComponent
} from '@gauzy/ui-core/shared';
import { NgxPermissionsService } from 'ngx-permissions';

@UntilDestroy({ checkProperties: true })
@Component({
Expand Down Expand Up @@ -86,7 +87,8 @@ export class ProjectListComponent extends PaginationFilterBaseComponent implemen
private readonly _toastrService: ToastrService,
private readonly _store: Store,
private readonly _dialogService: NbDialogService,
private readonly _organizationProjectStore: OrganizationProjectStore
private readonly _organizationProjectStore: OrganizationProjectStore,
private readonly _permissionsService: NgxPermissionsService
) {
super(translateService);
this.setView();
Expand Down Expand Up @@ -279,6 +281,10 @@ export class ProjectListComponent extends PaginationFilterBaseComponent implemen
.map((member: IOrganizationProjectEmployee) => member.employee);
}

isManagerOfProject(project: IOrganizationProject): boolean {
return project.members.some((member) => member.isManager && member.employee.userId === this._store.user.id);
}

/**
* Retrieves the non-manager employees from the list of members.
*
Expand Down Expand Up @@ -355,7 +361,7 @@ export class ProjectListComponent extends PaginationFilterBaseComponent implemen
*/
private filterProjectMembers(project: IOrganizationProject): IOrganizationProject {
project.members = project.members.filter(
(member: IOrganizationProjectEmployee) => member.employeeId === this._store.user?.employeeId
(member: IOrganizationProjectEmployee) => member.employeeId === this._store.user?.employee?.id
);
return project;
}
Expand Down Expand Up @@ -538,6 +544,28 @@ export class ProjectListComponent extends PaginationFilterBaseComponent implemen
this.disableButton = !isSelected;
this.selectedProject = isSelected ? data : null;

if (isSelected && this.selectedProject) {
// Check if the user is manager of the selected project
const isManager = this.isManagerOfProject(this.selectedProject);

// Check if the user has all the required permissions
const hasAllPermissions = this.hasAllPermissions([
PermissionsEnum.ALL_ORG_EDIT,
PermissionsEnum.ORG_PROJECT_EDIT,
PermissionsEnum.ORG_PROJECT_DELETE
]);

// Dynamically assign or remove the CAN_MANAGE_PROJECT permissions if either condition is true
if (!hasAllPermissions) {
const permissions = [PermissionsEnum.ORG_PROJECT_EDIT, PermissionsEnum.ORG_PROJECT_DELETE];

permissions.forEach((permission) =>
isManager
? this._permissionsService.addPermission(permission)
: this._permissionsService.removePermission(permission)
);
}
}
if (this._isGridCardLayout && this._grid) {
if (this._grid.customComponentInstance().constructor === ProjectOrganizationGridComponent) {
this.disableButton = true;
Expand Down Expand Up @@ -587,6 +615,10 @@ export class ProjectListComponent extends PaginationFilterBaseComponent implemen
}
}

hasAllPermissions(permissions: PermissionsEnum[]): boolean {
return permissions.every((permission) => this._permissionsService.getPermission(permission));
}

/**
* Clear the selected item and deselect all table rows.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// src/app/guards/project-manager-permission.guard.ts
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { ProjectManagerGuard } from './project-manager.guard';
import { PermissionsGuard } from '@gauzy/ui-core/core';

@Injectable({
providedIn: 'root'
})
export class ProjectManagerPermissionGuard {
constructor(
private readonly _projectManagerGuard: ProjectManagerGuard,
private readonly _permissionsGuard: PermissionsGuard,
private readonly _router: Router
) {}

/**
* First checks if the user is a manager of the project.
* If the user is a manager, allow access without checking permissions.
* If the user is not a manager, check if the user has the required permissions.
*
* @param route - The route being activated.
* @param state - The state of the router.
* @returns Observable<boolean> - True if either the manager check passes or permissions check passes, false otherwise.
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return new Observable<boolean>((observer) => {
// First, check if the user is a manager of the project using ProjectManagerGuard
this._projectManagerGuard.canActivate(route, state).subscribe((isManager) => {
if (isManager) {
// If the user is a manager, allow access without checking permissions
observer.next(true);
observer.complete();
} else {
// If the user is not a manager, check if they have the required permissions
this._permissionsGuard.canActivate(route, state).subscribe((hasPermissions) => {
if (hasPermissions) {
observer.next(true);
} else {
// Redirect if neither manager check nor permissions pass
this._router.navigate([route.data['permissions'].redirectTo || '/pages/dashboard']);
observer.next(false);
}
observer.complete();
});
}
});
});
}
}
61 changes: 61 additions & 0 deletions apps/gauzy/src/app/pages/projects/guards/project-manager.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { PermissionsEnum } from '@gauzy/contracts';
import { OrganizationProjectsService, Store } from '@gauzy/ui-core/core';

@Injectable({
providedIn: 'root'
})
export class ProjectManagerGuard implements CanActivate {
constructor(
private readonly _store: Store,
private readonly _router: Router,
private readonly _projectService: OrganizationProjectsService
) {}

/**
* Checks if the user is allowed to activate the route based on their manager status for the project.
*
* This guard ensures that the user either has the required permissions or is a manager of the project
* to access the route.
*
* @param route - The route being navigated to.
* @param state - The current state of the router.
* @returns An observable that resolves to a boolean indicating whether the user is allowed to activate the route.
*/
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
if (
!this._store.user?.employee &&
this._store.hasAllPermissions(PermissionsEnum.ORG_PROJECT_EDIT, PermissionsEnum.ORG_PROJECT_DELETE)
) {
return of(true);
}
// Get the project ID from the route parameters

const projectId = route.paramMap.get('id');
const employeeId = this._store.user?.employee?.id;

if (!projectId) {
this._router.navigate(['/pages/dashboard']);
return of(false);
}

// Check if the user has the necessary permissions or is a manager of the project
return this._projectService.isManagerOfProject(projectId, employeeId).pipe(
map((isManager) => {
if (isManager) {
return true; // Allow access if the user is a manager or has the necessary permissions
} else {
this._router.navigate(['/pages/dashboard']); // Redirect to dashboard if not a manager or does not have permission
return false;
}
}),
catchError(() => {
this._router.navigate(['/pages/dashboard']);
return of(false); // In case of an error, redirect to dashboard
})
);
}
}
3 changes: 2 additions & 1 deletion apps/gauzy/src/app/pages/projects/projects-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ProjectResolver } from './project.resolver';
import { ProjectCreateMutationComponent } from './components/project-create/create.component';
import { ProjectEditMutationComponent } from './components/project-edit/edit.component';
import { ProjectListComponent } from './components/project-list/list.component';
import { ProjectManagerPermissionGuard } from './guards/project-manager-permission.guard';

const routes: Routes = [
{
Expand Down Expand Up @@ -61,7 +62,7 @@ const routes: Routes = [
{
path: 'edit',
component: ProjectEditMutationComponent,
canActivate: [PermissionsGuard],
canActivate: [ProjectManagerPermissionGuard],
data: {
permissions: {
only: [PermissionsEnum.ALL_ORG_EDIT, PermissionsEnum.ORG_PROJECT_EDIT],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export class TeamsMutationComponent implements OnInit {
distinctUntilChange(),
filter((organization: IOrganization) => !!organization),
tap((organization: IOrganization) => (this.organization = organization)),
tap(() => this.initializeManager()),
tap(() => this.patchFormValue()),
untilDestroyed(this)
)
Expand All @@ -71,6 +72,18 @@ export class TeamsMutationComponent implements OnInit {
});
}

/**
* Automatically sets the current user as a manager if they are an employee
*/
initializeManager() {
const { employee } = this.store.user;
if (employee) {
this.form.patchValue({
managerIds: [employee.id] // Set the logged-in user's employeeId as the default manager
});
this.form.get('managerIds').updateValueAndValidity();
}
}
/**
* Set Form Values based on an existing team.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { SetMetadata, UseGuards, applyDecorators } from '@nestjs/common';
import { PERMISSIONS_METADATA } from '@gauzy/constants';
import { PermissionsEnum } from '@gauzy/contracts';
import { ManagerOrPermissionGuard } from '../guards/manager-or-permission.guard';

export const ManagerOrPermissions = (...permissions: PermissionsEnum[]) =>
applyDecorators(SetMetadata(PERMISSIONS_METADATA, permissions), UseGuards(ManagerOrPermissionGuard));
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { CanActivate, ExecutionContext, Inject, Injectable } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { OrganizationProjectService } from '../organization-project.service';
import { PermissionGuard } from '../../shared';

@Injectable()
export class ManagerOrPermissionGuard implements CanActivate {
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache,
private readonly _projectService: OrganizationProjectService,
private readonly _permissionGuard: PermissionGuard
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
console.log('✅ ManagerOrPermissionGuard canActivate called');

// Extract request and projectId from params
const request = context.switchToHttp().getRequest();
const projectId = request.params.id;

// Get user details from the request context
const { employeeId } = request.user;

// Check if the user is a project manager
if (employeeId && projectId) {
const isManager = await this._projectService.isManagerOfProject(projectId, employeeId);
if (isManager) {
console.log(`✅ User (employeeId: ${employeeId}) is manager of project ${projectId}, access granted.`);
return true;
}
}

// If the user is not a manager, delegate the check to PermissionGuard
console.log(`🔄 User is not a manager, deferring to PermissionGuard.`);
return this._permissionGuard.canActivate(context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
} from './commands';
import { OrganizationProject } from './organization-project.entity';
import { OrganizationProjectService } from './organization-project.service';
import { PermissionGuard, TenantPermissionGuard } from './../shared/guards';
import { TenantPermissionGuard } from './../shared/guards';
import { Permissions } from './../shared/decorators';
import { CountQueryDTO, RelationsQueryDTO } from './../shared/dto';
import { UUIDValidationPipe, UseValidationPipe } from './../shared/pipes';
Expand All @@ -43,8 +43,10 @@ import {
UpdateTaskModeDTO
} from './dto';

import { ManagerOrPermissionGuard } from './guards/manager-or-permission.guard';

@ApiTags('OrganizationProject')
@UseGuards(TenantPermissionGuard, PermissionGuard)
@UseGuards(TenantPermissionGuard, ManagerOrPermissionGuard)
@Permissions(PermissionsEnum.ALL_ORG_EDIT)
@Controller('/organization-projects')
export class OrganizationProjectController extends CrudController<OrganizationProject> {
Expand Down Expand Up @@ -276,6 +278,26 @@ export class OrganizationProjectController extends CrudController<OrganizationPr
return await this.commandBus.execute(new OrganizationProjectCreateCommand(entity));
}

/**
* CHECK if an employee is a manager of a project
*/
@ApiOperation({
summary: 'Check if an employee is a manager of a specific project.'
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Returns true if the employee is a manager of the project, otherwise false.',
type: Boolean
})
@Permissions(PermissionsEnum.ALL_ORG_VIEW, PermissionsEnum.ORG_PROJECT_VIEW)
@Get('/:projectId/is-manager/:employeeId')
async isManager(
@Param('projectId', UUIDValidationPipe) projectId: ID,
@Param('employeeId', UUIDValidationPipe) employeeId: ID
): Promise<boolean> {
return await this.organizationProjectService.isManagerOfProject(projectId, employeeId);
}

/**
* UPDATE organization project by id
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { RoleModule } from './../role/role.module';
import { EmployeeModule } from './../employee/employee.module';
import { TypeOrmOrganizationProjectRepository } from './repository/type-orm-organization-project.repository';
import { TypeOrmOrganizationProjectEmployeeRepository } from './repository/type-orm-organization-project-employee.repository';
import { PermissionGuard } from '../shared';

@Module({
imports: [
Expand All @@ -27,6 +28,7 @@ import { TypeOrmOrganizationProjectEmployeeRepository } from './repository/type-
OrganizationProjectService,
TypeOrmOrganizationProjectRepository,
TypeOrmOrganizationProjectEmployeeRepository,
PermissionGuard,
...CommandHandlers
],
exports: [
Expand Down
Loading
Loading