Skip to content

Commit

Permalink
Merge branch 'stage' into feature/IoT-1598_FeedbackDatalog
Browse files Browse the repository at this point in the history
  • Loading branch information
August Andersen committed Sep 25, 2024
2 parents caf3863 + 9678b11 commit 4ade1fa
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 21 deletions.
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

0 comments on commit 4ade1fa

Please sign in to comment.