Skip to content

Conversation

@leoraba
Copy link
Contributor

@leoraba leoraba commented Dec 10, 2025

Description

When application is approved, it should assign applicant and collaborators the corresponding permission to the specified study.

Details

  • Uses authz to assign an user to an specific study. (more info here)
  • 🐛 An issue reported in authz. Permissions are removed automatically after couple of minutes RESOLVED
  • Fetching the list of studies is not implemented yet. This PR uses a hardcoded list of studies for dev/testing purpose.
  • When an application is approved, the applicant and collaborators email addresses are used to retrieve their PCGL user IDs, which are then used to grant permissions for each study. Only users whose permissions are successfully granted are notified by email.
  • This PR does not include a retry mechanism for granting user permissions.
  • Added new env variables to configure retry fetch calls, default values:
    FETCH_RETRIES=3
    FETCH_RETRY_DELAY_MS=500
    FETCH_TIMEOUT_MS=10000

Ticket

#509

Comment on lines 51 to 54
{
studyName: 'TEST-CA',
studyID: 'TEST-CA',
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetching the list of studies available is not implemented yet.. hardcoding for dev/testing purpose


export type ApproveApplication = {
applicationId: number; // The ID of the application to be approved
approverAccessToken: string; // The access token of the user approving the application
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using the approver's access_token to add permissions in authz service. This requires that the DAC_CHAIR to be part of the CO:admin` group to allow this type of requests

<CheckCircleFilled style={{ color: token.colorPrimary, fontSize: 30 }} />
<Flex vertical>
<Text strong>{translate('dashboard.hasAccess')}</Text>
<Text>{formatDate(expiresAt)}</Text>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing Expires At date on the top status bar, as per ticket specification.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the check on line 63 confirm that the current date is before the expiresAt date?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a condition as suggested.

Comment on lines +35 to +36
# -- Approved Application --
APPROVED_PERMISSION_EXPIRES_IN_DAYS=365
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new env variable, updated readme file for more details

Copy link
Contributor

@joneubank joneubank left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work. We need a bit more to deal with error cases. What is missing is that if the request to add permissions fails, then we have no way remedy. We may need a manual process to correct individual failures, or some other mechanism to ensure the AuthZ state has all data from DACO.

}

const authConfigSchema = z.object({
APPROVED_PERMISSION_EXPIRES_IN_DAYS: z.coerce.number().int().optional().default(365),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

institutionalEmail,
approverAccessToken,
requestedStudies,
}: AssignUserPermissionsAndNotifyParams) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to declare the return type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added return type to this function.

Comment on lines 445 to 457
const permissionAdded = await assignUserPermissions({
institutionalEmail: applicant_institutional_email,
approverAccessToken,
requestedStudies: requested_studies,
});

if (permissionAdded) {
emailService.sendEmailApproval({
id: application.id,
to: applicant_institutional_email,
name: applicant_first_name || 'N/A',
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have some behaviour that handles the case where we fail to successfully assign user permissions.

We should at minimum have a retry mechanism for failed requests.

What I'm thinking about though is that we need a mechanism to notify an admin that the approval was completed but the permissions weren't granted, and then the admin needs a mechanism to correct the issue.

Copy link
Contributor Author

@leoraba leoraba Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To handle user permissions, included following changes:

  • A validation was added early in the code, to make sure applicant and collaborator's email address are valid PCGL users. This avoid approving an application with invalid users.
  • Implemented a retry mechanism on calls used to add user permission to Authz service
  • In case of an error occurs during approving the application. the DAC approver and the Applicant will be notified via email, UI displays a notification toast with a friendly error message.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A mechanism to handle retries will be implemented in a separate PR.

Comment on lines 28 to 40
/**
* This function assigns user permissions to the requested studies
* First it looks for the associated PCGL user ID using the institutional email
* Then it assigns the study permissions using the approver's access token
*
* @param param0
* @returns
*/
export const assignUserPermissions = async ({
institutionalEmail,
approverAccessToken,
requestedStudies,
}: AssignUserPermissionsAndNotifyParams) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function could fail at multiple stages, and the output from this function should correctly communicate where it failed and any work that was done.

  1. It could fail due to auth or network reasons not related to this specific request content
  2. It could fail because the user was not found (should not happen but let's handle this anyways).
  3. It could fail adding permission for some studies, but succeed for others.

If the request to add permissions fails for any studies, the response should include a list of studies failed and studies succeeded.

Copy link
Contributor Author

@leoraba leoraba Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retry Authz calls and Emails are sent when an error occur during approval.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Added a retry mechanism to the main function used for making requests to Authz service.
  • Once the retry limit is exceeded, errors are not handled in this PR, a separate PR will be required to implement proper error handling.

| `VALKEY_USER` | Valkey user name. | `string` | Required | |
| | | | | |
| `DISABLE_AUTH` | Set this to `true` to disable auth. Any other value and this will default to auth enabled. | `boolean` | Optional | `false` |
| `APPROVED_PERMISSION_EXPIRES_IN_DAYS` | Number of days the applicant will have **read** access to the study when application gets approved. | `number` | Optional | `365` days |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏

<CheckCircleFilled style={{ color: token.colorPrimary, fontSize: 30 }} />
<Flex vertical>
<Text strong>{translate('dashboard.hasAccess')}</Text>
<Text>{formatDate(expiresAt)}</Text>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the check on line 63 confirm that the current date is before the expiresAt date?

Comment on lines 417 to 421
const verifyUserAccountsResult = await verifyApplicationUserAccounts(applicationId, approverAccessToken);

if (!verifyUserAccountsResult.success) {
return verifyUserAccountsResult;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applicant and collaborators must be already registered as pcgl users.

message: translate('modals.approveApplication.notifications.applicationApproveFailed'),
});
onError: async (data, { applicationId }) => {
switch (data.error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User friendly errors to be displayed in the UI when application has errors during approval.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix a react warning. Adding key prop to each component in a list.

@leoraba leoraba requested a review from joneubank December 16, 2025 21:45
Copy link
Contributor

@joneubank joneubank left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You added a retry parameter to several functions but never specified the number of times they should be retried. Do you want to add an environment variable to the app to configure these, and also set a default retry at 2?


if (response.status === 404) {
const message = `No user found with email ${emailAddress}.`;
logger.info('[AUTHZ]:', `retry: ${retry}`, message);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the comment specify "Retries remaining:" instead of "retry:"? As written, the log seems to indicate how many times its already been attempted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed retry from this function, and updated the logs

export const lookupUserByEmail = async (
emailAddress: string,
accessToken: string,
retry = 0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional and simple retry mechanism. In the future, we should look for a way to make this reusable without having to implement all the retry logging and count tracking and final return cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactored the retry mechanism, now is handled in a single the function used to call authz, also included a generic fetchWithRetry.

Comment on lines +30 to +33
FETCH_RETRIES: z.coerce.number().optional().default(3),
FETCH_RETRY_DELAY_MS: z.coerce.number().optional().default(500),
FETCH_TIMEOUT_MS: z.coerce.number().optional().default(10000),

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new env. variables used to configure retry in fetch funcions

Comment on lines +122 to +127
// Retry mechanism for authz requires to refresh service token before retying fetch call
const { FETCH_RETRIES, FETCH_RETRY_DELAY_MS } = serverConfig;
let attempt = 0;
while (true) {
try {
const response = await _fetchFromAuthZ();
Copy link
Contributor Author

@leoraba leoraba Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

centralized the retry mechanism + refresh Token when calling Authz service.

* @param init
* @returns
*/
export async function fetchWithRetry(url: string, init: RequestInit): Promise<Response> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generic fetch wrapper with retry options

@leoraba leoraba requested a review from joneubank December 17, 2025 17:02
@leoraba leoraba marked this pull request as draft December 18, 2025 21:13
@leoraba leoraba force-pushed the feat/509-study-read-permission branch from 99e7e8a to 664198f Compare December 19, 2025 16:47
@leoraba
Copy link
Contributor Author

leoraba commented Dec 19, 2025

PR updated based on feedback received.

Note: A separate PR is required to handle permissions to unregistered users.

@leoraba leoraba marked this pull request as ready for review December 19, 2025 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants