Skip to content

Commit

Permalink
code changes
Browse files Browse the repository at this point in the history
Signed-off-by: Sukanya Rath <[email protected]>
  • Loading branch information
sukanya-rath committed Jun 25, 2024
1 parent 1153258 commit acfdf47
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 47 deletions.
8 changes: 3 additions & 5 deletions backend/db/migrations/V1.0.27__admin_tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,28 @@ SET search_path TO pay_transparency;

create table if not exists admin_user
(
admin_user_id uuid not null,
admin_user_id uuid not null default gen_random_uuid(),
idir_user_guid uuid not null,
display_name varchar(255) not null,
create_date timestamp default current_timestamp not null,
create_user varchar(255) not null,
update_date timestamp default current_timestamp not null,
update_user varchar(255) not null,
last_login_date timestamp default current_timestamp not null,
is_active boolean default true not null,
assigned_roles varchar(255) not null,
constraint admin_user_id_pk primary key (admin_user_id)
);

create table if not exists admin_user_history
(
admin_user_history_id uuid not null,
admin_user_history_id uuid not null default gen_random_uuid(),
admin_user_id uuid not null,
idir_user_guid uuid not null,
display_name varchar(255) not null,
create_date timestamp default current_timestamp not null,
create_user varchar(255) not null,
update_date timestamp default current_timestamp not null,
update_user varchar(255) not null,
last_login_date timestamp default current_timestamp not null,
is_active boolean default true not null,
assigned_roles varchar(255) not null,
constraint admin_user_history_id_pk primary key (admin_user_history_id),
Expand All @@ -34,7 +32,7 @@ create table if not exists admin_user_history

create table if not exists admin_user_onboarding
(
admin_user_onboarding_id uuid not null,
admin_user_onboarding_id uuid not null default gen_random_uuid(),
email varchar(255) not null,
first_name varchar(255) not null,
assigned_roles varchar(255) not null,
Expand Down
6 changes: 3 additions & 3 deletions backend/src/v1/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ model user_error {

/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model admin_user {
admin_user_id String @id(map: "admin_user_id_pk") @db.Uuid
admin_user_id String @id(map: "admin_user_id_pk") @default(dbgenerated("gen_random_uuid()")) @db.Uuid
idir_user_guid String @db.Uuid
display_name String @db.VarChar(255)
create_date DateTime @default(now()) @db.Timestamp(6)
Expand All @@ -244,7 +244,7 @@ model admin_user {

/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model admin_user_history {
admin_user_history_id String @id(map: "admin_user_history_id_pk") @db.Uuid
admin_user_history_id String @id(map: "admin_user_history_id_pk") @default(dbgenerated("gen_random_uuid()")) @db.Uuid
admin_user_id String @db.Uuid
idir_user_guid String @db.Uuid
display_name String @db.VarChar(255)
Expand All @@ -260,7 +260,7 @@ model admin_user_history {

/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model admin_user_onboarding {
admin_user_onboarding_id String @id(map: "admin_user_onboarding_id_pk") @db.Uuid
admin_user_onboarding_id String @id(map: "admin_user_onboarding_id_pk") @default(dbgenerated("gen_random_uuid()")) @db.Uuid
email String @db.VarChar(255)
first_name String @db.VarChar(255)
assigned_roles String @db.VarChar(255)
Expand Down
2 changes: 2 additions & 0 deletions backend/src/v1/routes/admin-auth-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ async function logoutHandler(

if (reason == 'sessionExpired') url = '/session-expired';
else if (reason == 'loginError') url = '/login-error';
else if (reason == 'notAuthorized') url = '/unauthorized';
else if (reason == 'roleChanged') url = '/login';
else if (reason == LogoutReason.LoginAzureIdir)
url = '/admin-api/auth/login-azureidir';
else if (reason == 'contactError') url = '/contact-error';
Expand Down
19 changes: 11 additions & 8 deletions backend/src/v1/routes/admin-users-routes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import express, { Request, Response } from 'express';
import { logger } from '../../logger';
import { SSO } from '../services/admin-users-services';
import { SSO, AdminUserService } from '../services/admin-users-services';
import { utils } from '../services/utils-service';
import { PTRT_ADMIN_ROLE_NAME } from '../../constants/admin';
import { HttpStatusCode } from 'axios';
import jsonwebtoken, { JwtPayload } from 'jsonwebtoken';

type ExtendedRequest = Request & { sso: SSO };
const router = express.Router();
Expand Down Expand Up @@ -44,16 +45,18 @@ router.get('', async (req: ExtendedRequest, res: Response) => {

router.post('', async (req: ExtendedRequest, res: Response) => {
try {
const { email, firstName, lastName } = req.body;
if(!email || !firstName || !lastName){
return res.status(400).json({ error: 'Missing required fields - email, firstname, lastname' });

const { email, firstName, roles } = req.body;
if(!email || !firstName || !roles){
return res.status(400).json({ error: 'Missing required fields - email, firstname, roles' });
}

return res.status(200).json(user);
const userInfo = utils.getSessionUser(req);
const jwtPayload = jsonwebtoken.decode(userInfo.jwt) as JwtPayload;
const idirUserGuid = jwtPayload?.idir_user_guid;
await new AdminUserService().addNewUser(email, roles, firstName, idirUserGuid);
return res.status(200).json();
} catch (error) {
logger.error(error);
return res.status(400).json({ error: 'Failed to create user' });
return res.status(500).json({ error: 'Failed to create user' });
}
});
export default router;
124 changes: 114 additions & 10 deletions backend/src/v1/services/admin-auth-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import { Request, Response } from 'express';
import HttpStatus from 'http-status-codes';
import jsonwebtoken, { JwtPayload } from 'jsonwebtoken';
import { config } from '../../config';
import {
KEYCLOAK_IDP_HINT_AZUREIDIR,
OIDC_AZUREIDIR_SCOPE,
} from '../../constants';
import { KEYCLOAK_IDP_HINT_AZUREIDIR, OIDC_AZUREIDIR_SCOPE } from '../../constants';
import { logger as log } from '../../logger';
import { AuthBase } from './auth-utils-service';
import { utils } from './utils-service';
import prisma, { PrismaTransactionalClient } from '../prisma/prisma-client';
import { SSO } from './admin-users-services';

enum LogoutReason {
Login = 'login', // ie. don't log out
Expand All @@ -17,6 +16,8 @@ enum LogoutReason {
LoginError = 'loginError',
LoginAzureIdir = 'loginAzureIdir',
ContactError = 'contactError',
NotAuthorized = 'notAuthorized',
RoleChanged = 'roleChanged'
}

class AdminAuth extends AuthBase {
Expand Down Expand Up @@ -44,13 +45,13 @@ class AdminAuth extends AuthBase {
if (payload?.identity_provider !== KEYCLOAK_IDP_HINT_AZUREIDIR) {
throw new Error(
`backend token invalid, identity_provider is not ${KEYCLOAK_IDP_HINT_AZUREIDIR}`,
jwt,
jwt
);
}
if (payload?.aud !== config.get('oidc:adminClientId')) {
throw new Error(
'backend token invalid, aud claim validation failed',
jwt,
jwt
);
}
return true;
Expand All @@ -68,7 +69,7 @@ class AdminAuth extends AuthBase {
const userInfo = utils.getSessionUser(req);
if (!userInfo?.jwt || !userInfo?._json) {
return res.status(HttpStatus.UNAUTHORIZED).json({
message: 'No session data',
message: 'No session data'
});
}
const userInfoFrontend = {
Expand All @@ -82,8 +83,10 @@ class AdminAuth extends AuthBase {
const userInfo = utils.getSessionUser(req);
const jwtPayload = jsonwebtoken.decode(userInfo.jwt) as JwtPayload;
const idirUserGuid = jwtPayload?.idir_user_guid;
if (!idirUserGuid) {
log.error(`no idir_user_guid found in the jwt token`, userInfo.jwt);
const email = jwtPayload?.email;
const preferred_username = jwtPayload?.preferred_username;
if (!idirUserGuid || !email || !preferred_username) {
log.error(`one of mandatory parameters missing in the token, idir_user_guid: ${idirUserGuid}, email: ${email}, preferred_username: ${preferred_username}`);
return LogoutReason.LoginError;
}

Expand All @@ -93,8 +96,109 @@ class AdminAuth extends AuthBase {
log.error('invalid claims in token', e);
return LogoutReason.LoginError;
}
try {
return await this.processUserOnboarding(email, preferred_username, userInfo?.refreshToken, idirUserGuid);
} catch (e) {
log.error('Failed while processing user onboarding.', e);
return LogoutReason.LoginError;
}

}

private async processUserOnboarding(email: string, preferred_username: string, refreshToken: string, idirUserGuid: string): Promise<LogoutReason> {
const adminUserOnboarding = await prisma.admin_user_onboarding.findFirst({
where: {
email: email,
is_onboarded: false
}
});
// record found , need to do the processing.
if (adminUserOnboarding) {
// get SSO roles from Keycloak
await this.processRolesWithKeycloak(preferred_username, adminUserOnboarding, refreshToken);
await this.storeUserInfoWithHistory(adminUserOnboarding, idirUserGuid);
return LogoutReason.RoleChanged;
} else {
log.info(`No user onboarding record found for the user ${preferred_username}, check user for roles`);
// check for roles, if no roles found throw error
const sso = await SSO.init();
const userRoles = await sso.getRolesByUser(preferred_username);
if (!userRoles || userRoles.length === 0) {
log.error(`No roles found for the user ${preferred_username}, please contact the administrator`);
return LogoutReason.NotAuthorized;
}
return LogoutReason.Login;
}
}

private async storeUserInfoWithHistory(adminUserOnboarding, idirUserGuid: string) {
await prisma.$transaction(async (tx: PrismaTransactionalClient) => {
// update the user onboarding record, idempotent operation, also solves the edge case when call to keycloak was
// successful earlier but db operation had failed.
await tx.admin_user_onboarding.update({
where: {
admin_user_onboarding_id: adminUserOnboarding.admin_user_onboarding_id
},
data: {
is_onboarded: true
}
});
//// create/update a record in the admin user table
const existing_admin_user = await tx.admin_user.findFirst({
where: {
idir_user_guid: idirUserGuid,
is_active: false
}
});
if (existing_admin_user) {
await tx.admin_user.update({
where: {
admin_user_id: existing_admin_user.admin_user_id
},
data: {
update_date: new Date(),
update_user: adminUserOnboarding.created_by,
assigned_roles: adminUserOnboarding.assigned_roles,
is_active: true
}
});
await tx.admin_user_history.create({
data: {
admin_user_id: existing_admin_user.admin_user_id,
display_name: existing_admin_user.display_name,
idir_user_guid: existing_admin_user.idir_user_guid,
create_user: existing_admin_user.create_user,
update_user: existing_admin_user.update_user,
assigned_roles: existing_admin_user.assigned_roles,
is_active: existing_admin_user.is_active
}
});

return LogoutReason.Login;
} else {
await tx.admin_user.create({
data: {
display_name: adminUserOnboarding.first_name,
idir_user_guid: idirUserGuid,
create_user: adminUserOnboarding.created_by,
update_user: adminUserOnboarding.created_by,
assigned_roles: adminUserOnboarding.assigned_roles,
is_active: true
}
});
}
});
}

private async processRolesWithKeycloak(preferred_username: string, adminUserOnboarding, refreshToken: string) {
const sso = await SSO.init();
const userRoles = await sso.getRolesByUser(preferred_username);
if (!userRoles || userRoles.length === 0) {
log.info(`No roles found for the user ${preferred_username}, call API to add the roles`);
const roles = adminUserOnboarding.assigned_roles.split(',').map(role => {
return { name: role };
});
await sso.addRolesToUser(preferred_username, roles);
}
}
}

Expand Down
Loading

0 comments on commit acfdf47

Please sign in to comment.