diff --git a/ormconfig.js b/ormconfig.js index 7c09f177..ccc4b141 100644 --- a/ormconfig.js +++ b/ormconfig.js @@ -7,7 +7,7 @@ module.exports = { "database": "os2iot", "synchronize": false, "logging": false, - "entities": ["src/entities/*.ts"], + "entities": ["src/entities/*.ts", "src/entities/permissions/*.ts"], "migrations": ["src/migration/*.ts"], "cli": { "migrationsDir": "src/migration" diff --git a/src/auth/roles.decorator.ts b/src/auth/roles.decorator.ts index bd4fe8de..aa601039 100644 --- a/src/auth/roles.decorator.ts +++ b/src/auth/roles.decorator.ts @@ -4,7 +4,7 @@ import { SetMetadata } from "@nestjs/common"; import { RolesMetaData } from "./constants"; export const Read = () => SetMetadata(RolesMetaData, PermissionType.Read); -export const Write = () => SetMetadata(RolesMetaData, PermissionType.Write); -export const OrganizationAdmin = () => - SetMetadata(RolesMetaData, PermissionType.OrganizationAdmin); +export const UserAdmin = () => SetMetadata(RolesMetaData, PermissionType.OrganizationUserAdmin); +export const GatewayAdmin = () => SetMetadata(RolesMetaData, PermissionType.OrganizationGatewayAdmin); +export const ApplicationAdmin = () => SetMetadata(RolesMetaData, PermissionType.OrganizationApplicationAdmin); export const GlobalAdmin = () => SetMetadata(RolesMetaData, PermissionType.GlobalAdmin); diff --git a/src/auth/roles.guard.ts b/src/auth/roles.guard.ts index 64c58745..53e73790 100644 --- a/src/auth/roles.guard.ts +++ b/src/auth/roles.guard.ts @@ -40,14 +40,17 @@ export class RolesGuard implements CanActivate { hasAccess(user: AuthenticatedUser, roleRequired: string): boolean { if (user.permissions.isGlobalAdmin) { return true; - } else if (roleRequired == PermissionType.OrganizationAdmin) { - return this.hasOrganizationAdminAccess(user); - } else if (roleRequired == PermissionType.Write) { - return this.hasOrganizationAdminAccess(user) || this.hasWriteAccess(user); + } else if (roleRequired == PermissionType.OrganizationApplicationAdmin) { + return this.hasOrganizationApplicationAdminAccess(user); + } else if (roleRequired == PermissionType.OrganizationUserAdmin) { + return this.hasOrganizationUserAdminAccess(user); + } else if (roleRequired == PermissionType.OrganizationGatewayAdmin) { + return this.hasOrganizationGatewayAdminAccess(user); } else if (roleRequired == PermissionType.Read) { return ( - this.hasOrganizationAdminAccess(user) || - this.hasWriteAccess(user) || + this.hasOrganizationApplicationAdminAccess(user) || + this.hasOrganizationUserAdminAccess(user) || + this.hasOrganizationGatewayAdminAccess(user) || this.hasReadAccess(user) ); } @@ -55,19 +58,19 @@ export class RolesGuard implements CanActivate { return false; } - hasOrganizationAdminAccess(user: AuthenticatedUser): boolean { - return user.permissions.organizationAdminPermissions.size > 0; + hasOrganizationApplicationAdminAccess(user: AuthenticatedUser): boolean { + return user.permissions.orgToApplicationAdminPermissions.size > 0; } - hasWriteAccess(user: AuthenticatedUser): boolean { - return this.hasSomeAccess(user.permissions.writePermissions); + hasOrganizationUserAdminAccess(user: AuthenticatedUser): boolean { + return user.permissions.orgToUserAdminPermissions.size > 0; } - hasReadAccess(user: AuthenticatedUser): boolean { - return this.hasSomeAccess(user.permissions.readPermissions); + hasOrganizationGatewayAdminAccess(user: AuthenticatedUser): boolean { + return user.permissions.orgToGatewayAdminPermissions.size > 0; } - hasSomeAccess(userPermission: Map): boolean { - return userPermission.size > 0; + hasReadAccess(user: AuthenticatedUser): boolean { + return user.permissions.orgToReadPermissions.size > 0; } } diff --git a/src/controllers/admin-controller/application.controller.ts b/src/controllers/admin-controller/application.controller.ts index 0c40e193..30f0f4da 100644 --- a/src/controllers/admin-controller/application.controller.ts +++ b/src/controllers/admin-controller/application.controller.ts @@ -27,7 +27,7 @@ import { } from "@nestjs/swagger"; import { ApiResponse } from "@nestjs/swagger"; -import { Read, Write } from "@auth/roles.decorator"; +import { Read, ApplicationAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { CreateApplicationDto } from "@dto/create-application.dto"; import { DeleteResponseDto } from "@dto/delete-application-response.dto"; @@ -38,10 +38,10 @@ import { Application } from "@entities/application.entity"; import { AuthenticatedRequest } from "@entities/dto/internal/authenticated-request"; import { ErrorCodes } from "@enum/error-codes.enum"; import { - checkIfUserHasReadAccessToApplication, - checkIfUserHasReadAccessToOrganization, - checkIfUserHasWriteAccessToApplication, - checkIfUserHasWriteAccessToOrganization, + ApplicationAccessScope, + checkIfUserHasAccessToApplication, + checkIfUserHasAccessToOrganization, + OrganizationAccessScope, } from "@helpers/security-helper"; import { ApplicationService } from "@services/device-management/application.service"; import { AuditLog } from "@services/audit-log.service"; @@ -90,11 +90,11 @@ export class ApplicationController { query: ListAllApplicationsDto ) { if (query?.organizationId) { - checkIfUserHasReadAccessToOrganization(req, query.organizationId); + checkIfUserHasAccessToOrganization(req, query.organizationId, OrganizationAccessScope.ApplicationRead); return await this.getApplicationsInOrganization(req, query); } - const allFromOrg = req.user.permissions.getAllOrganizationsWithAtLeastAdmin(); + const allFromOrg = req.user.permissions.getAllOrganizationsWithApplicationAdmin(); const allowedApplications = req.user.permissions.getAllApplicationsWithAtLeastRead(); const applications = await this.applicationService.findAndCountApplicationInWhitelistOrOrganization( query, @@ -109,7 +109,7 @@ export class ApplicationController { query: ListAllApplicationsDto ) { // If org admin give all - const allFromOrg = req.user.permissions.getAllOrganizationsWithAtLeastAdmin(); + const allFromOrg = req.user.permissions.getAllOrganizationsWithApplicationAdmin(); if (this.isOrganizationAdmin(allFromOrg, query)) { return await this.applicationService.findAndCountWithPagination(query, [ query.organizationId, @@ -137,7 +137,7 @@ export class ApplicationController { @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number ): Promise { - checkIfUserHasReadAccessToApplication(req, id); + checkIfUserHasAccessToApplication(req, id, ApplicationAccessScope.Read); try { return await this.applicationService.findOne(id); @@ -155,7 +155,7 @@ export class ApplicationController { @Param("id", new ParseIntPipe()) applicationId: number, @Query() query?: ListAllEntitiesDto ): Promise { - checkIfUserHasReadAccessToApplication(req, applicationId); + checkIfUserHasAccessToApplication(req, applicationId, ApplicationAccessScope.Read); try { return await this.applicationService.findDevicesForApplication( @@ -167,7 +167,7 @@ export class ApplicationController { } } - @Write() + @ApplicationAdmin() @Post() @Header("Cache-Control", "none") @ApiOperation({ summary: "Create a new Application" }) @@ -176,9 +176,10 @@ export class ApplicationController { @Req() req: AuthenticatedRequest, @Body() createApplicationDto: CreateApplicationDto ): Promise { - checkIfUserHasWriteAccessToOrganization( + checkIfUserHasAccessToOrganization( req, - createApplicationDto?.organizationId + createApplicationDto?.organizationId, + OrganizationAccessScope.ApplicationWrite ); const isValid = await this.applicationService.isNameValidAndNotUsed( @@ -207,7 +208,7 @@ export class ApplicationController { return application; } - @Write() + @ApplicationAdmin() @Put(":id") @Header("Cache-Control", "none") @ApiOperation({ summary: "Update an existing Application" }) @@ -217,7 +218,7 @@ export class ApplicationController { @Param("id", new ParseIntPipe()) id: number, @Body() updateApplicationDto: UpdateApplicationDto ): Promise { - checkIfUserHasWriteAccessToApplication(req, id); + checkIfUserHasAccessToApplication(req, id, ApplicationAccessScope.Write); if ( !(await this.applicationService.isNameValidAndNotUsed( updateApplicationDto?.name, @@ -247,7 +248,7 @@ export class ApplicationController { return application; } - @Write() + @ApplicationAdmin() @Delete(":id") @ApiOperation({ summary: "Delete an existing Application" }) @ApiBadRequestResponse() @@ -255,7 +256,7 @@ export class ApplicationController { @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number ): Promise { - checkIfUserHasWriteAccessToApplication(req, id); + checkIfUserHasAccessToApplication(req, id, ApplicationAccessScope.Write); try { const result = await this.applicationService.delete(id); diff --git a/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts b/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts index d378b253..fa964ec9 100644 --- a/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts +++ b/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts @@ -20,7 +20,7 @@ import { ApiTags, } from "@nestjs/swagger"; -import { Read, Write } from "@auth/roles.decorator"; +import { Read, GatewayAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { ChirpstackResponseStatus } from "@dto/chirpstack/chirpstack-response.dto"; import { CreateGatewayDto } from "@dto/chirpstack/create-gateway.dto"; @@ -29,7 +29,7 @@ import { SingleGatewayResponseDto } from "@dto/chirpstack/single-gateway-respons import { UpdateGatewayDto } from "@dto/chirpstack/update-gateway.dto"; import { ErrorCodes } from "@enum/error-codes.enum"; import { ChirpstackGatewayService } from "@services/chirpstack/chirpstack-gateway.service"; -import { checkIfUserHasWriteAccessToOrganization } from "@helpers/security-helper"; +import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; @@ -47,12 +47,12 @@ export class ChirpstackGatewayController { @ApiProduces("application/json") @ApiOperation({ summary: "Create a new Chirpstack Gateway" }) @ApiBadRequestResponse() - @Write() + @GatewayAdmin() async create( @Req() req: AuthenticatedRequest, @Body() dto: CreateGatewayDto ): Promise { - checkIfUserHasWriteAccessToOrganization(req, dto.organizationId); + checkIfUserHasAccessToOrganization(req, dto.organizationId, OrganizationAccessScope.GatewayWrite); try { const gateway = await this.chirpstackGatewayService.createNewGateway( dto, @@ -86,12 +86,8 @@ export class ChirpstackGatewayController { @ApiProduces("application/json") @ApiOperation({ summary: "List all Chirpstack gateways" }) @Read() - async getAll( - @Query() query?: ChirpstackGetAll - ): Promise { - return await this.chirpstackGatewayService.getAll( - query.organizationId - ); + async getAll(@Query() query?: ChirpstackGetAll): Promise { + return await this.chirpstackGatewayService.getAll(query.organizationId); } @Get(":gatewayId") @@ -116,7 +112,7 @@ export class ChirpstackGatewayController { @ApiProduces("application/json") @ApiOperation({ summary: "Create a new Chirpstack Gateway" }) @ApiBadRequestResponse() - @Write() + @GatewayAdmin() async update( @Req() req: AuthenticatedRequest, @Param("gatewayId") gatewayId: string, @@ -152,7 +148,7 @@ export class ChirpstackGatewayController { } @Delete(":gatewayId") - @Write() + @GatewayAdmin() async delete( @Req() req: AuthenticatedRequest, @Param("gatewayId") gatewayId: string @@ -160,9 +156,10 @@ export class ChirpstackGatewayController { try { const gw = await this.chirpstackGatewayService.getOne(gatewayId); if (gw.gateway.internalOrganizationId != null) { - checkIfUserHasWriteAccessToOrganization( + checkIfUserHasAccessToOrganization( req, - +gw.gateway.internalOrganizationId + +gw.gateway.internalOrganizationId, + OrganizationAccessScope.GatewayWrite ); } const deleteResult = await this.chirpstackGatewayService.deleteGateway( diff --git a/src/controllers/admin-controller/chirpstack/device-profile.controller.ts b/src/controllers/admin-controller/chirpstack/device-profile.controller.ts index 6d77d983..e406db01 100644 --- a/src/controllers/admin-controller/chirpstack/device-profile.controller.ts +++ b/src/controllers/admin-controller/chirpstack/device-profile.controller.ts @@ -24,7 +24,7 @@ import { ApiTags, } from "@nestjs/swagger"; -import { Read, Write } from "@auth/roles.decorator"; +import { Read, ApplicationAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { CreateChirpstackProfileResponseDto } from "@dto/chirpstack/create-chirpstack-profile-response.dto"; import { CreateDeviceProfileDto } from "@dto/chirpstack/create-device-profile.dto"; @@ -34,7 +34,7 @@ import { DeleteResponseDto } from "@dto/delete-application-response.dto"; import { ErrorCodes } from "@enum/error-codes.enum"; import { DeviceProfileService } from "@services/chirpstack/device-profile.service"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; -import { checkIfUserHasWriteAccessToOrganization } from "@helpers/security-helper"; +import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; import { ComposeAuthGuard } from "@auth/compose-auth.guard"; @@ -43,6 +43,7 @@ import { ComposeAuthGuard } from "@auth/compose-auth.guard"; @Controller("chirpstack/device-profiles") @UseGuards(ComposeAuthGuard, RolesGuard) @ApiBearerAuth() +@ApplicationAdmin() export class DeviceProfileController { constructor(private deviceProfileService: DeviceProfileService) {} @@ -53,16 +54,14 @@ export class DeviceProfileController { @ApiProduces("application/json") @ApiOperation({ summary: "Create a new DeviceProfile" }) @ApiBadRequestResponse() - @Write() + @ApplicationAdmin() async create( @Req() req: AuthenticatedRequest, @Body() createDto: CreateDeviceProfileDto ): Promise { + checkIfUserHasAccessToOrganization(req, createDto.internalOrganizationId, OrganizationAccessScope.ApplicationWrite); + try { - checkIfUserHasWriteAccessToOrganization( - req, - createDto.internalOrganizationId - ); const result = await this.deviceProfileService.createDeviceProfile( createDto, req.user.userId @@ -94,12 +93,18 @@ export class DeviceProfileController { @ApiOperation({ summary: "Update an existing DeviceProfile" }) @ApiBadRequestResponse() @HttpCode(204) - @Write() + @ApplicationAdmin() async update( @Req() req: AuthenticatedRequest, @Param("id") id: string, @Body() updateDto: UpdateDeviceProfileDto ): Promise { + checkIfUserHasAccessToOrganization( + req, + updateDto.deviceProfile.internalOrganizationId, + OrganizationAccessScope.ApplicationWrite + ); + try { await this.deviceProfileService.updateDeviceProfile(updateDto, id, req); AuditLog.success( @@ -170,7 +175,7 @@ export class DeviceProfileController { @Delete(":id") @ApiOperation({ summary: "Delete one DeviceProfile by id" }) @ApiNotFoundResponse() - @Write() + @ApplicationAdmin() async deleteOne( @Req() req: AuthenticatedRequest, @Param("id") id: string diff --git a/src/controllers/admin-controller/chirpstack/service-profile.controller.ts b/src/controllers/admin-controller/chirpstack/service-profile.controller.ts index 03d75493..2f1b1412 100644 --- a/src/controllers/admin-controller/chirpstack/service-profile.controller.ts +++ b/src/controllers/admin-controller/chirpstack/service-profile.controller.ts @@ -25,7 +25,7 @@ import { ApiTags, } from "@nestjs/swagger"; -import { Read, Write } from "@auth/roles.decorator"; +import { Read, ApplicationAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { CreateChirpstackProfileResponseDto } from "@dto/chirpstack/create-chirpstack-profile-response.dto"; import { CreateServiceProfileDto } from "@dto/chirpstack/create-service-profile.dto"; @@ -42,6 +42,7 @@ import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; @Controller("chirpstack/service-profiles") @UseGuards(ComposeAuthGuard, RolesGuard) @ApiBearerAuth() +@ApplicationAdmin() export class ServiceProfileController { constructor(private serviceProfileService: ServiceProfileService) {} private readonly logger = new Logger(ServiceProfileController.name); @@ -50,7 +51,7 @@ export class ServiceProfileController { @ApiProduces("application/json") @ApiOperation({ summary: "Create a new ServiceProfile" }) @ApiBadRequestResponse() - @Write() + @ApplicationAdmin() async create( @Req() req: AuthenticatedRequest, @Body() createDto: CreateServiceProfileDto @@ -71,7 +72,7 @@ export class ServiceProfileController { @ApiOperation({ summary: "Update an existing ServiceProfile" }) @ApiBadRequestResponse() @HttpCode(204) - @Write() + @ApplicationAdmin() async update( @Req() req: AuthenticatedRequest, @Param("id") id: string, @@ -131,7 +132,7 @@ export class ServiceProfileController { @Delete(":id") @ApiOperation({ summary: "Delete one ServiceProfile by id" }) @ApiNotFoundResponse() - @Write() + @ApplicationAdmin() async deleteOne( @Req() req: AuthenticatedRequest, @Param("id") id: string diff --git a/src/controllers/admin-controller/data-target.controller.ts b/src/controllers/admin-controller/data-target.controller.ts index 8a380a43..40b8cd89 100644 --- a/src/controllers/admin-controller/data-target.controller.ts +++ b/src/controllers/admin-controller/data-target.controller.ts @@ -24,7 +24,7 @@ import { } from "@nestjs/swagger"; import { ComposeAuthGuard } from '@auth/compose-auth.guard'; -import { Read, Write } from "@auth/roles.decorator"; +import { Read, ApplicationAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { CreateDataTargetDto } from "@dto/create-data-target.dto"; import { DeleteResponseDto } from "@dto/delete-application-response.dto"; @@ -35,8 +35,8 @@ import { UpdateDataTargetDto } from "@dto/update-data-target.dto"; import { DataTarget } from "@entities/data-target.entity"; import { ErrorCodes } from "@enum/error-codes.enum"; import { - checkIfUserHasReadAccessToApplication, - checkIfUserHasWriteAccessToApplication, + checkIfUserHasAccessToApplication, + ApplicationAccessScope, } from "@helpers/security-helper"; import { DataTargetService } from "@services/data-targets/data-target.service"; import { AuditLog } from "@services/audit-log.service"; @@ -85,7 +85,7 @@ export class DataTargetController { ): Promise { try { const dataTarget = await this.dataTargetService.findOne(id); - checkIfUserHasReadAccessToApplication(req, dataTarget.application.id); + checkIfUserHasAccessToApplication(req, dataTarget.application.id, ApplicationAccessScope.Read); return dataTarget; } catch (err) { throw new NotFoundException(ErrorCodes.IdDoesNotExists); @@ -100,9 +100,10 @@ export class DataTargetController { @Body() createDataTargetDto: CreateDataTargetDto ): Promise { try { - checkIfUserHasWriteAccessToApplication( + checkIfUserHasAccessToApplication( req, - createDataTargetDto.applicationId + createDataTargetDto.applicationId, + ApplicationAccessScope.Write ); const dataTarget = await this.dataTargetService.create( createDataTargetDto, @@ -133,9 +134,9 @@ export class DataTargetController { ): Promise { const oldDataTarget = await this.dataTargetService.findOne(id); try { - checkIfUserHasWriteAccessToApplication(req, oldDataTarget.application.id); + checkIfUserHasAccessToApplication(req, oldDataTarget.application.id, ApplicationAccessScope.Write); if (oldDataTarget.application.id !== updateDto.applicationId) { - checkIfUserHasWriteAccessToApplication(req, updateDto.applicationId); + checkIfUserHasAccessToApplication(req, updateDto.applicationId, ApplicationAccessScope.Write); } } catch (err) { AuditLog.fail( @@ -166,14 +167,14 @@ export class DataTargetController { @Delete(":id") @ApiOperation({ summary: "Delete an existing DataTarget" }) @ApiBadRequestResponse() - @Write() + @ApplicationAdmin() async delete( @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number ): Promise { try { const dt = await this.dataTargetService.findOne(id); - checkIfUserHasWriteAccessToApplication(req, dt.application.id); + checkIfUserHasAccessToApplication(req, dt.application.id, ApplicationAccessScope.Write); const result = await this.dataTargetService.delete(id); if (result.affected === 0) { diff --git a/src/controllers/admin-controller/device-model.controller.ts b/src/controllers/admin-controller/device-model.controller.ts index 80368ec6..782b1c4e 100644 --- a/src/controllers/admin-controller/device-model.controller.ts +++ b/src/controllers/admin-controller/device-model.controller.ts @@ -6,15 +6,11 @@ import { DeleteResponseDto } from "@dto/delete-application-response.dto"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { ListAllDeviceModelResponseDto } from "@dto/list-all-device-model-response.dto"; import { ListAllDeviceModelsDto } from "@dto/list-all-device-models.dto"; -import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; import { UpdateDeviceModelDto } from "@dto/update-device-model.dto"; import { ActionType } from "@entities/audit-log-entry"; import { DeviceModel } from "@entities/device-model.entity"; import { ErrorCodes } from "@enum/error-codes.enum"; -import { - checkIfUserHasReadAccessToOrganization, - checkIfUserHasWriteAccessToOrganization, -} from "@helpers/security-helper"; +import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; import { BadRequestException, Body, @@ -58,15 +54,15 @@ export class DeviceModelController { @Req() req: AuthenticatedRequest, @Query() query?: ListAllDeviceModelsDto ): Promise { - if (query?.organizationId != null) { - checkIfUserHasReadAccessToOrganization(req, query?.organizationId); + if (query?.organizationId != null) { + checkIfUserHasAccessToOrganization(req, query?.organizationId, OrganizationAccessScope.ApplicationRead); return this.service.getAllDeviceModelsByOrgIds( [query?.organizationId], query ); } - const orgIds = req.user.permissions.getAllOrganizationsWithAtLeastRead(); + const orgIds = req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead(); return this.service.getAllDeviceModelsByOrgIds(orgIds, query); } @@ -81,7 +77,7 @@ export class DeviceModelController { throw new NotFoundException(ErrorCodes.IdDoesNotExists); } - checkIfUserHasReadAccessToOrganization(req, deviceModel.belongsTo.id); + checkIfUserHasAccessToOrganization(req, deviceModel.belongsTo.id, OrganizationAccessScope.ApplicationRead); return deviceModel; } @@ -94,7 +90,7 @@ export class DeviceModelController { @Body() dto: CreateDeviceModelDto ): Promise { try { - checkIfUserHasWriteAccessToOrganization(req, dto.belongsToId); + checkIfUserHasAccessToOrganization(req, dto.belongsToId, OrganizationAccessScope.ApplicationWrite); const res = await this.service.create(dto, req.user.userId); AuditLog.success( @@ -121,7 +117,7 @@ export class DeviceModelController { ): Promise { try { const deviceModel = await this.service.getByIdWithRelations(id); - checkIfUserHasWriteAccessToOrganization(req, deviceModel.belongsTo.id); + checkIfUserHasAccessToOrganization(req, deviceModel.belongsTo.id, OrganizationAccessScope.ApplicationWrite); const res = await this.service.update(deviceModel, dto, req.user.userId); AuditLog.success(ActionType.UPDATE, DeviceModel.name, req.user.userId, id); return res; @@ -141,7 +137,7 @@ export class DeviceModelController { ): Promise { try { const deviceModel = await this.service.getByIdWithRelations(id); - checkIfUserHasWriteAccessToOrganization(req, deviceModel.belongsTo.id); + checkIfUserHasAccessToOrganization(req, deviceModel.belongsTo.id, OrganizationAccessScope.ApplicationWrite); const res = await this.service.delete(id); AuditLog.success(ActionType.DELETE, DeviceModel.name, req.user.userId, id); return new DeleteResponseDto(res.affected); diff --git a/src/controllers/admin-controller/iot-device-payload-decoder-data-target-connection.controller.ts b/src/controllers/admin-controller/iot-device-payload-decoder-data-target-connection.controller.ts index 455e703b..ad083119 100644 --- a/src/controllers/admin-controller/iot-device-payload-decoder-data-target-connection.controller.ts +++ b/src/controllers/admin-controller/iot-device-payload-decoder-data-target-connection.controller.ts @@ -25,7 +25,7 @@ import { } from "@nestjs/swagger"; import { ComposeAuthGuard } from '@auth/compose-auth.guard'; -import { Read, Write } from "@auth/roles.decorator"; +import { Read, ApplicationAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { CreateIoTDevicePayloadDecoderDataTargetConnectionDto } from "@dto/create-iot-device-payload-decoder-data-target-connection.dto"; import { DeleteResponseDto } from "@dto/delete-application-response.dto"; @@ -36,7 +36,7 @@ import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; import { UpdateIoTDevicePayloadDecoderDataTargetConnectionDto as UpdateConnectionDto } from "@dto/update-iot-device-payload-decoder-data-target-connection.dto"; import { IoTDevicePayloadDecoderDataTargetConnection } from "@entities/iot-device-payload-decoder-data-target-connection.entity"; import { ErrorCodes } from "@enum/error-codes.enum"; -import { checkIfUserHasWriteAccessToApplication } from "@helpers/security-helper"; +import { checkIfUserHasAccessToApplication, ApplicationAccessScope } from "@helpers/security-helper"; import { IoTDevicePayloadDecoderDataTargetConnectionService } from "@services/device-management/iot-device-payload-decoder-data-target-connection.service"; import { IoTDeviceService } from "@services/device-management/iot-device.service"; import { AuditLog } from "@services/audit-log.service"; @@ -120,7 +120,7 @@ export class IoTDevicePayloadDecoderDataTargetConnectionController { } else { return await this.service.findAllByPayloadDecoderId( id, - req.user.permissions.getAllOrganizationsWithAtLeastRead() + req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead() ); } } @@ -142,7 +142,7 @@ export class IoTDevicePayloadDecoderDataTargetConnectionController { } @Post() - @Write() + @ApplicationAdmin() @ApiOperation({ summary: "Create new connection", }) @@ -184,12 +184,12 @@ export class IoTDevicePayloadDecoderDataTargetConnectionController { ) { const iotDevices = await this.iotDeviceService.findManyByIds(ids); iotDevices.forEach(x => { - checkIfUserHasWriteAccessToApplication(req, x.application.id); + checkIfUserHasAccessToApplication(req, x.application.id, ApplicationAccessScope.Write); }); } @Put(":id") - @Write() + @ApplicationAdmin() @ApiNotFoundResponse({ description: "If the id of the entity doesn't exist", }) @@ -231,7 +231,7 @@ export class IoTDevicePayloadDecoderDataTargetConnectionController { const newIotDevice = await this.iotDeviceService.findOne( updateDto.iotDeviceIds[0] ); - checkIfUserHasWriteAccessToApplication(req, newIotDevice.application.id); + checkIfUserHasAccessToApplication(req, newIotDevice.application.id, ApplicationAccessScope.Write); const oldConnection = await this.service.findOne(id); await this.checkUserHasWriteAccessToAllIotDevices(updateDto.iotDeviceIds, req); const oldIds = oldConnection.iotDevices.map(x => x.id); @@ -241,7 +241,7 @@ export class IoTDevicePayloadDecoderDataTargetConnectionController { } @Delete(":id") - @Write() + @ApplicationAdmin() @ApiNotFoundResponse({ description: "If the id of the entity doesn't exist", }) diff --git a/src/controllers/admin-controller/iot-device.controller.ts b/src/controllers/admin-controller/iot-device.controller.ts index 42ddc134..5115ae7c 100644 --- a/src/controllers/admin-controller/iot-device.controller.ts +++ b/src/controllers/admin-controller/iot-device.controller.ts @@ -34,11 +34,7 @@ import { LoRaWANDeviceWithChirpstackDataDto } from "@dto/lorawan-device-with-chi import { UpdateIoTDeviceDto } from "@dto/update-iot-device.dto"; import { IoTDevice } from "@entities/iot-device.entity"; import { ErrorCodes } from "@enum/error-codes.enum"; -import { - checkIfUserHasAdminAccessToOrganization, - checkIfUserHasReadAccessToApplication, - checkIfUserHasWriteAccessToApplication, -} from "@helpers/security-helper"; +import { checkIfUserHasAccessToOrganization, OrganizationAccessScope, checkIfUserHasAccessToApplication, ApplicationAccessScope } from "@helpers/security-helper"; import { IoTDeviceService } from "@services/device-management/iot-device.service"; import { SigFoxDeviceWithBackendDataDto } from "@dto/sigfox-device-with-backend-data.dto"; import { CreateIoTDeviceDownlinkDto } from "@dto/create-iot-device-downlink.dto"; @@ -100,7 +96,7 @@ export class IoTDeviceController { throw new NotFoundException(ErrorCodes.IdDoesNotExists); } - checkIfUserHasReadAccessToApplication(req, result.application.id); + checkIfUserHasAccessToApplication(req, result.application.id, ApplicationAccessScope.Read); return result; } @@ -124,7 +120,7 @@ export class IoTDeviceController { if (!device) { throw new NotFoundException(ErrorCodes.IdDoesNotExists); } - checkIfUserHasReadAccessToApplication(req, device.application.id); + checkIfUserHasAccessToApplication(req, device.application.id, ApplicationAccessScope.Read); if (device.type == IoTDeviceType.LoRaWAN) { return this.chirpstackDeviceService.getDownlinkQueue( (device as LoRaWANDevice).deviceEUI @@ -145,7 +141,7 @@ export class IoTDeviceController { @Param("id", new ParseIntPipe()) id: number ): Promise { const device = await this.iotDeviceService.findOne(id); - checkIfUserHasReadAccessToApplication(req, device.application.id); + checkIfUserHasAccessToApplication(req, device.application.id, ApplicationAccessScope.Read); return this.iotDeviceService.findStats(device); } @@ -159,7 +155,7 @@ export class IoTDeviceController { @Body() createDto: CreateIoTDeviceDto ): Promise { try { - checkIfUserHasWriteAccessToApplication(req, createDto.applicationId); + checkIfUserHasAccessToOrganization(req, createDto.applicationId, OrganizationAccessScope.ApplicationWrite); const device = await this.iotDeviceService.create(createDto, req.user.userId); AuditLog.success( ActionType.CREATE, @@ -196,7 +192,7 @@ export class IoTDeviceController { if (!device) { throw new NotFoundException(); } - checkIfUserHasWriteAccessToApplication(req, device?.application?.id); + checkIfUserHasAccessToOrganization(req, device?.application?.id, OrganizationAccessScope.ApplicationWrite); const result = await this.downlinkService.createDownlink(dto, device); AuditLog.success(ActionType.CREATE, "Downlink", req.user.userId); return result; @@ -221,10 +217,10 @@ export class IoTDeviceController { false ); try { - checkIfUserHasWriteAccessToApplication(req, oldIotDevice.application.id); + checkIfUserHasAccessToOrganization(req, oldIotDevice.application.id, OrganizationAccessScope.ApplicationWrite); if (updateDto.applicationId !== oldIotDevice.application.id) { // New application - checkIfUserHasWriteAccessToApplication(req, updateDto.applicationId); + checkIfUserHasAccessToOrganization(req, updateDto.applicationId, OrganizationAccessScope.ApplicationWrite); } } catch (err) { AuditLog.fail(ActionType.UPDATE, IoTDevice.name, req.user.userId, id); @@ -254,10 +250,9 @@ export class IoTDeviceController { @Req() req: AuthenticatedRequest, @Body() createDto: CreateIoTDeviceBatchDto ): Promise { - try { - createDto.data.forEach(createDto => - checkIfUserHasWriteAccessToApplication(req, createDto.applicationId) - ); + try { + createDto.data.forEach(createDto => checkIfUserHasAccessToApplication(req, createDto.applicationId, ApplicationAccessScope.Write)); + const devices = await this.iotDeviceService.createMany( createDto.data, req.user.userId @@ -353,7 +348,7 @@ export class IoTDeviceController { id, false ); - checkIfUserHasWriteAccessToApplication(req, oldIotDevice?.application?.id); + checkIfUserHasAccessToOrganization(req, oldIotDevice?.application?.id, OrganizationAccessScope.ApplicationWrite); const result = await this.iotDeviceService.delete(oldIotDevice); AuditLog.success(ActionType.DELETE, IoTDevice.name, req.user.userId, id); return new DeleteResponseDto(result.affected); @@ -372,7 +367,7 @@ export class IoTDeviceController { ): Promise> { try { const oldIotDevice = await this.iotDeviceService.findOne(id); - checkIfUserHasWriteAccessToApplication(req, oldIotDevice?.application?.id); + checkIfUserHasAccessToApplication(req, oldIotDevice?.application?.id, ApplicationAccessScope.Write); if (oldIotDevice.type !== IoTDeviceType.GenericHttp) { throw new BadRequestException("The requested device is not a generic HTTP device"); diff --git a/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts b/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts index 8d518019..0febc74a 100644 --- a/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts +++ b/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts @@ -7,7 +7,6 @@ import { } from "@dto/chirpstack/backend/gateway-all-status.dto"; import { GatewayStatus, GetGatewayStatusQuery } from "@dto/chirpstack/backend/gateway-status.dto"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; -import { checkIfUserHasReadAccessToOrganization } from "@helpers/security-helper"; import { Controller, Get, Param, Query, Req, UseGuards } from "@nestjs/common"; import { ApiBearerAuth, ApiOperation, ApiProduces, ApiTags } from "@nestjs/swagger"; import { ChirpstackGatewayService } from "@services/chirpstack/chirpstack-gateway.service"; @@ -17,7 +16,6 @@ import { GatewayStatusHistoryService } from "@services/chirpstack/gateway-status @Controller("lorawan/gateway") @UseGuards(ComposeAuthGuard, RolesGuard) @ApiBearerAuth() -@Read() export class LoRaWANGatewayController { constructor( private onlineHistoryService: GatewayStatusHistoryService, @@ -32,11 +30,7 @@ export class LoRaWANGatewayController { @Req() req: AuthenticatedRequest, @Query() query: ListAllGatewayStatusDto ): Promise { - if (query.organizationId) { - // TODO: NEW USER MANAGEMENT: Update the rights once it's merged - checkIfUserHasReadAccessToOrganization(req, query.organizationId); - } - + // Currently, everyone is allowed to get the status return this.onlineHistoryService.findAllWithChirpstack(query); } @@ -48,9 +42,8 @@ export class LoRaWANGatewayController { @Param("id") id: string, @Query() query: GetGatewayStatusQuery ): Promise { + // Currently, everyone is allowed to get the status const gatewayDto = await this.chirpstackGatewayService.getOne(id); - checkIfUserHasReadAccessToOrganization(req, gatewayDto.gateway.internalOrganizationId); - return this.onlineHistoryService.findOne(gatewayDto.gateway, query.timeInterval); } } diff --git a/src/controllers/admin-controller/multicast.controller.ts b/src/controllers/admin-controller/multicast.controller.ts index c6488566..59c94b45 100644 --- a/src/controllers/admin-controller/multicast.controller.ts +++ b/src/controllers/admin-controller/multicast.controller.ts @@ -29,14 +29,14 @@ import { import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { Multicast } from "@entities/multicast.entity"; import { - checkIfUserHasReadAccessToApplication, - checkIfUserHasWriteAccessToApplication, + checkIfUserHasAccessToApplication, + ApplicationAccessScope, } from "@helpers/security-helper"; import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; import { ComposeAuthGuard } from '@auth/compose-auth.guard'; import { RolesGuard } from "@auth/roles.guard"; -import { Read, Write } from "@auth/roles.decorator"; +import { Read, ApplicationAdmin } from "@auth/roles.decorator"; import { ListAllMulticastsDto } from "@dto/list-all-multicasts.dto"; import { ListAllMulticastsResponseDto } from "@dto/list-all-multicasts-response.dto"; import { ErrorCodes } from "@enum/error-codes.enum"; @@ -64,7 +64,11 @@ export class MulticastController { @Body() createMulticastDto: CreateMulticastDto ): Promise { try { - checkIfUserHasWriteAccessToApplication(req, createMulticastDto.applicationID); + checkIfUserHasAccessToApplication( + req, + createMulticastDto.applicationID, + ApplicationAccessScope.Write + ); const multicast = await this.multicastService.create( createMulticastDto, req.user.userId @@ -117,7 +121,11 @@ export class MulticastController { ): Promise { try { const multicast = await this.multicastService.findOne(id); - checkIfUserHasReadAccessToApplication(req, multicast.application.id); + checkIfUserHasAccessToApplication( + req, + multicast.application.id, + ApplicationAccessScope.Read + ); return multicast; } catch (err) { throw new NotFoundException(ErrorCodes.IdDoesNotExists); @@ -140,7 +148,11 @@ export class MulticastController { if (!multicast) { throw new NotFoundException(ErrorCodes.IdDoesNotExists); } - checkIfUserHasReadAccessToApplication(req, multicast.application.id); + checkIfUserHasAccessToApplication( + req, + multicast.application.id, + ApplicationAccessScope.Read + ); return this.multicastService.getDownlinkQueue( multicast.lorawanMulticastDefinition.chirpstackGroupId @@ -161,7 +173,11 @@ export class MulticastController { if (!multicast) { throw new NotFoundException(); } - checkIfUserHasWriteAccessToApplication(req, multicast.application.id); + checkIfUserHasAccessToApplication( + req, + multicast.application.id, + ApplicationAccessScope.Write + ); const result = await this.multicastService.createDownlink(dto, multicast); AuditLog.success(ActionType.CREATE, "Downlink", req.user.userId); return result; @@ -182,9 +198,17 @@ export class MulticastController { ): Promise { const oldMulticast = await this.multicastService.findOne(id); try { - checkIfUserHasWriteAccessToApplication(req, oldMulticast.application.id); + checkIfUserHasAccessToApplication( + req, + oldMulticast.application.id, + ApplicationAccessScope.Write + ); if (oldMulticast.application.id !== updateDto.applicationID) { - checkIfUserHasWriteAccessToApplication(req, updateDto.applicationID); + checkIfUserHasAccessToApplication( + req, + updateDto.applicationID, + ApplicationAccessScope.Write + ); } } catch (err) { AuditLog.fail( @@ -215,14 +239,18 @@ export class MulticastController { @Delete(":id") @ApiOperation({ summary: "Delete an existing multicast" }) @ApiBadRequestResponse() - @Write() + @ApplicationAdmin() async delete( @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number ): Promise { try { const multicast = await this.multicastService.findOne(id); - checkIfUserHasWriteAccessToApplication(req, multicast.application.id); + checkIfUserHasAccessToApplication( + req, + multicast.application.id, + ApplicationAccessScope.Write + ); const result = await this.multicastService.multicastDelete(id, multicast); if (result.affected === 0) { diff --git a/src/controllers/admin-controller/payload-decoder.controller.ts b/src/controllers/admin-controller/payload-decoder.controller.ts index d8bbc35b..454cf4a9 100644 --- a/src/controllers/admin-controller/payload-decoder.controller.ts +++ b/src/controllers/admin-controller/payload-decoder.controller.ts @@ -26,7 +26,7 @@ import { } from "@nestjs/swagger"; import { ComposeAuthGuard } from '@auth/compose-auth.guard'; -import { Read, Write } from "@auth/roles.decorator"; +import { Read, ApplicationAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { CreatePayloadDecoderDto } from "@dto/create-payload-decoder.dto"; import { DeleteResponseDto } from "@dto/delete-application-response.dto"; @@ -36,10 +36,7 @@ import { ListAllPayloadDecoderResponseDto } from "@dto/list-all-payload-decoders import { UpdatePayloadDecoderDto } from "@dto/update-payload-decoder.dto"; import { PayloadDecoder } from "@entities/payload-decoder.entity"; import { ErrorCodes } from "@enum/error-codes.enum"; -import { - checkIfUserHasReadAccessToOrganization, - checkIfUserHasWriteAccessToOrganization, -} from "@helpers/security-helper"; +import { OrganizationAccessScope, checkIfUserHasAccessToOrganization } from "@helpers/security-helper"; import { PayloadDecoderService } from "@services/data-management/payload-decoder.service"; import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; @@ -90,7 +87,7 @@ export class PayloadDecoderController { } @Post() - @Write() + @ApplicationAdmin() @Header("Cache-Control", "none") @ApiOperation({ summary: "Create a new Payload Decoder" }) @ApiBadRequestResponse() @@ -99,7 +96,7 @@ export class PayloadDecoderController { @Body() createDto: CreatePayloadDecoderDto ): Promise { try { - checkIfUserHasWriteAccessToOrganization(req, createDto.organizationId); + checkIfUserHasAccessToOrganization(req, createDto.organizationId, OrganizationAccessScope.ApplicationWrite); // TODO: Valider at funktionen er gyldig const payloadDecoder = await this.payloadDecoderService.create( @@ -121,7 +118,7 @@ export class PayloadDecoderController { } @Put(":id") - @Write() + @ApplicationAdmin() @Header("Cache-Control", "none") @ApiOperation({ summary: "Update an existing Payload Decoder" }) @ApiBadRequestResponse() @@ -131,10 +128,10 @@ export class PayloadDecoderController { @Body() updateDto: UpdatePayloadDecoderDto ): Promise { try { - checkIfUserHasWriteAccessToOrganization(req, updateDto.organizationId); + checkIfUserHasAccessToOrganization(req, updateDto.organizationId, OrganizationAccessScope.ApplicationWrite); const oldDecoder = await this.payloadDecoderService.findOne(id); if (oldDecoder?.organization?.id) { - checkIfUserHasWriteAccessToOrganization(req, oldDecoder.organization.id); + checkIfUserHasAccessToOrganization(req, oldDecoder.organization.id, OrganizationAccessScope.ApplicationWrite); } // TODO: Valider at funktionen er gyldig const payloadDecoder = await this.payloadDecoderService.update( @@ -158,7 +155,7 @@ export class PayloadDecoderController { } @Delete(":id") - @Write() + @ApplicationAdmin() @ApiOperation({ summary: "Delete an existing Payload Decoder" }) @ApiNotFoundResponse() async delete( @@ -168,7 +165,7 @@ export class PayloadDecoderController { try { const oldDecoder = await this.payloadDecoderService.findOne(id); if (oldDecoder?.organization?.id) { - checkIfUserHasWriteAccessToOrganization(req, oldDecoder.organization.id); + checkIfUserHasAccessToOrganization(req, oldDecoder.organization.id, OrganizationAccessScope.ApplicationWrite); } const result = await this.payloadDecoderService.delete(id); diff --git a/src/controllers/admin-controller/sigfox/sigfox-api-contract.controller.ts b/src/controllers/admin-controller/sigfox/sigfox-api-contract.controller.ts index c6d41f41..77273e7e 100644 --- a/src/controllers/admin-controller/sigfox/sigfox-api-contract.controller.ts +++ b/src/controllers/admin-controller/sigfox/sigfox-api-contract.controller.ts @@ -4,7 +4,7 @@ import { RolesGuard } from "@auth/roles.guard"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { SigFoxApiContractInfosContent } from "@dto/sigfox/external/sigfox-api-contract-infos-response.dto"; import { SigFoxGroup } from "@entities/sigfox-group.entity"; -import { checkIfUserHasReadAccessToOrganization } from "@helpers/security-helper"; +import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; import { Controller, Get, ParseIntPipe, Query, Req, UseGuards } from "@nestjs/common"; import { ApiTags, @@ -38,7 +38,7 @@ export class SigFoxApiContractController { const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword( groupId ); - checkIfUserHasReadAccessToOrganization(req, group?.belongsTo?.id); + checkIfUserHasAccessToOrganization(req, group?.belongsTo?.id, OrganizationAccessScope.ApplicationRead); return await this.service.getContractInfos(group); } diff --git a/src/controllers/admin-controller/sigfox/sigfox-api-device.controller.ts b/src/controllers/admin-controller/sigfox/sigfox-api-device.controller.ts index 2165c21c..130a4d9d 100644 --- a/src/controllers/admin-controller/sigfox/sigfox-api-device.controller.ts +++ b/src/controllers/admin-controller/sigfox/sigfox-api-device.controller.ts @@ -4,7 +4,7 @@ import { RolesGuard } from "@auth/roles.guard"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { SigFoxApiDeviceResponse } from "@dto/sigfox/external/sigfox-api-device-response.dto"; import { SigFoxGroup } from "@entities/sigfox-group.entity"; -import { checkIfUserHasReadAccessToOrganization } from "@helpers/security-helper"; +import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; import { Controller, Get, ParseIntPipe, Query, Req, UseGuards } from "@nestjs/common"; import { ApiBearerAuth, @@ -41,7 +41,7 @@ export class SigFoxApiDeviceController { const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword( groupId ); - checkIfUserHasReadAccessToOrganization(req, group.belongsTo.id); + checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationRead); return await this.iotDeviceService.getAllSigfoxDevicesByGroup(group, true); } diff --git a/src/controllers/admin-controller/sigfox/sigfox-device-type.controller.ts b/src/controllers/admin-controller/sigfox/sigfox-device-type.controller.ts index 730a1f74..a0c8e04e 100644 --- a/src/controllers/admin-controller/sigfox/sigfox-device-type.controller.ts +++ b/src/controllers/admin-controller/sigfox/sigfox-device-type.controller.ts @@ -1,5 +1,5 @@ import { ComposeAuthGuard } from '@auth/compose-auth.guard'; -import { Read, Write } from "@auth/roles.decorator"; +import { Read, ApplicationAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { CreateSigFoxApiDeviceTypeRequestDto } from "@dto/sigfox/external/create-sigfox-api-device-type-request.dto"; @@ -11,10 +11,7 @@ import { SigFoxApiIdReferenceDto } from "@dto/sigfox/external/sigfox-api-id-refe import { UpdateSigFoxApiDeviceTypeRequestDto } from "@dto/sigfox/external/update-sigfox-api-device-type-request.dto"; import { ActionType } from "@entities/audit-log-entry"; import { SigFoxGroup } from "@entities/sigfox-group.entity"; -import { - checkIfUserHasReadAccessToOrganization, - checkIfUserHasWriteAccessToOrganization, -} from "@helpers/security-helper"; +import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; import { Body, Controller, @@ -66,7 +63,7 @@ export class SigfoxDeviceTypeController { const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword( groupId ); - checkIfUserHasReadAccessToOrganization(req, group.belongsTo.id); + checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationRead); const sigfoxApiGroup = await this.usersService.getByUserId(group.username, group); return await this.service.getAllByGroupIds(group, [sigfoxApiGroup.group.id]); @@ -83,13 +80,13 @@ export class SigfoxDeviceTypeController { const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword( groupId ); - checkIfUserHasReadAccessToOrganization(req, group.belongsTo.id); + checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationRead); return await this.service.getById(group, id); } @Post() - @Write() + @ApplicationAdmin() @ApiCreatedResponse() @ApiBadRequestResponse() async create( @@ -101,7 +98,7 @@ export class SigfoxDeviceTypeController { const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword( groupId ); - checkIfUserHasWriteAccessToOrganization(req, group.belongsTo.id); + checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationWrite); const res = await this.service.create(group, dto); AuditLog.success( ActionType.CREATE, @@ -118,7 +115,7 @@ export class SigfoxDeviceTypeController { } @Put(":id") - @Write() + @ApplicationAdmin() @ApiNoContentResponse() @ApiBadRequestResponse() @HttpCode(204) @@ -132,7 +129,7 @@ export class SigfoxDeviceTypeController { const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword( groupId ); - checkIfUserHasWriteAccessToOrganization(req, group.belongsTo.id); + checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationWrite); await this.service.update(group, id, dto); AuditLog.success( ActionType.UPDATE, diff --git a/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts b/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts index 3bcec8d4..fa291927 100644 --- a/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts +++ b/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts @@ -24,7 +24,7 @@ import { } from "@nestjs/swagger"; import { ComposeAuthGuard } from '@auth/compose-auth.guard'; -import { Read, Write } from "@auth/roles.decorator"; +import { Read, ApplicationAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { CreateSigFoxGroupRequestDto } from "@dto/sigfox/internal/create-sigfox-group-request.dto"; @@ -32,16 +32,13 @@ import { ListAllSigFoxGroupResponseDto } from "@dto/sigfox/internal/list-all-sig import { SigFoxGetAllRequestDto } from "@dto/sigfox/internal/sigfox-get-all-request.dto"; import { UpdateSigFoxGroupRequestDto } from "@dto/sigfox/internal/update-sigfox-group-request.dto"; import { SigFoxGroup } from "@entities/sigfox-group.entity"; -import { - checkIfUserHasReadAccessToOrganization, - checkIfUserHasWriteAccessToOrganization, -} from "@helpers/security-helper"; import { SigFoxGroupService } from "@services/sigfox/sigfox-group.service"; import { SigFoxTestResponse } from "@dto/sigfox/internal/sigfox-test-response.dto"; import { GenericSigfoxAdministationService } from "@services/sigfox/generic-sigfox-administation.service"; import { ErrorCodes } from "@enum/error-codes.enum"; import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; +import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; @ApiTags("SigFox") @Controller("sigfox-group") @@ -65,7 +62,7 @@ export class SigfoxGroupController { @Req() req: AuthenticatedRequest, @Query() query: SigFoxGetAllRequestDto ): Promise { - checkIfUserHasReadAccessToOrganization(req, query.organizationId); + checkIfUserHasAccessToOrganization(req, query.organizationId, OrganizationAccessScope.ApplicationRead); return await this.service.findAllForOrganization(query.organizationId); } @@ -83,7 +80,7 @@ export class SigfoxGroupController { } catch (err) { return null; } - checkIfUserHasReadAccessToOrganization(req, group.belongsTo.id); + checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationRead); return group; } @@ -91,13 +88,13 @@ export class SigfoxGroupController { @ApiProduces("application/json") @ApiOperation({ summary: "Create a SigFox Group connection" }) @ApiCreatedResponse() - @Write() + @ApplicationAdmin() async create( @Req() req: AuthenticatedRequest, @Body() query: CreateSigFoxGroupRequestDto ): Promise { try { - checkIfUserHasWriteAccessToOrganization(req, query.organizationId); + checkIfUserHasAccessToOrganization(req, query.organizationId, OrganizationAccessScope.ApplicationWrite); const group = await this.service.create(query, req.user.userId); AuditLog.success( @@ -121,7 +118,7 @@ export class SigfoxGroupController { @Put(":id") @ApiProduces("application/json") @ApiOperation({ summary: "Update a SigFox Groups" }) - @Write() + @ApplicationAdmin() async update( @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number, @@ -134,7 +131,7 @@ export class SigfoxGroupController { AuditLog.fail(ActionType.CREATE, SigFoxGroup.name, req.user.userId, id); throw new NotFoundException(); } - checkIfUserHasWriteAccessToOrganization(req, group.belongsTo.id); + checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationWrite); try { const changeGroup = await this.service.update(group, dto, req.user.userId); AuditLog.success( @@ -162,7 +159,7 @@ export class SigfoxGroupController { @Req() req: AuthenticatedRequest, @Body() dto: CreateSigFoxGroupRequestDto ): Promise { - checkIfUserHasWriteAccessToOrganization(req, dto.organizationId); + checkIfUserHasAccessToOrganization(req, dto.organizationId, OrganizationAccessScope.ApplicationWrite); const group = new SigFoxGroup(); group.username = dto.username; diff --git a/src/controllers/api-key/api-key-info.controller.ts b/src/controllers/api-key/api-key-info.controller.ts index ab4b657c..1a0b4ce0 100644 --- a/src/controllers/api-key/api-key-info.controller.ts +++ b/src/controllers/api-key/api-key-info.controller.ts @@ -37,7 +37,8 @@ export class ApiKeyInfoController { @Req() req: AuthenticatedRequest, @Query() query?: ListAllEntitiesDto ): Promise { - const allowedOrganizations = req.user.permissions.getAllOrganizationsWithAtLeastRead(); + // The API Key will have access to at least read from a specific + const allowedOrganizations = req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead(); if (allowedOrganizations.length !== 1) { this.logger.error( diff --git a/src/controllers/api-key/api-key.controller.ts b/src/controllers/api-key/api-key.controller.ts index 054779a5..91d429b1 100644 --- a/src/controllers/api-key/api-key.controller.ts +++ b/src/controllers/api-key/api-key.controller.ts @@ -1,5 +1,5 @@ import { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { OrganizationAdmin, Read } from "@auth/roles.decorator"; +import { Read, UserAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { ApiKeyResponseDto } from "@dto/api-key/api-key-response.dto"; import { CreateApiKeyDto } from "@dto/api-key/create-api-key.dto"; @@ -10,10 +10,6 @@ import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { ApiKey } from "@entities/api-key.entity"; import { ActionType } from "@entities/audit-log-entry"; import { ErrorCodes } from "@enum/error-codes.enum"; -import { - checkIfUserHasAdminAccessToAllOrganizations, - checkIfUserHasAdminAccessToOrganization, -} from "@helpers/security-helper"; import { Body, Controller, @@ -42,11 +38,14 @@ import { ApiKeyService } from "@services/api-key-management/api-key.service"; import { AuditLog } from "@services/audit-log.service"; import { OrganizationService } from "@services/user-management/organization.service"; import { UpdateApiKeyDto } from "@dto/api-key/update-api-key.dto"; +import { + checkIfUserHasAccessToOrganization, + OrganizationAccessScope, +} from "@helpers/security-helper"; @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() -@OrganizationAdmin() -@Read() +@UserAdmin() @ApiForbiddenResponse() @ApiUnauthorizedResponse() @ApiTags("API Key Management") @@ -134,7 +133,11 @@ export class ApiKeyController { @Req() req: AuthenticatedRequest, @Query() query: ListAllApiKeysDto ): Promise { - checkIfUserHasAdminAccessToOrganization(req, query.organizationId); + checkIfUserHasAccessToOrganization( + req, + query.organizationId, + OrganizationAccessScope.UserAdministrationWrite + ); try { return this.apiKeyService.findAllByOrganizationId(query); @@ -177,9 +180,12 @@ export class ApiKeyController { permissionIds ); - checkIfUserHasAdminAccessToAllOrganizations( - req, - apiKeyOrganizations.map(x => x.id) - ); + for (const id of apiKeyOrganizations.map(org => org.id)) { + checkIfUserHasAccessToOrganization( + req, + id, + OrganizationAccessScope.UserAdministrationWrite + ); + } } } diff --git a/src/controllers/user-management/auth.controller.ts b/src/controllers/user-management/auth.controller.ts index ac22f1e4..d1947b92 100644 --- a/src/controllers/user-management/auth.controller.ts +++ b/src/controllers/user-management/auth.controller.ts @@ -41,7 +41,7 @@ import { Request as expressRequest, Response } from "express"; import { KombitStrategy } from "@auth/kombit.strategy"; import { ErrorCodes } from "@enum/error-codes.enum"; import { CustomExceptionFilter } from "@auth/custom-exception-filter"; -import { OrganizationPermission } from "@entities/permission.entity"; +import { isOrganizationPermission } from "@helpers/security-helper"; @UseFilters(new CustomExceptionFilter()) @ApiTags("Auth") @@ -187,17 +187,12 @@ export class AuthController { return (await this.organisationService.findAll()).data; } - const orgs = user.permissions.map(x => { - if ( - [ - PermissionType.OrganizationAdmin, - PermissionType.Write, - PermissionType.Read, - ].some(p => p == x.type) - ) { - return (x as OrganizationPermission).organization; + const orgs = user.permissions.reduce((arr, orgP) => { + if (isOrganizationPermission(orgP)) { + arr.push(orgP.organization); } - }); + return arr; + }, []); return _.uniqBy(orgs, x => x.id); } } diff --git a/src/controllers/user-management/new-kombit-creation.controller.ts b/src/controllers/user-management/new-kombit-creation.controller.ts index b2605f2d..4c3e32cd 100644 --- a/src/controllers/user-management/new-kombit-creation.controller.ts +++ b/src/controllers/user-management/new-kombit-creation.controller.ts @@ -35,7 +35,6 @@ import { AuditLog } from "@services/audit-log.service"; import { OrganizationService } from "@services/user-management/organization.service"; import { PermissionService } from "@services/user-management/permission.service"; import { UserService } from "@services/user-management/user.service"; -import { Permission, OrganizationPermission } from "@entities/permission.entity"; @UseGuards(JwtAuthGuard) @ApiBearerAuth() @@ -119,14 +118,14 @@ export class NewKombitCreationController { @Req() req: AuthenticatedRequest, @Body() updateUserOrgsDto: UpdateUserOrgsDto ): Promise { - + try { const user = await this.userService.findOne(req.user.userId); - const permissions: OrganizationPermission[] = await this.permissionService.findManyWithRelations( + const permissions = await this.permissionService.findManyWithRelations( updateUserOrgsDto.requestedOrganizationIds ); - const requestedOrganizations = await this.organizationService.mapPermissionsToOrganizations( + const requestedOrganizations = this.organizationService.mapPermissionsToOrganizations( permissions ); @@ -160,7 +159,7 @@ export class NewKombitCreationController { @Query("extendedInfo") extendedInfo?: boolean ): Promise { - const getExtendedInfo = extendedInfo != null ? extendedInfo : false; + const getExtendedInfo = extendedInfo != null ? extendedInfo : false; try { // Don't leak the passwordHash // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/controllers/user-management/organization.controller.ts b/src/controllers/user-management/organization.controller.ts index fc4fc8e5..b8a0a05a 100644 --- a/src/controllers/user-management/organization.controller.ts +++ b/src/controllers/user-management/organization.controller.ts @@ -23,8 +23,7 @@ import { } from "@nestjs/swagger"; import { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { GlobalAdmin, Read } from "@auth/roles.decorator"; -import { OrganizationAdmin } from "@auth/roles.decorator"; +import { GlobalAdmin, Read, UserAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { DeleteResponseDto } from "@dto/delete-application-response.dto"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; @@ -36,7 +35,6 @@ import { CreateOrganizationDto } from "@dto/user-management/create-organization. import { UpdateOrganizationDto } from "@dto/user-management/update-organization.dto"; import { Organization } from "@entities/organization.entity"; import { ErrorCodes } from "@enum/error-codes.enum"; -import { checkIfUserHasAdminAccessToOrganization } from "@helpers/security-helper"; import { OrganizationService } from "@services/user-management/organization.service"; import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; @@ -45,18 +43,17 @@ import { UserService } from "@services/user-management/user.service"; @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() -@OrganizationAdmin() @ApiForbiddenResponse() @ApiUnauthorizedResponse() @ApiTags("User Management") @Controller("organization") +@GlobalAdmin() export class OrganizationController { constructor( private organizationService: OrganizationService, ) {} private readonly logger = new Logger(OrganizationController.name); - @GlobalAdmin() @Post() @ApiOperation({ summary: "Create a new Organization" }) async create( @@ -91,8 +88,6 @@ export class OrganizationController { @Body() updateOrganizationDto: UpdateOrganizationDto ): Promise { try { - checkIfUserHasAdminAccessToOrganization(req, id); - const organization = await this.organizationService.update( id, updateOrganizationDto, @@ -127,6 +122,7 @@ export class OrganizationController { @Get() @ApiOperation({ summary: "Get list of all Organizations" }) + @UserAdmin() async findAll( @Req() req: AuthenticatedRequest, @Query() query?: ListAllEntitiesDto @@ -134,7 +130,7 @@ export class OrganizationController { if (req.user.permissions.isGlobalAdmin) { return this.organizationService.findAllPaginated(query); } else { - const allowedOrganizations = req.user.permissions.getAllOrganizationsWithAtLeastAdmin(); + const allowedOrganizations = req.user.permissions.getAllOrganizationsWithUserAdmin(); return this.organizationService.findAllInOrganizationList( allowedOrganizations, query @@ -145,11 +141,11 @@ export class OrganizationController { @Get(":id") @ApiOperation({ summary: "Get one Organization" }) @ApiNotFoundResponse() + @UserAdmin() async findOne( @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number ): Promise { - checkIfUserHasAdminAccessToOrganization(req, id); try { return await this.organizationService.findByIdWithRelations(id); } catch (err) { @@ -165,8 +161,6 @@ export class OrganizationController { @Param("id", new ParseIntPipe()) id: number ): Promise { try { - checkIfUserHasAdminAccessToOrganization(req, id); - const result = await this.organizationService.delete(id); AuditLog.success(ActionType.DELETE, Organization.name, req.user.userId, id); diff --git a/src/controllers/user-management/permission.controller.ts b/src/controllers/user-management/permission.controller.ts index fdca60ab..982c0edb 100644 --- a/src/controllers/user-management/permission.controller.ts +++ b/src/controllers/user-management/permission.controller.ts @@ -22,19 +22,18 @@ import { } from "@nestjs/swagger"; import { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { OrganizationAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { DeleteResponseDto } from "@dto/delete-application-response.dto"; import { ListAllPermissionsResponseDto } from "@dto/list-all-permissions-response.dto"; import { CreatePermissionDto } from "@dto/user-management/create-permission.dto"; import { UpdatePermissionDto } from "@dto/user-management/update-permission.dto"; import { AuthenticatedRequest } from "@entities/dto/internal/authenticated-request"; -import { OrganizationPermission } from "@entities/permission.entity"; -import { Permission } from "@entities/permission.entity"; +import { Permission } from "@entities/permissions/permission.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { - checkIfUserHasAdminAccessToOrganization, checkIfUserIsGlobalAdmin, + checkIfUserHasAccessToOrganization, + OrganizationAccessScope, } from "@helpers/security-helper"; import { PermissionService } from "@services/user-management/permission.service"; import { AuditLog } from "@services/audit-log.service"; @@ -45,6 +44,7 @@ import { ListAllPaginated } from "@dto/list-all-paginated.dto"; import { ListAllPermissionsDto } from "@dto/list-all-permissions.dto"; import { ApplicationService } from "@services/device-management/application.service"; import { ListAllApplicationsResponseDto } from "@dto/list-all-applications-response.dto"; +import { UserAdmin } from "@auth/roles.decorator"; import { PermissionRequestAcceptUser } from "@dto/user-management/add-user-to-permission.dto"; import { OrganizationService } from "@services/user-management/organization.service"; import { Organization } from "@entities/organization.entity"; @@ -52,7 +52,6 @@ import { User } from "@entities/user.entity"; @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() -@OrganizationAdmin() @ApiForbiddenResponse() @ApiUnauthorizedResponse() @ApiTags("User Management") @@ -67,12 +66,17 @@ export class PermissionController { @Post() @ApiOperation({ summary: "Create new permission entity" }) + @UserAdmin() async createPermission( @Req() req: AuthenticatedRequest, @Body() dto: CreatePermissionDto ): Promise { try { - checkIfUserHasAdminAccessToOrganization(req, dto.organizationId); + checkIfUserHasAccessToOrganization( + req, + dto.organizationId, + OrganizationAccessScope.UserAdministrationWrite + ); const result = await this.permissionService.createNewPermission( dto, @@ -100,26 +104,30 @@ export class PermissionController { @Body() dto: PermissionRequestAcceptUser ): Promise { try { - checkIfUserHasAdminAccessToOrganization(req, dto.organizationId); - let dbPermission: Permission; + checkIfUserHasAccessToOrganization( + req, + dto.organizationId, + OrganizationAccessScope.UserAdministrationWrite + ); - const permissions: OrganizationPermission[] = await this.permissionService.findOneWithRelations( + const permissions = await this.permissionService.findOneWithRelations( dto.organizationId - ); + ); - const org: Organization = await this.organizationService.mapPermissionsToOneOrganization( + const org: Organization = this.organizationService.mapPermissionsToOneOrganization( permissions ); - + const user: User = await this.userService.findOne(dto.userId); - for (let index = 0; index < org.permissions.length; index++) { - if (org.permissions[index].type === dto.level) { - dbPermission = await this.permissionService.getPermission( - org.permissions[index].id - ); + const newUserPermissions: Permission[] = []; + + for (const orgPermission of org.permissions) { + if (dto.permissionIds.includes(orgPermission.id)) { + newUserPermissions.push(orgPermission); } } - const resultUser = await this.userService.acceptUser(user, org, dbPermission); + + const resultUser = await this.userService.acceptUser(user, org, newUserPermissions); AuditLog.success( ActionType.UPDATE, @@ -137,6 +145,7 @@ export class PermissionController { @Put(":id") @ApiOperation({ summary: "Update permission" }) + @UserAdmin() async updatePermission( @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number, @@ -144,13 +153,13 @@ export class PermissionController { ): Promise { try { const permission = await this.permissionService.getPermission(id); - if (permission.type == PermissionType.GlobalAdmin) { + if (permission.type.some(({ type }) => type === PermissionType.GlobalAdmin)) { checkIfUserIsGlobalAdmin(req); } else { - const organizationPermission = permission as OrganizationPermission; - checkIfUserHasAdminAccessToOrganization( + checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id + permission.organization.id, + OrganizationAccessScope.UserAdministrationWrite ); } @@ -176,19 +185,20 @@ export class PermissionController { @Delete(":id") @ApiOperation({ summary: "Delete a permission entity" }) + @UserAdmin() async deletePermission( @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number ): Promise { try { const permission = await this.permissionService.getPermission(id); - if (permission.type == PermissionType.GlobalAdmin) { + if (permission.type.some(({ type }) => type === PermissionType.GlobalAdmin)) { throw new BadRequestException("You cannot delete GlobalAdmin"); } else { - const organizationPermission = permission as OrganizationPermission; - checkIfUserHasAdminAccessToOrganization( + checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id + permission.organization.id, + OrganizationAccessScope.UserAdministrationWrite ); } @@ -211,7 +221,7 @@ export class PermissionController { if (req.user.permissions.isGlobalAdmin) { return this.permissionService.getAllPermissions(query); } else { - const allowedOrganizations = req.user.permissions.getAllOrganizationsWithAtLeastAdmin(); + const allowedOrganizations = req.user.permissions.getAllOrganizationsWithUserAdmin(); return this.permissionService.getAllPermissionsInOrganizations( allowedOrganizations, query @@ -235,17 +245,17 @@ export class PermissionController { if ( req.user.permissions.isGlobalAdmin || - permission.type == PermissionType.GlobalAdmin + permission.type.some(({ type }) => type === PermissionType.GlobalAdmin) ) { return permission; } else { - const organizationPermission = permission as OrganizationPermission; - checkIfUserHasAdminAccessToOrganization( + checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id + permission.organization.id, + OrganizationAccessScope.UserAdministrationWrite ); - return organizationPermission; + return permission; } } @@ -271,14 +281,14 @@ export class PermissionController { if ( req.user.permissions.isGlobalAdmin || - permission.type == PermissionType.GlobalAdmin + permission.type.some(({ type }) => type === PermissionType.GlobalAdmin) ) { return await applicationsPromise; } else { - const organizationPermission = permission as OrganizationPermission; - checkIfUserHasAdminAccessToOrganization( + checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id + permission.organization.id, + OrganizationAccessScope.UserAdministrationWrite ); return await applicationsPromise; @@ -304,14 +314,14 @@ export class PermissionController { if ( req.user.permissions.isGlobalAdmin || - permission.type == PermissionType.GlobalAdmin + permission.type.some(({ type }) => type === PermissionType.GlobalAdmin) ) { return await users; } else { - const organizationPermission = permission as OrganizationPermission; - checkIfUserHasAdminAccessToOrganization( + checkIfUserHasAccessToOrganization( req, - organizationPermission?.organization?.id + permission?.organization?.id, + OrganizationAccessScope.UserAdministrationWrite ); return users; diff --git a/src/controllers/user-management/user.controller.ts b/src/controllers/user-management/user.controller.ts index a28be883..f246357b 100644 --- a/src/controllers/user-management/user.controller.ts +++ b/src/controllers/user-management/user.controller.ts @@ -17,7 +17,6 @@ import { Logger } from "@nestjs/common"; import { ApiBearerAuth, ApiForbiddenResponse, - ApiNotFoundResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse, @@ -25,17 +24,14 @@ import { import { QueryFailedError } from "typeorm"; import { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { OrganizationAdmin, Read } from "@auth/roles.decorator"; +import { Read, UserAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { CreateUserDto } from "@dto/user-management/create-user.dto"; import { UpdateUserDto } from "@dto/user-management/update-user.dto"; import { UserResponseDto } from "@dto/user-response.dto"; import { ErrorCodes } from "@entities/enum/error-codes.enum"; -import { - checkIfUserHasAdminAccessToOrganization, - checkIfUserIsGlobalAdmin, -} from "@helpers/security-helper"; +import { checkIfUserIsGlobalAdmin, checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; import { UserService } from "@services/user-management/user.service"; import { ListAllUsersResponseDto } from "@dto/list-all-users-response.dto"; import { ListAllUsersMinimalResponseDto } from "@dto/list-all-users-minimal-response.dto"; @@ -43,15 +39,12 @@ import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; import { User } from "@entities/user.entity"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; -import { CreateNewKombitUserDto } from "@dto/user-management/create-new-kombit-user.dto"; import { OrganizationService } from "@services/user-management/organization.service"; -import { UpdateUserOrgsDto } from "@dto/user-management/update-user-orgs.dto"; import { Organization } from "@entities/organization.entity"; import { RejectUserDto } from "@dto/user-management/reject-user.dto"; @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() -@OrganizationAdmin() @ApiForbiddenResponse() @ApiUnauthorizedResponse() @ApiTags("User Management") @@ -73,6 +66,7 @@ export class UserController { @Post() @ApiOperation({ summary: "Create a new User" }) + @UserAdmin() async create( @Req() req: AuthenticatedRequest, @Body() createUserDto: CreateUserDto @@ -113,19 +107,20 @@ export class UserController { @Put("/rejectUser") @ApiOperation({ summary: "Rejects user and removes from awaiting users" }) async rejectUser( - @Req() req: AuthenticatedRequest, + @Req() req: AuthenticatedRequest, @Body() body: RejectUserDto ): Promise { - checkIfUserHasAdminAccessToOrganization(req, body.orgId); + checkIfUserHasAccessToOrganization(req, body.orgId, OrganizationAccessScope.UserAdministrationWrite); const user = await this.userService.findOne(body.userIdToReject); const organization = await this.organizationService.findByIdWithUsers(body.orgId); - return await this.organizationService.rejectAwaitingUser(user, organization); + return await this.organizationService.rejectAwaitingUser(user, organization); } @Put(":id") @ApiOperation({ summary: "Change a user" }) + @UserAdmin() async update( @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number, @@ -136,8 +131,7 @@ export class UserController { checkIfUserIsGlobalAdmin(req); } // Don't leak the passwordHash - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { passwordHash, ...user } = await this.userService.updateUser( + const { passwordHash: _, ...user } = await this.userService.updateUser( id, dto, req.user.userId @@ -184,8 +178,7 @@ export class UserController { const getExtendedInfo = extendedInfo != null ? extendedInfo : false; try { // Don't leak the passwordHash - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { passwordHash, ...user } = await this.userService.findOne( + const { passwordHash: _, ...user } = await this.userService.findOne( id, getExtendedInfo, getExtendedInfo diff --git a/src/entities/api-key.entity.ts b/src/entities/api-key.entity.ts index 57721b84..e0857227 100644 --- a/src/entities/api-key.entity.ts +++ b/src/entities/api-key.entity.ts @@ -10,7 +10,7 @@ import { Unique, } from "typeorm"; import { DbBaseEntity } from "./base.entity"; -import { ApiKeyPermission } from "./permission.entity"; +import { Permission } from "./permissions/permission.entity"; @Entity("api_key") @Unique([nameof("key")]) @@ -21,9 +21,9 @@ export class ApiKey extends DbBaseEntity { @Column() name: string; - @ManyToMany(_ => ApiKeyPermission, apiKeyPm => apiKeyPm.apiKeys) + @ManyToMany(_ => Permission, apiKeyPm => apiKeyPm.apiKeys) @JoinTable() - permissions: ApiKeyPermission[]; + permissions: Permission[]; @OneToOne(() => User, u => u.apiKeyRef, { nullable: false, diff --git a/src/entities/application.entity.ts b/src/entities/application.entity.ts index f8a2c774..c8835d49 100644 --- a/src/entities/application.entity.ts +++ b/src/entities/application.entity.ts @@ -1,6 +1,5 @@ import { DataTarget } from "@entities/data-target.entity"; import { IoTDevice } from "@entities/iot-device.entity"; -import { OrganizationApplicationPermission } from "@entities/permission.entity"; import { Organization } from "@entities/organization.entity"; import { ApplicationStatus } from "@enum/application-status.enum"; import { @@ -16,6 +15,7 @@ import { import { ApplicationDeviceType } from "./application-device-type.entity"; import { ControlledProperty } from "./controlled-property.entity"; import { Multicast } from "./multicast.entity"; +import { Permission } from "./permissions/permission.entity"; import { DbBaseEntity } from "@entities/base.entity"; @Entity("application") @@ -61,11 +61,11 @@ export class Application extends DbBaseEntity { @ManyToMany( // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => OrganizationApplicationPermission, + type => Permission, permission => permission.applications ) @JoinTable() - permissions: OrganizationApplicationPermission[]; + permissions: Permission[]; @Column({ nullable: true }) status?: ApplicationStatus; diff --git a/src/entities/dto/list-all-permissions-response.dto.ts b/src/entities/dto/list-all-permissions-response.dto.ts index ece88a5a..d5411a0a 100644 --- a/src/entities/dto/list-all-permissions-response.dto.ts +++ b/src/entities/dto/list-all-permissions-response.dto.ts @@ -1,4 +1,4 @@ -import { Permission } from "@entities/permission.entity"; +import { Permission } from "@entities/permissions/permission.entity"; import { ListAllEntitiesResponseDto } from "./list-all-entities-response.dto"; diff --git a/src/entities/dto/permission-minimal.dto.ts b/src/entities/dto/permission-minimal.dto.ts index d5bd19e8..f7aeb829 100644 --- a/src/entities/dto/permission-minimal.dto.ts +++ b/src/entities/dto/permission-minimal.dto.ts @@ -1,7 +1,7 @@ import { PermissionType } from "@enum/permission-type.enum"; export class PermissionMinimalDto { - permission_type: PermissionType; + permission_type_type: PermissionType; organization_id: number; application_id: number; } diff --git a/src/entities/dto/permission-organization-application.dto.ts b/src/entities/dto/permission-organization-application.dto.ts index 99919639..975a45f4 100644 --- a/src/entities/dto/permission-organization-application.dto.ts +++ b/src/entities/dto/permission-organization-application.dto.ts @@ -2,43 +2,53 @@ import * as _ from "lodash"; export class UserPermissions { constructor() { - this.readPermissions = new Map(); - this.writePermissions = new Map(); - this.organizationAdminPermissions = new Set(); + this.orgToReadPermissions = new Map(); + this.orgToUserAdminPermissions = new Set(); + this.orgToGatewayAdminPermissions = new Set(); + this.orgToApplicationAdminPermissions = new Map(); } - readPermissions: Map; - writePermissions: Map; - organizationAdminPermissions: Set; + orgToReadPermissions: Map; + orgToUserAdminPermissions: Set; + orgToGatewayAdminPermissions: Set; + orgToApplicationAdminPermissions: Map; isGlobalAdmin = false; getAllApplicationsWithAtLeastRead(): number[] { return _.union( - this.extractValues(this.readPermissions), - this.getAllApplicationsWithAtLeastWrite() + this.extractValues(this.orgToReadPermissions), + this.getAllApplicationsWithAdmin() ); } - getAllApplicationsWithAtLeastWrite(): number[] { - return this.extractValues(this.writePermissions); + getAllApplicationsWithAdmin(): number[] { + return this.extractValues(this.orgToApplicationAdminPermissions); } - getAllOrganizationsWithAtLeastRead(): number[] { + getAllOrganizationsWithAtLeastUserAdminRead(): number[] { return _.union( - this.extractKeys(this.readPermissions), - this.getAllOrganizationsWithAtLeastWrite() + this.extractKeys(this.orgToReadPermissions), + this.getAllOrganizationsWithUserAdmin() ); } - getAllOrganizationsWithAtLeastWrite(): number[] { + getAllOrganizationsWithAtLeastApplicationRead(): number[] { return _.union( - this.extractKeys(this.writePermissions), - this.getAllOrganizationsWithAtLeastAdmin() + this.extractKeys(this.orgToReadPermissions), + this.getAllOrganizationsWithApplicationAdmin() ); } - getAllOrganizationsWithAtLeastAdmin(): number[] { - return Array.from(this.organizationAdminPermissions); + getAllOrganizationsWithUserAdmin(): number[] { + return Array.from(this.orgToUserAdminPermissions); + } + + getAllOrganizationsWithGatewayAdmin(): number[] { + return Array.from(this.orgToGatewayAdminPermissions); + } + + getAllOrganizationsWithApplicationAdmin(): number[] { + return this.extractKeys(this.orgToApplicationAdminPermissions); } private extractValues(map: Map): number[] { diff --git a/src/entities/dto/user-management/add-user-to-permission.dto.ts b/src/entities/dto/user-management/add-user-to-permission.dto.ts index f7dd7274..0b084aa9 100644 --- a/src/entities/dto/user-management/add-user-to-permission.dto.ts +++ b/src/entities/dto/user-management/add-user-to-permission.dto.ts @@ -1,6 +1,5 @@ -import { PermissionType } from "@enum/permission-type.enum"; import { ApiProperty } from "@nestjs/swagger"; -import { IsEnum, IsNumber } from "class-validator"; +import { IsNumber, IsArray, ArrayUnique, ArrayNotEmpty } from "class-validator"; export class PermissionRequestAcceptUser { @ApiProperty({ required: true }) @@ -11,10 +10,9 @@ export class PermissionRequestAcceptUser { @IsNumber() userId: number; - @ApiProperty({ - required: true, - enum: PermissionType, - }) - @IsEnum(PermissionType) - level: "OrganizationAdmin" | "Write" | "Read"; + @ApiProperty({ required: true }) + @IsArray() + @ArrayNotEmpty() + @ArrayUnique() + permissionIds: number[]; } diff --git a/src/entities/dto/user-management/create-permission.dto.ts b/src/entities/dto/user-management/create-permission.dto.ts index 022862d0..d0eaaa1c 100644 --- a/src/entities/dto/user-management/create-permission.dto.ts +++ b/src/entities/dto/user-management/create-permission.dto.ts @@ -1,15 +1,21 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsEnum, IsNumber, IsString, Length } from "class-validator"; - import { PermissionType } from "@entities/enum/permission-type.enum"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsNumber, IsString, Length, ValidateNested, IsArray, ArrayUnique } from "class-validator"; +import { PermissionTypeDto } from "./permission-type.dto"; +import { Type } from "class-transformer"; +import { ArrayDistinct } from "@helpers/array-distinct.validator"; +import { nameof } from "@helpers/type-helper"; export class CreatePermissionDto { @ApiProperty({ required: true, enum: PermissionType, }) - @IsEnum(PermissionType) - level: "OrganizationAdmin" | "Write" | "Read"; + @IsArray() + @ArrayDistinct(nameof('type')) + @Type(() => PermissionTypeDto) + @ValidateNested({ each: true }) + levels: PermissionTypeDto[] @ApiProperty({ required: true }) @IsString() diff --git a/src/entities/dto/user-management/permission-type.dto.ts b/src/entities/dto/user-management/permission-type.dto.ts new file mode 100644 index 00000000..9a48328e --- /dev/null +++ b/src/entities/dto/user-management/permission-type.dto.ts @@ -0,0 +1,7 @@ +import { PermissionType } from "@enum/permission-type.enum"; +import { IsEnum } from "class-validator"; + +export class PermissionTypeDto { + @IsEnum(PermissionType) + type: PermissionType; +} diff --git a/src/entities/enum/error-codes.enum.ts b/src/entities/enum/error-codes.enum.ts index a9d74281..a32b9d7c 100644 --- a/src/entities/enum/error-codes.enum.ts +++ b/src/entities/enum/error-codes.enum.ts @@ -45,6 +45,7 @@ export enum ErrorCodes { DeviceModelDoesNotExist = "MESSAGE.DEVICE-MODEL-DOES-NOT-EXIST", InvalidKeyInKeyValuePair = "MESSAGE.INVALID-KEY-IN-KEY-VALUE-PAIR", InvalidValueInKeyValuePair = "MESSAGE.INVALID-VALUE-IN-KEY-VALUE-PAIR", + DuplicatePermissionTypes = "MESSAGE.DUPLICATE-PERMISSION-TYPES", SendMailError = "MESSAGE.SEND-MAIL-ERROR", UserDoesNotExistInArray = "MESSAGE.USER-DOES-NOT-EXIST", UserAlreadyInPermission = "MESSAGE.USER-ALREADY-IN-PERMISSION", diff --git a/src/entities/enum/permission-type.enum.ts b/src/entities/enum/permission-type.enum.ts index aef46d09..16fa326d 100644 --- a/src/entities/enum/permission-type.enum.ts +++ b/src/entities/enum/permission-type.enum.ts @@ -1,9 +1,7 @@ export enum PermissionType { GlobalAdmin = "GlobalAdmin", - OrganizationAdmin = "OrganizationAdmin", - Write = "Write", + OrganizationUserAdmin = "OrganizationUserAdmin", + OrganizationGatewayAdmin = "OrganizationGatewayAdmin", + OrganizationApplicationAdmin = "OrganizationApplicationAdmin", Read = "Read", - OrganizationPermission = "OrganizationPermission", - OrganizationApplicationPermissions = "OrganizationApplicationPermissions", - ApiKeyPermission = "ApiKeyPermission", } diff --git a/src/entities/organization.entity.ts b/src/entities/organization.entity.ts index c5f8d4ac..25694148 100644 --- a/src/entities/organization.entity.ts +++ b/src/entities/organization.entity.ts @@ -3,7 +3,7 @@ import { Column, Entity, ManyToMany, OneToMany, Unique } from "typeorm"; import { Application } from "@entities/application.entity"; import { DbBaseEntity } from "@entities/base.entity"; import { PayloadDecoder } from "@entities/payload-decoder.entity"; -import { OrganizationPermission, Permission } from "@entities/permission.entity"; +import { Permission } from "@entities/permissions/permission.entity"; import { SigFoxGroup } from "./sigfox-group.entity"; import { DeviceModel } from "./device-model.entity"; @@ -25,7 +25,7 @@ export class Organization extends DbBaseEntity { @OneToMany( // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => OrganizationPermission, + type => Permission, permission => permission.organization, { onDelete: "CASCADE" } ) diff --git a/src/entities/permission.entity.ts b/src/entities/permission.entity.ts deleted file mode 100644 index a03df00e..00000000 --- a/src/entities/permission.entity.ts +++ /dev/null @@ -1,104 +0,0 @@ -//All Permissions is included in one file since circular references and typescript makes the program crash unregularaly. -//It happens because circular references can happen between files and not only types. - -import { User } from "@entities/user.entity"; -import { PermissionType } from "@enum/permission-type.enum"; -import { ChildEntity, Column, Entity, ManyToMany, ManyToOne, TableInheritance } from "typeorm"; -import { DbBaseEntity } from "@entities/base.entity"; -import { Organization } from "./organization.entity"; -import { ApiKey } from "./api-key.entity"; -import { Application } from "./application.entity"; - -@Entity() -@TableInheritance({ - column: { type: "enum", name: "type", enum: PermissionType }, -}) -export abstract class Permission extends DbBaseEntity { - constructor(name: string) { - super(); - this.name = name; - } - - @Column("enum", { - enum: PermissionType, - }) - type: PermissionType; - - @Column() - name: string; - - @ManyToMany( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - () => User, - user => user.permissions - ) - users: User[]; -} - -@ChildEntity(PermissionType.OrganizationPermission) -export abstract class OrganizationPermission extends Permission { - constructor(name: string, org: Organization) { - super(name); - this.organization = org; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - @ManyToOne(() => Organization, { onDelete: "CASCADE" }) - organization: Organization; -} - -@ChildEntity(PermissionType.GlobalAdmin) -export class GlobalAdminPermission extends Permission { - constructor() { - super("GlobalAdmin"); - this.type = PermissionType.GlobalAdmin; - } -} - -@ChildEntity(PermissionType.ApiKeyPermission) -export abstract class ApiKeyPermission extends Permission { - @ManyToMany(_ => ApiKey, key => key.permissions, { onDelete: "CASCADE" }) - apiKeys: ApiKey[]; - -} - -@ChildEntity(PermissionType.OrganizationApplicationPermissions) -export abstract class OrganizationApplicationPermission extends OrganizationPermission { - constructor(name: string, org: Organization, addNewApps?: boolean) { - super(name, org); - this.automaticallyAddNewApplications = - addNewApps != undefined ? addNewApps : false; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - @ManyToMany(() => Application, application => application.permissions) - applications: Application[]; - - @Column({ nullable: true, default: false, type: Boolean }) - automaticallyAddNewApplications = false; -} - -@ChildEntity(PermissionType.OrganizationAdmin) -export class OrganizationAdminPermission extends OrganizationPermission { - constructor(name: string, org: Organization) { - super(name, org); - this.type = PermissionType.OrganizationAdmin; - } -} - -@ChildEntity(PermissionType.Write) -export class WritePermission extends OrganizationApplicationPermission { - constructor(name: string, org: Organization, addNewApps = false) { - super(name, org, addNewApps); - this.type = PermissionType.Write; - } -} - -@ChildEntity(PermissionType.Read) -export class ReadPermission extends OrganizationApplicationPermission { - constructor(name: string, org: Organization, addNewApps = false) { - super(name, org, addNewApps); - this.type = PermissionType.Read; - } -} - diff --git a/src/entities/permissions/permission-type.entity.ts b/src/entities/permissions/permission-type.entity.ts new file mode 100644 index 00000000..988944be --- /dev/null +++ b/src/entities/permissions/permission-type.entity.ts @@ -0,0 +1,21 @@ +import { DbBaseEntity } from "@entities/base.entity"; +import { PermissionType } from "@enum/permission-type.enum"; +import { Column, Entity, ManyToOne, Unique } from "typeorm"; +import { Permission } from "./permission.entity"; +import { nameof } from "@helpers/type-helper"; + +@Entity("permission_type") +@Unique([nameof("type"), nameof("permission")]) +export class PermissionTypeEntity extends DbBaseEntity { + @Column("enum", { + enum: PermissionType + }) + type: PermissionType; + + @ManyToOne(() => Permission, p => p.type, { + onDelete: "CASCADE", + // Delete the row instead of null'ing application. Useful for updates + orphanedRowAction: "delete", + }) + permission: Permission; +} diff --git a/src/entities/permissions/permission.entity.ts b/src/entities/permissions/permission.entity.ts new file mode 100644 index 00000000..0e2b4111 --- /dev/null +++ b/src/entities/permissions/permission.entity.ts @@ -0,0 +1,53 @@ +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, +} from "typeorm"; +import { PermissionTypeEntity } from "./permission-type.entity"; +import { Application } from "@entities/application.entity"; +import { Organization } from "@entities/organization.entity"; +import { ApiKey } from "@entities/api-key.entity"; + +@Entity("permission") +export class Permission extends DbBaseEntity { + constructor(name: string, org?: Organization, addNewApps = false) { + super(); + this.name = name; + this.organization = org; + this.automaticallyAddNewApplications = addNewApps; + } + + @OneToMany(() => PermissionTypeEntity, entity => entity.permission, { + nullable: false, + cascade: true, + }) + type: PermissionTypeEntity[]; + + @Column() + name: string; + + @ManyToMany( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + () => User, + user => user.permissions + ) + users: User[]; + + @ManyToMany(() => Application, application => application.permissions) + applications: Application[]; + + @Column({ nullable: true, default: false, type: Boolean }) + automaticallyAddNewApplications = false; + + @ManyToOne(() => Organization, { onDelete: "CASCADE" }) + organization: Organization; + + @ManyToMany(_ => ApiKey, key => key.permissions, { onDelete: "CASCADE" }) + apiKeys: ApiKey[]; +} diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 9d673c07..1e287a49 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -1,15 +1,8 @@ import { ApiKey } from "@entities/api-key.entity"; -import { - Column, - Entity, - JoinTable, - ManyToMany, - Unique, - OneToOne, -} from "typeorm"; -import { Organization } from "./organization.entity"; -import { Permission } from "@entities/permission.entity"; import { DbBaseEntity } from "@entities/base.entity"; +import { Permission } from "@entities/permissions/permission.entity"; +import { Column, Entity, JoinTable, ManyToMany, OneToOne, Unique } from "typeorm"; +import { Organization } from "./organization.entity"; @Entity("user") @Unique(["email"]) @@ -40,12 +33,16 @@ export class User extends DbBaseEntity { @JoinTable() permissions: Permission[]; - @ManyToMany(_ => Organization, requestedOrganizations => requestedOrganizations.awaitingUsers, { - nullable: true, - }) + @ManyToMany( + _ => Organization, + requestedOrganizations => requestedOrganizations.awaitingUsers, + { + nullable: true, + } + ) @JoinTable() requestedOrganizations: Organization[]; - + @OneToOne(type => ApiKey, a => a.systemUser, { nullable: true, cascade: false, diff --git a/src/helpers/array-distinct.validator.ts b/src/helpers/array-distinct.validator.ts new file mode 100644 index 00000000..9c5a4df2 --- /dev/null +++ b/src/helpers/array-distinct.validator.ts @@ -0,0 +1,41 @@ +import { + registerDecorator, + ValidationArguments, + ValidationOptions, +} from "class-validator"; +import { ErrorCodes } from "@enum/error-codes.enum"; + +/** + * + * @param property + * @param validationOptions + * @see https://github.com/typestack/class-validator/issues/592#issuecomment-621645012 + */ +export function ArrayDistinct( + property: string, + validationOptions?: ValidationOptions +) { + return (object: unknown, propertyName: string): void => { + registerDecorator({ + name: "ArrayDistinct", + target: object.constructor, + propertyName, + constraints: [property], + options: validationOptions, + validator: { + validate(value: unknown): boolean { + if (Array.isArray(value)) { + const distinct = [ + ...new Set(value.map((v): unknown => v[property])), + ]; + return distinct.length === value.length; + } + return false; + }, + defaultMessage(args: ValidationArguments): string { + return ErrorCodes.DuplicatePermissionTypes; + }, + }, + }); + }; +} diff --git a/src/helpers/iot-device.helper.ts b/src/helpers/iot-device.helper.ts index bb6ecebc..0efb9eb3 100644 --- a/src/helpers/iot-device.helper.ts +++ b/src/helpers/iot-device.helper.ts @@ -7,7 +7,10 @@ import { SigFoxDeviceWithBackendDataDto } from "@dto/sigfox-device-with-backend- import { UpdateIoTDeviceDto } from "@dto/update-iot-device.dto"; import { IoTDevice } from "@entities/iot-device.entity"; import { ErrorCodes } from "@enum/error-codes.enum"; -import { checkIfUserHasWriteAccessToApplication } from "./security-helper"; +import { + ApplicationAccessScope, + checkIfUserHasAccessToApplication, +} from "./security-helper"; /** * Iterate through the devices once, splitting it into a tuple with the data we want to log @@ -62,11 +65,19 @@ export function ensureUpdatePayload( return res; } - checkIfUserHasWriteAccessToApplication(req, oldDevice.application.id); + checkIfUserHasAccessToApplication( + req, + oldDevice.application.id, + ApplicationAccessScope.Write + ); if (updateDeviceDto.applicationId !== oldDevice.application.id) { // New application - checkIfUserHasWriteAccessToApplication(req, updateDeviceDto.applicationId); + checkIfUserHasAccessToApplication( + req, + updateDeviceDto.applicationId, + ApplicationAccessScope.Write + ); } res.push(updateDeviceDto); return res; diff --git a/src/helpers/permission.helper.ts b/src/helpers/permission.helper.ts new file mode 100644 index 00000000..9c161610 --- /dev/null +++ b/src/helpers/permission.helper.ts @@ -0,0 +1,82 @@ +import { Organization } from "@entities/organization.entity"; +import { Permission } from "@entities/permissions/permission.entity"; +import { PermissionType } from "@enum/permission-type.enum"; +import { PermissionTypeEntity } from "@entities/permissions/permission-type.entity"; + +export abstract class PermissionCreator { + private static create( + name: string, + org?: Organization, + addNewApps = false + ): Permission { + const pm = new Permission(name, org, addNewApps); + return pm; + } + + static createByTypes( + name: string, + types: PermissionType[], + org?: Organization, + addNewApps = false + ): Permission { + const pm = new Permission(name, org, addNewApps); + pm.type = types.map(type => { + const entity = new PermissionTypeEntity(); + entity.type = type; + return entity; + }); + return pm; + } + + static createGlobalAdmin(): Permission { + const pm = this.create("GlobalAdmin"); + pm.type = [{ type: PermissionType.GlobalAdmin } as PermissionTypeEntity]; + return pm; + } + + static createRead(name: string, org?: Organization, addNewApps = false): Permission { + const pm = this.create(name, org, addNewApps); + + pm.type = [{ type: PermissionType.Read } as PermissionTypeEntity]; + return pm; + } + + static createApplicationAdmin( + name: string, + org?: Organization, + addNewApps = false + ): Permission { + const pm = this.create(name, org, addNewApps); + + pm.type = [ + { type: PermissionType.OrganizationApplicationAdmin } as PermissionTypeEntity, + ]; + return pm; + } + + static createUserAdmin( + name: string, + org?: Organization, + addNewApps = false + ): Permission { + const pm = this.create(name, org, addNewApps); + + pm.type = [ + { type: PermissionType.OrganizationUserAdmin } as PermissionTypeEntity, + ]; + return pm; + } + + static createGatewayAdmin( + name: string, + org?: Organization, + addNewApps = false + ): Permission { + const pm = this.create(name, org, addNewApps); + + pm.type = [ + { type: PermissionType.OrganizationGatewayAdmin } as PermissionTypeEntity, + ]; + return pm; + } +} diff --git a/src/helpers/security-helper.ts b/src/helpers/security-helper.ts index c31176c0..195f87e1 100644 --- a/src/helpers/security-helper.ts +++ b/src/helpers/security-helper.ts @@ -1,100 +1,78 @@ import { AuthenticatedRequest } from "@entities/dto/internal/authenticated-request"; -import { ForbiddenException } from "@nestjs/common"; +import { Permission } from "@entities/permissions/permission.entity"; +import { PermissionType } from "@enum/permission-type.enum"; +import { ForbiddenException, BadRequestException } from "@nestjs/common"; import * as _ from "lodash"; +import { PermissionTypeEntity } from "@entities/permissions/permission-type.entity"; -export function checkIfUserHasWriteAccessToApplication( - req: AuthenticatedRequest, - applicationId: number -): void { - checkIfGlobalAdminOrInList( - req, - req.user.permissions.getAllApplicationsWithAtLeastWrite(), - applicationId - ); -} - -export function checkIfUserHasReadAccessToApplication( - req: AuthenticatedRequest, - applicationId: number -): void { - checkIfGlobalAdminOrInList( - req, - req.user.permissions.getAllApplicationsWithAtLeastRead(), - applicationId - ); -} - -export function checkIfUserHasReadAccessToOrganization( - req: AuthenticatedRequest, - organizationId: number -): void { - if (organizationId != null) { - checkIfGlobalAdminOrInList( - req, - req.user.permissions.getAllOrganizationsWithAtLeastRead(), - organizationId - ); - } -} - -export function checkIfUserHasWriteAccessToOrganization( - req: AuthenticatedRequest, - organizationId: number -): void { - checkIfGlobalAdminOrInList( - req, - req.user.permissions.getAllOrganizationsWithAtLeastWrite(), - organizationId - ); +export enum OrganizationAccessScope { + ApplicationRead, + ApplicationWrite, + GatewayWrite, + UserAdministrationRead, + UserAdministrationWrite, } -export function checkIfUserHasAdminAccessToOrganization( - req: AuthenticatedRequest, - organizationId: number -): void { - checkIfGlobalAdminOrInList( - req, - req.user.permissions.getAllOrganizationsWithAtLeastAdmin(), - organizationId - ); +export enum ApplicationAccessScope { + Read, + Write, } -// Checks if the user has admin access to ANY of the supplied organizations -export function checkIfUserHasAdminAccessToAnyOrganization( +export function checkIfUserHasAccessToOrganization( req: AuthenticatedRequest, - organisationIds: number[] + organizationId: number, + scope: OrganizationAccessScope ): void { - if (req.user.permissions.isGlobalAdmin) { - return; - } + if (!Number.isInteger(+organizationId)) return; - const userAdminOrganizations = req.user.permissions.getAllOrganizationsWithAtLeastAdmin(); + let allowedOrganizations: number[] = []; - for (const id of organisationIds) { - if (_.includes(userAdminOrganizations, id)) { - return; - } + switch (scope) { + case OrganizationAccessScope.ApplicationRead: + allowedOrganizations = req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead(); + break; + case OrganizationAccessScope.ApplicationWrite: + allowedOrganizations = req.user.permissions.getAllOrganizationsWithApplicationAdmin(); + break; + case OrganizationAccessScope.GatewayWrite: + allowedOrganizations = req.user.permissions.getAllOrganizationsWithGatewayAdmin(); + break; + case OrganizationAccessScope.UserAdministrationRead: + allowedOrganizations = req.user.permissions.getAllOrganizationsWithAtLeastUserAdminRead(); + break; + case OrganizationAccessScope.UserAdministrationWrite: + allowedOrganizations = req.user.permissions.getAllOrganizationsWithUserAdmin(); + break; + default: + // Should never happen + throw new BadRequestException("Bad organization access scope"); } - throw new ForbiddenException(); + checkIfGlobalAdminOrInList(req, allowedOrganizations, organizationId); } -// Checks if the user has admin access to ALL of the supplied organizations -export function checkIfUserHasAdminAccessToAllOrganizations( +export function checkIfUserHasAccessToApplication( req: AuthenticatedRequest, - organisationIds: number[] + applicationId: number, + scope: ApplicationAccessScope ): void { - if (req.user.permissions.isGlobalAdmin) { - return; - } + if (!Number.isInteger(applicationId)) return; - const userAdminOrganizations = req.user.permissions.getAllOrganizationsWithAtLeastAdmin(); + let allowedOrganizations: number[] = []; - for (const id of organisationIds) { - if (!_.includes(userAdminOrganizations, id)) { - throw new ForbiddenException(); - } - } + switch (scope) { + case ApplicationAccessScope.Read: + allowedOrganizations = req.user.permissions.getAllApplicationsWithAtLeastRead(); + break; + case ApplicationAccessScope.Write: + allowedOrganizations = req.user.permissions.getAllApplicationsWithAdmin(); + break; + default: + // Should never happen + throw new BadRequestException("Bad application access scope"); + } + + checkIfGlobalAdminOrInList(req, allowedOrganizations, applicationId); } export function checkIfUserIsGlobalAdmin(req: AuthenticatedRequest): void { @@ -116,3 +94,31 @@ function checkIfGlobalAdminOrInList( throw new ForbiddenException(); } } + +export function isOrganizationPermission(p: Permission): p is Permission { + return [ + PermissionType.OrganizationUserAdmin, + PermissionType.OrganizationApplicationAdmin, + PermissionType.OrganizationGatewayAdmin, + PermissionType.Read, + ].some(orgPermission => p.type.some(({ type }) => type === orgPermission)); +} + +export function isOrganizationApplicationPermission(p: { + type: PermissionTypeEntity[]; +}): p is Permission { + return p.type.some( + ({ type }) => + type === PermissionType.Read || + type === PermissionType.OrganizationApplicationAdmin + ); +} + +export function isPermissionType( + p: { + type: PermissionTypeEntity[]; + }, + targetType: PermissionType +): p is Permission { + return p.type.some(({ type }) => type === targetType); +} diff --git a/src/migration/1652951529000-revised-permissions.ts b/src/migration/1652951529000-revised-permissions.ts new file mode 100644 index 00000000..27759193 --- /dev/null +++ b/src/migration/1652951529000-revised-permissions.ts @@ -0,0 +1,468 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +type AppPermissions = { + applicationId: number; + permissionId: number; +}[]; + +type ApiKeyPermissions = { + apiKeyId: number; + permissionId: number; +}[]; + +type PermissionInfo = { + id: number; + clonedFromId: number; +}; + +type UserPermissionInfo = PermissionInfo & { + userIds?: number[]; +}; + +type AppPermissionInfo = PermissionInfo & { + applicationIds?: number[]; +}; + +type ApiKeyPermissionInfo = PermissionInfo & { + apiKeyIds?: number[]; +} + +type UserPermissions = { + userId: number; + permissionId: number; +}[]; + +/** + * Create a temporary enum which is a union of both old and new enum values + */ +const permissionTypeUnionName = "permission_type_enum_temp"; +const createPermissionTypeUnionSql = `CREATE TYPE "${permissionTypeUnionName}" AS ENUM('OrganizationAdmin', 'Write', 'GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')`; + +export class revisedPermissions1652951529000 implements MigrationInterface { + name = "revisedPermissions1652951529000"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TYPE "public"."permission_type_enum" RENAME TO "permission_type_enum_old"` + ); + await queryRunner.query( + `CREATE TYPE "permission_type_enum" AS ENUM('GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read')` + ); + + // Migrates existing data. This can result in duplicate permissions and duplicates of its dependents + // Must be resolved by a user administrator or above or directly on the database + await this.migrateUp(queryRunner); + await queryRunner.query(`DROP TYPE "permission_type_enum_old"`); + + // Update permission so it can refer to multiple types + await this.migratePermissionTypeUp(queryRunner); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "permission_type_enum_old" AS ENUM('GlobalAdmin', 'OrganizationAdmin', 'Write', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` + ); + // Create a temporary enum which is a union of both old and new enum values + await queryRunner.query(createPermissionTypeUnionSql); + + // Revert permission so each one only has exactly one type (level) + await this.migratePermissionTypeDown(queryRunner); + await queryRunner.query(`COMMENT ON COLUMN "permission"."type" IS NULL`); + + // Migrates existing data. This can result in duplicate permissions and duplicates of its dependents + // ASSUMPTION: this migration is only reverted immediately after executing it. + // There's no 1-1 translation between the three organization admin permissions and the old ones. Ex. OrganizationGatewayAdmin has no equivalent + // To avoid elevating users unexpectedly, for security reasons, we translate them to "Read". + await this.migrateDown(queryRunner); + + await queryRunner.query( + `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_old" USING "type"::"text"::"permission_type_enum_old"` + ); + await queryRunner.query(`DROP TYPE IF EXISTS "permission_type_enum"`); + await queryRunner.query( + `ALTER TYPE "permission_type_enum_old" RENAME TO "permission_type_enum"` + ); + + } + + private async migrateUp(queryRunner: QueryRunner): Promise { + await queryRunner.query(createPermissionTypeUnionSql); + await queryRunner.query( + `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "${permissionTypeUnionName}" USING "type"::"text"::"${permissionTypeUnionName}"` + ); + + // When migrating permisisons tied to old permission types, we need to keep track of the old id. This is for updating any dependents + await queryRunner.query( + `ALTER TABLE "permission" ADD COLUMN "clonedFromId" integer` + ); + + // Begin cloning. Store both the old and new ids as mappings + const applicationAdminFromWriteInfo: PermissionInfo[] = await queryRunner.query( + this.copyPermissionsQuery("Write", "OrganizationApplicationAdmin") + ); + const readFromWriteInfo: PermissionInfo[] = await queryRunner.query( + this.copyPermissionsQuery("Write", "Read") + ); + const userAdminFromOrgAdminInfo: PermissionInfo[] = await queryRunner.query( + this.copyPermissionsQuery("OrganizationAdmin", "OrganizationUserAdmin") + ); + const applicationAdminFromOrgAdminInfo: PermissionInfo[] = await queryRunner.query( + this.copyPermissionsQuery("OrganizationAdmin", "OrganizationApplicationAdmin") + ); + const gatewayAdminFromOrgAdminInfo: PermissionInfo[] = await queryRunner.query( + this.copyPermissionsQuery("OrganizationAdmin", "OrganizationGatewayAdmin") + ); + const readAdminFromOrgAdminInfo: PermissionInfo[] = await queryRunner.query( + this.copyPermissionsQuery("OrganizationAdmin", "Read") + ); + + // Migrate entities in other tables with a foreign key to the permission table + await this.migrateUserPermissions ( + queryRunner, + applicationAdminFromWriteInfo, + readFromWriteInfo, + userAdminFromOrgAdminInfo, + applicationAdminFromOrgAdminInfo, + gatewayAdminFromOrgAdminInfo, + readAdminFromOrgAdminInfo + ); + + await this.migrateApplicationPermissions ( + queryRunner, + applicationAdminFromWriteInfo, + readFromWriteInfo, + userAdminFromOrgAdminInfo, + applicationAdminFromOrgAdminInfo, + gatewayAdminFromOrgAdminInfo, + readAdminFromOrgAdminInfo + ); + + await this.migrateApiKeyPermissions ( + queryRunner, + applicationAdminFromWriteInfo, + readFromWriteInfo, + userAdminFromOrgAdminInfo, + applicationAdminFromOrgAdminInfo, + gatewayAdminFromOrgAdminInfo, + readAdminFromOrgAdminInfo + ); + + // Cleanup + await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "clonedFromId"`); + await this.cleanupPermissionRelations(queryRunner, "'Write', 'OrganizationAdmin'"); + + await queryRunner.query( + `DELETE FROM "public"."permission" where type IN ('OrganizationAdmin', 'Write')` + ); + await queryRunner.query( + `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum" USING "type"::"text"::"permission_type_enum"` + ); + await queryRunner.query(`DROP TYPE "${permissionTypeUnionName}"`); + } + + private async migrateUserPermissions( + queryRunner: QueryRunner, + ...permissionInfos: PermissionInfo[][] + ) { + // User-permission table depends on permission. Fetch and map the user ids + const userPermissions: UserPermissions = await queryRunner.query(`select "userId", + "permissionId" + from public.user_permissions_permission`); + + // .reduce instead of .flatmap as it's only available in ES2019+ + const newUserPermissions: UserPermissionInfo[] = permissionInfos.reduce( + (acc, info) => acc.concat(this.mapUserPermissions(userPermissions, info)), + [] + ); + + // User-permission table + await queryRunner.query(this.copyUserPermissionsQuery(newUserPermissions)); + } + + private async migrateApplicationPermissions( + queryRunner: QueryRunner, + ...permissionInfos: PermissionInfo[][] + ) { + const appPermissions: AppPermissions = await queryRunner.query(`select "applicationId" +,"permissionId" +from public.application_permissions_permission`); + + // .reduce instead of .flatmap as it's only available in ES2019+ + const newAppPermissions: AppPermissionInfo[] = permissionInfos.reduce( + (acc, info) => acc.concat(this.mapAppPermissions(appPermissions, info)), + [] + ); + + // App-permission table + await queryRunner.query(this.copyAppPermissionsQuery(newAppPermissions)); + } + + private async migrateApiKeyPermissions( + queryRunner: QueryRunner, + ...permissionInfos: PermissionInfo[][] + ) { + const apiKeyPermissions: ApiKeyPermissions = await queryRunner.query(`select "apiKeyId" +,"permissionId" +from public.api_key_permissions_permission`); + + // .reduce instead of .flatmap as it's only available in ES2019+ + const newApiKeyPermissions: ApiKeyPermissionInfo[] = permissionInfos.reduce( + (acc, info) => acc.concat(this.mapApiKeyPermissions(apiKeyPermissions, info)), + [] + ); + + // ApiKey-permission table + await queryRunner.query(this.copyApiKeyPermissionsQuery(newApiKeyPermissions)); + } + + private fetchPermissionsIdsQuery(oldType: string, newType = oldType): string { + return `select "createdAt", + "updatedAt", + '${newType}', + name, + "automaticallyAddNewApplications", + "createdById", + "updatedById", + "organizationId", + "id" AS "clonedFromId" + from "public"."permission" + where type = '${oldType}'`; + } + + private copyPermissionsQuery(oldType: string, newType: string): string { + return `INSERT INTO "public"."permission"("createdAt","updatedAt",type,name,"automaticallyAddNewApplications","createdById","updatedById","organizationId","clonedFromId") + ${this.fetchPermissionsIdsQuery(oldType, newType)} +returning id, "permission"."clonedFromId"`; + } + + private mapUserPermissions( + userPermissions: UserPermissions, + infos: UserPermissionInfo[] + ): PermissionInfo[] { + const mappedInfos = infos.map(info => { + const matches = userPermissions.filter(p => p.permissionId === info.clonedFromId); + return matches.length + ? { ...info, userIds: matches.map(x => x.userId) } + : info; + }); + return mappedInfos.filter(info => info.userIds?.length); + } + + private mapAppPermissions( + appPermissions: AppPermissions, + infos: AppPermissionInfo[] + ): PermissionInfo[] { + const mappedInfos = infos.map(info => { + const matches = appPermissions.filter(p => p.permissionId === info.clonedFromId); + return matches.length + ? { ...info, applicationIds: matches.map(x => x.applicationId) } + : info; + }); + return mappedInfos.filter(info => info.applicationIds?.length); + } + + private mapApiKeyPermissions( + apiKeyPermissions: ApiKeyPermissions, + infos: ApiKeyPermissionInfo[] + ): PermissionInfo[] { + const mappedInfos = infos.map(info => { + const matches = apiKeyPermissions.filter(p => p.permissionId === info.clonedFromId); + return matches ? { ...info, apiKeyIds: matches.map(x => x.apiKeyId) } : info; + }); + return mappedInfos.filter(info => info.apiKeyIds?.length); + } + + private copyUserPermissionsQuery(infos: UserPermissionInfo[]): string { + if (!infos.length) return ""; + + const insertIntoStatements = infos + .map(info => info.userIds.map(userId => `(${userId}, ${info.id})`)) + .join(","); + return `INSERT INTO "public"."user_permissions_permission"("userId","permissionId") VALUES + ${insertIntoStatements}`; + } + + private copyAppPermissionsQuery(infos: AppPermissionInfo[]): string { + if (!infos.length) return ""; + + const insertIntoStatements = infos + .map(info => info.applicationIds.map(appId => `(${appId}, ${info.id})`)) + .join(","); + return `INSERT INTO "public"."application_permissions_permission"("applicationId","permissionId") VALUES + ${insertIntoStatements}`; + } + + private copyApiKeyPermissionsQuery(infos: ApiKeyPermissionInfo[]): string { + if (!infos.length) return ""; + + const insertIntoStatements = infos + .map(info => info.apiKeyIds.map(keyId => `(${keyId}, ${info.id})`)) + .join(","); + return `INSERT INTO "public"."api_key_permissions_permission"("apiKeyId","permissionId") VALUES + ${insertIntoStatements}`; + } + + private async migratePermissionTypeUp(queryRunner: QueryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_71bf2818fb2ad92e208d7aeadf"`); + await queryRunner.query(`CREATE TYPE "public"."permission_type_type_enum" AS ENUM('GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read')`); + await queryRunner.query(`CREATE TABLE "permission_type" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "type" "public"."permission_type_type_enum" NOT NULL, "createdById" integer, "updatedById" integer, "permissionId" integer, CONSTRAINT "PK_3f2a17e0bff1bc4e34254b27d78" PRIMARY KEY ("id"))`); + + const fetchAllPermissions = `select "createdAt", + "updatedAt", + -- Casting from one enum to another requires casting it to text first + type::text::"public"."permission_type_type_enum", + "createdById", + "updatedById", + "id" AS "permissionId" + from "public"."permission"`; + + // For each permission, create a corresponding permission type + await queryRunner.query(`INSERT INTO "public"."permission_type"("createdAt","updatedAt",type,"createdById","updatedById","permissionId") + ${fetchAllPermissions}`); + + await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "type"`); + await queryRunner.query(`DROP TYPE "public"."permission_type_enum"`); + await queryRunner.query(`ALTER TABLE "permission_type" ADD CONSTRAINT "FK_abd46fe625f90edc07441bd0bb2" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "permission_type" ADD CONSTRAINT "FK_6ebf76b0f055fe09e42edfe4848" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "permission_type" ADD CONSTRAINT "FK_b8613564bc719a6e37ff0ba243b" FOREIGN KEY ("permissionId") REFERENCES "permission"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "permission_type" ADD CONSTRAINT "UQ_3acd70f5a3895ee2fb92b2a4290" UNIQUE ("type", "permissionId")`); + } + + private async migrateDown(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "${permissionTypeUnionName}" USING "type"::"text"::"${permissionTypeUnionName}"` + ); + + // When migrating permisisons tied to old permission types, we need to keep track of the old id. This is for updating any dependents + await queryRunner.query( + `ALTER TABLE "permission" ADD COLUMN "clonedFromId" integer` + ); + + // Begin cloning. Store both the old and new ids as mappings. The clone must not have access to more than the original. + const readFromUserAdminInfo: PermissionInfo[] = await queryRunner.query( + this.copyPermissionsQuery("OrganizationUserAdmin", "Read") + ); + const readFromGatewayAdminInfo: PermissionInfo[] = await queryRunner.query( + this.copyPermissionsQuery("OrganizationGatewayAdmin", "Read") + ); + // In every scenario, an application admin has write access + const writeFromApplicationAdminInfo: PermissionInfo[] = await queryRunner.query( + this.copyPermissionsQuery("OrganizationApplicationAdmin", "Write") + ); + // Copy to "Read" on the off chance that "Write" isn't treated as a superset of "Read" + const readFromApplicationAdminInfo: PermissionInfo[] = await queryRunner.query( + this.copyPermissionsQuery("OrganizationApplicationAdmin", "Read") + ); + + // Migrate entities in other tables with a foreign key to the permission table + await this.migrateUserPermissions( + queryRunner, + readFromUserAdminInfo, + readFromGatewayAdminInfo, + readFromApplicationAdminInfo, + writeFromApplicationAdminInfo + ); + + await this.migrateApplicationPermissions( + queryRunner, + readFromUserAdminInfo, + readFromGatewayAdminInfo, + readFromApplicationAdminInfo, + writeFromApplicationAdminInfo + ); + + await this.migrateApiKeyPermissions( + queryRunner, + readFromUserAdminInfo, + readFromGatewayAdminInfo, + readFromApplicationAdminInfo, + writeFromApplicationAdminInfo + ); + + // Cleanup + await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "clonedFromId"`); + await this.cleanupPermissionRelations(queryRunner, "'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin'"); + + await queryRunner.query( + `DELETE FROM "public"."permission" where type IN ('OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin')` + ); + await queryRunner.query( + `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_old" USING "type"::"text"::"permission_type_enum_old"` + ); + await queryRunner.query(`DROP TYPE "${permissionTypeUnionName}"`); + } + + private async migratePermissionTypeDown(queryRunner: QueryRunner) { + await queryRunner.query(`ALTER TABLE "permission_type" DROP CONSTRAINT "UQ_3acd70f5a3895ee2fb92b2a4290"`); + await queryRunner.query(`ALTER TABLE "permission_type" DROP CONSTRAINT "FK_b8613564bc719a6e37ff0ba243b"`); + await queryRunner.query(`ALTER TABLE "permission_type" DROP CONSTRAINT "FK_6ebf76b0f055fe09e42edfe4848"`); + await queryRunner.query(`ALTER TABLE "permission_type" DROP CONSTRAINT "FK_abd46fe625f90edc07441bd0bb2"`); + + // Add permission level "type". It's nullable to make the migration possible + await queryRunner.query(`ALTER TABLE "permission" ADD "type" "${permissionTypeUnionName}"`); + + // Temporary table with every permission settable by a client prioritized. Any permission with an unknown type is ignored. + // Unless otherwise specified, the table is automatically dropped after session end. + await queryRunner.query(`CREATE TEMP TABLE "permission_type_priority" ON COMMIT DROP AS + SELECT * FROM ( + VALUES + ('GlobalAdmin'::text, 0), + ('OrganizationUserAdmin'::text, 10), + ('OrganizationApplicationAdmin'::text, 20), + ('OrganizationGatewayAdmin'::text, 30), + ('Read'::text, 40) + ) as t (type, priority)`); + + // Migrate permission levels + await queryRunner.query(`UPDATE + public.permission pm + SET + -- The column is updated for each row with identical "permissionId" + -- Casting from one enum to another requires casting it to text first + type = highestPmType.type::text::${permissionTypeUnionName} + FROM + ( + SELECT pt.* + FROM permission_type_priority ptp + JOIN permission_type pt ON ptp.type::${permissionTypeUnionName} = pt.type::text::${permissionTypeUnionName} + -- Order from lowest to highest priority. The last update for any given permission + -- thus be the type with highest priority (lowest value) + ORDER BY ptp.priority DESC + ) highestPmType + WHERE + pm.id = highestPmType."permissionId"`) + + // Cleanup. If setting "type" to non-nullable fails, then there exists permissions without any level. + // This should not happen. Review them manually. + await queryRunner.query(`ALTER TABLE "permission" ALTER COLUMN "type" SET NOT NULL`); + await queryRunner.query(`DROP TABLE "permission_type"`); + await queryRunner.query(`DROP TYPE "public"."permission_type_type_enum"`); + await queryRunner.query(`CREATE INDEX "IDX_71bf2818fb2ad92e208d7aeadf" ON "permission" ("type") `); + } + + private async cleanupPermissionRelations(queryRunner: QueryRunner, permissionTypesToRemove: string) { + await queryRunner.query(`DELETE FROM user_permissions_permission +WHERE "permissionId" IN +( + SELECT "permission"."id" FROM user_permissions_permission + JOIN permission ON permission.id = "public"."user_permissions_permission"."permissionId" + WHERE permission.type IN (${permissionTypesToRemove}) +);`); + + await queryRunner.query(`DELETE FROM application_permissions_permission +WHERE "permissionId" IN +( + SELECT "permission"."id" FROM application_permissions_permission + JOIN permission ON permission.id = "public"."application_permissions_permission"."permissionId" + WHERE permission.type IN (${permissionTypesToRemove}) +)`); + + await queryRunner.query(`DELETE FROM api_key_permissions_permission +WHERE "permissionId" IN +( + SELECT "permission"."id" FROM api_key_permissions_permission + JOIN permission ON permission.id = "public"."api_key_permissions_permission"."permissionId" + WHERE permission.type IN (${permissionTypesToRemove}) +)`); + } +} diff --git a/src/modules/shared.module.ts b/src/modules/shared.module.ts index fa244f60..cd1ef5c1 100644 --- a/src/modules/shared.module.ts +++ b/src/modules/shared.module.ts @@ -4,36 +4,30 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { Application } from "@entities/application.entity"; import { DataTarget } from "@entities/data-target.entity"; import { GenericHTTPDevice } from "@entities/generic-http-device.entity"; -import { GlobalAdminPermission } from "@entities/permission.entity"; import { HttpPushDataTarget } from "@entities/http-push-data-target.entity"; import { FiwareDataTarget } from "@entities/fiware-data-target.entity"; import { IoTDevicePayloadDecoderDataTargetConnection } from "@entities/iot-device-payload-decoder-data-target-connection.entity"; import { IoTDevice } from "@entities/iot-device.entity"; import { LoRaWANDevice } from "@entities/lorawan-device.entity"; -import { OrganizationAdminPermission } from "@entities/permission.entity"; -import { OrganizationApplicationPermission } from "@entities/permission.entity"; import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/permission.entity"; import { PayloadDecoder } from "@entities/payload-decoder.entity"; -import { Permission } from "@entities/permission.entity"; -import { ReadPermission } from "@entities/permission.entity"; +import { Permission } from "@entities/permissions/permission.entity"; import { ReceivedMessage } from "@entities/received-message.entity"; import { ReceivedMessageMetadata } from "@entities/received-message-metadata.entity"; import { SigFoxDevice } from "@entities/sigfox-device.entity"; import { SigFoxGroup } from "@entities/sigfox-group.entity"; import { User } from "@entities/user.entity"; -import { WritePermission } from "@entities/permission.entity"; import { DeviceModel } from "@entities/device-model.entity"; import { OpenDataDkDataset } from "@entities/open-data-dk-dataset.entity"; import { AuditLog } from "@services/audit-log.service"; import { ApiKey } from "@entities/api-key.entity"; -import { ApiKeyPermission } from "@entities/permission.entity"; import { Multicast } from "@entities/multicast.entity"; import { LorawanMulticastDefinition } from "@entities/lorawan-multicast.entity"; import { ControlledProperty } from "@entities/controlled-property.entity"; import { ApplicationDeviceType } from "@entities/application-device-type.entity"; import { ReceivedMessageSigFoxSignals } from "@entities/received-message-sigfox-signals.entity"; import { MqttDataTarget } from "@entities/mqtt-data-target.entity"; +import { PermissionTypeEntity } from "@entities/permissions/permission-type.entity"; import { GatewayStatusHistory } from "@entities/gateway-status-history.entity"; @Module({ @@ -43,7 +37,6 @@ import { GatewayStatusHistory } from "@entities/gateway-status-history.entity"; Application, DataTarget, GenericHTTPDevice, - GlobalAdminPermission, HttpPushDataTarget, FiwareDataTarget, MqttDataTarget, @@ -53,24 +46,20 @@ import { GatewayStatusHistory } from "@entities/gateway-status-history.entity"; LoRaWANDevice, OpenDataDkDataset, Organization, - OrganizationAdminPermission, - OrganizationApplicationPermission, - OrganizationPermission, PayloadDecoder, Permission, - ReadPermission, ReceivedMessage, ReceivedMessageMetadata, SigFoxDevice, SigFoxGroup, - WritePermission, - ApiKeyPermission, + User, Multicast, LorawanMulticastDefinition, ControlledProperty, ApplicationDeviceType, ApiKey, ReceivedMessageSigFoxSignals, + PermissionTypeEntity, GatewayStatusHistory, ]), ], diff --git a/src/services/api-key-management/api-key.service.ts b/src/services/api-key-management/api-key.service.ts index 1fd55e8f..1d437c6b 100644 --- a/src/services/api-key-management/api-key.service.ts +++ b/src/services/api-key-management/api-key.service.ts @@ -4,7 +4,6 @@ import { CreateApiKeyDto } from "@dto/api-key/create-api-key.dto"; import { ListAllApiKeysResponseDto } from "@dto/api-key/list-all-api-keys-response.dto"; import { ListAllApiKeysDto } from "@dto/api-key/list-all-api-keys.dto"; import { DeleteResponseDto } from "@dto/delete-application-response.dto"; -import { ApiKeyPermission } from "@entities/permission.entity"; import { ApiKey } from "@entities/api-key.entity"; import { forwardRef, Inject, Injectable, Logger } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; @@ -102,7 +101,7 @@ export class ApiKeyService { ); apiKey.permissions = permissionsDb.map( - pm => ({ ...pm, apiKeys: null } as ApiKeyPermission) + pm => ({ ...pm, apiKeys: null }) ); } diff --git a/src/services/chirpstack/chirpstack-gateway.service.ts b/src/services/chirpstack/chirpstack-gateway.service.ts index b1be303e..d4948327 100644 --- a/src/services/chirpstack/chirpstack-gateway.service.ts +++ b/src/services/chirpstack/chirpstack-gateway.service.ts @@ -24,7 +24,7 @@ import { ChirpstackSetupNetworkServerService } from "@services/chirpstack/networ import { GatewayContentsDto } from "@dto/chirpstack/gateway-contents.dto"; import * as _ from "lodash"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; -import { checkIfUserHasWriteAccessToOrganization } from "@helpers/security-helper"; +import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; import { GatewayResponseDto } from "@dto/chirpstack/gateway-response.dto"; @Injectable() @@ -203,8 +203,9 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ const existing = await this.getOne(gatewayId); const tags = dto.gateway.tags; tags[this.ORG_ID_KEY] = `${existing.gateway.internalOrganizationId}`; + // TODO: Interpolated string will never be null? if (tags[this.ORG_ID_KEY] != null) { - checkIfUserHasWriteAccessToOrganization(req, +tags[this.ORG_ID_KEY]); + checkIfUserHasAccessToOrganization(req, +tags[this.ORG_ID_KEY], OrganizationAccessScope.GatewayWrite); } return tags; } diff --git a/src/services/chirpstack/device-profile.service.ts b/src/services/chirpstack/device-profile.service.ts index 191c1ab4..07306e15 100644 --- a/src/services/chirpstack/device-profile.service.ts +++ b/src/services/chirpstack/device-profile.service.ts @@ -7,9 +7,9 @@ import { ListAllDeviceProfilesResponseDto } from "@dto/chirpstack/list-all-devic import { GenericChirpstackConfigurationService } from "./generic-chirpstack-configuration.service"; import { UpdateDeviceProfileDto } from "@dto/chirpstack/update-device-profile.dto"; import { DeviceProfileDto } from "@dto/chirpstack/device-profile.dto"; -import { checkIfUserHasWriteAccessToOrganization } from "@helpers/security-helper"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { ErrorCodes } from "@enum/error-codes.enum"; +import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; @Injectable() export class DeviceProfileService extends GenericChirpstackConfigurationService { @@ -80,7 +80,7 @@ export class DeviceProfileService extends GenericChirpstackConfigurationService const tags = result.deviceProfile.tags; tags[this.UPDATED_BY_KEY] = `${req.user.userId}`; if (tags[this.ORG_ID_KEY] != null) { - checkIfUserHasWriteAccessToOrganization(req, +tags[this.ORG_ID_KEY]); + checkIfUserHasAccessToOrganization(req, +tags[this.ORG_ID_KEY], OrganizationAccessScope.ApplicationWrite); } return tags; } @@ -94,9 +94,10 @@ export class DeviceProfileService extends GenericChirpstackConfigurationService id ); if (result.deviceProfile.tags[this.ORG_ID_KEY] != null) { - checkIfUserHasWriteAccessToOrganization( + checkIfUserHasAccessToOrganization( req, - +result.deviceProfile.tags[this.ORG_ID_KEY] + +result.deviceProfile.tags[this.ORG_ID_KEY], + OrganizationAccessScope.ApplicationWrite ); } return await this.delete("device-profiles", id); diff --git a/src/services/device-management/iot-device.service.ts b/src/services/device-management/iot-device.service.ts index 324aa7e9..caea420f 100644 --- a/src/services/device-management/iot-device.service.ts +++ b/src/services/device-management/iot-device.service.ts @@ -212,7 +212,7 @@ export class IoTDeviceService { req: AuthenticatedRequest ): Promise { const applications = req.user.permissions.getAllApplicationsWithAtLeastRead(); - const organizations = req.user.permissions.getAllOrganizationsWithAtLeastAdmin(); + const organizations = req.user.permissions.getAllOrganizationsWithApplicationAdmin(); return (await data).map(x => { return { id: x.id, @@ -961,7 +961,6 @@ export class IoTDeviceService { dto.name ); - // Save application const applicationId = await this.chirpstackDeviceService.findOrCreateDefaultApplication( chirpstackDeviceDto, loraApplications diff --git a/src/services/user-management/organization.service.ts b/src/services/user-management/organization.service.ts index 2e5b127b..f1f53d95 100644 --- a/src/services/user-management/organization.service.ts +++ b/src/services/user-management/organization.service.ts @@ -23,7 +23,7 @@ import { ErrorCodes } from "@enum/error-codes.enum"; import { PermissionService } from "./permission.service"; import { User } from "@entities/user.entity"; import { UserService } from "./user.service"; -import { Permission, OrganizationPermission } from "@entities/permission.entity"; +import { Permission } from "@entities/permissions/permission.entity"; @Injectable() export class OrganizationService { @@ -100,9 +100,9 @@ export class OrganizationService { }; } - async mapPermissionsToOrganizations( - permissions: OrganizationPermission[] - ): Promise { + mapPermissionsToOrganizations( + permissions: Permission[] + ): Organization[] { const requestedOrganizations: Organization[] = []; for (let index = 0; index < permissions.length; index++) { @@ -131,9 +131,9 @@ export class OrganizationService { return requestedOrganizations; } - async mapPermissionsToOneOrganization( - permissions: OrganizationPermission[] - ): Promise { + mapPermissionsToOneOrganization( + permissions: Permission[] + ): Organization { const org: Organization = new Organization(); permissions.map(permission => { diff --git a/src/services/user-management/permission.service.ts b/src/services/user-management/permission.service.ts index 401c3072..1c88aa1f 100644 --- a/src/services/user-management/permission.service.ts +++ b/src/services/user-management/permission.service.ts @@ -15,15 +15,9 @@ import { PermissionMinimalDto } from "@dto/permission-minimal.dto"; import { UserPermissions } from "@dto/permission-organization-application.dto"; import { CreatePermissionDto } from "@dto/user-management/create-permission.dto"; import { UpdatePermissionDto } from "@dto/user-management/update-permission.dto"; -import { GlobalAdminPermission } from "@entities/permission.entity"; -import { OrganizationAdminPermission } from "@entities/permission.entity"; -import { OrganizationApplicationPermission } from "@entities/permission.entity"; import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/permission.entity"; -import { Permission } from "@entities/permission.entity"; -import { ReadPermission } from "@entities/permission.entity"; +import { Permission } from "@entities/permissions/permission.entity"; import { User } from "@entities/user.entity"; -import { WritePermission } from "@entities/permission.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { ApplicationService } from "@services/device-management/application.service"; import { OrganizationService } from "@services/user-management/organization.service"; @@ -34,6 +28,10 @@ import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; import { ListAllPermissionsDto } from "@dto/list-all-permissions.dto"; +import { isOrganizationApplicationPermission } from "@helpers/security-helper"; +import { PermissionTypeEntity } from "@entities/permissions/permission-type.entity"; +import { PermissionCreator } from "@helpers/permission.helper"; +import { nameof } from "@helpers/type-helper"; @Injectable() export class PermissionService { @@ -48,86 +46,108 @@ export class PermissionService { private applicationService: ApplicationService ) {} - READ_SUFFIX = " - Read"; - WRITE_SUFFIX = " - Write"; - ADMIN_SUFFIX = " - OrganizationAdmin"; - async createDefaultPermissions( org: Organization, userId: number - ): Promise { - const readPermission = new ReadPermission(org.name + this.READ_SUFFIX, org, true); - const writePermission = new WritePermission( - org.name + this.WRITE_SUFFIX, + ): Promise { + const { readPermission, orgApplicationAdminPermission, orgAdminPermission, orgGatewayAadminPermission } = this.instantiateDefaultPermissions(org, userId); + + // Use the manager since otherwise, we'd need a repository for each of them + const entityManager = getManager(); + const r = await entityManager.save([ + readPermission, + orgApplicationAdminPermission, + orgAdminPermission, + orgGatewayAadminPermission, + ]); + r.forEach(val => + AuditLog.success(ActionType.CREATE, Permission.name, userId, val.id, val.name) + ); + return r; + } + + private instantiateDefaultPermissions(org: Organization, userId: number) { + const nameSuffixSeparator = " - "; + const readSuffix = `${nameSuffixSeparator}${PermissionType.Read}`; + const organizationUserAdminSuffix = `${nameSuffixSeparator}${PermissionType.OrganizationUserAdmin}`; + const organizationGatewayAdminSuffix = `${nameSuffixSeparator}${PermissionType.OrganizationGatewayAdmin}`; + const organizationApplicationAdminSuffix = `${nameSuffixSeparator}${PermissionType.OrganizationApplicationAdmin}`; + + const readPermission = PermissionCreator.createRead(org.name + readSuffix, org, true); + const orgApplicationAdminPermission = PermissionCreator.createApplicationAdmin( + org.name + organizationApplicationAdminSuffix, org, true ); - const adminPermission = new OrganizationAdminPermission( - org.name + this.ADMIN_SUFFIX, + const orgAdminPermission = PermissionCreator.createUserAdmin( + org.name + organizationUserAdminSuffix, + org + ); + const orgGatewayAadminPermission = PermissionCreator.createGatewayAdmin( + org.name + organizationGatewayAdminSuffix, org ); + this.setUserIdOnPermissions(readPermission, userId); + this.setUserIdOnPermissions(orgApplicationAdminPermission, userId); + this.setUserIdOnPermissions(orgAdminPermission, userId); + this.setUserIdOnPermissions(orgGatewayAadminPermission, userId); + readPermission.createdBy = userId; readPermission.updatedBy = userId; - writePermission.createdBy = userId; - writePermission.updatedBy = userId; - adminPermission.createdBy = userId; - adminPermission.updatedBy = userId; + orgApplicationAdminPermission.createdBy = userId; + orgApplicationAdminPermission.updatedBy = userId; + orgAdminPermission.createdBy = userId; + orgAdminPermission.updatedBy = userId; + orgGatewayAadminPermission.createdBy = userId; + orgGatewayAadminPermission.updatedBy = userId; + return { readPermission, orgApplicationAdminPermission, orgAdminPermission, orgGatewayAadminPermission }; + } - // Use the manager since otherwise, we'd need a repository for each of them - const entityManager = getManager(); - const r = await entityManager.save([ - adminPermission, - writePermission, - readPermission, - ]); - AuditLog.success(ActionType.CREATE, Permission.name, userId, r[0].id, r[0].name); - AuditLog.success(ActionType.CREATE, Permission.name, userId, r[1].id, r[1].name); - AuditLog.success(ActionType.CREATE, Permission.name, userId, r[2].id, r[2].name); - return r; + private setUserIdOnPermissions(permission: Permission, userId: number) { + permission.type.forEach(type => { + type.createdBy = userId; + type.updatedBy = userId; + }); } - async findOrCreateGlobalAdminPermission(): Promise { - const globalAdmin = await getManager().findOne(GlobalAdminPermission); + async findOrCreateGlobalAdminPermission(): Promise { + // Use query builder since the other syntax doesn't support one-to-many for property querying + const globalAdmin = await this.permissionRepository + .createQueryBuilder("permission") + .where( + " type.type = :permType", + { + permType: PermissionType.GlobalAdmin, + } + ) + .leftJoin("permission.type", "type") + .getOne(); + if (globalAdmin) { return globalAdmin; } - return await getManager().save(new GlobalAdminPermission()); + return await getManager().save(PermissionCreator.createGlobalAdmin()); } async createNewPermission( dto: CreatePermissionDto, userId: number ): Promise { - let permission; const org: Organization = await this.organizationService.findById( dto.organizationId ); - switch (dto.level) { - case PermissionType.OrganizationAdmin: { - permission = new OrganizationAdminPermission(dto.name, org); - break; - } - case PermissionType.Write: { - permission = new WritePermission( - dto.name, - org, - dto.automaticallyAddNewApplications - ); - break; - } - case PermissionType.Read: { - permission = new ReadPermission( - dto.name, - org, - dto.automaticallyAddNewApplications - ); - break; - } - default: - throw new BadRequestException("Bad PermissionLevel"); - } + const permission = PermissionCreator.createByTypes( + dto.name, + dto.levels.map(level => level.type), + org, + dto.automaticallyAddNewApplications + ); + permission.type.forEach(type => { + type.createdBy = userId; + type.updatedBy = userId; + }); await this.mapToPermission(permission, dto); permission.createdBy = userId; @@ -137,18 +157,21 @@ export class PermissionService { } async autoAddPermissionsToApplication(app: Application): Promise { - const permissionsInOrganisation = await getManager().find( - OrganizationApplicationPermission, - { - where: { - organization: { - id: app.belongsTo.id, - }, - automaticallyAddNewApplications: true, - }, - relations: ["applications"], - } - ); + // Use query builder since the other syntax doesn't support one-to-many for property querying + const permissionsInOrganisation = await this.permissionRepository + .createQueryBuilder("permission") + .where( + "permission.organization.id = :orgId" + + " AND type.type IN (:...permType)" + + ` AND "${nameof('automaticallyAddNewApplications')}" = True`, + { + orgId: app.belongsTo.id, + permType: [PermissionType.OrganizationApplicationAdmin, PermissionType.Read], + } + ) + .leftJoinAndSelect("permission.applications", "app") + .leftJoin("permission.type", "type") + .getMany(); await Promise.all( permissionsInOrganisation.map(async p => { @@ -167,24 +190,24 @@ export class PermissionService { user.permissions = user.permissions.filter(x => x.id != permission.id); } - async findManyWithRelations(organizationIds: number[]): Promise + async findManyWithRelations(organizationIds: number[]): Promise { const perm = await this.permissionRepository.find({ - relations: ["organization", "users"], + relations: ["organization", "users", "type"], where: {organization: {id: In(organizationIds)}} - }); + }); - return perm as OrganizationPermission[]; + return perm; } - async findOneWithRelations(organizationId: number): Promise + async findOneWithRelations(organizationId: number): Promise { const perm = await this.permissionRepository.find({ - relations: ["organization", "users"], + relations: ["organization", "users", "type"], where: {organization: {id: organizationId}} - }); + }); - return perm as OrganizationPermission[]; + return perm; } async updatePermission( @@ -194,7 +217,7 @@ export class PermissionService { ): Promise { const permission = await getManager().findOne(Permission, { where: { id: id }, - relations: ["organization", "users", "applications"], + relations: ["organization", "users", "applications", "type"], }); permission.name = dto.name; @@ -211,15 +234,12 @@ export class PermissionService { permission: Permission, dto: UpdatePermissionDto ): Promise { - if ( - permission.type == PermissionType.Read || - permission.type == PermissionType.Write - ) { - (permission as OrganizationApplicationPermission).applications = await this.applicationService.findManyByIds( + if (isOrganizationApplicationPermission(permission)) { + permission.applications = await this.applicationService.findManyByIds( dto.applicationIds ); - (permission as OrganizationApplicationPermission).automaticallyAddNewApplications = + permission.automaticallyAddNewApplications = dto.automaticallyAddNewApplications; } if (dto?.userIds?.length >= 0) { @@ -245,6 +265,7 @@ export class PermissionService { ) .leftJoinAndSelect("permission.organization", "org") .leftJoinAndSelect("permission.users", "user") + .leftJoinAndSelect("permission.type", "permission_type") .take(query?.limit ? +query.limit : 100) .skip(query?.offset ? +query.offset : 0) .orderBy(orderBy, order); @@ -293,18 +314,25 @@ export class PermissionService { async getPermission(id: number): Promise { return await getManager().findOneOrFail(Permission, { where: { id: id }, - relations: ["organization", "users", "applications"], + relations: ["organization", "users", "applications", "type"], loadRelationIds: { relations: ["createdBy", "updatedBy"], }, }); } - async getGlobalPermission(): Promise { - return await getManager().findOneOrFail(Permission, { - where: { type: PermissionType.GlobalAdmin }, - relations: ["users"], - }); + getGlobalPermission(): Promise { + return this.permissionRepository + .createQueryBuilder("permission") + .where( + " type.type = :permType", + { + permType: PermissionType.GlobalAdmin, + } + ) + .leftJoin("permission.type", "type") + .leftJoinAndSelect("permission.users", "users") + .getOneOrFail(); } buildPermissionsQuery(): SelectQueryBuilder { @@ -320,8 +348,13 @@ export class PermissionService { "application", '"application"."id"="application_permission"."applicationId" ' ) + .leftJoinAndSelect( + "permission_type", + "permission_type", + '"permission_type"."permissionId"="permission"."id"' + ) .select([ - "permission.type as permission_type", + "permission_type.type as permission_type_type", "permission.organization as organization_id", "application.id as application_id", ]); @@ -346,8 +379,8 @@ export class PermissionService { ): Promise { return await this.buildPermissionsWithApplicationsQuery() .leftJoin("permission.users", "user") - .where("permission.type = :permType AND user.id = :id", { - permType: PermissionType.OrganizationAdmin, + .where("permission_type.type = :permType AND user.id = :id", { + permType: PermissionType.OrganizationApplicationAdmin, id: userId, }) .getRawMany(); @@ -358,8 +391,9 @@ export class PermissionService { .createQueryBuilder("permission") .leftJoinAndSelect("permission.organization", "organization") .leftJoinAndSelect("organization.applications", "application") + .leftJoinAndSelect("permission.type", "permission_type") .select([ - "permission.type as permission_type", + "permission_type.type as permission_type_type", "permission.organization as organization_id", "application.id as application_id", ]); @@ -371,7 +405,7 @@ export class PermissionService { return await this.buildPermissionsWithApplicationsQuery() .leftJoin("permission.apiKeys", "apiKey") .where("permission.type = :permType AND apiKey.id = :id", { - permType: PermissionType.OrganizationAdmin, + permType: PermissionType.OrganizationUserAdmin, id: apiKeyId, }) .getRawMany(); @@ -379,7 +413,7 @@ export class PermissionService { async findPermissionGroupedByLevelForUser(userId: number): Promise { let permissions = await this.findPermissionsForUser(userId); - if (this.isOrganizationAdmin(permissions)) { + if (this.isOrganizationApplicationAdmin(permissions)) { // For organization admins, we need to fetch all applications they have permissions to const permissionsForOrgAdmin = await this.findPermissionsForOrgAdminWithApplications( userId @@ -394,7 +428,7 @@ export class PermissionService { apiKeyId: number ): Promise { let permissions = await this.findPermissionsForApiKey(apiKeyId); - if (this.isOrganizationAdmin(permissions)) { + if (this.isOrganizationUserAdmin(permissions)) { // For organization admins, we need to fetch all applications they have permissions to const permissionsForOrgAdmin = await this.findPermissionsForApiKeyOrgAdminWithApplications( apiKeyId @@ -410,16 +444,16 @@ export class PermissionService { const res = new UserPermissions(); permissions.forEach(p => { - if (p.permission_type == PermissionType.GlobalAdmin) { + if (p.permission_type_type == PermissionType.GlobalAdmin) { res.isGlobalAdmin = true; - } else if (p.permission_type == PermissionType.OrganizationAdmin) { - res.organizationAdminPermissions.add(p.organization_id); - // Also grant writePermission to the application - this.addOrUpdate(res.writePermissions, p); - } else if (p.permission_type == PermissionType.Write) { - this.addOrUpdate(res.writePermissions, p); - } else if (p.permission_type == PermissionType.Read) { - this.addOrUpdate(res.readPermissions, p); + } else if (p.permission_type_type == PermissionType.OrganizationApplicationAdmin) { + this.addOrUpdateApplicationIds(res.orgToApplicationAdminPermissions, p); + } else if (p.permission_type_type == PermissionType.OrganizationGatewayAdmin) { + res.orgToGatewayAdminPermissions.add(p.organization_id); + } else if (p.permission_type_type == PermissionType.OrganizationUserAdmin) { + res.orgToUserAdminPermissions.add(p.organization_id); + } else if (p.permission_type_type == PermissionType.Read) { + this.addOrUpdateApplicationIds(res.orgToReadPermissions, p); } }); @@ -430,13 +464,19 @@ export class PermissionService { return await this.permissionRepository.findByIds(ids); } - private isOrganizationAdmin(permissions: PermissionMinimalDto[]) { + private isOrganizationApplicationAdmin(permissions: PermissionMinimalDto[]) { + return permissions.some( + x => x.permission_type_type == PermissionType.OrganizationApplicationAdmin + ); + } + + private isOrganizationUserAdmin(permissions: PermissionMinimalDto[]) { return permissions.some( - x => x.permission_type == PermissionType.OrganizationAdmin + x => x.permission_type_type == PermissionType.OrganizationUserAdmin ); } - private addOrUpdate(permissions: Map, p: PermissionMinimalDto) { + private addOrUpdateApplicationIds(permissions: Map, p: PermissionMinimalDto) { if (!permissions.has(p.organization_id)) { permissions.set(p.organization_id, []); } diff --git a/src/services/user-management/user.service.ts b/src/services/user-management/user.service.ts index 3503199c..7237f4f5 100644 --- a/src/services/user-management/user.service.ts +++ b/src/services/user-management/user.service.ts @@ -12,7 +12,7 @@ import { Repository } from "typeorm"; import { CreateUserDto } from "@dto/user-management/create-user.dto"; import { UpdateUserDto } from "@dto/user-management/update-user.dto"; import { UserResponseDto } from "@dto/user-response.dto"; -import { Permission } from "@entities/permission.entity"; +import { Permission } from "@entities/permissions/permission.entity"; import { User } from "@entities/user.entity"; import { ErrorCodes } from "@enum/error-codes.enum"; @@ -27,6 +27,7 @@ import { Organization } from "@entities/organization.entity"; import SMTPTransport from "nodemailer/lib/smtp-transport"; import { PermissionType } from "@enum/permission-type.enum"; import { ConfigService } from "@nestjs/config"; +import { isPermissionType } from "@helpers/security-helper"; @Injectable() export class UserService { @@ -51,18 +52,22 @@ export class UserService { async acceptUser( user: User, org: Organization, - dbPermission: Permission + newUserPermissions: Permission[] ): Promise { user.awaitingConfirmation = false; - if (user.permissions.find(perms => perms.id === dbPermission.id)) { + if ( + user.permissions.find(perms => + newUserPermissions.some(newPerm => newPerm.id === perms.id) + ) + ) { throw new BadRequestException(ErrorCodes.UserAlreadyInPermission); } else { const index = user.requestedOrganizations.findIndex( dbOrg => dbOrg.id === org.id ); user.requestedOrganizations.splice(index, 1); - user.permissions.push(dbPermission); + user.permissions.push(...newUserPermissions); await this.sendVerificationMail(user, org); return await this.userRepository.save(user); } @@ -117,7 +122,7 @@ export class UserService { async findOneWithOrganizations(id: number): Promise { return await this.userRepository.findOne(id, { - relations: ["permissions", "permissions.organization"], + relations: ["permissions", "permissions.organization", "permissions.type"], }); } @@ -294,7 +299,7 @@ export class UserService { } const [data, count] = await this.userRepository.findAndCount({ - relations: ["permissions"], + relations: ["permissions", "permissions.type"], take: +query.limit, skip: +query.offset, order: sorting, @@ -415,7 +420,7 @@ export class UserService { const emails: string[] = []; const globalAdminPermission: Permission = await this.permissionService.getGlobalPermission(); organization.permissions.forEach(permission => { - if (permission.type === PermissionType.OrganizationAdmin) { + if (isPermissionType(permission, PermissionType.OrganizationUserAdmin)) { if (permission.users.length > 0) { permission.users.forEach(user => { emails.push(user.email); diff --git a/test/e2e/chirpstack/chirpstack-gateway-configuration.e2e-spec.ts b/test/e2e/chirpstack/chirpstack-gateway-configuration.e2e-spec.ts index eec9bbb6..f9399d3a 100644 --- a/test/e2e/chirpstack/chirpstack-gateway-configuration.e2e-spec.ts +++ b/test/e2e/chirpstack/chirpstack-gateway-configuration.e2e-spec.ts @@ -81,7 +81,7 @@ describe("ChirpstackGatewayController (e2e)", () => { beforeEach(async () => { await clearDatabase(); // Delete all gateways created in E2E tests: - const existing = await service.getAll(1000, 0); + const existing = await service.getAll(1000); existing.result.forEach(async element => { if (element.name.startsWith(gatewayNamePrefix)) { // Logger.debug(`Found ${element.name}, deleting.`); diff --git a/test/e2e/crud/application.e2e-spec.ts b/test/e2e/crud/application.e2e-spec.ts index 9ad984cd..8e0f7261 100644 --- a/test/e2e/crud/application.e2e-spec.ts +++ b/test/e2e/crud/application.e2e-spec.ts @@ -24,7 +24,7 @@ import { randomMacAddress, } from "../test-helpers"; import { PermissionType } from "@enum/permission-type.enum"; -import { ReadPermission } from "@entities/read-permission.entity"; +import { ReadPermission } from "@entities/permissions/read-permission.entity"; import { IoTDeviceService } from "@services/device-management/iot-device.service"; import { IoTDeviceType } from "@enum/device-type.enum"; import { ActivationType } from "@enum/lorawan-activation-type.enum"; @@ -32,9 +32,9 @@ import { DeviceProfileService } from "@services/chirpstack/device-profile.servic import { ServiceProfileService } from "@services/chirpstack/service-profile.service"; import { ChirpstackAdministrationModule } from "@modules/device-integrations/chirpstack-administration.module"; import { IoTDeviceModule } from "@modules/device-management/iot-device.module"; -import { WritePermission } from "@entities/write-permission.entity"; import { User } from "@entities/user.entity"; import { AuditLog } from "@services/audit-log.service"; +import { OrganizationApplicationAdminPermission } from "@entities/permissions/organization-application-admin-permission.entity"; describe("ApplicationController (e2e)", () => { let app: INestApplication; @@ -553,14 +553,14 @@ describe("ApplicationController (e2e)", () => { const user = await generateSavedReadWriteUser(org); const readPerm = user.permissions.find( - x => x.type == PermissionType.Read + x => x.type.some(({ type }) => type === PermissionType.Read) ) as ReadPermission; readPerm.applications = [app1]; - const writePerm = user.permissions.find( - x => x.type == PermissionType.Write - ) as ReadPermission; - writePerm.applications = [app1]; - await getManager().save([readPerm, writePerm]); + const orgAppAdminPerm = user.permissions.find( + x => x.type.some(({ type }) => type === PermissionType.OrganizationApplicationAdmin) + ) as OrganizationApplicationAdminPermission; + orgAppAdminPerm.applications = [app1]; + await getManager().save([readPerm, orgAppAdminPerm]); const jwt = generateValidJwtForUser(user); @@ -617,14 +617,14 @@ describe("ApplicationController (e2e)", () => { const org = await generateSavedOrganization(); const readPerm = org.permissions.find( - x => x.type == PermissionType.Read + x => x.type.some(({ type }) => type === PermissionType.Read) ) as ReadPermission; expect(readPerm.automaticallyAddNewApplications).toBeTruthy(); - const writePerm = org.permissions.find( - x => x.type == PermissionType.Write - ) as WritePermission; - expect(writePerm.automaticallyAddNewApplications).toBeTruthy(); + const orgAppAdminPerm = org.permissions.find( + x => x.type.some(({ type }) => type === PermissionType.OrganizationApplicationAdmin) + ) as OrganizationApplicationAdminPermission; + expect(orgAppAdminPerm.automaticallyAddNewApplications).toBeTruthy(); const testAppOne: CreateApplicationDto = { name: "AutoAdd", diff --git a/test/e2e/crud/data-target.e2e-spec.ts b/test/e2e/crud/data-target.e2e-spec.ts index 8b30448a..5e57e8a0 100644 --- a/test/e2e/crud/data-target.e2e-spec.ts +++ b/test/e2e/crud/data-target.e2e-spec.ts @@ -245,6 +245,8 @@ describe("DataTargetController (e2e)", () => { url: "http://example.com/test-endepunkt", timeout: 3000, authorizationHeader: null, + tenant: '', + context: '', }; await request(app.getHttpServer()) @@ -338,6 +340,8 @@ describe("DataTargetController (e2e)", () => { url: "http://example.com/test-endepunkt", timeout: 3000, authorizationHeader: null, + tenant: '', + context: '', }; await request(app.getHttpServer()) @@ -378,6 +382,8 @@ describe("DataTargetController (e2e)", () => { authorEmail: "e2e@test.dk", resourceTitle: "Rumsensor2", }, + tenant: '', + context: '', }; await request(app.getHttpServer()) @@ -421,6 +427,8 @@ describe("DataTargetController (e2e)", () => { authorEmail: "e2e@test.dk", resourceTitle: "Rumsensor2", }, + tenant: '', + context: '', }; await request(app.getHttpServer()) diff --git a/test/e2e/crud/organization.e2e-spec.ts b/test/e2e/crud/organization.e2e-spec.ts index cced0ef0..b9a8f9c1 100644 --- a/test/e2e/crud/organization.e2e-spec.ts +++ b/test/e2e/crud/organization.e2e-spec.ts @@ -198,21 +198,21 @@ describe("OrganizationController (e2e)", () => { updatedAt: expect.any(String), automaticallyAddNewApplications: true, }); - expect(response.body.permissions).toContainEqual({ - id: expect.any(Number), - name: `${org.name} - Write`, - type: PermissionType.Write, - createdAt: expect.any(String), - updatedAt: expect.any(String), - automaticallyAddNewApplications: true, - }); - expect(response.body.permissions).toContainEqual({ - id: expect.any(Number), - name: `${org.name} - OrganizationAdmin`, - type: PermissionType.OrganizationAdmin, - createdAt: expect.any(String), - updatedAt: expect.any(String), - }); + expectOrgPermission( + response.body.permissions, + `${org.name} - OrganizationAppAdmin`, + PermissionType.OrganizationApplicationAdmin + ); + expectOrgPermission( + response.body.permissions, + `${org.name} - OrganizationGatewayAdmin`, + PermissionType.OrganizationGatewayAdmin + ); + expectOrgPermission( + response.body.permissions, + `${org.name} - OrganizationUserAdmin`, + PermissionType.OrganizationUserAdmin + ); }); }); @@ -349,3 +349,14 @@ describe("OrganizationController (e2e)", () => { .expect("Content-Type", /json/); }); }); + +function expectOrgPermission(permissions: unknown, name: string, type: PermissionType) { + expect(permissions).toContainEqual({ + id: expect.any(Number), + name, + type, + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); +} + diff --git a/test/e2e/crud/permission.e2e-spec.ts b/test/e2e/crud/permission.e2e-spec.ts index 82f635c1..1ef45037 100644 --- a/test/e2e/crud/permission.e2e-spec.ts +++ b/test/e2e/crud/permission.e2e-spec.ts @@ -9,8 +9,6 @@ import { getManager } from "typeorm"; import configuration from "@config/configuration"; import { CreatePermissionDto } from "@dto/user-management/create-permission.dto"; import { UpdatePermissionDto } from "@dto/user-management/update-permission.dto"; -import { OrganizationApplicationPermission } from "@entities/organization-application-permission.entity"; -import { ReadPermission } from "@entities/read-permission.entity"; import { User } from "@entities/user.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { AuthModule } from "@modules/user-management/auth.module"; @@ -116,12 +114,16 @@ describe("PermissionController (e2e)", () => { type: PermissionType.GlobalAdmin, }), expect.objectContaining({ - name: org.name + " - OrganizationAdmin", - type: PermissionType.OrganizationAdmin, + name: org.name + " - OrganizationAppAdmin", + type: PermissionType.OrganizationApplicationAdmin, }), expect.objectContaining({ - name: org.name + " - Write", - type: PermissionType.Write, + name: org.name + " - OrganizationGatewayAdmin", + type: PermissionType.OrganizationGatewayAdmin, + }), + expect.objectContaining({ + name: org.name + " - OrganizationUserAdmin", + type: PermissionType.OrganizationUserAdmin, automaticallyAddNewApplications: true, }), expect.objectContaining({ @@ -158,7 +160,7 @@ describe("PermissionController (e2e)", () => { const org = await generateSavedOrganization("E2E"); const dto: CreatePermissionDto = { - level: PermissionType.Read, + levels: PermissionType.Read, name: "E2E readers", organizationId: org.id, userIds: [], diff --git a/test/e2e/crud/search.e2e-spec.ts b/test/e2e/crud/search.e2e-spec.ts index 9b442d3e..37383c88 100644 --- a/test/e2e/crud/search.e2e-spec.ts +++ b/test/e2e/crud/search.e2e-spec.ts @@ -30,12 +30,12 @@ import { Application } from "@entities/application.entity"; import { GenericHTTPDevice } from "@entities/generic-http-device.entity"; import { LoRaWANDevice } from "@entities/lorawan-device.entity"; import { SigFoxDevice } from "@entities/sigfox-device.entity"; -import { WritePermission } from "@entities/write-permission.entity"; import { ChirpstackGatewayService } from "@services/chirpstack/chirpstack-gateway.service"; import { CreateGatewayDto } from "@dto/chirpstack/create-gateway.dto"; import { ChirpstackSetupNetworkServerService } from "@services/chirpstack/network-server.service"; import { ChirpstackAdministrationModule } from "@modules/device-integrations/chirpstack-administration.module"; import { User } from "@entities/user.entity"; +import { OrganizationApplicationAdminPermission } from "@entities/permissions/organization-application-admin-permission.entity"; describe("SearchController (e2e)", () => { let app: INestApplication; @@ -93,7 +93,7 @@ describe("SearchController (e2e)", () => { await clearDatabase(); // Delete all gateways created in E2E tests: - const existing = await chirpstackGatewayService.getAll(1000, 0); + const existing = await chirpstackGatewayService.getAll(1000); existing.result.forEach(async element => { if (element.name.startsWith(gatewayNamePrefix)) { // Logger.debug(`Found ${element.name}, deleting.`); @@ -127,11 +127,11 @@ describe("SearchController (e2e)", () => { const orgAdminUser = await generateSavedOrganizationAdminUser(org1); orgAdminJwt = generateValidJwtForUser(orgAdminUser); - const writePerm = org1.permissions.find( - x => x.type == PermissionType.Write - ) as WritePermission; - writePerm.applications = [app1_1]; - await getManager().save(writePerm); + const orgAppAdminPermission = org1.permissions.find( + x => x.type.some(({ type }) => type === PermissionType.OrganizationApplicationAdmin) + ) as OrganizationApplicationAdminPermission; + orgAppAdminPermission.applications = [app1_1]; + await getManager().save(orgAppAdminPermission); const readUser = await generateSavedReadWriteUser(org1); readUserJwt = generateValidJwtForUser(readUser); diff --git a/test/e2e/test-helpers.ts b/test/e2e/test-helpers.ts index 9d5d069e..c9367d02 100644 --- a/test/e2e/test-helpers.ts +++ b/test/e2e/test-helpers.ts @@ -7,19 +7,17 @@ import { RawRequestDto } from "@dto/kafka/raw-request.dto"; import { Application } from "@entities/application.entity"; import { DataTarget } from "@entities/data-target.entity"; import { GenericHTTPDevice } from "@entities/generic-http-device.entity"; -import { GlobalAdminPermission } from "@entities/global-admin-permission.entity"; +import { GlobalAdminPermission } from "@entities/permissions/global-admin-permission.entity"; import { HttpPushDataTarget } from "@entities/http-push-data-target.entity"; import { IoTDevicePayloadDecoderDataTargetConnection } from "@entities/iot-device-payload-decoder-data-target-connection.entity"; import { IoTDevice } from "@entities/iot-device.entity"; import { LoRaWANDevice } from "@entities/lorawan-device.entity"; -import { OrganizationAdminPermission } from "@entities/organization-admin-permission.entity"; import { Organization } from "@entities/organization.entity"; import { PayloadDecoder } from "@entities/payload-decoder.entity"; -import { Permission } from "@entities/permission.entity"; -import { ReadPermission } from "@entities/read-permission.entity"; +import { Permission } from "@entities/permissions/permission.entity"; +import { ReadPermission } from "@entities/permissions/read-permission.entity"; import { SigFoxDevice } from "@entities/sigfox-device.entity"; import { User } from "@entities/user.entity"; -import { WritePermission } from "@entities/write-permission.entity"; import { KafkaTopic } from "@enum/kafka-topic.enum"; import { PermissionType } from "@enum/permission-type.enum"; import { ChirpstackSetupNetworkServerService } from "@services/chirpstack/network-server.service"; @@ -38,12 +36,15 @@ import { DeviceProfileDto } from "@dto/chirpstack/device-profile.dto"; import { GenericChirpstackConfigurationService } from "@services/chirpstack/generic-chirpstack-configuration.service"; import { ListAllChirpstackApplicationsResponseDto } from "@dto/chirpstack/list-all-applications-response.dto"; import { CreateIoTDeviceDownlinkDto } from "@dto/create-iot-device-downlink.dto"; -import { INestApplication } from "@nestjs/common"; +import { INestApplication, NotImplementedException } from "@nestjs/common"; import { CreateIoTDeviceDto } from "@dto/create-iot-device.dto"; import { IoTDeviceType } from "@enum/device-type.enum"; import { ActivationType } from "@enum/lorawan-activation-type.enum"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { DeviceProfileService } from "@services/chirpstack/device-profile.service"; +import { OrganizationApplicationAdminPermission } from "@entities/permissions/organization-application-admin-permission.entity"; +import { OrganizationGatewayAdminPermission } from "@entities/permissions/organization-gateway-admin-permission.entity"; +import { OrganizationUserAdminPermission } from "@entities/permissions/organization-user-admin-permission.entity"; export async function clearDatabase(): Promise { await getManager().query( @@ -142,17 +143,19 @@ export async function generateSavedGlobalAdminPermission(): Promise< return await getManager().save(generateGlobalAdminPermission()); } -export function generateOrganizationAdminPermission( - org: Organization -): OrganizationAdminPermission { - return new OrganizationAdminPermission("E2E Test - Org admin", org); -} +// TODO: Due to the changes in permissions, these are no longer valid. +// See generateSavedOrganizationAdminUser() +// export function generateOrganizationAdminPermission( +// org: Organization +// ): OrganizationAdminPermission { +// return new OrganizationAdminPermission("E2E Test - Org admin", org); +// } -export async function generateSavedOrganizationAdminPermission( - org: Organization -): Promise { - return await getManager().save(generateOrganizationAdminPermission(org)); -} +// export async function generateSavedOrganizationAdminPermission( +// org: Organization +// ): Promise { +// return await getManager().save(generateOrganizationAdminPermission(org)); +// } export function generateDeviceModel( org: Organization, @@ -191,13 +194,20 @@ export function generateOrganization(name?: string): Organization { org.deviceModels = []; const READ_SUFFIX = " - Read"; - const WRITE_SUFFIX = " - Write"; - const ADMIN_SUFFIX = " - OrganizationAdmin"; + const APP_ADMIN_SUFFIX = " - OrganizationAppAdmin"; + const GATEWAY_ADMIN_SUFFIX = " - OrganizationGatewayAdmin"; + const USER_ADMIN_SUFFIX = " - OrganizationUserAdmin"; const readPermission = new ReadPermission(org.name + READ_SUFFIX, org, true); - const writePermission = new WritePermission(org.name + WRITE_SUFFIX, org, true); - const adminPermission = new OrganizationAdminPermission(org.name + ADMIN_SUFFIX, org); - org.permissions = [adminPermission, writePermission, readPermission]; + const appAdminPermission = new OrganizationApplicationAdminPermission(org.name + APP_ADMIN_SUFFIX, org, true); + const gatewayAdminPermission = new OrganizationGatewayAdminPermission(org.name + GATEWAY_ADMIN_SUFFIX, org); + const userAdminPermission = new OrganizationUserAdminPermission(org.name + USER_ADMIN_SUFFIX, org); + org.permissions = [ + appAdminPermission, + gatewayAdminPermission, + userAdminPermission, + readPermission, + ]; return org; } @@ -247,19 +257,27 @@ export async function generateSavedGlobalAdminUser(): Promise { export async function generateSavedOrganizationAdminUser( org: Organization ): Promise { - let orgAdmin = org.permissions.find(x => x.type == PermissionType.OrganizationAdmin); - if (!orgAdmin) { - orgAdmin = await generateSavedOrganizationAdminPermission(org); - } - const user = await getManager().save(generateUser([orgAdmin])); + // let orgAdmin = org.permissions.find(x => x.type == PermissionType.OrganizationAdmin); + // if (!orgAdmin) { + // orgAdmin = await generateSavedOrganizationAdminPermission(org); + // } + // const user = await getManager().save(generateUser([orgAdmin])); - return user; + // return user; + + // TODO: Due to the changes in permissions, this test is no longer valid + // Tests aren't used anymore. Instead of "fixing" it, we throw an exception to raise awareness + throw new NotImplementedException(); } export async function generateSavedReadWriteUser(org: Organization): Promise { - const writePerm = org.permissions.find(x => x.type == PermissionType.Write); - const readPerm = org.permissions.find(x => x.type == PermissionType.Read); - return await getManager().save(generateUser([writePerm, readPerm])); + const appAdminPerm = org.permissions.find(x => x.type.some(({ type }) => type === PermissionType.OrganizationApplicationAdmin)); + const gatewayAdminPerm = org.permissions.find(x => x.type.some(({ type }) => type === PermissionType.OrganizationGatewayAdmin)); + const userAdminPerm = org.permissions.find(x => x.type.some(({ type }) => type === PermissionType.OrganizationUserAdmin)); + const readPerm = org.permissions.find(x => x.type.some(({ type }) => type === PermissionType.Read)); + return await getManager().save( + generateUser([appAdminPerm, gatewayAdminPerm, userAdminPerm, readPerm]) + ); } export function generateApplication(org?: Organization, name?: string): Application { @@ -480,19 +498,19 @@ export function generatePayloadDecoder( const TYPE_EXT_DIGITAL2 = 0x1a; // 1bytes value 1 or 0 const TYPE_EXT_ANALOG_UV = 0x1b; // 4 bytes signed int (uV) const TYPE_DEBUG = 0x3d; // 4bytes debug - + function bin16dec(bin) { var num = bin & 0xffff; if (0x8000 & num) num = -(0x010000 - num); return num; } - + function bin8dec(bin) { var num = bin & 0xff; if (0x80 & num) num = -(0x0100 - num); return num; } - + function base64ToBytes(str) { return atob(str) .split("") @@ -500,13 +518,13 @@ export function generatePayloadDecoder( return c.charCodeAt(0); }); } - + function hexToBytes(hex) { for (var bytes = [], c = 0; c < hex.length; c += 2) bytes.push(parseInt(hex.substr(c, 2), 16)); return bytes; } - + function DecodeElsysPayload(data) { var obj = new Object(); for (i = 0; i < data.length; i++) { @@ -685,11 +703,11 @@ export function generatePayloadDecoder( } return obj; } - + function decode(payload, metadata) { let res = {}; res.decoded = DecodeElsysPayload(base64ToBytes(payload.data)); - ${includeDeviceModel ? "res.deviceModel = metadata.deviceModel.body;" : ""} + ${includeDeviceModel ? "res.deviceModel = metadata.deviceModel.body;" : ""} return res; } `; @@ -724,6 +742,7 @@ export function generateLoRaWANRawRequestDto(iotDeviceId?: number): RawRequestDt }`), iotDeviceId: iotDeviceId || 1, unixTimestamp: 1596921546, + type: IoTDeviceType.LoRaWAN, }; } @@ -770,6 +789,7 @@ export function generateSigfoxRawRequestDto(iotDeviceId?: number): RawRequestDto rawPayload: JSON.parse(SIGFOX_PAYLOAD), iotDeviceId: iotDeviceId || 1, unixTimestamp: 1596721546, + type: IoTDeviceType.SigFox, }; } diff --git a/test/unit/device-integration-persistence.service.spec.ts b/test/unit/device-integration-persistence.service.spec.ts index ecb9c167..783580dd 100644 --- a/test/unit/device-integration-persistence.service.spec.ts +++ b/test/unit/device-integration-persistence.service.spec.ts @@ -8,7 +8,7 @@ import { GenericHTTPDevice } from "@entities/generic-http-device.entity"; import { IoTDevice } from "@entities/iot-device.entity"; import { LoRaWANDevice } from "@entities/lorawan-device.entity"; import { Organization } from "@entities/organization.entity"; -import { Permission } from "@entities/permission.entity"; +import { Permission } from "@entities/permissions/permission.entity"; import { ReceivedMessage } from "@entities/received-message.entity"; import { ReceivedMessageMetadata } from "@entities/received-message-metadata.entity"; import { SigFoxDevice } from "@entities/sigfox-device.entity"; @@ -119,6 +119,7 @@ describe("DeviceIntegrationPersistenceService", () => { updatedAt: new Date(), belongsTo: org, permissions: [], + multicasts: [], }, connections: [], name: "Test IoTDevice", @@ -128,6 +129,8 @@ describe("DeviceIntegrationPersistenceService", () => { type: IoTDeviceType.GenericHttp, latestReceivedMessage: null, receivedMessagesMetadata: [], + multicasts: [], + receivedSigFoxSignalsMessages: [], }; it("test mapDtoToNewReceivedMessageMetadata - Sigfox data + with timestmap", async () => {