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

Validating BNR permission for consent revocation #66

Merged
merged 6 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static org.wso2.openbanking.cds.consent.extensions.common.CDSConsentExtensionConstants.AUTH_RESOURCE_TYPE_PRIMARY;
Expand Down Expand Up @@ -110,6 +112,11 @@ public void handleSearch(ConsentAdminData consentAdminData) throws ConsentExcept
throw new OpenBankingRuntimeException("Error while adding sharing dates to permissions", e);
}
}

if (consentAdminData.getQueryParams().containsKey(CDSConsentExtensionConstants.CONSENT_IDS_QUERY_PARAM_NAME)) {
addBNRRevokeStatus(consentAdminData);
}

}

@Override
Expand All @@ -124,6 +131,16 @@ public void handleRevoke(ConsentAdminData consentAdminData) throws ConsentExcept
final String userID = validateAndGetQueryParam(queryParams, USER_ID);
DetailedConsentResource detailedConsentResource = this.consentCoreService.getDetailedConsent(consentID);
if (detailedConsentResource != null) {
ArrayList<String> userIDs = (ArrayList<String>) consentAdminData.getQueryParams()
.get(CDSConsentExtensionConstants.USER_ID_KEY_NAME);
// userIDs can be null or empty when the request comes from a CustomerCareOfficer
if (userIDs != null && !userIDs.isEmpty()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

when can there be a list of userIDs? because usually revoke is done by a single user right? If there is a scenario when a list of userIDs can come shall we add that as a comment as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is because consentAdminData.getQueryParams() returns a Map, and and .get(<key>) returns an array list. That doesn't mean that we can have multiple userIDs for revoke. Since ConsentAdminData is a common class we cannot change that behaviour for revoke only.
For this scenario (revoking a consent from consent manager) we only have 1 user.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There was already a validateAndGetQueryParam method to get the first element from query params. Modified with bc8b123

String userId = userIDs.get(0);
if (!canRevokeByBNR(detailedConsentResource, userId)) {
throw new ConsentException(ResponseStatus.FORBIDDEN,
"User is not authorized to revoke the consent");
}
}
if (StringUtils.isNotBlank(userID) && !isPrimaryUserRevoking(detailedConsentResource, userID)) {
// Deactivate consent mappings as secondary consent holder
deactivateAccountMappings(detailedConsentResource, userID);
Expand Down Expand Up @@ -521,6 +538,91 @@ private boolean isActionByPrimaryUser(DetailedConsentResource detailedConsentRes
return false;
}

private void addBNRRevokeStatus(ConsentAdminData consentAdminData) {

JSONObject responsePayload = consentAdminData.getResponsePayload();
JSONArray consentData = (JSONArray) responsePayload.get(CDSConsentExtensionConstants.DATA);
ArrayList<String> userIDs = (ArrayList<String>) consentAdminData.getQueryParams()
.get(CDSConsentExtensionConstants.USER_IDS_QUERY_PARAM_NAME);
if (userIDs == null || userIDs.isEmpty()) {
// Customer care officer scenario
consentData.forEach(consent -> {
JSONObject consentObject = (JSONObject) consent;
consentObject.put(CDSConsentExtensionConstants.CAN_REVOKE, true);
});
} else {
String userId = userIDs.get(0);
consentData.forEach(consent -> {
JSONObject consentObject = (JSONObject) consent;
consentObject.put(CDSConsentExtensionConstants.CAN_REVOKE, canRevokeByBNR(consentAdminData, userId));
});
}
}

private boolean canRevokeByBNR(DetailedConsentResource detailedConsentResource, String userId) {

Set<String> activeAccountIDs = new HashSet<>();
for (ConsentMappingResource consentMappingResource : detailedConsentResource.getConsentMappingResources()) {
if (consentMappingResource.getMappingStatus().equals(CDSConsentExtensionConstants.ACTIVE_STATUS)) {
activeAccountIDs.add(consentMappingResource.getAccountID());
}
}

return canRevokeByBNR(activeAccountIDs, userId);
}

private boolean canRevokeByBNR(ConsentAdminData consentAdminData, String userId) {

Set<String> activeAccountIDs = new HashSet<>();

for (Object item : (JSONArray) consentAdminData.getResponsePayload().
get(CDSConsentExtensionConstants.DATA)) {
JSONObject itemJSONObject = (JSONObject) item;
JSONArray consentMappingResourcesArray = (JSONArray) itemJSONObject.
get(CDSConsentExtensionConstants.CONSENT_MAPPING_RESOURCES);
for (Object consentMappingResource : consentMappingResourcesArray) {
JSONObject consentMappingResourceObject = (JSONObject) consentMappingResource;
if (consentMappingResourceObject.getAsString(CDSConsentExtensionConstants.MAPPING_STATUS).
equals(CDSConsentExtensionConstants.ACTIVE_STATUS)) {
activeAccountIDs.add(consentMappingResourceObject.getAsString(
CDSConsentExtensionConstants.ACCOUNT_ID));
}
}
}
return canRevokeByBNR(activeAccountIDs, userId);
}

private boolean canRevokeByBNR(Set<String> activeAccountIDs, String userId) {

if (activeAccountIDs.isEmpty()) {
return true;
}

AccountMetadataServiceImpl accountMetadataService = AccountMetadataServiceImpl.getInstance();

/*
* For all the active accounts, if the user has AUTHORIZE or REVOKE permission for BNR, then the user can
Copy link
Contributor

Choose a reason for hiding this comment

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

User can revoke the consent only if he has authorize permission.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed with e7670fa

* revoke that consent. (i.e. For any given active account in consent mappings, if the user does not have
* AUTHORIZE or REVOKE permission, he/she cannot revoke the consent)
*/
for (String accountID : activeAccountIDs) {
try {
String permissionStatus = accountMetadataService.getAccountMetadataByKey(accountID, userId,
CDSConsentExtensionConstants.BNR_PERMISSION);
if (StringUtils.isNotBlank(permissionStatus) &&
!(CDSConsentExtensionConstants.BNR_AUTHORIZE_PERMISSION.equals(permissionStatus))) {

return false;
}
} catch (OpenBankingException e) {
log.error(e.getMessage());
throw new OpenBankingRuntimeException("Error while getting BNR permissions.", e);
}
}

return true;
}

public void updateDOMSStatusForConsentData(ConsentAdminData consentAdminData) {
try {
AccountMetadataService accountMetadataService = AccountMetadataServiceImpl.getInstance();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ public class CDSConsentExtensionConstants {
public static final String CONSENT_EXPIRY = "consent_expiration";
public static final String ACCOUNT_MASKING_ENABLED = "account_masking_enabled";
public static final String USER_ID_KEY_NAME = "userID";
public static final String USER_IDS_QUERY_PARAM_NAME = "userIDs";
public static final String CONSENT_IDS_QUERY_PARAM_NAME = "consentIDs";
public static final String CONSENT_STATUS_REVOKED = "revoked";
public static final String CONSENT_ATTRIBUTES = "consentAttributes";
public static final String INCLUDE_SHARING_DATES = "includeSharingDates";
Expand Down Expand Up @@ -141,6 +143,7 @@ public class CDSConsentExtensionConstants {
public static final String BNR_AUTHORIZE_PERMISSION = "AUTHORIZE";
public static final String BNR_VIEW_PERMISSION = "VIEW";
public static final String BNR_REVOKE_PERMISSION = "REVOKE";
public static final String CAN_REVOKE = "can_revoke";

//Multi Profile Constants
public static final String INDIVIDUAL_PROFILE_TYPE = "Individual";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,19 +116,19 @@ public void setUp() throws ConsentManagementException {
mapping1.setMappingID(MAPPING_ID_1);
mapping1.setAccountID(JOINT_ACCOUNT_ID);
mapping1.setAuthorizationID(AUTH_ID_PRIMARY);
mapping1.setMappingStatus("true");
mapping1.setMappingStatus("active");

ConsentMappingResource mapping2 = new ConsentMappingResource();
mapping2.setMappingID(MAPPING_ID_2);
mapping2.setAccountID("test-regular-account-id");
mapping2.setAuthorizationID(AUTH_ID_PRIMARY);
mapping2.setMappingStatus("true");
mapping2.setMappingStatus("active");

ConsentMappingResource mapping3 = new ConsentMappingResource();
mapping3.setMappingID(MAPPING_ID_3);
mapping3.setAccountID(JOINT_ACCOUNT_ID);
mapping3.setAuthorizationID(AUTH_ID_SECONDARY);
mapping3.setMappingStatus("true");
mapping3.setMappingStatus("active");

detailedConsentResource = new DetailedConsentResource();
detailedConsentResource.setAuthorizationResources(new ArrayList<>(Arrays.asList(authResource1, authResource2)));
Expand Down Expand Up @@ -164,6 +164,10 @@ public void testHandleRevoke() throws ConsentManagementException {
ConsentAdminData consentAdminDataMock = mock(ConsentAdminData.class);
when(consentAdminDataMock.getQueryParams()).thenReturn(queryParams);

this.accountMetadataServiceMock = mock(AccountMetadataServiceImpl.class);
mockStatic(AccountMetadataServiceImpl.class);
when(AccountMetadataServiceImpl.getInstance()).thenReturn(accountMetadataServiceMock);

uut.handleRevoke(consentAdminDataMock);

ArgumentCaptor<ArrayList> argumentCaptor = ArgumentCaptor.forClass(ArrayList.class);
Expand All @@ -185,6 +189,10 @@ public void testHandleRevokeForPrimaryUser() {
ConsentAdminData consentAdminDataMock = mock(ConsentAdminData.class);
when(consentAdminDataMock.getQueryParams()).thenReturn(queryParams);

this.accountMetadataServiceMock = mock(AccountMetadataServiceImpl.class);
mockStatic(AccountMetadataServiceImpl.class);
when(AccountMetadataServiceImpl.getInstance()).thenReturn(accountMetadataServiceMock);

uut.handleRevoke(consentAdminDataMock);

verify(consentAdminDataMock).setResponseStatus(ResponseStatus.NO_CONTENT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const ProfileMain = ({consent, infoLabel, appicationName, logoURL}) => {
</a>
</div>
{consent.currentStatus.toLowerCase() ===
specConfigurations.status.authorised.toLowerCase() && isNotExpired() ? (
specConfigurations.status.authorised.toLowerCase() && isNotExpired() && consent.can_revoke ? (
<div className="actionButtons">
<div className="actionBtnDiv">
<Link
Expand Down
Loading