diff --git a/.github/workflows/on-push-pr.action.yml b/.github/workflows/on-push-pr.action.yml index c78692c9..e0d57076 100644 --- a/.github/workflows/on-push-pr.action.yml +++ b/.github/workflows/on-push-pr.action.yml @@ -17,12 +17,9 @@ jobs: vulnerabilities-scan: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - name: Checkout repository - - uses: debricked/actions/scan@v1 - name: Run a vulnerability scan + - uses: actions/checkout@v4 + - uses: debricked/actions@v4 env: - # Token must have API access scope to run scans DEBRICKED_TOKEN: ${{ secrets.DEBRICKED_TOKEN }} code-build: runs-on: ubuntu-latest diff --git a/src/controllers/user-management/permission.controller.ts b/src/controllers/user-management/permission.controller.ts index 7661e670..93c3b4df 100644 --- a/src/controllers/user-management/permission.controller.ts +++ b/src/controllers/user-management/permission.controller.ts @@ -183,6 +183,19 @@ export class PermissionController { return this.permissionService.getAllPermissions(query); } + @Get("getAllPermissionsWithoutUsers") + @ApiOperation({ summary: "Get list of all permissions without include users" }) + async getAllPermissionsWithoutUsers( + @Req() req: AuthenticatedRequest, + @Query() query?: ListAllPermissionsDto + ): Promise { + if (!req.user.permissions.isGlobalAdmin) { + const allowedOrganizations = req.user.permissions.getAllOrganizationsWithUserAdmin(); + return this.permissionService.getAllPermissionsWithoutUsers(query, allowedOrganizations); + } + return this.permissionService.getAllPermissionsWithoutUsers(query); + } + @Get(":id") @ApiOperation({ summary: "Get permissions entity" }) @ApiNotFoundResponse() diff --git a/src/controllers/user-management/user.controller.ts b/src/controllers/user-management/user.controller.ts index d43c60b7..3be024f4 100644 --- a/src/controllers/user-management/user.controller.ts +++ b/src/controllers/user-management/user.controller.ts @@ -70,7 +70,7 @@ export class UserController { try { // Don't leak the passwordHash // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { passwordHash, ...user } = await this.userService.createUser(createUserDto, req.user.userId); + const { passwordHash, ...user } = await this.userService.createUser(createUserDto, req.user.userId, req); AuditLog.success(ActionType.CREATE, User.name, req.user.userId, user.id, user.name); return user; @@ -122,7 +122,7 @@ export class UserController { } // Don't leak the passwordHash - const { passwordHash: _, ...user } = await this.userService.updateUser(id, dto, req.user.userId); + const { passwordHash: _, ...user } = await this.userService.updateUser(id, dto, req.user.userId, req); AuditLog.success(ActionType.UPDATE, User.name, req.user.userId, user.id, user.name); return user; diff --git a/src/entities/dto/list-all-permissions.dto.ts b/src/entities/dto/list-all-permissions.dto.ts index b92304d1..9faefa4a 100644 --- a/src/entities/dto/list-all-permissions.dto.ts +++ b/src/entities/dto/list-all-permissions.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { ApiProperty } from "@nestjs/swagger"; import { ListAllEntitiesDto } from "./list-all-entities.dto"; export class ListAllPermissionsDto extends ListAllEntitiesDto { @@ -7,4 +7,7 @@ export class ListAllPermissionsDto extends ListAllEntitiesDto { @ApiProperty({ type: String, required: false }) userId?: string; + + @ApiProperty({ type: Boolean, required: false }) + ignoreGlobalAdmin?: boolean; } diff --git a/src/entities/dto/permission-organization-application.dto.ts b/src/entities/dto/permission-organization-application.dto.ts index 018172b8..cbc734a4 100644 --- a/src/entities/dto/permission-organization-application.dto.ts +++ b/src/entities/dto/permission-organization-application.dto.ts @@ -54,7 +54,7 @@ export class UserPermissions { if (this.isGlobalAdmin) { return true; } else { - let organizationsWithAdmin = this.getAllOrganizationsWithUserAdmin(); + const organizationsWithAdmin = this.getAllOrganizationsWithUserAdmin(); return organizationsWithAdmin.indexOf(organizationId) > -1; } } diff --git a/src/entities/dto/user-management/create-user.dto.ts b/src/entities/dto/user-management/create-user.dto.ts index c78df51c..9d370778 100644 --- a/src/entities/dto/user-management/create-user.dto.ts +++ b/src/entities/dto/user-management/create-user.dto.ts @@ -1,3 +1,4 @@ +import { Permission } from "@entities/permissions/permission.entity"; import { IsNotBlank } from "@helpers/is-not-blank.validator"; import { ApiProperty } from "@nestjs/swagger"; import { IsEmail, IsString, Length } from "class-validator"; @@ -23,4 +24,7 @@ export class CreateUserDto { @ApiProperty({ required: false }) globalAdmin?: boolean; + + @ApiProperty({ required: false }) + permissionIds?: number[]; } diff --git a/src/entities/permissions/permission.entity.ts b/src/entities/permissions/permission.entity.ts index fa919cf2..4e51e02e 100644 --- a/src/entities/permissions/permission.entity.ts +++ b/src/entities/permissions/permission.entity.ts @@ -1,7 +1,6 @@ import { DbBaseEntity } from "@entities/base.entity"; import { User } from "@entities/user.entity"; -import { PermissionType } from "@enum/permission-type.enum"; -import { Column, Entity, ManyToMany, TableInheritance, OneToMany, ManyToOne, RelationId } from "typeorm"; +import { Column, Entity, ManyToMany, OneToMany, ManyToOne, RelationId } from "typeorm"; import { PermissionTypeEntity } from "./permission-type.entity"; import { Application } from "@entities/application.entity"; import { Organization } from "@entities/organization.entity"; diff --git a/src/services/device-management/lorawan-device-database-enrich-job.ts b/src/services/device-management/lorawan-device-database-enrich-job.ts index c1db53b0..1c9d6fa5 100644 --- a/src/services/device-management/lorawan-device-database-enrich-job.ts +++ b/src/services/device-management/lorawan-device-database-enrich-job.ts @@ -56,7 +56,7 @@ export class LorawanDeviceDatabaseEnrichJob { stats.rxPacketsReceived, stats.txPacketsEmitted, gateway.updatedAt, - chirpstackGateway.lastSeenAt ? timestampToDate(chirpstackGateway.lastSeenAt) : undefined + chirpstackGateway?.lastSeenAt ? timestampToDate(chirpstackGateway.lastSeenAt) : undefined ); } catch (err) { this.logger.error(`Gateway status fetch failed with: ${JSON.stringify(err)}`, err); diff --git a/src/services/user-management/permission.service.ts b/src/services/user-management/permission.service.ts index e214d9f3..f73cad2e 100644 --- a/src/services/user-management/permission.service.ts +++ b/src/services/user-management/permission.service.ts @@ -226,8 +226,8 @@ export class PermissionService { async getAllPermissions(query?: ListAllPermissionsDto, orgs?: number[]): Promise { const orderBy = this.getSorting(query); - const order: "DESC" | "ASC" = query?.sort?.toLocaleUpperCase() === "DESC" ? "DESC" : "ASC"; - let qb: SelectQueryBuilder = this.permissionRepository + const order = query?.sort?.toLocaleUpperCase() === "DESC" ? "DESC" : "ASC"; + let queryBuilder = this.permissionRepository .createQueryBuilder("permission") .leftJoinAndSelect("permission.organization", "org") .leftJoinAndSelect("permission.users", "user") @@ -237,15 +237,48 @@ export class PermissionService { .orderBy(orderBy, order); if (query?.userId !== undefined && query.userId !== "undefined") { - qb = qb.andWhere("user.id = :userId", { userId: +query.userId }); + queryBuilder = queryBuilder.andWhere("user.id = :userId", { userId: +query.userId }); } if (orgs) { - qb = qb.andWhere({ organization: In(orgs) }); + queryBuilder = queryBuilder.andWhere({ organization: In(orgs) }); } else if (query?.organisationId !== undefined && query.organisationId !== "undefined") { - qb = qb.andWhere("org.id = :orgId", { orgId: +query.organisationId }); + queryBuilder = queryBuilder.andWhere("org.id = :orgId", { orgId: +query.organisationId }); } + const [data, count] = await queryBuilder.getManyAndCount(); - const [data, count] = await qb.getManyAndCount(); + return { + data: data, + count: count, + }; + } + + async getAllPermissionsWithoutUsers( + query?: ListAllPermissionsDto, + orgs?: number[] + ): Promise { + const orderBy = this.getSorting(query); + const order = query?.sort?.toLocaleUpperCase() === "DESC" ? "DESC" : "ASC"; + let queryBuilder = this.permissionRepository + .createQueryBuilder("permission") + .leftJoinAndSelect("permission.organization", "org") + .leftJoinAndSelect("permission.type", "permission_type") + .take(query?.limit ? +query.limit : 100) + .skip(query?.offset ? +query.offset : 0) + .orderBy(orderBy, order); + + if (orgs) { + queryBuilder = queryBuilder.andWhere({ organization: In(orgs) }); + } else if (query?.organisationId !== undefined && query.organisationId !== "undefined") { + queryBuilder = queryBuilder.andWhere("org.id = :orgId", { orgId: +query.organisationId }); + } + + if (query?.ignoreGlobalAdmin) { + queryBuilder = queryBuilder.andWhere("org.name != :globalAdminName", { + globalAdminName: PermissionType.GlobalAdmin, + }); + } + + const [data, count] = await queryBuilder.getManyAndCount(); return { data: data, @@ -417,6 +450,17 @@ export class PermissionService { return await this.permissionRepository.findBy({ id: In(ids) }); } + async findManyByIdsIncludeOrgs(ids: number[]): Promise { + if (!ids || ids.length === 0) { + return []; + } + + return await this.permissionRepository.find({ + where: { id: In(ids) }, + relations: ["organization"], + }); + } + private hasAccessToAllApplicationsInOrganization(permissions: PermissionMinimalDto[]) { return permissions.some(x => x.permission_type_type == PermissionType.OrganizationUserAdmin); } diff --git a/src/services/user-management/user.service.ts b/src/services/user-management/user.service.ts index 06b3a546..8ef64e30 100644 --- a/src/services/user-management/user.service.ts +++ b/src/services/user-management/user.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, forwardRef, Inject, Injectable, Logger } from "@nestjs/common"; +import { BadRequestException, ForbiddenException, forwardRef, Inject, Injectable, Logger } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import * as bcrypt from "bcryptjs"; import { In, Repository } from "typeorm"; @@ -22,6 +22,7 @@ import { ConfigService } from "@nestjs/config"; import { isPermissionType } from "@helpers/security-helper"; import { nameof } from "@helpers/type-helper"; import { OS2IoTMail } from "@services/os2iot-mail.service"; +import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; @Injectable() export class UserService { @@ -134,8 +135,15 @@ export class UserService { .execute(); } - async createUser(dto: CreateUserDto, userId: number): Promise { + async createUser(dto: CreateUserDto, userId: number, req?: AuthenticatedRequest): Promise { const user = new User(); + const permissions = await this.permissionService.findManyByIdsIncludeOrgs(dto.permissionIds); + + if (req) { + this.checkForAccessToPermissions(req, permissions); + } + + user.permissions = permissions; const mappedUser = this.mapDtoToUser(user, dto); mappedUser.createdBy = userId; mappedUser.updatedBy = userId; @@ -205,12 +213,18 @@ export class UserService { return user; } - async updateUser(id: number, dto: UpdateUserDto, userId: number): Promise { + async updateUser(id: number, dto: UpdateUserDto, userId: number, req: AuthenticatedRequest): Promise { const user = await this.userRepository.findOne({ where: { id }, relations: ["permissions"], }); + const permissions = await this.permissionService.findManyByIdsIncludeOrgs(dto.permissionIds); + + if (req) { + this.checkForAccessToPermissions(req, permissions); + } + user.permissions = permissions; const mappedUser = this.mapDtoToUser(user, dto); mappedUser.updatedBy = userId; @@ -225,6 +239,19 @@ export class UserService { return await this.userRepository.save(mappedUser); } + private checkForAccessToPermissions(req: AuthenticatedRequest, permissions: Permission[]) { + const allowedOrganizations = req?.user.permissions.getAllOrganizationsWithUserAdmin(); + if (!req.user.permissions.isGlobalAdmin) { + const hasAccessToPermissions = permissions.every(permission => + allowedOrganizations.some(org => org === permission.organization?.id) + ); + + if (!hasAccessToPermissions) { + throw new ForbiddenException(); + } + } + } + private async updateGlobalAdminStatusIfNeeded(dto: UpdateUserDto, mappedUser: User) { if (dto.globalAdmin) { const globalAdminPermission = await this.permissionService.findOrCreateGlobalAdminPermission();