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

Feature/175 #177

Merged
merged 4 commits into from
Feb 24, 2025
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
67 changes: 56 additions & 11 deletions force-app/main/default/classes/GGW_ApplicationCtrl.cls
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,17 @@ public with sharing class GGW_ApplicationCtrl {
// Delete logo file from grant
@AuraEnabled
public static String deleteLogo(String recordId){
//Check Field Level Security
Boolean canAccessGrantApp = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Grant_Application__c', new List<String>{'DistributionPublicUrl__c','Logo_Download_Url__c','Include_Logo__c' }, 'Upsert');

GGW_Grant_Application__c app = new GGW_Grant_Application__c();
app.Id = recordId;
app.DistributionPublicUrl__c = null; // Logo public URL
app.Logo_Download_Url__c = null; // Logo display URL
app.Include_Logo__c = false;
// Check object CRUD
if(Schema.sObjectType.GGW_Grant_Application__c.isUpdateable()){
if(Schema.sObjectType.GGW_Grant_Application__c.isUpdateable() && canAccessGrantApp){
update app;
}
// Find and delete content file and related records
Expand All @@ -45,10 +49,15 @@ public with sharing class GGW_ApplicationCtrl {
// Include or exclude logo image into grant application
@AuraEnabled
public static String includeLogo(String recordId, Boolean state){
//Check Field Level Security
Boolean canAccessGrantApp2 = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Grant_Application__c', new List<String>{'Include_Logo__c' }, 'Upsert');


GGW_Grant_Application__c app = new GGW_Grant_Application__c();
app.Id = recordId;
app.Include_Logo__c = state;
if(Schema.sObjectType.GGW_Grant_Application__c.isUpdateable()){
if(Schema.sObjectType.GGW_Grant_Application__c.isUpdateable() && canAccessGrantApp2){
update app;
}
return 'Application logo updated';
Expand Down Expand Up @@ -85,13 +94,17 @@ public with sharing class GGW_ApplicationCtrl {
*/
@AuraEnabled
public static void saveSelectedSectionText(String itemid, String blockid){

Boolean canAccessSelectedItem = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Selected_Item__c', new List<String>{'Selected_Block__c','Text_Block__c' }, 'Upsert');

GGW_Content_Block__c cBlock = GGW_ContentBlockSelector.queryContentBlockById(blockid);
// Construct selected Item to update
GGW_Selected_Item__c item = new GGW_Selected_Item__c();
item.Id = itemid;
item.Selected_Block__c = blockid;
item.Text_Block__c = cBlock.Description__c; // Copy rich text from block to item for edits
if(Schema.sObjectType.GGW_Selected_Item__c.isUpdateable()){
if(Schema.sObjectType.GGW_Selected_Item__c.isUpdateable() && canAccessSelectedItem){
update item;
}
}
Expand All @@ -103,25 +116,32 @@ public with sharing class GGW_ApplicationCtrl {
*/
@AuraEnabled
public static GGW_SectionWrapper createNewSection(String name){
Boolean canAccessSection= GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Section__c', new List<String>{'Name', 'Recommended__c', 'Suggested__c', 'Language__c', 'Sort_Order__c' }, 'Upsert');

GGW_Section__c maxOrder = GGW_SectionSelector.findMaxOrderSection();
GGW_Section__c s = new GGW_Section__c();
s.Name = name;
s.Recommended__c = true;
s.Suggested__c = true;
s.Language__c = GGW_Util.getGrantLanguage();
s.Sort_Order__c = getSectionSortOrder(maxOrder);
if(Schema.sObjectType.GGW_Section__c.isCreateable()){
if(Schema.sObjectType.GGW_Section__c.isCreateable() && canAccessSection){
insert s;
}
return new GGW_SectionWrapper(s);
}
// Edit rich text inside item method called from Section component when edit rich text
@AuraEnabled
public static void updateSelectedItemText(String itemid, String richtext){
//Check FLS
Boolean canAccessSelectedItem2 = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Selected_Item__c', new List<String>{'Text_Block__c'}, 'Upsert');

GGW_Selected_Item__c item = new GGW_Selected_Item__c();
item.Id = itemid;
item.Text_Block__c = richtext; // Update rich text from block to item on edit button click
if(Schema.sObjectType.GGW_Selected_Item__c.isUpdateable()){
if(Schema.sObjectType.GGW_Selected_Item__c.isUpdateable() && canAccessSelectedItem2){
update item;
}
}
Expand All @@ -138,6 +158,10 @@ public with sharing class GGW_ApplicationCtrl {
@AuraEnabled
public static void reorderSections(List<String> sectionList, String appId){
List<GGW_Selected_Item__c> updateOrderList = new List<GGW_Selected_Item__c>();
//Check FLS
Boolean canAccessSelectedItem3 = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Selected_Item__c', new List<String>{'GGW_Section__c','Grant_Application__c','Sort_Order__c'}, 'Upsert');

// Clean up items for reorder, delete items that are NOT on this list
cleanSelectedSections(sectionList, appId);
Integer cnt = 1;
Expand All @@ -160,7 +184,7 @@ public with sharing class GGW_ApplicationCtrl {
cnt++;
}
if(Schema.sObjectType.GGW_Selected_Item__c.isUpdateable() ||
Schema.sObjectType.GGW_Selected_Item__c.isCreateable()){
Schema.sObjectType.GGW_Selected_Item__c.isCreateable() && canAccessSelectedItem3){
upsert updateOrderList; // Some records here exist some may be new added sections
}
}
Expand Down Expand Up @@ -206,11 +230,15 @@ public with sharing class GGW_ApplicationCtrl {
*/
@AuraEnabled
public static String addTextBlockToLibrary(String sectionid, String richtext, String name){
//Check FLS
Boolean canAccessContentBlock = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Content_Block__c', new List<String>{'name','Section__c','Description__c'}, 'Upsert');

GGW_Content_Block__c cb = new GGW_Content_Block__c();
cb.name = getValidBlockName(name); // strange error Layout Field:Name must not be Readonly
cb.Section__c = sectionid;
cb.Description__c = richtext;
if(Schema.sObjectType.GGW_Content_Block__c.isCreateable()){
if(Schema.sObjectType.GGW_Content_Block__c.isCreateable() && canAccessContentBlock){
insert cb;
}
return cb.Id+'';
Expand Down Expand Up @@ -354,25 +382,34 @@ public with sharing class GGW_ApplicationCtrl {
return null;
}
private static void updateGrantAppLogoURL(String grantId, ContentDistribution cdURL){
//Check FLS
Boolean canAccessGrantApp3 = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Grant_Application__c', new List<String>{'DistributionPublicUrl__c','Logo_Download_Url__c'}, 'Upsert');

// Update Grant with new logo
if(cdURL.ContentDownloadUrl != null){
GGW_Grant_Application__c app = new GGW_Grant_Application__c();
app.Id = grantId;
app.DistributionPublicUrl__c = cdURL.DistributionPublicUrl; // Logo public URL
app.Logo_Download_Url__c = cdURL.ContentDownloadUrl; // Logo display URL
// Check object CRUD
if(Schema.sObjectType.GGW_Grant_Application__c.isUpdateable()){
if(Schema.sObjectType.GGW_Grant_Application__c.isUpdateable() ){
update app;
}
}
}
private static ContentDistribution insertContentDistribution(ContentVersion file){
//Check FLS

Boolean canAccessCD = GGW_PermissionValidator.getInstance()
.hasStandFLSAccessForFields('ContentDistribution', new List<String>{'Name','ContentVersionId', 'PreferencesAllowViewInBrowser'}, 'insert');

ContentDistribution cdr = new ContentDistribution(
Name = file.Title,
ContentVersionId = file.Id,
PreferencesAllowViewInBrowser = true );
// Check object CRUD
if(Schema.sObjectType.ContentDistribution.isCreateable()){
if(Schema.sObjectType.ContentDistribution.isCreateable() ){
insert cdr;
}
return cdr;
Expand Down Expand Up @@ -471,18 +508,26 @@ public with sharing class GGW_ApplicationCtrl {
}

private static GGW_Grant_Application__c insertGrantRecord(String name){
//Check FLS
Boolean canAccessGrantApp4 = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Grant_Application__c', new List<String>{'Name','Status__c','Language__c'}, 'Upsert');

GGW_Grant_Application__c gapp = new GGW_Grant_Application__c();
if (GGW_Util.isValidString(name)){
gapp.Name = name;
gapp.Status__c = 'In Progress';
gapp.Language__c = GGW_Util.getGrantLanguage();
if(Schema.sObjectType.GGW_Grant_Application__c.isCreateable()){
if(Schema.sObjectType.GGW_Grant_Application__c.isCreateable() && canAccessGrantApp4){
insert gapp;
}
}
return gapp;
}
private static void insertSelectedItemsForGrant(Id appId, List<String> sections){
//Check FLS
Boolean canAccessSI2 = GGW_PermissionValidator.getInstance()
.hasFLSAccessForFields('GGW_Selected_Item__c', new List<String>{'GGW_Section__c','Grant_Application__c','Sort_Order__c'}, 'Upsert');

// Add selected sections itterate over selected section IDs param
List<GGW_Selected_Item__c> selectedItems = new List<GGW_Selected_Item__c>();
Integer itemSortOrder = 1;
Expand All @@ -494,7 +539,7 @@ public with sharing class GGW_ApplicationCtrl {
selectedItems.add(item);
itemSortOrder++; // increment sort order set as default
}
if(Schema.sObjectType.GGW_Selected_Item__c.isCreateable()){
if(Schema.sObjectType.GGW_Selected_Item__c.isCreateable() && canAccessSI2){
insert selectedItems;
}
}
Expand Down
5 changes: 4 additions & 1 deletion force-app/main/default/classes/GGW_ApplicationCtrlTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ public class GGW_ApplicationCtrlTest {
}
}
GGW_Grant_Application__c app = GGW_ApplicationCtrl.newGrant('Content Distribution Test', sections);

System.debug('Grant App: '+ app.Id);

ContentVersion cvo = new Contentversion();
cvo.Title = 'Test Content file';
cvo.PathOnClient = 'test';
Expand All @@ -186,7 +189,7 @@ public class GGW_ApplicationCtrlTest {
String downloadURL = GGW_ApplicationCtrl.createContentDistribution(app.Id, cvo.Id);
Test.stopTest();

System.assertNotEquals(null, downloadURL, 'Contenet distribution is invalid');
System.assertNotEquals(null, downloadURL, 'Content distribution is invalid');
GGW_GrantApplicationWrapper grant = GGW_ApplicationCtrl.getApplication(app.Id);
System.assertEquals(downloadURL, grant.logodisplayurl, 'Logo download URL is not valid');
}
Expand Down
164 changes: 164 additions & 0 deletions force-app/main/default/classes/GGW_PermissionValidator.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright (c) 2022, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*
* GGW_PermissionValidator class used to support checking object and field level access while using the Grant Content Kit.
*
*/
public with sharing class GGW_PermissionValidator {
@TestVisible
private static GGW_PermissionValidator instance;

public static GGW_PermissionValidator getInstance() {
if (instance == null) {
instance = new GGW_PermissionValidator();
}
return instance;
}

private static final String NAMESPACE = '%%%NAMESPACED_ORG%%%';

public enum CRUDAccessType {
CREATEABLE,
READABLE,
UPDATEABLE,
DELETEABLE
}

public Boolean hasFLSAccessForFields(
String objectName,
List<String> fields,
String operation
) {
return hasFLSAccessForFields(objectName, fields, operation, true);
}

public Boolean hasFLSAccessForFields(
String objectName,
List<String> fields,
String operation,
Boolean strictMode
) {
try {
String nameSpacedObjectName = NAMESPACE + objectName;
Schema.DescribeSobjectResult[] results = Schema.describeSObjects(

new List<String>{ nameSpacedObjectName }
);
Map<String, Schema.SObjectField> fieldsMap = results[0].fields.getMap();

for (String fieldName : fields) {
//Prepend the Namespace if it exists in the Environment
String nameSpacedFN = NAMESPACE + fieldname;

if (strictMode && !fieldsMap.containsKey(nameSpacedFN)) {
return false;
} else if (!strictMode && !fieldsMap.containsKey(nameSpacedFN)) {
return true;
} else if (
operation == 'insert' &&
!fieldsMap.get(nameSpacedFN).getDescribe().isCreateable()
) {
return false;
} else if (
operation == 'upsert' &&
(!fieldsMap.get(nameSpacedFN).getDescribe().isCreateable() ||
!fieldsMap.get(nameSpacedFN).getDescribe().isUpdateable())
) {
return false;
} else if (
operation == 'read' &&
!hasFieldReadAccess(fieldsMap.get(nameSpacedFN).getDescribe())
) {
return false;
}
}
return true;
} catch (Exception e) {
return false;
}
}
//FLS Check for Standard Objects without Namespace

public Boolean hasStandFLSAccessForFields(
String objectName,
List<String> fields,
String operation
) {
return hasStandFLSAccessForFields(objectName, fields, operation, true);
}

public Boolean hasStandFLSAccessForFields(
String objectName,
List<String> fields,
String operation,
Boolean strictMode
) {
try {

Schema.DescribeSobjectResult[] results = Schema.describeSObjects(

new List<String>{ ObjectName }
);
Map<String, Schema.SObjectField> fieldsMap = results[0].fields.getMap();

for (String fieldName : fields) {

if (strictMode && !fieldsMap.containsKey(fieldname)) {
return false;
} else if (!strictMode && !fieldsMap.containsKey(fieldname)) {
return true;
} else if (
operation == 'insert' &&
!fieldsMap.get(fieldname).getDescribe().isCreateable()
) {
return false;
} else if (
operation == 'upsert' &&
(!fieldsMap.get(fieldname).getDescribe().isCreateable() ||
!fieldsMap.get(fieldname).getDescribe().isUpdateable())
) {
return false;
} else if (
operation == 'read' &&
!hasFieldReadAccess(fieldsMap.get(fieldname).getDescribe())
) {
return false;
}
}
return true;
} catch (Exception e) {
return false;
}
}

public Boolean hasFieldReadAccess(DescribeFieldResult field) {
return field.isAccessible();
}

public Boolean hasObjectAccess(SObjectType sObjectType, CRUDAccessType accessType) {
if (sObjectType == null) {
return false;
}

switch on accessType {
when CREATEABLE {
return sObjectType.getDescribe().isCreateable();
}
when READABLE {
return sObjectType.getDescribe().isAccessible();
}
when UPDATEABLE {
return sObjectType.getDescribe().isUpdateable();
}
when DELETEABLE {
return sObjectType.getDescribe().isDeletable();
}
when else {
return false;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>62.0</apiVersion>
<status>Active</status>
</ApexClass>
Loading
Loading