diff --git a/src/controllers/user-management/new-kombit-creation.controller.ts b/src/controllers/user-management/new-kombit-creation.controller.ts index df9f3662..f3d62adf 100644 --- a/src/controllers/user-management/new-kombit-creation.controller.ts +++ b/src/controllers/user-management/new-kombit-creation.controller.ts @@ -165,11 +165,7 @@ export class NewKombitCreationController { 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 - ); + const { passwordHash, ...user } = await this.userService.findOne(id, getExtendedInfo); return user; } catch (err) { diff --git a/src/controllers/user-management/user.controller.ts b/src/controllers/user-management/user.controller.ts index b6f98bbb..81fbab4e 100644 --- a/src/controllers/user-management/user.controller.ts +++ b/src/controllers/user-management/user.controller.ts @@ -33,7 +33,11 @@ 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, checkIfUserHasAccessToOrganization, OrganizationAccessScope } 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"; @@ -56,7 +60,7 @@ export class UserController { constructor( private userService: UserService, private organizationService: OrganizationService - ) { } + ) {} private readonly logger = new Logger(UserController.name); @@ -111,7 +115,11 @@ export class UserController { @Req() req: AuthenticatedRequest, @Body() body: RejectUserDto ): Promise { - checkIfUserHasAccessToOrganization(req, body.orgId, OrganizationAccessScope.UserAdministrationWrite); + checkIfUserHasAccessToOrganization( + req, + body.orgId, + OrganizationAccessScope.UserAdministrationWrite + ); const user = await this.userService.findOne(body.userIdToReject); const organization = await this.organizationService.findByIdWithUsers(body.orgId); @@ -130,17 +138,22 @@ export class UserController { // Verify that we have admin access to the user and that the user is on an organization const dbUser = await this.userService.findOneWithOrganizations(id); - // Requesting user has to be admin for at least one organization containing the user + // Requesting user has to be admin for at least one organization containing the user // _OR_ be global admin - if (!req.user.permissions.isGlobalAdmin && !dbUser.permissions.some(perm => req.user.permissions.hasUserAdminOnOrganization(perm.organization.id))) { + if ( + !req.user.permissions.isGlobalAdmin && + !dbUser.permissions.some(perm => + req.user.permissions.hasUserAdminOnOrganization(perm.organization.id) + ) + ) { throw new ForbiddenException(); - } - + } + // Only a global admin can modify a global admin user if (dto.globalAdmin) { checkIfUserIsGlobalAdmin(req); } - + // Don't leak the passwordHash const { passwordHash: _, ...user } = await this.userService.updateUser( id, @@ -176,7 +189,7 @@ export class UserController { req.user.username ); - return wasOk + return wasOk; } @Get("/awaitingUsers") @@ -232,7 +245,6 @@ export class UserController { // Don't leak the passwordHash const { passwordHash: _, ...user } = await this.userService.findOne( id, - getExtendedInfo, getExtendedInfo ); @@ -243,7 +255,10 @@ export class UserController { } @Get("organizationUsers/:organizationId") - @ApiOperation({ summary: "Get all users for an organization. Requires UserAdmin priviledges for the specified organization" }) + @ApiOperation({ + summary: + "Get all users for an organization. Requires UserAdmin priviledges for the specified organization", + }) async findByOrganizationId( @Req() req: AuthenticatedRequest, @Param("organizationId", new ParseIntPipe()) organizationId: number, @@ -252,7 +267,9 @@ export class UserController { try { // Check if user has access to organization if (!req.user.permissions.hasUserAdminOnOrganization(organizationId)) { - throw new ForbiddenException("User does not have org admin permissions for this organization"); + throw new ForbiddenException( + "User does not have org admin permissions for this organization" + ); } // Get user objects diff --git a/src/entities/dto/chirpstack/service-profile.dto.ts b/src/entities/dto/chirpstack/service-profile.dto.ts index 3fda4717..d0e3caf5 100644 --- a/src/entities/dto/chirpstack/service-profile.dto.ts +++ b/src/entities/dto/chirpstack/service-profile.dto.ts @@ -32,14 +32,14 @@ export class ServiceProfileDto { @ApiProperty({ required: false }) @IsInt() - @Min(0) - @Max(5) + @Min(0, { message: "Max data rate må ikke være negativ" }) + @Max(7, { message: "Max data rate må ikke være større end 7" }) drMax?: number; @ApiProperty({ required: true }) @IsInt() - @Min(0) - @Max(5) + @Min(0, { message: "Min data rate må ikke være negativ" }) + @Max(7, { message: "Min data rate må ikke være større end 7" }) drMin?: number; @ApiProperty({ required: false }) diff --git a/src/entities/dto/list-all-permissions.dto.ts b/src/entities/dto/list-all-permissions.dto.ts index 47b77ff9..8728eff9 100644 --- a/src/entities/dto/list-all-permissions.dto.ts +++ b/src/entities/dto/list-all-permissions.dto.ts @@ -3,7 +3,7 @@ import { ListAllEntitiesDto } from "./list-all-entities.dto"; export class ListAllPermissionsDto extends ListAllEntitiesDto { @ApiProperty({ type: String, required: false }) - organisationId?: number; + organisationId?: string; @ApiProperty({ type: String, required: false }) userId?: string; diff --git a/src/modules/data-management/payload-decoder-kafka.module.ts b/src/modules/data-management/payload-decoder-kafka.module.ts index f692e53f..80c21c46 100644 --- a/src/modules/data-management/payload-decoder-kafka.module.ts +++ b/src/modules/data-management/payload-decoder-kafka.module.ts @@ -10,6 +10,7 @@ import { SharedModule } from "@modules/shared.module"; import { HttpModule } from "@nestjs/axios"; import { Module } from "@nestjs/common"; import { PayloadDecoderListenerService } from "@services/data-management/payload-decoder-listener.service"; +import { ChirpstackAdministrationModule } from "@modules/device-integrations/chirpstack-administration.module"; @Module({ imports: [ @@ -22,6 +23,7 @@ import { PayloadDecoderListenerService } from "@services/data-management/payload HttpModule, ApplicationModule, PayloadDecoderExecutorModuleModule, + ChirpstackAdministrationModule, ], controllers: [PayloadDecoderController], providers: [PayloadDecoderListenerService], diff --git a/src/modules/device-management/application.module.ts b/src/modules/device-management/application.module.ts index 2e46329f..769ed725 100644 --- a/src/modules/device-management/application.module.ts +++ b/src/modules/device-management/application.module.ts @@ -1,5 +1,4 @@ -import { Module, forwardRef } from "@nestjs/common"; - +import { forwardRef, Module } from "@nestjs/common"; import { ApplicationController } from "@admin-controller/application.controller"; import { SharedModule } from "@modules/shared.module"; import { OrganizationModule } from "@modules/user-management/organization.module"; @@ -7,6 +6,7 @@ import { ApplicationService } from "@services/device-management/application.serv import { ChirpstackAdministrationModule } from "@modules/device-integrations/chirpstack-administration.module"; import { PermissionModule } from "@modules/user-management/permission.module"; import { MulticastModule } from "./multicast.module"; +import { DataTargetModule } from "@modules/device-management/data-target.module"; @Module({ imports: [ @@ -14,6 +14,7 @@ import { MulticastModule } from "./multicast.module"; forwardRef(() => OrganizationModule), forwardRef(() => PermissionModule), forwardRef(() => MulticastModule), // because of circular reference + forwardRef(() => DataTargetModule), ChirpstackAdministrationModule, ], exports: [ApplicationService], diff --git a/src/modules/device-management/data-target.module.ts b/src/modules/device-management/data-target.module.ts index 7cf061fb..902773c9 100644 --- a/src/modules/device-management/data-target.module.ts +++ b/src/modules/device-management/data-target.module.ts @@ -3,7 +3,7 @@ import configuration from "@config/configuration"; import { ApplicationModule } from "@modules/device-management/application.module"; import { SharedModule } from "@modules/shared.module"; import { OrganizationModule } from "@modules/user-management/organization.module"; -import { Module } from "@nestjs/common"; +import { forwardRef, Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { DataTargetService } from "@services/data-targets/data-target.service"; import { OS2IoTMail } from "@services/os2iot-mail.service"; @@ -15,7 +15,7 @@ import { @Module({ imports: [ SharedModule, - ApplicationModule, + forwardRef(() => ApplicationModule), OrganizationModule, ConfigModule.forRoot({ load: [configuration] }), ], diff --git a/src/services/data-management/payload-decoder-executor.service.ts b/src/services/data-management/payload-decoder-executor.service.ts index 0ed0131d..624fb204 100644 --- a/src/services/data-management/payload-decoder-executor.service.ts +++ b/src/services/data-management/payload-decoder-executor.service.ts @@ -24,11 +24,11 @@ export class PayloadDecoderExecutorService { rawPayload: JSON ): string { const vm2Logger = new Logger(`${PayloadDecoderExecutorService.name}-VM2`); - + // Make copies of inputs to untrusted code to avoid unintended side effects if the code chooses to modify these const iotDeviceCopy = JSON.parse(JSON.stringify(iotDevice)); const payloadCopy = JSON.parse(JSON.stringify(rawPayload)); - + const vm = new VM({ timeout: 5000, sandbox: { diff --git a/src/services/data-management/payload-decoder-listener.service.ts b/src/services/data-management/payload-decoder-listener.service.ts index cb3b8a71..4bf46e9f 100644 --- a/src/services/data-management/payload-decoder-listener.service.ts +++ b/src/services/data-management/payload-decoder-listener.service.ts @@ -13,11 +13,14 @@ import { RecordMetadata } from "kafkajs"; import * as _ from "lodash"; import { KafkaService } from "../kafka/kafka.service"; import { PayloadDecoderExecutorService } from "./payload-decoder-executor.service"; +import { IoTDeviceType } from "@enum/device-type.enum"; +import { ChirpstackDeviceService } from "@services/chirpstack/chirpstack-device.service"; @Injectable() export class PayloadDecoderListenerService extends AbstractKafkaConsumer { constructor( private connectionService: IoTDevicePayloadDecoderDataTargetConnectionService, + private chirpstackDeviceService: ChirpstackDeviceService, private kafkaService: KafkaService, private executor: PayloadDecoderExecutorService ) { @@ -52,10 +55,10 @@ export class PayloadDecoderListenerService extends AbstractKafkaConsumer { dto: RawIoTDeviceRequestDto ) { const uniqueCombinations = _.uniqBy(connections.data, x => x.payloadDecoder?.id); - uniqueCombinations.forEach(async connection => { + for (const connection of uniqueCombinations) { try { const iotDevice = connection.iotDevices.find( - x => x.id == dto.iotDeviceId + x => x.id === dto.iotDeviceId ); await this.decodeAndSendTransformed( @@ -66,7 +69,7 @@ export class PayloadDecoderListenerService extends AbstractKafkaConsumer { } catch (err) { this.logger.error(err); } - }); + } } private async decodeAndSendTransformed( @@ -99,10 +102,21 @@ export class PayloadDecoderListenerService extends AbstractKafkaConsumer { `Decoding payload of IoT-Device ${relatedIoTDevice.id} with decoder ${payloadDecoder?.id}` ); + let localDevice = relatedIoTDevice; + // Check if lorawanSettings are read, if they are the iotDevice needs enrichment + if ( + relatedIoTDevice.type === IoTDeviceType.LoRaWAN && + payloadDecoder.decodingFunction.includes("lorawanSettings") + ) { + localDevice = await this.chirpstackDeviceService.enrichLoRaWANDevice( + relatedIoTDevice + ); + } + // Decode the payload - res = await this.executor.callUntrustedCode( + res = this.executor.callUntrustedCode( payloadDecoder.decodingFunction, - relatedIoTDevice, + localDevice, rawPayload ); diff --git a/src/services/data-targets/data-target.service.ts b/src/services/data-targets/data-target.service.ts index d36f9ca0..aea365d3 100644 --- a/src/services/data-targets/data-target.service.ts +++ b/src/services/data-targets/data-target.service.ts @@ -12,12 +12,21 @@ import { OpenDataDkDataset } from "@entities/open-data-dk-dataset.entity"; import { dataTargetTypeMap } from "@enum/data-target-type-mapping"; import { DataTargetType } from "@enum/data-target-type.enum"; import { ErrorCodes } from "@enum/error-codes.enum"; -import { BadRequestException, Inject, Injectable, Logger } from "@nestjs/common"; +import { + BadRequestException, + forwardRef, + Inject, + Injectable, + Logger, +} from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { ApplicationService } from "@services/device-management/application.service"; import { OS2IoTMail } from "@services/os2iot-mail.service"; import { DeleteResult, Repository, SelectQueryBuilder } from "typeorm"; -import { CLIENT_SECRET_PROVIDER, ClientSecretProvider } from "../../helpers/fiware-token.helper"; +import { + CLIENT_SECRET_PROVIDER, + ClientSecretProvider, +} from "../../helpers/fiware-token.helper"; import { User } from "@entities/user.entity"; @Injectable() @@ -27,9 +36,11 @@ export class DataTargetService { private dataTargetRepository: Repository, @InjectRepository(User) private userRepository: Repository, + @Inject(forwardRef(() => ApplicationService)) private applicationService: ApplicationService, - @Inject(CLIENT_SECRET_PROVIDER) private clientSecretProvider: ClientSecretProvider, - private oS2IoTMail: OS2IoTMail, + @Inject(CLIENT_SECRET_PROVIDER) + private clientSecretProvider: ClientSecretProvider, + private oS2IoTMail: OS2IoTMail ) {} private readonly logger = new Logger(DataTargetService.name); @@ -97,7 +108,7 @@ export class DataTargetService { ): Promise { const res = await this.dataTargetRepository .createQueryBuilder("dt") - .addSelect('dt.clientSecret') + .addSelect("dt.clientSecret") .innerJoin( "iot_device_payload_decoder_data_target_connection", "con", diff --git a/src/services/device-management/application.service.ts b/src/services/device-management/application.service.ts index 9823b107..ed7152d2 100644 --- a/src/services/device-management/application.service.ts +++ b/src/services/device-management/application.service.ts @@ -27,6 +27,7 @@ import { OrganizationService } from "@services/user-management/organization.serv import { PermissionService } from "@services/user-management/permission.service"; import { DeleteResult, In, Repository } from "typeorm"; import { MulticastService } from "./multicast.service"; +import { DataTargetService } from "@services/data-targets/data-target.service"; @Injectable() export class ApplicationService { @@ -40,7 +41,9 @@ export class ApplicationService { private multicastService: MulticastService, private chirpstackDeviceService: ChirpstackDeviceService, @Inject(forwardRef(() => PermissionService)) - private permissionService: PermissionService + private permissionService: PermissionService, + @Inject(forwardRef(() => DataTargetService)) + private dataTargetService: DataTargetService ) {} async findAndCountInList( @@ -262,7 +265,7 @@ export class ApplicationService { async delete(id: number): Promise { const application = await this.applicationRepository.findOne({ where: { id }, - relations: ["iotDevices", "multicasts"], + relations: ["iotDevices", "multicasts", "dataTargets"], }); // Don't allow delete if this application contains any sigfox devices. @@ -274,6 +277,10 @@ export class ApplicationService { throw new ConflictException(ErrorCodes.DeleteNotAllowedHasSigfoxDevice); } + for (const dataTarget of application.dataTargets) { + await this.dataTargetService.delete(dataTarget.id); + } + // Delete all LoRaWAN devices in ChirpStack const loRaWANDevices = application.iotDevices.filter( device => device.type === IoTDeviceType.LoRaWAN @@ -293,7 +300,6 @@ export class ApplicationService { dbMulticast.lorawanMulticastDefinition.chirpstackGroupId ); } - return this.applicationRepository.delete(id); } diff --git a/src/services/user-management/permission.service.ts b/src/services/user-management/permission.service.ts index dc3cbc82..e10f76db 100644 --- a/src/services/user-management/permission.service.ts +++ b/src/services/user-management/permission.service.ts @@ -280,7 +280,10 @@ export class PermissionService { } if (orgs) { qb = qb.andWhere({ organization: In(orgs) }); - } else if (query?.organisationId) { + } else if ( + query?.organisationId !== undefined && + query.organisationId !== "undefined" + ) { qb = qb.andWhere("org.id = :orgId", { orgId: +query.organisationId }); } diff --git a/src/services/user-management/user.service.ts b/src/services/user-management/user.service.ts index 42b2b51c..3c8826bc 100644 --- a/src/services/user-management/user.service.ts +++ b/src/services/user-management/user.service.ts @@ -39,7 +39,7 @@ export class UserService { @Inject(forwardRef(() => PermissionService)) private permissionService: PermissionService, private configService: ConfigService, - private oS2IoTMail: OS2IoTMail, + private oS2IoTMail: OS2IoTMail ) {} private readonly logger = new Logger(UserService.name, { timestamp: true }); @@ -90,17 +90,13 @@ export class UserService { }); } - async findOne( - id: number, - getPermissionOrganisationInfo = false, - getPermissionUsersInfo = false - ): Promise { + async findOne(id: number, getExtendedInformation: boolean = false): Promise { const relations = ["permissions", "requestedOrganizations"]; - if (getPermissionOrganisationInfo) { + + if (getExtendedInformation) { relations.push("permissions.organization"); - } - if (getPermissionUsersInfo) { relations.push("permissions.users"); + relations.push("permissions.type"); } return await this.userRepository.findOne({ @@ -221,7 +217,7 @@ export class UserService { if (user.nameId != null) { if (dto.name && user.name != dto.name) { throw new BadRequestException(ErrorCodes.CannotModifyOnKombitUser); - } + } if (dto.password) { throw new BadRequestException(ErrorCodes.CannotModifyOnKombitUser); } @@ -352,11 +348,13 @@ export class UserService { } const order: "DESC" | "ASC" = query?.sort?.toLocaleUpperCase() == "DESC" ? "DESC" : "ASC"; - + const [data, count] = await this.userRepository .createQueryBuilder("user") .innerJoin("user.permissions", "p") - .where('"p"."organizationId" = :organizationId', { organizationId: organizationId }) + .where('"p"."organizationId" = :organizationId', { + organizationId: organizationId, + }) .take(+query.limit) .skip(+query.offset) .orderBy(orderBy, order)