From f2374d196c63228ee644f0948017eaab67275597 Mon Sep 17 00:00:00 2001 From: Nishad Date: Mon, 19 Feb 2024 19:26:01 +0530 Subject: [PATCH 01/11] Refactored org roles API to fetch roles from keycloak, worked on user registration as holder, worked on the create org invitation Signed-off-by: Nishad --- .../organization/organization.controller.ts | 18 +- .../src/organization/organization.service.ts | 14 +- .../src/organization.controller.ts | 14 +- apps/organization/src/organization.service.ts | 149 +++++++++++--- apps/user/interfaces/user.interface.ts | 1 + apps/user/repositories/user.repository.ts | 2 + apps/user/src/user.service.ts | 17 +- .../src/client-registration.service.ts | 194 ++++++++++++++++-- .../src/interfaces/client.interface.ts | 8 + libs/common/src/response-messages/index.ts | 2 +- libs/keycloak-url/src/keycloak-url.service.ts | 42 ++++ .../interfaces/org-roles.interface.ts | 8 +- 12 files changed, 401 insertions(+), 68 deletions(-) create mode 100644 libs/client-registration/src/interfaces/client.interface.ts diff --git a/apps/api-gateway/src/organization/organization.controller.ts b/apps/api-gateway/src/organization/organization.controller.ts index 20635a1f9..e9f5b82fc 100644 --- a/apps/api-gateway/src/organization/organization.controller.ts +++ b/apps/api-gateway/src/organization/organization.controller.ts @@ -95,7 +95,7 @@ export class OrganizationController { * @returns get organization roles */ - @Get('/roles') + @Get('/:orgId/roles') @ApiOperation({ summary: 'Fetch org-roles details', description: 'Fetch org-roles details' @@ -103,9 +103,9 @@ export class OrganizationController { @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() - async getOrgRoles(@Res() res: Response): Promise { + async getOrgRoles(@Param('orgId', new ParseUUIDPipe({exceptionFactory: (): Error => { throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); }})) orgId: string, @Res() res: Response): Promise { - const orgRoles = await this.organizationService.getOrgRoles(); + const orgRoles = await this.organizationService.getOrgRoles(orgId.trim()); const finalResponse: IResponse = { statusCode: HttpStatus.OK, @@ -327,7 +327,11 @@ export class OrganizationController { @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() async createOrganization(@Body() createOrgDto: CreateOrganizationDto, @Res() res: Response, @User() reqUser: user): Promise { - const orgData = await this.organizationService.createOrganization(createOrgDto, reqUser.id); + + // eslint-disable-next-line prefer-destructuring + const keycloakUserId = reqUser.keycloakUserId; + + const orgData = await this.organizationService.createOrganization(createOrgDto, reqUser.id, keycloakUserId); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.organisation.success.create, @@ -350,7 +354,11 @@ export class OrganizationController { @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @ApiBearerAuth() async createOrgCredentials(@Param('orgId') orgId: string, @Res() res: Response, @User() reqUser: user): Promise { - const orgCredentials = await this.organizationService.createOrgCredentials(orgId, reqUser.id); + + // eslint-disable-next-line prefer-destructuring + const keycloakUserId = reqUser.keycloakUserId; + + const orgCredentials = await this.organizationService.createOrgCredentials(orgId, reqUser.id, keycloakUserId); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.organisation.success.orgCredentials, diff --git a/apps/api-gateway/src/organization/organization.service.ts b/apps/api-gateway/src/organization/organization.service.ts index 1e4d377d8..68277b7dd 100644 --- a/apps/api-gateway/src/organization/organization.service.ts +++ b/apps/api-gateway/src/organization/organization.service.ts @@ -6,7 +6,6 @@ import { CreateOrganizationDto } from './dtos/create-organization-dto'; import { BulkSendInvitationDto } from './dtos/send-invitation.dto'; import { UpdateUserRolesDto } from './dtos/update-user-roles.dto'; import { UpdateOrganizationDto } from './dtos/update-organization-dto'; -import { IOrgRoles } from 'libs/org-roles/interfaces/org-roles.interface'; import { organisation } from '@prisma/client'; import { IGetOrgById, IGetOrganization } from 'apps/organization/interfaces/organization.interface'; import { IOrgUsers } from 'apps/user/interfaces/user.interface'; @@ -14,6 +13,7 @@ import { IOrgCredentials, IOrganization, IOrganizationInvitations, IOrganization import { ClientCredentialsDto } from './dtos/client-credentials.dto'; import { IAccessTokenData } from '@credebl/common/interfaces/interface'; import { PaginationDto } from '@credebl/common/dtos/pagination.dto'; +import { IClientRoles } from '@credebl/client-registration/interfaces/client.interface'; @Injectable() export class OrganizationService extends BaseService { @@ -26,8 +26,8 @@ export class OrganizationService extends BaseService { * @param createOrgDto * @returns Organization creation Success */ - async createOrganization(createOrgDto: CreateOrganizationDto, userId: string): Promise { - const payload = { createOrgDto, userId }; + async createOrganization(createOrgDto: CreateOrganizationDto, userId: string, keycloakUserId: string): Promise { + const payload = { createOrgDto, userId, keycloakUserId }; return this.sendNatsMessage(this.serviceProxy, 'create-organization', payload); } @@ -37,8 +37,8 @@ export class OrganizationService extends BaseService { * @param userId * @returns Orgnization client credentials */ - async createOrgCredentials(orgId: string, userId: string): Promise { - const payload = { orgId, userId }; + async createOrgCredentials(orgId: string, userId: string, keycloakUserId: string): Promise { + const payload = { orgId, userId, keycloakUserId }; return this.sendNatsMessage(this.serviceProxy, 'create-org-credentials', payload); } @@ -133,8 +133,8 @@ export class OrganizationService extends BaseService { * @returns get organization roles */ - async getOrgRoles(): Promise { - const payload = {}; + async getOrgRoles(orgId: string): Promise { + const payload = {orgId}; return this.sendNatsMessage(this.serviceProxy, 'get-org-roles', payload); } diff --git a/apps/organization/src/organization.controller.ts b/apps/organization/src/organization.controller.ts index 87b1c45ea..3e6ddc2c0 100644 --- a/apps/organization/src/organization.controller.ts +++ b/apps/organization/src/organization.controller.ts @@ -8,9 +8,9 @@ import { BulkSendInvitationDto } from '../dtos/send-invitation.dto'; import { UpdateInvitationDto } from '../dtos/update-invitation.dt'; import { IGetOrgById, IGetOrganization, IUpdateOrganization, Payload } from '../interfaces/organization.interface'; import { organisation } from '@prisma/client'; -import { IOrgRoles } from 'libs/org-roles/interfaces/org-roles.interface'; import { IOrgCredentials, IOrganizationInvitations, IOrganization, IOrganizationDashboard } from '@credebl/common/interfaces/organization.interface'; import { IAccessTokenData } from '@credebl/common/interfaces/interface'; +import { IClientRoles } from '@credebl/client-registration/interfaces/client.interface'; @Controller() export class OrganizationController { @@ -24,8 +24,8 @@ export class OrganizationController { */ @MessagePattern({ cmd: 'create-organization' }) - async createOrganization(@Body() payload: { createOrgDto: CreateOrganizationDto; userId: string }): Promise { - return this.organizationService.createOrganization(payload.createOrgDto, payload.userId); + async createOrganization(@Body() payload: { createOrgDto: CreateOrganizationDto; userId: string, keycloakUserId: string }): Promise { + return this.organizationService.createOrganization(payload.createOrgDto, payload.userId, payload.keycloakUserId); } /** @@ -34,8 +34,8 @@ export class OrganizationController { * @returns organization client credentials */ @MessagePattern({ cmd: 'create-org-credentials' }) - async createOrgCredentials(@Body() payload: { orgId: string; userId: string }): Promise { - return this.organizationService.createOrgCredentials(payload.orgId); + async createOrgCredentials(@Body() payload: { orgId: string; userId: string, keycloakUserId: string }): Promise { + return this.organizationService.createOrgCredentials(payload.orgId, payload.userId, payload.keycloakUserId); } /** @@ -113,8 +113,8 @@ export class OrganizationController { */ @MessagePattern({ cmd: 'get-org-roles' }) - async getOrgRoles(): Promise { - return this.organizationService.getOrgRoles(); + async getOrgRoles(payload: {orgId: string}): Promise { + return this.organizationService.getOrgRoles(payload.orgId); } /** diff --git a/apps/organization/src/organization.service.ts b/apps/organization/src/organization.service.ts index aa2d580cf..a52164450 100644 --- a/apps/organization/src/organization.service.ts +++ b/apps/organization/src/organization.service.ts @@ -25,11 +25,11 @@ import { map } from 'rxjs/operators'; import { Cache } from 'cache-manager'; import { AwsService } from '@credebl/aws'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { IOrgRoles } from 'libs/org-roles/interfaces/org-roles.interface'; import { IOrgCredentials, IOrganization, IOrganizationInvitations, IOrganizationDashboard } from '@credebl/common/interfaces/organization.interface'; import { ClientCredentialTokenPayloadDto } from '@credebl/client-registration/dtos/client-credential-token-payload.dto'; import { IAccessTokenData } from '@credebl/common/interfaces/interface'; +import { IClientRoles } from '@credebl/client-registration/interfaces/client.interface'; @Injectable() export class OrganizationService { constructor( @@ -53,7 +53,7 @@ export class OrganizationService { */ // eslint-disable-next-line camelcase - async createOrganization(createOrgDto: CreateOrganizationDto, userId: string): Promise { + async createOrganization(createOrgDto: CreateOrganizationDto, userId: string, keycloakUserId: string): Promise { try { const organizationExist = await this.organizationRepository.checkOrganizationNameExist(createOrgDto.name); @@ -82,9 +82,20 @@ export class OrganizationService { delete organizationDetails.orgSlug; delete organizationDetails.website; - const ownerRoleData = await this.orgRoleService.getRole(OrgRoles.OWNER); + const orgCredentials = await this.registerToKeycloak(organizationDetails.name, organizationDetails.id, keycloakUserId); + + const {clientId, idpId} = orgCredentials; + + const updateOrgData = { + clientId, + idpId + }; - await this.userOrgRoleService.createUserOrgRole(userId, ownerRoleData.id, organizationDetails.id); + const updatedOrg = await this.organizationRepository.updateOrganizationById(updateOrgData, organizationDetails.id); + + if (!updatedOrg) { + throw new InternalServerErrorException(ResponseMessages.organisation.error.credentialsNotUpdate); + } await this.userActivityService.createActivity(userId, organizationDetails.id, `${organizationDetails.name} organization created`, 'Get started with inviting users to join organization'); @@ -101,7 +112,7 @@ export class OrganizationService { * @param orgId * @returns organization client credentials */ - async createOrgCredentials(orgId: string): Promise { + async createOrgCredentials(orgId: string, userId: string, keycloakUserId: string): Promise { try { const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); @@ -109,16 +120,34 @@ export class OrganizationService { if (!organizationDetails) { throw new ConflictException(ResponseMessages.organisation.error.orgNotFound); } - - const orgCredentials = await this.registerToKeycloak(organizationDetails.name, organizationDetails.id); - const {clientId, clientSecret, idpId} = orgCredentials; + let updateOrgData = {}; + let generatedClientSecret = ''; - const updateOrgData = { - clientId, - clientSecret: this.maskString(clientSecret), - idpId - }; + if (organizationDetails.idpId) { + + const token = await this.clientRegistrationService.getManagementToken(); + + generatedClientSecret = await this.clientRegistrationService.generateClientSecret(organizationDetails.idpId, token); + + updateOrgData = { + clientSecret: this.maskString(generatedClientSecret) + }; + + } else { + + const orgCredentials = await this.registerToKeycloak(organizationDetails.name, organizationDetails.id, keycloakUserId); + + const { clientId, idpId, clientSecret } = orgCredentials; + + generatedClientSecret = clientSecret; + + updateOrgData = { + clientId, + clientSecret: this.maskString(clientSecret), + idpId + }; + } const updatedOrg = await this.organizationRepository.updateOrganizationById(updateOrgData, orgId); @@ -126,7 +155,11 @@ export class OrganizationService { throw new InternalServerErrorException(ResponseMessages.organisation.error.credentialsNotUpdate); } - return orgCredentials; + return { + idpId: updatedOrg.idpId, + clientId: updatedOrg.clientId, + clientSecret: generatedClientSecret + }; } catch (error) { this.logger.error(`In createOrgCredentials : ${JSON.stringify(error)}`); @@ -140,9 +173,44 @@ export class OrganizationService { * @param orgId * @returns client credentials */ - async registerToKeycloak(orgName: string, orgId: string): Promise { + async registerToKeycloak(orgName: string, orgId: string, userId: string): Promise { const token = await this.clientRegistrationService.getManagementToken(); - return this.clientRegistrationService.createClient(orgName, orgId, token); + const orgDetails = await this.clientRegistrationService.createClient(orgName, orgId, token); + + const orgRolesList = [ + OrgRoles.OWNER, + OrgRoles.ADMIN, + OrgRoles.ISSUER, + OrgRoles.VERIFIER, + OrgRoles.MEMBER + ]; + + try { + for (const role of orgRolesList) { + await this.clientRegistrationService.createClientRole( + orgDetails.idpId, + token, + role, + role + ); + } + } catch (error) { + this.logger.error(`Error In creating client roles : ${JSON.stringify(error)}`); + throw new InternalServerErrorException('Unable to create client roles'); + } + + const ownerRole = await this.clientRegistrationService.getClientSpecificRoles(orgDetails.idpId, token, OrgRoles.OWNER); + + const payload = [ + { + id: ownerRole.id, + name: ownerRole.name + } + ]; + + await this.clientRegistrationService.createUserClientRole(orgDetails.idpId, token, userId, payload); + + return orgDetails; } @@ -443,9 +511,22 @@ export class OrganizationService { */ - async getOrgRoles(): Promise { + async getOrgRoles(orgId: string): Promise { try { - return this.orgRoleService.getOrgRoles(); + + if (!orgId) { + throw new BadRequestException(ResponseMessages.organisation.error.orgIdIsRequired); + } + + const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); + + if (!organizationDetails) { + throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); + } + + const token = await this.clientRegistrationService.getManagementToken(); + + return this.clientRegistrationService.getAllClientRoles(organizationDetails.idpId, token); } catch (error) { this.logger.error(`In getOrgRoles : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -514,9 +595,13 @@ export class OrganizationService { const userData = await this.getUserFirstName(userEmail); const {firstName} = userData; - const orgRolesDetails = await this.orgRoleService.getOrgRolesByIds(orgRoleId); - - if (0 === orgRolesDetails.length) { + + const token = await this.clientRegistrationService.getManagementToken(); + const clientRolesList = await this.clientRegistrationService.getAllClientRoles(organizationDetails.idpId, token); + + const matchedRoles = clientRolesList.filter(role => orgRoleId.includes(role.id.trim())); + + if (orgRoleId.length !== matchedRoles.length) { throw new NotFoundException(ResponseMessages.organisation.error.orgRoleIdNotFound); } @@ -524,10 +609,10 @@ export class OrganizationService { if (!isInvitationExist && userEmail !== invitation.email) { - await this.organizationRepository.createSendInvitation(email, String(orgId), String(userId), orgRoleId); + await this.organizationRepository.createSendInvitation(email, String(orgId), String(userId), matchedRoles.map(role => role.id)); try { - await this.sendInviteEmailTemplate(email, organizationDetails.name, orgRolesDetails, firstName, isUserExist); + await this.sendInviteEmailTemplate(email, organizationDetails.name, matchedRoles, firstName, isUserExist); } catch (error) { throw new InternalServerErrorException(ResponseMessages.user.error.emailSend); } @@ -644,6 +729,12 @@ export class OrganizationService { throw new NotFoundException(ResponseMessages.user.error.invalidOrgId); } + const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); + + if (!organizationDetails) { + throw new ConflictException(ResponseMessages.organisation.error.orgNotFound); + } + const invitationStatus = invitation.status as Invitation; if (!transition(invitationStatus, payload.status)) { throw new BadRequestException(`${ResponseMessages.user.error.invitationStatusUpdateInvalid} ${invitation.status}`); @@ -658,9 +749,15 @@ export class OrganizationService { if (status === Invitation.REJECTED) { return ResponseMessages.user.success.invitationReject; } - for (const roleId of invitation.orgRoles) { - await this.userOrgRoleService.createUserOrgRole(userId, roleId, orgId); - } + + const token = await this.clientRegistrationService.getManagementToken(); + + const clientRoles = await this.clientRegistrationService.getAllClientRoles(organizationDetails.idpId, token); + + const rolesPayload: { id: string; name: string}[] = clientRoles.filter(orgRole => (invitation.orgRoles.includes(orgRole.id.trim()) + && {id: orgRole.id, name: orgRole.name})); + + await this.clientRegistrationService.createUserClientRole(organizationDetails.idpId, token, userId, rolesPayload); return ResponseMessages.user.success.invitationAccept; diff --git a/apps/user/interfaces/user.interface.ts b/apps/user/interfaces/user.interface.ts index 7eaa65d52..503370357 100644 --- a/apps/user/interfaces/user.interface.ts +++ b/apps/user/interfaces/user.interface.ts @@ -5,6 +5,7 @@ export interface IUsersProfile { firstName?: string; lastName?: string; supabaseUserId?: string; + keycloakUserId?: string; userOrgRoles?: IUserOrgRole[]; } diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index f49d1cc79..95337baab 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -223,6 +223,7 @@ export class UserRepository { clientId: true, clientSecret: true, supabaseUserId: true, + keycloakUserId: true, userOrgRoles: { include: { orgRole: true, @@ -271,6 +272,7 @@ export class UserRepository { profileImg: true, publicProfile: true, supabaseUserId: true, + keycloakUserId: true, isEmailVerified: true, userOrgRoles: { select:{ diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index 37e6de446..e47b569c0 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -271,8 +271,19 @@ export class UserService { keycloakDetails.keycloakUserId.toString() ); - const holderRoleData = await this.orgRoleService.getRole(OrgRoles.HOLDER); - await this.userOrgRoleService.createUserOrgRole(userDetails.id, holderRoleData.id); + const realmRoles = await this.clientRegistrationService.getAllRealmRoles(token); + + const holderRole = realmRoles.filter(role => role.name === OrgRoles.HOLDER); + const holderRoleData = 0 < holderRole.length && holderRole[0]; + + const payload = [ + { + id: holderRoleData.id, + name: holderRoleData.name + } + ]; + + await this.clientRegistrationService.createUserHolderRole(token, keycloakDetails.keycloakUserId.toString(), payload); return ResponseMessages.user.success.signUpUser; } catch (error) { @@ -778,7 +789,7 @@ export class UserService { async acceptRejectInvitations(acceptRejectInvitation: AcceptRejectInvitationDto, userId: string): Promise { try { const userData = await this.userRepository.getUserById(userId); - return this.fetchInvitationsStatus(acceptRejectInvitation, userId, userData.email); + return this.fetchInvitationsStatus(acceptRejectInvitation, userData.keycloakUserId, userData.email); } catch (error) { this.logger.error(`acceptRejectInvitations: ${error}`); throw new RpcException(error.response ? error.response : error); diff --git a/libs/client-registration/src/client-registration.service.ts b/libs/client-registration/src/client-registration.service.ts index 7766e0926..4ce1b7621 100644 --- a/libs/client-registration/src/client-registration.service.ts +++ b/libs/client-registration/src/client-registration.service.ts @@ -18,6 +18,7 @@ import { userTokenPayloadDto } from './dtos/userTokenPayloadDto'; import { KeycloakUserRegistrationDto } from 'apps/user/dtos/keycloak-register.dto'; import { ResponseMessages } from '@credebl/common/response-messages'; import { ResponseService } from '@credebl/response'; +import { IClientRoles } from './interfaces/client.interface'; @Injectable() export class ClientRegistrationService { @@ -186,7 +187,6 @@ export class ClientRegistrationService { } } - async getManagementTokenForMobile() { try { const payload = new ClientCredentialTokenPayloadDto(); @@ -277,6 +277,183 @@ export class ClientRegistrationService { } + async createUserClientRole( + idpId: string, + token: string, + userId: string, + payload: object[] + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const createClientRolesResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetClientUserRoleURL(realmName, userId, idpId), + payload, + this.getAuthHeader(token) + ); + + this.logger.debug( + `createUserClientRolesResponse ${JSON.stringify( + createClientRolesResponse + )}` + ); + + return 'User client role is assigned'; + } + + async createUserHolderRole( + token: string, + userId: string, + payload: object[] + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const createClientRolesResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetClientUserRoleURL(realmName, userId), + payload, + this.getAuthHeader(token) + ); + + this.logger.debug( + `createUserHolderRole ${JSON.stringify( + createClientRolesResponse + )}` + ); + + return 'User holder role is assigned'; + } + + async getAllClientRoles( + idpId: string, + token: string + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const clientRolesResponse = await this.commonService.httpGet( + await this.keycloakUrlService.GetClientRoleURL(realmName, idpId), + this.getAuthHeader(token) + ); + + this.logger.debug( + `getAllClientRoles ${JSON.stringify( + clientRolesResponse + )}` + ); + + return clientRolesResponse; + } + + async getClientSpecificRoles( + idpId: string, + token: string, + roleName: string + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const clientRolesResponse = await this.commonService.httpGet( + await this.keycloakUrlService.GetClientRoleURL(realmName, idpId, roleName), + this.getAuthHeader(token) + ); + + this.logger.debug( + `getClientSpecificRoles ${JSON.stringify( + clientRolesResponse + )}` + ); + + return clientRolesResponse; + } + + async getAllRealmRoles( + token: string + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const realmRolesResponse = await this.commonService.httpGet( + await this.keycloakUrlService.GetRealmRoleURL(realmName), + this.getAuthHeader(token) + ); + + this.logger.debug( + `getAllRealmRoles ${JSON.stringify( + realmRolesResponse + )}` + ); + + return realmRolesResponse; + } + + + async createClientRole( + idpId: string, + token: string, + name: string, + description: string + ): Promise { + + const payload = { + clientRole: true, + name, + description + }; + + const realmName = process.env.KEYCLOAK_REALM; + + const createClientRolesResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetClientRoleURL(realmName, idpId), + payload, + this.getAuthHeader(token) + ); + + this.logger.debug( + `createClientRolesResponse ${JSON.stringify( + createClientRolesResponse + )}` + ); + + return 'Client role is created'; + + } + + async generateClientSecret( + idpId: string, + token: string + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const createClientSercretResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetClientSecretURL(realmName, idpId), + {}, + this.getAuthHeader(token) + ); + + this.logger.debug( + `ClientRegistrationService create realm client secret ${JSON.stringify( + createClientSercretResponse + )}` + ); + + const getClientSercretResponse = await this.commonService.httpGet( + await this.keycloakUrlService.GetClientSecretURL(realmName, idpId), + this.getAuthHeader(token) + ); + this.logger.debug( + `ClientRegistrationService get client secret ${JSON.stringify( + getClientSercretResponse + )}` + ); + this.logger.log(`${getClientSercretResponse.value}`); + const clientSecret = getClientSercretResponse.value; + + return clientSecret; + + } + async createClient( orgName: string, orgId: string, @@ -324,20 +501,7 @@ export class ClientRegistrationService { publicClient: false, frontchannelLogout: false, fullScopeAllowed: false, - nodeReRegistrationTimeout: 0, - defaultClientScopes: [ - 'web-origins', - 'role_list', - 'profile', - 'roles', - 'email' - ], - optionalClientScopes: [ - 'address', - 'phone', - 'offline_access', - 'microprofile-jwt' - ] + nodeReRegistrationTimeout: 0 }; const createClientResponse = await this.commonService.httpPost( diff --git a/libs/client-registration/src/interfaces/client.interface.ts b/libs/client-registration/src/interfaces/client.interface.ts new file mode 100644 index 000000000..59e78a52c --- /dev/null +++ b/libs/client-registration/src/interfaces/client.interface.ts @@ -0,0 +1,8 @@ +export interface IClientRoles { + id: string + name: string + description?: string + composite?: boolean + clientRole?: boolean + containerId?: string +} \ No newline at end of file diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index ecea0f3a2..e57e8358b 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -91,7 +91,7 @@ export const ResponseMessages = { rolesNotExist: 'Provided roles not exists in the platform', orgProfile: 'Organization profile not found', userNotFound: 'User not found for the given organization', - orgRoleIdNotFound:'Provided roles not exists in the platform', + orgRoleIdNotFound:'Provided roles not exists for this organization', updateUserRoles: 'Unable to update user roles', deleteOrg: 'Organization not found', deleteOrgInvitation: 'Organization does not have access to delete this invitation', diff --git a/libs/keycloak-url/src/keycloak-url.service.ts b/libs/keycloak-url/src/keycloak-url.service.ts index cb766e604..d10c9f5f8 100644 --- a/libs/keycloak-url/src/keycloak-url.service.ts +++ b/libs/keycloak-url/src/keycloak-url.service.ts @@ -71,6 +71,48 @@ export class KeycloakUrlService { return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/clients/${clientid}/client-secret`; } + async GetClientRoleURL( + realm: string, + clientid: string, + roleName = '' + ):Promise { + + if ('' === roleName) { + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/clients/${clientid}/roles`; + } + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/clients/${clientid}/roles/${roleName}`; + + } + + async GetRealmRoleURL( + realm: string, + roleName = '' + ):Promise { + + if ('' === roleName) { + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/roles`; + } + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/roles/${roleName}`; + + } + + async GetClientUserRoleURL( + realm: string, + userId: string, + clientId?: string + ):Promise { + + if (clientId) { + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/users/${userId}/role-mappings/clients/${clientId}`; + } + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/users/${userId}/role-mappings/realm`; + + } + + async GetClientIdpURL( realm: string, idp: string diff --git a/libs/org-roles/interfaces/org-roles.interface.ts b/libs/org-roles/interfaces/org-roles.interface.ts index b170d93af..ec63e63c1 100644 --- a/libs/org-roles/interfaces/org-roles.interface.ts +++ b/libs/org-roles/interfaces/org-roles.interface.ts @@ -2,8 +2,8 @@ export interface IOrgRoles { id: string; name: string; description: string; - createDateTime: Date; - createdBy: string; - lastChangedDateTime: Date; - lastChangedBy: string; + createDateTime?: Date; + createdBy?: string; + lastChangedDateTime?: Date; + lastChangedBy?: string; } \ No newline at end of file From 79a57ca423cf3b35a1d7aa22f6adb59c41f182ef Mon Sep 17 00:00:00 2001 From: Nishad Date: Tue, 20 Feb 2024 17:28:11 +0530 Subject: [PATCH 02/11] Updated org invitation Signed-off-by: Nishad --- apps/organization/src/organization.service.ts | 4 ++-- libs/common/src/response-messages/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/organization/src/organization.service.ts b/apps/organization/src/organization.service.ts index a52164450..584e27d29 100644 --- a/apps/organization/src/organization.service.ts +++ b/apps/organization/src/organization.service.ts @@ -718,10 +718,10 @@ export class OrganizationService { */ async updateOrgInvitation(payload: UpdateInvitationDto): Promise { try { - const { orgId, status, invitationId, userId } = payload; + const { orgId, status, invitationId, userId, email } = payload; const invitation = await this.organizationRepository.getInvitationById(String(invitationId)); - if (!invitation) { + if (!invitation || (invitation && invitation.email !== email)) { throw new NotFoundException(ResponseMessages.user.error.invitationNotFound); } diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index e57e8358b..4116f97eb 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -40,7 +40,7 @@ export const ResponseMessages = { verifyMail: 'Please verify your email', invalidCredentials: 'Invalid Credentials', registerFido: 'Please complete your fido registration', - invitationNotFound: 'Invitation not found', + invitationNotFound: 'Invitation not found for this user', invitationAlreadyAccepted:'Organization invitation already accepted', invitationAlreadyRejected:'Organization invitation already rejected', invalidInvitationStatus: 'Invalid invitation status', From 169565599ea154fc995e85870a3b7599f3d0bc1b Mon Sep 17 00:00:00 2001 From: Nishad Date: Wed, 21 Feb 2024 20:51:31 +0530 Subject: [PATCH 03/11] created deleteUserCLient roles API, created migration file for idpRoleId, created userorgrole with idp role id Signed-off-by: Nishad --- .../organization/dtos/update-invitation.dt.ts | 1 + apps/organization/src/organization.service.ts | 49 +++++++++++++++++-- apps/user/src/user.controller.ts | 6 +++ apps/user/src/user.service.ts | 14 ++++-- .../src/client-registration.service.ts | 24 ++++++++- .../migration.sql | 2 + libs/prisma-service/prisma/schema.prisma | 3 +- libs/user-org-roles/repositories/index.ts | 7 ++- .../src/user-org-roles.service.ts | 8 +-- 9 files changed, 100 insertions(+), 14 deletions(-) create mode 100644 libs/prisma-service/prisma/migrations/20240220121359_user_org_idproleid/migration.sql diff --git a/apps/organization/dtos/update-invitation.dt.ts b/apps/organization/dtos/update-invitation.dt.ts index 1d80b08d4..21e345858 100644 --- a/apps/organization/dtos/update-invitation.dt.ts +++ b/apps/organization/dtos/update-invitation.dt.ts @@ -5,5 +5,6 @@ export class UpdateInvitationDto { orgId: string; status: Invitation; userId: string; + keycloakUserId: string; email: string; } \ No newline at end of file diff --git a/apps/organization/src/organization.service.ts b/apps/organization/src/organization.service.ts index e006eb3d3..190293d9f 100644 --- a/apps/organization/src/organization.service.ts +++ b/apps/organization/src/organization.service.ts @@ -775,6 +775,27 @@ export class OrganizationService { return userData; } + async getUserUserId(userId: string): Promise { + const pattern = { cmd: 'get-user-by-user-id' }; + // const payload = { id: userId }; + + const userData = await this.organizationServiceProxy + .send(pattern, userId) + .toPromise() + .catch((error) => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.status, + error: error.error, + message: error.message + }, + error.status + ); + }); + return userData; + } + async fetchUserInvitation( email: string, status: string, @@ -886,12 +907,34 @@ export class OrganizationService { throw new NotFoundException(ResponseMessages.organisation.error.userNotFound); } - const isRolesExist = await this.orgRoleService.getOrgRolesByIds(roleIds); + const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); + + if (!organizationDetails) { + throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); + } + + // const roleIdsList = []; + + const token = await this.clientRegistrationService.getManagementToken(); + const clientRolesList = await this.clientRegistrationService.getAllClientRoles(organizationDetails.idpId, token); + // const orgRoles = await this.orgRoleService.getOrgRoles(); - if (isRolesExist && 0 === isRolesExist.length) { - throw new NotFoundException(ResponseMessages.organisation.error.rolesNotExist); + const matchedRoles = clientRolesList.filter((role) => roleIds.includes(role.id.trim())).map((role) => role.name); + + if (roleIds.length !== matchedRoles.length) { + throw new NotFoundException(ResponseMessages.organisation.error.orgRoleIdNotFound); } + const userData = await this.getUserUserId(userId); + + // const isRolesExist = await this.orgRoleService.getOrgRolesByIds(roleIds); + + // if (isRolesExist && 0 === isRolesExist.length) { + // throw new NotFoundException(ResponseMessages.organisation.error.rolesNotExist); + // } + + await this.clientRegistrationService.deleteUserClientRoles(organizationDetails.idpId, token, userData.keycloakUserId); + const deleteUserRecords = await this.userOrgRoleService.deleteOrgRoles(userId, orgId); if (0 === deleteUserRecords['count']) { diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts index 35574e26e..802b7b150 100644 --- a/apps/user/src/user.controller.ts +++ b/apps/user/src/user.controller.ts @@ -90,6 +90,12 @@ export class UserController { async findUserByEmail(payload: { email }): Promise { return this.userService.findUserByEmail(payload); } + + @MessagePattern({ cmd: 'get-user-by-user-id' }) + async findUserByUserId(id: string): Promise { + return this.userService.findUserByUserId(id); + } + /** * @param credentialId * @returns User credentials diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index e47b569c0..4a3f427e1 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -504,6 +504,11 @@ export class UserService { } } + findUserByUserId(id: string): Promise { + return this.userRepository.getUserById(id); + + } + async resetPassword(resetPasswordDto: IUserResetPassword): Promise { const { email, oldPassword, newPassword } = resetPasswordDto; @@ -789,7 +794,7 @@ export class UserService { async acceptRejectInvitations(acceptRejectInvitation: AcceptRejectInvitationDto, userId: string): Promise { try { const userData = await this.userRepository.getUserById(userId); - return this.fetchInvitationsStatus(acceptRejectInvitation, userData.keycloakUserId, userData.email); + return this.fetchInvitationsStatus(acceptRejectInvitation, userData.keycloakUserId, userData.email, userId); } catch (error) { this.logger.error(`acceptRejectInvitations: ${error}`); throw new RpcException(error.response ? error.response : error); @@ -894,15 +899,16 @@ export class UserService { */ async fetchInvitationsStatus( acceptRejectInvitation: AcceptRejectInvitationDto, - userId: string, - email: string + keycloakUserId: string, + email: string, + userId: string ): Promise { try { const pattern = { cmd: 'update-invitation-status' }; const { orgId, invitationId, status } = acceptRejectInvitation; - const payload = { userId, orgId, invitationId, status, email }; + const payload = { userId, keycloakUserId, orgId, invitationId, status, email }; const invitationsData = await this.userServiceProxy .send(pattern, payload) diff --git a/libs/client-registration/src/client-registration.service.ts b/libs/client-registration/src/client-registration.service.ts index 4ce1b7621..7c2d2b7bb 100644 --- a/libs/client-registration/src/client-registration.service.ts +++ b/libs/client-registration/src/client-registration.service.ts @@ -301,6 +301,28 @@ export class ClientRegistrationService { return 'User client role is assigned'; } + async deleteUserClientRoles( + idpId: string, + token: string, + userId: string + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const createClientRolesResponse = await this.commonService.httpDelete( + await this.keycloakUrlService.GetClientUserRoleURL(realmName, userId, idpId), + this.getAuthHeader(token) + ); + + this.logger.debug( + `deleteUserClientRoles ${JSON.stringify( + createClientRolesResponse + )}` + ); + + return 'User client role is deleted'; + } + async createUserHolderRole( token: string, userId: string, @@ -730,7 +752,7 @@ export class ClientRegistrationService { payload.username = email; payload.password = password; - this.logger.log(`User Token Payload: ${JSON.stringify(payload)}`); + // this.logger.log(`User Token Payload: ${JSON.stringify(payload)}`); if ( diff --git a/libs/prisma-service/prisma/migrations/20240220121359_user_org_idproleid/migration.sql b/libs/prisma-service/prisma/migrations/20240220121359_user_org_idproleid/migration.sql new file mode 100644 index 000000000..4d6fed26f --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20240220121359_user_org_idproleid/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "user_org_roles" ADD COLUMN "idpRoleId" UUID; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index 714d724d6..28dcfbf44 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -84,6 +84,7 @@ model user_org_roles { userId String @db.Uuid orgRoleId String @db.Uuid orgId String? @db.Uuid + idpRoleId String? @db.Uuid organisation organisation? @relation(fields: [orgId], references: [id]) orgRole org_roles @relation(fields: [orgRoleId], references: [id]) user user @relation(fields: [userId], references: [id]) @@ -484,4 +485,4 @@ model notification { lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) lastChangedBy String @default("1") deletedAt DateTime? @db.Timestamp(6) -} \ No newline at end of file +} diff --git a/libs/user-org-roles/repositories/index.ts b/libs/user-org-roles/repositories/index.ts index 618c6d50d..5458796cd 100644 --- a/libs/user-org-roles/repositories/index.ts +++ b/libs/user-org-roles/repositories/index.ts @@ -18,13 +18,14 @@ export class UserOrgRolesRepository { * @returns user details */ // eslint-disable-next-line camelcase - async createUserOrgRole(userId: string, roleId: string, orgId?: string): Promise { + async createUserOrgRole(userId: string, roleId: string, orgId?: string, idpRoleId?: string): Promise { try { const data: { orgRole: { connect: { id: string } }; user: { connect: { id: string } }; organisation?: { connect: { id: string } }; + idpRoleId?: string } = { orgRole: { connect: { id: roleId } }, user: { connect: { id: userId } } @@ -34,6 +35,10 @@ export class UserOrgRolesRepository { data.organisation = { connect: { id: orgId } }; } + if (idpRoleId) { + data.idpRoleId = idpRoleId; + } + const saveResponse = await this.prisma.user_org_roles.create({ data }); diff --git a/libs/user-org-roles/src/user-org-roles.service.ts b/libs/user-org-roles/src/user-org-roles.service.ts index e5677dc19..ecad069db 100644 --- a/libs/user-org-roles/src/user-org-roles.service.ts +++ b/libs/user-org-roles/src/user-org-roles.service.ts @@ -13,8 +13,8 @@ export class UserOrgRolesService { * @returns user details */ // eslint-disable-next-line camelcase - async createUserOrgRole(userId: string, roleId: string, orgId?: string): Promise { - return this.userOrgRoleRepository.createUserOrgRole(userId, roleId, orgId); + async createUserOrgRole(userId: string, roleId: string, orgId?: string, idpRoleId?: string): Promise { + return this.userOrgRoleRepository.createUserOrgRole(userId, roleId, orgId, idpRoleId); } @@ -46,10 +46,10 @@ export class UserOrgRolesService { * @param roleIds * @returns */ - async updateUserOrgRole(userId: string, orgId: string, roleIds: string[]): Promise { + async updateUserOrgRole(userId: string, orgId: string, roleIds: string[], idpRoleId?: string): Promise { for (const role of roleIds) { - this.userOrgRoleRepository.createUserOrgRole(userId, role, orgId); + this.userOrgRoleRepository.createUserOrgRole(userId, role, orgId, idpRoleId); } return true; From b608fde5895170bbdeebcf8e061a5173273ad898 Mon Sep 17 00:00:00 2001 From: Nishad Date: Thu, 22 Feb 2024 19:12:44 +0530 Subject: [PATCH 04/11] Refactored the updateUserOrgRole function, Worked on the orgroleguard to get client details from user access token, worked on the update user roles for the client Signed-off-by: Nishad --- .../src/authz/guards/org-roles.guard.ts | 17 +++- apps/organization/src/organization.service.ts | 88 +++++++++++++------ libs/common/src/common.service.ts | 1 - .../src/user-org-roles.service.ts | 10 ++- 4 files changed, 84 insertions(+), 32 deletions(-) diff --git a/apps/api-gateway/src/authz/guards/org-roles.guard.ts b/apps/api-gateway/src/authz/guards/org-roles.guard.ts index b4e71fb26..5888b1dc7 100644 --- a/apps/api-gateway/src/authz/guards/org-roles.guard.ts +++ b/apps/api-gateway/src/authz/guards/org-roles.guard.ts @@ -40,9 +40,22 @@ export class OrgRolesGuard implements CanActivate { if (!isValidUUID(orgId)) { throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); - } + } - if (orgId) { + if (orgId) { + + const orgDetails = user.resource_access[orgId]; + + if (orgDetails) { + const orgRoles: string[] = orgDetails.roles; + const roleAccess = requiredRoles.some((role) => orgRoles.includes(role)); + + if (!roleAccess) { + throw new ForbiddenException(ResponseMessages.organisation.error.roleNotMatch, { cause: new Error(), description: ResponseMessages.errorMessages.forbidden }); + } + return roleAccess; + } + const specificOrg = user.userOrgRoles.find((orgDetails) => { if (!orgDetails.orgId) { return false; diff --git a/apps/organization/src/organization.service.ts b/apps/organization/src/organization.service.ts index 190293d9f..6f9ed83d2 100644 --- a/apps/organization/src/organization.service.ts +++ b/apps/organization/src/organization.service.ts @@ -260,7 +260,14 @@ export class OrganizationService { const deleteUserRecords = await this.userOrgRoleService.deleteOrgRoles(userId, orgId); this.logger.log(`deleteUserRecords::`, deleteUserRecords); - await this.userOrgRoleService.updateUserOrgRole(userId, orgId, [ownerRoleData.id], ownerRoleClient.id); + const roleIdList = [ + { + roleId: ownerRoleData.id, + idpRoleId: ownerRoleClient.id + } + ]; + + await this.userOrgRoleService.updateUserOrgRole(userId, orgId, roleIdList); } return orgDetails; @@ -846,9 +853,8 @@ export class OrganizationService { status }; - await this.organizationRepository.updateOrgInvitation(invitationId, data); - if (status === Invitation.REJECTED) { + await this.organizationRepository.updateOrgInvitation(invitationId, data); return ResponseMessages.user.success.invitationReject; } @@ -857,15 +863,15 @@ export class OrganizationService { const orgRoles = await this.orgRoleService.getOrgRolesByIds(invitation.orgRoles); - const rolesPayload: { id: string; name: string; idpId: string }[] = orgRoles.map((orgRole: IOrgRole) => { - let roleObj: { id: string; name: string; idpId: string } = null; + const rolesPayload: { roleId: string; name: string; idpRoleId: string }[] = orgRoles.map((orgRole: IOrgRole) => { + let roleObj: { roleId: string; name: string; idpRoleId: string} = null; for (let index = 0; index < clientRolesList.length; index++) { if (clientRolesList[index].name === orgRole.name) { roleObj = { - id: orgRole.id, + roleId: orgRole.id, name: orgRole.name, - idpId: clientRolesList[index].id + idpRoleId: clientRolesList[index].id }; break; } @@ -874,16 +880,11 @@ export class OrganizationService { return roleObj; }); - await this.clientRegistrationService.createUserClientRole( - organizationDetails.idpId, - token, - keycloakUserId, - rolesPayload.map((role) => ({ id: role.idpId, name: role.name })) - ); - - for (const roleData of rolesPayload) { - await this.userOrgRoleService.createUserOrgRole(userId, roleData.id, orgId, roleData.idpId); - } + await Promise.all([ + this.organizationRepository.updateOrgInvitation(invitationId, data), + this.clientRegistrationService.createUserClientRole(organizationDetails.idpId, token, keycloakUserId, rolesPayload.map(role => ({id: role.idpRoleId, name: role.name}))), + this.userOrgRoleService.updateUserOrgRole(userId, orgId, rolesPayload) + ]); return ResponseMessages.user.success.invitationAccept; } catch (error) { @@ -917,14 +918,33 @@ export class OrganizationService { const token = await this.clientRegistrationService.getManagementToken(); const clientRolesList = await this.clientRegistrationService.getAllClientRoles(organizationDetails.idpId, token); - // const orgRoles = await this.orgRoleService.getOrgRoles(); + const orgRoles = await this.orgRoleService.getOrgRoles(); - const matchedRoles = clientRolesList.filter((role) => roleIds.includes(role.id.trim())).map((role) => role.name); + const matchedClientRoles = clientRolesList.filter((role) => roleIds.includes(role.id.trim())); + // .map((role) => role.name); + // const matchedOrgRoles = orgRoles.filter((role) => matchedClientRoles.some(clientRole => clientRole.name === role.name)); - if (roleIds.length !== matchedRoles.length) { + if (roleIds.length !== matchedClientRoles.length) { throw new NotFoundException(ResponseMessages.organisation.error.orgRoleIdNotFound); } + const rolesPayload: { roleId: string; name: string; idpRoleId: string }[] = matchedClientRoles.map((clientRole: IClientRoles) => { + let roleObj: { roleId: string; name: string; idpRoleId: string} = null; + + for (let index = 0; index < orgRoles.length; index++) { + if (orgRoles[index].name === clientRole.name) { + roleObj = { + roleId: orgRoles[index].id, + name: orgRoles[index].name, + idpRoleId: clientRole.id + }; + break; + } + } + + return roleObj; + }); + const userData = await this.getUserUserId(userId); // const isRolesExist = await this.orgRoleService.getOrgRolesByIds(roleIds); @@ -933,15 +953,31 @@ export class OrganizationService { // throw new NotFoundException(ResponseMessages.organisation.error.rolesNotExist); // } - await this.clientRegistrationService.deleteUserClientRoles(organizationDetails.idpId, token, userData.keycloakUserId); + // await this.clientRegistrationService.deleteUserClientRoles(organizationDetails.idpId, token, userData.keycloakUserId); - const deleteUserRecords = await this.userOrgRoleService.deleteOrgRoles(userId, orgId); + // const deleteUserRecords = await this.userOrgRoleService.deleteOrgRoles(userId, orgId); - if (0 === deleteUserRecords['count']) { - throw new InternalServerErrorException(ResponseMessages.organisation.error.updateUserRoles); - } + const [ + , + deletedUserRoleRecords + ] = await Promise.all([ + this.clientRegistrationService.deleteUserClientRoles(organizationDetails.idpId, token, userData.keycloakUserId), + this.userOrgRoleService.deleteOrgRoles(userId, orgId) + ]); - return this.userOrgRoleService.updateUserOrgRole(userId, orgId, roleIds); + if (0 === deletedUserRoleRecords['count']) { + throw new InternalServerErrorException(ResponseMessages.organisation.error.updateUserRoles); + } + + const [ + , + isUserRoleUpdated + ] = await Promise.all([ + this.clientRegistrationService.createUserClientRole(organizationDetails.idpId, token, userData.keycloakUserId, rolesPayload.map(role => ({id: role.idpRoleId, name: role.name}))), + this.userOrgRoleService.updateUserOrgRole(userId, orgId, rolesPayload) + ]); + + return isUserRoleUpdated; } catch (error) { this.logger.error(`Error in updateUserRoles: ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); diff --git a/libs/common/src/common.service.ts b/libs/common/src/common.service.ts index 5a34f2247..f6050f09b 100644 --- a/libs/common/src/common.service.ts +++ b/libs/common/src/common.service.ts @@ -36,7 +36,6 @@ export class CommonService { .post(url, payload, apiKey) .toPromise() .then((response: any) => { - this.logger.error(response.data); return response.data; }); } catch (error) { diff --git a/libs/user-org-roles/src/user-org-roles.service.ts b/libs/user-org-roles/src/user-org-roles.service.ts index ecad069db..0b87cd9cf 100644 --- a/libs/user-org-roles/src/user-org-roles.service.ts +++ b/libs/user-org-roles/src/user-org-roles.service.ts @@ -46,10 +46,14 @@ export class UserOrgRolesService { * @param roleIds * @returns */ - async updateUserOrgRole(userId: string, orgId: string, roleIds: string[], idpRoleId?: string): Promise { + async updateUserOrgRole( + userId: string, + orgId: string, + roleIdList: {roleId: string, idpRoleId: string}[] + ): Promise { - for (const role of roleIds) { - this.userOrgRoleRepository.createUserOrgRole(userId, role, orgId, idpRoleId); + for (const roleData of roleIdList) { + this.userOrgRoleRepository.createUserOrgRole(userId, roleData.roleId, orgId, roleData.idpRoleId); } return true; From d633fe1ae93169b0b2d8b23340a463b794af94d2 Mon Sep 17 00:00:00 2001 From: Nishad Date: Fri, 23 Feb 2024 20:24:15 +0530 Subject: [PATCH 05/11] worked on the code optimization of invitations flow, worked on the user access guard and apply on the user controller Signed-off-by: Nishad --- .../src/authz/guards/user-access-guard.ts | 16 + apps/api-gateway/src/user/user.controller.ts | 17 +- apps/organization/src/organization.service.ts | 362 +++++++++++------- 3 files changed, 250 insertions(+), 145 deletions(-) create mode 100644 apps/api-gateway/src/authz/guards/user-access-guard.ts diff --git a/apps/api-gateway/src/authz/guards/user-access-guard.ts b/apps/api-gateway/src/authz/guards/user-access-guard.ts new file mode 100644 index 000000000..ea53f63fc --- /dev/null +++ b/apps/api-gateway/src/authz/guards/user-access-guard.ts @@ -0,0 +1,16 @@ +import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { Observable } from 'rxjs'; + +@Injectable() +export class UserAccessGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean | Promise | Observable { + const request = context.switchToHttp().getRequest(); + + const { user } = request; + + if (user.hasOwnProperty('client_id')) { + throw new UnauthorizedException('You do not have access'); + } + return true; + } +} diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts index 8e9ee72a4..5e85bbfbc 100644 --- a/apps/api-gateway/src/user/user.controller.ts +++ b/apps/api-gateway/src/user/user.controller.ts @@ -50,6 +50,7 @@ import { OrgRoles } from 'libs/org-roles/enums'; import { AwsService } from '@credebl/aws/aws.service'; import { PaginationDto } from '@credebl/common/dtos/pagination.dto'; import { CreateCertificateDto } from './dto/share-certificate.dto'; +import { UserAccessGuard } from '../authz/guards/user-access-guard'; @UseFilters(CustomExceptionFilter) @Controller('users') @@ -132,7 +133,7 @@ export class UserController { summary: 'Fetch login user details', description: 'Fetch login user details' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) @ApiBearerAuth() async getProfile(@User() reqUser: user, @Res() res: Response): Promise { const userData = await this.userService.getProfile(reqUser.id); @@ -154,7 +155,7 @@ export class UserController { summary: 'Get all platform and ecosystem settings', description: 'Get all platform and ecosystem settings' }) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard, UserAccessGuard) @Roles(OrgRoles.PLATFORM_ADMIN) @ApiBearerAuth() async getPlatformSettings(@Res() res: Response): Promise { @@ -174,7 +175,7 @@ export class UserController { summary: 'users activity', description: 'Fetch users activity' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) @ApiBearerAuth() @ApiQuery({ name: 'limit', required: true }) async getUserActivities( @@ -201,7 +202,7 @@ export class UserController { summary: 'organization invitations', description: 'Fetch organization invitations' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) @ApiBearerAuth() @ApiQuery({ name: 'pageNumber', @@ -293,7 +294,7 @@ export class UserController { summary: 'accept/reject organization invitation', description: 'Accept or Reject organization invitations' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) @ApiBearerAuth() async acceptRejectInvitaion( @Body() acceptRejectInvitation: AcceptRejectInvitationDto, @@ -349,7 +350,7 @@ export class UserController { }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) @ApiBearerAuth() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) async updateUserProfile( @Body() updateUserProfileDto: UpdateUserProfileDto, @User() reqUser: user, @@ -375,7 +376,7 @@ export class UserController { @ApiOperation({ summary: 'Store user password details', description: 'Store user password details' }) @ApiExcludeEndpoint() @ApiBearerAuth() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) async addPasskey( @Body() userInfo: AddPasskeyDetailsDto, @@ -403,7 +404,7 @@ export class UserController { summary: 'Update platform and ecosystem settings', description: 'Update platform and ecosystem settings' }) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard, UserAccessGuard) @Roles(OrgRoles.PLATFORM_ADMIN) @ApiBearerAuth() async updatePlatformSettings( diff --git a/apps/organization/src/organization.service.ts b/apps/organization/src/organization.service.ts index 6f9ed83d2..a301a2a79 100644 --- a/apps/organization/src/organization.service.ts +++ b/apps/organization/src/organization.service.ts @@ -1,5 +1,6 @@ /* eslint-disable prefer-destructuring */ -import { organisation, user } from '@prisma/client'; +// eslint-disable-next-line camelcase +import { org_invitations, organisation, user } from '@prisma/client'; import { Injectable, Logger, @@ -105,29 +106,35 @@ export class OrganizationService { delete organizationDetails.orgSlug; delete organizationDetails.website; - const orgCredentials = await this.registerToKeycloak( - organizationDetails.name, - organizationDetails.id, - keycloakUserId, - userId, - false - ); - - const { clientId, idpId } = orgCredentials; + try { + const orgCredentials = await this.registerToKeycloak( + organizationDetails.name, + organizationDetails.id, + keycloakUserId, + userId, + false + ); - const updateOrgData = { - clientId, - idpId - }; + const { clientId, idpId } = orgCredentials; - const updatedOrg = await this.organizationRepository.updateOrganizationById( - updateOrgData, - organizationDetails.id - ); - - if (!updatedOrg) { - throw new InternalServerErrorException(ResponseMessages.organisation.error.credentialsNotUpdate); + const updateOrgData = { + clientId, + idpId + }; + + const updatedOrg = await this.organizationRepository.updateOrganizationById( + updateOrgData, + organizationDetails.id + ); + + if (!updatedOrg) { + throw new InternalServerErrorException(ResponseMessages.organisation.error.credentialsNotUpdate); + } + } catch (error) { + this.logger.error(`Error In creating client : ${JSON.stringify(error)}`); + throw new InternalServerErrorException('Unable to create client'); } + if (createOrgDto.notificationWebhook) { await this.storeOrgWebhookEndpoint(organizationDetails.id, createOrgDto.notificationWebhook); } @@ -174,23 +181,29 @@ export class OrganizationService { clientSecret: this.maskString(generatedClientSecret) }; } else { - const orgCredentials = await this.registerToKeycloak( - organizationDetails.name, - organizationDetails.id, - keycloakUserId, - userId, - true - ); - - const { clientId, idpId, clientSecret } = orgCredentials; - generatedClientSecret = clientSecret; - - updateOrgData = { - clientId, - clientSecret: this.maskString(clientSecret), - idpId - }; + try { + const orgCredentials = await this.registerToKeycloak( + organizationDetails.name, + organizationDetails.id, + keycloakUserId, + userId, + true + ); + + const { clientId, idpId, clientSecret } = orgCredentials; + + generatedClientSecret = clientSecret; + + updateOrgData = { + clientId, + clientSecret: this.maskString(clientSecret), + idpId + }; + } catch (error) { + this.logger.error(`Error In creating client : ${JSON.stringify(error)}`); + throw new InternalServerErrorException('Unable to create client'); + } } const updatedOrg = await this.organizationRepository.updateOrganizationById(updateOrgData, orgId); @@ -228,14 +241,9 @@ export class OrganizationService { const orgRolesList = [OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER]; - try { for (const role of orgRolesList) { await this.clientRegistrationService.createClientRole(orgDetails.idpId, token, role, role); - } - } catch (error) { - this.logger.error(`Error In creating client roles : ${JSON.stringify(error)}`); - throw new InternalServerErrorException('Unable to create client roles'); - } + } const ownerRoleClient = await this.clientRegistrationService.getClientSpecificRoles( orgDetails.idpId, @@ -250,24 +258,28 @@ export class OrganizationService { } ]; - await this.clientRegistrationService.createUserClientRole(orgDetails.idpId, token, keycloakUserId, payload); - const ownerRoleData = await this.orgRoleService.getRole(OrgRoles.OWNER); if (!shouldUpdateRole) { - await this.userOrgRoleService.createUserOrgRole(userId, ownerRoleData.id, orgId, ownerRoleClient.id); - } else { - const deleteUserRecords = await this.userOrgRoleService.deleteOrgRoles(userId, orgId); - this.logger.log(`deleteUserRecords::`, deleteUserRecords); + await Promise.all([ + this.clientRegistrationService.createUserClientRole(orgDetails.idpId, token, keycloakUserId, payload), + this.userOrgRoleService.createUserOrgRole(userId, ownerRoleData.id, orgId, ownerRoleClient.id) + ]); + + } else { const roleIdList = [ { roleId: ownerRoleData.id, idpRoleId: ownerRoleClient.id } - ]; - - await this.userOrgRoleService.updateUserOrgRole(userId, orgId, roleIdList); + ]; + + await Promise.all([ + this.clientRegistrationService.createUserClientRole(orgDetails.idpId, token, keycloakUserId, payload), + this.userOrgRoleService.deleteOrgRoles(userId, orgId), + this.userOrgRoleService.updateUserOrgRole(userId, orgId, roleIdList) + ]); } return orgDetails; @@ -576,6 +588,10 @@ export class OrganizationService { throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); } + if (!organizationDetails.idpId) { + return this.orgRoleService.getOrgRoles(); + } + const token = await this.clientRegistrationService.getManagementToken(); return this.clientRegistrationService.getAllClientRoles(organizationDetails.idpId, token); @@ -622,73 +638,134 @@ export class OrganizationService { } } - /** - * - * @Body sendInvitationDto - * @returns createInvitation - */ - - async createInvitation(bulkInvitationDto: BulkSendInvitationDto, userId: string, userEmail: string): Promise { + async createInvitationByOrgRoles( + bulkInvitationDto: BulkSendInvitationDto, + userEmail: string, + userId: string, + orgName: string + ): Promise { const { invitations, orgId } = bulkInvitationDto; - try { - const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); - - const token = await this.clientRegistrationService.getManagementToken(); - const clientRolesList = await this.clientRegistrationService.getAllClientRoles(organizationDetails.idpId, token); - const orgRoles = await this.orgRoleService.getOrgRoles(); - for (const invitation of invitations) { const { orgRoleId, email } = invitation; const isUserExist = await this.checkUserExistInPlatform(email); const userData = await this.getUserFirstName(userEmail); - - const { firstName } = userData; - - const matchedRoles = clientRolesList - .filter((role) => orgRoleId.includes(role.id.trim())) - .map((role) => role.name); - - if (orgRoleId.length !== matchedRoles.length) { + + const {firstName} = userData; + const orgRolesDetails = await this.orgRoleService.getOrgRolesByIds(orgRoleId); + + if (0 === orgRolesDetails.length) { throw new NotFoundException(ResponseMessages.organisation.error.orgRoleIdNotFound); } - const filteredOrgRoles = orgRoles.filter((role) => matchedRoles.includes(role.name.trim())); - - // const orgRolesDetails = await this.orgRoleService.getOrgRolesByIds(orgRoleId); - - // if (0 === orgRolesDetails.length) { - // throw new NotFoundException(ResponseMessages.organisation.error.orgRoleIdNotFound); - // } - const isInvitationExist = await this.checkInvitationExist(email, orgId); if (!isInvitationExist && userEmail !== invitation.email) { - // await this.organizationRepository.createSendInvitation(email, String(orgId), String(userId), matchedRoles.map(role => role.id)); - await this.organizationRepository.createSendInvitation( - email, - String(orgId), - String(userId), - filteredOrgRoles.map((role) => role.id) - ); + await this.organizationRepository.createSendInvitation(email, String(orgId), String(userId), orgRoleId); try { - // await this.sendInviteEmailTemplate(email, organizationDetails.name, matchedRoles, firstName, isUserExist); - await this.sendInviteEmailTemplate( - email, - organizationDetails.name, - filteredOrgRoles, - firstName, - isUserExist - ); + await this.sendInviteEmailTemplate(email, orgName, orgRolesDetails, firstName, isUserExist); } catch (error) { throw new InternalServerErrorException(ResponseMessages.user.error.emailSend); } } } + } + + async createInvitationByClientRoles( + bulkInvitationDto: BulkSendInvitationDto, + userEmail: string, + userId: string, + orgName: string, + idpId: string + ): Promise { + const { invitations, orgId } = bulkInvitationDto; + + const token = await this.clientRegistrationService.getManagementToken(); + const clientRolesList = await this.clientRegistrationService.getAllClientRoles(idpId, token); + const orgRoles = await this.orgRoleService.getOrgRoles(); + + for (const invitation of invitations) { + const { orgRoleId, email } = invitation; + + const isUserExist = await this.checkUserExistInPlatform(email); + + const userData = await this.getUserFirstName(userEmail); + + const { firstName } = userData; + + const matchedRoles = clientRolesList + .filter((role) => orgRoleId.includes(role.id.trim())) + .map((role) => role.name); + + if (orgRoleId.length !== matchedRoles.length) { + throw new NotFoundException(ResponseMessages.organisation.error.orgRoleIdNotFound); + } + + const filteredOrgRoles = orgRoles.filter((role) => matchedRoles.includes(role.name.trim())); + + const isInvitationExist = await this.checkInvitationExist(email, orgId); + + if (!isInvitationExist && userEmail !== invitation.email) { + + await this.organizationRepository.createSendInvitation( + email, + String(orgId), + String(userId), + filteredOrgRoles.map((role) => role.id) + ); + + try { + await this.sendInviteEmailTemplate( + email, + orgName, + filteredOrgRoles, + firstName, + isUserExist + ); + } catch (error) { + throw new InternalServerErrorException(ResponseMessages.user.error.emailSend); + } + } + } + } + + /** + * + * @Body sendInvitationDto + * @returns createInvitation + */ + + async createInvitation(bulkInvitationDto: BulkSendInvitationDto, userId: string, userEmail: string): Promise { + const { orgId } = bulkInvitationDto; + + try { + const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); + + if (!organizationDetails) { + throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); + } + + if (!organizationDetails.idpId) { + await this.createInvitationByOrgRoles( + bulkInvitationDto, + userEmail, + userId, + organizationDetails.name + ); + } else { + await this.createInvitationByClientRoles( + bulkInvitationDto, + userEmail, + userId, + organizationDetails.name, + organizationDetails.idpId + ); + } + await this.userActivityService.createActivity( userId, organizationDetails.id, @@ -818,6 +895,49 @@ export class OrganizationService { } } + async updateClientInvitation( + // eslint-disable-next-line camelcase + invitation: org_invitations, + idpId: string, + userId: string, + keycloakUserId: string, + orgId: string, + status: string + ): Promise { + const token = await this.clientRegistrationService.getManagementToken(); + const clientRolesList = await this.clientRegistrationService.getAllClientRoles(idpId, token); + + const orgRoles = await this.orgRoleService.getOrgRolesByIds(invitation.orgRoles); + + const rolesPayload: { roleId: string; name: string; idpRoleId: string }[] = orgRoles.map((orgRole: IOrgRole) => { + let roleObj: { roleId: string; name: string; idpRoleId: string} = null; + + for (let index = 0; index < clientRolesList.length; index++) { + if (clientRolesList[index].name === orgRole.name) { + roleObj = { + roleId: orgRole.id, + name: orgRole.name, + idpRoleId: clientRolesList[index].id + }; + break; + } + } + + return roleObj; + }); + + const data = { + status + }; + + await Promise.all([ + this.organizationRepository.updateOrgInvitation(invitation.id, data), + this.clientRegistrationService.createUserClientRole(idpId, token, keycloakUserId, rolesPayload.map(role => ({id: role.idpRoleId, name: role.name}))), + this.userOrgRoleService.updateUserOrgRole(userId, orgId, rolesPayload) + ]); + + } + /** * * @param payload @@ -858,33 +978,15 @@ export class OrganizationService { return ResponseMessages.user.success.invitationReject; } - const token = await this.clientRegistrationService.getManagementToken(); - const clientRolesList = await this.clientRegistrationService.getAllClientRoles(organizationDetails.idpId, token); - - const orgRoles = await this.orgRoleService.getOrgRolesByIds(invitation.orgRoles); - - const rolesPayload: { roleId: string; name: string; idpRoleId: string }[] = orgRoles.map((orgRole: IOrgRole) => { - let roleObj: { roleId: string; name: string; idpRoleId: string} = null; + if (organizationDetails.idpId) { + await this.updateClientInvitation(invitation, organizationDetails.idpId, userId, keycloakUserId, orgId, status); + } else { + await this.organizationRepository.updateOrgInvitation(invitationId, data); - for (let index = 0; index < clientRolesList.length; index++) { - if (clientRolesList[index].name === orgRole.name) { - roleObj = { - roleId: orgRole.id, - name: orgRole.name, - idpRoleId: clientRolesList[index].id - }; - break; - } + for (const roleId of invitation.orgRoles) { + await this.userOrgRoleService.createUserOrgRole(userId, roleId, orgId); } - - return roleObj; - }); - - await Promise.all([ - this.organizationRepository.updateOrgInvitation(invitationId, data), - this.clientRegistrationService.createUserClientRole(organizationDetails.idpId, token, keycloakUserId, rolesPayload.map(role => ({id: role.idpRoleId, name: role.name}))), - this.userOrgRoleService.updateUserOrgRole(userId, orgId, rolesPayload) - ]); + } return ResponseMessages.user.success.invitationAccept; } catch (error) { @@ -914,16 +1016,12 @@ export class OrganizationService { throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); } - // const roleIdsList = []; - const token = await this.clientRegistrationService.getManagementToken(); const clientRolesList = await this.clientRegistrationService.getAllClientRoles(organizationDetails.idpId, token); const orgRoles = await this.orgRoleService.getOrgRoles(); const matchedClientRoles = clientRolesList.filter((role) => roleIds.includes(role.id.trim())); - // .map((role) => role.name); - // const matchedOrgRoles = orgRoles.filter((role) => matchedClientRoles.some(clientRole => clientRole.name === role.name)); - + if (roleIds.length !== matchedClientRoles.length) { throw new NotFoundException(ResponseMessages.organisation.error.orgRoleIdNotFound); } @@ -947,16 +1045,6 @@ export class OrganizationService { const userData = await this.getUserUserId(userId); - // const isRolesExist = await this.orgRoleService.getOrgRolesByIds(roleIds); - - // if (isRolesExist && 0 === isRolesExist.length) { - // throw new NotFoundException(ResponseMessages.organisation.error.rolesNotExist); - // } - - // await this.clientRegistrationService.deleteUserClientRoles(organizationDetails.idpId, token, userData.keycloakUserId); - - // const deleteUserRecords = await this.userOrgRoleService.deleteOrgRoles(userId, orgId); - const [ , deletedUserRoleRecords From 8ebec4dbeaea5b5594896752aff6170e1eea4da6 Mon Sep 17 00:00:00 2001 From: Nishad Date: Mon, 26 Feb 2024 18:07:55 +0530 Subject: [PATCH 06/11] WIP migrate org as client to keycloak Signed-off-by: Nishad --- .../organization/organization.controller.ts | 22 +++++++++++++++++++ .../src/organization/organization.service.ts | 5 +++++ .../src/organization.controller.ts | 5 +++++ apps/organization/src/organization.service.ts | 12 ++++++++++ 4 files changed, 44 insertions(+) diff --git a/apps/api-gateway/src/organization/organization.controller.ts b/apps/api-gateway/src/organization/organization.controller.ts index e9f5b82fc..4145490a8 100644 --- a/apps/api-gateway/src/organization/organization.controller.ts +++ b/apps/api-gateway/src/organization/organization.controller.ts @@ -389,6 +389,28 @@ export class OrganizationController { return res.status(HttpStatus.OK).json(finalResponse); } + @Post('/resiter-org-map-users') + @ApiOperation({ + summary: 'Register client and map users', + description: 'Register client and map users' + }) + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + @Roles(OrgRoles.OWNER, OrgRoles.SUPER_ADMIN, OrgRoles.ADMIN) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @ApiBearerAuth() + async registerOrgsMapUsers(@User() user: user, @Res() res: Response): Promise { + + await this.organizationService.registerOrgsMapUsers(user.id); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: 'Organization client created and users mapped to client' + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + + } + @Post('/:orgId/invitations') @ApiOperation({ summary: 'Create organization invitation', diff --git a/apps/api-gateway/src/organization/organization.service.ts b/apps/api-gateway/src/organization/organization.service.ts index 68277b7dd..98290e009 100644 --- a/apps/api-gateway/src/organization/organization.service.ts +++ b/apps/api-gateway/src/organization/organization.service.ts @@ -148,6 +148,11 @@ export class OrganizationService extends BaseService { return this.sendNatsMessage(this.serviceProxy, 'send-invitation', payload); } + async registerOrgsMapUsers(userId: string): Promise { + const payload = {userId}; + return this.sendNatsMessage(this.serviceProxy, 'register-orgs-users-map', payload); + } + /** * * @param updateUserDto diff --git a/apps/organization/src/organization.controller.ts b/apps/organization/src/organization.controller.ts index 3e6ddc2c0..245f4a8e8 100644 --- a/apps/organization/src/organization.controller.ts +++ b/apps/organization/src/organization.controller.ts @@ -117,6 +117,11 @@ export class OrganizationController { return this.organizationService.getOrgRoles(payload.orgId); } + @MessagePattern({ cmd: 'register-orgs-users-map' }) + async registerOrgsMapUsers(): Promise { + return this.organizationService.registerOrgsMapUsers(); + } + /** * Description: create new organization invitation * @param payload invitation Details diff --git a/apps/organization/src/organization.service.ts b/apps/organization/src/organization.service.ts index a301a2a79..72a429af1 100644 --- a/apps/organization/src/organization.service.ts +++ b/apps/organization/src/organization.service.ts @@ -1207,6 +1207,18 @@ export class OrganizationService { } } + async registerOrgsMapUsers(): Promise { + + try { + + return ''; + } catch (error) { + this.logger.error(`Error in registerOrgsMapUsers: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + + } + } + async deleteOrganizationInvitation(orgId: string, invitationId: string): Promise { try { const invitationDetails = await this.organizationRepository.getInvitationById(invitationId); From 0ba61cc78be7d0b1796030742bc082a2cf34e694 Mon Sep 17 00:00:00 2001 From: Nishad Date: Wed, 28 Feb 2024 16:44:12 +0530 Subject: [PATCH 07/11] WIP register org to keycloak and map owner Signed-off-by: Nishad --- .../organization/organization.controller.ts | 7 +--- .../src/organization/organization.service.ts | 4 +- .../repositories/organization.repository.ts | 38 +++++++++++++++++++ apps/organization/src/organization.service.ts | 36 ++++++++++++++++++ 4 files changed, 78 insertions(+), 7 deletions(-) diff --git a/apps/api-gateway/src/organization/organization.controller.ts b/apps/api-gateway/src/organization/organization.controller.ts index 4145490a8..8ea50d754 100644 --- a/apps/api-gateway/src/organization/organization.controller.ts +++ b/apps/api-gateway/src/organization/organization.controller.ts @@ -395,12 +395,9 @@ export class OrganizationController { description: 'Register client and map users' }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) - @Roles(OrgRoles.OWNER, OrgRoles.SUPER_ADMIN, OrgRoles.ADMIN) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - @ApiBearerAuth() - async registerOrgsMapUsers(@User() user: user, @Res() res: Response): Promise { + async registerOrgsMapUsers(@Res() res: Response): Promise { - await this.organizationService.registerOrgsMapUsers(user.id); + await this.organizationService.registerOrgsMapUsers(); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, diff --git a/apps/api-gateway/src/organization/organization.service.ts b/apps/api-gateway/src/organization/organization.service.ts index 98290e009..0baf88fa0 100644 --- a/apps/api-gateway/src/organization/organization.service.ts +++ b/apps/api-gateway/src/organization/organization.service.ts @@ -148,8 +148,8 @@ export class OrganizationService extends BaseService { return this.sendNatsMessage(this.serviceProxy, 'send-invitation', payload); } - async registerOrgsMapUsers(userId: string): Promise { - const payload = {userId}; + async registerOrgsMapUsers(): Promise { + const payload = {}; return this.sendNatsMessage(this.serviceProxy, 'register-orgs-users-map', payload); } diff --git a/apps/organization/repositories/organization.repository.ts b/apps/organization/repositories/organization.repository.ts index 930fe04d7..05f9eebd6 100644 --- a/apps/organization/repositories/organization.repository.ts +++ b/apps/organization/repositories/organization.repository.ts @@ -14,6 +14,7 @@ import { UserOrgRolesService } from '@credebl/user-org-roles'; import { organisation } from '@prisma/client'; import { ResponseMessages } from '@credebl/common/response-messages'; import { IOrganizationInvitations, IOrganization, IOrganizationDashboard} from '@credebl/common/interfaces/organization.interface'; +import { OrgRoles } from 'libs/org-roles/enums'; @Injectable() export class OrganizationRepository { @@ -478,6 +479,43 @@ export class OrganizationRepository { } } + async getUnregisteredClientOrgs(): Promise { + try { + const recordsWithNullValues = await this.prisma.organisation.findMany({ + where: { + idpId: null + }, + include: { + userOrgRoles: { + where: { + orgRole: { + name: OrgRoles.OWNER + } + }, + include: { + user: { + select: { + email: true, + username: true, + id: true, + keycloakUserId: true, + isEmailVerified: true + } + }, + orgRole: true + } + } + } + }); + + return recordsWithNullValues; + + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + /** * * @param queryObject diff --git a/apps/organization/src/organization.service.ts b/apps/organization/src/organization.service.ts index d1da417bf..b96528b8a 100644 --- a/apps/organization/src/organization.service.ts +++ b/apps/organization/src/organization.service.ts @@ -1235,6 +1235,42 @@ export class OrganizationService { async registerOrgsMapUsers(): Promise { try { + + const unregisteredOrgsList = await this.organizationRepository.getUnregisteredClientOrgs(); + + for (const org of unregisteredOrgsList) { + const ownerUser = 0 < org['userOrgRoles'].length && org['userOrgRoles'][0].user; + const orgObj = { + id: org.id, + idpId: org.idpId, + name: org.name, + ownerId: ownerUser.id, + ownerEmail: ownerUser.email, + ownerKeycloakId: ownerUser.keycloakUserId + }; + + if (orgObj.ownerKeycloakId) { + const orgCredentials = await this.registerToKeycloak( + orgObj.name, + orgObj.id, + orgObj.ownerKeycloakId, + orgObj.ownerId, + true + ); + + const { clientId, idpId, clientSecret } = orgCredentials; + + const updateOrgData = { + clientId, + clientSecret: this.maskString(clientSecret), + idpId + }; + + const updatedOrg = await this.organizationRepository.updateOrganizationById(updateOrgData, orgObj.id); + + this.logger.log(`updatedOrg::`, updatedOrg); + } + } return ''; } catch (error) { From d14a8f57c138d0ff9a2c051e93788527e82ec9df Mon Sep 17 00:00:00 2001 From: Nishad Date: Thu, 29 Feb 2024 19:59:08 +0530 Subject: [PATCH 08/11] refactored role guard, worked on the API to register clients and map users in keycloak Signed-off-by: Nishad --- .../src/authz/guards/org-roles.guard.ts | 10 +- .../organization/organization.controller.ts | 4 + .../repositories/organization.repository.ts | 6 - apps/organization/src/organization.service.ts | 182 +++++++++++++----- 4 files changed, 145 insertions(+), 57 deletions(-) diff --git a/apps/api-gateway/src/authz/guards/org-roles.guard.ts b/apps/api-gateway/src/authz/guards/org-roles.guard.ts index 5888b1dc7..b9018781b 100644 --- a/apps/api-gateway/src/authz/guards/org-roles.guard.ts +++ b/apps/api-gateway/src/authz/guards/org-roles.guard.ts @@ -1,7 +1,5 @@ import { BadRequestException, CanActivate, ExecutionContext, ForbiddenException, Logger } from '@nestjs/common'; -import { HttpException } from '@nestjs/common'; -import { HttpStatus } from '@nestjs/common'; import { Injectable } from '@nestjs/common'; import { OrgRoles } from 'libs/org-roles/enums'; import { ROLES_KEY } from '../decorators/roles.decorator'; @@ -34,15 +32,11 @@ export class OrgRolesGuard implements CanActivate { const orgId = req.params.orgId || req.query.orgId || req.body.orgId; - if (!orgId) { - throw new BadRequestException(ResponseMessages.organisation.error.orgIdIsRequired); - } + if (orgId) { if (!isValidUUID(orgId)) { throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); } - - if (orgId) { const orgDetails = user.resource_access[orgId]; @@ -91,7 +85,7 @@ export class OrgRolesGuard implements CanActivate { return false; } else { - throw new HttpException('organization is required', HttpStatus.BAD_REQUEST); + throw new BadRequestException('organization is required'); } // Sending user friendly message if a user attempts to access an API that is inaccessible to their role diff --git a/apps/api-gateway/src/organization/organization.controller.ts b/apps/api-gateway/src/organization/organization.controller.ts index 8ea50d754..f894e091d 100644 --- a/apps/api-gateway/src/organization/organization.controller.ts +++ b/apps/api-gateway/src/organization/organization.controller.ts @@ -394,6 +394,9 @@ export class OrganizationController { summary: 'Register client and map users', description: 'Register client and map users' }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.PLATFORM_ADMIN) + @ApiBearerAuth() @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) async registerOrgsMapUsers(@Res() res: Response): Promise { @@ -509,6 +512,7 @@ export class OrganizationController { @ApiOperation({ summary: 'Delete Organization Client Credentials', description: 'Delete Organization Client Credentials' }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) @ApiBearerAuth() + @ApiExcludeEndpoint() @UseGuards(AuthGuard('jwt')) async deleteOrgClientCredentials(@Param('orgId') orgId: string, @Res() res: Response): Promise { diff --git a/apps/organization/repositories/organization.repository.ts b/apps/organization/repositories/organization.repository.ts index 05f9eebd6..bbf32493d 100644 --- a/apps/organization/repositories/organization.repository.ts +++ b/apps/organization/repositories/organization.repository.ts @@ -14,7 +14,6 @@ import { UserOrgRolesService } from '@credebl/user-org-roles'; import { organisation } from '@prisma/client'; import { ResponseMessages } from '@credebl/common/response-messages'; import { IOrganizationInvitations, IOrganization, IOrganizationDashboard} from '@credebl/common/interfaces/organization.interface'; -import { OrgRoles } from 'libs/org-roles/enums'; @Injectable() export class OrganizationRepository { @@ -487,11 +486,6 @@ export class OrganizationRepository { }, include: { userOrgRoles: { - where: { - orgRole: { - name: OrgRoles.OWNER - } - }, include: { user: { select: { diff --git a/apps/organization/src/organization.service.ts b/apps/organization/src/organization.service.ts index b96528b8a..76917cff3 100644 --- a/apps/organization/src/organization.service.ts +++ b/apps/organization/src/organization.service.ts @@ -1020,6 +1020,73 @@ export class OrganizationService { } } + async updateUserClientRoles( + // eslint-disable-next-line camelcase + roleIds: string[], + idpId: string, + userId: string, + orgId: string + ): Promise { + const token = await this.clientRegistrationService.getManagementToken(); + const clientRolesList = await this.clientRegistrationService.getAllClientRoles( + idpId, + token + ); + const orgRoles = await this.orgRoleService.getOrgRoles(); + + const matchedClientRoles = clientRolesList.filter((role) => roleIds.includes(role.id.trim())); + + if (roleIds.length !== matchedClientRoles.length) { + throw new NotFoundException(ResponseMessages.organisation.error.orgRoleIdNotFound); + } + + const rolesPayload: { roleId: string; name: string; idpRoleId: string }[] = matchedClientRoles.map( + (clientRole: IClientRoles) => { + let roleObj: { roleId: string; name: string; idpRoleId: string } = null; + + for (let index = 0; index < orgRoles.length; index++) { + if (orgRoles[index].name === clientRole.name) { + roleObj = { + roleId: orgRoles[index].id, + name: orgRoles[index].name, + idpRoleId: clientRole.id + }; + break; + } + } + + return roleObj; + } + ); + + const userData = await this.getUserUserId(userId); + + const [, deletedUserRoleRecords] = await Promise.all([ + this.clientRegistrationService.deleteUserClientRoles( + idpId, + token, + userData.keycloakUserId + ), + this.userOrgRoleService.deleteOrgRoles(userId, orgId) + ]); + + if (0 === deletedUserRoleRecords['count']) { + throw new InternalServerErrorException(ResponseMessages.organisation.error.updateUserRoles); + } + + const [, isUserRoleUpdated] = await Promise.all([ + this.clientRegistrationService.createUserClientRole( + idpId, + token, + userData.keycloakUserId, + rolesPayload.map((role) => ({ id: role.idpRoleId, name: role.name })) + ), + this.userOrgRoleService.updateUserOrgRole(userId, orgId, rolesPayload) + ]); + + return isUserRoleUpdated; + } + /** * * @param orgId @@ -1041,56 +1108,34 @@ export class OrganizationService { throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); } - const token = await this.clientRegistrationService.getManagementToken(); - const clientRolesList = await this.clientRegistrationService.getAllClientRoles(organizationDetails.idpId, token); - const orgRoles = await this.orgRoleService.getOrgRoles(); + if (!organizationDetails.idpId) { + const isRolesExist = await this.orgRoleService.getOrgRolesByIds(roleIds); - const matchedClientRoles = clientRolesList.filter((role) => roleIds.includes(role.id.trim())); - - if (roleIds.length !== matchedClientRoles.length) { - throw new NotFoundException(ResponseMessages.organisation.error.orgRoleIdNotFound); - } + if (isRolesExist && 0 === isRolesExist.length) { + throw new NotFoundException(ResponseMessages.organisation.error.rolesNotExist); + } - const rolesPayload: { roleId: string; name: string; idpRoleId: string }[] = matchedClientRoles.map((clientRole: IClientRoles) => { - let roleObj: { roleId: string; name: string; idpRoleId: string} = null; + const deleteUserRecords = await this.userOrgRoleService.deleteOrgRoles(userId, orgId); - for (let index = 0; index < orgRoles.length; index++) { - if (orgRoles[index].name === clientRole.name) { - roleObj = { - roleId: orgRoles[index].id, - name: orgRoles[index].name, - idpRoleId: clientRole.id - }; - break; - } + if (0 === deleteUserRecords['count']) { + throw new InternalServerErrorException(ResponseMessages.organisation.error.updateUserRoles); } - return roleObj; - }); - - const userData = await this.getUserUserId(userId); + for (const role of roleIds) { + this.userOrgRoleService.createUserOrgRole(userId, role, orgId); + } - const [ - , - deletedUserRoleRecords - ] = await Promise.all([ - this.clientRegistrationService.deleteUserClientRoles(organizationDetails.idpId, token, userData.keycloakUserId), - this.userOrgRoleService.deleteOrgRoles(userId, orgId) - ]); + return true; + } else { - if (0 === deletedUserRoleRecords['count']) { - throw new InternalServerErrorException(ResponseMessages.organisation.error.updateUserRoles); - } + return this.updateUserClientRoles( + roleIds, + organizationDetails.idpId, + userId, + organizationDetails.id + ); + } - const [ - , - isUserRoleUpdated - ] = await Promise.all([ - this.clientRegistrationService.createUserClientRole(organizationDetails.idpId, token, userData.keycloakUserId, rolesPayload.map(role => ({id: role.idpRoleId, name: role.name}))), - this.userOrgRoleService.updateUserOrgRole(userId, orgId, rolesPayload) - ]); - - return isUserRoleUpdated; } catch (error) { this.logger.error(`Error in updateUserRoles: ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -1237,9 +1282,19 @@ export class OrganizationService { try { const unregisteredOrgsList = await this.organizationRepository.getUnregisteredClientOrgs(); + + if (!unregisteredOrgsList || 0 === unregisteredOrgsList.length) { + throw new NotFoundException('Unregistered client organizations not found'); + } for (const org of unregisteredOrgsList) { - const ownerUser = 0 < org['userOrgRoles'].length && org['userOrgRoles'][0].user; + const userOrgRoles = 0 < org['userOrgRoles'].length && org['userOrgRoles']; + + const ownerUserList = 0 < org['userOrgRoles'].length + && userOrgRoles.filter(userOrgRole => userOrgRole.orgRole.name === OrgRoles.OWNER); + + const ownerUser = 0 < ownerUserList.length && ownerUserList[0].user; + const orgObj = { id: org.id, idpId: org.idpId, @@ -1269,6 +1324,47 @@ export class OrganizationService { const updatedOrg = await this.organizationRepository.updateOrganizationById(updateOrgData, orgObj.id); this.logger.log(`updatedOrg::`, updatedOrg); + + const usersToRegisterList = userOrgRoles.filter(userOrgRole => null !== userOrgRole.user.keycloakUserId); + + const token = await this.clientRegistrationService.getManagementToken(); + const clientRolesList = await this.clientRegistrationService.getAllClientRoles(idpId, token); + + const deletedUserDetails: string[] = []; + for (const userRole of usersToRegisterList) { + const user = userRole.user; + + const matchedClientRoles = clientRolesList.filter((role) => userRole.orgRole.name === role.name) + .map(clientRole => ({roleId: userRole.orgRole.id, idpRoleId: clientRole.id, name: clientRole.name})); + + if (!deletedUserDetails.includes(user.id)) { + const [, deletedUserRoleRecords] = await Promise.all([ + this.clientRegistrationService.deleteUserClientRoles(idpId, token, user.keycloakUserId), + this.userOrgRoleService.deleteOrgRoles(user.id, orgObj.id) + ]); + + this.logger.log(`deletedUserRoleRecords::`, deletedUserRoleRecords); + + deletedUserDetails.push(user.id); + } + + + await Promise.all([ + this.clientRegistrationService.createUserClientRole( + idpId, + token, + user.keycloakUserId, + matchedClientRoles.map((role) => ({ id: role.idpRoleId, name: role.name })) + ), + this.userOrgRoleService.updateUserOrgRole( + user.id, + orgObj.id, + matchedClientRoles.map((role) => ({ roleId: role.roleId, idpRoleId: role.idpRoleId })) + ) + ]); + this.logger.log(`Organization client created and users mapped to roles`); + + } } } From 6f7ba0fc10d1b5b5950c176ba7d0749d43ec8d73 Mon Sep 17 00:00:00 2001 From: Nishad Date: Fri, 1 Mar 2024 11:50:17 +0530 Subject: [PATCH 09/11] refactor register user for org roles in DB Signed-off-by: Nishad --- apps/user/src/user.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index 5c1c77d96..0f03c13a8 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -284,6 +284,8 @@ export class UserService { ]; await this.clientRegistrationService.createUserHolderRole(token, keycloakDetails.keycloakUserId.toString(), payload); + const holderOrgRole = await this.orgRoleService.getRole(OrgRoles.HOLDER); + await this.userOrgRoleService.createUserOrgRole(userDetails.id, holderOrgRole.id, null, holderRoleData.id); return ResponseMessages.user.success.signUpUser; } catch (error) { From b85a5ad939d5c5da758c431a498c94777625449b Mon Sep 17 00:00:00 2001 From: Nishad Date: Tue, 5 Mar 2024 18:31:40 +0530 Subject: [PATCH 10/11] refactor API endpoint name Signed-off-by: Nishad --- apps/api-gateway/src/organization/organization.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api-gateway/src/organization/organization.controller.ts b/apps/api-gateway/src/organization/organization.controller.ts index f894e091d..39e4e4047 100644 --- a/apps/api-gateway/src/organization/organization.controller.ts +++ b/apps/api-gateway/src/organization/organization.controller.ts @@ -389,7 +389,7 @@ export class OrganizationController { return res.status(HttpStatus.OK).json(finalResponse); } - @Post('/resiter-org-map-users') + @Post('/register-org-map-users') @ApiOperation({ summary: 'Register client and map users', description: 'Register client and map users' From b666d97963e7779662a1024c8eef7d2a3e1cbd78 Mon Sep 17 00:00:00 2001 From: Nishad Date: Wed, 6 Mar 2024 18:38:26 +0530 Subject: [PATCH 11/11] renamed the variable in organization repository Signed-off-by: Nishad --- apps/organization/repositories/organization.repository.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/organization/repositories/organization.repository.ts b/apps/organization/repositories/organization.repository.ts index a267176e4..e2464aab6 100644 --- a/apps/organization/repositories/organization.repository.ts +++ b/apps/organization/repositories/organization.repository.ts @@ -494,7 +494,7 @@ export class OrganizationRepository { async getUnregisteredClientOrgs(): Promise { try { - const recordsWithNullValues = await this.prisma.organisation.findMany({ + const recordsWithNullIdpId = await this.prisma.organisation.findMany({ where: { idpId: null }, @@ -516,7 +516,7 @@ export class OrganizationRepository { } }); - return recordsWithNullValues; + return recordsWithNullIdpId; } catch (error) { this.logger.error(`error: ${JSON.stringify(error)}`);