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

BHBC-2144: Fix Project Team Member interface + Amend system user GUID implementation #926

Merged
merged 16 commits into from
Jan 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 5 additions & 7 deletions api/src/database/db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import SQL from 'sql-template-strings';
import { SYSTEM_IDENTITY_SOURCE } from '../constants/database';
import { ApiExecuteSQLError } from '../errors/api-error';
import { HTTPError } from '../errors/http-error';
import { setSystemUserContextSQL } from '../queries/database/user-context-queries';
import * as db from './db';
import { getAPIUserDBConnection, getDBConnection, getDBPool, getKnex, IDBConnection, initDBPool } from './db';

Expand Down Expand Up @@ -84,12 +83,6 @@ describe('db', () => {
expect(getDBPoolStub).to.have.been.calledOnce;
expect(connectStub).to.have.been.calledOnce;

const expectedSystemUserContextSQL = setSystemUserContextSQL('testguid', SYSTEM_IDENTITY_SOURCE.IDIR);
expect(queryStub).to.have.been.calledWith(
expectedSystemUserContextSQL?.text,
expectedSystemUserContextSQL?.values
);

expect(queryStub).to.have.been.calledWith('BEGIN');
});
});
Expand Down Expand Up @@ -360,6 +353,10 @@ describe('db', () => {
});

