Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Keycloak user role org map #559

Merged
merged 15 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 recordsWithNullValues = await this.prisma.organisation.findMany({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update variable name with recordsWithNullIdpId

where: {
idpId: null
},
include: {
userOrgRoles: {
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
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