Skip to content

Commit

Permalink
fix: GEO-1023, GEO-1024 Fix update_date and update_user in admin_user…
Browse files Browse the repository at this point in the history
… table. Fix admin_user_history record. (#621)
  • Loading branch information
jer3k committed Jul 26, 2024
1 parent 7f941c6 commit 2735695
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 77 deletions.
11 changes: 11 additions & 0 deletions backend/src/v1/routes/admin-users-routes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import express, { Application } from 'express';
import request from 'supertest';
import router from './admin-users-routes';
import bodyParser from 'body-parser';
import { faker } from '@faker-js/faker';

const mockGetUsers = jest.fn();
const mockInitSSO = jest.fn();
Expand All @@ -22,6 +23,16 @@ jest.mock('../middlewares/authorization/authorize', () => ({
authorize: () => (req, res, next) => next(),
}));

jest.mock('../middlewares/authorization/authenticate-admin', () => ({
authenticateAdmin:
(...args) =>
(req, res, next) => {
console.log('mockAuthenticateAdmin');
req.user = { admin_user_id: faker.string.uuid(), userInfo: {} };
next();
},
}));

let app: Application;
describe('admin-users-router', () => {
beforeEach(() => {
Expand Down
36 changes: 22 additions & 14 deletions backend/src/v1/routes/admin-users-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ import {
} from '../middlewares/validations/schemas';
import { SSO } from '../services/sso-service';
import { authorize } from '../middlewares/authorization/authorize';
import { ExtendedRequest } from '../types';
import { authenticateAdmin } from '../middlewares/authorization/authenticate-admin';

type ExtendedRequest = Request & { sso: SSO };
type SsoRequest = Request & { sso: SSO };
type SsoExtendedRequest = ExtendedRequest & { sso: SSO };
const router = express.Router();
router.use(authorize([PTRT_ADMIN_ROLE_NAME]));

/**
* Attach the CSS SSO client
*/
router.use(async (req: ExtendedRequest, _, next) => {
router.use(async (req: SsoRequest, _, next) => {
try {
const sso = await SSO.init();
req.sso = sso;
Expand All @@ -30,7 +33,7 @@ router.use(async (req: ExtendedRequest, _, next) => {
/**
* Get all users in the system
*/
router.get('', async (req: ExtendedRequest, res: Response) => {
router.get('', async (req: SsoRequest, res: Response) => {
try {
const users = await req.sso.getUsers();
return res.status(200).json(users);
Expand All @@ -46,7 +49,7 @@ router.get('', async (req: ExtendedRequest, res: Response) => {
router.patch(
'/:userId',
useValidate({ mode: 'body', schema: ASSIGN_ROLE_SCHEMA }),
async (req: ExtendedRequest, res: Response) => {
async (req: SsoRequest, res: Response) => {
const { userId } = req.params;
const data: AssignRoleType = req.body;

Expand All @@ -66,16 +69,21 @@ router.patch(
/**
* Delete user
*/
router.delete('/:userId', async (req: ExtendedRequest, res: Response) => {
const { userId } = req.params;
router.delete(
'/:userId',
authenticateAdmin(),
authorize(['PTRT-ADMIN']),
async (req: SsoExtendedRequest, res: Response) => {
const { userId } = req.params;

try {
await req.sso.deleteUser(userId);
return res.json();
} catch (error) {
logger.error(error);
return res.status(400).json({ error: 'Failed to delete user' });
}
});
try {
await req.sso.deleteUser(userId, req.user.admin_user_id);
return res.json();
} catch (error) {
logger.error(error);
return res.status(400).json({ error: 'Failed to delete user' });
}
},
);

export default router;
30 changes: 13 additions & 17 deletions backend/src/v1/services/admin-auth-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { LocalDateTime, ZoneId, nativeJs } from '@js-joda/core';
import {
LocalDateTime,
ZoneId,
ZonedDateTime,
convert,
nativeJs,
} from '@js-joda/core';
import { admin_user, admin_user_onboarding } from '@prisma/client';
import { Request, Response } from 'express';
import HttpStatus from 'http-status-codes';
Expand Down Expand Up @@ -284,27 +290,17 @@ class AdminAuth extends AuthBase {
display_name: userDetails.displayName,
preferred_username: userDetails.preferredUsername,
email: userDetails.email,
update_date: new Date(),
update_date: convert(ZonedDateTime.now(ZoneId.UTC)).toDate(),
update_user: adminUserOnboarding?.created_by ?? 'Keycloak',
assigned_roles: assigned_roles,
is_active: true,
last_login: isLogin ? new Date() : undefined,
last_login: isLogin
? convert(ZonedDateTime.now(ZoneId.UTC)).toDate()
: undefined,
},
});
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,
preferred_username: existing_admin_user.preferred_username,
email: existing_admin_user.email,
create_date: existing_admin_user.create_date,
update_date: existing_admin_user.update_date,
},
data: existing_admin_user,
});
modified = true;
} else if (existing_admin_user && isLogin) {
Expand All @@ -315,7 +311,7 @@ class AdminAuth extends AuthBase {
admin_user_id: existing_admin_user.admin_user_id,
},
data: {
last_login: new Date(),
last_login: convert(ZonedDateTime.now(ZoneId.UTC)).toDate(),
},
});
modified = true;
Expand Down
19 changes: 15 additions & 4 deletions backend/src/v1/services/sso-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ const mockPrisma = {
jest.mock('../prisma/prisma-client', () => ({
admin_user: {
findMany: (...args) => mockFindMany(...args),
findUniqueOrThrow: (...args) => {
return mockFindUniqueOrThrow(...args);
},
},
$transaction: jest.fn().mockImplementation((fn) => fn(mockPrisma)),
}));
Expand Down Expand Up @@ -350,7 +353,9 @@ describe('sso-service', () => {
mockFindUniqueOrThrow.mockImplementation(() => {
throw new Error('User not found');
});
await expect(client.deleteUser(faker.string.uuid())).rejects.toThrow();
await expect(
client.deleteUser(faker.string.uuid(), faker.string.uuid()),
).rejects.toThrow();
});
});

Expand All @@ -363,7 +368,9 @@ describe('sso-service', () => {
assigned_roles: [],
});
const userId = faker.string.uuid();
await expect(client.deleteUser(userId)).rejects.toThrow(
await expect(
client.deleteUser(userId, faker.string.uuid()),
).rejects.toThrow(
`User not found with id: ${userId}. User name is missing.`,
);
});
Expand All @@ -372,6 +379,7 @@ describe('sso-service', () => {
it('should delete user', async () => {
mockUpdate.mockClear();
const userId = faker.string.uuid();
const modifiedByUserId = faker.string.uuid();
const user = {
admin_user_id: userId,
idirUserGuid: faker.string.uuid(),
Expand All @@ -380,7 +388,7 @@ describe('sso-service', () => {
assigned_roles: 'PTRT-ADMIN,PTRT-USER',
};
mockFindUniqueOrThrow.mockResolvedValue(user);
await client.deleteUser(userId);
await client.deleteUser(userId, modifiedByUserId);
expect(mockAxiosDelete).toHaveBeenCalledTimes(2);
expect(mockAxiosDelete).toHaveBeenCalledWith(
`/users/${user.preferred_username}/roles/PTRT-ADMIN`,
Expand All @@ -390,7 +398,10 @@ describe('sso-service', () => {
);
expect(mockUpdate).toHaveBeenCalledWith({
where: { admin_user_id: userId },
data: { is_active: false },
data: expect.objectContaining({
is_active: false,
update_user: modifiedByUserId,
}),
});
expect(mockCreateHistory).toHaveBeenCalledTimes(1);
});
Expand Down
81 changes: 39 additions & 42 deletions backend/src/v1/services/sso-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { logger } from '../../logger';
import prisma from '../prisma/prisma-client';
import { adminAuth, IUserDetails } from '../services/admin-auth-service';
import { RoleType } from '../types/users';
import { convert, ZonedDateTime, ZoneId } from '@js-joda/core';

const CSS_SSO_BASE_URL = 'https://api.loginproxy.gov.bc.ca/api/v1';
const CSS_SSO_TOKEN_URL =
Expand Down Expand Up @@ -157,16 +158,10 @@ export class SSO {
Object.keys(ssoUsers),
);
for (const prefUser of deletedUsers) {
const user = localUsers.find(
const localUser = localUsers.find(
(localUser) => localUser.preferred_username == prefUser,
);
await prisma.$transaction(async (tx) => {
await tx.admin_user.update({
where: { admin_user_id: user.admin_user_id },
data: { is_active: false },
});
await this.recordHistory(tx, user);
});
await this.setUserInactiveInDatabase(localUser, 'Keycloak');
}

// need new admin_user_id from database
Expand Down Expand Up @@ -309,51 +304,53 @@ export class SSO {

/**
* Delete the user role in keycloak and deactivate the user in the database
* @param userId
* @param userId - the user to delete
* @param modifiedByUserId - the user who modified this record
*/
async deleteUser(userId: string) {
await prisma.$transaction(async (tx) => {
const localUser = await tx.admin_user.findUniqueOrThrow({
where: { admin_user_id: userId },
});

if (!localUser.preferred_username) {
throw new Error(
`User not found with id: ${userId}. User name is missing.`,
);
}
async deleteUser(userId: string, modifiedByUserId: string) {
const localUser = await prisma.admin_user.findUniqueOrThrow({
where: { admin_user_id: userId },
});

const roles = localUser.assigned_roles.split(',') as RoleType[];
await Promise.all(
roles.map((role) =>
this.removeRoleFromUser(localUser.preferred_username, role),
),
if (!localUser.preferred_username) {
throw new Error(
`User not found with id: ${userId}. User name is missing.`,
);
await tx.admin_user.update({
where: { admin_user_id: userId },
data: { is_active: false },
});
await this.recordHistory(tx, localUser);
});
}

const roles = localUser.assigned_roles.split(',') as RoleType[];
await Promise.all(
roles.map((role) =>
this.removeRoleFromUser(localUser.preferred_username, role),
),
);
await this.setUserInactiveInDatabase(localUser, modifiedByUserId);
}

private async removeRoleFromUser(userName: string, roleName: RoleType) {
await this.client.delete(`/users/${userName}/roles/${roleName}`);
}

private async setUserInactiveInDatabase(
localUser: admin_user,
modifiedByUserId: string,
) {
await prisma.$transaction(async (tx) => {
await tx.admin_user.update({
where: { admin_user_id: localUser.admin_user_id },
data: {
is_active: false,
update_date: convert(ZonedDateTime.now(ZoneId.UTC)).toDate(),
update_user: modifiedByUserId,
},
});
await this.recordHistory(tx, localUser);
});
}

private async recordHistory(tx: PrismaTransactionalClient, user: admin_user) {
await tx.admin_user_history.create({
data: {
admin_user_id: user.admin_user_id,
display_name: user.display_name,
idir_user_guid: user.idir_user_guid,
create_user: user.create_user,
update_user: user.update_user,
assigned_roles: user.assigned_roles,
is_active: user.is_active,
preferred_username: user.preferred_username,
email: user.email,
},
data: user,
});
}
}

0 comments on commit 2735695

Please sign in to comment.