diff --git a/force-app/main/default/classes/GGW_ApplicationCtrl.cls b/force-app/main/default/classes/GGW_ApplicationCtrl.cls index 278196a..c7c4fb8 100644 --- a/force-app/main/default/classes/GGW_ApplicationCtrl.cls +++ b/force-app/main/default/classes/GGW_ApplicationCtrl.cls @@ -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{'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 @@ -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{'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'; @@ -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{'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; } } @@ -103,6 +116,9 @@ 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{'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; @@ -110,7 +126,7 @@ public with sharing class GGW_ApplicationCtrl { 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); @@ -118,10 +134,14 @@ public with sharing class GGW_ApplicationCtrl { // 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{'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; } } @@ -138,6 +158,10 @@ public with sharing class GGW_ApplicationCtrl { @AuraEnabled public static void reorderSections(List sectionList, String appId){ List updateOrderList = new List(); + //Check FLS + Boolean canAccessSelectedItem3 = GGW_PermissionValidator.getInstance() + .hasFLSAccessForFields('GGW_Selected_Item__c', new List{'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; @@ -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 } } @@ -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{'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+''; @@ -354,6 +382,10 @@ 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{'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(); @@ -361,18 +393,23 @@ public with sharing class GGW_ApplicationCtrl { 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{'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; @@ -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{'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 sections){ + //Check FLS + Boolean canAccessSI2 = GGW_PermissionValidator.getInstance() + .hasFLSAccessForFields('GGW_Selected_Item__c', new List{'GGW_Section__c','Grant_Application__c','Sort_Order__c'}, 'Upsert'); + // Add selected sections itterate over selected section IDs param List selectedItems = new List(); Integer itemSortOrder = 1; @@ -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; } } diff --git a/force-app/main/default/classes/GGW_ApplicationCtrlTest.cls b/force-app/main/default/classes/GGW_ApplicationCtrlTest.cls index 615adca..c80a641 100644 --- a/force-app/main/default/classes/GGW_ApplicationCtrlTest.cls +++ b/force-app/main/default/classes/GGW_ApplicationCtrlTest.cls @@ -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'; @@ -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'); } diff --git a/force-app/main/default/classes/GGW_PermissionValidator.cls b/force-app/main/default/classes/GGW_PermissionValidator.cls new file mode 100644 index 0000000..b3cc001 --- /dev/null +++ b/force-app/main/default/classes/GGW_PermissionValidator.cls @@ -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 fields, + String operation + ) { + return hasFLSAccessForFields(objectName, fields, operation, true); + } + + public Boolean hasFLSAccessForFields( + String objectName, + List fields, + String operation, + Boolean strictMode + ) { + try { + String nameSpacedObjectName = NAMESPACE + objectName; + Schema.DescribeSobjectResult[] results = Schema.describeSObjects( + + new List{ nameSpacedObjectName } + ); + Map 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 fields, + String operation + ) { + return hasStandFLSAccessForFields(objectName, fields, operation, true); + } + + public Boolean hasStandFLSAccessForFields( + String objectName, + List fields, + String operation, + Boolean strictMode + ) { + try { + + Schema.DescribeSobjectResult[] results = Schema.describeSObjects( + + new List{ ObjectName } + ); + Map 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; + } + } + } +} \ No newline at end of file diff --git a/force-app/main/default/classes/GGW_PermissionValidator.cls-meta.xml b/force-app/main/default/classes/GGW_PermissionValidator.cls-meta.xml new file mode 100644 index 0000000..998805a --- /dev/null +++ b/force-app/main/default/classes/GGW_PermissionValidator.cls-meta.xml @@ -0,0 +1,5 @@ + + + 62.0 + Active + diff --git a/force-app/main/default/classes/GGW_PermissionValidatorTest.cls b/force-app/main/default/classes/GGW_PermissionValidatorTest.cls new file mode 100644 index 0000000..3a3ea67 --- /dev/null +++ b/force-app/main/default/classes/GGW_PermissionValidatorTest.cls @@ -0,0 +1,300 @@ +/* + * 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 support the Security checks for Grant Content Kit Users. + * + */ +@IsTest +private class GGW_PermissionValidatorTest { + @IsTest + private static void testHasFLSAccessInsertGoodField(){ + Boolean canAccessContactName = GGW_PermissionValidator.getInstance() + .hasFLSAccessForFields('Contact', new List{'FirstName'}, 'Insert'); + System.Assert(canAccessContactName,'Has insert permission on Contact.FirstName field'); + } + + @IsTest + private static void testHasFLSAccessInsertBadObjectName() { + Boolean badObjectName = GGW_PermissionValidator.getInstance() + .hasFLSAccessForFields('TestObject', new List{ 'Test' }, 'insert'); + System.assert(!badObjectName, 'Bad object name, expect false'); + } + + @IsTest + private static void testHasFLSAccessInsertBadFieldNameStrictModeDisabled() { + Boolean badFieldName = GGW_PermissionValidator.getInstance() + .hasFLSAccessForFields( + 'Contact', + new List{ 'BadFieldName__123' }, + 'insert', + false + ); + System.assert(badFieldName, 'Returns true when a field does not exist.'); + } + + @IsTest + private static void testHasFLSAccessInsertBadFieldName() { + Boolean badFieldName = GGW_PermissionValidator.getInstance() + .hasFLSAccessForFields( + 'Contact', + new List{ 'BadFieldName__123' }, + 'insert' + ); + System.assert(!badFieldName, 'Bad field name, expect false'); + } + + @IsTest + private static void testHasFLSAccessInsertNonCreatableField() { + Boolean nonCreatableField = GGW_PermissionValidator.getInstance() + .hasFLSAccessForFields( + 'Contact', + new List{ 'LastModifiedById' }, + 'insert' + ); + System.assert(!nonCreatableField, 'Can not modify system field, expect false'); + } + + @IsTest + private static void testHasFLSAccessUpsertBadFieldName() { + Boolean nonExistentField = GGW_PermissionValidator.getInstance() + .hasFLSAccessForFields( + 'Contact', + new List{ 'BadObjectName__123' }, + 'upsert' + ); + System.assert(!nonExistentField, 'Bad field name, expect false'); + } + + @IsTest + private static void testHasFLSAccessUpsertNonReparentable() { + Boolean nonUpdateableField = GGW_PermissionValidator.getInstance() + .hasFLSAccessForFields( + Schema.SObjectType.GGW_Grant_Application__c.getName(), + new List{ + Schema.SObjectType.GGW_Grant_Application__c.fields.LastModifiedById.getName() + }, + 'upsert' + ); + System.assert(!nonUpdateableField, 'Non updateable system field, expect false'); + } + + @IsTest + private static void testHasFLSAccessUpsertGoodField() { + Boolean editableField = GGW_PermissionValidator.getInstance() + .hasFLSAccessForFields('Contact', new List{ 'FirstName' }, 'upsert'); + System.assert(editableField, 'Editable field, expect true'); + } + + @IsTest + private static void testHasFLSAccessReadGoodField() { + Boolean readableField = GGW_PermissionValidator.getInstance() + .hasFLSAccessForFields('Contact', new List{ 'FirstName' }, 'read'); + System.assert(readableField, 'Readable field, expect true'); + } + + @IsTest + private static void testHasFLSAccessNoFLSField() { + Boolean noFLSField; + System.runAs(GGW_TestDataFactory.getTestUser()) { + // test user has no custom object permissions + noFLSField = GGW_PermissionValidator.getInstance() + .hasFLSAccessForFields( + 'Contact', + new List{ 'DoNotCall' }, + 'read' + ); + } + System.assert(!noFLSField, 'Custom field name, expect false'); + } + //Test Stand FLS Check method + @IsTest + private static void testHasStandFLSAccessInsertGoodField(){ + Boolean canAccessContactName = GGW_PermissionValidator.getInstance() + .hasStandFLSAccessForFields('Contact', new List{'FirstName'}, 'Insert'); + System.Assert(canAccessContactName,'Has insert permission on Contact.FirstName field'); + } + + @IsTest + private static void testStandHasFLSAccessInsertBadObjectName() { + Boolean badObjectName = GGW_PermissionValidator.getInstance() + .hasStandFLSAccessForFields('TestObject', new List{ 'Test' }, 'insert'); + System.assert(!badObjectName, 'Bad object name, expect false'); + } + + @IsTest + private static void testHasStandFLSAccessInsertBadFieldNameStrictModeDisabled() { + Boolean badFieldName = GGW_PermissionValidator.getInstance() + .hasStandFLSAccessForFields( + 'Contact', + new List{ 'BadFieldName__123' }, + 'insert', + false + ); + System.assert(badFieldName, 'Returns true when a field does not exist.'); + } + + @IsTest + private static void testHasStandFLSAccessInsertBadFieldName() { + Boolean badFieldName = GGW_PermissionValidator.getInstance() + .hasStandFLSAccessForFields( + 'Contact', + new List{ 'BadFieldName__123' }, + 'insert' + ); + System.assert(!badFieldName, 'Bad field name, expect false'); + } + + @IsTest + private static void testHasStandFLSAccessInsertNonCreatableField() { + Boolean nonCreatableField = GGW_PermissionValidator.getInstance() + .hasStandFLSAccessForFields( + 'Contact', + new List{ 'LastModifiedById' }, + 'insert' + ); + System.assert(!nonCreatableField, 'Can not modify system field, expect false'); + } + + @IsTest + private static void testHasStandFLSAccessUpsertBadFieldName() { + Boolean nonExistentField = GGW_PermissionValidator.getInstance() + .hasStandFLSAccessForFields( + 'Contact', + new List{ 'BadObjectName__123' }, + 'upsert' + ); + System.assert(!nonExistentField, 'Bad field name, expect false'); + } + + @IsTest + private static void testHasStandFLSAccessUpsertNonReparentable() { + Boolean nonUpdateableField = GGW_PermissionValidator.getInstance() + .hasStandFLSAccessForFields( + Schema.SObjectType.GGW_Grant_Application__c.getName(), + new List{ + Schema.SObjectType.GGW_Grant_Application__c.fields.LastModifiedById.getName() + }, + 'upsert' + ); + System.assert(!nonUpdateableField, 'Non updateable system field, expect false'); + } + + @IsTest + private static void testHasStandFLSAccessUpsertGoodField() { + Boolean editableField = GGW_PermissionValidator.getInstance() + .hasStandFLSAccessForFields('Contact', new List{ 'FirstName' }, 'upsert'); + System.assert(editableField, 'Editable field, expect true'); + } + + @IsTest + private static void testHasStandFLSAccessReadGoodField() { + Boolean readableField = GGW_PermissionValidator.getInstance() + .hasStandFLSAccessForFields('Contact', new List{ 'FirstName' }, 'read'); + System.assert(readableField, 'Readable field, expect true'); + } + + @IsTest + private static void testHasStandFLSAccessNoFLSField() { + Boolean noFLSField; + System.runAs(GGW_TestDataFactory.getTestUser()) { + // test user has no custom object permissions + noFLSField = GGW_PermissionValidator.getInstance() + .hasStandFLSAccessForFields( + 'Contact', + new List{ 'DoNotCall' }, + 'read' + ); + } + System.assert(!noFLSField, 'Custom field name, expect false'); + } + + @IsTest + private static void testHasObjectAccessWithStandardObjectsAsAdmin() { + Boolean canCreateContact = GGW_PermissionValidator.getInstance() + .hasObjectAccess( + Contact.SObjectType, + GGW_PermissionValidator.CRUDAccessType.CREATEABLE + ); + System.assert(canCreateContact, 'Has Create perms on Contact, expect true'); + + Boolean canReadContact = GGW_PermissionValidator.getInstance() + .hasObjectAccess( + Contact.SObjectType, + GGW_PermissionValidator.CRUDAccessType.READABLE + ); + System.assert(canReadContact, 'Has Read perms on Contact, expect true'); + + Boolean canUpdateContact = GGW_PermissionValidator.getInstance() + .hasObjectAccess( + Contact.SObjectType, + GGW_PermissionValidator.CRUDAccessType.UPDATEABLE + ); + System.assert(canUpdateContact, 'Has Update perms on Contact, expect true'); + + Boolean canDeleteContact = GGW_PermissionValidator.getInstance() + .hasObjectAccess( + Contact.SObjectType, + GGW_PermissionValidator.CRUDAccessType.DELETEABLE + ); + System.assert(canDeleteContact, 'Has Delete perms on Contact, expect true'); + } + + @IsTest + private static void testHasObjectAccessWithCustomObjectsAsReadOnlyUser() { + System.runAs(GGW_TestDataFactory.getTestUser()) { + Boolean canCreateGrantApplication = GGW_PermissionValidator.getInstance() + .hasObjectAccess( + GGW_Grant_Application__c.SObjectType, + GGW_PermissionValidator.CRUDAccessType.CREATEABLE + ); + System.assert( + !canCreateGrantApplication, + 'Has no Create perms on GrantApplication, expect false' + ); + + Boolean canReadGrantApplication = GGW_PermissionValidator.getInstance() + .hasObjectAccess( + GGW_Grant_Application__c.SObjectType, + GGW_PermissionValidator.CRUDAccessType.READABLE + ); + System.assert(!canReadGrantApplication, 'Has no Read perms on GrantApplication, expect false'); + + Boolean canUpdateGrantApplication = GGW_PermissionValidator.getInstance() + .hasObjectAccess( + GGW_Grant_Application__c.SObjectType, + GGW_PermissionValidator.CRUDAccessType.UPDATEABLE + ); + System.assert( + !canUpdateGrantApplication, + 'Has no Update perms on GrantApplication, expect false' + ); + + Boolean canDeleteGrantApplication = GGW_PermissionValidator.getInstance() + .hasObjectAccess( + GGW_Grant_Application__c.SObjectType, + GGW_PermissionValidator.CRUDAccessType.DELETEABLE + ); + System.assert( + !canDeleteGrantApplication, + 'Has no Delete perms on GrantApplication, expect false' + ); + } + } + + @IsTest + private static void testHasObjectAccessWithNullSObjectType() { + Boolean nullSObjectType = GGW_PermissionValidator.getInstance() + .hasObjectAccess(null, GGW_PermissionValidator.CRUDAccessType.READABLE); + System.assert(!nullSObjectType, 'Null SObjectType, expect false'); + } + + @IsTest + private static void testHasObjectAccessWithNullCRUDAccessType() { + Boolean nullCRUDAccessType = GGW_PermissionValidator.getInstance() + .hasObjectAccess(Contact.SObjectType, null); + System.assert(!nullCRUDAccessType, 'Null CRUDAccessType, expect false'); + } +} diff --git a/force-app/main/default/classes/GGW_PermissionValidatorTest.cls-meta.xml b/force-app/main/default/classes/GGW_PermissionValidatorTest.cls-meta.xml new file mode 100644 index 0000000..5f399c3 --- /dev/null +++ b/force-app/main/default/classes/GGW_PermissionValidatorTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 63.0 + Active + diff --git a/force-app/main/default/classes/GGW_SampleData.cls b/force-app/main/default/classes/GGW_SampleData.cls index 46e29d4..3e5ae38 100644 --- a/force-app/main/default/classes/GGW_SampleData.cls +++ b/force-app/main/default/classes/GGW_SampleData.cls @@ -83,11 +83,20 @@ public with sharing class GGW_SampleData { // Insert sample Grant app private static void createSampleGrant(){ List lstBlock = [SELECT Id, Name, Section__c, Description__c, Short_Description__c FROM GGW_Content_Block__c WITH SECURITY_ENFORCED LIMIT 20]; + + //Check FLS + Boolean canEditGA = GGW_PermissionValidator.getInstance() + .hasFLSAccessForFields('GGW_Grant_Application__c', new List{'Name','Status__c','Language__c'}, 'Upsert'); + + Boolean canEditSI = GGW_PermissionValidator.getInstance() + .hasFLSAccessForFields('GGW_Selected_Item__c', new List{'GGW_Section__c','Grant_Application__c','Selected_Block__c','Text_Block__c', 'Sort_Order__c'}, 'Upsert'); + + GGW_Grant_Application__c gapp = new GGW_Grant_Application__c(); gapp.Name = 'Cloudy Grant Sample'; gapp.Status__c = 'In Progress'; gapp.Language__c = 'en_US'; - if(Schema.sObjectType.GGW_Grant_Application__c.isCreateable()){ + if(Schema.sObjectType.GGW_Grant_Application__c.isCreateable() && canEditGA){ insert gapp; } List items = new List(); @@ -104,7 +113,7 @@ public with sharing class GGW_SampleData { } itemSortOrder++; // increment sort order set as default } - if(Schema.sObjectType.GGW_Selected_Item__c.isCreateable()){ + if(Schema.sObjectType.GGW_Selected_Item__c.isCreateable() && canEditSI){ insert items; } } diff --git a/force-app/main/default/classes/GGW_TestDataFactory.cls b/force-app/main/default/classes/GGW_TestDataFactory.cls index 0c5cc13..9d32662 100644 --- a/force-app/main/default/classes/GGW_TestDataFactory.cls +++ b/force-app/main/default/classes/GGW_TestDataFactory.cls @@ -79,4 +79,23 @@ public class GGW_TestDataFactory { insert sblock; } + + public static User getTestUser() { + + Profile p = [SELECT Id FROM Profile WHERE Name='Standard User']; + + User user = new User( + ProfileId = p.Id, + Username = 'test' + UserInfo.getOrganizationId() + System.currentTimeMillis() + '@test.com', + Email = 'test' + UserInfo.getOrganizationId() + System.currentTimeMillis() + '@test.com', + LastName = 'Test', + FirstName = 'Test', + LanguageLocaleKey = 'en_US', + LocaleSidKey = 'en_US', + TimeZoneSidKey = 'America/Los_Angeles', + EmailEncodingKey = 'UTF-8', + Alias = 'Test' + ); + return user; + } } \ No newline at end of file diff --git a/force-app/main/default/classes/GGW_Util.cls b/force-app/main/default/classes/GGW_Util.cls index 70d894d..0318c34 100644 --- a/force-app/main/default/classes/GGW_Util.cls +++ b/force-app/main/default/classes/GGW_Util.cls @@ -33,6 +33,10 @@ public with sharing class GGW_Util { // Method save Langugae selection to user state public static String saveGrantLanguage(String lang, String grantId){ String msg = 'Save language select failed.'; + + Boolean editableField = GGW_PermissionValidator.getInstance() + .hasStandFLSAccessForFields('GGW_Grant_State__c', new List{ 'Language__c', 'Grant_Application__c' }, 'upsert'); + GGW_Grant_State__c st = getGrantState(); if(st != null && st.Id != null){ if(isValidString(lang)){ @@ -41,7 +45,7 @@ public with sharing class GGW_Util { if(isValidString(grantId)){ st.Grant_Application__c = grantId; } - if(Schema.sObjectType.GGW_Grant_State__c.isUpdateable()){ + if(Schema.sObjectType.GGW_Grant_State__c.isUpdateable() && editableField){ update st; msg = 'Language state is updated'; } @@ -53,6 +57,10 @@ public with sharing class GGW_Util { // Create New Grant state record for user when there is NONE exists private static String insertNewState(String lang, String grantId){ String msg = 'New state insert failed.'; + + Boolean editableField = GGW_PermissionValidator.getInstance() + .hasStandFLSAccessForFields('GGW_Grant_State__c', new List{ 'Language__c', 'Grant_Application__c', 'Current_User__c'}, 'upsert'); + GGW_Grant_State__c st = new GGW_Grant_State__c(); st.Current_User__c = UserInfo.getUserId(); // save state for current user if(isValidString(lang)){ @@ -63,7 +71,7 @@ public with sharing class GGW_Util { if(isValidString(grantId)){ st.Grant_Application__c = grantId; } - if(Schema.sObjectType.GGW_Grant_State__c.isCreateable()){ + if(Schema.sObjectType.GGW_Grant_State__c.isCreateable() && editableField){ insert st; msg = 'NEW Language state is inserted'; } @@ -71,18 +79,23 @@ public with sharing class GGW_Util { } // Save Grant state for User public static void saveGrantState(String grantId){ + + Boolean editableField = GGW_PermissionValidator.getInstance() + .hasStandFLSAccessForFields('GGW_Grant_State__c', new List{ 'Language__c', 'Grant_Application__c', 'Current_User__c'}, 'upsert'); + + GGW_Grant_State__c st = getGrantState(); if(st != null && st.Id != null){ // Update existing single state st.Grant_Application__c = grantId; - if(Schema.sObjectType.GGW_Grant_State__c.isUpdateable()){ + if(Schema.sObjectType.GGW_Grant_State__c.isUpdateable() && editableField){ update st; } }else{ st = new GGW_Grant_State__c(); st.Current_User__c = UserInfo.getUserId(); // save state for current user st.Grant_Application__c = grantId; - if(Schema.sObjectType.GGW_Grant_State__c.isCreateable()){ + if(Schema.sObjectType.GGW_Grant_State__c.isCreateable() && editableField){ insert st; } } diff --git a/force-app/main/default/permissionsets/GGW_User_Permissions.permissionset-meta.xml b/force-app/main/default/permissionsets/GGW_User_Permissions.permissionset-meta.xml index 4487089..d371f62 100644 --- a/force-app/main/default/permissionsets/GGW_User_Permissions.permissionset-meta.xml +++ b/force-app/main/default/permissionsets/GGW_User_Permissions.permissionset-meta.xml @@ -32,6 +32,10 @@ GGW_GrantApplicationWrapper true + + GGW_PermissionValidator + true + GGW_SampleData true