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
2 changes: 2 additions & 0 deletions apps/api-gateway/src/authz/authz.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CommonConstants } from '@credebl/common/common.constant';
import { CommonModule } from '../../../../libs/common/src/common.module';
import { CommonService } from '../../../../libs/common/src/common.service';
import { ConnectionService } from '../connection/connection.service';
import { EcosystemModule } from '../ecosystem/ecosystem.module';
import { HttpModule } from '@nestjs/axios';
import { JwtStrategy } from './jwt.strategy';
import { MobileJwtStrategy } from './mobile-jwt.strategy';
Expand All @@ -25,6 +26,7 @@ import { getNatsOptions } from '@credebl/common/nats.config';

@Module({
imports: [
EcosystemModule,
HttpModule,
PassportModule.register({
defaultStrategy: 'jwt',
Expand Down
117 changes: 117 additions & 0 deletions apps/api-gateway/src/authz/guards/ecosystem-roles.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { BadRequestException, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';

import { Injectable } from '@nestjs/common';
import { OrgRoles } from 'libs/org-roles/enums';
import { ROLES_KEY } from '../decorators/roles.decorator';
import { Reflector } from '@nestjs/core';
import { ResponseMessages } from '@credebl/common/response-messages';
import { validate as isValidUUID } from 'uuid';

@Injectable()
export class EcosystemRolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {} // eslint-disable-next-line array-callback-return

async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<OrgRoles[]>(ROLES_KEY, [
context.getHandler(),
context.getClass()
]);

if (!requiredRoles || 0 === requiredRoles.length) {
return true;
}
const requiredRolesNames = requiredRoles as string[];
const reqData = context.switchToHttp().getRequest();
const { user } = reqData;

let orgId = '';

switch (true) {
case 'string' === typeof reqData.params?.orgId:
orgId = reqData.params.orgId.trim();
break;

case 'string' === typeof reqData.query?.orgId:
orgId = reqData.query.orgId.trim();
break;

case 'string' === typeof reqData.body?.orgId:
orgId = reqData.body.orgId.trim();
break;

default:
orgId = '';
}

const isPlatformAdmin = user.email === process.env.PLATFORM_ADMIN_EMAIL;

if (user?.ecosystemRoles && requiredRolesNames.some((role: string) => user.ecosystemRoles.includes(role))) {
return true;
}

if (isPlatformAdmin && requiredRolesNames.includes(OrgRoles.PLATFORM_ADMIN)) {
// eslint-disable-next-line array-callback-return
const isPlatformAdminFlag = user.userOrgRoles.find((orgDetails) => {
if (orgDetails.orgRole.name === OrgRoles.PLATFORM_ADMIN) {
return true;
}
});

if (isPlatformAdminFlag) {
return true;
}
}

if (orgId) {
if (!isValidUUID(orgId)) {
throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId);
}

if (user.hasOwnProperty('resource_access') && user.resource_access[orgId]) {
const orgRoles: string[] = user.resource_access[orgId].roles;
const roleAccess = requiredRoles.some((role) => orgRoles.includes(role));

if (!roleAccess) {
throw new ForbiddenException(ResponseMessages.organisation.error.roleNotMatch, {
cause: new Error('error'),
description: ResponseMessages.errorMessages.forbidden
});
}
return roleAccess;
}

const specificOrg = user.userOrgRoles.find((orgDetails) => {
if (!orgDetails.orgId) {
return false;
}
return orgDetails.orgId.toString().trim() === orgId.toString().trim();
});

if (!specificOrg) {
throw new ForbiddenException(ResponseMessages.organisation.error.orgNotMatch, {
cause: new Error('error'),
description: ResponseMessages.errorMessages.forbidden
});
}

user.selectedOrg = specificOrg;
// eslint-disable-next-line array-callback-return
user.selectedOrg.orgRoles = user.userOrgRoles
.filter((orgRoleItem) => orgRoleItem.orgId && orgRoleItem.orgId.toString().trim() === orgId.toString().trim())
.map((orgRoleItem) => orgRoleItem.orgRole.name);
} else {
return false;
}

// Sending user friendly message if a user attempts to access an API that is inaccessible to their role
const roleAccess = requiredRoles.some((role) => user.selectedOrg?.orgRoles.includes(role));
if (!roleAccess) {
throw new ForbiddenException(ResponseMessages.organisation.error.roleNotMatch, {
cause: new Error('error'),
description: ResponseMessages.errorMessages.forbidden
});
}

return roleAccess;
}
}
28 changes: 24 additions & 4 deletions apps/api-gateway/src/authz/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Injectable, Logger, NotFoundException, UnauthorizedException } from '@n

import { AuthzService } from './authz.service';
import { CommonConstants } from '@credebl/common/common.constant';
import { EcosystemService } from '../ecosystem/ecosystem.service';
import { IOrganization } from '@credebl/common/interfaces/organization.interface';
import { JwtPayload } from './jwt-payload.interface';
import { OrganizationService } from '../organization/organization.service';
Expand All @@ -23,7 +24,8 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly usersService: UserService,
private readonly organizationService: OrganizationService,
private readonly authzService: AuthzService
private readonly authzService: AuthzService,
private readonly ecosystemService: EcosystemService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
Expand Down Expand Up @@ -69,22 +71,33 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
throw new UnauthorizedException(ResponseMessages.user.error.invalidAccessToken);
}
}

