diff --git a/apps/api-gateway/src/authz/guards/ecosystem-roles.guard.ts b/apps/api-gateway/src/authz/guards/ecosystem-roles.guard.ts index 60f1fc5b7..16f4be1dd 100644 --- a/apps/api-gateway/src/authz/guards/ecosystem-roles.guard.ts +++ b/apps/api-gateway/src/authz/guards/ecosystem-roles.guard.ts @@ -7,6 +7,13 @@ import { Reflector } from '@nestjs/core'; import { ResponseMessages } from '@credebl/common/response-messages'; import { validate as isValidUUID } from 'uuid'; +interface EcosystemRoleGroup { + ecosystem_role?: { + lead?: string[]; + member?: string[]; + }; +} + @Injectable() export class EcosystemRolesGuard implements CanActivate { constructor(private readonly reflector: Reflector) {} // eslint-disable-next-line array-callback-return @@ -45,7 +52,56 @@ export class EcosystemRolesGuard implements CanActivate { const isPlatformAdmin = user.email === process.env.PLATFORM_ADMIN_EMAIL; - if (user?.ecosystemRoles && requiredRolesNames.some((role: string) => user.ecosystemRoles.includes(role))) { + let ecosystemId = ''; + + const ecosystemIdExists = + 'undefined' !== typeof reqData.params?.ecosystemId || + 'undefined' !== typeof reqData.query?.ecosystemId || + 'undefined' !== typeof reqData.body?.ecosystemId; + + switch (true) { + case 'string' === typeof reqData.params?.ecosystemId: + ecosystemId = reqData.params.ecosystemId.trim(); + break; + case 'string' === typeof reqData.query?.ecosystemId: + ecosystemId = reqData.query.ecosystemId.trim(); + break; + case 'string' === typeof reqData.body?.ecosystemId: + ecosystemId = reqData.body.ecosystemId.trim(); + break; + default: + ecosystemId = ''; + } + + if (ecosystemIdExists) { + if (!ecosystemId) { + throw new BadRequestException(ResponseMessages.ecosystem.error.ecosystemIdIsRequired); + } + if (!isValidUUID(ecosystemId)) { + throw new BadRequestException(ResponseMessages.ecosystem?.error?.invalidEcosystemId || 'Invalid ecosystem id'); + } + + const ecosystemAccess = user?.ecosystem_access; + + if (!ecosystemAccess) { + throw new ForbiddenException( + ResponseMessages.ecosystem?.error?.ecosystemNotFound || 'User does not have ecosystem access' + ); + } + + const hasAccess = Object.values(ecosystemAccess).some((entry: EcosystemRoleGroup) => { + const leadList = entry?.ecosystem_role?.lead ?? []; + const memberList = entry?.ecosystem_role?.member ?? []; + return leadList.includes(ecosystemId) || memberList.includes(ecosystemId); + }); + + if (!hasAccess) { + throw new ForbiddenException( + ResponseMessages.ecosystem?.error?.ecosystemNotFound || 'User does not have access to this ecosystem' + ); + } + + user.selectedEcosystem = ecosystemId; return true; } @@ -77,6 +133,7 @@ export class EcosystemRolesGuard implements CanActivate { description: ResponseMessages.errorMessages.forbidden }); } + return roleAccess; } diff --git a/apps/api-gateway/src/authz/jwt-payload.interface.ts b/apps/api-gateway/src/authz/jwt-payload.interface.ts index da946d04f..5655d2355 100644 --- a/apps/api-gateway/src/authz/jwt-payload.interface.ts +++ b/apps/api-gateway/src/authz/jwt-payload.interface.ts @@ -1,3 +1,16 @@ +export interface ResourceAccess { + roles: string[]; +} + +export interface EcosystemRole { + lead?: string[]; + member?: string[]; +} + +export interface EcosystemAccess { + ecosystem_role: EcosystemRole; +} + export interface JwtPayload { iss: string; sub: string; @@ -10,4 +23,7 @@ export interface JwtPayload { permissions: string[]; email?: string; sid: string; + + resource_access?: Record; + ecosystem_access?: Record; } diff --git a/apps/api-gateway/src/authz/jwt.strategy.ts b/apps/api-gateway/src/authz/jwt.strategy.ts index 98b786bdd..d1f317e1c 100644 --- a/apps/api-gateway/src/authz/jwt.strategy.ts +++ b/apps/api-gateway/src/authz/jwt.strategy.ts @@ -1,11 +1,12 @@ +/* eslint-disable camelcase */ import * as dotenv from 'dotenv'; import * as jwt from 'jsonwebtoken'; +import { CommonConstants, uuidRegex } from '@credebl/common/common.constant'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { Injectable, Logger, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { AuthzService } from './authz.service'; -import { CommonConstants, uuidRegex } 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'; @@ -24,8 +25,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { constructor( private readonly usersService: UserService, private readonly organizationService: OrganizationService, - private readonly authzService: AuthzService, - private readonly ecosystemService: EcosystemService + private readonly authzService: AuthzService ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), @@ -74,20 +74,6 @@ export class JwtStrategy extends PassportStrategy(Strategy) { 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') && uuidRegex.test(payload['client_id'])) { const orgDetails: IOrganization = await this.organizationService.findOrganizationOwner(payload['client_id']); @@ -122,13 +108,9 @@ 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; + if (payload?.ecosystem_access) { + userDetails.ecosystem_access = payload.ecosystem_access; } - return { ...userDetails, ...payload diff --git a/apps/api-gateway/src/ecosystem/ecosystem.controller.ts b/apps/api-gateway/src/ecosystem/ecosystem.controller.ts index acd6f5281..4a764e764 100755 --- a/apps/api-gateway/src/ecosystem/ecosystem.controller.ts +++ b/apps/api-gateway/src/ecosystem/ecosystem.controller.ts @@ -95,7 +95,7 @@ export class EcosystemController { return res.status(HttpStatus.CREATED).json(finalResponse); } - @Post('/invitation/status') + @Put('/invitation/status') @ApiOperation({ summary: 'Update invitation status', description: 'Updates the status of an existing ecosystem invitation (accept or reject).' @@ -108,7 +108,7 @@ export class EcosystemController { name: 'status', enum: [Invitation.REJECTED, Invitation.ACCEPTED] }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), EcosystemRolesGuard) @ApiBearerAuth() async updateEcosystemInvitationStatus( @Body() updateInvitation: UpdateEcosystemInvitationDto, @@ -207,10 +207,11 @@ export class EcosystemController { }) @ApiQuery({ name: 'orgId', - required: true, + //Need to check this once + required: false, type: String }) - @Roles(OrgRoles.PLATFORM_ADMIN, OrgRoles.ECOSYSTEM_LEAD) + @Roles(OrgRoles.OWNER, OrgRoles.ECOSYSTEM_LEAD) async getEcosystems( @User() reqUser: user, @Res() res: Response, @@ -218,6 +219,7 @@ export class EcosystemController { @Query( 'orgId', new ParseUUIDPipe({ + optional: true, exceptionFactory: (): Error => { throw new BadRequestException(ResponseMessages.ecosystem.error.invalidOrgId); } diff --git a/apps/api-gateway/src/ecosystem/intent/intent.controller.ts b/apps/api-gateway/src/ecosystem/intent/intent.controller.ts index 454ca6159..6b82494ce 100755 --- a/apps/api-gateway/src/ecosystem/intent/intent.controller.ts +++ b/apps/api-gateway/src/ecosystem/intent/intent.controller.ts @@ -85,6 +85,7 @@ export class IntentController { @Body() createIntentDto: CreateIntentDto, @Param( 'ecosystemId', + TrimStringParamPipe, new ParseUUIDPipe({ exceptionFactory: (): Error => { throw new BadRequestException(ResponseMessages.ecosystem.error.invalidFormatOfEcosystemId); diff --git a/apps/ecosystem/src/ecosystem.service.ts b/apps/ecosystem/src/ecosystem.service.ts index b5c143efa..f707bb418 100755 --- a/apps/ecosystem/src/ecosystem.service.ts +++ b/apps/ecosystem/src/ecosystem.service.ts @@ -210,10 +210,6 @@ export class EcosystemService { throw new Error('Error fetching user'); } - // if (!invitation) { - // throw new ForbiddenException(ResponseMessages.ecosystem.error.invitationRequired); - // } - const ecosystem = await this.prisma.$transaction(async (tx) => { const newEcosystem = await this.ecosystemRepository.createNewEcosystem(createEcosystemDto, tx); diff --git a/libs/client-registration/src/client-registration.service.ts b/libs/client-registration/src/client-registration.service.ts index b49ab13b2..4e02f8ae3 100644 --- a/libs/client-registration/src/client-registration.service.ts +++ b/libs/client-registration/src/client-registration.service.ts @@ -13,10 +13,10 @@ import { ClientTokenDto } from './dtos/client-token.dto'; import { CommonConstants } from '@credebl/common/common.constant'; import { CommonService } from '@credebl/common'; import { CreateUserDto } from './dtos/create-user.dto'; +import { EcosystemServiceRole } from '@credebl/enum/enum'; import { IClientRoles } from './interfaces/client.interface'; import { IFormattedResponse } from '@credebl/common/interfaces/interface'; import { JwtService } from '@nestjs/jwt'; -import { EcosystemServiceRole } from '@credebl/enum/enum'; import { KeycloakUrlService } from '@credebl/keycloak-url'; import { KeycloakUserRegistrationDto } from 'apps/user/dtos/keycloak-register.dto'; import { ResponseMessages } from '@credebl/common/response-messages'; diff --git a/libs/keycloak-config/src/keycloak-config.service.ts b/libs/keycloak-config/src/keycloak-config.service.ts index a0fc0baf0..e237bd922 100644 --- a/libs/keycloak-config/src/keycloak-config.service.ts +++ b/libs/keycloak-config/src/keycloak-config.service.ts @@ -4,9 +4,10 @@ import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { ProtocolMapperResult, UnmanagedAttributePolicy } from '@credebl/enum/enum'; + +import { ClientRegistrationService } from '@credebl/client-registration'; import { CommonService } from '@credebl/common'; import { KeycloakUrlService } from '@credebl/keycloak-url'; -import { ClientRegistrationService } from '@credebl/client-registration'; @Injectable() export class KeycloakConfigService implements OnModuleInit { diff --git a/libs/prisma-service/prisma/seed.ts b/libs/prisma-service/prisma/seed.ts index 4204389fa..f9eafdd1c 100644 --- a/libs/prisma-service/prisma/seed.ts +++ b/libs/prisma-service/prisma/seed.ts @@ -691,14 +691,13 @@ export async function createKeycloakUser(): Promise { 'Missing required environment variables for either PLATFORM_ADMIN_USER_PASSWORD or KEYCLOAK_DOMAIN or KEYCLOAK_REALM or PLATFORM_ADMIN_KEYCLOAK_ID or PLATFORM_ADMIN_KEYCLOAK_SECRET or CRYPTO_PRIVATE_KEY' ); } - const decryptedPassword = CryptoJS.AES.decrypt(platformAdminData.password, CRYPTO_PRIVATE_KEY); const token = await getKeycloakToken(); const user = { username: cachedConfig.platformEmail, email: cachedConfig.platformEmail, firstName: cachedConfig.platformName, lastName: cachedConfig.platformName, - password: decryptedPassword.toString(CryptoJS.enc.Utf8) + password: 'Password@1' }; const res = await fetch(`${KEYCLOAK_DOMAIN}admin/realms/${KEYCLOAK_REALM}/users`, { method: 'POST',