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

TechDebt: System User Enhancements/Fixes #1316

Merged
merged 20 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ae58f54
Copy existing database functions to procedures folder, where they can…
NickPhura Jun 26, 2024
b7961f4
Fix procedures not having 'seed' function
NickPhura Jun 27, 2024
a4c12b6
migration to fix duplicate data
mauberti-bc Jun 27, 2024
6ce3528
Merge branch 'npTechDebt-System-User' of github.com:bcgov/biohubbc in…
mauberti-bc Jun 27, 2024
b1a86a3
fix typo by removing lower from constraint
mauberti-bc Jun 27, 2024
3f0aa2c
ensure system user record includes guid
mauberti-bc Jun 28, 2024
94fef77
Merge branch 'dev' into npTechDebt-System-User
mauberti-bc Jun 28, 2024
1d11298
Merge branch 'dev' into npTechDebt-System-User
mauberti-bc Jul 12, 2024
27737be
Merge branch 'dev' into npTechDebt-System-User
NickPhura Jul 18, 2024
a646855
Remove some unused references to the codes context.
NickPhura Jul 20, 2024
a6bebae
Merge branch 'dev' into npTechDebt-System-User
NickPhura Jul 22, 2024
ade20f8
Rename migration file. ignore-skip.
NickPhura Jul 22, 2024
fc9b927
Merge branch 'dev' into npTechDebt-System-User
NickPhura Jul 23, 2024
dc0af23
Rename migration
NickPhura Jul 23, 2024
6652af7
Merge branch 'npTechDebt-System-User' of https://github.com/bcgov/bio…
NickPhura Jul 23, 2024
a7e03d0
Duplicate system user sql now also patches their user record details.
NickPhura Jul 23, 2024
1d9c6e7
Update authorization to reject admins with end-dated records
NickPhura Jul 23, 2024
95a12b1
fix sql syntax typo
mauberti-bc Jul 23, 2024
9a22d09
Update query to handle project/survey participation edge cases
NickPhura Jul 24, 2024
c0e03f3
Merge branch 'dev' into npTechDebt-System-User
NickPhura Jul 24, 2024
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
11 changes: 10 additions & 1 deletion api/src/paths/administrative-activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,16 @@ POST.apiDoc = {
schema: {
title: 'Administrative Activity request object',
type: 'object',
properties: {}
additionalProperties: false,
properties: {
reason: { type: 'string', description: 'Reason for the access request.' },
userGuid: { type: 'string', description: 'Unique keycloak GUID for the user.' },
name: { type: 'string' },
username: { type: 'string' },
email: { type: 'string' },
identitySource: { type: 'string', description: 'Whether the account is an IDIR or BCeID.' },
displayName: { type: 'string' }
}
}
}
}
Expand Down
12 changes: 9 additions & 3 deletions api/src/paths/user/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { SystemUser } from '../../repositories/user-repository';
import { UserService } from '../../services/user-service';
import * as keycloakUtils from '../../utils/keycloak-utils';
import { getMockDBConnection, getRequestHandlerMocks } from '../../__mocks__/db';
import * as user from './add';
import { addSystemRoleUser } from './add';

chai.use(sinonChai);

