Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export class ChirpstackGatewayController {

@Put("updateGatewayOrganization/:id")
@ApiProduces("application/json")
@ApiOperation({ summary: "Create a new Chirpstack Gateway" })
@ApiOperation({ summary: "Update gateway organization" })
@ApiBadRequestResponse()
@GatewayAdmin()
async changeOrganization(
Expand Down
16 changes: 13 additions & 3 deletions src/controllers/admin-controller/data-target-log.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import { ComposeAuthGuard } from "@auth/compose-auth.guard";
import { Read } from "@auth/roles.decorator";
import { RolesGuard } from "@auth/roles.guard";
import { ApiAuth } from "@auth/swagger-auth-decorator";
import { AuthenticatedRequest } from "@dto/internal/authenticated-request";
import { DatatargetLog } from "@entities/datatarget-log.entity";
import { Controller, Get, Param, ParseIntPipe, UseGuards } from "@nestjs/common";
import { ApplicationAccessScope, checkIfUserHasAccessToApplication } from "@helpers/security-helper";
import { Controller, Get, Param, ParseIntPipe, Req, UseGuards } from "@nestjs/common";
import { ApiForbiddenResponse, ApiTags, ApiUnauthorizedResponse } from "@nestjs/swagger";
import { InjectRepository } from "@nestjs/typeorm";
import { DataTargetService } from "@services/data-targets/data-target.service";
import { Repository } from "typeorm";

@ApiTags("Data Target Logs")
Expand All @@ -18,11 +21,18 @@ import { Repository } from "typeorm";
export class DatatargetLogController {
constructor(
@InjectRepository(DatatargetLog)
private datatargetLogRepository: Repository<DatatargetLog>
private datatargetLogRepository: Repository<DatatargetLog>,
private dataTargetService: DataTargetService
) {}

@Get(":datatargetId")
async getDatatargetLogs(@Param("datatargetId", new ParseIntPipe()) datatargetId: number): Promise<DatatargetLog[]> {
async getDatatargetLogs(
@Req() req: AuthenticatedRequest,
@Param("datatargetId", new ParseIntPipe()) datatargetId: number
): Promise<DatatargetLog[]> {
const dataTarget = await this.dataTargetService.findOne(datatargetId);
checkIfUserHasAccessToApplication(req, dataTarget.application.id, ApplicationAccessScope.Read);

return await this.datatargetLogRepository.find({
where: {
datatarget: { id: datatargetId },
Expand Down
19 changes: 15 additions & 4 deletions src/controllers/admin-controller/data-target.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,18 @@ export class DataTargetController {
@Get(":id")
@ApiOperation({ summary: "Find DataTarget by id" })
async findOne(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise<DataTarget> {
let dataTarget;
try {
dataTarget = await this.dataTargetService.findOneWithHasRecentError(id);
} catch (err) {
throw new NotFoundException(ErrorCodes.IdDoesNotExists);
}

try {
const dataTarget = await this.dataTargetService.findOneWithHasRecentError(id);
checkIfUserHasAccessToApplication(req, dataTarget.application.id, ApplicationAccessScope.Read);
return dataTarget;
} catch (err) {
throw new NotFoundException(ErrorCodes.IdDoesNotExists);
throw err;
}
}

Expand Down Expand Up @@ -197,8 +203,13 @@ export class DataTargetController {

@Post("testDataTarget")
@ApiOperation({ summary: "Send a ping or test data packet to a data target" })
async testDataTarget(@Body() testDto: TestDataTargetDto): Promise<TestDataTargetResultDto> {
async testDataTarget(
@Req() req: AuthenticatedRequest,
@Body() testDto: TestDataTargetDto
): Promise<TestDataTargetResultDto> {
const dataTarget = await this.dataTargetService.findOne(testDto.dataTargetId);
checkIfUserHasAccessToApplication(req, dataTarget.application.id, ApplicationAccessScope.Read);
// Send package
return await this.dataTargetService.testDataTarget(testDto);
return await this.dataTargetService.testDataTarget(testDto, dataTarget);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,38 +56,6 @@ export class IoTDevicePayloadDecoderDataTargetConnectionController {
private iotDeviceService: IoTDeviceService
) {}

@Get()
@ApiProduces("application/json")
@ApiOperation({
summary: "Find all connections between IoT-Devices, PayloadDecoders and DataTargets (paginated)",
})
@ApiResponse({
status: 200,
description: "Success",
type: ListAllApplicationsResponseDto,
})
async findAll(
@Req() req: AuthenticatedRequest,
@Query() query?: ListAllEntitiesDto
): Promise<ListAllConnectionsResponseDto> {
if (req.user.permissions.isGlobalAdmin) {
return await this.service.findAndCountWithPagination(query);
} else {
const allowed = req.user.permissions.getAllApplicationsWithAtLeastRead();
return await this.service.findAndCountWithPagination(query, allowed);
}
}

@Get(":id")
@ApiNotFoundResponse({
description: "If the id of the entity doesn't exist",
})
async findOne(
@Req() req: AuthenticatedRequest,
@Param("id", new ParseIntPipe()) id: number
): Promise<IoTDevicePayloadDecoderDataTargetConnection> {
return await this.service.findOne(id);
}

@Get("byIoTDevice/:id")
@ApiOperation({
Expand All @@ -104,24 +72,6 @@ export class IoTDevicePayloadDecoderDataTargetConnectionController {
}
}

@Get("byPayloadDecoder/:id")
@ApiOperation({
summary: "Find all connections by PayloadDecoder id",
})
async findByPayloadDecoderId(
@Req() req: AuthenticatedRequest,
@Param("id", new ParseIntPipe()) id: number
): Promise<ListAllConnectionsResponseDto> {
if (req.user.permissions.isGlobalAdmin) {
return await this.service.findAllByPayloadDecoderId(id);
} else {
return await this.service.findAllByPayloadDecoderId(
id,
req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead()
);
}
}

@Get("byDataTarget/:id")
@ApiOperation({
summary: "Find all connections by DataTarget id",
Expand Down Expand Up @@ -196,7 +146,7 @@ export class IoTDevicePayloadDecoderDataTargetConnectionController {
}

private async checkIfUpdateIsAllowed(updateDto: UpdateConnectionDto, req: AuthenticatedRequest, id: number) {
const newIotDevice = await this.iotDeviceService.findOne(updateDto.iotDeviceIds[0]);
const newIotDevice = await this.iotDeviceService.findOneWithApplicationAndMetadata(updateDto.iotDeviceIds[0]);
checkIfUserHasAccessToApplication(req, newIotDevice.application.id, ApplicationAccessScope.Write);
const oldConnection = await this.service.findOne(id);
await this.checkUserHasWriteAccessToAllIotDevices(updateDto.iotDeviceIds, req);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,29 @@ export class IoTDevicePayloadDecoderController {
@Query() query: PayloadDecoderIoDeviceMinimalQuery
): Promise<ListAllIoTDevicesMinimalResponseDto> {
try {
return await this.iotDeviceService.findAllByPayloadDecoder(req, payloadDecoderId, +query.limit, +query.offset);
const iotDevices = await this.iotDeviceService.findAllByPayloadDecoder(
req,
payloadDecoderId,
+query.limit,
+query.offset
);

if (req.user.permissions.isGlobalAdmin) {
return iotDevices;
}

const allowedAppIds = req.user.permissions.getAllApplicationsWithAtLeastRead();

const filteredIotDevices = iotDevices.data.filter(device =>
allowedAppIds.find(appId => appId === device.applicationId)
);

const response: ListAllIoTDevicesMinimalResponseDto = {
data: filteredIotDevices,
count: filteredIotDevices.length,
};

return response;
} catch (err) {
throw new NotFoundException(ErrorCodes.IdDoesNotExists);
}
Expand Down
1 change: 1 addition & 0 deletions src/controllers/admin-controller/iot-device.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ export class IoTDeviceController {
return new StreamableFile(csvFile);
} catch (err) {
this.logger.error(err);
throw err;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AuthenticatedRequest } from "@dto/internal/authenticated-request";
import { TestPayloadDecoderDto } from "@dto/test-payload-decoder.dto";
import { BadRequestException, Body, Controller, Post } from "@nestjs/common";
import { BadRequestException, Body, Controller, Post, Req } from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { PayloadDecoderExecutorService } from "@services/data-management/payload-decoder-executor.service";

Expand Down
27 changes: 18 additions & 9 deletions src/controllers/user-management/new-kombit-creation.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
Param,
ParseIntPipe,
Put,
Query,
Req,
UseGuards,
} from "@nestjs/common";
Expand All @@ -35,6 +34,7 @@ import { OrganizationService } from "@services/user-management/organization.serv
import { PermissionService } from "@services/user-management/permission.service";
import { UserService } from "@services/user-management/user.service";
import { ApiAuth } from "@auth/swagger-auth-decorator";
import { checkIfUserHasAccessToUser } from "@helpers/security-helper";

@UseGuards(JwtAuthGuard)
@ApiAuth()
Expand Down Expand Up @@ -133,19 +133,28 @@ export class NewKombitCreationController {

@Get(":id")
@ApiOperation({ summary: "Get one user" })
async find(
@Param("id", new ParseIntPipe()) id: number,
@Query("extendedInfo") extendedInfo?: boolean
): Promise<UserResponseDto> {
const getExtendedInfo = extendedInfo != null ? extendedInfo : false;
async find(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise<UserResponseDto> {
let dbUser;

try {
dbUser = await this.userService.findOne(id);
} catch (err) {
throw new NotFoundException(ErrorCodes.IdDoesNotExists);
}

try {
checkIfUserHasAccessToUser(req, dbUser);

dbUser.permissions.forEach(perm => {
delete perm.organization;
});

// Don't leak the passwordHash
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { passwordHash, ...user } = await this.userService.findOne(id, getExtendedInfo);
const { passwordHash: _, ...user } = dbUser;

return user;
} catch (err) {
throw new NotFoundException(ErrorCodes.IdDoesNotExists);
throw err;
}
}
}
9 changes: 0 additions & 9 deletions src/controllers/user-management/organization.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,6 @@ export class OrganizationController {
}
}

@Get("minimal")
@ApiOperation({
summary: "Get list of the minimal representation of organizations, i.e. id and name.",
})
@Read()
async findAllMinimal(): Promise<ListAllMinimalOrganizationsResponseDto> {
return await this.organizationService.findAllMinimal();
}

@Get()
@ApiOperation({ summary: "Get list of all Organizations" })
@UserAdmin()
Expand Down
43 changes: 24 additions & 19 deletions src/controllers/user-management/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { UserResponseDto } from "@dto/user-response.dto";
import { ErrorCodes } from "@entities/enum/error-codes.enum";
import {
checkIfUserHasAccessToOrganization,
checkIfUserHasAccessToUser,
checkIfUserIsGlobalAdmin,
OrganizationAccessScope,
} from "@helpers/security-helper";
Expand All @@ -54,12 +55,6 @@ export class UserController {

constructor(private userService: UserService, private organizationService: OrganizationService) {}

@Get("minimal")
@ApiOperation({ summary: "Get all id,names of users" })
async findAllMinimal(): Promise<ListAllUsersMinimalResponseDto> {
return await this.userService.findAllMinimal();
}

@Post()
@ApiOperation({ summary: "Create a new User" })
async create(@Req() req: AuthenticatedRequest, @Body() createUserDto: CreateUserDto): Promise<UserResponseDto> {
Expand Down Expand Up @@ -189,18 +184,28 @@ export class UserController {

@Get(":id")
@ApiOperation({ summary: "Get one user" })
async find(
@Param("id", new ParseIntPipe()) id: number,
@Query("extendedInfo") extendedInfo?: boolean
): Promise<UserResponseDto> {
const getExtendedInfo = extendedInfo != null ? extendedInfo : false;
async find(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise<UserResponseDto> {
let dbUser;

try {
dbUser = await this.userService.findOne(id);
} catch (err) {
throw new NotFoundException(ErrorCodes.IdDoesNotExists);
}

try {
checkIfUserHasAccessToUser(req, dbUser);

dbUser.permissions.forEach(perm => {
delete perm.organization;
});

// Don't leak the passwordHash
const { passwordHash: _, ...user } = await this.userService.findOne(id, getExtendedInfo);
const { passwordHash: _, ...user } = dbUser;

return user;
} catch (err) {
throw new NotFoundException(ErrorCodes.IdDoesNotExists);
throw err;
}
}

Expand All @@ -213,13 +218,13 @@ export class UserController {
@Param("organizationId", new ParseIntPipe()) organizationId: number,
@Query() query?: ListAllEntitiesDto
): Promise<ListAllUsersResponseDto> {
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");
}
// 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");
}

// Get user objects
// Get user objects
try {
return await this.userService.getUsersOnOrganization(organizationId, query);
} catch (err) {
throw new NotFoundException(ErrorCodes.IdDoesNotExists);
Expand Down
11 changes: 11 additions & 0 deletions src/helpers/security-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { PermissionType } from "@enum/permission-type.enum";
import { ForbiddenException, BadRequestException } from "@nestjs/common";
import * as _ from "lodash";
import { PermissionTypeEntity } from "@entities/permissions/permission-type.entity";
import { User } from "@entities/user.entity";

export enum OrganizationAccessScope {
ApplicationRead,
Expand Down Expand Up @@ -75,6 +76,16 @@ export function checkIfUserHasAccessToApplication(
checkIfGlobalAdminOrInList(req, allowedOrganizations, applicationId);
}

export function checkIfUserHasAccessToUser(req: AuthenticatedRequest, user: User) {
const orgs = req.user.permissions.getAllOrganizationsWithAtLeastUserAdminRead();

const hasAccess = user.permissions.some(perm => orgs.includes(perm.organization?.id));

if (!hasAccess && !req.user.permissions.isGlobalAdmin) {
throw new ForbiddenException();
}
}

export function checkIfUserIsGlobalAdmin(req: AuthenticatedRequest): void {
if (!req.user.permissions.isGlobalAdmin) {
throw new ForbiddenException();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { AuthenticatedRequest } from "@dto/internal/authenticated-request";
import { IoTDevice } from "@entities/iot-device.entity";
import { ApplicationAccessScope, checkIfUserHasAccessToApplication } from "@helpers/security-helper";
import { Injectable, Logger } from "@nestjs/common";
import * as worker_threads from "node:worker_threads";

Expand Down
3 changes: 1 addition & 2 deletions src/services/data-targets/data-target.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,7 @@ export class DataTargetService {
);
}

public async testDataTarget(testDto: TestDataTargetDto): Promise<TestDataTargetResultDto> {
const dataTarget = await this.findOne(testDto.dataTargetId);
public async testDataTarget(testDto: TestDataTargetDto, dataTarget: DataTarget): Promise<TestDataTargetResultDto> {
let iotDevice = await this.iotDeviceService.findOne(testDto.iotDeviceId);

if (dataTarget.type === DataTargetType.MQTT && !testDto.dataPackage) {
Expand Down
Loading