if (payload?.email) {
userInfo = await this.usersService.getUserByUserIdInKeycloak(payload?.email);
}
let ecosystemRole = null;
if (userInfo?.id) {
try {
const user = await this.ecosystemService.getUserByKeycloakId(userInfo.id);
if (user?.id) {
const ecosystem = await this.ecosystemService.getEcosystemDetailsByUserId(user.id);
if (ecosystem?.id) {
ecosystemRole = await this.ecosystemService.getEcosystemOrgDetailsByUserId(user.id, ecosystem.id);
}
}
} catch (error) {
this.logger.warn('Failed to fetch ecosystem roles', JSON.stringify(error));
}
}

if (payload.hasOwnProperty('client_id')) {
const orgDetails: IOrganization = await this.organizationService.findOrganizationOwner(payload['client_id']);

this.logger.log('Organization details fetched');
if (!orgDetails) {
throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound);
}

// eslint-disable-next-line prefer-destructuring
const userOrgDetails = 0 < orgDetails.userOrgRoles.length && orgDetails.userOrgRoles[0];

userDetails = userOrgDetails.user;
userDetails.userOrgRoles = [];
userDetails.userOrgRoles.push({
Expand All @@ -108,6 +121,13 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
userDetails['userRole'] = userInfo?.['attributes']?.userRole;
}

if (Array.isArray(ecosystemRole) && 0 < ecosystemRole.length) {
const ecosystemRoleList = [
...new Set(ecosystemRole.map((record: { ecosystemRole: { name: string } }) => record.ecosystemRole.name))
];
userDetails.ecosystemRoles = ecosystemRoleList;
}

return {
...userDetails,
...payload
Expand Down
28 changes: 28 additions & 0 deletions apps/api-gateway/src/ecosystem/dtos/delete-ecosystem-users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, IsUUID } from 'class-validator';

import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';

export class DeleteEcosystemOrgDto {
@ApiProperty({
example: [
'6e672a9c-64f0-4d98-b312-f578f633800b',
'2f1a5a3c-91a2-4c4b-9f7d-1b7e6a22a111'
],
isArray: true
})
@IsArray({ message: 'orgId must be an array' })
@ArrayNotEmpty({ message: 'orgId cannot be empty' })
@IsUUID('4', { each: true })
@IsString({ each: true })
@Transform(({ value }) => Array.isArray(value) ? value.map(v => v.trim()) : value
)
orgIds: string[];

@ApiProperty({ example: '61ec22e3-9158-409d-874d-345ad2fc51e4' })
@IsUUID()
@IsNotEmpty({ message: 'ecosystemId is required' })
@IsString({ message: 'ecosystemId should be a string' })
@Transform(({ value }) => value?.trim())
ecosystemId: string;
}
57 changes: 57 additions & 0 deletions apps/api-gateway/src/ecosystem/dtos/ecosystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ArrayNotEmpty, IsArray, IsEmail, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';

import { EcosystemInvitationRoles } from 'apps/ecosystem/interfaces/ecosystem.interfaces';
import { EcosystemOrgStatus } from '@credebl/enum/enum';
import { OrgRoles } from 'libs/org-roles/enums';
import { Transform } from 'class-transformer';

export class UpdateEcosystemOrgStatusDto {
@ApiProperty({
example: ['ef93be23-d950-497c-a886-22fcd98370fe'],
isArray: true
})
@IsArray({ message: 'orgId must be an array for updating organization status' })
@ArrayNotEmpty({ message: 'orgId cannot be empty' })
@IsUUID('4', { each: true })
@IsString({ each: true })
@Transform(({ value }) => (Array.isArray(value) ? value.map((v) => v.trim()) : value))
orgIds: string[];

@ApiProperty({ example: 'c78046ba-c98a-4785-80c6-06ad5167e74c' })
@IsUUID()
@IsNotEmpty({ message: 'ecosystemId is required to update status of an organization' })
@IsString({ message: 'ecosystemId should be a string to update status of an organization' })
@Transform(({ value }) => value?.trim())
ecosystemId: string;

@ApiProperty({ enum: EcosystemOrgStatus, example: EcosystemOrgStatus.INACTIVE })
@IsEnum(EcosystemOrgStatus, { message: `Status must be one of: ${Object.values(EcosystemOrgStatus).join(', ')}` })
@IsNotEmpty({ message: 'Status is required to update status of an organization' })
status: EcosystemOrgStatus;
}

export enum InvitationViewRole {
ECOSYSTEM_MEMBER = OrgRoles.ECOSYSTEM_MEMBER,
ECOSYSTEM_LEAD = OrgRoles.ECOSYSTEM_LEAD
}

export class GetEcosystemInvitationsQueryDto {
@IsEnum(InvitationViewRole)
role: EcosystemInvitationRoles;

@ApiPropertyOptional({ format: 'uuid' })
@IsOptional()
@IsUUID()
ecosystemId?: string;

@ApiPropertyOptional({ example: 'user@example.com' })
@IsOptional()
@IsEmail()
email?: string;

@ApiPropertyOptional({ format: 'uuid' })
@IsOptional()
@IsUUID()
userId?: string;
}
39 changes: 38 additions & 1 deletion apps/api-gateway/src/ecosystem/dtos/send-ecosystem-invitation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
import { IsEmail, IsEnum, IsNotEmpty, IsString, IsUUID } from 'class-validator';

import { ApiProperty } from '@nestjs/swagger';
import { Invitation } from '@credebl/enum/enum';
import { Transform } from 'class-transformer';

export class CreateEcosystemInvitationDto {
Expand All @@ -11,3 +12,39 @@ export class CreateEcosystemInvitationDto {
@Transform(({ value }) => value?.trim())
email: string;
}

export class InviteMemberToEcosystemDto {
@ApiProperty({ example: '6e672a9c-64f0-4d98-b312-f578f633800b' })
@IsUUID()
@IsNotEmpty({ message: 'OrgId is required' })
@IsString({ message: 'OrgId should be a string' })
@Transform(({ value }) => value?.trim())
orgId: string;

@ApiProperty({ example: '61ec22e3-9158-409d-874d-345ad2fc51e4' })
@IsUUID()
@IsNotEmpty({ message: 'ecosystemId is required' })
@IsString({ message: 'ecosystemId should be a string' })
@Transform(({ value }) => value?.trim())
ecosystemId: string;
}

export class OrgIdParam {
@IsUUID() // or @IsString()
orgId: string;
}

export class UpdateEcosystemInvitationDto {
@ApiProperty({ enum: Invitation, example: Invitation.ACCEPTED })
@Transform(({ value }) => ('string' === typeof value ? value.toLowerCase() : value))
@IsEnum(Invitation, { message: `Status must be one of: ${Object.values(Invitation).join(', ')}` })
@IsNotEmpty({ message: 'Status is required' })
status: Invitation;

@ApiProperty({ example: '61ec22e3-9158-409d-874d-345ad2fc51e4' })
@IsUUID()
@IsNotEmpty({ message: 'ecosystemId is required' })
@IsString({ message: 'ecosystemId should be a string' })
@Transform(({ value }) => value?.trim())
ecosystemId: string;
}
Loading