describe('getAPIUserDBConnection', () => {
beforeEach(() => {
process.env.DB_USER_API = 'example_db_username';
});

afterEach(() => {
Sinon.restore();
});
Expand All @@ -375,6 +372,7 @@ describe('db', () => {

expect(getDBConnectionStub).to.have.been.calledWith({
preferred_username: `${DB_USERNAME}@database`,
sims_system_username: DB_USERNAME,
identity_provider: 'database'
});
});
Expand Down
68 changes: 48 additions & 20 deletions api/src/database/db.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import knex, { Knex } from 'knex';
import * as pg from 'pg';
import { SQLStatement } from 'sql-template-strings';
import SQL, { SQLStatement } from 'sql-template-strings';
import { ApiExecuteSQLError, ApiGeneralError } from '../errors/api-error';
import { queries } from '../queries/queries';
import { getUserGuid, getUserIdentitySource } from '../utils/keycloak-utils';
import { getUserGuid, getUserIdentifier, getUserIdentitySource } from '../utils/keycloak-utils';
import { getLogger } from '../utils/logger';

const defaultLog = getLogger('database/db');

const DB_HOST = process.env.DB_HOST;
const DB_PORT = Number(process.env.DB_PORT);
const DB_USERNAME = process.env.DB_USER_API;
const DB_PASSWORD = process.env.DB_USER_API_PASS;
const DB_DATABASE = process.env.DB_DATABASE;
const getDbHost = () => process.env.DB_HOST;
const getDbPort = () => Number(process.env.DB_PORT);
const getDbUsername = () => process.env.DB_USER_API;
const getDbPassword = () => process.env.DB_USER_API_PASS;
const getDbDatabase = () => process.env.DB_DATABASE;

const DB_POOL_SIZE: number = Number(process.env.DB_POOL_SIZE) || 20;
const DB_CONNECTION_TIMEOUT: number = Number(process.env.DB_CONNECTION_TIMEOUT) || 0;
Expand All @@ -21,11 +20,11 @@ const DB_IDLE_TIMEOUT: number = Number(process.env.DB_IDLE_TIMEOUT) || 10000;
const DB_CLIENT = 'pg';

export const defaultPoolConfig: pg.PoolConfig = {
user: DB_USERNAME,
password: DB_PASSWORD,
database: DB_DATABASE,
port: DB_PORT,
host: DB_HOST,
user: getDbUsername(),
password: getDbPassword(),
database: getDbDatabase(),
port: getDbPort(),
host: getDbHost(),
max: DB_POOL_SIZE,
connectionTimeoutMillis: DB_CONNECTION_TIMEOUT,
idleTimeoutMillis: DB_IDLE_TIMEOUT
Expand Down Expand Up @@ -314,20 +313,48 @@ export const getDBConnection = function (keycloakToken: object): IDBConnection {
*/
const _setUserContext = async () => {
const userGuid = getUserGuid(_token);
const userIdentifier = getUserIdentifier(_token);
const userIdentitySource = getUserIdentitySource(_token);
defaultLog.debug({ label: '_setUserContext', userGuid, userIdentifier, userIdentitySource });

if (!userGuid || !userIdentitySource) {
if (!userGuid || !userIdentifier || !userIdentitySource) {
throw new ApiGeneralError('Failed to identify authenticated user');
}

// Set the user context for all queries made using this connection
const setSystemUserContextSQLStatement = queries.database.setSystemUserContextSQL(userGuid, userIdentitySource);
// Patch user GUID
const patchUserGuidSqlStatement = SQL`
UPDATE
system_user
SET
user_guid = ${userGuid.toLowerCase()}
WHERE
system_user_id
IN (
SELECT
su.system_user_id
FROM
system_user su
LEFT JOIN
user_identity_source uis
ON
uis.user_identity_source_id = su.user_identity_source_id
WHERE
su.user_identifier ILIKE ${userIdentifier}
AND
uis.name ILIKE ${userIdentitySource}
AND
user_guid IS NULL
);
`;

if (!setSystemUserContextSQLStatement) {
throw new ApiExecuteSQLError('Failed to build SQL user context statement');
}
// Set the user context for all queries made using this connection
const setSystemUserContextSQLStatement = SQL`
SELECT api_set_context(${userGuid}, ${userIdentitySource});
`;

try {
await _client.query(patchUserGuidSqlStatement.text, patchUserGuidSqlStatement.values);

const response = await _client.query(
setSystemUserContextSQLStatement.text,
setSystemUserContextSQLStatement.values
Expand Down Expand Up @@ -361,7 +388,8 @@ export const getDBConnection = function (keycloakToken: object): IDBConnection {
*/
export const getAPIUserDBConnection = (): IDBConnection => {
return getDBConnection({
preferred_username: `${DB_USERNAME}@database`,
preferred_username: `${getDbUsername()}@database`,
sims_system_username: getDbUsername(),
identity_provider: 'database'
});
};
Expand Down
2 changes: 1 addition & 1 deletion api/src/models/user.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export class UserObject {
id: number;
user_identifier: string;
user_guid: string;
user_guid: string | null;
identity_source: string;
record_end_date: string;
role_ids: number[];
Expand Down
23 changes: 9 additions & 14 deletions api/src/paths/project/{projectId}/participants/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,8 @@ POST.apiDoc = {
type: 'array',
items: {
type: 'object',
required: ['userGuid', 'userIdentifier', 'identitySource', 'roleId'],
required: ['userIdentifier', 'identitySource', 'roleId'],
properties: {
userGuid: {
type: 'string',
description: 'The GUID for the user.'
},
userIdentifier: {
description: 'A IDIR or BCEID username.',
type: 'string'
Expand Down Expand Up @@ -108,6 +104,8 @@ POST.apiDoc = {
}
};

type Participant = { userIdentifier: string; identitySource: string; roleId: number };

export function createProjectParticipants(): RequestHandler {
return async (req, res) => {
const projectId = Number(req.params.projectId);
Expand All @@ -123,16 +121,13 @@ export function createProjectParticipants(): RequestHandler {
const connection = getDBConnection(req['keycloak_token']);

try {
const participants: { userGuid: string; userIdentifier: string; identitySource: string; roleId: number }[] =
req.body.participants;
const participants: Participant[] = req.body.participants;

await connection.open();

const promises: Promise<any>[] = [];

participants.forEach((participant) =>
promises.push(ensureSystemUserAndProjectParticipantUser(projectId, participant, connection))
);
const promises: Promise<any>[] = participants.map((participant) => {
return ensureSystemUserAndProjectParticipantUser(projectId, { ...participant, userGuid: null }, connection);
});

await Promise.all(promises);

Expand All @@ -150,12 +145,12 @@ export function createProjectParticipants(): RequestHandler {

export const ensureSystemUserAndProjectParticipantUser = async (
projectId: number,
participant: { userGuid: string; userIdentifier: string; identitySource: string; roleId: number },
participant: Participant & { userGuid: string | null },
connection: IDBConnection
) => {
const userService = new UserService(connection);

// Add a system user, unless they already have one
// Create or activate the system user
const systemUserObject = await userService.ensureSystemUser(
participant.userGuid,
participant.userIdentifier,
Expand Down
3 changes: 2 additions & 1 deletion api/src/paths/project/{projectId}/participants/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ GET.apiDoc = {
},
user_guid: {
type: 'string',
description: 'The GUID for the user.'
description: 'The GUID for the user.',
nullable: true
},
user_identifier: {
type: 'string'
Expand Down
54 changes: 32 additions & 22 deletions api/src/paths/user/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('user', () => {
expect.fail();
} catch (actualError) {
expect((actualError as HTTPError).status).to.equal(400);
expect((actualError as HTTPError).message).to.equal('Missing required body param: userGuid');
expect((actualError as HTTPError).message).to.equal('Missing required body param: userIdentifier');
}
});

Expand Down Expand Up @@ -62,15 +62,15 @@ describe('user', () => {
}
});

it('should throw a 400 error when no userGuid', async () => {
it('should throw a 400 error when no identitySource', async () => {
const dbConnectionObj = getMockDBConnection();

sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);

const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();

mockReq.body = {
identitySource: SYSTEM_IDENTITY_SOURCE.IDIR,
userGuid: 'aaaa',
userIdentifier: 'username',
roleId: 1
};
Expand All @@ -82,11 +82,11 @@ describe('user', () => {
expect.fail();
} catch (actualError) {
expect((actualError as HTTPError).status).to.equal(400);
expect((actualError as HTTPError).message).to.equal('Missing required body param: userGuid');
expect((actualError as HTTPError).message).to.equal('Missing required body param: identitySource');
}
});

it('should throw a 400 error when no identitySource', async () => {
it('should throw a 400 error when no roleId', async () => {
const dbConnectionObj = getMockDBConnection();

sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);
Expand All @@ -96,7 +96,7 @@ describe('user', () => {
mockReq.body = {
userGuid: 'aaaa',
userIdentifier: 'username',
roleId: 1
identitySource: SYSTEM_IDENTITY_SOURCE.IDIR
};

try {
Expand All @@ -106,11 +106,11 @@ describe('user', () => {
expect.fail();
} catch (actualError) {
expect((actualError as HTTPError).status).to.equal(400);
expect((actualError as HTTPError).message).to.equal('Missing required body param: identitySource');
expect((actualError as HTTPError).message).to.equal('Missing required body param: roleId');
}
});

it('should throw a 400 error when no roleId', async () => {
it('adds a system user and returns 200 on success', async () => {
const dbConnectionObj = getMockDBConnection();

sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);
Expand All @@ -120,38 +120,49 @@ describe('user', () => {
mockReq.body = {
userGuid: 'aaaa',
userIdentifier: 'username',
identitySource: SYSTEM_IDENTITY_SOURCE.IDIR
identitySource: SYSTEM_IDENTITY_SOURCE.IDIR,
roleId: 1
};

try {
const requestHandler = user.addSystemRoleUser();
const mockUserObject: UserObject = {
id: 1,
user_identifier: '',
user_guid: '',
identity_source: '',
record_end_date: '',
role_ids: [1],
role_names: []
};

await requestHandler(mockReq, mockRes, mockNext);
expect.fail();
} catch (actualError) {
expect((actualError as HTTPError).status).to.equal(400);
expect((actualError as HTTPError).message).to.equal('Missing required body param: roleId');
}
const ensureSystemUserStub = sinon.stub(UserService.prototype, 'ensureSystemUser').resolves(mockUserObject);

const adduserSystemRolesStub = sinon.stub(UserService.prototype, 'addUserSystemRoles');

const requestHandler = user.addSystemRoleUser();

await requestHandler(mockReq, mockRes, mockNext);

expect(ensureSystemUserStub).to.have.been.calledOnce;
expect(adduserSystemRolesStub).to.have.been.calledOnce;
});

it('adds a system user and returns 200 on success', async () => {
it('should success when no userGuid', async () => {
const dbConnectionObj = getMockDBConnection();

sinon.stub(db, 'getDBConnection').returns(dbConnectionObj);

const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();

mockReq.body = {
userGuid: 'aaaa',
userIdentifier: 'username',
identitySource: SYSTEM_IDENTITY_SOURCE.IDIR,
userIdentifier: 'username',
roleId: 1
};

const mockUserObject: UserObject = {
id: 1,
user_identifier: '',
user_guid: '',
user_guid: null,
identity_source: '',
record_end_date: '',
role_ids: [1],
Expand All @@ -165,7 +176,6 @@ describe('user', () => {
const requestHandler = user.addSystemRoleUser();

await requestHandler(mockReq, mockRes, mockNext);

expect(ensureSystemUserStub).to.have.been.calledOnce;
expect(adduserSystemRolesStub).to.have.been.calledOnce;
});
Expand Down
12 changes: 4 additions & 8 deletions api/src/paths/user/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ POST.apiDoc = {
schema: {
title: 'User Response Object',
type: 'object',
required: ['userGuid', 'userIdentifier', 'identitySource', 'roleId'],
required: ['userIdentifier', 'identitySource', 'roleId'],
properties: {
userGuid: {
type: 'string',
Expand Down Expand Up @@ -97,16 +97,12 @@ export function addSystemRoleUser(): RequestHandler {
return async (req, res) => {
const connection = getDBConnection(req['keycloak_token']);

const userGuid = req.body?.userGuid || null;
const userIdentifier = req.body?.userIdentifier || null;
const identitySource = req.body?.identitySource || null;
const userGuid: string | null = req.body?.userGuid || null;
const userIdentifier: string | null = req.body?.userIdentifier || null;
const identitySource: string | null = req.body?.identitySource || null;

const roleId = req.body?.roleId || null;

if (!userGuid) {
throw new HTTP400('Missing required body param: userGuid');
}

if (!userIdentifier) {
throw new HTTP400('Missing required body param: userIdentifier');
}
Expand Down
Loading