Expand Down Expand Up @@ -52,14 +52,17 @@ describe('user', () => {
agency: null
};

const getUserByIdentifierStub = sinon.stub(UserService.prototype, 'getUserByIdentifier').resolves();

const ensureSystemUserStub = sinon.stub(UserService.prototype, 'ensureSystemUser').resolves(mockUserObject);

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

const requestHandler = user.addSystemRoleUser();
const requestHandler = addSystemRoleUser();

await requestHandler(mockReq, mockRes, mockNext);

expect(getUserByIdentifierStub).to.have.been.calledOnce;
expect(ensureSystemUserStub).to.have.been.calledOnce;
expect(adduserSystemRolesStub).to.have.been.calledOnce;
});
Expand Down Expand Up @@ -97,13 +100,16 @@ describe('user', () => {
agency: null
};

const getUserByIdentifierStub = sinon.stub(UserService.prototype, 'getUserByIdentifier').resolves();

const ensureSystemUserStub = sinon.stub(UserService.prototype, 'ensureSystemUser').resolves(mockUserObject);

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

const requestHandler = user.addSystemRoleUser();
const requestHandler = addSystemRoleUser();

await requestHandler(mockReq, mockRes, mockNext);
expect(getUserByIdentifierStub).to.have.been.calledOnce;
expect(ensureSystemUserStub).to.have.been.calledOnce;
expect(adduserSystemRolesStub).to.have.been.calledOnce;
});
Expand Down
14 changes: 13 additions & 1 deletion api/src/paths/user/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Operation } from 'express-openapi';
import { SOURCE_SYSTEM, SYSTEM_IDENTITY_SOURCE } from '../../constants/database';
import { SYSTEM_ROLE } from '../../constants/roles';
import { getDBConnection, getServiceClientDBConnection } from '../../database/db';
import { HTTP409 } from '../../errors/http-error';
import { authorizeRequestHandler } from '../../request-handlers/security/authorization';
import { UserService } from '../../services/user-service';
import { getKeycloakSource } from '../../utils/keycloak-utils';
Expand Down Expand Up @@ -105,6 +106,9 @@ POST.apiDoc = {
403: {
$ref: '#/components/responses/403'
},
409: {
$ref: '#/components/responses/409'
},
500: {
$ref: '#/components/responses/500'
},
Expand Down Expand Up @@ -144,6 +148,14 @@ export function addSystemRoleUser(): RequestHandler {

const userService = new UserService(connection);

// If user already exists, do nothing and return early
const user = await userService.getUserByIdentifier(userIdentifier, identitySource);

if (user) {
throw new HTTP409('Failed to add user. User with matching identifier already exists.');
}

// This function also verifies that the user exists, but we explicitly check above to return a useful error message.
const userObject = await userService.ensureSystemUser(
userGuid,
userIdentifier,
Expand All @@ -166,7 +178,7 @@ export function addSystemRoleUser(): RequestHandler {

return res.status(200).send();
} catch (error) {
defaultLog.error({ label: 'getUser', message: 'error', error });
defaultLog.error({ label: 'addSystemRoleUser', message: 'error', error });
await connection.rollback();
throw error;
} finally {
Expand Down
25 changes: 22 additions & 3 deletions api/src/services/authorization-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,23 @@ describe('AuthorizationService', () => {
expect(isAuthorizedByServiceClient).to.equal(false);
});

it('returns false if `record_end_date` is null', async function () {
const mockDBConnection = getMockDBConnection();

const mockGetSystemUsersObjectResponse = {
record_end_date: '2021-01-01',
role_names: [SYSTEM_ROLE.SYSTEM_ADMIN]
} as unknown as SystemUser;

sinon.stub(AuthorizationService.prototype, 'getSystemUserObject').resolves(mockGetSystemUsersObjectResponse);

const authorizationService = new AuthorizationService(mockDBConnection);

const isAuthorizedByServiceClient = await authorizationService.authorizeSystemAdministrator();

expect(isAuthorizedByServiceClient).to.equal(false);
});

it('returns true if `systemUserObject` is not null and includes admin role', async function () {
const mockDBConnection = getMockDBConnection();

Expand Down Expand Up @@ -194,7 +211,7 @@ describe('AuthorizationService', () => {
};
const mockDBConnection = getMockDBConnection();

const mockGetSystemUsersObjectResponse = { record_end_date: 'datetime' } as unknown as SystemUser;
const mockGetSystemUsersObjectResponse = { record_end_date: '2021-01-01' } as unknown as SystemUser;
sinon.stub(AuthorizationService.prototype, 'getSystemUserObject').resolves(mockGetSystemUsersObjectResponse);

const authorizationService = new AuthorizationService(mockDBConnection);
Expand Down Expand Up @@ -415,7 +432,8 @@ describe('AuthorizationService', () => {
};
const mockDBConnection = getMockDBConnection();

const mockGetSystemUsersObjectResponse = { record_end_date: 'datetime' } as unknown as ProjectUser & SystemUser;
const mockGetSystemUsersObjectResponse = { record_end_date: '2021-01-01' } as unknown as ProjectUser &
SystemUser;
sinon
.stub(AuthorizationService.prototype, 'getProjectUserObjectByProjectId')
.resolves(mockGetSystemUsersObjectResponse);
Expand Down Expand Up @@ -538,7 +556,8 @@ describe('AuthorizationService', () => {
};
const mockDBConnection = getMockDBConnection();

const mockGetSystemUsersObjectResponse = { record_end_date: 'datetime' } as unknown as ProjectUser & SystemUser;
const mockGetSystemUsersObjectResponse = { record_end_date: '2021-01-01' } as unknown as ProjectUser &
SystemUser;
sinon
.stub(AuthorizationService.prototype, 'getProjectUserObjectByProjectId')
.resolves(mockGetSystemUsersObjectResponse);
Expand Down
5 changes: 5 additions & 0 deletions api/src/services/authorization-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ export class AuthorizationService extends DBService {
// Cache the _systemUser for future use, if needed
this._systemUser = systemUserObject;

if (systemUserObject.record_end_date) {
// system user has an expired record
return false;
}

return systemUserObject.role_names.includes(SYSTEM_ROLE.SYSTEM_ADMIN);
}

Expand Down
7 changes: 1 addition & 6 deletions app/src/features/admin/users/AccessRequestList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ describe('AccessRequestList', () => {
username: 'testusername',
userGuid: 'aaaa',
email: 'email@email.com',
role: 2,
identitySource: SYSTEM_IDENTITY_SOURCE.IDIR,
displayName: 'test user',
company: 'test company',
Expand Down Expand Up @@ -110,7 +109,6 @@ describe('AccessRequestList', () => {
username: 'testusername',
userGuid: 'aaaa',
email: 'email@email.com',
role: 2,
identitySource: SYSTEM_IDENTITY_SOURCE.IDIR,
displayName: 'test user',
company: 'test company',
Expand Down Expand Up @@ -147,7 +145,6 @@ describe('AccessRequestList', () => {
username: 'testusername',
userGuid: 'aaaa',
email: 'email@email.com',
role: 2,
identitySource: SYSTEM_IDENTITY_SOURCE.IDIR,
displayName: 'test user',
company: 'test company',
Expand Down Expand Up @@ -211,7 +208,6 @@ describe('AccessRequestList', () => {
username: 'testusername',
userGuid: 'aaaa',
email: 'email@email.com',
role: 2,
identitySource: SYSTEM_IDENTITY_SOURCE.IDIR,
displayName: 'test user',
company: 'test company',
Expand Down Expand Up @@ -240,7 +236,7 @@ describe('AccessRequestList', () => {
displayName: 'test user',
email: 'email@email.com',
identitySource: 'IDIR',
roleIds: [2],
roleIds: [],
userGuid: 'aaaa',
userIdentifier: 'testusername'
});
Expand All @@ -265,7 +261,6 @@ describe('AccessRequestList', () => {
username: 'testusername',
userGuid: 'aaaa',
email: 'email@email.com',
role: 1,
identitySource: SYSTEM_IDENTITY_SOURCE.IDIR,
displayName: 'test user',
company: 'test company',
Expand Down
12 changes: 2 additions & 10 deletions app/src/features/admin/users/AccessRequestList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ import { AdministrativeActivityStatusType } from 'constants/misc';
import { DialogContext } from 'contexts/dialogContext';
import { APIError } from 'hooks/api/useAxios';
import { useBiohubApi } from 'hooks/useBioHubApi';
import {
IAccessRequestDataObject,
IGetAccessRequestsListResponse,
IIDIRAccessRequestDataObject
} from 'interfaces/useAdminApi.interface';
import { IGetAccessRequestsListResponse } from 'interfaces/useAdminApi.interface';
import { IGetAllCodeSetsResponse } from 'interfaces/useCodesApi.interface';
import { useContext, useState } from 'react';
import { getFormattedDate } from 'utils/Utils';
Expand Down Expand Up @@ -245,11 +241,7 @@ const AccessRequestList = (props: IAccessRequestListProps) => {
onDeny={handleReviewDialogDeny}
onApprove={handleReviewDialogApprove}
component={{
initialValues: {
...ReviewAccessRequestFormInitialValues,
system_role:
(activeReview?.data as (IAccessRequestDataObject & IIDIRAccessRequestDataObject) | undefined)?.role ?? ''
},
initialValues: ReviewAccessRequestFormInitialValues,
validationSchema: ReviewAccessRequestFormYupSchema,
element: activeReview ? (
<ReviewAccessRequestForm
Expand Down
41 changes: 28 additions & 13 deletions app/src/features/admin/users/ActiveUsersList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -303,19 +303,34 @@ const ActiveUsersList = (props: IActiveUsersListProps) => {
});
} catch (error) {
const apiError = error as APIError;
dialogContext.setErrorDialog({
open: true,
dialogTitle: AddSystemUserI18N.addUserErrorTitle,
dialogText: AddSystemUserI18N.addUserErrorText,
dialogError: apiError.message,
dialogErrorDetails: apiError.errors,
onClose: () => {
dialogContext.setErrorDialog({ open: false });
},
onOk: () => {
dialogContext.setErrorDialog({ open: false });
}
});

if (apiError.status === 409) {
dialogContext.setErrorDialog({
open: true,
dialogTitle: 'Failed to create users',
dialogText: 'One of the users you added already exists.',
onClose: () => {
dialogContext.setErrorDialog({ open: false });
},
onOk: () => {
dialogContext.setErrorDialog({ open: false });
}
});
} else {
dialogContext.setErrorDialog({
open: true,
dialogTitle: AddSystemUserI18N.addUserErrorTitle,
dialogText: AddSystemUserI18N.addUserErrorText,
dialogError: apiError.message,
dialogErrorDetails: apiError.errors,
onClose: () => {
dialogContext.setErrorDialog({ open: false });
},
onOk: () => {
dialogContext.setErrorDialog({ open: false });
}
});
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ describe('ReviewAccessRequestForm', () => {
email: 'test data email',
identitySource: SYSTEM_IDENTITY_SOURCE.IDIR,
displayName: 'test user',
role: 2,
reason: 'reason for request'
}
};
Expand Down
1 change: 0 additions & 1 deletion app/src/interfaces/useAdminApi.interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export type IIDIRAccessRequestDataObject = {
role: number;
reason: string;
};

Expand Down
24 changes: 0 additions & 24 deletions app/src/pages/access/AccessRequestPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ jest.mock('../../hooks/useBioHubApi');
const mockBiohubApi = useBiohubApi as jest.Mock;

const mockUseApi = {
codes: {
getAllCodeSets: jest.fn<Promise<object>, []>()
},
admin: {
createAdministrativeActivity: jest.fn()
}
Expand All @@ -42,7 +39,6 @@ const renderContainer = () => {
describe('AccessRequestPage', () => {
beforeEach(() => {
mockBiohubApi.mockImplementation(() => mockUseApi);
mockUseApi.codes.getAllCodeSets.mockClear();
mockUseApi.admin.createAdministrativeActivity.mockClear();
});

Expand All @@ -54,10 +50,6 @@ describe('AccessRequestPage', () => {
const history = createMemoryHistory();

it('should call the auth signoutRedirect function', async () => {
mockUseApi.codes.getAllCodeSets.mockResolvedValue({
system_roles: [{ id: 1, name: 'Creator' }]
});

const signoutRedirectStub = jest.fn();

const authState = getMockAuthState({
Expand Down Expand Up @@ -90,10 +82,6 @@ describe('AccessRequestPage', () => {
});

it.skip('processes a successful request submission', async () => {
mockUseApi.codes.getAllCodeSets.mockResolvedValue({
system_roles: [{ id: 1, name: 'Creator' }]
});

mockUseApi.admin.createAdministrativeActivity.mockResolvedValue({
id: 1
});
Expand All @@ -118,10 +106,6 @@ describe('AccessRequestPage', () => {
});

it.skip('takes the user to the request-submitted page immediately if they already have an access request', async () => {
mockUseApi.codes.getAllCodeSets.mockResolvedValue({
system_roles: [{ id: 1, name: 'Creator' }]
});

const authState = getMockAuthState({ base: SystemAdminAuthState });

render(
Expand All @@ -140,10 +124,6 @@ describe('AccessRequestPage', () => {
});

it.skip('shows error dialog with api error message when submission fails', async () => {
mockUseApi.codes.getAllCodeSets.mockResolvedValue({
system_roles: [{ id: 1, name: 'Creator' }]
});

mockUseApi.admin.createAdministrativeActivity = jest.fn(() => Promise.reject(new Error('API Error is Here')));

const { getByText, getAllByRole, getByRole, queryByText } = renderContainer();
Expand Down Expand Up @@ -172,10 +152,6 @@ describe('AccessRequestPage', () => {
});

it.skip('shows error dialog with default error message when response from createAdministrativeActivity is invalid', async () => {
mockUseApi.codes.getAllCodeSets.mockResolvedValue({
system_roles: [{ id: 1, name: 'Creator' }]
});

mockUseApi.admin.createAdministrativeActivity.mockResolvedValue({
id: null
});
Expand Down
Loading