Skip to content

Commit

Permalink
Merge pull request #559 from credebl/keycloak-user-role-org-map
Browse files Browse the repository at this point in the history
feat: Keycloak user role org map
  • Loading branch information
vivekayanworks authored Mar 6, 2024
2 parents e3146df + b666d97 commit f9df244
Show file tree
Hide file tree
Showing 22 changed files with 1,098 additions and 221 deletions.
25 changes: 16 additions & 9 deletions apps/api-gateway/src/authz/guards/org-roles.guard.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -34,15 +32,24 @@ 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];

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;
Expand Down Expand Up @@ -78,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
Expand Down
16 changes: 16 additions & 0 deletions apps/api-gateway/src/authz/guards/user-access-guard.ts
Original file line number Diff line number Diff line change
@@ -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<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();

const { user } = request;

if (user.hasOwnProperty('client_id')) {
throw new UnauthorizedException('You do not have access');
}
return true;
}
}
41 changes: 36 additions & 5 deletions apps/api-gateway/src/organization/organization.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,17 @@ export class OrganizationController {
* @returns get organization roles
*/

@Get('/roles')
@Get('/:orgId/roles')
@ApiOperation({
summary: 'Fetch org-roles details',
description: 'Fetch org-roles details'
})
@ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto })
@UseGuards(AuthGuard('jwt'))
@ApiBearerAuth()
async getOrgRoles(@Res() res: Response): Promise<Response> {
async getOrgRoles(@Param('orgId', new ParseUUIDPipe({exceptionFactory: (): Error => { throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); }})) orgId: string, @Res() res: Response): Promise<Response> {

const orgRoles = await this.organizationService.getOrgRoles();
const orgRoles = await this.organizationService.getOrgRoles(orgId.trim());

const finalResponse: IResponse = {
statusCode: HttpStatus.OK,
Expand Down Expand Up @@ -327,7 +327,11 @@ export class OrganizationController {
@UseGuards(AuthGuard('jwt'))
@ApiBearerAuth()
async createOrganization(@Body() createOrgDto: CreateOrganizationDto, @Res() res: Response, @User() reqUser: user): Promise<Response> {
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,
Expand All @@ -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<Response> {
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,
Expand Down Expand Up @@ -381,6 +389,28 @@ export class OrganizationController {
return res.status(HttpStatus.OK).json(finalResponse);
}

@Post('/register-org-map-users')
@ApiOperation({
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<Response> {

await this.organizationService.registerOrgsMapUsers();

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',
Expand Down Expand Up @@ -482,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<Response> {

Expand Down
19 changes: 12 additions & 7 deletions apps/api-gateway/src/organization/organization.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ 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';
import { IOrgCredentials, IOrganization, IOrganizationInvitations, IOrganizationDashboard } from '@credebl/common/interfaces/organization.interface';
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 {
Expand All @@ -26,8 +26,8 @@ export class OrganizationService extends BaseService {
* @param createOrgDto
* @returns Organization creation Success
*/
async createOrganization(createOrgDto: CreateOrganizationDto, userId: string): Promise<organisation> {
const payload = { createOrgDto, userId };
async createOrganization(createOrgDto: CreateOrganizationDto, userId: string, keycloakUserId: string): Promise<organisation> {
const payload = { createOrgDto, userId, keycloakUserId };
return this.sendNatsMessage(this.serviceProxy, 'create-organization', payload);
}

Expand All @@ -37,8 +37,8 @@ export class OrganizationService extends BaseService {
* @param userId
* @returns Orgnization client credentials
*/
async createOrgCredentials(orgId: string, userId: string): Promise<IOrgCredentials> {
const payload = { orgId, userId };
async createOrgCredentials(orgId: string, userId: string, keycloakUserId: string): Promise<IOrgCredentials> {
const payload = { orgId, userId, keycloakUserId };
return this.sendNatsMessage(this.serviceProxy, 'create-org-credentials', payload);
}

Expand Down Expand Up @@ -133,8 +133,8 @@ export class OrganizationService extends BaseService {
* @returns get organization roles
*/

async getOrgRoles(): Promise<IOrgRoles[]> {
const payload = {};
async getOrgRoles(orgId: string): Promise<IClientRoles[]> {
const payload = {orgId};
return this.sendNatsMessage(this.serviceProxy, 'get-org-roles', payload);
}

Expand All @@ -148,6 +148,11 @@ export class OrganizationService extends BaseService {
return this.sendNatsMessage(this.serviceProxy, 'send-invitation', payload);
}

async registerOrgsMapUsers(): Promise<string> {
const payload = {};
return this.sendNatsMessage(this.serviceProxy, 'register-orgs-users-map', payload);
}

/**
*
* @param updateUserDto
Expand Down
17 changes: 9 additions & 8 deletions apps/api-gateway/src/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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<Response> {
const userData = await this.userService.getProfile(reqUser.id);
Expand All @@ -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<Response> {
Expand All @@ -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(
Expand All @@ -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',
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions apps/organization/dtos/update-invitation.dt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export class UpdateInvitationDto {
orgId: string;
status: Invitation;
userId: string;
keycloakUserId: string;
email: string;
}
32 changes: 32 additions & 0 deletions apps/organization/repositories/organization.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,38 @@ export class OrganizationRepository {
}
}

async getUnregisteredClientOrgs(): Promise<organisation[]> {
try {
const recordsWithNullIdpId = await this.prisma.organisation.findMany({
where: {
idpId: null
},
include: {
userOrgRoles: {
include: {
user: {
select: {
email: true,
username: true,
id: true,
keycloakUserId: true,
isEmailVerified: true
}
},
orgRole: true
}
}
}
});

return recordsWithNullIdpId;

} catch (error) {
this.logger.error(`error: ${JSON.stringify(error)}`);
throw error;
}
}

/**
*
* @param queryObject
Expand Down
19 changes: 12 additions & 7 deletions apps/organization/src/organization.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -24,8 +24,8 @@ export class OrganizationController {
*/

@MessagePattern({ cmd: 'create-organization' })
async createOrganization(@Body() payload: { createOrgDto: CreateOrganizationDto; userId: string }): Promise<organisation> {
return this.organizationService.createOrganization(payload.createOrgDto, payload.userId);
async createOrganization(@Body() payload: { createOrgDto: CreateOrganizationDto; userId: string, keycloakUserId: string }): Promise<organisation> {
return this.organizationService.createOrganization(payload.createOrgDto, payload.userId, payload.keycloakUserId);
}

/**
Expand All @@ -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<IOrgCredentials> {
return this.organizationService.createOrgCredentials(payload.orgId);
async createOrgCredentials(@Body() payload: { orgId: string; userId: string, keycloakUserId: string }): Promise<IOrgCredentials> {
return this.organizationService.createOrgCredentials(payload.orgId, payload.userId, payload.keycloakUserId);
}

/**
Expand Down Expand Up @@ -113,8 +113,13 @@ export class OrganizationController {
*/

@MessagePattern({ cmd: 'get-org-roles' })
async getOrgRoles(): Promise<IOrgRoles[]> {
return this.organizationService.getOrgRoles();
async getOrgRoles(payload: {orgId: string}): Promise<IClientRoles[]> {
return this.organizationService.getOrgRoles(payload.orgId);
}

@MessagePattern({ cmd: 'register-orgs-users-map' })
async registerOrgsMapUsers(): Promise<string> {
return this.organizationService.registerOrgsMapUsers();
}

/**
Expand Down
Loading

0 comments on commit f9df244

Please sign in to comment.