From 5a68b9923474cfbb38aeec31d54e1a80b540aa17 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Thu, 16 Dec 2021 18:23:56 +0100 Subject: [PATCH 01/11] Revised permission system on backend. Pending integration test with frontend --- src/auth/roles.decorator.ts | 6 +- src/auth/roles.guard.ts | 31 ++-- .../application.controller.ts | 35 ++-- .../chirpstack-gateway.controller.ts | 20 +-- .../chirpstack/device-profile.controller.ts | 20 +-- .../chirpstack/service-profile.controller.ts | 10 +- .../data-target.controller.ts | 21 +-- .../device-model.controller.ts | 18 +-- ...coder-data-target-connection.controller.ts | 16 +- .../admin-controller/iot-device.controller.ts | 19 +-- .../payload-decoder.controller.ts | 21 ++- .../sigfox/sigfox-api-contract.controller.ts | 4 +- .../sigfox/sigfox-api-device.controller.ts | 4 +- .../sigfox/sigfox-device-type.controller.ts | 19 +-- .../sigfox/sigfox-group.controller.ts | 21 ++- .../user-management/auth.controller.ts | 18 +-- .../organization.controller.ts | 15 +- .../user-management/permission.controller.ts | 40 +++-- .../user-management/user.controller.ts | 3 +- src/entities/application.entity.ts | 2 +- .../dto/list-all-permissions-response.dto.ts | 2 +- ...permission-organization-application.dto.ts | 46 +++--- .../user-management/create-permission.dto.ts | 9 +- src/entities/enum/permission-type.enum.ts | 6 +- .../organization-admin-permission.entity.ts | 15 -- src/entities/organization.entity.ts | 4 +- .../global-admin-permission.entity.ts | 2 +- ...ion-application-admin-permission.entity.ts | 13 ++ ...anization-application-permission.entity.ts | 3 +- ...ization-gateway-admin-permission.entity.ts | 11 ++ .../organization-permission.entity.ts | 2 +- ...ganization-user-admin-permission.entity.ts | 11 ++ .../{ => permissions}/permission.entity.ts | 0 .../read-permission.entity.ts | 2 +- src/entities/user.entity.ts | 2 +- src/entities/write-permission.entity.ts | 13 -- src/helpers/security-helper.ts | 127 +++++++++------ src/modules/shared.module.ts | 20 +-- .../chirpstack/chirpstack-gateway.service.ts | 5 +- .../chirpstack/device-profile.service.ts | 9 +- .../device-management/iot-device.service.ts | 4 +- .../user-management/permission.service.ts | 149 ++++++++++-------- src/services/user-management/user.service.ts | 2 +- ...irpstack-gateway-configuration.e2e-spec.ts | 2 +- test/e2e/crud/application.e2e-spec.ts | 22 +-- test/e2e/crud/organization.e2e-spec.ts | 41 +++-- test/e2e/crud/permission.e2e-spec.ts | 16 +- test/e2e/crud/search.e2e-spec.ts | 14 +- test/e2e/test-helpers.ts | 90 ++++++----- ...ce-integration-persistence.service.spec.ts | 2 +- 50 files changed, 538 insertions(+), 449 deletions(-) delete mode 100644 src/entities/organization-admin-permission.entity.ts rename src/entities/{ => permissions}/global-admin-permission.entity.ts (81%) create mode 100644 src/entities/permissions/organization-application-admin-permission.entity.ts rename src/entities/{ => permissions}/organization-application-permission.entity.ts (84%) create mode 100644 src/entities/permissions/organization-gateway-admin-permission.entity.ts rename src/entities/{ => permissions}/organization-permission.entity.ts (88%) create mode 100644 src/entities/permissions/organization-user-admin-permission.entity.ts rename src/entities/{ => permissions}/permission.entity.ts (100%) rename src/entities/{ => permissions}/read-permission.entity.ts (78%) delete mode 100644 src/entities/write-permission.entity.ts diff --git a/src/auth/roles.decorator.ts b/src/auth/roles.decorator.ts index db72ccc1..7801915f 100644 --- a/src/auth/roles.decorator.ts +++ b/src/auth/roles.decorator.ts @@ -4,7 +4,7 @@ import { SetMetadata } from "@nestjs/common"; import { PermissionType } from "@enum/permission-type.enum"; export const Read = () => SetMetadata("roles", PermissionType.Read); -export const Write = () => SetMetadata("roles", PermissionType.Write); -export const OrganizationAdmin = () => - SetMetadata("roles", PermissionType.OrganizationAdmin); +export const UserAdmin = () => SetMetadata("roles", PermissionType.OrganizationUserAdmin); +export const GatewayAdmin = () => SetMetadata("roles", PermissionType.OrganizationGatewayAdmin); +export const ApplicationAdmin = () => SetMetadata("roles", PermissionType.OrganizationApplicationAdmin); export const GlobalAdmin = () => SetMetadata("roles", PermissionType.GlobalAdmin); diff --git a/src/auth/roles.guard.ts b/src/auth/roles.guard.ts index c57cc48f..b782efff 100644 --- a/src/auth/roles.guard.ts +++ b/src/auth/roles.guard.ts @@ -30,14 +30,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) ); } @@ -45,19 +48,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 cc155d6c..f53f6f2c 100644 --- a/src/controllers/admin-controller/application.controller.ts +++ b/src/controllers/admin-controller/application.controller.ts @@ -28,7 +28,7 @@ import { import { ApiResponse } from "@nestjs/swagger"; import { JwtAuthGuard } from "@auth/jwt-auth.guard"; -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"; @@ -39,10 +39,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 eb918791..67f7b4d0 100644 --- a/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts +++ b/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts @@ -21,7 +21,7 @@ import { } from "@nestjs/swagger"; import { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { Read, Write } from "@auth/roles.decorator"; +import { Read } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { ChirpstackPaginatedListDto } from "@dto/chirpstack/chirpstack-paginated-list.dto"; import { ChirpstackResponseStatus } from "@dto/chirpstack/chirpstack-response.dto"; @@ -31,7 +31,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"; @@ -41,7 +41,6 @@ import { ChirpstackGetAll } from "@dto/chirpstack/chirpstack-get-all.dto"; @Controller("chirpstack/gateway") @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() -@Write() export class ChirpstackGatewayController { constructor(private chirpstackGatewayService: ChirpstackGatewayService) {} @@ -53,7 +52,7 @@ export class ChirpstackGatewayController { @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, @@ -87,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") @@ -158,9 +153,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 0908db7b..b4f98c42 100644 --- a/src/controllers/admin-controller/chirpstack/device-profile.controller.ts +++ b/src/controllers/admin-controller/chirpstack/device-profile.controller.ts @@ -25,7 +25,7 @@ import { } from "@nestjs/swagger"; import { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { Read, Write } from "@auth/roles.decorator"; +import { Read } 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"; @@ -35,7 +35,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"; @@ -43,7 +43,6 @@ import { ActionType } from "@entities/audit-log-entry"; @Controller("chirpstack/device-profiles") @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() -@Write() export class DeviceProfileController { constructor(private deviceProfileService: DeviceProfileService) {} @@ -54,16 +53,13 @@ export class DeviceProfileController { @ApiProduces("application/json") @ApiOperation({ summary: "Create a new DeviceProfile" }) @ApiBadRequestResponse() - @Write() 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 @@ -95,12 +91,17 @@ export class DeviceProfileController { @ApiOperation({ summary: "Update an existing DeviceProfile" }) @ApiBadRequestResponse() @HttpCode(204) - @Write() 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( @@ -171,7 +172,6 @@ export class DeviceProfileController { @Delete(":id") @ApiOperation({ summary: "Delete one DeviceProfile by id" }) @ApiNotFoundResponse() - @Write() 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 a9b96cdc..e08f6b64 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 { } from "@nestjs/swagger"; import { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { Read, Write } from "@auth/roles.decorator"; +import { Read, UserAdmin } 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,7 +42,7 @@ import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; @Controller("chirpstack/service-profiles") @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() -@Write() +@UserAdmin() export class ServiceProfileController { constructor(private serviceProfileService: ServiceProfileService) {} private readonly logger = new Logger(ServiceProfileController.name); @@ -51,7 +51,7 @@ export class ServiceProfileController { @ApiProduces("application/json") @ApiOperation({ summary: "Create a new ServiceProfile" }) @ApiBadRequestResponse() - @Write() + @UserAdmin() async create( @Req() req: AuthenticatedRequest, @Body() createDto: CreateServiceProfileDto @@ -72,7 +72,7 @@ export class ServiceProfileController { @ApiOperation({ summary: "Update an existing ServiceProfile" }) @ApiBadRequestResponse() @HttpCode(204) - @Write() + @UserAdmin() async update( @Req() req: AuthenticatedRequest, @Param("id") id: string, @@ -132,7 +132,7 @@ export class ServiceProfileController { @Delete(":id") @ApiOperation({ summary: "Delete one ServiceProfile by id" }) @ApiNotFoundResponse() - @Write() + @UserAdmin() 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 29b86685..45e12bc8 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 { JwtAuthGuard } from "@auth/jwt-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 IoT-Device" }) @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 8690385b..d1cf1be0 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, @@ -59,14 +55,14 @@ export class DeviceModelController { @Query() query?: ListAllDeviceModelsDto ): Promise { if (query?.organizationId != null) { - checkIfUserHasReadAccessToOrganization(req, query?.organizationId); + checkIfUserHasAccessToOrganization(req, query?.organizationId, OrganizationAccessScope.UserAdministrationRead); return this.service.getAllDeviceModelsByOrgIds( [query?.organizationId], query ); } - const orgIds = req.user.permissions.getAllOrganizationsWithAtLeastRead(); + const orgIds = req.user.permissions.getAllOrganizationsWithAtLeastUserAdminRead(); 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.UserAdministrationRead); 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 2dba6beb..d899e8db 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 { JwtAuthGuard } from "@auth/jwt-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.getAllOrganizationsWithAtLeastUserAdminRead() ); } } @@ -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 8bcb8206..6768f53e 100644 --- a/src/controllers/admin-controller/iot-device.controller.ts +++ b/src/controllers/admin-controller/iot-device.controller.ts @@ -34,10 +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 { - 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"; @@ -90,7 +87,7 @@ export class IoTDeviceController { throw new NotFoundException(ErrorCodes.IdDoesNotExists); } - checkIfUserHasReadAccessToApplication(req, result.application.id); + checkIfUserHasAccessToApplication(req, result.application.id, ApplicationAccessScope.Read); return result; } @@ -114,7 +111,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 @@ -135,7 +132,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, @@ -172,7 +169,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; @@ -197,10 +194,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); @@ -234,7 +231,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); diff --git a/src/controllers/admin-controller/payload-decoder.controller.ts b/src/controllers/admin-controller/payload-decoder.controller.ts index c39dda96..b41101d9 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 { JwtAuthGuard } from "@auth/jwt-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 1a7f87eb..e3bfc539 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 542709c5..61a02e27 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 605cf7f7..f6dee8d7 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 { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { Read, Write } 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 { 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() + @UserAdmin() @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() + @UserAdmin() @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 22b86d15..323add45 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 { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { Read, Write } 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 { 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) { throw new NotFoundException(); } - 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() + @UserAdmin() 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() + @UserAdmin() 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/user-management/auth.controller.ts b/src/controllers/user-management/auth.controller.ts index ed326138..4fdd9d18 100644 --- a/src/controllers/user-management/auth.controller.ts +++ b/src/controllers/user-management/auth.controller.ts @@ -31,7 +31,7 @@ import { import { JwtPayloadDto } from "@dto/internal/jwt-payload.dto"; import { LoginDto } from "@dto/login.dto"; import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/organization-permission.entity"; +import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; import { User } from "@entities/user.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { AuthService } from "@services/user-management/auth.service"; @@ -42,6 +42,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 { isOrganizationPermission } from "@helpers/security-helper"; @UseFilters(new CustomExceptionFilter()) @ApiTags("Auth") @@ -188,17 +189,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/organization.controller.ts b/src/controllers/user-management/organization.controller.ts index ff1df55f..19562d9d 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,7 @@ 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 { checkIfUserHasAccessToOrganization, OrganizationAccessScope } 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"; @@ -44,7 +43,7 @@ import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() -@OrganizationAdmin() +@UserAdmin() @ApiForbiddenResponse() @ApiUnauthorizedResponse() @ApiTags("User Management") @@ -88,7 +87,7 @@ export class OrganizationController { @Body() updateOrganizationDto: UpdateOrganizationDto ): Promise { try { - checkIfUserHasAdminAccessToOrganization(req, id); + checkIfUserHasAccessToOrganization(req, id, OrganizationAccessScope.UserAdministrationWrite); const organization = await this.organizationService.update( id, @@ -131,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 @@ -146,7 +145,7 @@ export class OrganizationController { @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number ): Promise { - checkIfUserHasAdminAccessToOrganization(req, id); + checkIfUserHasAccessToOrganization(req, id, OrganizationAccessScope.UserAdministrationWrite); try { return await this.organizationService.findByIdWithRelations(id); } catch (err) { @@ -162,7 +161,7 @@ export class OrganizationController { @Param("id", new ParseIntPipe()) id: number ): Promise { try { - checkIfUserHasAdminAccessToOrganization(req, id); + checkIfUserHasAccessToOrganization(req, id, OrganizationAccessScope.UserAdministrationWrite); const result = await this.organizationService.delete(id); diff --git a/src/controllers/user-management/permission.controller.ts b/src/controllers/user-management/permission.controller.ts index c4898ff2..e8d6d158 100644 --- a/src/controllers/user-management/permission.controller.ts +++ b/src/controllers/user-management/permission.controller.ts @@ -23,19 +23,19 @@ 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/organization-permission.entity"; -import { Permission } from "@entities/permission.entity"; +import { OrganizationPermission } from "@entities/permissions/organization-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"; @@ -48,10 +48,11 @@ 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"; @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() -@OrganizationAdmin() +@UserAdmin() @ApiForbiddenResponse() @ApiUnauthorizedResponse() @ApiTags("User Management") @@ -70,7 +71,7 @@ export class PermissionController { @Body() dto: CreatePermissionDto ): Promise { try { - checkIfUserHasAdminAccessToOrganization(req, dto.organizationId); + checkIfUserHasAccessToOrganization(req, dto.organizationId, OrganizationAccessScope.UserAdministrationWrite); const result = await this.permissionService.createNewPermission( dto, @@ -104,9 +105,10 @@ export class PermissionController { checkIfUserIsGlobalAdmin(req); } else { const organizationPermission = permission as OrganizationPermission; - checkIfUserHasAdminAccessToOrganization( + checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id + organizationPermission.organization.id, + OrganizationAccessScope.UserAdministrationWrite ); } @@ -142,9 +144,10 @@ export class PermissionController { throw new BadRequestException("You cannot delete GlobalAdmin"); } else { const organizationPermission = permission as OrganizationPermission; - checkIfUserHasAdminAccessToOrganization( + checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id + organizationPermission.organization.id, + OrganizationAccessScope.UserAdministrationWrite ); } @@ -167,7 +170,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 @@ -196,9 +199,10 @@ export class PermissionController { return permission; } else { const organizationPermission = permission as OrganizationPermission; - checkIfUserHasAdminAccessToOrganization( + checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id + organizationPermission.organization.id, + OrganizationAccessScope.UserAdministrationWrite ); return organizationPermission; @@ -232,9 +236,10 @@ export class PermissionController { return await applicationsPromise; } else { const organizationPermission = permission as OrganizationPermission; - checkIfUserHasAdminAccessToOrganization( + checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id + organizationPermission.organization.id, + OrganizationAccessScope.UserAdministrationWrite ); return await applicationsPromise; @@ -265,9 +270,10 @@ export class PermissionController { return await users; } else { const organizationPermission = permission as OrganizationPermission; - checkIfUserHasAdminAccessToOrganization( + checkIfUserHasAccessToOrganization( req, - organizationPermission?.organization?.id + organizationPermission?.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 9102232c..240e7d6e 100644 --- a/src/controllers/user-management/user.controller.ts +++ b/src/controllers/user-management/user.controller.ts @@ -25,7 +25,7 @@ import { import { QueryFailedError } from "typeorm"; import { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { OrganizationAdmin, Read } from "@auth/roles.decorator"; +import { Read } 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"; @@ -43,7 +43,6 @@ import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() -@OrganizationAdmin() @ApiForbiddenResponse() @ApiUnauthorizedResponse() @ApiTags("User Management") diff --git a/src/entities/application.entity.ts b/src/entities/application.entity.ts index c38248e5..e2ae7f8b 100644 --- a/src/entities/application.entity.ts +++ b/src/entities/application.entity.ts @@ -11,7 +11,7 @@ import { import { DbBaseEntity } from "@entities/base.entity"; import { DataTarget } from "@entities/data-target.entity"; import { IoTDevice } from "@entities/iot-device.entity"; -import { OrganizationApplicationPermission } from "@entities/organization-application-permission.entity"; +import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; import { Organization } from "@entities/organization.entity"; @Entity("application") 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-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/create-permission.dto.ts b/src/entities/dto/user-management/create-permission.dto.ts index 022862d0..2c28e57e 100644 --- a/src/entities/dto/user-management/create-permission.dto.ts +++ b/src/entities/dto/user-management/create-permission.dto.ts @@ -1,15 +1,18 @@ +import { PermissionType } from "@entities/enum/permission-type.enum"; import { ApiProperty } from "@nestjs/swagger"; import { IsEnum, IsNumber, IsString, Length } from "class-validator"; -import { PermissionType } from "@entities/enum/permission-type.enum"; - export class CreatePermissionDto { @ApiProperty({ required: true, enum: PermissionType, }) @IsEnum(PermissionType) - level: "OrganizationAdmin" | "Write" | "Read"; + level: + | PermissionType.OrganizationUserAdmin + | PermissionType.OrganizationGatewayAdmin + | PermissionType.OrganizationApplicationAdmin + | PermissionType.Read; @ApiProperty({ required: true }) @IsString() diff --git a/src/entities/enum/permission-type.enum.ts b/src/entities/enum/permission-type.enum.ts index b584f1fe..d39eb7f8 100644 --- a/src/entities/enum/permission-type.enum.ts +++ b/src/entities/enum/permission-type.enum.ts @@ -1,8 +1,10 @@ export enum PermissionType { GlobalAdmin = "GlobalAdmin", - OrganizationAdmin = "OrganizationAdmin", - Write = "Write", + OrganizationUserAdmin = "OrganizationUserAdmin", + OrganizationGatewayAdmin = "OrganizationGatewayAdmin", + OrganizationApplicationAdmin = "OrganizationApplicationAdmin", Read = "Read", + OrganizationPermission = "OrganizationPermission", OrganizationApplicationPermissions = "OrganizationApplicationPermissions", } diff --git a/src/entities/organization-admin-permission.entity.ts b/src/entities/organization-admin-permission.entity.ts deleted file mode 100644 index 567e5a71..00000000 --- a/src/entities/organization-admin-permission.entity.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ChildEntity } from "typeorm"; - -import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/organization-permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -import { OrganizationAdmin } from "../auth/roles.decorator"; - -@ChildEntity(PermissionType.OrganizationAdmin) -export class OrganizationAdminPermission extends OrganizationPermission { - constructor(name: string, org: Organization) { - super(name, org); - this.type = PermissionType.OrganizationAdmin; - } -} diff --git a/src/entities/organization.entity.ts b/src/entities/organization.entity.ts index 289d6966..e2294e87 100644 --- a/src/entities/organization.entity.ts +++ b/src/entities/organization.entity.ts @@ -2,9 +2,9 @@ import { Column, Entity, OneToMany, Unique } from "typeorm"; import { Application } from "@entities/application.entity"; import { DbBaseEntity } from "@entities/base.entity"; -import { OrganizationPermission } from "@entities/organization-permission.entity"; +import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; import { PayloadDecoder } from "@entities/payload-decoder.entity"; -import { Permission } from "@entities/permission.entity"; +import { Permission } from "@entities/permissions/permission.entity"; import { SigFoxGroup } from "./sigfox-group.entity"; import { DeviceModel } from "./device-model.entity"; diff --git a/src/entities/global-admin-permission.entity.ts b/src/entities/permissions/global-admin-permission.entity.ts similarity index 81% rename from src/entities/global-admin-permission.entity.ts rename to src/entities/permissions/global-admin-permission.entity.ts index 2586d579..3a07a33b 100644 --- a/src/entities/global-admin-permission.entity.ts +++ b/src/entities/permissions/global-admin-permission.entity.ts @@ -1,6 +1,6 @@ import { ChildEntity } from "typeorm"; -import { Permission } from "@entities/permission.entity"; +import { Permission } from "@entities/permissions/permission.entity"; import { PermissionType } from "@enum/permission-type.enum"; @ChildEntity(PermissionType.GlobalAdmin) diff --git a/src/entities/permissions/organization-application-admin-permission.entity.ts b/src/entities/permissions/organization-application-admin-permission.entity.ts new file mode 100644 index 00000000..354ad146 --- /dev/null +++ b/src/entities/permissions/organization-application-admin-permission.entity.ts @@ -0,0 +1,13 @@ +import { ChildEntity } from "typeorm"; + +import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; +import { Organization } from "@entities/organization.entity"; +import { PermissionType } from "@enum/permission-type.enum"; + +@ChildEntity(PermissionType.OrganizationApplicationAdmin) +export class OrganizationApplicationAdminPermission extends OrganizationApplicationPermission { + constructor(name: string, org: Organization, addNewApps = false) { + super(name, org, addNewApps); + this.type = PermissionType.OrganizationApplicationAdmin; + } +} diff --git a/src/entities/organization-application-permission.entity.ts b/src/entities/permissions/organization-application-permission.entity.ts similarity index 84% rename from src/entities/organization-application-permission.entity.ts rename to src/entities/permissions/organization-application-permission.entity.ts index 2f9c28ce..21403eae 100644 --- a/src/entities/organization-application-permission.entity.ts +++ b/src/entities/permissions/organization-application-permission.entity.ts @@ -2,7 +2,7 @@ import { ChildEntity, Column, ManyToMany } from "typeorm"; import { Application } from "@entities/application.entity"; import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/organization-permission.entity"; +import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; import { PermissionType } from "@enum/permission-type.enum"; @ChildEntity(PermissionType.OrganizationApplicationPermissions) @@ -13,7 +13,6 @@ export abstract class OrganizationApplicationPermission extends OrganizationPerm addNewApps != undefined ? addNewApps : false; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars @ManyToMany(() => Application, application => application.permissions) applications: Application[]; diff --git a/src/entities/permissions/organization-gateway-admin-permission.entity.ts b/src/entities/permissions/organization-gateway-admin-permission.entity.ts new file mode 100644 index 00000000..20f14d0f --- /dev/null +++ b/src/entities/permissions/organization-gateway-admin-permission.entity.ts @@ -0,0 +1,11 @@ +import { Organization } from "@entities/organization.entity"; +import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; +import { PermissionType } from "@enum/permission-type.enum"; +import { ChildEntity } from "typeorm"; + +@ChildEntity(PermissionType.OrganizationGatewayAdmin) +export class OrganizationGatewayAdminPermission extends OrganizationPermission { + constructor(name: string, org: Organization) { + super(name, org); + } +} diff --git a/src/entities/organization-permission.entity.ts b/src/entities/permissions/organization-permission.entity.ts similarity index 88% rename from src/entities/organization-permission.entity.ts rename to src/entities/permissions/organization-permission.entity.ts index 96e5fe0e..9d611cd0 100644 --- a/src/entities/organization-permission.entity.ts +++ b/src/entities/permissions/organization-permission.entity.ts @@ -1,7 +1,7 @@ import { ChildEntity, ManyToOne } from "typeorm"; import { Organization } from "@entities/organization.entity"; -import { Permission } from "@entities/permission.entity"; +import { Permission } from "@entities/permissions/permission.entity"; import { PermissionType } from "@enum/permission-type.enum"; @ChildEntity(PermissionType.OrganizationPermission) diff --git a/src/entities/permissions/organization-user-admin-permission.entity.ts b/src/entities/permissions/organization-user-admin-permission.entity.ts new file mode 100644 index 00000000..df2d98ff --- /dev/null +++ b/src/entities/permissions/organization-user-admin-permission.entity.ts @@ -0,0 +1,11 @@ +import { Organization } from "@entities/organization.entity"; +import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; +import { PermissionType } from "@enum/permission-type.enum"; +import { ChildEntity } from "typeorm"; + +@ChildEntity(PermissionType.OrganizationUserAdmin) +export class OrganizationUserAdminPermission extends OrganizationPermission { + constructor(name: string, org: Organization) { + super(name, org); + } +} diff --git a/src/entities/permission.entity.ts b/src/entities/permissions/permission.entity.ts similarity index 100% rename from src/entities/permission.entity.ts rename to src/entities/permissions/permission.entity.ts diff --git a/src/entities/read-permission.entity.ts b/src/entities/permissions/read-permission.entity.ts similarity index 78% rename from src/entities/read-permission.entity.ts rename to src/entities/permissions/read-permission.entity.ts index 4de6b0fa..f756d62b 100644 --- a/src/entities/read-permission.entity.ts +++ b/src/entities/permissions/read-permission.entity.ts @@ -1,6 +1,6 @@ import { ChildEntity } from "typeorm"; -import { OrganizationApplicationPermission } from "@entities/organization-application-permission.entity"; +import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; import { Organization } from "@entities/organization.entity"; import { PermissionType } from "@enum/permission-type.enum"; diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 7ebab355..646dfe53 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -1,7 +1,7 @@ import { Column, Entity, JoinTable, ManyToMany, Unique } from "typeorm"; import { DbBaseEntity } from "@entities/base.entity"; -import { Permission } from "@entities/permission.entity"; +import { Permission } from "@entities/permissions/permission.entity"; @Entity("user") @Unique(["email"]) diff --git a/src/entities/write-permission.entity.ts b/src/entities/write-permission.entity.ts deleted file mode 100644 index bdc68a28..00000000 --- a/src/entities/write-permission.entity.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ChildEntity } from "typeorm"; - -import { OrganizationApplicationPermission } from "@entities/organization-application-permission.entity"; -import { Organization } from "@entities/organization.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@ChildEntity(PermissionType.Write) -export class WritePermission extends OrganizationApplicationPermission { - constructor(name: string, org: Organization, addNewApps = false) { - super(name, org, addNewApps); - this.type = PermissionType.Write; - } -} diff --git a/src/helpers/security-helper.ts b/src/helpers/security-helper.ts index ae68adb6..486d7cf6 100644 --- a/src/helpers/security-helper.ts +++ b/src/helpers/security-helper.ts @@ -1,63 +1,79 @@ -import { ForbiddenException } from "@nestjs/common"; -import * as _ from "lodash"; - import { AuthenticatedRequest } from "@entities/dto/internal/authenticated-request"; +import { Permission } from "@entities/permissions/permission.entity"; +import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; +import { PermissionType } from "@enum/permission-type.enum"; +import { ForbiddenException, BadRequestException } from "@nestjs/common"; +import * as _ from "lodash"; +import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; -export function checkIfUserHasWriteAccessToApplication( - req: AuthenticatedRequest, - applicationId: number -): void { - checkIfGlobalAdminOrInList( - req, - req.user.permissions.getAllApplicationsWithAtLeastWrite(), - applicationId - ); +export enum OrganizationAccessScope { + ApplicationRead, + ApplicationWrite, + GatewayWrite, + UserAdministrationRead, + UserAdministrationWrite, } -export function checkIfUserHasReadAccessToApplication( - req: AuthenticatedRequest, - applicationId: number -): void { - checkIfGlobalAdminOrInList( - req, - req.user.permissions.getAllApplicationsWithAtLeastRead(), - applicationId - ); +export enum ApplicationAccessScope { + Read, + Write, } -export function checkIfUserHasReadAccessToOrganization( +export function checkIfUserHasAccessToOrganization( req: AuthenticatedRequest, - organizationId: number + organizationId: number, + scope: OrganizationAccessScope ): void { - if (organizationId != null) { - checkIfGlobalAdminOrInList( - req, - req.user.permissions.getAllOrganizationsWithAtLeastRead(), - organizationId - ); + if (!Number.isInteger(organizationId)) return; + + let allowedOrganizations: number[] = []; + + 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"); } -} -export function checkIfUserHasWriteAccessToOrganization( - req: AuthenticatedRequest, - organizationId: number -): void { - checkIfGlobalAdminOrInList( - req, - req.user.permissions.getAllOrganizationsWithAtLeastWrite(), - organizationId - ); + checkIfGlobalAdminOrInList(req, allowedOrganizations, organizationId); } -export function checkIfUserHasAdminAccessToOrganization( +export function checkIfUserHasAccessToApplication( req: AuthenticatedRequest, - organizationId: number + applicationId: number, + scope: ApplicationAccessScope ): void { - checkIfGlobalAdminOrInList( - req, - req.user.permissions.getAllOrganizationsWithAtLeastAdmin(), - organizationId - ); + if (!Number.isInteger(applicationId)) return; + + let allowedOrganizations: number[] = []; + + 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 { @@ -79,3 +95,22 @@ function checkIfGlobalAdminOrInList( throw new ForbiddenException(); } } + +export function isOrganizationPermission(p: Permission): p is OrganizationPermission { + return [ + PermissionType.OrganizationUserAdmin, + PermissionType.OrganizationApplicationAdmin, + PermissionType.OrganizationGatewayAdmin, + PermissionType.Read, + ].some(x => x === p.type); +} + +// TODO: Mark - NOT TESTED +export function isOrganizationApplicationPermission(p: { + type: PermissionType; +}): p is OrganizationApplicationPermission { + return ( + p.type === PermissionType.Read || + p.type === PermissionType.OrganizationApplicationAdmin + ); +} diff --git a/src/modules/shared.module.ts b/src/modules/shared.module.ts index 3fc21d53..71c6dd42 100644 --- a/src/modules/shared.module.ts +++ b/src/modules/shared.module.ts @@ -4,27 +4,28 @@ 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/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 { OrganizationApplicationPermission } from "@entities/organization-application-permission.entity"; +import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/organization-permission.entity"; +import { OrganizationPermission } from "@entities/permissions/organization-permission.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 { 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/write-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 { OrganizationApplicationAdminPermission } from "@entities/permissions/organization-application-admin-permission.entity"; +import { OrganizationUserAdminPermission } from "@entities/permissions/organization-user-admin-permission.entity"; +import { OrganizationGatewayAdminPermission } from "@entities/permissions/organization-gateway-admin-permission.entity"; @Module({ imports: [ @@ -40,8 +41,10 @@ import { AuditLog } from "@services/audit-log.service"; LoRaWANDevice, OpenDataDkDataset, Organization, - OrganizationAdminPermission, OrganizationApplicationPermission, + OrganizationApplicationAdminPermission, + OrganizationUserAdminPermission, + OrganizationGatewayAdminPermission, OrganizationPermission, PayloadDecoder, Permission, @@ -51,7 +54,6 @@ import { AuditLog } from "@services/audit-log.service"; SigFoxDevice, SigFoxGroup, User, - WritePermission, ]), ], providers: [AuditLog], diff --git a/src/services/chirpstack/chirpstack-gateway.service.ts b/src/services/chirpstack/chirpstack-gateway.service.ts index f354a107..4f89cdaa 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() @@ -198,8 +198,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 44bb3ffe..b98ca5ed 100644 --- a/src/services/device-management/iot-device.service.ts +++ b/src/services/device-management/iot-device.service.ts @@ -189,7 +189,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, @@ -665,7 +665,6 @@ export class IoTDeviceService { dto.name ); - // Save application const applicationId = await this.chirpstackDeviceService.findOrCreateDefaultApplication( chirpstackDeviceDto ); @@ -673,7 +672,6 @@ export class IoTDeviceService { chirpstackDeviceDto.device.applicationID = applicationId.toString(); await this.chirpstackDeviceService.createOrUpdateDevice(chirpstackDeviceDto); - await this.doActivation(dto, isUpdate); } catch (err) { this.logger.error(err); diff --git a/src/services/user-management/permission.service.ts b/src/services/user-management/permission.service.ts index e8e2c2f0..70fa4a7a 100644 --- a/src/services/user-management/permission.service.ts +++ b/src/services/user-management/permission.service.ts @@ -15,15 +15,13 @@ 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/global-admin-permission.entity"; -import { OrganizationAdminPermission } from "@entities/organization-admin-permission.entity"; -import { OrganizationApplicationPermission } from "@entities/organization-application-permission.entity"; +import { GlobalAdminPermission } from "@entities/permissions/global-admin-permission.entity"; +import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/organization-permission.entity"; -import { Permission } from "@entities/permission.entity"; -import { ReadPermission } from "@entities/read-permission.entity"; +import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; +import { Permission } from "@entities/permissions/permission.entity"; +import { ReadPermission } from "@entities/permissions/read-permission.entity"; import { User } from "@entities/user.entity"; -import { WritePermission } from "@entities/write-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 +32,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 { OrganizationGatewayAdminPermission } from "@entities/permissions/organization-gateway-admin-permission.entity"; +import { OrganizationUserAdminPermission } from "@entities/permissions/organization-user-admin-permission.entity"; +import { OrganizationApplicationAdminPermission } from "@entities/permissions/organization-application-admin-permission.entity"; @Injectable() export class PermissionService { @@ -48,42 +50,56 @@ 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, + 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 = new ReadPermission(org.name + readSuffix, org, true); + const orgApplicationAdminPermission = new OrganizationApplicationAdminPermission( + org.name + organizationApplicationAdminSuffix, org, true ); - const adminPermission = new OrganizationAdminPermission( - org.name + this.ADMIN_SUFFIX, + const orgAdminPermission = new OrganizationUserAdminPermission( + org.name + organizationUserAdminSuffix, + org + ); + const orgGatewayAadminPermission = new OrganizationGatewayAdminPermission( + org.name + organizationGatewayAdminSuffix, org ); readPermission.createdBy = userId; readPermission.updatedBy = userId; - writePermission.createdBy = userId; - writePermission.updatedBy = userId; - adminPermission.createdBy = userId; - adminPermission.updatedBy = userId; - - // 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; + orgApplicationAdminPermission.createdBy = userId; + orgApplicationAdminPermission.updatedBy = userId; + orgAdminPermission.createdBy = userId; + orgAdminPermission.updatedBy = userId; + orgGatewayAadminPermission.createdBy = userId; + orgGatewayAadminPermission.updatedBy = userId; + return { readPermission, orgApplicationAdminPermission, orgAdminPermission, orgGatewayAadminPermission }; } async findOrCreateGlobalAdminPermission(): Promise { @@ -99,41 +115,40 @@ export class PermissionService { dto: CreatePermissionDto, userId: number ): Promise { - let permission; const org: Organization = await this.organizationService.findById( dto.organizationId ); + const permission = this.createPermission(dto, org); + + await this.mapToPermission(permission, dto); + permission.createdBy = userId; + permission.updatedBy = userId; + + return await getManager().save(permission); + } + + private createPermission(dto: CreatePermissionDto, org: Organization): Permission { switch (dto.level) { - case PermissionType.OrganizationAdmin: { - permission = new OrganizationAdminPermission(dto.name, org); - break; + case PermissionType.OrganizationApplicationAdmin: { + return new OrganizationApplicationAdminPermission(dto.name, org); } - case PermissionType.Write: { - permission = new WritePermission( - dto.name, - org, - dto.automaticallyAddNewApplications - ); - break; + case PermissionType.OrganizationGatewayAdmin: { + return new OrganizationGatewayAdminPermission(dto.name, org); + } + case PermissionType.OrganizationUserAdmin: { + return new OrganizationUserAdminPermission(dto.name, org); } case PermissionType.Read: { - permission = new ReadPermission( + return new ReadPermission( dto.name, org, dto.automaticallyAddNewApplications ); - break; } default: throw new BadRequestException("Bad PermissionLevel"); } - - await this.mapToPermission(permission, dto); - permission.createdBy = userId; - permission.updatedBy = userId; - - return await getManager().save(permission); } async autoAddPermissionsToApplication(app: Application): Promise { @@ -192,15 +207,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) { @@ -313,7 +325,7 @@ export class PermissionService { .leftJoinAndSelect("permission.organization", "organization") .leftJoinAndSelect("organization.applications", "application") .where("permission.type = :permType AND user.id = :id", { - permType: PermissionType.OrganizationAdmin, + permType: PermissionType.OrganizationApplicationAdmin, id: userId, }) .select([ @@ -326,7 +338,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 @@ -339,27 +351,28 @@ export class PermissionService { permissions.forEach(p => { if (p.permission_type == PermissionType.GlobalAdmin) { res.isGlobalAdmin = true; - } else if (p.permission_type == PermissionType.OrganizationAdmin) { - res.organizationAdminPermissions.add(p.organization_id); + } else if (p.permission_type == PermissionType.OrganizationApplicationAdmin) { + this.addOrUpdateApplicationIds(res.orgToApplicationAdminPermissions, p); // 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.OrganizationGatewayAdmin) { + res.orgToGatewayAdminPermissions.add(p.organization_id); + } else if (p.permission_type == PermissionType.OrganizationUserAdmin) { + res.orgToUserAdminPermissions.add(p.organization_id); } else if (p.permission_type == PermissionType.Read) { - this.addOrUpdate(res.readPermissions, p); + this.addOrUpdateApplicationIds(res.orgToReadPermissions, p); } }); return res; } - private isOrganizationAdmin(permissions: PermissionMinimalDto[]) { + private isOrganizationApplicationAdmin(permissions: PermissionMinimalDto[]) { return permissions.some( - x => x.permission_type == PermissionType.OrganizationAdmin + x => x.permission_type == PermissionType.OrganizationApplicationAdmin ); } - 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 36f5b0a3..4da34b3b 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"; 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..5faa1d6f 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; @@ -556,11 +556,11 @@ describe("ApplicationController (e2e)", () => { x => x.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 == PermissionType.OrganizationApplicationAdmin + ) as OrganizationApplicationAdminPermission; + orgAppAdminPerm.applications = [app1]; + await getManager().save([readPerm, orgAppAdminPerm]); const jwt = generateValidJwtForUser(user); @@ -621,10 +621,10 @@ describe("ApplicationController (e2e)", () => { ) 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 == PermissionType.OrganizationApplicationAdmin + ) as OrganizationApplicationAdminPermission; + expect(orgAppAdminPerm.automaticallyAddNewApplications).toBeTruthy(); const testAppOne: CreateApplicationDto = { name: "AutoAdd", 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..8926d1d8 100644 --- a/test/e2e/crud/permission.e2e-spec.ts +++ b/test/e2e/crud/permission.e2e-spec.ts @@ -9,8 +9,8 @@ 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 { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; +import { ReadPermission } from "@entities/permissions/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 +116,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({ diff --git a/test/e2e/crud/search.e2e-spec.ts b/test/e2e/crud/search.e2e-spec.ts index 9b442d3e..d003f982 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 == 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..9736c5fc 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 appAdminPerm = org.permissions.find(x => x.type == PermissionType.OrganizationApplicationAdmin); + const gatewayAdminPerm = org.permissions.find(x => x.type == PermissionType.OrganizationGatewayAdmin); + const userAdminPerm = org.permissions.find(x => x.type == PermissionType.OrganizationUserAdmin); const readPerm = org.permissions.find(x => x.type == PermissionType.Read); - return await getManager().save(generateUser([writePerm, readPerm])); + 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; } `; diff --git a/test/unit/device-integration-persistence.service.spec.ts b/test/unit/device-integration-persistence.service.spec.ts index ecb9c167..1510e351 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"; From 685cca14341de4a254ff3007a541f45b43352a10 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Fri, 17 Dec 2021 14:09:05 +0100 Subject: [PATCH 02/11] Check for the proper permissions --- .../chirpstack/chirpstack-gateway.controller.ts | 5 ++++- .../chirpstack/device-profile.controller.ts | 6 +++++- .../chirpstack/service-profile.controller.ts | 10 +++++----- .../admin-controller/device-model.controller.ts | 6 +++--- ...ayload-decoder-data-target-connection.controller.ts | 2 +- .../sigfox/sigfox-device-type.controller.ts | 6 +++--- .../admin-controller/sigfox/sigfox-group.controller.ts | 6 +++--- .../user-management/organization.controller.ts | 4 +++- .../user-management/permission.controller.ts | 4 +++- src/controllers/user-management/user.controller.ts | 10 +++++----- 10 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts b/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts index 67f7b4d0..dfc890d5 100644 --- a/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts +++ b/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts @@ -21,7 +21,7 @@ import { } from "@nestjs/swagger"; import { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { Read } from "@auth/roles.decorator"; +import { Read, GatewayAdmin } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { ChirpstackPaginatedListDto } from "@dto/chirpstack/chirpstack-paginated-list.dto"; import { ChirpstackResponseStatus } from "@dto/chirpstack/chirpstack-response.dto"; @@ -48,6 +48,7 @@ export class ChirpstackGatewayController { @ApiProduces("application/json") @ApiOperation({ summary: "Create a new Chirpstack Gateway" }) @ApiBadRequestResponse() + @GatewayAdmin() async create( @Req() req: AuthenticatedRequest, @Body() dto: CreateGatewayDto @@ -111,6 +112,7 @@ export class ChirpstackGatewayController { @ApiProduces("application/json") @ApiOperation({ summary: "Create a new Chirpstack Gateway" }) @ApiBadRequestResponse() + @GatewayAdmin() async update( @Req() req: AuthenticatedRequest, @Param("gatewayId") gatewayId: string, @@ -146,6 +148,7 @@ export class ChirpstackGatewayController { } @Delete(":gatewayId") + @GatewayAdmin() async delete( @Req() req: AuthenticatedRequest, @Param("gatewayId") gatewayId: string diff --git a/src/controllers/admin-controller/chirpstack/device-profile.controller.ts b/src/controllers/admin-controller/chirpstack/device-profile.controller.ts index b4f98c42..f2b9c927 100644 --- a/src/controllers/admin-controller/chirpstack/device-profile.controller.ts +++ b/src/controllers/admin-controller/chirpstack/device-profile.controller.ts @@ -25,7 +25,7 @@ import { } from "@nestjs/swagger"; import { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { Read } 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"; @@ -43,6 +43,7 @@ import { ActionType } from "@entities/audit-log-entry"; @Controller("chirpstack/device-profiles") @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() +@ApplicationAdmin() export class DeviceProfileController { constructor(private deviceProfileService: DeviceProfileService) {} @@ -53,6 +54,7 @@ export class DeviceProfileController { @ApiProduces("application/json") @ApiOperation({ summary: "Create a new DeviceProfile" }) @ApiBadRequestResponse() + @ApplicationAdmin() async create( @Req() req: AuthenticatedRequest, @Body() createDto: CreateDeviceProfileDto @@ -91,6 +93,7 @@ export class DeviceProfileController { @ApiOperation({ summary: "Update an existing DeviceProfile" }) @ApiBadRequestResponse() @HttpCode(204) + @ApplicationAdmin() async update( @Req() req: AuthenticatedRequest, @Param("id") id: string, @@ -172,6 +175,7 @@ export class DeviceProfileController { @Delete(":id") @ApiOperation({ summary: "Delete one DeviceProfile by id" }) @ApiNotFoundResponse() + @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 e08f6b64..fa0b2845 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 { } from "@nestjs/swagger"; import { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { Read, UserAdmin } 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,7 +42,7 @@ import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; @Controller("chirpstack/service-profiles") @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() -@UserAdmin() +@ApplicationAdmin() export class ServiceProfileController { constructor(private serviceProfileService: ServiceProfileService) {} private readonly logger = new Logger(ServiceProfileController.name); @@ -51,7 +51,7 @@ export class ServiceProfileController { @ApiProduces("application/json") @ApiOperation({ summary: "Create a new ServiceProfile" }) @ApiBadRequestResponse() - @UserAdmin() + @ApplicationAdmin() async create( @Req() req: AuthenticatedRequest, @Body() createDto: CreateServiceProfileDto @@ -72,7 +72,7 @@ export class ServiceProfileController { @ApiOperation({ summary: "Update an existing ServiceProfile" }) @ApiBadRequestResponse() @HttpCode(204) - @UserAdmin() + @ApplicationAdmin() async update( @Req() req: AuthenticatedRequest, @Param("id") id: string, @@ -132,7 +132,7 @@ export class ServiceProfileController { @Delete(":id") @ApiOperation({ summary: "Delete one ServiceProfile by id" }) @ApiNotFoundResponse() - @UserAdmin() + @ApplicationAdmin() async deleteOne( @Req() req: AuthenticatedRequest, @Param("id") id: string diff --git a/src/controllers/admin-controller/device-model.controller.ts b/src/controllers/admin-controller/device-model.controller.ts index d1cf1be0..3bcf487d 100644 --- a/src/controllers/admin-controller/device-model.controller.ts +++ b/src/controllers/admin-controller/device-model.controller.ts @@ -55,14 +55,14 @@ export class DeviceModelController { @Query() query?: ListAllDeviceModelsDto ): Promise { if (query?.organizationId != null) { - checkIfUserHasAccessToOrganization(req, query?.organizationId, OrganizationAccessScope.UserAdministrationRead); + checkIfUserHasAccessToOrganization(req, query?.organizationId, OrganizationAccessScope.ApplicationRead); return this.service.getAllDeviceModelsByOrgIds( [query?.organizationId], query ); } - const orgIds = req.user.permissions.getAllOrganizationsWithAtLeastUserAdminRead(); + const orgIds = req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead(); return this.service.getAllDeviceModelsByOrgIds(orgIds, query); } @@ -77,7 +77,7 @@ export class DeviceModelController { throw new NotFoundException(ErrorCodes.IdDoesNotExists); } - checkIfUserHasAccessToOrganization(req, deviceModel.belongsTo.id, OrganizationAccessScope.UserAdministrationRead); + checkIfUserHasAccessToOrganization(req, deviceModel.belongsTo.id, OrganizationAccessScope.ApplicationRead); return deviceModel; } 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 d899e8db..e6dd2ca2 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 @@ -120,7 +120,7 @@ export class IoTDevicePayloadDecoderDataTargetConnectionController { } else { return await this.service.findAllByPayloadDecoderId( id, - req.user.permissions.getAllOrganizationsWithAtLeastUserAdminRead() + req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead() ); } } 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 f6dee8d7..4275cb03 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 { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { Read, UserAdmin } 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"; @@ -86,7 +86,7 @@ export class SigfoxDeviceTypeController { } @Post() - @UserAdmin() + @ApplicationAdmin() @ApiCreatedResponse() @ApiBadRequestResponse() async create( @@ -115,7 +115,7 @@ export class SigfoxDeviceTypeController { } @Put(":id") - @UserAdmin() + @ApplicationAdmin() @ApiNoContentResponse() @ApiBadRequestResponse() @HttpCode(204) diff --git a/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts b/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts index 323add45..b42de3f4 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 { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { Read, UserAdmin } 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"; @@ -88,7 +88,7 @@ export class SigfoxGroupController { @ApiProduces("application/json") @ApiOperation({ summary: "Create a SigFox Group connection" }) @ApiCreatedResponse() - @UserAdmin() + @ApplicationAdmin() async create( @Req() req: AuthenticatedRequest, @Body() query: CreateSigFoxGroupRequestDto @@ -118,7 +118,7 @@ export class SigfoxGroupController { @Put(":id") @ApiProduces("application/json") @ApiOperation({ summary: "Update a SigFox Groups" }) - @UserAdmin() + @ApplicationAdmin() async update( @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number, diff --git a/src/controllers/user-management/organization.controller.ts b/src/controllers/user-management/organization.controller.ts index 19562d9d..2baac17c 100644 --- a/src/controllers/user-management/organization.controller.ts +++ b/src/controllers/user-management/organization.controller.ts @@ -43,7 +43,6 @@ import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() -@UserAdmin() @ApiForbiddenResponse() @ApiUnauthorizedResponse() @ApiTags("User Management") @@ -55,6 +54,7 @@ export class OrganizationController { @GlobalAdmin() @Post() @ApiOperation({ summary: "Create a new Organization" }) + @UserAdmin() async create( @Req() req: AuthenticatedRequest, @Body() createOrganizationDto: CreateOrganizationDto @@ -81,6 +81,7 @@ export class OrganizationController { @Put(":id") @ApiOperation({ summary: "Update an Organization" }) @ApiNotFoundResponse() + @UserAdmin() async update( @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number, @@ -156,6 +157,7 @@ export class OrganizationController { @Delete(":id") @ApiOperation({ summary: "Delete an Organization" }) @ApiNotFoundResponse() + @UserAdmin() async delete( @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number diff --git a/src/controllers/user-management/permission.controller.ts b/src/controllers/user-management/permission.controller.ts index e8d6d158..c93474cc 100644 --- a/src/controllers/user-management/permission.controller.ts +++ b/src/controllers/user-management/permission.controller.ts @@ -52,7 +52,6 @@ import { UserAdmin } from "@auth/roles.decorator"; @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() -@UserAdmin() @ApiForbiddenResponse() @ApiUnauthorizedResponse() @ApiTags("User Management") @@ -66,6 +65,7 @@ export class PermissionController { @Post() @ApiOperation({ summary: "Create new permission entity" }) + @UserAdmin() async createPermission( @Req() req: AuthenticatedRequest, @Body() dto: CreatePermissionDto @@ -94,6 +94,7 @@ export class PermissionController { @Put(":id") @ApiOperation({ summary: "Update permission" }) + @UserAdmin() async updatePermission( @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number, @@ -134,6 +135,7 @@ export class PermissionController { @Delete(":id") @ApiOperation({ summary: "Delete a permission entity" }) + @UserAdmin() async deletePermission( @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number diff --git a/src/controllers/user-management/user.controller.ts b/src/controllers/user-management/user.controller.ts index 240e7d6e..0d1ac003 100644 --- a/src/controllers/user-management/user.controller.ts +++ b/src/controllers/user-management/user.controller.ts @@ -25,7 +25,7 @@ import { import { QueryFailedError } from "typeorm"; import { JwtAuthGuard } from "@auth/jwt-auth.guard"; -import { 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"; @@ -61,6 +61,7 @@ export class UserController { @Post() @ApiOperation({ summary: "Create a new User" }) + @UserAdmin() async create( @Req() req: AuthenticatedRequest, @Body() createUserDto: CreateUserDto @@ -100,6 +101,7 @@ export class UserController { @Put(":id") @ApiOperation({ summary: "Change a user" }) + @UserAdmin() async update( @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number, @@ -110,8 +112,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 @@ -140,8 +141,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 From 81ea830aebd238c4db60abf37a31d34597fc1867 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Tue, 11 Jan 2022 11:38:55 +0100 Subject: [PATCH 03/11] Initial migration --- .eslintrc.js | 2 +- src/migration/1641894827503-Initial.ts | 200 +++++++++++++++++++++++++ tsconfig.json | 2 +- 3 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 src/migration/1641894827503-Initial.ts diff --git a/.eslintrc.js b/.eslintrc.js index d3256d24..67b58f09 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,7 +27,7 @@ module.exports = { }, overrides: [ { - files: ["test/**/*.ts"], + files: ["test/**/*.ts", "src/migration/*.ts"], rules: { "max-lines-per-function": "off", }, diff --git a/src/migration/1641894827503-Initial.ts b/src/migration/1641894827503-Initial.ts new file mode 100644 index 00000000..b2935c1c --- /dev/null +++ b/src/migration/1641894827503-Initial.ts @@ -0,0 +1,200 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class Initial1641894827503 implements MigrationInterface { + name = 'Initial1641894827503' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "open_data_dk_dataset" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "name" character varying NOT NULL, "description" character varying NOT NULL DEFAULT '', "keywords" text array, "license" character varying NOT NULL, "authorName" character varying NOT NULL, "authorEmail" character varying NOT NULL, "resourceTitle" character varying NOT NULL DEFAULT '', "createdById" integer, "updatedById" integer, "dataTargetId" integer, CONSTRAINT "REL_a0b7439e2a06cd0b67c4486a4f" UNIQUE ("dataTargetId"), CONSTRAINT "PK_07c9a82d4ee55c1207a2ce17731" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "received_message" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "rawData" jsonb NOT NULL, "sentTime" TIMESTAMP NOT NULL, "createdById" integer, "updatedById" integer, "deviceId" integer, CONSTRAINT "REL_b0a8014e917afd2b1152c83dc1" UNIQUE ("deviceId"), CONSTRAINT "PK_0bd1cfe05dbb3bc22d0abbcf7ac" PRIMARY KEY ("id")); COMMENT ON COLUMN "received_message"."sentTime" IS 'Time reported by device (if possible, otherwise time received)'`); + await queryRunner.query(`CREATE TABLE "received_message_metadata" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "sentTime" TIMESTAMP NOT NULL, "signalData" jsonb, "createdById" integer, "updatedById" integer, "deviceId" integer, CONSTRAINT "PK_95d11e96204b0a42da220970411" PRIMARY KEY ("id")); COMMENT ON COLUMN "received_message_metadata"."sentTime" IS 'Time reported by device (if possible, otherwise time received)'`); + await queryRunner.query(`CREATE INDEX "IDX_4e710f4b84fd89dbbbab4616bf" ON "received_message_metadata" ("deviceId", "sentTime") `); + await queryRunner.query(`CREATE TABLE "user" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "name" character varying NOT NULL, "email" character varying, "passwordHash" character varying, "active" boolean NOT NULL DEFAULT true, "lastLogin" TIMESTAMP, "nameId" character varying, "createdById" integer, "updatedById" integer, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "permission_type_enum" AS ENUM('GlobalAdmin', 'OrganizationAdmin', 'Write', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions')`); + await queryRunner.query(`CREATE TABLE "permission" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "type" "permission_type_enum" NOT NULL, "name" character varying NOT NULL, "automaticallyAddNewApplications" boolean DEFAULT false, "createdById" integer, "updatedById" integer, "organizationId" integer, CONSTRAINT "PK_3b8b97af9d9d8807e41e6f48362" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_71bf2818fb2ad92e208d7aeadf" ON "permission" ("type") `); + await queryRunner.query(`CREATE TABLE "payload_decoder" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "name" character varying NOT NULL, "decodingFunction" text NOT NULL, "createdById" integer, "updatedById" integer, "organizationId" integer, CONSTRAINT "PK_04d515ff430849ef550db2f4495" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "sigfox_group" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "username" character varying NOT NULL, "password" character varying NOT NULL, "sigfoxGroupId" character varying NOT NULL DEFAULT '', "createdById" integer, "updatedById" integer, "belongsToId" integer, CONSTRAINT "UQ_c658773871b8ec5ad9f9f4f692d" UNIQUE ("username", "belongsToId"), CONSTRAINT "PK_d4573623c283131a7901097798d" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "organization" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "name" character varying NOT NULL, "createdById" integer, "updatedById" integer, CONSTRAINT "UQ_c21e615583a3ebbb0977452afb0" UNIQUE ("name"), CONSTRAINT "UQ_c21e615583a3ebbb0977452afb0" UNIQUE ("name"), CONSTRAINT "PK_472c1f99a32def1b0abb219cd67" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "device_model" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "body" jsonb, "createdById" integer, "updatedById" integer, "belongsToId" integer, CONSTRAINT "PK_0ad50a03f4778f2d1dc83386a77" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "lorawan-multicast" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "address" character varying NOT NULL, "networkSessionKey" character varying NOT NULL, "applicationSessionKey" character varying NOT NULL, "frameCounter" integer NOT NULL, "dataRate" integer NOT NULL, "frequency" integer NOT NULL, "groupType" character varying NOT NULL, "chirpstackGroupId" character varying, "createdById" integer, "updatedById" integer, CONSTRAINT "PK_3a1533b403f6e38b0ec4df5aee5" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "multicast" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "groupName" character varying NOT NULL, "createdById" integer, "updatedById" integer, "applicationId" integer, "lorawanMulticastDefinitionId" integer, CONSTRAINT "REL_5a058ff45da16000d4e3b409e3" UNIQUE ("lorawanMulticastDefinitionId"), CONSTRAINT "PK_5c305350f804318120f45107a64" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TYPE "iot_device_type_enum" AS ENUM('GENERIC_HTTP', 'LORAWAN', 'SIGFOX')`); + await queryRunner.query(`CREATE TABLE "iot_device" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "name" character varying NOT NULL, "location" geometry(Point,4326), "commentOnLocation" character varying, "comment" character varying, "metadata" jsonb, "type" "iot_device_type_enum" NOT NULL, "apiKey" character varying, "deviceEUI" character varying, "chirpstackApplicationId" integer, "deviceId" character varying, "deviceTypeId" character varying, "groupId" character varying, "downlinkPayload" character varying, "createdById" integer, "updatedById" integer, "applicationId" integer, "deviceModelId" integer, CONSTRAINT "PK_9ebe9e6bfa9a631bfba27b3d57d" PRIMARY KEY ("id")); COMMENT ON COLUMN "iot_device"."apiKey" IS 'Used for GenericHTTPDevice'`); + await queryRunner.query(`CREATE INDEX "IDX_18ce5007f4efcbe92338c10f5d" ON "iot_device" ("type") `); + await queryRunner.query(`CREATE TABLE "iot_device_payload_decoder_data_target_connection" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "createdById" integer, "updatedById" integer, "payloadDecoderId" integer, "dataTargetId" integer, CONSTRAINT "PK_a0470515beb1c8087c4e868bf06" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_7055bf87cc824d4d1268704818" ON "iot_device_payload_decoder_data_target_connection" ("dataTargetId") `); + await queryRunner.query(`CREATE INDEX "IDX_1ef58c35ae5562242966d483a1" ON "iot_device_payload_decoder_data_target_connection" ("payloadDecoderId") `); + await queryRunner.query(`CREATE TYPE "data_target_type_enum" AS ENUM('HTTP_PUSH')`); + await queryRunner.query(`CREATE TABLE "data_target" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "type" "data_target_type_enum" NOT NULL, "name" character varying NOT NULL, "url" character varying, "timeout" integer DEFAULT '30000', "authorizationHeader" character varying, "createdById" integer, "updatedById" integer, "applicationId" integer, CONSTRAINT "PK_cd4e902e1acce0793eeba6d0dc5" PRIMARY KEY ("id")); COMMENT ON COLUMN "data_target"."timeout" IS 'HTTP call timeout in milliseconds'`); + await queryRunner.query(`CREATE INDEX "IDX_af951594386c64a22f8fc057be" ON "data_target" ("type") `); + await queryRunner.query(`CREATE TABLE "application" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "name" character varying NOT NULL, "description" character varying, "createdById" integer, "updatedById" integer, "belongsToId" integer, CONSTRAINT "UQ_608bb41e7e1ef5f6d7abb07e394" UNIQUE ("name"), CONSTRAINT "PK_569e0c3e863ebdf5f2408ee1670" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "user_permissions_permission" ("userId" integer NOT NULL, "permissionId" integer NOT NULL, CONSTRAINT "PK_8dd49853fbad35f9a0f91b11877" PRIMARY KEY ("userId", "permissionId"))`); + await queryRunner.query(`CREATE INDEX "IDX_5b72d197d92b8bafbe7906782e" ON "user_permissions_permission" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_c43a6a56e3ef281cbfba9a7745" ON "user_permissions_permission" ("permissionId") `); + await queryRunner.query(`CREATE TABLE "multicast_iot_devices_iot_device" ("multicastId" integer NOT NULL, "iotDeviceId" integer NOT NULL, CONSTRAINT "PK_281c23f69718bec32d05cd8398c" PRIMARY KEY ("multicastId", "iotDeviceId"))`); + await queryRunner.query(`CREATE INDEX "IDX_f502bc0bd3c1406ae2b3dc1562" ON "multicast_iot_devices_iot_device" ("multicastId") `); + await queryRunner.query(`CREATE INDEX "IDX_0c5a137689ddc88c8257e2cd46" ON "multicast_iot_devices_iot_device" ("iotDeviceId") `); + await queryRunner.query(`CREATE TABLE "iot_device_multicasts_multicast" ("iotDeviceId" integer NOT NULL, "multicastId" integer NOT NULL, CONSTRAINT "PK_d3186bb9eaa001bf2ea3ccefa37" PRIMARY KEY ("iotDeviceId", "multicastId"))`); + await queryRunner.query(`CREATE INDEX "IDX_9b91f8a9dcc02c5926fced99e1" ON "iot_device_multicasts_multicast" ("iotDeviceId") `); + await queryRunner.query(`CREATE INDEX "IDX_d38b5d829712b4e0df2bae160f" ON "iot_device_multicasts_multicast" ("multicastId") `); + await queryRunner.query(`CREATE TABLE "iot_dev_pay_dec_dat_tar_con_iot_dev_iot_dev" ("iotDevicePayloadDecoderDataTargetConnectionId" integer NOT NULL, "iotDeviceId" integer NOT NULL, CONSTRAINT "PK_f03f957266ca9997d3f492ed018" PRIMARY KEY ("iotDevicePayloadDecoderDataTargetConnectionId", "iotDeviceId"))`); + await queryRunner.query(`CREATE INDEX "IDX_daf134b834f403ea98efa1fc09" ON "iot_dev_pay_dec_dat_tar_con_iot_dev_iot_dev" ("iotDevicePayloadDecoderDataTargetConnectionId") `); + await queryRunner.query(`CREATE INDEX "IDX_c88fdc6da057254fe8f9e15555" ON "iot_dev_pay_dec_dat_tar_con_iot_dev_iot_dev" ("iotDeviceId") `); + await queryRunner.query(`CREATE TABLE "application_permissions_permission" ("applicationId" integer NOT NULL, "permissionId" integer NOT NULL, CONSTRAINT "PK_27688b5488c8a5b7fc415663a17" PRIMARY KEY ("applicationId", "permissionId"))`); + await queryRunner.query(`CREATE INDEX "IDX_6c691b1ba972915dc7bf324420" ON "application_permissions_permission" ("applicationId") `); + await queryRunner.query(`CREATE INDEX "IDX_c1bbb34687ca84f2a166ee376e" ON "application_permissions_permission" ("permissionId") `); + await queryRunner.query(`ALTER TABLE "open_data_dk_dataset" ADD CONSTRAINT "FK_981af5bddacc8b7bf9f37247fb6" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "open_data_dk_dataset" ADD CONSTRAINT "FK_d5b350d2e5efe0229d8e01f7507" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "open_data_dk_dataset" ADD CONSTRAINT "FK_a0b7439e2a06cd0b67c4486a4f7" FOREIGN KEY ("dataTargetId") REFERENCES "data_target"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "received_message" ADD CONSTRAINT "FK_38b28c3a0241fcec40207e0745d" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "received_message" ADD CONSTRAINT "FK_21ec54fded11c9d4ee8df3c17dd" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "received_message" ADD CONSTRAINT "FK_b0a8014e917afd2b1152c83dc10" FOREIGN KEY ("deviceId") REFERENCES "iot_device"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "received_message_metadata" ADD CONSTRAINT "FK_4834b1a60043b2c443b07c08b17" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "received_message_metadata" ADD CONSTRAINT "FK_8399e14dc77145a32ccb755a84d" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "received_message_metadata" ADD CONSTRAINT "FK_de1719d0660b369de300440ad6f" FOREIGN KEY ("deviceId") REFERENCES "iot_device"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_45c0d39d1f9ceeb56942db93cc5" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_db5173f7d27aa8a98a9fe6113df" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "permission" ADD CONSTRAINT "FK_00e2c09abd157b5358faf3f43d0" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "permission" ADD CONSTRAINT "FK_40c4877af6e402a449d56af4d39" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "permission" ADD CONSTRAINT "FK_2102b10c8a5424189ac612ca8d9" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "payload_decoder" ADD CONSTRAINT "FK_33f98e074272079c02d792aa852" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "payload_decoder" ADD CONSTRAINT "FK_ce6313b2fa3de41f2d4d9409003" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "payload_decoder" ADD CONSTRAINT "FK_20c47c4f5719e0d398743401acd" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "sigfox_group" ADD CONSTRAINT "FK_f5401745b31e0eedff2d219f77d" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "sigfox_group" ADD CONSTRAINT "FK_23d9047d051ab1ad8875306ae98" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "sigfox_group" ADD CONSTRAINT "FK_a38129f018eb963aea886de12e9" FOREIGN KEY ("belongsToId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "organization" ADD CONSTRAINT "FK_acdbd1e490930af04b4ff569ca9" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "organization" ADD CONSTRAINT "FK_a7a6b96e8460aeb314599cf3580" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "device_model" ADD CONSTRAINT "FK_9d26f066b617bbdac41d72e82b2" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "device_model" ADD CONSTRAINT "FK_ed6727d1c4a7f9e6299ba6e2961" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "device_model" ADD CONSTRAINT "FK_75a9d1c5495facf583614ca2b5f" FOREIGN KEY ("belongsToId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "lorawan-multicast" ADD CONSTRAINT "FK_bfb223faca94d9e217c39439c49" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "lorawan-multicast" ADD CONSTRAINT "FK_c776cdd3a4b1818c0995ee60baa" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "multicast" ADD CONSTRAINT "FK_60c29583cfbafbd6b0e1a9679c9" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "multicast" ADD CONSTRAINT "FK_0000d423c968ec8ba68482f8b0b" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "multicast" ADD CONSTRAINT "FK_ea6c184aa80e1f16cc8edb8e743" FOREIGN KEY ("applicationId") REFERENCES "application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "multicast" ADD CONSTRAINT "FK_5a058ff45da16000d4e3b409e36" FOREIGN KEY ("lorawanMulticastDefinitionId") REFERENCES "lorawan-multicast"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "iot_device" ADD CONSTRAINT "FK_36879bbabdd338403df3639ebf7" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "iot_device" ADD CONSTRAINT "FK_a15ceb5dd1d6c1eff4bccddc513" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "iot_device" ADD CONSTRAINT "FK_3e4f8964a797bd27b29a72a4718" FOREIGN KEY ("applicationId") REFERENCES "application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "iot_device" ADD CONSTRAINT "FK_bd1fee6c283fe1db22aeed6ac36" FOREIGN KEY ("deviceModelId") REFERENCES "device_model"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "iot_device_payload_decoder_data_target_connection" ADD CONSTRAINT "FK_e1e5066f7ec7093fe51b9a84eca" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "iot_device_payload_decoder_data_target_connection" ADD CONSTRAINT "FK_dfe8b280f6a586177f7f2badc2f" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "iot_device_payload_decoder_data_target_connection" ADD CONSTRAINT "FK_1ef58c35ae5562242966d483a16" FOREIGN KEY ("payloadDecoderId") REFERENCES "payload_decoder"("id") ON DELETE RESTRICT ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "iot_device_payload_decoder_data_target_connection" ADD CONSTRAINT "FK_7055bf87cc824d4d1268704818b" FOREIGN KEY ("dataTargetId") REFERENCES "data_target"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "data_target" ADD CONSTRAINT "FK_062e2d7b403e1d9736d3a26c86e" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "data_target" ADD CONSTRAINT "FK_aec53ab5541644651b38921719d" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "data_target" ADD CONSTRAINT "FK_dc5681eea8037a19634c4630057" FOREIGN KEY ("applicationId") REFERENCES "application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "application" ADD CONSTRAINT "FK_d7021375eb0ef5d648641b78886" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "application" ADD CONSTRAINT "FK_faa0fa6bed13e319c54ad6c4636" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "application" ADD CONSTRAINT "FK_7eb7ebc6eb5a830b9fb78030a8e" FOREIGN KEY ("belongsToId") REFERENCES "organization"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_permissions_permission" ADD CONSTRAINT "FK_5b72d197d92b8bafbe7906782ec" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_permissions_permission" ADD CONSTRAINT "FK_c43a6a56e3ef281cbfba9a77457" FOREIGN KEY ("permissionId") REFERENCES "permission"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "multicast_iot_devices_iot_device" ADD CONSTRAINT "FK_f502bc0bd3c1406ae2b3dc15628" FOREIGN KEY ("multicastId") REFERENCES "multicast"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "multicast_iot_devices_iot_device" ADD CONSTRAINT "FK_0c5a137689ddc88c8257e2cd46f" FOREIGN KEY ("iotDeviceId") REFERENCES "iot_device"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "iot_device_multicasts_multicast" ADD CONSTRAINT "FK_9b91f8a9dcc02c5926fced99e1b" FOREIGN KEY ("iotDeviceId") REFERENCES "iot_device"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "iot_device_multicasts_multicast" ADD CONSTRAINT "FK_d38b5d829712b4e0df2bae160f4" FOREIGN KEY ("multicastId") REFERENCES "multicast"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "iot_dev_pay_dec_dat_tar_con_iot_dev_iot_dev" ADD CONSTRAINT "FK_daf134b834f403ea98efa1fc09f" FOREIGN KEY ("iotDevicePayloadDecoderDataTargetConnectionId") REFERENCES "iot_device_payload_decoder_data_target_connection"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "iot_dev_pay_dec_dat_tar_con_iot_dev_iot_dev" ADD CONSTRAINT "FK_c88fdc6da057254fe8f9e155559" FOREIGN KEY ("iotDeviceId") REFERENCES "iot_device"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "application_permissions_permission" ADD CONSTRAINT "FK_6c691b1ba972915dc7bf3244204" FOREIGN KEY ("applicationId") REFERENCES "application"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "application_permissions_permission" ADD CONSTRAINT "FK_c1bbb34687ca84f2a166ee376e2" FOREIGN KEY ("permissionId") REFERENCES "permission"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "application_permissions_permission" DROP CONSTRAINT "FK_c1bbb34687ca84f2a166ee376e2"`); + await queryRunner.query(`ALTER TABLE "application_permissions_permission" DROP CONSTRAINT "FK_6c691b1ba972915dc7bf3244204"`); + await queryRunner.query(`ALTER TABLE "iot_dev_pay_dec_dat_tar_con_iot_dev_iot_dev" DROP CONSTRAINT "FK_c88fdc6da057254fe8f9e155559"`); + await queryRunner.query(`ALTER TABLE "iot_dev_pay_dec_dat_tar_con_iot_dev_iot_dev" DROP CONSTRAINT "FK_daf134b834f403ea98efa1fc09f"`); + await queryRunner.query(`ALTER TABLE "iot_device_multicasts_multicast" DROP CONSTRAINT "FK_d38b5d829712b4e0df2bae160f4"`); + await queryRunner.query(`ALTER TABLE "iot_device_multicasts_multicast" DROP CONSTRAINT "FK_9b91f8a9dcc02c5926fced99e1b"`); + await queryRunner.query(`ALTER TABLE "multicast_iot_devices_iot_device" DROP CONSTRAINT "FK_0c5a137689ddc88c8257e2cd46f"`); + await queryRunner.query(`ALTER TABLE "multicast_iot_devices_iot_device" DROP CONSTRAINT "FK_f502bc0bd3c1406ae2b3dc15628"`); + await queryRunner.query(`ALTER TABLE "user_permissions_permission" DROP CONSTRAINT "FK_c43a6a56e3ef281cbfba9a77457"`); + await queryRunner.query(`ALTER TABLE "user_permissions_permission" DROP CONSTRAINT "FK_5b72d197d92b8bafbe7906782ec"`); + await queryRunner.query(`ALTER TABLE "application" DROP CONSTRAINT "FK_7eb7ebc6eb5a830b9fb78030a8e"`); + await queryRunner.query(`ALTER TABLE "application" DROP CONSTRAINT "FK_faa0fa6bed13e319c54ad6c4636"`); + await queryRunner.query(`ALTER TABLE "application" DROP CONSTRAINT "FK_d7021375eb0ef5d648641b78886"`); + await queryRunner.query(`ALTER TABLE "data_target" DROP CONSTRAINT "FK_dc5681eea8037a19634c4630057"`); + await queryRunner.query(`ALTER TABLE "data_target" DROP CONSTRAINT "FK_aec53ab5541644651b38921719d"`); + await queryRunner.query(`ALTER TABLE "data_target" DROP CONSTRAINT "FK_062e2d7b403e1d9736d3a26c86e"`); + await queryRunner.query(`ALTER TABLE "iot_device_payload_decoder_data_target_connection" DROP CONSTRAINT "FK_7055bf87cc824d4d1268704818b"`); + await queryRunner.query(`ALTER TABLE "iot_device_payload_decoder_data_target_connection" DROP CONSTRAINT "FK_1ef58c35ae5562242966d483a16"`); + await queryRunner.query(`ALTER TABLE "iot_device_payload_decoder_data_target_connection" DROP CONSTRAINT "FK_dfe8b280f6a586177f7f2badc2f"`); + await queryRunner.query(`ALTER TABLE "iot_device_payload_decoder_data_target_connection" DROP CONSTRAINT "FK_e1e5066f7ec7093fe51b9a84eca"`); + await queryRunner.query(`ALTER TABLE "iot_device" DROP CONSTRAINT "FK_bd1fee6c283fe1db22aeed6ac36"`); + await queryRunner.query(`ALTER TABLE "iot_device" DROP CONSTRAINT "FK_3e4f8964a797bd27b29a72a4718"`); + await queryRunner.query(`ALTER TABLE "iot_device" DROP CONSTRAINT "FK_a15ceb5dd1d6c1eff4bccddc513"`); + await queryRunner.query(`ALTER TABLE "iot_device" DROP CONSTRAINT "FK_36879bbabdd338403df3639ebf7"`); + await queryRunner.query(`ALTER TABLE "multicast" DROP CONSTRAINT "FK_5a058ff45da16000d4e3b409e36"`); + await queryRunner.query(`ALTER TABLE "multicast" DROP CONSTRAINT "FK_ea6c184aa80e1f16cc8edb8e743"`); + await queryRunner.query(`ALTER TABLE "multicast" DROP CONSTRAINT "FK_0000d423c968ec8ba68482f8b0b"`); + await queryRunner.query(`ALTER TABLE "multicast" DROP CONSTRAINT "FK_60c29583cfbafbd6b0e1a9679c9"`); + await queryRunner.query(`ALTER TABLE "lorawan-multicast" DROP CONSTRAINT "FK_c776cdd3a4b1818c0995ee60baa"`); + await queryRunner.query(`ALTER TABLE "lorawan-multicast" DROP CONSTRAINT "FK_bfb223faca94d9e217c39439c49"`); + await queryRunner.query(`ALTER TABLE "device_model" DROP CONSTRAINT "FK_75a9d1c5495facf583614ca2b5f"`); + await queryRunner.query(`ALTER TABLE "device_model" DROP CONSTRAINT "FK_ed6727d1c4a7f9e6299ba6e2961"`); + await queryRunner.query(`ALTER TABLE "device_model" DROP CONSTRAINT "FK_9d26f066b617bbdac41d72e82b2"`); + await queryRunner.query(`ALTER TABLE "organization" DROP CONSTRAINT "FK_a7a6b96e8460aeb314599cf3580"`); + await queryRunner.query(`ALTER TABLE "organization" DROP CONSTRAINT "FK_acdbd1e490930af04b4ff569ca9"`); + await queryRunner.query(`ALTER TABLE "sigfox_group" DROP CONSTRAINT "FK_a38129f018eb963aea886de12e9"`); + await queryRunner.query(`ALTER TABLE "sigfox_group" DROP CONSTRAINT "FK_23d9047d051ab1ad8875306ae98"`); + await queryRunner.query(`ALTER TABLE "sigfox_group" DROP CONSTRAINT "FK_f5401745b31e0eedff2d219f77d"`); + await queryRunner.query(`ALTER TABLE "payload_decoder" DROP CONSTRAINT "FK_20c47c4f5719e0d398743401acd"`); + await queryRunner.query(`ALTER TABLE "payload_decoder" DROP CONSTRAINT "FK_ce6313b2fa3de41f2d4d9409003"`); + await queryRunner.query(`ALTER TABLE "payload_decoder" DROP CONSTRAINT "FK_33f98e074272079c02d792aa852"`); + await queryRunner.query(`ALTER TABLE "permission" DROP CONSTRAINT "FK_2102b10c8a5424189ac612ca8d9"`); + await queryRunner.query(`ALTER TABLE "permission" DROP CONSTRAINT "FK_40c4877af6e402a449d56af4d39"`); + await queryRunner.query(`ALTER TABLE "permission" DROP CONSTRAINT "FK_00e2c09abd157b5358faf3f43d0"`); + await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_db5173f7d27aa8a98a9fe6113df"`); + await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_45c0d39d1f9ceeb56942db93cc5"`); + await queryRunner.query(`ALTER TABLE "received_message_metadata" DROP CONSTRAINT "FK_de1719d0660b369de300440ad6f"`); + await queryRunner.query(`ALTER TABLE "received_message_metadata" DROP CONSTRAINT "FK_8399e14dc77145a32ccb755a84d"`); + await queryRunner.query(`ALTER TABLE "received_message_metadata" DROP CONSTRAINT "FK_4834b1a60043b2c443b07c08b17"`); + await queryRunner.query(`ALTER TABLE "received_message" DROP CONSTRAINT "FK_b0a8014e917afd2b1152c83dc10"`); + await queryRunner.query(`ALTER TABLE "received_message" DROP CONSTRAINT "FK_21ec54fded11c9d4ee8df3c17dd"`); + await queryRunner.query(`ALTER TABLE "received_message" DROP CONSTRAINT "FK_38b28c3a0241fcec40207e0745d"`); + await queryRunner.query(`ALTER TABLE "open_data_dk_dataset" DROP CONSTRAINT "FK_a0b7439e2a06cd0b67c4486a4f7"`); + await queryRunner.query(`ALTER TABLE "open_data_dk_dataset" DROP CONSTRAINT "FK_d5b350d2e5efe0229d8e01f7507"`); + await queryRunner.query(`ALTER TABLE "open_data_dk_dataset" DROP CONSTRAINT "FK_981af5bddacc8b7bf9f37247fb6"`); + await queryRunner.query(`DROP INDEX "IDX_c1bbb34687ca84f2a166ee376e"`); + await queryRunner.query(`DROP INDEX "IDX_6c691b1ba972915dc7bf324420"`); + await queryRunner.query(`DROP TABLE "application_permissions_permission"`); + await queryRunner.query(`DROP INDEX "IDX_c88fdc6da057254fe8f9e15555"`); + await queryRunner.query(`DROP INDEX "IDX_daf134b834f403ea98efa1fc09"`); + await queryRunner.query(`DROP TABLE "iot_dev_pay_dec_dat_tar_con_iot_dev_iot_dev"`); + await queryRunner.query(`DROP INDEX "IDX_d38b5d829712b4e0df2bae160f"`); + await queryRunner.query(`DROP INDEX "IDX_9b91f8a9dcc02c5926fced99e1"`); + await queryRunner.query(`DROP TABLE "iot_device_multicasts_multicast"`); + await queryRunner.query(`DROP INDEX "IDX_0c5a137689ddc88c8257e2cd46"`); + await queryRunner.query(`DROP INDEX "IDX_f502bc0bd3c1406ae2b3dc1562"`); + await queryRunner.query(`DROP TABLE "multicast_iot_devices_iot_device"`); + await queryRunner.query(`DROP INDEX "IDX_c43a6a56e3ef281cbfba9a7745"`); + await queryRunner.query(`DROP INDEX "IDX_5b72d197d92b8bafbe7906782e"`); + await queryRunner.query(`DROP TABLE "user_permissions_permission"`); + await queryRunner.query(`DROP TABLE "application"`); + await queryRunner.query(`DROP INDEX "IDX_af951594386c64a22f8fc057be"`); + await queryRunner.query(`DROP TABLE "data_target"`); + await queryRunner.query(`DROP TYPE "data_target_type_enum"`); + await queryRunner.query(`DROP INDEX "IDX_1ef58c35ae5562242966d483a1"`); + await queryRunner.query(`DROP INDEX "IDX_7055bf87cc824d4d1268704818"`); + await queryRunner.query(`DROP TABLE "iot_device_payload_decoder_data_target_connection"`); + await queryRunner.query(`DROP INDEX "IDX_18ce5007f4efcbe92338c10f5d"`); + await queryRunner.query(`DROP TABLE "iot_device"`); + await queryRunner.query(`DROP TYPE "iot_device_type_enum"`); + await queryRunner.query(`DROP TABLE "multicast"`); + await queryRunner.query(`DROP TABLE "lorawan-multicast"`); + await queryRunner.query(`DROP TABLE "device_model"`); + await queryRunner.query(`DROP TABLE "organization"`); + await queryRunner.query(`DROP TABLE "sigfox_group"`); + await queryRunner.query(`DROP TABLE "payload_decoder"`); + await queryRunner.query(`DROP INDEX "IDX_71bf2818fb2ad92e208d7aeadf"`); + await queryRunner.query(`DROP TABLE "permission"`); + await queryRunner.query(`DROP TYPE "permission_type_enum"`); + await queryRunner.query(`DROP TABLE "user"`); + await queryRunner.query(`DROP INDEX "IDX_4e710f4b84fd89dbbbab4616bf"`); + await queryRunner.query(`DROP TABLE "received_message_metadata"`); + await queryRunner.query(`DROP TABLE "received_message"`); + await queryRunner.query(`DROP TABLE "open_data_dk_dataset"`); + } + +} diff --git a/tsconfig.json b/tsconfig.json index c914c178..071893ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,5 +32,5 @@ "@resources/*": ["src/resources/*"], } }, - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/migration/*"] } From 80961760e4bb67a944ec28b35f74bfcd31f7db38 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Wed, 12 Jan 2022 16:53:57 +0100 Subject: [PATCH 04/11] Fix proper linting ignore of migrations --- .eslintignore | 1 + .eslintrc.js | 2 +- tsconfig.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.eslintignore b/.eslintignore index 537b7fae..b07d3d6a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,3 +4,4 @@ node_modules dist # don't lint nyc coverage output coverage +src/migration/ diff --git a/.eslintrc.js b/.eslintrc.js index 67b58f09..d3256d24 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,7 +27,7 @@ module.exports = { }, overrides: [ { - files: ["test/**/*.ts", "src/migration/*.ts"], + files: ["test/**/*.ts"], rules: { "max-lines-per-function": "off", }, diff --git a/tsconfig.json b/tsconfig.json index 071893ac..c914c178 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,5 +32,5 @@ "@resources/*": ["src/resources/*"], } }, - "exclude": ["node_modules", "dist", "src/migration/*"] + "exclude": ["node_modules", "dist"] } From b865ead43d0daefeaf54bf2f32a98392f10a3936 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Wed, 12 Jan 2022 17:10:42 +0100 Subject: [PATCH 05/11] Migrate existing permissions --- ormconfig.ts | 2 +- package.json | 2 +- .../admin-controller/multicast.controller.ts | 50 ++- src/helpers/security-helper.ts | 1 - .../1641898407691-RevisedPermissions.ts | 291 ++++++++++++++++++ 5 files changed, 332 insertions(+), 14 deletions(-) create mode 100644 src/migration/1641898407691-RevisedPermissions.ts diff --git a/ormconfig.ts b/ormconfig.ts index a3cd9fd4..25309016 100644 --- a/ormconfig.ts +++ b/ormconfig.ts @@ -7,7 +7,7 @@ export = { 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/package.json b/package.json index f82b39e6..58468162 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build": "nest build", "generate-migration": "npm run typeorm migration:generate -- -n", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "prestart": "npm run runMigrations", + "prestart": "npm run run-migrations", "prestart:debug": "npm run run-migrations", "prestart:dev": "npm run run-migrations", "prestart:prod": "npm run run-migrations", diff --git a/src/controllers/admin-controller/multicast.controller.ts b/src/controllers/admin-controller/multicast.controller.ts index f92fbc60..163a741a 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 { JwtAuthGuard } from "@auth/jwt-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/helpers/security-helper.ts b/src/helpers/security-helper.ts index 486d7cf6..ae1735c3 100644 --- a/src/helpers/security-helper.ts +++ b/src/helpers/security-helper.ts @@ -105,7 +105,6 @@ export function isOrganizationPermission(p: Permission): p is OrganizationPermis ].some(x => x === p.type); } -// TODO: Mark - NOT TESTED export function isOrganizationApplicationPermission(p: { type: PermissionType; }): p is OrganizationApplicationPermission { diff --git a/src/migration/1641898407691-RevisedPermissions.ts b/src/migration/1641898407691-RevisedPermissions.ts new file mode 100644 index 00000000..35645f23 --- /dev/null +++ b/src/migration/1641898407691-RevisedPermissions.ts @@ -0,0 +1,291 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +type AppPermissions = { + applicationId: number; + permissionId: number; +}[]; + +type PermissionInfo = { + id: number; + clonedFromId: number; +}; + +type UserPermissionInfo = PermissionInfo & { + userId?: number; +}; + +type AppPermissionInfo = PermissionInfo & { + applicationId?: number; +}; + +type UserPermissions = { + userId: number; + permissionId: number; +}[]; + +export class RevisedPermissions1641898407691 implements MigrationInterface { + name = "RevisedPermissions1641898407691"; + + 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', 'OrganizationPermission', 'OrganizationApplicationPermissions')` + ); + + // 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( + `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum" USING "type"::"text"::"permission_type_enum"` + ); + await queryRunner.query(`DROP TYPE "permission_type_enum_old"`); + await queryRunner.query(`COMMENT ON COLUMN "permission"."type" IS NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`COMMENT ON COLUMN "permission"."type" IS NULL`); + await queryRunner.query( + `CREATE TYPE "permission_type_enum_old" AS ENUM('GlobalAdmin', 'OrganizationAdmin', 'Write', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions')` + ); + + // 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 "permission_type_enum"`); + await queryRunner.query( + `ALTER TYPE "permission_type_enum_old" RENAME TO "permission_type_enum"` + ); + } + + private async migrateUp(queryRunner: QueryRunner): Promise { + // Create a temporary enum which is a union of both old and new enum values + await queryRunner.query( + `CREATE TYPE "permission_type_enum_temp" AS ENUM('OrganizationAdmin', 'Write', 'GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions')` + ); + await queryRunner.query( + `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_temp" USING "type"::"text"::"permission_type_enum_temp"` + ); + + // 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 + ); + + // Cleanup + await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "clonedFromId"`); + 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 "permission_type_enum_temp"`); + } + + 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)), + [] + ); + + // User-permission table + await queryRunner.query(this.copyAppPermissionsQuery(newAppPermissions)); + } + + 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 match = userPermissions.find(p => p.permissionId === info.clonedFromId); + return match ? { ...info, userId: match.userId } : info; + }); + return mappedInfos.filter(info => typeof info.userId === "number"); + } + + private mapAppPermissions( + appPermissions: AppPermissions, + infos: AppPermissionInfo[] + ): PermissionInfo[] { + const mappedInfos = infos.map(info => { + const match = appPermissions.find(p => p.permissionId === info.clonedFromId); + return match ? { ...info, applicationId: match.applicationId } : info; + }); + return mappedInfos.filter(info => typeof info.applicationId === "number"); + } + + private copyUserPermissionsQuery(infos: UserPermissionInfo[]): string { + if (!infos.length) return ""; + + const insertIntoStatements = infos + .map(info => `(${info.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.applicationId}, ${info.id})`) + .join(","); + return `INSERT INTO "public"."application_permissions_permission"("applicationId","permissionId") VALUES + ${insertIntoStatements}`; + } + + private async migrateDown(queryRunner: QueryRunner): Promise { + // Create a temporary enum which is a union of both old and new enum values + await queryRunner.query( + `CREATE TYPE "permission_type_enum_temp" AS ENUM('OrganizationAdmin', 'Write', 'GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions')` + ); + await queryRunner.query( + `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_temp" USING "type"::"text"::"permission_type_enum_temp"` + ); + + // 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 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 + ); + + // Cleanup + await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "clonedFromId"`); + 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 "permission_type_enum_temp"`); + } +} From 4a4e07e7e02df19b37dcf9478b621de2b0e57f7e Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Mon, 31 Jan 2022 15:32:04 +0100 Subject: [PATCH 06/11] Update organization controller role --- .../user-management/organization.controller.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/controllers/user-management/organization.controller.ts b/src/controllers/user-management/organization.controller.ts index 2baac17c..a3feb375 100644 --- a/src/controllers/user-management/organization.controller.ts +++ b/src/controllers/user-management/organization.controller.ts @@ -35,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 { checkIfUserHasAccessToOrganization, OrganizationAccessScope } 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"; @@ -47,14 +46,13 @@ import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; @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" }) - @UserAdmin() async create( @Req() req: AuthenticatedRequest, @Body() createOrganizationDto: CreateOrganizationDto @@ -81,15 +79,12 @@ export class OrganizationController { @Put(":id") @ApiOperation({ summary: "Update an Organization" }) @ApiNotFoundResponse() - @UserAdmin() async update( @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number, @Body() updateOrganizationDto: UpdateOrganizationDto ): Promise { try { - checkIfUserHasAccessToOrganization(req, id, OrganizationAccessScope.UserAdministrationWrite); - const organization = await this.organizationService.update( id, updateOrganizationDto, @@ -124,6 +119,7 @@ export class OrganizationController { @Get() @ApiOperation({ summary: "Get list of all Organizations" }) + @UserAdmin() async findAll( @Req() req: AuthenticatedRequest, @Query() query?: ListAllEntitiesDto @@ -142,11 +138,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 { - checkIfUserHasAccessToOrganization(req, id, OrganizationAccessScope.UserAdministrationWrite); try { return await this.organizationService.findByIdWithRelations(id); } catch (err) { @@ -157,14 +153,11 @@ export class OrganizationController { @Delete(":id") @ApiOperation({ summary: "Delete an Organization" }) @ApiNotFoundResponse() - @UserAdmin() async delete( @Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number ): Promise { try { - checkIfUserHasAccessToOrganization(req, id, OrganizationAccessScope.UserAdministrationWrite); - const result = await this.organizationService.delete(id); AuditLog.success(ActionType.DELETE, Organization.name, req.user.userId, id); From a067f21609bc2af7bd66c1186a07a2ebccb6667e Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Tue, 22 Feb 2022 16:05:19 +0100 Subject: [PATCH 07/11] Fix merge issues --- .../admin-controller/iot-device.controller.ts | 2 +- src/helpers/iot-device.helper.ts | 17 ++++++++++++++--- ....ts => 1645536193420-revised-permissions.ts} | 4 ++-- 3 files changed, 17 insertions(+), 6 deletions(-) rename src/migration/{1643627475551-RevisedPermissions.ts => 1645536193420-revised-permissions.ts} (99%) diff --git a/src/controllers/admin-controller/iot-device.controller.ts b/src/controllers/admin-controller/iot-device.controller.ts index 498519dc..a59c0089 100644 --- a/src/controllers/admin-controller/iot-device.controller.ts +++ b/src/controllers/admin-controller/iot-device.controller.ts @@ -234,7 +234,7 @@ export class IoTDeviceController { ): Promise { try { - createDto.data.forEach(createDto => checkIfUserHasWriteAccessToApplication(req, createDto.applicationId)); + createDto.data.forEach(createDto => checkIfUserHasAccessToApplication(req, createDto.applicationId, ApplicationAccessScope.Write)); const devices = await this.iotDeviceService.createMany( createDto.data, req.user.userId 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/migration/1643627475551-RevisedPermissions.ts b/src/migration/1645536193420-revised-permissions.ts similarity index 99% rename from src/migration/1643627475551-RevisedPermissions.ts rename to src/migration/1645536193420-revised-permissions.ts index 531cc6d7..0d20ddd4 100644 --- a/src/migration/1643627475551-RevisedPermissions.ts +++ b/src/migration/1645536193420-revised-permissions.ts @@ -23,8 +23,8 @@ type UserPermissions = { permissionId: number; }[]; -export class RevisedPermissions1643627475551 implements MigrationInterface { - name = "RevisedPermissions1643627475551"; +export class revisedPermissions1645536193420 implements MigrationInterface { + name = "revisedPermissions1645536193420"; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( From 84f68b83bc7810ffbb8d28cdbccc0f35818d2cd5 Mon Sep 17 00:00:00 2001 From: nlg Date: Thu, 28 Apr 2022 14:45:57 +0200 Subject: [PATCH 08/11] Migrations build --- .../1651142158492-revised-permissions.ts | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 src/migration/1651142158492-revised-permissions.ts diff --git a/src/migration/1651142158492-revised-permissions.ts b/src/migration/1651142158492-revised-permissions.ts new file mode 100644 index 00000000..a385c449 --- /dev/null +++ b/src/migration/1651142158492-revised-permissions.ts @@ -0,0 +1,292 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +type AppPermissions = { + applicationId: number; + permissionId: number; +}[]; + +type PermissionInfo = { + id: number; + clonedFromId: number; +}; + +type UserPermissionInfo = PermissionInfo & { + userId?: number; +}; + +type AppPermissionInfo = PermissionInfo & { + applicationId?: number; +}; + +type UserPermissions = { + userId: number; + permissionId: number; +}[]; + +export class revisedPermissions1651142158492 implements MigrationInterface { + name = "revisedPermissions1651142158492"; + + 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', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` + ); + + // 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( + // `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum" USING "type"::"text"::"permission_type_enum"` + // ); + await queryRunner.query(`DROP TYPE "permission_type_enum_old"`); + await queryRunner.query(`COMMENT ON COLUMN "permission"."type" IS NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`COMMENT ON COLUMN "permission"."type" IS NULL`); + await queryRunner.query( + `CREATE TYPE "permission_type_enum_old" AS ENUM('GlobalAdmin', 'OrganizationAdmin', 'Write', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` + ); + + // 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 "permission_type_enum"`); + await queryRunner.query( + `ALTER TYPE "permission_type_enum_old" RENAME TO "permission_type_enum"` + ); + } + + private async migrateUp(queryRunner: QueryRunner): Promise { + // Create a temporary enum which is a union of both old and new enum values + await queryRunner.query( + `CREATE TYPE "permission_type_enum_temp" AS ENUM('OrganizationAdmin', 'Write', 'GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` + ); + await queryRunner.query( + `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_temp" USING "type"::"text"::"permission_type_enum_temp"` + ); + + // 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 + ); + + // Cleanup + await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "clonedFromId"`); + + // 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 "permission_type_enum_temp"`); + } + + 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)), + [] + ); + + // User-permission table + await queryRunner.query(this.copyAppPermissionsQuery(newAppPermissions)); + } + + 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 match = userPermissions.find(p => p.permissionId === info.clonedFromId); + return match ? { ...info, userId: match.userId } : info; + }); + return mappedInfos.filter(info => typeof info.userId === "number"); + } + + private mapAppPermissions( + appPermissions: AppPermissions, + infos: AppPermissionInfo[] + ): PermissionInfo[] { + const mappedInfos = infos.map(info => { + const match = appPermissions.find(p => p.permissionId === info.clonedFromId); + return match ? { ...info, applicationId: match.applicationId } : info; + }); + return mappedInfos.filter(info => typeof info.applicationId === "number"); + } + + private copyUserPermissionsQuery(infos: UserPermissionInfo[]): string { + if (!infos.length) return ""; + + const insertIntoStatements = infos + .map(info => `(${info.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.applicationId}, ${info.id})`) + .join(","); + return `INSERT INTO "public"."application_permissions_permission"("applicationId","permissionId") VALUES + ${insertIntoStatements}`; + } + + private async migrateDown(queryRunner: QueryRunner): Promise { + // Create a temporary enum which is a union of both old and new enum values + await queryRunner.query( + `CREATE TYPE "permission_type_enum_temp" AS ENUM('OrganizationAdmin', 'Write', 'GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` + ); + await queryRunner.query( + `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_temp" USING "type"::"text"::"permission_type_enum_temp"` + ); + + // 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 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 + ); + + // Cleanup + await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "clonedFromId"`); + 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 "permission_type_enum_temp"`); + } +} From 751eaa688cd2c19d3fbe722070b003ec425e40eb Mon Sep 17 00:00:00 2001 From: nlg Date: Wed, 4 May 2022 09:41:45 +0200 Subject: [PATCH 09/11] Modified Migration --- .../1645536193420-revised-permissions.ts | 291 ------------------ .../1651142158492-revised-permissions.ts | 38 ++- 2 files changed, 31 insertions(+), 298 deletions(-) delete mode 100644 src/migration/1645536193420-revised-permissions.ts diff --git a/src/migration/1645536193420-revised-permissions.ts b/src/migration/1645536193420-revised-permissions.ts deleted file mode 100644 index 81d93103..00000000 --- a/src/migration/1645536193420-revised-permissions.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -type AppPermissions = { - applicationId: number; - permissionId: number; -}[]; - -type PermissionInfo = { - id: number; - clonedFromId: number; -}; - -type UserPermissionInfo = PermissionInfo & { - userId?: number; -}; - -type AppPermissionInfo = PermissionInfo & { - applicationId?: number; -}; - -type UserPermissions = { - userId: number; - permissionId: number; -}[]; - -export class revisedPermissions1645536193420 implements MigrationInterface { - name = "revisedPermissions1645536193420"; - - 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', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` - ); - - // 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( - `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum" USING "type"::"text"::"permission_type_enum"` - ); - await queryRunner.query(`DROP TYPE "permission_type_enum_old"`); - await queryRunner.query(`COMMENT ON COLUMN "permission"."type" IS NULL`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`COMMENT ON COLUMN "permission"."type" IS NULL`); - await queryRunner.query( - `CREATE TYPE "permission_type_enum_old" AS ENUM('GlobalAdmin', 'OrganizationAdmin', 'Write', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` - ); - - // 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 "permission_type_enum"`); - await queryRunner.query( - `ALTER TYPE "permission_type_enum_old" RENAME TO "permission_type_enum"` - ); - } - - private async migrateUp(queryRunner: QueryRunner): Promise { - // Create a temporary enum which is a union of both old and new enum values - await queryRunner.query( - `CREATE TYPE "permission_type_enum_temp" AS ENUM('OrganizationAdmin', 'Write', 'GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` - ); - await queryRunner.query( - `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_temp" USING "type"::"text"::"permission_type_enum_temp"` - ); - - // 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 - ); - - // Cleanup - await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "clonedFromId"`); - 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 "permission_type_enum_temp"`); - } - - 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)), - [] - ); - - // User-permission table - await queryRunner.query(this.copyAppPermissionsQuery(newAppPermissions)); - } - - 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 match = userPermissions.find(p => p.permissionId === info.clonedFromId); - return match ? { ...info, userId: match.userId } : info; - }); - return mappedInfos.filter(info => typeof info.userId === "number"); - } - - private mapAppPermissions( - appPermissions: AppPermissions, - infos: AppPermissionInfo[] - ): PermissionInfo[] { - const mappedInfos = infos.map(info => { - const match = appPermissions.find(p => p.permissionId === info.clonedFromId); - return match ? { ...info, applicationId: match.applicationId } : info; - }); - return mappedInfos.filter(info => typeof info.applicationId === "number"); - } - - private copyUserPermissionsQuery(infos: UserPermissionInfo[]): string { - if (!infos.length) return ""; - - const insertIntoStatements = infos - .map(info => `(${info.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.applicationId}, ${info.id})`) - .join(","); - return `INSERT INTO "public"."application_permissions_permission"("applicationId","permissionId") VALUES - ${insertIntoStatements}`; - } - - private async migrateDown(queryRunner: QueryRunner): Promise { - // Create a temporary enum which is a union of both old and new enum values - await queryRunner.query( - `CREATE TYPE "permission_type_enum_temp" AS ENUM('OrganizationAdmin', 'Write', 'GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` - ); - await queryRunner.query( - `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_temp" USING "type"::"text"::"permission_type_enum_temp"` - ); - - // 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 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 - ); - - // Cleanup - await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "clonedFromId"`); - 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 "permission_type_enum_temp"`); - } -} diff --git a/src/migration/1651142158492-revised-permissions.ts b/src/migration/1651142158492-revised-permissions.ts index a385c449..b7713225 100644 --- a/src/migration/1651142158492-revised-permissions.ts +++ b/src/migration/1651142158492-revised-permissions.ts @@ -123,14 +123,38 @@ export class revisedPermissions1651142158492 implements MigrationInterface { // Cleanup await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "clonedFromId"`); + + 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 ('Write', 'OrganizationAdmin') +);`); + + 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 ('Write', 'OrganizationAdmin') +)`); + + 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 ('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 "permission_type_enum_temp"`); + 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 "permission_type_enum_temp"`); } private async migrateUserPermissions( From 0c1cfad29983bd889b3ce123984efa6e60315d60 Mon Sep 17 00:00:00 2001 From: nlg Date: Wed, 11 May 2022 08:37:11 +0200 Subject: [PATCH 10/11] Fixed permission migration and the organizationID check --- .../device-model.controller.ts | 2 +- src/helpers/security-helper.ts | 2 +- .../1651142158492-revised-permissions.ts | 64 ++++++++++++++++++- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/controllers/admin-controller/device-model.controller.ts b/src/controllers/admin-controller/device-model.controller.ts index 57ca0720..782b1c4e 100644 --- a/src/controllers/admin-controller/device-model.controller.ts +++ b/src/controllers/admin-controller/device-model.controller.ts @@ -54,7 +54,7 @@ export class DeviceModelController { @Req() req: AuthenticatedRequest, @Query() query?: ListAllDeviceModelsDto ): Promise { - if (query?.organizationId != null) { + if (query?.organizationId != null) { checkIfUserHasAccessToOrganization(req, query?.organizationId, OrganizationAccessScope.ApplicationRead); return this.service.getAllDeviceModelsByOrgIds( [query?.organizationId], diff --git a/src/helpers/security-helper.ts b/src/helpers/security-helper.ts index ae1735c3..26b61eed 100644 --- a/src/helpers/security-helper.ts +++ b/src/helpers/security-helper.ts @@ -24,7 +24,7 @@ export function checkIfUserHasAccessToOrganization( organizationId: number, scope: OrganizationAccessScope ): void { - if (!Number.isInteger(organizationId)) return; + if (!Number.isInteger(+organizationId)) return; let allowedOrganizations: number[] = []; diff --git a/src/migration/1651142158492-revised-permissions.ts b/src/migration/1651142158492-revised-permissions.ts index b7713225..f07b10f5 100644 --- a/src/migration/1651142158492-revised-permissions.ts +++ b/src/migration/1651142158492-revised-permissions.ts @@ -5,6 +5,11 @@ type AppPermissions = { permissionId: number; }[]; +type ApiKeyPermissions = { + apiKeyId: number; + permissionId: number; +}[]; + type PermissionInfo = { id: number; clonedFromId: number; @@ -18,6 +23,10 @@ type AppPermissionInfo = PermissionInfo & { applicationId?: number; }; +type ApiKeyPermissionInfo = PermissionInfo & { + apiKeyId?: number; +} + type UserPermissions = { userId: number; permissionId: number; @@ -101,7 +110,7 @@ export class revisedPermissions1651142158492 implements MigrationInterface { ); // Migrate entities in other tables with a foreign key to the permission table - await this.migrateUserPermissions( + await this.migrateUserPermissions ( queryRunner, applicationAdminFromWriteInfo, readFromWriteInfo, @@ -111,7 +120,17 @@ export class revisedPermissions1651142158492 implements MigrationInterface { readAdminFromOrgAdminInfo ); - await this.migrateApplicationPermissions( + await this.migrateApplicationPermissions ( + queryRunner, + applicationAdminFromWriteInfo, + readFromWriteInfo, + userAdminFromOrgAdminInfo, + applicationAdminFromOrgAdminInfo, + gatewayAdminFromOrgAdminInfo, + readAdminFromOrgAdminInfo + ); + + await this.migrateApiKeyPermissions ( queryRunner, applicationAdminFromWriteInfo, readFromWriteInfo, @@ -190,10 +209,28 @@ from public.application_permissions_permission`); [] ); - // User-permission table + // 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", @@ -236,6 +273,17 @@ returning id, "permission"."clonedFromId"`; return mappedInfos.filter(info => typeof info.applicationId === "number"); } + private mapApiKeyPermissions( + apiKeyPermissions: ApiKeyPermissions, + infos: ApiKeyPermissionInfo[] + ): PermissionInfo[] { + const mappedInfos = infos.map(info => { + const match = apiKeyPermissions.find(p => p.permissionId === info.clonedFromId); + return match ? { ...info, apiKeyId: match.apiKeyId } : info; + }); + return mappedInfos.filter(info => typeof info.apiKeyId === "number"); + } + private copyUserPermissionsQuery(infos: UserPermissionInfo[]): string { if (!infos.length) return ""; @@ -256,6 +304,16 @@ returning id, "permission"."clonedFromId"`; ${insertIntoStatements}`; } + private copyApiKeyPermissionsQuery(infos: ApiKeyPermissionInfo[]): string { + if (!infos.length) return ""; + + const insertIntoStatements = infos + .map(info => `(${info.apiKeyId}, ${info.id})`) + .join(","); + return `INSERT INTO "public"."api_key_permissions_permission"("apiKeyId","permissionId") VALUES + ${insertIntoStatements}`; + } + private async migrateDown(queryRunner: QueryRunner): Promise { // Create a temporary enum which is a union of both old and new enum values await queryRunner.query( From 2814b8f941a46920ab2c1fb9ec43ca5b5453f169 Mon Sep 17 00:00:00 2001 From: AramAlsabti <92869496+AramAlsabti@users.noreply.github.com> Date: Thu, 19 May 2022 14:07:42 +0200 Subject: [PATCH 11/11] New user management multiple levels (#174) * Add flow for hiding new welcome screen (#172) * Merge permission entities into one. Support multiple levels per permission * Fix some test errors * Feature/iot 1320 fix sig fox connection (#165) * Fixes issues with communication with the SigFox API * Better description for SigFox controller endpoint Co-authored-by: nlg * Feature/iot 1249 manage kombit users merged (#168) * Made it possible to get all organizations without other permissions that you have to be logged in. Nessesary since a new user should choose which organizations the user wishes to be a part of. * Ready for put setEmail * set email for user * When logging in to kombit user, the entered email and organizations is now saved in db. * changed to ManyToMany between user and organizations. Made a migration * Possible to update the organizations that the user applies. * Changed names so it's more clear that the organizations on the user is requested organizations. Changed the migration. * Made mail work with mail test server. It's possible to send a mail. Only org admins should get the mail, and if no org admins then global admin. Need to take care of exception in user.service * Changed user name to awaiting users. Made api call to get awaiting users. * Made backend for kombit. Sends mail at verification and rejection. Has to be changed to environment variables. Check all TODOS before merging * changed migration to match with stage * Getting ready for OAuth2 mail system. We need to generate refreshtoken ourselves. Made TODO::: for places that needs to do. * Service is now looking at environment variables. Currently set to Ethereal Email server for test. * Changed default email values to mailgun * Made a new controller for new kombit users so endpoints is allowed without permissions. Made map functions * Now possible to get permissions onto an organization * updated to correct and latest migration * Renamed migration * Added frontend.baseurl environment property Changed UserRejectDTO to also have id of the user to reject Minor code quality changes Co-authored-by: August Andersen Co-authored-by: nlg * Bumped momemnt version one minor version * Fixed package.lock * Fixed casing of Kombit migration * Finish permission levels migration * Deny duplicate permission types * Constrain permission type to enum * Removed unused permission types * Fix kombit permissions * Show overview of gateway status (#170) * Init kafka for online status service * Subscribe to gateway connection state Migration, data storage and error handling are missing. * Init get gateway status * Refactor gateway status * Implement gateway status fetch. Missing save in db * Store gateway status messages. Cleanup * Organization id is optional * Re-timestamped migration to to make it fit in the migration timeline * Minor renaming * Bump migration timestamp * Fetch status for single gateway (#171) Co-authored-by: nlg * Update accept kombit to use group instead of level * Remove permission check from gateway status * Cleanup permission relations on down * Fix multiple permission relations not mapped to the same new permissions * Fix global admin not created on startup Co-authored-by: Nikolaj Gustafsson Co-authored-by: nlg Co-authored-by: augusthjerrild <70511721+augusthjerrild@users.noreply.github.com> Co-authored-by: August Andersen --- package-lock.json | 104 ++++++++- package.json | 3 + resources/chirpstack-state.proto | 16 ++ src/config/configuration.ts | 13 +- .../lorawan/lorawan-gateway.controller.ts | 50 ++++ .../sigfox/sigfox-group.controller.ts | 4 +- .../receive-data.controller.ts | 2 +- .../sigfox-listener.controller.ts | 2 +- .../user-management/auth.controller.ts | 1 - .../new-kombit-creation.controller.ts | 177 ++++++++++++++ .../organization.controller.ts | 5 +- .../user-management/permission.controller.ts | 91 ++++++-- .../user-management/user.controller.ts | 56 ++++- src/entities/api-key-permission.entity.ts | 10 - src/entities/api-key.entity.ts | 6 +- src/entities/application.entity.ts | 8 +- .../backend/gateway-all-status.dto.ts | 22 ++ .../chirpstack/backend/gateway-status.dto.ts | 23 ++ .../chirpstack/chirpstack-mqtt-message.dto.ts | 5 + .../chirpstack-mqtt-state-message.dto.ts | 4 + .../dto/kafka/raw-gateway-state.dto.ts | 5 + .../dto/kafka/raw-iot-device-request.dto.ts | 5 + src/entities/dto/kafka/raw-request.dto.ts | 1 - src/entities/dto/permission-minimal.dto.ts | 2 +- .../add-user-to-permission.dto.ts | 18 ++ .../create-new-kombit-user.dto.ts | 13 ++ .../user-management/create-permission.dto.ts | 17 +- .../user-management/permission-type.dto.ts | 7 + .../dto/user-management/reject-user.dto.ts | 12 + .../user-management/update-user-orgs.dto.ts | 9 + src/entities/enum/error-codes.enum.ts | 5 + .../enum/gateway-status-interval.enum.ts | 20 ++ src/entities/enum/kafka-topic.enum.ts | 1 + src/entities/enum/permission-type.enum.ts | 4 - src/entities/gateway-status-history.entity.ts | 16 ++ src/entities/lorawan-multicast.entity.ts | 2 +- src/entities/organization.entity.ts | 11 +- .../global-admin-permission.entity.ts | 12 - ...ion-application-admin-permission.entity.ts | 13 -- ...anization-application-permission.entity.ts | 21 -- ...ization-gateway-admin-permission.entity.ts | 11 - .../organization-permission.entity.ts | 17 -- ...ganization-user-admin-permission.entity.ts | 11 - .../permissions/permission-type.entity.ts | 21 ++ src/entities/permissions/permission.entity.ts | 45 +++- .../permissions/read-permission.entity.ts | 13 -- src/entities/user.entity.ts | 17 ++ src/helpers/array-distinct.validator.ts | 41 ++++ src/helpers/permission.helper.ts | 82 +++++++ src/helpers/security-helper.ts | 27 ++- src/migration/1652342361070-welcome-screen.ts | 14 ++ .../1652771064000-KombitUserManagement.ts | 24 ++ .../1652861431000-gateway-status-history.ts | 18 ++ ...s => 1652951529000-revised-permissions.ts} | 218 +++++++++++++----- src/modules/app.module.ts | 2 + .../gateway-persistence.module.ts | 17 ++ .../data-target/data-target-kafka.module.ts | 2 + .../lorawan-gateway.module.ts | 18 ++ src/modules/shared.module.ts | 23 +- .../new-kombit-creation.module.ts | 13 ++ .../user-management/organization.module.ts | 3 +- src/modules/user-management/user.module.ts | 5 +- src/resources/resource-paths.ts | 8 + .../api-key-management/api-key.service.ts | 3 +- .../chirpstack/chirpstack-gateway.service.ts | 7 +- .../gateway-status-history.service.ts | 103 +++++++++ .../chirpstack-mqtt-listener.service.ts | 69 +++++- .../device-integration-persistence.service.ts | 18 +- .../gateway-persistence.service.ts | 124 ++++++++++ .../payload-decoder-listener.service.ts | 17 +- .../data-management/receive-data.service.ts | 60 +++-- .../data-target-kafka-listener.service.ts | 2 +- src/services/kafka/kafka.message.ts | 2 +- .../generic-sigfox-administation.service.ts | 2 +- .../user-management/organization.service.ts | 109 ++++++++- .../user-management/permission.service.ts | 187 +++++++++------ src/services/user-management/user.service.ts | 188 ++++++++++++++- test/e2e/crud/application.e2e-spec.ts | 8 +- test/e2e/crud/data-target.e2e-spec.ts | 8 + test/e2e/crud/permission.e2e-spec.ts | 4 +- test/e2e/crud/search.e2e-spec.ts | 2 +- test/e2e/test-helpers.ts | 10 +- ...ce-integration-persistence.service.spec.ts | 3 + 83 files changed, 1961 insertions(+), 411 deletions(-) create mode 100644 resources/chirpstack-state.proto create mode 100644 src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts create mode 100644 src/controllers/user-management/new-kombit-creation.controller.ts delete mode 100644 src/entities/api-key-permission.entity.ts create mode 100644 src/entities/dto/chirpstack/backend/gateway-all-status.dto.ts create mode 100644 src/entities/dto/chirpstack/backend/gateway-status.dto.ts create mode 100644 src/entities/dto/chirpstack/state/chirpstack-mqtt-state-message.dto.ts create mode 100644 src/entities/dto/kafka/raw-gateway-state.dto.ts create mode 100644 src/entities/dto/kafka/raw-iot-device-request.dto.ts create mode 100644 src/entities/dto/user-management/add-user-to-permission.dto.ts create mode 100644 src/entities/dto/user-management/create-new-kombit-user.dto.ts create mode 100644 src/entities/dto/user-management/permission-type.dto.ts create mode 100644 src/entities/dto/user-management/reject-user.dto.ts create mode 100644 src/entities/dto/user-management/update-user-orgs.dto.ts create mode 100644 src/entities/enum/gateway-status-interval.enum.ts create mode 100644 src/entities/gateway-status-history.entity.ts delete mode 100644 src/entities/permissions/global-admin-permission.entity.ts delete mode 100644 src/entities/permissions/organization-application-admin-permission.entity.ts delete mode 100644 src/entities/permissions/organization-application-permission.entity.ts delete mode 100644 src/entities/permissions/organization-gateway-admin-permission.entity.ts delete mode 100644 src/entities/permissions/organization-permission.entity.ts delete mode 100644 src/entities/permissions/organization-user-admin-permission.entity.ts create mode 100644 src/entities/permissions/permission-type.entity.ts delete mode 100644 src/entities/permissions/read-permission.entity.ts create mode 100644 src/helpers/array-distinct.validator.ts create mode 100644 src/helpers/permission.helper.ts create mode 100644 src/migration/1652342361070-welcome-screen.ts create mode 100644 src/migration/1652771064000-KombitUserManagement.ts create mode 100644 src/migration/1652861431000-gateway-status-history.ts rename src/migration/{1651142158492-revised-permissions.ts => 1652951529000-revised-permissions.ts} (61%) create mode 100644 src/modules/data-management/gateway-persistence.module.ts create mode 100644 src/modules/device-integrations/lorawan-gateway.module.ts create mode 100644 src/modules/user-management/new-kombit-creation.module.ts create mode 100644 src/resources/resource-paths.ts create mode 100644 src/services/chirpstack/gateway-status-history.service.ts create mode 100644 src/services/data-management/gateway-persistence.service.ts diff --git a/package-lock.json b/package-lock.json index af9809bf..e70f2fa4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1486,6 +1486,60 @@ } } }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + }, "@schematics/schematics": { "version": "0.1102.6", "resolved": "https://registry.npmjs.org/@schematics/schematics/-/schematics-0.1102.6.tgz", @@ -1757,6 +1811,11 @@ "integrity": "sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg==", "dev": true }, + "@types/long": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + }, "@types/mime": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", @@ -1767,6 +1826,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.14.tgz", "integrity": "sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ==" }, + "@types/nodemailer": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.4.tgz", + "integrity": "sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -7381,6 +7449,11 @@ } } }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -7642,9 +7715,9 @@ } }, "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + "version": "2.29.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", + "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==" }, "moment-timezone": { "version": "0.5.31", @@ -7933,6 +8006,11 @@ "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", "dev": true }, + "nodemailer": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.2.tgz", + "integrity": "sha512-Dz7zVwlef4k5R71fdmxwR8Q39fiboGbu3xgswkzGwczUfjp873rVxt1O46+Fh0j1ORnAC6L9+heI8uUpO6DT7Q==" + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -8581,6 +8659,26 @@ "sisteransi": "^1.0.5" } }, + "protobufjs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", + "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + } + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", diff --git a/package.json b/package.json index e7c4ccfb..22e33b1b 100644 --- a/package.json +++ b/package.json @@ -58,12 +58,14 @@ "mqtt": "^4.2.6", "nestjs-pino": "^1.3.0", "njwt": "^1.0.0", + "nodemailer": "^6.7.2", "passport": "^0.4.1", "passport-headerapikey": "^1.2.2", "passport-jwt": "^4.0.0", "passport-local": "^1.0.0", "passport-saml": "^1.3.5", "pg": "^8.5.1", + "protobufjs": "^6.11.2", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^6.6.3", @@ -83,6 +85,7 @@ "@types/express": "^4.17.9", "@types/lodash": "^4.14.165", "@types/node": "^14.14.14", + "@types/nodemailer": "^6.4.4", "@types/passport-jwt": "^3.0.3", "@types/passport-local": "^1.0.33", "@types/supertest": "^2.0.10", diff --git a/resources/chirpstack-state.proto b/resources/chirpstack-state.proto new file mode 100644 index 00000000..1befa749 --- /dev/null +++ b/resources/chirpstack-state.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package gw; + +// ConnState contains the connection state of a gateway. +message ConnState { + // Gateway ID. + bytes gateway_id = 1 [json_name = "gatewayID"]; + + enum State { + OFFLINE = 0; + ONLINE = 1; + } + + State state = 2; +} diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 2a601ac0..735d110c 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -35,5 +35,16 @@ export default (): any => { logLevels: process.env.LOG_LEVEL ? GetLogLevels(process.env.LOG_LEVEL) : GetLogLevels("debug"), - }; + email: { + host: process.env.EMAIL_HOST || "smtp.ethereal.email", + port: process.env.EMAIL_PORT || 587, + user: process.env.EMAIL_USER || "ara.kertzmann8@ethereal.email", + pass: process.env.EMAIL_PASS || "KzRSyYReEygpFPPZdd", + from: process.env.EMAIL_FROM || "ara.kertzmann8@ethereal.email" + }, + frontend: { + baseurl: + process.env.FRONTEND_BASEURL || "http://localhost:8081" + } + }; }; diff --git a/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts b/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts new file mode 100644 index 00000000..c5ca309a --- /dev/null +++ b/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts @@ -0,0 +1,50 @@ +import { ComposeAuthGuard } from "@auth/compose-auth.guard"; +import { Read } from "@auth/roles.decorator"; +import { RolesGuard } from "@auth/roles.guard"; +import { + GatewayGetAllStatusResponseDto, + ListAllGatewayStatusDto, +} 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 { 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"; +import { GatewayStatusHistoryService } from "@services/chirpstack/gateway-status-history.service"; +import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; + +@ApiTags("LoRaWAN gateway") +@Controller("lorawan/gateway") +@UseGuards(ComposeAuthGuard, RolesGuard) +@ApiBearerAuth() +export class LoRaWANGatewayController { + constructor( + private onlineHistoryService: GatewayStatusHistoryService, + private chirpstackGatewayService: ChirpstackGatewayService + ) {} + + @Get("/status") + @ApiProduces("application/json") + @ApiOperation({ summary: "Get the status for all LoRaWAN gateways" }) + @Read() + async getAllStatus( + @Req() req: AuthenticatedRequest, + @Query() query: ListAllGatewayStatusDto + ): Promise { + // Currently, everyone is allowed to get the status + return this.onlineHistoryService.findAllWithChirpstack(query); + } + + @Get("/status/:id") + @ApiProduces("application/json") + @ApiOperation({ summary: "Get the status for a LoRaWAN gateway" }) + async getStatus( + @Req() req: AuthenticatedRequest, + @Param("id") id: string, + @Query() query: GetGatewayStatusQuery + ): Promise { + // Currently, everyone is allowed to get the status + const gatewayDto = await this.chirpstackGatewayService.getOne(id); + return this.onlineHistoryService.findOne(gatewayDto.gateway, query.timeInterval); + } +} diff --git a/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts b/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts index c1d79674..fa291927 100644 --- a/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts +++ b/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts @@ -68,7 +68,7 @@ export class SigfoxGroupController { @Get(":id") @ApiProduces("application/json") - @ApiOperation({ summary: "List a SigFox Groups" }) + @ApiOperation({ summary: "Get one group by ID" }) @Read() async getOne( @Req() req: AuthenticatedRequest, @@ -78,7 +78,7 @@ export class SigfoxGroupController { try { group = await this.service.findOne(id); } catch (err) { - throw new NotFoundException(); + return null; } checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationRead); return group; diff --git a/src/controllers/device-data-controller/receive-data.controller.ts b/src/controllers/device-data-controller/receive-data.controller.ts index 8a45798f..be52563b 100644 --- a/src/controllers/device-data-controller/receive-data.controller.ts +++ b/src/controllers/device-data-controller/receive-data.controller.ts @@ -44,7 +44,7 @@ export class ReceiveDataController { // @HACK: Convert the 'data' back to a string. // NestJS / BodyParser always converts the input to an object for us. const dataAsString = JSON.stringify(data); - await this.receiveDataService.sendToKafka( + await this.receiveDataService.sendRawIotDeviceRequestToKafka( iotDevice, dataAsString, IoTDeviceType.GenericHttp.toString() diff --git a/src/controllers/device-data-controller/sigfox-listener.controller.ts b/src/controllers/device-data-controller/sigfox-listener.controller.ts index 7896688e..df51bd91 100644 --- a/src/controllers/device-data-controller/sigfox-listener.controller.ts +++ b/src/controllers/device-data-controller/sigfox-listener.controller.ts @@ -49,7 +49,7 @@ export class SigFoxListenerController { const sigfoxDevice = await this.findSigFoxDevice(data); const dataAsString = JSON.stringify(data); - await this.receiveDataService.sendToKafka( + await this.receiveDataService.sendRawIotDeviceRequestToKafka( sigfoxDevice, dataAsString, IoTDeviceType.SigFox.toString(), diff --git a/src/controllers/user-management/auth.controller.ts b/src/controllers/user-management/auth.controller.ts index 89baa021..d1947b92 100644 --- a/src/controllers/user-management/auth.controller.ts +++ b/src/controllers/user-management/auth.controller.ts @@ -31,7 +31,6 @@ import { import { JwtPayloadDto } from "@dto/internal/jwt-payload.dto"; import { LoginDto } from "@dto/login.dto"; import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; import { User } from "@entities/user.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { AuthService } from "@services/user-management/auth.service"; diff --git a/src/controllers/user-management/new-kombit-creation.controller.ts b/src/controllers/user-management/new-kombit-creation.controller.ts new file mode 100644 index 00000000..4c3e32cd --- /dev/null +++ b/src/controllers/user-management/new-kombit-creation.controller.ts @@ -0,0 +1,177 @@ +import { JwtAuthGuard } from "@auth/jwt-auth.guard"; +import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; +import { ListAllMinimalOrganizationsResponseDto } from "@dto/list-all-organizations-response.dto"; +import { ListAllUsersMinimalResponseDto } from "@dto/list-all-users-minimal-response.dto"; +import { CreateNewKombitUserDto } from "@dto/user-management/create-new-kombit-user.dto"; +import { UpdateUserOrgsDto } from "@dto/user-management/update-user-orgs.dto"; +import { UserResponseDto } from "@dto/user-response.dto"; +import { ActionType } from "@entities/audit-log-entry"; +import { Organization } from "@entities/organization.entity"; +import { User } from "@entities/user.entity"; +import { ErrorCodes } from "@enum/error-codes.enum"; + +import { + BadRequestException, + Body, + Controller, + Get, + NotFoundException, + Param, + ParseIntPipe, + Put, + Query, + Req, + UseGuards, +} from "@nestjs/common"; +import { + ApiBearerAuth, + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, +} from "@nestjs/swagger"; +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"; + +@UseGuards(JwtAuthGuard) +@ApiBearerAuth() +@ApiForbiddenResponse() +@ApiUnauthorizedResponse() +@ApiTags("KombitEmailCreation") +@Controller("kombitCreation") +export class NewKombitCreationController { + constructor( + private organizationService: OrganizationService, + private userService: UserService, + private permissionService: PermissionService + ) {} + + @Put("createNewKombitUser") + @ApiOperation({ summary: "Create kombit-user Email" }) + async newKombitUser( + @Req() req: AuthenticatedRequest, + @Body() dto: CreateNewKombitUserDto + ): Promise { + + try { + const user: User = await this.userService.findOne(req.user.userId); + const permissions = await this.permissionService.findManyWithRelations( + dto.requestedOrganizationIds + ); + + const requestedOrganizations: Organization[] = await this.organizationService.mapPermissionsToOrganizations( + permissions + ); + + if (!user.email) { + const updatedUser: User = await this.userService.newKombitUser( + dto, + requestedOrganizations, + user + ); + + for (let index = 0; index < dto.requestedOrganizationIds.length; index++) { + const dbOrg = await this.organizationService.findByIdWithUsers( + requestedOrganizations[index].id + ); + + await this.organizationService.updateAwaitingUsers( + dbOrg, + updatedUser + ); + } + + AuditLog.success(ActionType.UPDATE, User.name, req.user.userId); + return updatedUser; + + } else { + throw new BadRequestException(ErrorCodes.EmailAlreadyExists); + } + } catch (err) { + AuditLog.fail(ActionType.UPDATE, User.name, req.user.userId); + throw err; + } + } + + @Get("minimal") + @ApiOperation({ + summary: + "Get list of the minimal representation of organizations, i.e. id and name.", + }) + async findAllMinimal(): Promise { + return await this.organizationService.findAllMinimal(); + } + + @Get("minimalUsers") + @ApiOperation({ summary: "Get all id,names of users" }) + async findAllMinimalUsers(): Promise { + return await this.userService.findAllMinimal(); + } + + @Put("updateUserOrgs") + @ApiOperation({ summary: "Updates the users organizations" }) + @ApiNotFoundResponse() + async updateUserOrgs( + @Req() req: AuthenticatedRequest, + @Body() updateUserOrgsDto: UpdateUserOrgsDto + ): Promise { + + try { + const user = await this.userService.findOne(req.user.userId); + const permissions = await this.permissionService.findManyWithRelations( + updateUserOrgsDto.requestedOrganizationIds + ); + + const requestedOrganizations = this.organizationService.mapPermissionsToOrganizations( + permissions + ); + + for (let index = 0; index < requestedOrganizations.length; index++) { + await this.userService.sendOrganizationRequestMail( + user, + requestedOrganizations[index] + ); + } + + for (let index = 0; index < updateUserOrgsDto.requestedOrganizationIds.length; index++) { + const dbOrg = await this.organizationService.findByIdWithUsers( + requestedOrganizations[index].id + ); + + await this.organizationService.updateAwaitingUsers(dbOrg, user); + } + + AuditLog.success(ActionType.UPDATE, User.name, req.user.userId); + return updateUserOrgsDto; + } catch (err) { + AuditLog.fail(ActionType.UPDATE, User.name, req.user.userId); + throw err; + } + } + + @Get(":id") + @ApiOperation({ summary: "Get one user" }) + async find( + @Param("id", new ParseIntPipe()) id: number, + @Query("extendedInfo") extendedInfo?: boolean + ): Promise { + + 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( + id, + getExtendedInfo, + getExtendedInfo + ); + + return user; + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + } +} diff --git a/src/controllers/user-management/organization.controller.ts b/src/controllers/user-management/organization.controller.ts index a3feb375..b8a0a05a 100644 --- a/src/controllers/user-management/organization.controller.ts +++ b/src/controllers/user-management/organization.controller.ts @@ -39,6 +39,7 @@ import { OrganizationService } from "@services/user-management/organization.serv import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; +import { UserService } from "@services/user-management/user.service"; @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() @@ -48,7 +49,9 @@ import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; @Controller("organization") @GlobalAdmin() export class OrganizationController { - constructor(private organizationService: OrganizationService) {} + constructor( + private organizationService: OrganizationService, + ) {} private readonly logger = new Logger(OrganizationController.name); @Post() diff --git a/src/controllers/user-management/permission.controller.ts b/src/controllers/user-management/permission.controller.ts index c93474cc..291a0507 100644 --- a/src/controllers/user-management/permission.controller.ts +++ b/src/controllers/user-management/permission.controller.ts @@ -3,7 +3,6 @@ import { Body, Controller, Delete, - ForbiddenException, Get, NotFoundException, Param, @@ -29,7 +28,6 @@ import { ListAllPermissionsResponseDto } from "@dto/list-all-permissions-respons 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/permissions/organization-permission.entity"; import { Permission } from "@entities/permissions/permission.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { @@ -40,15 +38,17 @@ import { import { PermissionService } from "@services/user-management/permission.service"; import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; -import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; import { UserService } from "@services/user-management/user.service"; -import { UserResponseDto } from "@dto/user-response.dto"; import { ListAllUsersResponseDto } from "@dto/list-all-users-response.dto"; 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"; +import { User } from "@entities/user.entity"; @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() @@ -60,7 +60,8 @@ export class PermissionController { constructor( private permissionService: PermissionService, private userService: UserService, - private applicationService: ApplicationService + private applicationService: ApplicationService, + private organizationService: OrganizationService ) {} @Post() @@ -71,7 +72,11 @@ export class PermissionController { @Body() dto: CreatePermissionDto ): Promise { try { - checkIfUserHasAccessToOrganization(req, dto.organizationId, OrganizationAccessScope.UserAdministrationWrite); + checkIfUserHasAccessToOrganization( + req, + dto.organizationId, + OrganizationAccessScope.UserAdministrationWrite + ); const result = await this.permissionService.createNewPermission( dto, @@ -92,6 +97,53 @@ export class PermissionController { } } + @Put("/acceptUser") + @ApiOperation({ summary: "add user to permission" }) + async addUserToPermission( + @Req() req: AuthenticatedRequest, + @Body() dto: PermissionRequestAcceptUser + ): Promise { + try { + checkIfUserHasAccessToOrganization( + req, + dto.organizationId, + OrganizationAccessScope.UserAdministrationWrite + ); + + const permissions = await this.permissionService.findOneWithRelations( + dto.organizationId + ); + + const org: Organization = this.organizationService.mapPermissionsToOneOrganization( + permissions + ); + + const user: User = await this.userService.findOne(dto.userId); + 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, newUserPermissions); + + AuditLog.success( + ActionType.UPDATE, + Permission.name, + req.user.userId, + resultUser.id, + resultUser.name + ); + return resultUser; + + } catch (err) { + AuditLog.fail(ActionType.UPDATE, Permission.name, req.user.userId); + throw err; + } + } + @Put(":id") @ApiOperation({ summary: "Update permission" }) @UserAdmin() @@ -102,13 +154,12 @@ 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; checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id, + permission.organization.id, OrganizationAccessScope.UserAdministrationWrite ); } @@ -142,13 +193,12 @@ 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)) { throw new BadRequestException("You cannot delete GlobalAdmin"); } else { - const organizationPermission = permission as OrganizationPermission; checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id, + permission.organization.id, OrganizationAccessScope.UserAdministrationWrite ); } @@ -196,18 +246,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; checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id, + permission.organization.id, OrganizationAccessScope.UserAdministrationWrite ); - return organizationPermission; + return permission; } } @@ -233,14 +282,13 @@ 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; checkIfUserHasAccessToOrganization( req, - organizationPermission.organization.id, + permission.organization.id, OrganizationAccessScope.UserAdministrationWrite ); @@ -267,14 +315,13 @@ 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; checkIfUserHasAccessToOrganization( req, - organizationPermission?.organization?.id, + permission?.organization?.id, OrganizationAccessScope.UserAdministrationWrite ); diff --git a/src/controllers/user-management/user.controller.ts b/src/controllers/user-management/user.controller.ts index 0d1ac003..f246357b 100644 --- a/src/controllers/user-management/user.controller.ts +++ b/src/controllers/user-management/user.controller.ts @@ -6,7 +6,6 @@ import { InternalServerErrorException, NotFoundException, Param, - ParseBoolPipe, ParseIntPipe, Post, Put, @@ -32,7 +31,7 @@ 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 { 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"; @@ -40,6 +39,9 @@ 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 { OrganizationService } from "@services/user-management/organization.service"; +import { Organization } from "@entities/organization.entity"; +import { RejectUserDto } from "@dto/user-management/reject-user.dto"; @UseGuards(JwtAuthGuard, RolesGuard) @ApiBearerAuth() @@ -48,7 +50,10 @@ import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; @ApiTags("User Management") @Controller("user") export class UserController { - constructor(private userService: UserService) {} + constructor( + private userService: UserService, + private organizationService: OrganizationService + ) {} private readonly logger = new Logger(UserController.name); @@ -99,6 +104,20 @@ export class UserController { } } + @Put("/rejectUser") + @ApiOperation({ summary: "Rejects user and removes from awaiting users" }) + async rejectUser( + @Req() req: AuthenticatedRequest, + @Body() body: RejectUserDto + ): Promise { + 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); + } + @Put(":id") @ApiOperation({ summary: "Change a user" }) @UserAdmin() @@ -132,8 +151,26 @@ export class UserController { } } + @Put(":id/hide-welcome") + @ApiOperation({ summary: "Don't show welcome screen for a user again" }) + @Read() + async hideWelcome(@Req() req: AuthenticatedRequest): Promise { + const wasOk = await this.userService.hideWelcome(req.user.userId); + + AuditLog.success( + ActionType.UPDATE, + User.name, + req.user.userId, + req.user.userId, + req.user.username + ); + + return wasOk + } + @Get(":id") @ApiOperation({ summary: "Get one user" }) + @Read() async find( @Param("id", new ParseIntPipe()) id: number, @Query("extendedInfo") extendedInfo?: boolean @@ -158,4 +195,17 @@ export class UserController { async findAll(@Query() query?: ListAllEntitiesDto): Promise { return await this.userService.findAll(query); } + + @Get("/awaitingUsers/:id") + @ApiOperation({ summary: "Get awaiting users" }) + async findAwaitingUsers( + @Param("id", new ParseIntPipe()) organizationId: number, + @Query() query?: ListAllEntitiesDto + ): Promise { + try { + return await this.userService.getAwaitingUsers(organizationId, query); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + } } diff --git a/src/entities/api-key-permission.entity.ts b/src/entities/api-key-permission.entity.ts deleted file mode 100644 index bc28f9c8..00000000 --- a/src/entities/api-key-permission.entity.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { PermissionType } from "@enum/permission-type.enum"; -import { ChildEntity, ManyToMany } from "typeorm"; -import { ApiKey } from "./api-key.entity"; -import { Permission } from "./permissions/permission.entity"; - -@ChildEntity(PermissionType.ApiKeyPermission) -export abstract class ApiKeyPermission extends Permission { - @ManyToMany(_ => ApiKey, key => key.permissions, { onDelete: "CASCADE" }) - apiKeys: ApiKey[]; -} diff --git a/src/entities/api-key.entity.ts b/src/entities/api-key.entity.ts index da650106..e0857227 100644 --- a/src/entities/api-key.entity.ts +++ b/src/entities/api-key.entity.ts @@ -9,8 +9,8 @@ import { OneToOne, Unique, } from "typeorm"; -import { ApiKeyPermission } from "./api-key-permission.entity"; import { DbBaseEntity } from "./base.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 2ceb328b..c8835d49 100644 --- a/src/entities/application.entity.ts +++ b/src/entities/application.entity.ts @@ -1,4 +1,3 @@ -import { DbBaseEntity } from "@entities/base.entity"; import { DataTarget } from "@entities/data-target.entity"; import { IoTDevice } from "@entities/iot-device.entity"; import { Organization } from "@entities/organization.entity"; @@ -15,8 +14,9 @@ import { } from "typeorm"; import { ApplicationDeviceType } from "./application-device-type.entity"; import { ControlledProperty } from "./controlled-property.entity"; -import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; import { Multicast } from "./multicast.entity"; +import { Permission } from "./permissions/permission.entity"; +import { DbBaseEntity } from "@entities/base.entity"; @Entity("application") @Unique(["name"]) @@ -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/chirpstack/backend/gateway-all-status.dto.ts b/src/entities/dto/chirpstack/backend/gateway-all-status.dto.ts new file mode 100644 index 00000000..48d0a3d2 --- /dev/null +++ b/src/entities/dto/chirpstack/backend/gateway-all-status.dto.ts @@ -0,0 +1,22 @@ +import { ListAllEntitiesResponseDto } from "@dto/list-all-entities-response.dto"; +import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; +import { GatewayStatusInterval } from "@enum/gateway-status-interval.enum"; +import { IsSwaggerOptional } from "@helpers/optional-validator"; +import { StringToNumber } from "@helpers/string-to-number-validator"; +import { IsEnum } from "class-validator"; +import { GatewayStatus } from "./gateway-status.dto"; + +export class GatewayGetAllStatusResponseDto extends ListAllEntitiesResponseDto {} + +export class ListAllGatewayStatusDto extends ListAllEntitiesDto { + @IsSwaggerOptional() + @StringToNumber() + organizationId?: number; + + @IsSwaggerOptional({ + default: GatewayStatusInterval.DAY, + enum: GatewayStatusInterval, + }) + @IsEnum(GatewayStatusInterval) + timeInterval: GatewayStatusInterval = GatewayStatusInterval.DAY; +} diff --git a/src/entities/dto/chirpstack/backend/gateway-status.dto.ts b/src/entities/dto/chirpstack/backend/gateway-status.dto.ts new file mode 100644 index 00000000..4acff60d --- /dev/null +++ b/src/entities/dto/chirpstack/backend/gateway-status.dto.ts @@ -0,0 +1,23 @@ +import { GatewayStatusInterval } from "@enum/gateway-status-interval.enum"; +import { IsSwaggerOptional } from "@helpers/optional-validator"; +import { IsEnum } from "class-validator"; + +export interface StatusTimestamp { + timestamp: Date; + wasOnline: boolean; +} + +export interface GatewayStatus { + id: string; + name: string; + statusTimestamps: StatusTimestamp[]; +} + +export class GetGatewayStatusQuery { + @IsSwaggerOptional({ + default: GatewayStatusInterval.DAY, + enum: GatewayStatusInterval, + }) + @IsEnum(GatewayStatusInterval) + timeInterval: GatewayStatusInterval = GatewayStatusInterval.DAY; +} diff --git a/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts b/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts index 3b7b3eb8..9136b803 100644 --- a/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts @@ -16,3 +16,8 @@ export class ChirpstackMQTTMessageTxInfoDto { frequency: number; dr: number; } + +export class ChirpstackMQTTConnectionStateMessageDto { + gatewayId: string; + isOnline: boolean; +} diff --git a/src/entities/dto/chirpstack/state/chirpstack-mqtt-state-message.dto.ts b/src/entities/dto/chirpstack/state/chirpstack-mqtt-state-message.dto.ts new file mode 100644 index 00000000..40ec92e6 --- /dev/null +++ b/src/entities/dto/chirpstack/state/chirpstack-mqtt-state-message.dto.ts @@ -0,0 +1,4 @@ +export class ChirpstackMQTTConnectionStateMessage { + gatewayId: string; + state: "ONLINE"; +} diff --git a/src/entities/dto/kafka/raw-gateway-state.dto.ts b/src/entities/dto/kafka/raw-gateway-state.dto.ts new file mode 100644 index 00000000..6b9d57fb --- /dev/null +++ b/src/entities/dto/kafka/raw-gateway-state.dto.ts @@ -0,0 +1,5 @@ +import { RawRequestDto } from "./raw-request.dto"; + +export class RawGatewayStateDto extends RawRequestDto { + gatewayId: string; +} diff --git a/src/entities/dto/kafka/raw-iot-device-request.dto.ts b/src/entities/dto/kafka/raw-iot-device-request.dto.ts new file mode 100644 index 00000000..072efb61 --- /dev/null +++ b/src/entities/dto/kafka/raw-iot-device-request.dto.ts @@ -0,0 +1,5 @@ +import { RawRequestDto } from "./raw-request.dto"; + +export class RawIoTDeviceRequestDto extends RawRequestDto { + iotDeviceId: number; +} diff --git a/src/entities/dto/kafka/raw-request.dto.ts b/src/entities/dto/kafka/raw-request.dto.ts index 49805988..b15cf370 100644 --- a/src/entities/dto/kafka/raw-request.dto.ts +++ b/src/entities/dto/kafka/raw-request.dto.ts @@ -3,6 +3,5 @@ import { IoTDeviceType } from "@enum/device-type.enum"; export class RawRequestDto { type: IoTDeviceType[number]; rawPayload: JSON; - iotDeviceId: number; unixTimestamp?: number; } 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/user-management/add-user-to-permission.dto.ts b/src/entities/dto/user-management/add-user-to-permission.dto.ts new file mode 100644 index 00000000..0b084aa9 --- /dev/null +++ b/src/entities/dto/user-management/add-user-to-permission.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNumber, IsArray, ArrayUnique, ArrayNotEmpty } from "class-validator"; + +export class PermissionRequestAcceptUser { + @ApiProperty({ required: true }) + @IsNumber() + organizationId: number; + + @ApiProperty({ required: true }) + @IsNumber() + userId: number; + + @ApiProperty({ required: true }) + @IsArray() + @ArrayNotEmpty() + @ArrayUnique() + permissionIds: number[]; +} diff --git a/src/entities/dto/user-management/create-new-kombit-user.dto.ts b/src/entities/dto/user-management/create-new-kombit-user.dto.ts new file mode 100644 index 00000000..d252077d --- /dev/null +++ b/src/entities/dto/user-management/create-new-kombit-user.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { ArrayMinSize, IsEmail, IsNotEmpty } from "class-validator"; + +export class CreateNewKombitUserDto { + @ApiProperty({ required: true }) + @IsEmail() + @IsNotEmpty() + email: string; + + @ApiProperty({ required: true }) + @ArrayMinSize(1) + requestedOrganizationIds: number[]; +} diff --git a/src/entities/dto/user-management/create-permission.dto.ts b/src/entities/dto/user-management/create-permission.dto.ts index 2c28e57e..d0eaaa1c 100644 --- a/src/entities/dto/user-management/create-permission.dto.ts +++ b/src/entities/dto/user-management/create-permission.dto.ts @@ -1,18 +1,21 @@ import { PermissionType } from "@entities/enum/permission-type.enum"; import { ApiProperty } from "@nestjs/swagger"; -import { IsEnum, IsNumber, IsString, Length } from "class-validator"; +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: - | PermissionType.OrganizationUserAdmin - | PermissionType.OrganizationGatewayAdmin - | PermissionType.OrganizationApplicationAdmin - | PermissionType.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/dto/user-management/reject-user.dto.ts b/src/entities/dto/user-management/reject-user.dto.ts new file mode 100644 index 00000000..97d07ff6 --- /dev/null +++ b/src/entities/dto/user-management/reject-user.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNumber } from "class-validator"; + +export class RejectUserDto { + @ApiProperty({ required: true }) + @IsNumber() + orgId: number; + + @ApiProperty({ required: true }) + @IsNumber() + userIdToReject: number; +} diff --git a/src/entities/dto/user-management/update-user-orgs.dto.ts b/src/entities/dto/user-management/update-user-orgs.dto.ts new file mode 100644 index 00000000..c7c297fa --- /dev/null +++ b/src/entities/dto/user-management/update-user-orgs.dto.ts @@ -0,0 +1,9 @@ +import { Organization } from "@entities/organization.entity"; +import { ApiProperty } from "@nestjs/swagger"; +import { ArrayMinSize } from "class-validator"; + +export class UpdateUserOrgsDto { + @ApiProperty({ required: true }) + @ArrayMinSize(1) + requestedOrganizationIds: number[]; +} diff --git a/src/entities/enum/error-codes.enum.ts b/src/entities/enum/error-codes.enum.ts index 8598d96d..a32b9d7c 100644 --- a/src/entities/enum/error-codes.enum.ts +++ b/src/entities/enum/error-codes.enum.ts @@ -1,4 +1,5 @@ export enum ErrorCodes { + EmailAlreadyExists = "MESSAGE.USER-ALREADY-HAVE-MAIL", IdDoesNotExists = "MESSAGE.ID-DOES-NOT-EXIST", IdMissing = "MESSAGE.ID-MISSING-FROM-REQUEST", NameInvalidOrAlreadyInUse = "MESSAGE.NAME-INVALID-OR-ALREADY-IN-USE", @@ -44,4 +45,8 @@ 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/gateway-status-interval.enum.ts b/src/entities/enum/gateway-status-interval.enum.ts new file mode 100644 index 00000000..d9fc604d --- /dev/null +++ b/src/entities/enum/gateway-status-interval.enum.ts @@ -0,0 +1,20 @@ +import { subtractDays } from "@helpers/date.helper"; + +export enum GatewayStatusInterval { + DAY = "DAY", + WEEK = "WEEK", + MONTH = "MONTH", +} + +export const gatewayStatusIntervalToDate = (interval: GatewayStatusInterval): Date => { + const now = new Date(); + + switch (interval) { + case GatewayStatusInterval.WEEK: + return subtractDays(now, 7); + case GatewayStatusInterval.MONTH: + return subtractDays(now, 30); + default: + return subtractDays(now, 1); + } +}; diff --git a/src/entities/enum/kafka-topic.enum.ts b/src/entities/enum/kafka-topic.enum.ts index 46572f5e..af881201 100644 --- a/src/entities/enum/kafka-topic.enum.ts +++ b/src/entities/enum/kafka-topic.enum.ts @@ -1,4 +1,5 @@ export enum KafkaTopic { RAW_REQUEST = "request.raw", TRANSFORMED_REQUEST = "request.transformed", + RAW_GATEWAY_STATE = "request.gateway.state" } diff --git a/src/entities/enum/permission-type.enum.ts b/src/entities/enum/permission-type.enum.ts index 5d1e165c..16fa326d 100644 --- a/src/entities/enum/permission-type.enum.ts +++ b/src/entities/enum/permission-type.enum.ts @@ -4,8 +4,4 @@ export enum PermissionType { OrganizationGatewayAdmin = "OrganizationGatewayAdmin", OrganizationApplicationAdmin = "OrganizationApplicationAdmin", Read = "Read", - - OrganizationPermission = "OrganizationPermission", - OrganizationApplicationPermissions = "OrganizationApplicationPermissions", - ApiKeyPermission = "ApiKeyPermission", } diff --git a/src/entities/gateway-status-history.entity.ts b/src/entities/gateway-status-history.entity.ts new file mode 100644 index 00000000..622c6120 --- /dev/null +++ b/src/entities/gateway-status-history.entity.ts @@ -0,0 +1,16 @@ +import { nameof } from "@helpers/type-helper"; +import { Column, CreateDateColumn, Entity, Unique } from "typeorm"; +import { DbBaseEntity } from "./base.entity"; + +@Entity("gateway_status_history") +@Unique([nameof("mac"), nameof("timestamp")]) +export class GatewayStatusHistory extends DbBaseEntity { + @Column() + mac: string; + + @Column() + wasOnline: boolean; + + @CreateDateColumn() + timestamp: Date; +} diff --git a/src/entities/lorawan-multicast.entity.ts b/src/entities/lorawan-multicast.entity.ts index bec8fe3f..d781bc9a 100644 --- a/src/entities/lorawan-multicast.entity.ts +++ b/src/entities/lorawan-multicast.entity.ts @@ -1,4 +1,4 @@ -import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from "typeorm"; +import { Column, Entity, OneToOne } from "typeorm"; import { multicastGroup } from "@enum/multicast-type.enum"; import { Multicast } from "./multicast.entity"; diff --git a/src/entities/organization.entity.ts b/src/entities/organization.entity.ts index e2294e87..25694148 100644 --- a/src/entities/organization.entity.ts +++ b/src/entities/organization.entity.ts @@ -1,13 +1,13 @@ -import { Column, Entity, OneToMany, Unique } from "typeorm"; +import { Column, Entity, ManyToMany, OneToMany, Unique } from "typeorm"; import { Application } from "@entities/application.entity"; import { DbBaseEntity } from "@entities/base.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; import { PayloadDecoder } from "@entities/payload-decoder.entity"; import { Permission } from "@entities/permissions/permission.entity"; import { SigFoxGroup } from "./sigfox-group.entity"; import { DeviceModel } from "./device-model.entity"; +import { User } from "./user.entity"; @Entity("organization") @Unique(["name"]) @@ -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" } ) @@ -52,4 +52,9 @@ export class Organization extends DbBaseEntity { nullable: true, }) deviceModels?: DeviceModel[]; + + @ManyToMany(_ => User, user => user.requestedOrganizations, { + nullable: true, + }) + awaitingUsers?: User[]; } diff --git a/src/entities/permissions/global-admin-permission.entity.ts b/src/entities/permissions/global-admin-permission.entity.ts deleted file mode 100644 index 3a07a33b..00000000 --- a/src/entities/permissions/global-admin-permission.entity.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ChildEntity } from "typeorm"; - -import { Permission } from "@entities/permissions/permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@ChildEntity(PermissionType.GlobalAdmin) -export class GlobalAdminPermission extends Permission { - constructor() { - super("GlobalAdmin"); - this.type = PermissionType.GlobalAdmin; - } -} diff --git a/src/entities/permissions/organization-application-admin-permission.entity.ts b/src/entities/permissions/organization-application-admin-permission.entity.ts deleted file mode 100644 index 354ad146..00000000 --- a/src/entities/permissions/organization-application-admin-permission.entity.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ChildEntity } from "typeorm"; - -import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; -import { Organization } from "@entities/organization.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@ChildEntity(PermissionType.OrganizationApplicationAdmin) -export class OrganizationApplicationAdminPermission extends OrganizationApplicationPermission { - constructor(name: string, org: Organization, addNewApps = false) { - super(name, org, addNewApps); - this.type = PermissionType.OrganizationApplicationAdmin; - } -} diff --git a/src/entities/permissions/organization-application-permission.entity.ts b/src/entities/permissions/organization-application-permission.entity.ts deleted file mode 100644 index 21403eae..00000000 --- a/src/entities/permissions/organization-application-permission.entity.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ChildEntity, Column, ManyToMany } from "typeorm"; - -import { Application } from "@entities/application.entity"; -import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@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; - } - - @ManyToMany(() => Application, application => application.permissions) - applications: Application[]; - - @Column({ nullable: true, default: false, type: Boolean }) - automaticallyAddNewApplications = false; -} diff --git a/src/entities/permissions/organization-gateway-admin-permission.entity.ts b/src/entities/permissions/organization-gateway-admin-permission.entity.ts deleted file mode 100644 index 20f14d0f..00000000 --- a/src/entities/permissions/organization-gateway-admin-permission.entity.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; -import { ChildEntity } from "typeorm"; - -@ChildEntity(PermissionType.OrganizationGatewayAdmin) -export class OrganizationGatewayAdminPermission extends OrganizationPermission { - constructor(name: string, org: Organization) { - super(name, org); - } -} diff --git a/src/entities/permissions/organization-permission.entity.ts b/src/entities/permissions/organization-permission.entity.ts deleted file mode 100644 index 9d611cd0..00000000 --- a/src/entities/permissions/organization-permission.entity.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ChildEntity, ManyToOne } from "typeorm"; - -import { Organization } from "@entities/organization.entity"; -import { Permission } from "@entities/permissions/permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@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; -} diff --git a/src/entities/permissions/organization-user-admin-permission.entity.ts b/src/entities/permissions/organization-user-admin-permission.entity.ts deleted file mode 100644 index df2d98ff..00000000 --- a/src/entities/permissions/organization-user-admin-permission.entity.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; -import { PermissionType } from "@enum/permission-type.enum"; -import { ChildEntity } from "typeorm"; - -@ChildEntity(PermissionType.OrganizationUserAdmin) -export class OrganizationUserAdminPermission extends OrganizationPermission { - constructor(name: string, org: Organization) { - super(name, org); - } -} 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 index 58b665fd..0e2b4111 100644 --- a/src/entities/permissions/permission.entity.ts +++ b/src/entities/permissions/permission.entity.ts @@ -1,22 +1,33 @@ import { DbBaseEntity } from "@entities/base.entity"; import { User } from "@entities/user.entity"; import { PermissionType } from "@enum/permission-type.enum"; -import { Column, Entity, ManyToMany, TableInheritance } from "typeorm"; - -@Entity() -@TableInheritance({ - column: { type: "enum", name: "type", enum: PermissionType }, -}) -export abstract class Permission extends DbBaseEntity { - constructor(name: string) { +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; } - @Column("enum", { - enum: PermissionType, + @OneToMany(() => PermissionTypeEntity, entity => entity.permission, { + nullable: false, + cascade: true, }) - type: PermissionType; + type: PermissionTypeEntity[]; @Column() name: string; @@ -27,4 +38,16 @@ export abstract class Permission extends DbBaseEntity { 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/permissions/read-permission.entity.ts b/src/entities/permissions/read-permission.entity.ts deleted file mode 100644 index f756d62b..00000000 --- a/src/entities/permissions/read-permission.entity.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ChildEntity } from "typeorm"; - -import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; -import { Organization } from "@entities/organization.entity"; -import { PermissionType } from "@enum/permission-type.enum"; - -@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/user.entity.ts b/src/entities/user.entity.ts index 1ccc092c..1e287a49 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -2,6 +2,7 @@ import { ApiKey } from "@entities/api-key.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"]) @@ -24,11 +25,24 @@ export class User extends DbBaseEntity { @Column({ nullable: true }) nameId: string; + @Column({ nullable: true }) + awaitingConfirmation: boolean; + // eslint-disable-next-line @typescript-eslint/no-unused-vars @ManyToMany(type => Permission, permission => permission.users) @JoinTable() permissions: Permission[]; + @ManyToMany( + _ => Organization, + requestedOrganizations => requestedOrganizations.awaitingUsers, + { + nullable: true, + } + ) + @JoinTable() + requestedOrganizations: Organization[]; + @OneToOne(type => ApiKey, a => a.systemUser, { nullable: true, cascade: false, @@ -37,4 +51,7 @@ export class User extends DbBaseEntity { @Column({ default: false }) isSystemUser: boolean; + + @Column({ default: false }) + showWelcomeScreen: boolean; } 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/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 26b61eed..195f87e1 100644 --- a/src/helpers/security-helper.ts +++ b/src/helpers/security-helper.ts @@ -1,10 +1,9 @@ import { AuthenticatedRequest } from "@entities/dto/internal/authenticated-request"; import { Permission } from "@entities/permissions/permission.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { ForbiddenException, BadRequestException } from "@nestjs/common"; import * as _ from "lodash"; -import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; +import { PermissionTypeEntity } from "@entities/permissions/permission-type.entity"; export enum OrganizationAccessScope { ApplicationRead, @@ -96,20 +95,30 @@ function checkIfGlobalAdminOrInList( } } -export function isOrganizationPermission(p: Permission): p is OrganizationPermission { +export function isOrganizationPermission(p: Permission): p is Permission { return [ PermissionType.OrganizationUserAdmin, PermissionType.OrganizationApplicationAdmin, PermissionType.OrganizationGatewayAdmin, PermissionType.Read, - ].some(x => x === p.type); + ].some(orgPermission => p.type.some(({ type }) => type === orgPermission)); } export function isOrganizationApplicationPermission(p: { - type: PermissionType; -}): p is OrganizationApplicationPermission { - return ( - p.type === PermissionType.Read || - p.type === PermissionType.OrganizationApplicationAdmin + 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/1652342361070-welcome-screen.ts b/src/migration/1652342361070-welcome-screen.ts new file mode 100644 index 00000000..bd75c9da --- /dev/null +++ b/src/migration/1652342361070-welcome-screen.ts @@ -0,0 +1,14 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class welcomeScreen1652342361070 implements MigrationInterface { + name = 'welcomeScreen1652342361070' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" ADD "showWelcomeScreen" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "showWelcomeScreen"`); + } + +} diff --git a/src/migration/1652771064000-KombitUserManagement.ts b/src/migration/1652771064000-KombitUserManagement.ts new file mode 100644 index 00000000..1cc587e3 --- /dev/null +++ b/src/migration/1652771064000-KombitUserManagement.ts @@ -0,0 +1,24 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class kombitUserManagement1652771064000 implements MigrationInterface { + name = 'kombitUserManagement1652771064000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "user_requested_organizations_organization" ("userId" integer NOT NULL, "organizationId" integer NOT NULL, CONSTRAINT "PK_b228a18276f4dc0153b74e04370" PRIMARY KEY ("userId", "organizationId"))`); + await queryRunner.query(`CREATE INDEX "IDX_87ad1ad67570c6ca20f62c9531" ON "user_requested_organizations_organization" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_6a0a3602e88b71bc867af425d8" ON "user_requested_organizations_organization" ("organizationId") `); + await queryRunner.query(`ALTER TABLE "user" ADD "awaitingConfirmation" boolean`); + await queryRunner.query(`ALTER TABLE "user_requested_organizations_organization" ADD CONSTRAINT "FK_87ad1ad67570c6ca20f62c95313" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "user_requested_organizations_organization" ADD CONSTRAINT "FK_6a0a3602e88b71bc867af425d89" FOREIGN KEY ("organizationId") REFERENCES "organization"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_requested_organizations_organization" DROP CONSTRAINT "FK_6a0a3602e88b71bc867af425d89"`); + await queryRunner.query(`ALTER TABLE "user_requested_organizations_organization" DROP CONSTRAINT "FK_87ad1ad67570c6ca20f62c95313"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "awaitingConfirmation"`); + await queryRunner.query(`DROP INDEX "public"."IDX_6a0a3602e88b71bc867af425d8"`); + await queryRunner.query(`DROP INDEX "public"."IDX_87ad1ad67570c6ca20f62c9531"`); + await queryRunner.query(`DROP TABLE "user_requested_organizations_organization"`); + } + +} diff --git a/src/migration/1652861431000-gateway-status-history.ts b/src/migration/1652861431000-gateway-status-history.ts new file mode 100644 index 00000000..08fad067 --- /dev/null +++ b/src/migration/1652861431000-gateway-status-history.ts @@ -0,0 +1,18 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class gatewayStatusHistory1652861431000 implements MigrationInterface { + name = 'gatewayStatusHistory1652861431000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "gateway_status_history" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "mac" character varying NOT NULL, "wasOnline" boolean NOT NULL, "timestamp" TIMESTAMP NOT NULL DEFAULT now(), "createdById" integer, "updatedById" integer, CONSTRAINT "UQ_5370e37f34adf6e9c9350bc46c7" UNIQUE ("mac", "timestamp"), CONSTRAINT "PK_defafcb0f6f1ba7a612395b62f8" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "gateway_status_history" ADD CONSTRAINT "FK_196255adef4a4011a1dea022630" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "gateway_status_history" ADD CONSTRAINT "FK_94b6a464efce5a478b185bf7e89" FOREIGN KEY ("updatedById") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "gateway_status_history" DROP CONSTRAINT "FK_94b6a464efce5a478b185bf7e89"`); + await queryRunner.query(`ALTER TABLE "gateway_status_history" DROP CONSTRAINT "FK_196255adef4a4011a1dea022630"`); + await queryRunner.query(`DROP TABLE "gateway_status_history"`); + } + +} diff --git a/src/migration/1651142158492-revised-permissions.ts b/src/migration/1652951529000-revised-permissions.ts similarity index 61% rename from src/migration/1651142158492-revised-permissions.ts rename to src/migration/1652951529000-revised-permissions.ts index f07b10f5..27759193 100644 --- a/src/migration/1651142158492-revised-permissions.ts +++ b/src/migration/1652951529000-revised-permissions.ts @@ -16,15 +16,15 @@ type PermissionInfo = { }; type UserPermissionInfo = PermissionInfo & { - userId?: number; + userIds?: number[]; }; type AppPermissionInfo = PermissionInfo & { - applicationId?: number; + applicationIds?: number[]; }; type ApiKeyPermissionInfo = PermissionInfo & { - apiKeyId?: number; + apiKeyIds?: number[]; } type UserPermissions = { @@ -32,33 +32,42 @@ type UserPermissions = { permissionId: number; }[]; -export class revisedPermissions1651142158492 implements MigrationInterface { - name = "revisedPermissions1651142158492"; +/** + * 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', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` + `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( - // `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum" USING "type"::"text"::"permission_type_enum"` - // ); await queryRunner.query(`DROP TYPE "permission_type_enum_old"`); - await queryRunner.query(`COMMENT ON COLUMN "permission"."type" IS NULL`); + + // Update permission so it can refer to multiple types + await this.migratePermissionTypeUp(queryRunner); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`COMMENT ON COLUMN "permission"."type" IS NULL`); 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. @@ -69,19 +78,17 @@ export class revisedPermissions1651142158492 implements MigrationInterface { 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 "permission_type_enum"`); + 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 { - // Create a temporary enum which is a union of both old and new enum values - await queryRunner.query( - `CREATE TYPE "permission_type_enum_temp" AS ENUM('OrganizationAdmin', 'Write', 'GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` - ); + await queryRunner.query(createPermissionTypeUnionSql); await queryRunner.query( - `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_temp" USING "type"::"text"::"permission_type_enum_temp"` + `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 @@ -142,38 +149,15 @@ export class revisedPermissions1651142158492 implements MigrationInterface { // Cleanup await queryRunner.query(`ALTER TABLE "permission" DROP COLUMN "clonedFromId"`); + await this.cleanupPermissionRelations(queryRunner, "'Write', 'OrganizationAdmin'"); - 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 ('Write', 'OrganizationAdmin') -);`); - - 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 ('Write', 'OrganizationAdmin') -)`); - - 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 ('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 "permission_type_enum_temp"`); + await queryRunner.query(`DROP TYPE "${permissionTypeUnionName}"`); } private async migrateUserPermissions( @@ -256,10 +240,12 @@ returning id, "permission"."clonedFromId"`; infos: UserPermissionInfo[] ): PermissionInfo[] { const mappedInfos = infos.map(info => { - const match = userPermissions.find(p => p.permissionId === info.clonedFromId); - return match ? { ...info, userId: match.userId } : 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 => typeof info.userId === "number"); + return mappedInfos.filter(info => info.userIds?.length); } private mapAppPermissions( @@ -267,10 +253,12 @@ returning id, "permission"."clonedFromId"`; infos: AppPermissionInfo[] ): PermissionInfo[] { const mappedInfos = infos.map(info => { - const match = appPermissions.find(p => p.permissionId === info.clonedFromId); - return match ? { ...info, applicationId: match.applicationId } : 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 => typeof info.applicationId === "number"); + return mappedInfos.filter(info => info.applicationIds?.length); } private mapApiKeyPermissions( @@ -278,17 +266,17 @@ returning id, "permission"."clonedFromId"`; infos: ApiKeyPermissionInfo[] ): PermissionInfo[] { const mappedInfos = infos.map(info => { - const match = apiKeyPermissions.find(p => p.permissionId === info.clonedFromId); - return match ? { ...info, apiKeyId: match.apiKeyId } : info; + const matches = apiKeyPermissions.filter(p => p.permissionId === info.clonedFromId); + return matches ? { ...info, apiKeyIds: matches.map(x => x.apiKeyId) } : info; }); - return mappedInfos.filter(info => typeof info.apiKeyId === "number"); + return mappedInfos.filter(info => info.apiKeyIds?.length); } private copyUserPermissionsQuery(infos: UserPermissionInfo[]): string { if (!infos.length) return ""; const insertIntoStatements = infos - .map(info => `(${info.userId}, ${info.id})`) + .map(info => info.userIds.map(userId => `(${userId}, ${info.id})`)) .join(","); return `INSERT INTO "public"."user_permissions_permission"("userId","permissionId") VALUES ${insertIntoStatements}`; @@ -298,7 +286,7 @@ returning id, "permission"."clonedFromId"`; if (!infos.length) return ""; const insertIntoStatements = infos - .map(info => `(${info.applicationId}, ${info.id})`) + .map(info => info.applicationIds.map(appId => `(${appId}, ${info.id})`)) .join(","); return `INSERT INTO "public"."application_permissions_permission"("applicationId","permissionId") VALUES ${insertIntoStatements}`; @@ -308,19 +296,41 @@ returning id, "permission"."clonedFromId"`; if (!infos.length) return ""; const insertIntoStatements = infos - .map(info => `(${info.apiKeyId}, ${info.id})`) + .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 { - // Create a temporary enum which is a union of both old and new enum values await queryRunner.query( - `CREATE TYPE "permission_type_enum_temp" AS ENUM('OrganizationAdmin', 'Write', 'GlobalAdmin', 'OrganizationUserAdmin', 'OrganizationGatewayAdmin', 'OrganizationApplicationAdmin', 'Read', 'OrganizationPermission', 'OrganizationApplicationPermissions', 'ApiKeyPermission')` - ); - await queryRunner.query( - `ALTER TABLE "permission" ALTER COLUMN "type" TYPE "permission_type_enum_temp" USING "type"::"text"::"permission_type_enum_temp"` + `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 @@ -328,7 +338,7 @@ returning id, "permission"."clonedFromId"`; `ALTER TABLE "permission" ADD COLUMN "clonedFromId" integer` ); - // Begin cloning. Store both the old and new ids as mappings + // 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") ); @@ -361,14 +371,98 @@ returning id, "permission"."clonedFromId"`; 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 "permission_type_enum_temp"`); + 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/app.module.ts b/src/modules/app.module.ts index 70abb3dc..0b3efb28 100644 --- a/src/modules/app.module.ts +++ b/src/modules/app.module.ts @@ -32,6 +32,7 @@ import { MulticastModule } from "./device-management/multicast.module"; import { OpenDataDkSharingModule } from "./open-data-dk-sharing.module"; import { SearchModule } from "./search.module"; import { TestPayloadDecoderModule } from "./test-payload-decoder.module"; +import { NewKombitCreationModule } from "./user-management/new-kombit-creation.module"; @Module({ imports: [ @@ -92,6 +93,7 @@ import { TestPayloadDecoderModule } from "./test-payload-decoder.module"; MulticastModule, IoTLoRaWANDeviceModule, ApiKeyInfoModule, + NewKombitCreationModule, ], controllers: [], providers: [], diff --git a/src/modules/data-management/gateway-persistence.module.ts b/src/modules/data-management/gateway-persistence.module.ts new file mode 100644 index 00000000..fe9bb710 --- /dev/null +++ b/src/modules/data-management/gateway-persistence.module.ts @@ -0,0 +1,17 @@ +import configuration from "@config/configuration"; +import { LoRaWANGatewayModule } from "@modules/device-integrations/lorawan-gateway.module"; +import { SharedModule } from "@modules/shared.module"; +import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { GatewayPersistenceService } from "@services/data-management/gateway-persistence.service"; + +@Module({ + imports: [ + SharedModule, + ConfigModule.forRoot({ load: [configuration] }), + LoRaWANGatewayModule, + ], + exports: [], + providers: [GatewayPersistenceService], +}) +export class GatewayPersistenceModule {} diff --git a/src/modules/data-target/data-target-kafka.module.ts b/src/modules/data-target/data-target-kafka.module.ts index 6c841953..217ee3c0 100644 --- a/src/modules/data-target/data-target-kafka.module.ts +++ b/src/modules/data-target/data-target-kafka.module.ts @@ -11,6 +11,7 @@ import { KafkaModule } from "@modules/kafka.module"; import { SharedModule } from "@modules/shared.module"; import { DataTargetKafkaListenerService } from "@services/data-targets/data-target-kafka-listener.service"; import { DataTargetFiwareSenderModule } from "./data-target-fiware-sender.module"; +import { GatewayPersistenceModule } from "@modules/data-management/gateway-persistence.module"; @Module({ imports: [ @@ -25,6 +26,7 @@ import { DataTargetFiwareSenderModule } from "./data-target-fiware-sender.module IoTDevicePayloadDecoderDataTargetConnectionModule, ApplicationModule, DataTargetModule, + GatewayPersistenceModule, ], providers: [DataTargetKafkaListenerService], }) diff --git a/src/modules/device-integrations/lorawan-gateway.module.ts b/src/modules/device-integrations/lorawan-gateway.module.ts new file mode 100644 index 00000000..ed96b405 --- /dev/null +++ b/src/modules/device-integrations/lorawan-gateway.module.ts @@ -0,0 +1,18 @@ +import { LoRaWANGatewayController } from "@admin-controller/lorawan/lorawan-gateway.controller"; +import { SharedModule } from "@modules/shared.module"; +import { HttpModule, Module } from "@nestjs/common"; +import { ChirpstackGatewayService } from "@services/chirpstack/chirpstack-gateway.service"; +import { GatewayStatusHistoryService } from "@services/chirpstack/gateway-status-history.service"; +import { ChirpstackSetupNetworkServerService } from "@services/chirpstack/network-server.service"; + +@Module({ + controllers: [LoRaWANGatewayController], + imports: [SharedModule, HttpModule], + providers: [ + ChirpstackGatewayService, + ChirpstackSetupNetworkServerService, + GatewayStatusHistoryService, + ], + exports: [GatewayStatusHistoryService], +}) +export class LoRaWANGatewayModule {} diff --git a/src/modules/shared.module.ts b/src/modules/shared.module.ts index 747d52a1..cd1ef5c1 100644 --- a/src/modules/shared.module.ts +++ b/src/modules/shared.module.ts @@ -4,18 +4,14 @@ 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/permissions/global-admin-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 { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; import { PayloadDecoder } from "@entities/payload-decoder.entity"; import { Permission } from "@entities/permissions/permission.entity"; -import { ReadPermission } from "@entities/permissions/read-permission.entity"; import { ReceivedMessage } from "@entities/received-message.entity"; import { ReceivedMessageMetadata } from "@entities/received-message-metadata.entity"; import { SigFoxDevice } from "@entities/sigfox-device.entity"; @@ -25,25 +21,22 @@ 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/api-key-permission.entity"; import { Multicast } from "@entities/multicast.entity"; import { LorawanMulticastDefinition } from "@entities/lorawan-multicast.entity"; -import { OrganizationApplicationAdminPermission } from "@entities/permissions/organization-application-admin-permission.entity"; -import { OrganizationUserAdminPermission } from "@entities/permissions/organization-user-admin-permission.entity"; -import { OrganizationGatewayAdminPermission } from "@entities/permissions/organization-gateway-admin-permission.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({ imports: [ TypeOrmModule.forFeature([ - ApiKey, + User, Application, DataTarget, GenericHTTPDevice, - GlobalAdminPermission, HttpPushDataTarget, FiwareDataTarget, MqttDataTarget, @@ -53,25 +46,21 @@ import { MqttDataTarget } from "@entities/mqtt-data-target.entity"; LoRaWANDevice, OpenDataDkDataset, Organization, - OrganizationApplicationPermission, - OrganizationApplicationAdminPermission, - OrganizationUserAdminPermission, - OrganizationGatewayAdminPermission, - OrganizationPermission, PayloadDecoder, Permission, - ReadPermission, ReceivedMessage, ReceivedMessageMetadata, SigFoxDevice, SigFoxGroup, User, - ApiKeyPermission, Multicast, LorawanMulticastDefinition, ControlledProperty, ApplicationDeviceType, + ApiKey, ReceivedMessageSigFoxSignals, + PermissionTypeEntity, + GatewayStatusHistory, ]), ], providers: [AuditLog], diff --git a/src/modules/user-management/new-kombit-creation.module.ts b/src/modules/user-management/new-kombit-creation.module.ts new file mode 100644 index 00000000..0c90543d --- /dev/null +++ b/src/modules/user-management/new-kombit-creation.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; + +import { SharedModule } from "@modules/shared.module"; +import { NewKombitCreationController } from "@user-management-controller/new-kombit-creation.controller"; +import { OrganizationModule } from "./organization.module"; +import { UserModule } from "./user.module"; +import { PermissionModule } from "./permission.module"; + +@Module({ + imports: [SharedModule, OrganizationModule, UserModule, PermissionModule], + controllers: [NewKombitCreationController], +}) +export class NewKombitCreationModule {} diff --git a/src/modules/user-management/organization.module.ts b/src/modules/user-management/organization.module.ts index 82947830..591d331e 100644 --- a/src/modules/user-management/organization.module.ts +++ b/src/modules/user-management/organization.module.ts @@ -3,9 +3,10 @@ import { PermissionModule } from "@modules/user-management/permission.module"; import { forwardRef, Module } from "@nestjs/common"; import { OrganizationService } from "@services/user-management/organization.service"; import { OrganizationController } from "@user-management-controller/organization.controller"; +import { UserModule } from "./user.module"; @Module({ - imports: [SharedModule, forwardRef(() => PermissionModule)], + imports: [SharedModule, forwardRef(() => PermissionModule), forwardRef(() => UserModule)], providers: [OrganizationService], exports: [OrganizationService], controllers: [OrganizationController], diff --git a/src/modules/user-management/user.module.ts b/src/modules/user-management/user.module.ts index 0b9cfbc9..1cf2d0d8 100644 --- a/src/modules/user-management/user.module.ts +++ b/src/modules/user-management/user.module.ts @@ -5,9 +5,12 @@ import { PermissionModule } from "@modules/user-management/permission.module"; import { UserBootstrapperService } from "@services/user-management/user-bootstrapper.service"; import { UserService } from "@services/user-management/user.service"; import { UserController } from "@user-management-controller/user.controller"; +import { OrganizationModule } from "./organization.module"; +import { ConfigModule } from "@nestjs/config"; +import configuration from "@config/configuration"; @Module({ - imports: [SharedModule, forwardRef(() => PermissionModule)], + imports: [SharedModule, ConfigModule.forRoot({ load: [configuration] }), forwardRef(() => PermissionModule), forwardRef(() => OrganizationModule)], controllers: [UserController], providers: [UserService, UserBootstrapperService], exports: [UserService], diff --git a/src/resources/resource-paths.ts b/src/resources/resource-paths.ts new file mode 100644 index 00000000..430bc93b --- /dev/null +++ b/src/resources/resource-paths.ts @@ -0,0 +1,8 @@ +import { join } from "path"; + +const goToRootFolder = "../../"; + +export const ChirpstackStateTemplatePath = join( + __dirname, + `${goToRootFolder}resources/chirpstack-state.proto` +); diff --git a/src/services/api-key-management/api-key.service.ts b/src/services/api-key-management/api-key.service.ts index 37208734..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/api-key-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 4f89cdaa..d4948327 100644 --- a/src/services/chirpstack/chirpstack-gateway.service.ts +++ b/src/services/chirpstack/chirpstack-gateway.service.ts @@ -83,7 +83,7 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ const limit = 1000; let allResults: GatewayResponseDto[] = []; let totalCount = 0; - let lastResults; + let lastResults: ListAllGatewaysResponseDto; do { // Default parameters if not set lastResults = await this.getAllWithPagination( @@ -112,6 +112,11 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ }; } + /** + * Fetch gateways individually. This gives us the tags which contain the OS2 organization id. + * This is a very expensive operation, but it's the only way to retrieve gateway tags. + * @param results + */ private async enrichWithOrganizationId(results: GatewayResponseDto[]) { await BluebirdPromise.all( BluebirdPromise.map( diff --git a/src/services/chirpstack/gateway-status-history.service.ts b/src/services/chirpstack/gateway-status-history.service.ts new file mode 100644 index 00000000..8f1b1e4c --- /dev/null +++ b/src/services/chirpstack/gateway-status-history.service.ts @@ -0,0 +1,103 @@ +import { + GatewayGetAllStatusResponseDto, + ListAllGatewayStatusDto, +} from "@dto/chirpstack/backend/gateway-all-status.dto"; +import { GatewayStatus } from "@dto/chirpstack/backend/gateway-status.dto"; +import { GatewayStatusHistory } from "@entities/gateway-status-history.entity"; +import { + GatewayStatusInterval, + gatewayStatusIntervalToDate, +} from "@enum/gateway-status-interval.enum"; +import { Injectable, Logger } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { In, MoreThanOrEqual, Repository } from "typeorm"; +import { ChirpstackGatewayService } from "./chirpstack-gateway.service"; + +type GatewayId = { id: string; name: string }; + +@Injectable() +export class GatewayStatusHistoryService { + constructor( + @InjectRepository(GatewayStatusHistory) + private gatewayStatusHistoryRepository: Repository, + private chirpstackGatewayService: ChirpstackGatewayService + ) {} + private readonly logger = new Logger(GatewayStatusHistoryService.name); + + public async findAllWithChirpstack( + query: ListAllGatewayStatusDto + ): Promise { + // Very expensive operation. Since no gateway data is stored on the backend database, we need + // to get them from Chirpstack. There's no filter by tags support so we must fetch all gateways. + const gateways = await this.chirpstackGatewayService.getAll(query.organizationId); + const gatewayIds = gateways.result.map(gateway => gateway.id); + const fromDate = gatewayStatusIntervalToDate(query.timeInterval); + + const statusHistories = await this.gatewayStatusHistoryRepository.find({ + where: { + mac: In(gatewayIds), + timestamp: MoreThanOrEqual(fromDate), + }, + }); + + const data: GatewayStatus[] = this.mapStatusHistoryToGateways( + gateways.result, + statusHistories + ); + + return { + data, + count: gateways.totalCount, + }; + } + + public async findOne( + gateway: Gateway, + timeInterval: GatewayStatusInterval + ): Promise { + const fromDate = gatewayStatusIntervalToDate(timeInterval); + + const statusHistories = await this.gatewayStatusHistoryRepository.find({ + where: { + mac: gateway.id, + timestamp: MoreThanOrEqual(fromDate), + }, + }); + + return this.mapStatusHistoryToGateway(gateway, statusHistories); + } + + private mapStatusHistoryToGateways( + gateways: Gateway[], + statusHistories: GatewayStatusHistory[] + ): GatewayStatus[] { + return gateways.map(gateway => { + return this.mapStatusHistoryToGateway(gateway, statusHistories); + }); + } + + private mapStatusHistoryToGateway( + gateway: Gateway, + statusHistories: GatewayStatusHistory[] + ) { + const statusTimestamps = statusHistories.reduce( + (res: GatewayStatus["statusTimestamps"], history) => { + if (history.mac === gateway.id) { + res.push({ + timestamp: history.timestamp, + wasOnline: history.wasOnline, + }); + } + + return res; + }, + [] + ); + + return { + id: gateway.id, + name: gateway.name, + statusTimestamps, + }; + } +} diff --git a/src/services/data-management/chirpstack-mqtt-listener.service.ts b/src/services/data-management/chirpstack-mqtt-listener.service.ts index 49c9c75c..b04147c0 100644 --- a/src/services/data-management/chirpstack-mqtt-listener.service.ts +++ b/src/services/data-management/chirpstack-mqtt-listener.service.ts @@ -1,43 +1,74 @@ import { MqttClientId } from "@config/constants/mqtt-constants"; -import { ChirpstackMQTTMessageDto } from "@dto/chirpstack/chirpstack-mqtt-message.dto"; +import { + ChirpstackMQTTConnectionStateMessageDto, + ChirpstackMQTTMessageDto, +} from "@dto/chirpstack/chirpstack-mqtt-message.dto"; +import { ChirpstackMQTTConnectionStateMessage } from "@dto/chirpstack/state/chirpstack-mqtt-state-message.dto"; import { IoTDeviceType } from "@enum/device-type.enum"; +import { hasProps, nameof } from "@helpers/type-helper"; import { Injectable, Logger, OnApplicationBootstrap } from "@nestjs/common"; +import { ChirpstackStateTemplatePath } from "@resources/resource-paths"; import { ReceiveDataService } from "@services/data-management/receive-data.service"; import { IoTDeviceService } from "@services/device-management/iot-device.service"; import * as mqtt from "mqtt"; import { Client } from "mqtt"; +import * as Protobuf from "protobufjs"; @Injectable() export class ChirpstackMQTTListenerService implements OnApplicationBootstrap { constructor( private receiveDataService: ReceiveDataService, private iotDeviceService: IoTDeviceService - ) {} + ) { + const connStateFullTemplate = Protobuf.loadSync(ChirpstackStateTemplatePath); + this.connStateType = connStateFullTemplate.lookupType("ConnState"); + } private readonly logger = new Logger(ChirpstackMQTTListenerService.name); + private readonly connStateType: Protobuf.Type; MQTT_URL = `mqtt://${process.env.CS_MQTT_HOSTNAME || "localhost"}:${ process.env.CS_MQTT_PORT || "1883" }`; client: Client; + private readonly CHIRPSTACK_MQTT_DEVICE_DATA_PREFIX = "application/"; private readonly CHIRPSTACK_MQTT_DEVICE_DATA_TOPIC = - "application/+/device/+/event/up"; + this.CHIRPSTACK_MQTT_DEVICE_DATA_PREFIX + "+/device/+/event/up"; + private readonly CHIRPSTACK_MQTT_GATEWAY_PREFIX = "gateway/"; + private readonly CHIRPSTACK_MQTT_GATEWAY_TOPIC = + this.CHIRPSTACK_MQTT_GATEWAY_PREFIX + "+/state/conn"; public async onApplicationBootstrap(): Promise { this.logger.debug("Pre-init"); + this.client = mqtt.connect(this.MQTT_URL, { clean: true, clientId: MqttClientId, }); this.client.on("connect", () => { this.client.subscribe(this.CHIRPSTACK_MQTT_DEVICE_DATA_TOPIC); + this.client.subscribe(this.CHIRPSTACK_MQTT_GATEWAY_TOPIC); this.client.on("message", async (topic, message) => { this.logger.debug( `Received MQTT - Topic: '${topic}' - message: '${message}'` ); - await this.receiveMqttMessage(message.toString()); + + if (topic.startsWith(this.CHIRPSTACK_MQTT_DEVICE_DATA_PREFIX)) { + await this.receiveMqttMessage(message.toString()); + } else if (topic.startsWith(this.CHIRPSTACK_MQTT_GATEWAY_PREFIX)) { + try { + const decoded = this.connStateType.decode(message); + await this.receiveMqttGatewayStatusMessage(decoded.toJSON()); + } catch (error) { + this.logger.error( + `Gateway status data could not be processed. Error: ${error}` + ); + } + } else { + this.logger.warn("Unrecognized MQTT topic " + topic); + } }); this.logger.debug("Connected to MQTT."); }); @@ -56,10 +87,38 @@ export class ChirpstackMQTTListenerService implements OnApplicationBootstrap { return; } - await this.receiveDataService.sendToKafka( + await this.receiveDataService.sendRawIotDeviceRequestToKafka( iotDevice, message, IoTDeviceType.LoRaWAN.toString() ); } + + async receiveMqttGatewayStatusMessage( + message: Record + ): Promise { + if ( + message && + hasProps( + message, + nameof("gatewayId") + ) && + typeof message.gatewayId === "string" + ) { + const dto: ChirpstackMQTTConnectionStateMessageDto = { + gatewayId: Buffer.from(message.gatewayId, "base64").toString("hex"), + isOnline: message.state === "ONLINE", + }; + const jsonDto = JSON.stringify(dto); + + await this.receiveDataService.sendRawGatewayStateToKafka( + dto.gatewayId, + jsonDto + ); + } else { + this.logger.error( + `Gateway status message is not properly formatted. Gateway id, if any, is ${message?.id}` + ); + } + } } diff --git a/src/services/data-management/device-integration-persistence.service.ts b/src/services/data-management/device-integration-persistence.service.ts index 2150e6d7..9740e292 100644 --- a/src/services/data-management/device-integration-persistence.service.ts +++ b/src/services/data-management/device-integration-persistence.service.ts @@ -1,4 +1,4 @@ -import { RawRequestDto } from "@dto/kafka/raw-request.dto"; +import { RawIoTDeviceRequestDto } from "@dto/kafka/raw-iot-device-request.dto"; import { IoTDevice } from "@entities/iot-device.entity"; import { ReceivedMessageMetadata } from "@entities/received-message-metadata.entity"; import { ReceivedMessageSigFoxSignals } from "@entities/received-message-sigfox-signals.entity"; @@ -16,7 +16,7 @@ import { IoTDeviceService } from "@services/device-management/iot-device.service import { AbstractKafkaConsumer } from "@services/kafka/kafka.abstract.consumer"; import { CombinedSubscribeTo } from "@services/kafka/kafka.decorator"; import { KafkaPayload } from "@services/kafka/kafka.message"; -import { MoreThan, Repository, LessThan } from "typeorm"; +import { LessThan, MoreThan, Repository } from "typeorm"; @Injectable() export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { @@ -50,7 +50,7 @@ export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { @CombinedSubscribeTo(KafkaTopic.RAW_REQUEST, "DeviceIntegrationPersistence") async rawRequestListener(payload: KafkaPayload): Promise { this.logger.debug(`RAW_REQUEST: '${JSON.stringify(payload)}'`); - const dto: RawRequestDto = payload.body; + const dto = payload.body as RawIoTDeviceRequestDto; let relatedIoTDevice; try { relatedIoTDevice = await this.ioTDeviceService.findOne(dto.iotDeviceId); @@ -77,7 +77,7 @@ export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { } private async saveLatestMessage( - dto: RawRequestDto, + dto: RawIoTDeviceRequestDto, relatedIoTDevice: IoTDevice ): Promise { let existingMessage = await this.findExistingRecevedMessage(relatedIoTDevice); @@ -115,7 +115,7 @@ export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { } mapDtoToReceivedMessage( - dto: RawRequestDto, + dto: RawIoTDeviceRequestDto, existingMessage: ReceivedMessage, relatedIoTDevice: IoTDevice ): ReceivedMessage { @@ -204,7 +204,7 @@ export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { } private async saveMessageMetadata( - dto: RawRequestDto, + dto: RawIoTDeviceRequestDto, relatedIoTDevice: IoTDevice ): Promise { // Save this @@ -267,7 +267,7 @@ export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { } mapDtoToNewReceivedMessageMetadata( - dto: RawRequestDto, + dto: RawIoTDeviceRequestDto, relatedIoTDevice: IoTDevice ): ReceivedMessageMetadata { const newMetadata = new ReceivedMessageMetadata(); @@ -306,7 +306,7 @@ export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { if (oldestToDelete.length === 0) { this.logger.debug( - `Less than ${this.maxSigFoxSignalsMessagesPerHour} SigFox stat objects for device ${deviceId} found in database. Deleting no rows.` + `Less than ${this.maxSigFoxSignalsMessagesPerHour} SigFox stat objects for device ${deviceId} found in database. Deleting no rows.` ); return; } @@ -320,7 +320,7 @@ export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { ); } - /** + /** * Clean up SigFox stats for the device if they are older than 1 year * @param deviceId */ diff --git a/src/services/data-management/gateway-persistence.service.ts b/src/services/data-management/gateway-persistence.service.ts new file mode 100644 index 00000000..bd9ca523 --- /dev/null +++ b/src/services/data-management/gateway-persistence.service.ts @@ -0,0 +1,124 @@ +import { ChirpstackMQTTConnectionStateMessageDto } from "@dto/chirpstack/chirpstack-mqtt-message.dto"; +import { RawGatewayStateDto } from "@dto/kafka/raw-gateway-state.dto"; +import { GatewayStatusHistory } from "@entities/gateway-status-history.entity"; +import { KafkaTopic } from "@enum/kafka-topic.enum"; +import { subtractDays, subtractHours } from "@helpers/date.helper"; +import { Injectable, Logger } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { AbstractKafkaConsumer } from "@services/kafka/kafka.abstract.consumer"; +import { CombinedSubscribeTo } from "@services/kafka/kafka.decorator"; +import { KafkaPayload } from "@services/kafka/kafka.message"; +import { LessThan, MoreThan, Repository } from "typeorm"; + +@Injectable() +export class GatewayPersistenceService extends AbstractKafkaConsumer { + private readonly gatewayStatusSavedDays = 30; + /** + * Limit how many messages can be stored for each hour + */ + private readonly maxStatusMessagesPerHour = 10; + + constructor( + @InjectRepository(GatewayStatusHistory) + private gatewayStatusHistoryRepository: Repository + ) { + super(); + } + + private readonly logger = new Logger(GatewayPersistenceService.name); + + protected registerTopic(): void { + this.addTopic(KafkaTopic.RAW_GATEWAY_STATE, "GatewayPersistence"); + } + + // Listen to Kafka event + @CombinedSubscribeTo(KafkaTopic.RAW_GATEWAY_STATE, "GatewayPersistence") + async rawRequestListener(payload: KafkaPayload): Promise { + this.logger.debug(`RAW_GATEWAY_STATE: '${JSON.stringify(payload)}'`); + const dto = payload.body as RawGatewayStateDto; + const messageState = (dto.rawPayload as unknown) as ChirpstackMQTTConnectionStateMessageDto; + + const statusHistory = this.mapDtoToEntity(dto, messageState); + await this.gatewayStatusHistoryRepository.save(statusHistory); + + // Clean up old statuses + await this.deleteStatusHistoriesSinceLastHour( + statusHistory.timestamp, + dto.gatewayId + ); + await this.deleteOldStatusHistories(dto.gatewayId); + } + + private mapDtoToEntity( + dto: RawGatewayStateDto, + messageState: ChirpstackMQTTConnectionStateMessageDto + ) { + const statusHistory = new GatewayStatusHistory(); + statusHistory.mac = dto.gatewayId; + statusHistory.timestamp = dto.unixTimestamp + ? new Date(dto.unixTimestamp) + : new Date(); + statusHistory.wasOnline = !!messageState?.isOnline; + return statusHistory; + } + + /** + * Make sure we never have histories for more than X messages per gateway per hour + * to avoid filling the database + * @param latestMessageTime + * @param gatewayId + */ + private async deleteStatusHistoriesSinceLastHour( + latestMessageTime: Date, + gatewayId: string + ): Promise { + const lastHour = subtractHours(latestMessageTime); + // Find the oldest items since the last hour + const oldestToDelete = await this.gatewayStatusHistoryRepository.find({ + where: { mac: gatewayId, timestamp: MoreThan(lastHour) }, + skip: this.maxStatusMessagesPerHour, + order: { + timestamp: "DESC", + }, + }); + + if (oldestToDelete.length === 0) { + this.logger.debug( + `Less than ${this.maxStatusMessagesPerHour} gateway status' for gateway ${gatewayId} found in database. Deleting no rows.` + ); + return; + } + + const result = await this.gatewayStatusHistoryRepository.delete( + oldestToDelete.map(old => old.id) + ); + + this.logger.debug(`Deleted: ${result.affected} rows from gateway_status_history`); + } + + /** + * Clean up data if it's older than a specified time period + * @param deviceId + */ + private async deleteOldStatusHistories(gatewayId: string): Promise { + const minDate = subtractDays(new Date(), this.gatewayStatusSavedDays); + // Find messages older than a date and delete them + const oldestToDelete = await this.gatewayStatusHistoryRepository.find({ + where: [ + { mac: gatewayId, timestamp: LessThan(minDate) }, + { mac: gatewayId, updatedAt: LessThan(minDate) }, + ], + }); + + if (oldestToDelete.length === 0) { + this.logger.debug("There's no old gateway status messages"); + return; + } + + const result = await this.gatewayStatusHistoryRepository.delete( + oldestToDelete.map(old => old.id) + ); + + this.logger.debug(`Deleted: ${result.affected} rows from gateway_status_history`); + } +} diff --git a/src/services/data-management/payload-decoder-listener.service.ts b/src/services/data-management/payload-decoder-listener.service.ts index 746a4ae2..a76b1453 100644 --- a/src/services/data-management/payload-decoder-listener.service.ts +++ b/src/services/data-management/payload-decoder-listener.service.ts @@ -1,20 +1,17 @@ -import { Injectable, Logger } from "@nestjs/common"; -import { RecordMetadata } from "kafkajs"; -import { VM, VMScript } from "vm2"; - -import { RawRequestDto } from "@dto/kafka/raw-request.dto"; +import { RawIoTDeviceRequestDto } from "@dto/kafka/raw-iot-device-request.dto"; import { TransformedPayloadDto } from "@dto/kafka/transformed-payload.dto"; +import { ListAllConnectionsResponseDto } from "@dto/list-all-connections-response.dto"; import { IoTDevice } from "@entities/iot-device.entity"; import { PayloadDecoder } from "@entities/payload-decoder.entity"; import { KafkaTopic } from "@enum/kafka-topic.enum"; +import { Injectable, Logger } from "@nestjs/common"; import { IoTDevicePayloadDecoderDataTargetConnectionService } from "@services/device-management/iot-device-payload-decoder-data-target-connection.service"; import { AbstractKafkaConsumer } from "@services/kafka/kafka.abstract.consumer"; import { CombinedSubscribeTo } from "@services/kafka/kafka.decorator"; import { KafkaPayload } from "@services/kafka/kafka.message"; - -import { KafkaService } from "../kafka/kafka.service"; +import { RecordMetadata } from "kafkajs"; import * as _ from "lodash"; -import { ListAllConnectionsResponseDto } from "@dto/list-all-connections-response.dto"; +import { KafkaService } from "../kafka/kafka.service"; import { PayloadDecoderExecutorService } from "./payload-decoder-executor.service"; @Injectable() @@ -38,7 +35,7 @@ export class PayloadDecoderListenerService extends AbstractKafkaConsumer { this.logger.debug(`RAW_REQUEST: '${JSON.stringify(payload)}'`); // Fetch related objects - const dto: RawRequestDto = payload.body; + const dto = payload.body as RawIoTDeviceRequestDto; const connections = await this.connectionService.findAllByIoTDeviceIdWithDeviceModel( dto.iotDeviceId ); @@ -52,7 +49,7 @@ export class PayloadDecoderListenerService extends AbstractKafkaConsumer { private async doTransformationsAndSend( connections: ListAllConnectionsResponseDto, - dto: RawRequestDto + dto: RawIoTDeviceRequestDto ) { const uniqueCombinations = _.uniqBy(connections.data, x => x.payloadDecoder?.id); uniqueCombinations.forEach(async connection => { diff --git a/src/services/data-management/receive-data.service.ts b/src/services/data-management/receive-data.service.ts index 48879913..f0a8a07a 100644 --- a/src/services/data-management/receive-data.service.ts +++ b/src/services/data-management/receive-data.service.ts @@ -1,44 +1,78 @@ -import { Injectable, Logger } from "@nestjs/common"; -import { RecordMetadata } from "kafkajs"; - +import { RawGatewayStateDto } from "@dto/kafka/raw-gateway-state.dto"; +import { RawIoTDeviceRequestDto } from "@dto/kafka/raw-iot-device-request.dto"; import { RawRequestDto } from "@dto/kafka/raw-request.dto"; import { KafkaTopic } from "@entities/enum/kafka-topic.enum"; import { IoTDevice } from "@entities/iot-device.entity"; +import { IoTDeviceType } from "@enum/device-type.enum"; +import { Injectable, Logger } from "@nestjs/common"; import { KafkaPayload } from "@services/kafka/kafka.message"; import { KafkaService } from "@services/kafka/kafka.service"; -import { IoTDeviceType } from "@enum/device-type.enum"; +import { RecordMetadata } from "kafkajs"; @Injectable() export class ReceiveDataService { constructor(private kafkaService: KafkaService) {} private readonly logger = new Logger(ReceiveDataService.name); - async sendToKafka( + async sendRawIotDeviceRequestToKafka( iotDevice: IoTDevice, data: string, type: IoTDeviceType[number], timestamp?: number ): Promise { - this.logger.debug(`Received data, sending to Kafka`); - const dto = new RawRequestDto(); + const dto = new RawIoTDeviceRequestDto(); dto.iotDeviceId = iotDevice.id; dto.rawPayload = JSON.parse(data); + const payload = this.buildMessage(dto, type, KafkaTopic.RAW_REQUEST, timestamp); + + await this.doSendToKafka(payload, KafkaTopic.RAW_REQUEST); + } + + async sendRawGatewayStateToKafka( + gatewayId: string, + data: string, + timestamp?: number + ): Promise { + const dto = new RawGatewayStateDto(); + dto.gatewayId = gatewayId; + dto.rawPayload = JSON.parse(data); + const payload = this.buildMessage( + dto, + "GATEWAY", + KafkaTopic.RAW_GATEWAY_STATE, + timestamp + ); + + await this.doSendToKafka(payload, KafkaTopic.RAW_GATEWAY_STATE); + } + + private buildMessage( + dto: RawRequestDto, + type: string, + topicName: KafkaTopic, + timestamp?: number + ): KafkaPayload { + this.logger.debug(`Received data, sending to Kafka`); dto.type = type; // We cannot generically know when it was sent by the device, "now" is accurate enough - dto.unixTimestamp = timestamp != null ? timestamp : new Date().valueOf(); + dto.unixTimestamp = + timestamp !== null && timestamp !== undefined + ? timestamp + : new Date().valueOf(); const payload: KafkaPayload = { messageId: `${type}${new Date().valueOf()}`, body: dto, messageType: `receiveData.${type}`, - topicName: KafkaTopic.RAW_REQUEST, + topicName, }; this.logger.debug(`Made payload: '${JSON.stringify(payload)}'`); - const rawStatus = await this.kafkaService.sendMessage( - KafkaTopic.RAW_REQUEST, - payload - ); + return payload; + } + + private async doSendToKafka(payload: KafkaPayload, topic: KafkaTopic) { + const rawStatus = await this.kafkaService.sendMessage(topic, payload); this.logger.debug(`Sent message to Kafka: ${JSON.stringify(rawStatus)}`); diff --git a/src/services/data-targets/data-target-kafka-listener.service.ts b/src/services/data-targets/data-target-kafka-listener.service.ts index b9a2b137..8363d33e 100644 --- a/src/services/data-targets/data-target-kafka-listener.service.ts +++ b/src/services/data-targets/data-target-kafka-listener.service.ts @@ -39,7 +39,7 @@ export class DataTargetKafkaListenerService extends AbstractKafkaConsumer { async transformedRequestListener(payload: KafkaPayload): Promise { this.logger.debug(`TRANSFORMED_REQUEST: '${JSON.stringify(payload)}'`); - const dto: TransformedPayloadDto = payload.body; + const dto = payload.body as TransformedPayloadDto; let iotDevice: IoTDevice; try { iotDevice = await this.ioTDeviceService.findOne(dto.iotDeviceId); diff --git a/src/services/kafka/kafka.message.ts b/src/services/kafka/kafka.message.ts index 0e5e2b3e..074073cc 100644 --- a/src/services/kafka/kafka.message.ts +++ b/src/services/kafka/kafka.message.ts @@ -1,5 +1,5 @@ export class KafkaPayload { - public body: any; + public body: unknown; public messageId: string; public messageType: string; public topicName: string; diff --git a/src/services/sigfox/generic-sigfox-administation.service.ts b/src/services/sigfox/generic-sigfox-administation.service.ts index 53c1d19c..15fc42d1 100644 --- a/src/services/sigfox/generic-sigfox-administation.service.ts +++ b/src/services/sigfox/generic-sigfox-administation.service.ts @@ -81,7 +81,7 @@ export class GenericSigfoxAdministationService { path, sigfoxGroup, method, - dto = {}, + dto = undefined, useCache = false, }: RequestParameters): Promise { const config = await this.generateAxiosConfig( diff --git a/src/services/user-management/organization.service.ts b/src/services/user-management/organization.service.ts index b219dfa5..f1f53d95 100644 --- a/src/services/user-management/organization.service.ts +++ b/src/services/user-management/organization.service.ts @@ -1,3 +1,14 @@ +import { + BadRequestException, + Inject, + Injectable, + Logger, + forwardRef, + NotFoundException, +} from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { In, Repository } from "typeorm"; + import { DeleteResponseDto } from "@dto/delete-application-response.dto"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; import { @@ -8,17 +19,11 @@ 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 { - BadRequestException, - forwardRef, - Inject, - Injectable, - Logger, - NotFoundException, -} from "@nestjs/common"; -import { InjectRepository } from "@nestjs/typeorm"; -import { In, Repository } from "typeorm"; + import { PermissionService } from "./permission.service"; +import { User } from "@entities/user.entity"; +import { UserService } from "./user.service"; +import { Permission } from "@entities/permissions/permission.entity"; @Injectable() export class OrganizationService { @@ -26,7 +31,9 @@ export class OrganizationService { @InjectRepository(Organization) private organizationRepository: Repository, @Inject(forwardRef(() => PermissionService)) - private permissionService: PermissionService + private permissionService: PermissionService, + @Inject(forwardRef(() => UserService)) + private userService: UserService ) {} private readonly logger = new Logger(OrganizationService.name, true); @@ -60,6 +67,28 @@ export class OrganizationService { return await this.organizationRepository.save(org); } + async updateAwaitingUsers(org: Organization, user: User): Promise { + if (!org.awaitingUsers.find(dbUser => dbUser.id === user.id)) { + org.awaitingUsers.push(user); + } + return await this.organizationRepository.save(org); + } + + async rejectAwaitingUser( + user: User, + organization: Organization + ): Promise { + if (organization.awaitingUsers.find(dbUser => dbUser.id === user.id)) { + const index = organization.awaitingUsers.findIndex( + dbUser => dbUser.id === user.id + ); + organization.awaitingUsers.splice(index, 1); + await this.userService.sendRejectionMail(user, organization); + return await this.organizationRepository.save(organization); + } + throw new NotFoundException(ErrorCodes.UserDoesNotExistInArray); + } + async findAll(): Promise { const [data, count] = await this.organizationRepository.findAndCount({ relations: ["applications", "permissions"], @@ -71,6 +100,59 @@ export class OrganizationService { }; } + mapPermissionsToOrganizations( + permissions: Permission[] + ): Organization[] { + const requestedOrganizations: Organization[] = []; + + for (let index = 0; index < permissions.length; index++) { + if ( + requestedOrganizations.find(org => { + return permissions[index].organization.id === org.id; + }) + ) { + } else { + requestedOrganizations.push(permissions[index].organization); + } + } + + requestedOrganizations.forEach(org => { + org.permissions = []; + permissions.forEach(permission => { + if (org.id === permission.organization.id) { + org.permissions.push(permission); + } + }); + }); + permissions.forEach(permission => { + permission.organization = null; + }); + + return requestedOrganizations; + } + + mapPermissionsToOneOrganization( + permissions: Permission[] + ): Organization { + const org: Organization = new Organization(); + + permissions.map(permission => { + org.id = permission.organization.id; + org.name = permission.organization.name + }); + + org.permissions = []; + permissions.forEach(permission => { + if (org.id === permission.organization.id) { + org.permissions.push(permission); + } + }); + permissions.forEach(permission => { + permission.organization = null; + }); + return org; + } + async findAllPaginated( query?: ListAllEntitiesDto ): Promise { @@ -149,6 +231,11 @@ export class OrganizationService { }, }); } + async findByIdWithUsers(organizationId: number): Promise { + return await this.organizationRepository.findOneOrFail(organizationId, { + relations: ["awaitingUsers"], + }); + } async findByIdWithPermissions(organizationId: number): Promise { return await this.organizationRepository.findOneOrFail(organizationId, { diff --git a/src/services/user-management/permission.service.ts b/src/services/user-management/permission.service.ts index c73539a2..1c88aa1f 100644 --- a/src/services/user-management/permission.service.ts +++ b/src/services/user-management/permission.service.ts @@ -15,12 +15,8 @@ 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/permissions/global-admin-permission.entity"; -import { OrganizationApplicationPermission } from "@entities/permissions/organization-application-permission.entity"; import { Organization } from "@entities/organization.entity"; -import { OrganizationPermission } from "@entities/permissions/organization-permission.entity"; import { Permission } from "@entities/permissions/permission.entity"; -import { ReadPermission } from "@entities/permissions/read-permission.entity"; import { User } from "@entities/user.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { ApplicationService } from "@services/device-management/application.service"; @@ -33,9 +29,9 @@ 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 { OrganizationGatewayAdminPermission } from "@entities/permissions/organization-gateway-admin-permission.entity"; -import { OrganizationUserAdminPermission } from "@entities/permissions/organization-user-admin-permission.entity"; -import { OrganizationApplicationAdminPermission } from "@entities/permissions/organization-application-admin-permission.entity"; +import { PermissionTypeEntity } from "@entities/permissions/permission-type.entity"; +import { PermissionCreator } from "@helpers/permission.helper"; +import { nameof } from "@helpers/type-helper"; @Injectable() export class PermissionService { @@ -53,12 +49,12 @@ export class PermissionService { async createDefaultPermissions( org: Organization, userId: number - ): Promise { + ): 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([ + const r = await entityManager.save([ readPermission, orgApplicationAdminPermission, orgAdminPermission, @@ -77,20 +73,25 @@ export class PermissionService { const organizationGatewayAdminSuffix = `${nameSuffixSeparator}${PermissionType.OrganizationGatewayAdmin}`; const organizationApplicationAdminSuffix = `${nameSuffixSeparator}${PermissionType.OrganizationApplicationAdmin}`; - const readPermission = new ReadPermission(org.name + readSuffix, org, true); - const orgApplicationAdminPermission = new OrganizationApplicationAdminPermission( + const readPermission = PermissionCreator.createRead(org.name + readSuffix, org, true); + const orgApplicationAdminPermission = PermissionCreator.createApplicationAdmin( org.name + organizationApplicationAdminSuffix, org, true ); - const orgAdminPermission = new OrganizationUserAdminPermission( + const orgAdminPermission = PermissionCreator.createUserAdmin( org.name + organizationUserAdminSuffix, org ); - const orgGatewayAadminPermission = new OrganizationGatewayAdminPermission( + 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; orgApplicationAdminPermission.createdBy = userId; @@ -102,13 +103,31 @@ export class PermissionService { return { readPermission, orgApplicationAdminPermission, orgAdminPermission, orgGatewayAadminPermission }; } - async findOrCreateGlobalAdminPermission(): Promise { - const globalAdmin = await getManager().findOne(GlobalAdminPermission); + private setUserIdOnPermissions(permission: Permission, userId: number) { + permission.type.forEach(type => { + type.createdBy = userId; + type.updatedBy = userId; + }); + } + + 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( @@ -119,7 +138,16 @@ export class PermissionService { dto.organizationId ); - const permission = this.createPermission(dto, org); + 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; @@ -128,42 +156,22 @@ export class PermissionService { return await getManager().save(permission); } - private createPermission(dto: CreatePermissionDto, org: Organization): Permission { - switch (dto.level) { - case PermissionType.OrganizationApplicationAdmin: { - return new OrganizationApplicationAdminPermission(dto.name, org); - } - case PermissionType.OrganizationGatewayAdmin: { - return new OrganizationGatewayAdminPermission(dto.name, org); - } - case PermissionType.OrganizationUserAdmin: { - return new OrganizationUserAdminPermission(dto.name, org); - } - case PermissionType.Read: { - return new ReadPermission( - dto.name, - org, - dto.automaticallyAddNewApplications - ); - } - default: - throw new BadRequestException("Bad PermissionLevel"); - } - } - 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 => { @@ -178,11 +186,30 @@ export class PermissionService { x.permissions = _.union(x.permissions, [permission]); }); } - async removeUserFromPermission(permission: Permission, user: User): Promise { user.permissions = user.permissions.filter(x => x.id != permission.id); } + async findManyWithRelations(organizationIds: number[]): Promise + { + const perm = await this.permissionRepository.find({ + relations: ["organization", "users", "type"], + where: {organization: {id: In(organizationIds)}} + }); + + return perm; + } + + async findOneWithRelations(organizationId: number): Promise + { + const perm = await this.permissionRepository.find({ + relations: ["organization", "users", "type"], + where: {organization: {id: organizationId}} + }); + + return perm; + } + async updatePermission( id: number, dto: UpdatePermissionDto, @@ -190,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; @@ -238,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); @@ -286,14 +314,28 @@ 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"], }, }); } - buildPermissionsQuery(): SelectQueryBuilder { + 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 { return this.permissionRepository .createQueryBuilder("permission") .leftJoinAndSelect( @@ -306,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", ]); @@ -327,25 +374,26 @@ export class PermissionService { .getRawMany(); } - async findPermissionsForOrgAdminWithApplications( + async findPermissionsForOrgAdminWithApplications( userId: number ): Promise { return await this.buildPermissionsWithApplicationsQuery() .leftJoin("permission.users", "user") - .where("permission.type = :permType AND user.id = :id", { + .where("permission_type.type = :permType AND user.id = :id", { permType: PermissionType.OrganizationApplicationAdmin, id: userId, }) .getRawMany(); } - buildPermissionsWithApplicationsQuery(): SelectQueryBuilder { + buildPermissionsWithApplicationsQuery(): SelectQueryBuilder { return this.permissionRepository .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", ]); @@ -396,22 +444,21 @@ 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.OrganizationApplicationAdmin) { + } else if (p.permission_type_type == PermissionType.OrganizationApplicationAdmin) { this.addOrUpdateApplicationIds(res.orgToApplicationAdminPermissions, p); - // Also grant writePermission to the application - } else if (p.permission_type == PermissionType.OrganizationGatewayAdmin) { + } else if (p.permission_type_type == PermissionType.OrganizationGatewayAdmin) { res.orgToGatewayAdminPermissions.add(p.organization_id); - } else if (p.permission_type == PermissionType.OrganizationUserAdmin) { + } else if (p.permission_type_type == PermissionType.OrganizationUserAdmin) { res.orgToUserAdminPermissions.add(p.organization_id); - } else if (p.permission_type == PermissionType.Read) { + } else if (p.permission_type_type == PermissionType.Read) { this.addOrUpdateApplicationIds(res.orgToReadPermissions, p); } }); return res; - } + } async findManyByIds(ids: number[]): Promise { return await this.permissionRepository.findByIds(ids); @@ -419,13 +466,13 @@ export class PermissionService { private isOrganizationApplicationAdmin(permissions: PermissionMinimalDto[]) { return permissions.some( - x => x.permission_type == PermissionType.OrganizationApplicationAdmin + x => x.permission_type_type == PermissionType.OrganizationApplicationAdmin ); } private isOrganizationUserAdmin(permissions: PermissionMinimalDto[]) { return permissions.some( - x => x.permission_type == PermissionType.OrganizationUserAdmin + x => x.permission_type_type == PermissionType.OrganizationUserAdmin ); } diff --git a/src/services/user-management/user.service.ts b/src/services/user-management/user.service.ts index 8dbcf412..7237f4f5 100644 --- a/src/services/user-management/user.service.ts +++ b/src/services/user-management/user.service.ts @@ -21,6 +21,13 @@ import { ListAllUsersResponseDto } from "@dto/list-all-users-response.dto"; import { Profile } from "passport-saml"; import { ListAllUsersMinimalResponseDto } from "@dto/list-all-users-minimal-response.dto"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; +import { CreateNewKombitUserDto } from "@dto/user-management/create-new-kombit-user.dto"; +import * as nodemailer from "nodemailer"; +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 { @@ -28,7 +35,8 @@ export class UserService { @InjectRepository(User) private userRepository: Repository, @Inject(forwardRef(() => PermissionService)) - private permissionService: PermissionService + private permissionService: PermissionService, + private configService: ConfigService ) {} private readonly logger = new Logger(UserService.name, true); @@ -41,6 +49,30 @@ export class UserService { ); } + async acceptUser( + user: User, + org: Organization, + newUserPermissions: Permission[] + ): Promise { + user.awaitingConfirmation = false; + + 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(...newUserPermissions); + await this.sendVerificationMail(user, org); + return await this.userRepository.save(user); + } + } + async findOneUserByEmailWithPassword(email: string): Promise { return await this.userRepository.findOne( { email: email }, @@ -62,7 +94,7 @@ export class UserService { getPermissionOrganisationInfo = false, getPermissionUsersInfo = false ): Promise { - const relations = ["permissions"]; + const relations = ["permissions", "requestedOrganizations"]; if (getPermissionOrganisationInfo) { relations.push("permissions.organization"); } @@ -90,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"], }); } @@ -122,6 +154,7 @@ export class UserService { const mappedUser = this.mapDtoToUser(user, dto); mappedUser.createdBy = userId; mappedUser.updatedBy = userId; + mappedUser.showWelcomeScreen = true; await this.setPasswordHash(mappedUser, dto.password); @@ -138,6 +171,7 @@ export class UserService { async createUserFromKombit(profile: Profile): Promise { const user = new User(); await this.mapKombitLoginProfileToUser(user, profile); + user.showWelcomeScreen = true; return await this.userRepository.save(user); } @@ -234,6 +268,19 @@ export class UserService { } } + async newKombitUser( + dto: CreateNewKombitUserDto, + requestedOrganizations: Organization[], + user: User + ): Promise { + user.email = dto.email; + user.awaitingConfirmation = true; + for (let index = 0; index < requestedOrganizations.length; index++) { + await this.sendOrganizationRequestMail(user, requestedOrganizations[index]); + } + return await this.userRepository.save(user); + } + async findManyUsersByIds(userIds: number[]): Promise { return await this.userRepository.findByIds(userIds); } @@ -252,13 +299,13 @@ export class UserService { } const [data, count] = await this.userRepository.findAndCount({ - relations: ["permissions"], + relations: ["permissions", "permissions.type"], take: +query.limit, skip: +query.offset, order: sorting, - where: { - isSystemUser: false - } + where: { + isSystemUser: false, + }, }); return { @@ -294,4 +341,131 @@ export class UserService { users: result, }; } + + basicMailTransporter(): nodemailer.Transporter { + return nodemailer.createTransport({ + host: this.configService.get("email.host"), + port: this.configService.get("email.port"), + auth: { + user: this.configService.get("email.user"), + pass: this.configService.get("email.pass") + }, + }); + } + + async sendOrganizationRequestMail( + user: User, + organization: Organization + ): Promise { + const emails = await this.getOrgAdminEmails(organization); + const transporter: nodemailer.Transporter = this.basicMailTransporter(); + try { + await transporter.verify(); + } catch (error) { + throw new BadRequestException(ErrorCodes.SendMailError); + } + try { + await transporter.sendMail({ + from: this.configService.get("email.from"), // sender address + to: emails, // list of receivers + subject: "Ny ansøgning til din organisation!", // Subject line + html: `

Ny ansøgning om tilladelse til organisationen "${organization.name}"!

Klik her for at bekræfte eller afvise brugeren med navnet: "${user.name}."`, // html body + }); + } catch (error) { + throw new BadRequestException(ErrorCodes.SendMailError); + } + } + + async sendRejectionMail(user: User, organization: Organization): Promise { + const transporter = this.basicMailTransporter(); + + try { + await transporter.verify(); + } catch (error) { + throw new BadRequestException(ErrorCodes.SendMailError); + } + try { + await transporter.sendMail({ + from: this.configService.get("email.from"), // sender address + to: user.email, // list of receivers + subject: "Ansøgning afvist!", // Subject line + html: `

Din ansøgning om bekræftelse hos "${organization.name}" er afvist!

`, // html body + }); + } catch (error) { + throw new BadRequestException(ErrorCodes.SendMailError); + } + } + + async sendVerificationMail(user: User, organization: Organization): Promise { + const transporter = this.basicMailTransporter(); + + try { + await transporter.verify(); + } catch (error) { + throw new BadRequestException(ErrorCodes.SendMailError); + } + try { + await transporter.sendMail({ + from: this.configService.get("email.from"), // sender address + to: user.email, // list of receivers + subject: "Ansøgning bekræftet!", // Subject line + html: `

Din ansøgning om bekræftelse hos "${organization.name}" er godkendt!

`, // html body + }); + } catch (error) { + throw new BadRequestException(ErrorCodes.SendMailError); + } + } + + async getOrgAdminEmails(organization: Organization): Promise { + const emails: string[] = []; + const globalAdminPermission: Permission = await this.permissionService.getGlobalPermission(); + organization.permissions.forEach(permission => { + if (isPermissionType(permission, PermissionType.OrganizationUserAdmin)) { + if (permission.users.length > 0) { + permission.users.forEach(user => { + emails.push(user.email); + }); + } else { + globalAdminPermission.users.forEach(user => { + emails.push(user.email); + }); + } + } + }); + return emails; + } + + async getAwaitingUsers( + organizationId: number, + query?: ListAllEntitiesDto + ): Promise { + let orderBy = `user.id`; + if ( + query.orderOn !== null && + (query.orderOn === "id" || query.orderOn === "name") + ) { + orderBy = `user.${query.orderOn}`; + } + const order: "DESC" | "ASC" = + query?.sort?.toLocaleUpperCase() == "DESC" ? "DESC" : "ASC"; + + const [data, count] = await this.userRepository + .createQueryBuilder("user") + .innerJoin("user.requestedOrganizations", "org") + .where("org.id = :id", { id: organizationId }) + .take(+query.limit) + .skip(+query.offset) + .orderBy(orderBy, order) + .getManyAndCount(); + + return { + data: data.map(x => x as UserResponseDto), + count: count, + }; + } + + async hideWelcome(id: number): Promise { + const res = await this.userRepository.update(id, { showWelcomeScreen: false }); + return !!res.affected + } } diff --git a/test/e2e/crud/application.e2e-spec.ts b/test/e2e/crud/application.e2e-spec.ts index 5faa1d6f..8e0f7261 100644 --- a/test/e2e/crud/application.e2e-spec.ts +++ b/test/e2e/crud/application.e2e-spec.ts @@ -553,11 +553,11 @@ 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 orgAppAdminPerm = user.permissions.find( - x => x.type == PermissionType.OrganizationApplicationAdmin + x => x.type.some(({ type }) => type === PermissionType.OrganizationApplicationAdmin) ) as OrganizationApplicationAdminPermission; orgAppAdminPerm.applications = [app1]; await getManager().save([readPerm, orgAppAdminPerm]); @@ -617,12 +617,12 @@ 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 orgAppAdminPerm = org.permissions.find( - x => x.type == PermissionType.OrganizationApplicationAdmin + x => x.type.some(({ type }) => type === PermissionType.OrganizationApplicationAdmin) ) as OrganizationApplicationAdminPermission; expect(orgAppAdminPerm.automaticallyAddNewApplications).toBeTruthy(); 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/permission.e2e-spec.ts b/test/e2e/crud/permission.e2e-spec.ts index 8926d1d8..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/permissions/organization-application-permission.entity"; -import { ReadPermission } from "@entities/permissions/read-permission.entity"; import { User } from "@entities/user.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { AuthModule } from "@modules/user-management/auth.module"; @@ -162,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 d003f982..37383c88 100644 --- a/test/e2e/crud/search.e2e-spec.ts +++ b/test/e2e/crud/search.e2e-spec.ts @@ -128,7 +128,7 @@ describe("SearchController (e2e)", () => { orgAdminJwt = generateValidJwtForUser(orgAdminUser); const orgAppAdminPermission = org1.permissions.find( - x => x.type == PermissionType.OrganizationApplicationAdmin + x => x.type.some(({ type }) => type === PermissionType.OrganizationApplicationAdmin) ) as OrganizationApplicationAdminPermission; orgAppAdminPermission.applications = [app1_1]; await getManager().save(orgAppAdminPermission); diff --git a/test/e2e/test-helpers.ts b/test/e2e/test-helpers.ts index 9736c5fc..c9367d02 100644 --- a/test/e2e/test-helpers.ts +++ b/test/e2e/test-helpers.ts @@ -271,10 +271,10 @@ export async function generateSavedOrganizationAdminUser( } export async function generateSavedReadWriteUser(org: Organization): Promise { - const appAdminPerm = org.permissions.find(x => x.type == PermissionType.OrganizationApplicationAdmin); - const gatewayAdminPerm = org.permissions.find(x => x.type == PermissionType.OrganizationGatewayAdmin); - const userAdminPerm = org.permissions.find(x => x.type == PermissionType.OrganizationUserAdmin); - const readPerm = org.permissions.find(x => x.type == PermissionType.Read); + 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]) ); @@ -742,6 +742,7 @@ export function generateLoRaWANRawRequestDto(iotDeviceId?: number): RawRequestDt }`), iotDeviceId: iotDeviceId || 1, unixTimestamp: 1596921546, + type: IoTDeviceType.LoRaWAN, }; } @@ -788,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 1510e351..783580dd 100644 --- a/test/unit/device-integration-persistence.service.spec.ts +++ b/test/unit/device-integration-persistence.service.spec.ts @@ -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 () => {