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

TICDI-99 - Handle roles for IDIR - MFA logins #231

Merged
merged 1 commit into from
Aug 30, 2024
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
8 changes: 4 additions & 4 deletions backend/src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,12 @@ export class AdminController {
}

@Post('add-admin')
async addAdmin(@Body() searchInputs: { idirUsername: string }): Promise<{ userObject: UserObject; error: string }> {
async addAdmin(@Body() searchInputs: { idirUsername: string }): Promise<{ error: string }> {
try {
const user = await this.adminService.addAdmin(searchInputs.idirUsername);
return { userObject: user, error: null };
await this.adminService.addAdmin(searchInputs.idirUsername);
return { error: null };
} catch (err) {
return { userObject: null, error: err.message };
return { error: err.message };
}
}

Expand Down
237 changes: 177 additions & 60 deletions backend/src/admin/admin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
}> {
const url = `${process.env.users_api_base_url}/${process.env.css_environment}/idir/users?&email=${email}`;
const bearerToken = await this.getToken();

const searchData: SearchResultsItem[] = await axios
.get(url, {
headers: { Authorization: 'Bearer ' + bearerToken },
Expand Down Expand Up @@ -139,7 +140,8 @@
username: username,
idirUsername: idirUsername,
};
const roleUrl = `${process.env.users_api_base_url}/integrations/${process.env.integration_id}/${process.env.css_environment}/users/${idirUsername}@idir/roles`;
const cleanIdirUsername = idirUsername.split('@')[0].concat('@idir');
const roleUrl = `${process.env.users_api_base_url}/integrations/${process.env.integration_id}/${process.env.css_environment}/users/${cleanIdirUsername}/roles`;
const roles = await axios
.get(roleUrl, {
headers: { Authorization: 'Bearer ' + bearerToken },
Expand All @@ -165,66 +167,105 @@

/**
* Searches for an IDIR user with the given search params and if only one is found
* then gives them the ticdi_admin role
* then gives them the ticdi_admin & generate_documents roles in both IDIR and AzureIDIR
*
* @param firstName
* @param lastName
* @param email
* @returns user object to be displayed in the frontend
*/
async addAdmin(idirUsername: string): Promise<UserObject> {
const url = `${process.env.users_api_base_url}/${process.env.css_environment}/idir/users?&guid=${idirUsername}`;
async addAdmin(username: string): Promise<void> {
username = username.split('@')[0]; // remove any '@idir' or '@azureidir' from the username
const addAdminUrl = `${process.env.users_api_base_url}/integrations/${process.env.integration_id}/${process.env.css_environment}/user-role-mappings`;
const bearerToken = await this.getToken();
const searchData: SearchResultsItem[] = await axios
.get(url, {
headers: { Authorization: 'Bearer ' + bearerToken },
})
.then((res) => {
return res.data.data;
})
.catch((err) => console.log(err.response.data));
if (searchData.length > 1) {
throw new Error('More than one user was found');
} else if (searchData.length == 0) {
throw new Error('No users were found');

try {
const idirRolesUrl = `${process.env.users_api_base_url}/integrations/${process.env.integration_id}/${
process.env.css_environment
}/users/${username + '@idir'}/roles`;
const idirRoleData = await axios
.get(idirRolesUrl, { headers: { Authorization: 'Bearer ' + bearerToken } })
Dismissed Show dismissed Hide dismissed
.then((res) => {
return res.data.data;
})
.catch((err) => console.log(err.response.data));
if (idirRoleData.length > 0) {
const existingUserRoles = idirRoleData.map((item) => item.name).join(', ');
console.log(`User already has roles: ${existingUserRoles}`);
// throw new Error('User already has roles.');
}

await axios
.post(
addAdminUrl,
{
roleName: Role.TICDI_ADMIN,
username: username + '@idir',
operation: 'add',
},
{ headers: { Authorization: 'Bearer ' + bearerToken } }
)
.then((res) => {
return res.data;
})
.catch((err) => {
console.log(err);
throw new Error('Failed to add idir ticdi_admin role');
});
await axios
.post(
addAdminUrl,
{
roleName: Role.GENERATE_DOCUMENTS,
username: username + '@idir',
operation: 'add',
},
{ headers: { Authorization: 'Bearer ' + bearerToken } }
)
.then((res) => {
return res.data;
})
.catch((err) => {
console.log(err);
throw new Error('Failed to add idir generate_documents role');
});
await axios
.post(
addAdminUrl,
{
roleName: Role.TICDI_ADMIN,
username: username + '@azureidir',
operation: 'add',
},
{ headers: { Authorization: 'Bearer ' + bearerToken } }
)
.then((res) => {
return res.data;
})
.catch((err) => {
console.log(err);
throw new Error('Failed to add azureidir ticdi_admin role');
});
await axios
.post(
addAdminUrl,
{
roleName: Role.GENERATE_DOCUMENTS,
username: username + '@azureidir',
operation: 'add',
},
{ headers: { Authorization: 'Bearer ' + bearerToken } }
)
.then((res) => {
return res.data;
})
.catch((err) => {
console.log(err);
throw new Error('Failed to add azureidir generate_documents role');
});
} catch (err) {
console.log(err);
}
const userObject: UserObject = this.formatSearchData(searchData)[0];
await axios
.post(
addAdminUrl,
{
roleName: Role.TICDI_ADMIN,
username: userObject.idirUsername + '@idir',
operation: 'add',
},
{ headers: { Authorization: 'Bearer ' + bearerToken } }
)
.then((res) => {
return res.data;
})
.catch((err) => {
console.log(err);
throw new Error('Failed to add admin role');
});
await axios
.post(
addAdminUrl,
{
roleName: Role.GENERATE_DOCUMENTS,
username: userObject.idirUsername + '@idir',
operation: 'add',
},
{ headers: { Authorization: 'Bearer ' + bearerToken } }
)
.then((res) => {
return res.data;
})
.catch((err) => {
console.log(err);
throw new Error('Failed to add admin role');
});
return userObject;
}

/**
Expand All @@ -234,7 +275,7 @@
*/
async getAdminUsers(): Promise<UserObject[]> {
const bearerToken = await this.getToken();
const url = `${process.env.users_api_base_url}/integrations/${process.env.integration_id}/${process.env.css_environment}/roles/ticdi_admin/users`;
const url = `${process.env.users_api_base_url}/integrations/${process.env.integration_id}/${process.env.css_environment}/roles/${Role.TICDI_ADMIN}/users`;
const data: SearchResultsItem[] = await axios
.get(url, {
headers: { Authorization: 'Bearer ' + bearerToken },
Expand All @@ -249,7 +290,7 @@

async getExportData(): Promise<string> {
const bearerToken = await this.getToken();
const url = `${process.env.users_api_base_url}/integrations/${process.env.integration_id}/${process.env.css_environment}/roles/ticdi_admin/users`;
const url = `${process.env.users_api_base_url}/integrations/${process.env.integration_id}/${process.env.css_environment}/roles/${Role.TICDI_ADMIN}/users`;
const data: SearchResultsItem[] = await axios
.get(url, {
headers: { Authorization: 'Bearer ' + bearerToken },
Expand All @@ -264,20 +305,24 @@
}

/**
* Removes the ticdi_admin role from an IDIR user
* Removes the ticdi_admin & generate_documents role from an IDIR user
*
* @param username
* @returns null
*/
async removeAdmin(username: string): Promise<{ error: string | null }> {
const ticdiAdminRole = 'ticdi_admin';
const bearerToken = await this.getToken();
const url = `${process.env.users_api_base_url}/integrations/${process.env.integration_id}/${process.env.css_environment}/users/${username}@idir/roles/${ticdiAdminRole}`;
const idirUsername = username?.split('@')[0].concat('@idir');
const azureidirUsername = username?.split('@')[0].concat('@azureidir');
const idirAdminUrl = `${process.env.users_api_base_url}/integrations/${process.env.integration_id}/${process.env.css_environment}/users/${idirUsername}/roles/${Role.TICDI_ADMIN}`;
const idirGDUrl = `${process.env.users_api_base_url}/integrations/${process.env.integration_id}/${process.env.css_environment}/users/${idirUsername}/roles/${Role.GENERATE_DOCUMENTS}`;
const azureidirAdminUrl = `${process.env.users_api_base_url}/integrations/${process.env.integration_id}/${process.env.css_environment}/users/${azureidirUsername}/roles/${Role.TICDI_ADMIN}`;
const azureidirGDUrl = `${process.env.users_api_base_url}/integrations/${process.env.integration_id}/${process.env.css_environment}/users/${azureidirUsername}/roles/${Role.GENERATE_DOCUMENTS}`;
try {
await axios
.delete(url, {
.delete(idirAdminUrl, {
headers: { Authorization: 'Bearer ' + bearerToken },
})

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.
.then((res) => {
return res;
})
Expand All @@ -286,7 +331,68 @@
});
} catch (err) {
console.log(err.response.data);
return { error: 'Failed to remove admin privileges' };
if (err?.response?.data?.message?.includes('not associated')) {
// ignore error if user is not associated with the role
} else {
return { error: 'Failed to remove idir_admin role' };
}
}
try {
await axios
.delete(idirGDUrl, {
headers: { Authorization: 'Bearer ' + bearerToken },
})
Dismissed Show dismissed Hide dismissed
.then((res) => {
return res;
})
.catch((err) => {
throw err;
});
} catch (err) {
console.log(err.response.data);
if (err?.response?.data?.message?.includes('not associated')) {
// ignore error if user is not associated with the role
} else {
return { error: 'Failed to remove generate_documents role' };
}
}
try {
await axios
.delete(azureidirAdminUrl, {
headers: { Authorization: 'Bearer ' + bearerToken },
})
Dismissed Show dismissed Hide dismissed
.then((res) => {
return res;
})
.catch((err) => {
throw err;
});
} catch (err) {
console.log(err.response.data);
if (err?.response?.data?.message?.includes('not associated')) {
// ignore error if user is not associated with the role
} else {
return { error: 'Failed to remove idir_admin role' };
}
}
try {
await axios
.delete(azureidirGDUrl, {
headers: { Authorization: 'Bearer ' + bearerToken },
})
Dismissed Show dismissed Hide dismissed
.then((res) => {
return res;
})
.catch((err) => {
throw err;
});
} catch (err) {
console.log(err.response.data);
if (err?.response?.data?.message?.includes('not associated')) {
// ignore error if user is not associated with the role
} else {
return { error: 'Failed to remove generate_documents role' };
}
}
return { error: null };
}
Expand Down Expand Up @@ -351,12 +457,23 @@
*/
formatSearchData(data: SearchResultsItem[]): UserObject[] {
let userObjectArray = [];
let usernames = new Set();

for (let entry of data) {
const idirUsername = entry.username ? entry.username.split('@')[0] : '';

// if this username has already been added, skip this entry
if (usernames.has(idirUsername)) {
continue;
}

usernames.add(idirUsername);

const firstName = entry.firstName ? entry.firstName : '';
const lastName = entry.lastName ? entry.lastName : '';
const username = entry.attributes.idir_username[0] ? entry.attributes.idir_username[0] : '';
const email = entry.email ? entry.email : '';
const idirUsername = entry.username ? entry.username.replace('@idir', '') : '';

const userObject: UserObject = {
name: firstName + ' ' + lastName,
username: username,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/app/common/manage-admins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ export const findIdirUser = async (
return { foundUserObject: response.userObject || null, error: response.error || null };
};

export const addAdmin = async (idirUsername: string): Promise<{ userObject: UserObject; error: string }> => {
export const addAdmin = async (idirUsername: string): Promise<{ error: string }> => {
const url = `${config.API_BASE_URL}/admin/add-admin`;
const data = { idirUsername };
const postParameters = api.generateApiParameters(url, data);
const response: { userObject: UserObject; error: string } = await api.post(postParameters);
const response: { error: string } = await api.post(postParameters);
return response;
};

Expand Down
13 changes: 9 additions & 4 deletions frontend/src/app/components/modal/manage-admins/AddAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const AddAdmin: FC<AddAdminProps> = ({ show, onHide, refreshTable }) => {
</Form.Group>
<Form.Group as={Row} className="mb-3">
<Col sm={{ span: 10, offset: 2 }}>
<Button variant="success" onClick={searchUsers} disabled={loading}>
<Button variant="success" onClick={searchUsers} disabled={loading} style={{ minWidth: '70px' }}>
{loading ? (
<Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" />
) : (
Expand Down Expand Up @@ -146,11 +146,16 @@ const AddAdmin: FC<AddAdminProps> = ({ show, onHide, refreshTable }) => {
{showError && <div className="alert alert-danger">{error}</div>}
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={onHide}>
Cancel
<Button variant="secondary" onClick={onHide} style={{ minWidth: '70px' }}>
{loading ? <Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" /> : 'Cancel'}
</Button>

<Button variant="primary" onClick={addAdminHandler} disabled={loading || !userObject}>
<Button
variant="primary"
onClick={addAdminHandler}
disabled={loading || !userObject}
style={{ minWidth: '160px' }}
>
{loading ? (
<Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" />
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { AdminData } from '../../table/manage-admins/AdminDataTable';
import Modal from 'react-bootstrap/Modal';
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';
import { Col, Row } from 'react-bootstrap';
import { Col, Row, Spinner } from 'react-bootstrap';
import { removeAdmin } from '../../../common/manage-admins';

type RemoveAdminProps = {
Expand Down Expand Up @@ -75,10 +75,11 @@ const RemoveAdmin: FC<RemoveAdminProps> = ({ admin, show, onHide, refreshTable }
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={onHide} disabled={loading}>
No
{loading ? <Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" /> : 'No'}
</Button>

<Button variant="primary" onClick={() => removeHandler()} disabled={loading}>
Yes
{loading ? <Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" /> : 'Yes'}
</Button>
</Modal.Footer>
</Modal>
Expand Down
Loading