Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/on-push-pr.action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,9 @@ jobs:
vulnerabilities-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
name: Checkout repository
- uses: debricked/actions/scan@v1
name: Run a vulnerability scan
- uses: actions/checkout@v4
- uses: debricked/actions@v4
env:
# Token must have API access scope to run scans
DEBRICKED_TOKEN: ${{ secrets.DEBRICKED_TOKEN }}
code-build:
runs-on: ubuntu-latest
Expand Down
13 changes: 13 additions & 0 deletions src/controllers/user-management/permission.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,19 @@ export class PermissionController {
return this.permissionService.getAllPermissions(query);
}

@Get("getAllPermissionsWithoutUsers")
@ApiOperation({ summary: "Get list of all permissions without include users" })
async getAllPermissionsWithoutUsers(
@Req() req: AuthenticatedRequest,
@Query() query?: ListAllPermissionsDto
): Promise<ListAllPermissionsResponseDto> {
if (!req.user.permissions.isGlobalAdmin) {
const allowedOrganizations = req.user.permissions.getAllOrganizationsWithUserAdmin();
return this.permissionService.getAllPermissionsWithoutUsers(query, allowedOrganizations);
}
return this.permissionService.getAllPermissionsWithoutUsers(query);
}

@Get(":id")
@ApiOperation({ summary: "Get permissions entity" })
@ApiNotFoundResponse()
Expand Down
4 changes: 2 additions & 2 deletions src/controllers/user-management/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class UserController {
try {
// Don't leak the passwordHash
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { passwordHash, ...user } = await this.userService.createUser(createUserDto, req.user.userId);
const { passwordHash, ...user } = await this.userService.createUser(createUserDto, req.user.userId, req);
AuditLog.success(ActionType.CREATE, User.name, req.user.userId, user.id, user.name);

return user;
Expand Down Expand Up @@ -122,7 +122,7 @@ export class UserController {
}

// Don't leak the passwordHash
const { passwordHash: _, ...user } = await this.userService.updateUser(id, dto, req.user.userId);
const { passwordHash: _, ...user } = await this.userService.updateUser(id, dto, req.user.userId, req);
AuditLog.success(ActionType.UPDATE, User.name, req.user.userId, user.id, user.name);

return user;
Expand Down
5 changes: 4 additions & 1 deletion src/entities/dto/list-all-permissions.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { ApiProperty } from "@nestjs/swagger";
import { ListAllEntitiesDto } from "./list-all-entities.dto";

export class ListAllPermissionsDto extends ListAllEntitiesDto {
Expand All @@ -7,4 +7,7 @@ export class ListAllPermissionsDto extends ListAllEntitiesDto {

@ApiProperty({ type: String, required: false })
userId?: string;

@ApiProperty({ type: Boolean, required: false })
ignoreGlobalAdmin?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class UserPermissions {
if (this.isGlobalAdmin) {
return true;
} else {
let organizationsWithAdmin = this.getAllOrganizationsWithUserAdmin();
const organizationsWithAdmin = this.getAllOrganizationsWithUserAdmin();
return organizationsWithAdmin.indexOf(organizationId) > -1;
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/entities/dto/user-management/create-user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Permission } from "@entities/permissions/permission.entity";
import { IsNotBlank } from "@helpers/is-not-blank.validator";
import { ApiProperty } from "@nestjs/swagger";
import { IsEmail, IsString, Length } from "class-validator";
Expand All @@ -23,4 +24,7 @@ export class CreateUserDto {

@ApiProperty({ required: false })
globalAdmin?: boolean;

@ApiProperty({ required: false })
permissionIds?: number[];
}
3 changes: 1 addition & 2 deletions src/entities/permissions/permission.entity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { DbBaseEntity } from "@entities/base.entity";
import { User } from "@entities/user.entity";
import { PermissionType } from "@enum/permission-type.enum";
import { Column, Entity, ManyToMany, TableInheritance, OneToMany, ManyToOne, RelationId } from "typeorm";
import { Column, Entity, ManyToMany, OneToMany, ManyToOne, RelationId } from "typeorm";
import { PermissionTypeEntity } from "./permission-type.entity";
import { Application } from "@entities/application.entity";
import { Organization } from "@entities/organization.entity";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class LorawanDeviceDatabaseEnrichJob {
stats.rxPacketsReceived,
stats.txPacketsEmitted,
gateway.updatedAt,
chirpstackGateway.lastSeenAt ? timestampToDate(chirpstackGateway.lastSeenAt) : undefined
chirpstackGateway?.lastSeenAt ? timestampToDate(chirpstackGateway.lastSeenAt) : undefined
);
} catch (err) {
this.logger.error(`Gateway status fetch failed with: ${JSON.stringify(err)}`, err);
Expand Down
56 changes: 50 additions & 6 deletions src/services/user-management/permission.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,8 @@ export class PermissionService {

async getAllPermissions(query?: ListAllPermissionsDto, orgs?: number[]): Promise<ListAllPermissionsResponseDto> {
const orderBy = this.getSorting(query);
const order: "DESC" | "ASC" = query?.sort?.toLocaleUpperCase() === "DESC" ? "DESC" : "ASC";
let qb: SelectQueryBuilder<Permission> = this.permissionRepository
const order = query?.sort?.toLocaleUpperCase() === "DESC" ? "DESC" : "ASC";
let queryBuilder = this.permissionRepository
.createQueryBuilder("permission")
.leftJoinAndSelect("permission.organization", "org")
.leftJoinAndSelect("permission.users", "user")
Expand All @@ -237,15 +237,48 @@ export class PermissionService {
.orderBy(orderBy, order);

if (query?.userId !== undefined && query.userId !== "undefined") {
qb = qb.andWhere("user.id = :userId", { userId: +query.userId });
queryBuilder = queryBuilder.andWhere("user.id = :userId", { userId: +query.userId });
}
if (orgs) {
qb = qb.andWhere({ organization: In(orgs) });
queryBuilder = queryBuilder.andWhere({ organization: In(orgs) });
} else if (query?.organisationId !== undefined && query.organisationId !== "undefined") {
qb = qb.andWhere("org.id = :orgId", { orgId: +query.organisationId });
queryBuilder = queryBuilder.andWhere("org.id = :orgId", { orgId: +query.organisationId });
}
const [data, count] = await queryBuilder.getManyAndCount();

const [data, count] = await qb.getManyAndCount();
return {
data: data,
count: count,
};
}

async getAllPermissionsWithoutUsers(
query?: ListAllPermissionsDto,
orgs?: number[]
): Promise<ListAllPermissionsResponseDto> {
const orderBy = this.getSorting(query);
const order = query?.sort?.toLocaleUpperCase() === "DESC" ? "DESC" : "ASC";
let queryBuilder = this.permissionRepository
.createQueryBuilder("permission")
.leftJoinAndSelect("permission.organization", "org")
.leftJoinAndSelect("permission.type", "permission_type")
.take(query?.limit ? +query.limit : 100)
.skip(query?.offset ? +query.offset : 0)
.orderBy(orderBy, order);

if (orgs) {
queryBuilder = queryBuilder.andWhere({ organization: In(orgs) });
} else if (query?.organisationId !== undefined && query.organisationId !== "undefined") {
queryBuilder = queryBuilder.andWhere("org.id = :orgId", { orgId: +query.organisationId });
}

if (query?.ignoreGlobalAdmin) {
queryBuilder = queryBuilder.andWhere("org.name != :globalAdminName", {
globalAdminName: PermissionType.GlobalAdmin,
});
}

const [data, count] = await queryBuilder.getManyAndCount();

return {
data: data,
Expand Down Expand Up @@ -417,6 +450,17 @@ export class PermissionService {
return await this.permissionRepository.findBy({ id: In(ids) });
}

async findManyByIdsIncludeOrgs(ids: number[]): Promise<Permission[]> {
if (!ids || ids.length === 0) {
return [];
}

return await this.permissionRepository.find({
where: { id: In(ids) },
relations: ["organization"],
});
}

private hasAccessToAllApplicationsInOrganization(permissions: PermissionMinimalDto[]) {
return permissions.some(x => x.permission_type_type == PermissionType.OrganizationUserAdmin);
}
Expand Down
33 changes: 30 additions & 3 deletions src/services/user-management/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BadRequestException, forwardRef, Inject, Injectable, Logger } from "@nestjs/common";
import { BadRequestException, ForbiddenException, forwardRef, Inject, Injectable, Logger } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import * as bcrypt from "bcryptjs";
import { In, Repository } from "typeorm";
Expand All @@ -22,6 +22,7 @@ import { ConfigService } from "@nestjs/config";
import { isPermissionType } from "@helpers/security-helper";
import { nameof } from "@helpers/type-helper";
import { OS2IoTMail } from "@services/os2iot-mail.service";
import { AuthenticatedRequest } from "@dto/internal/authenticated-request";

@Injectable()
export class UserService {
Expand Down Expand Up @@ -134,8 +135,15 @@ export class UserService {
.execute();
}

async createUser(dto: CreateUserDto, userId: number): Promise<User> {
async createUser(dto: CreateUserDto, userId: number, req?: AuthenticatedRequest): Promise<User> {
const user = new User();
const permissions = await this.permissionService.findManyByIdsIncludeOrgs(dto.permissionIds);

if (req) {
this.checkForAccessToPermissions(req, permissions);
}

user.permissions = permissions;
const mappedUser = this.mapDtoToUser(user, dto);
mappedUser.createdBy = userId;
mappedUser.updatedBy = userId;
Expand Down Expand Up @@ -205,12 +213,18 @@ export class UserService {
return user;
}

async updateUser(id: number, dto: UpdateUserDto, userId: number): Promise<User> {
async updateUser(id: number, dto: UpdateUserDto, userId: number, req: AuthenticatedRequest): Promise<User> {
const user = await this.userRepository.findOne({
where: { id },
relations: ["permissions"],
});
const permissions = await this.permissionService.findManyByIdsIncludeOrgs(dto.permissionIds);

if (req) {
this.checkForAccessToPermissions(req, permissions);
}

user.permissions = permissions;
const mappedUser = this.mapDtoToUser(user, dto);
mappedUser.updatedBy = userId;

Expand All @@ -225,6 +239,19 @@ export class UserService {
return await this.userRepository.save(mappedUser);
}

private checkForAccessToPermissions(req: AuthenticatedRequest, permissions: Permission[]) {
const allowedOrganizations = req?.user.permissions.getAllOrganizationsWithUserAdmin();
if (!req.user.permissions.isGlobalAdmin) {
const hasAccessToPermissions = permissions.every(permission =>
allowedOrganizations.some(org => org === permission.organization?.id)
);

if (!hasAccessToPermissions) {
throw new ForbiddenException();
}
}
}

private async updateGlobalAdminStatusIfNeeded(dto: UpdateUserDto, mappedUser: User) {
if (dto.globalAdmin) {
const globalAdminPermission = await this.permissionService.findOrCreateGlobalAdminPermission();
Expand Down