Sample Custom Adapter for providing Row Level security for Big Objects / External Objects.
For more information, please see:
Sample Custom Adapter for providing Row Level security for Big Objects / External Objects.
For more information, please see:
/** | |
* Example DataSource.Connection class | |
* that provides row level security | |
* for Archived BigObject records | |
* | |
* For more information, please see: | |
* https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_connector_start_connection_class.htm | |
* or | |
* https://help.salesforce.com/articleView?id=apex_adapter_setup.htm&type=0 | |
* | |
* @author Paul Roth <proth@salesforce.com> | |
**/ | |
global class BigO_SecurityConnection | |
extends DataSource.Connection | |
{ | |
global final String EXTERNAL_TABLE_NAME = 'MyArchivedCaseObject'; | |
global final String EXTERNAL_ID = 'ExternalId'; | |
/** | |
* Constructor | |
**/ | |
global BigO_SecurityConnection(){} | |
global BigO_SecurityConnection(DataSource.ConnectionParams connectionParams){} | |
/** | |
* The sync() method is invoked when an administrator clicks the | |
* <b>Validate and Sync</b> button on the external data source detail page. | |
* | |
* <p>It returns information that describes the structural metadata on the external system.</p> | |
* | |
* <p>Please note: Changing the <b>Sync</b> method on the <b>DataSource.Connection</b> | |
* class does not automaticaly resync any external objects. | |
* That will happen only when re-clicking the <b>Validate and Sync</b> button.</p> | |
* | |
* @see https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_class_DataSource_Column.htm | |
* @see https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_connector_start_connection_class.htm#section_sync_method | |
* @return (DataSource.Table[]) - list of table results | |
**/ | |
override global DataSource.Table[] sync(){ | |
DataSource.Table[] results = new DataSource.Table[]{}; | |
//-- Please see the following for a list of other data types supported by columns | |
//-- https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_class_DataSource_Column.htm | |
//-- plese note: multiple object can be supported | |
//-- simply include defintions for each additional object | |
DataSource.Column[] columns = new DataSource.Column[]{ | |
DataSource.Column.text('ExternalId', 'External Id', 30), | |
DataSource.Column.text('CaseNumber__c', 'Case Number', 30), | |
DataSource.Column.text('Subject__c', 255), | |
DataSource.Column.text('Description__c', 255), | |
DataSource.Column.text('Owner__c', 18) | |
}; | |
results.add(DataSource.Table.get(EXTERNAL_TABLE_NAME, EXTERNAL_TABLE_NAME, columns)); | |
//-- add additional table/columns for any others to be handled here. | |
return(results); | |
} | |
/** | |
* Wrapper for performing SOQL queries. | |
* | |
* <p>The query method is invoked when a SOQL query is executed on an external object.</p> | |
* | |
* <p>A SOQL query is automatically generated and executed when a user opens an | |
* external object’s list view or detail page in Salesforce.</p> | |
* | |
* <p>The DataSource.QueryContext is always only for a single table.</p> | |
* | |
* <p>The <b>DataSource.QueryUtils</b> class and its helper methods can process | |
* query results locally within your Salesforce org, | |
* and is mentioned here only for completeness.</p> | |
* | |
* <p>This class is provided for your convenience to simplify the development | |
* of your Salesforce Connect custom adapter for initial tests.</p> | |
* | |
* <p>However, the DataSource.QueryUtils class and its methods | |
* aren’t supported for use in production environments | |
* that use callouts to retrieve data from external systems.</p> | |
* | |
* <p>For the purposes of Row Level Security, however, | |
* <b>DataSource.QueryUtils</b> will be less helpful</p> | |
* | |
* <p>NOTE: this includes the userId filter on top of any user supplied | |
* where clauses</p> | |
* | |
* @see https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_class_DataSource_QueryUtils.htm | |
* @see https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_connector_start_connection_class.htm#section_query_method | |
* @param context (DataSource.QueryContext) - context for the query | |
* @return (DataSource.TableResult) - table result from the query. | |
**/ | |
override global DataSource.TableResult query( | |
DataSource.QueryContext context | |
){ | |
System.debug('Beginning of Search'); | |
List<Map<String,Object>> rows = new List<Map<String,Object>>(); | |
String tableName; | |
DataSource.Filter filter = context.tableSelection.filter; | |
//System.debug('TableSelection:' + context.tableSelection.toString()); | |
if(filter != null){ | |
tableName = filter.tableName; | |
} else { | |
tableName = context.tableSelection.tableSelected; | |
} | |
System.debug('TableName attempted within security filter:' + tableName); | |
//-- this can support multiple objects for filtering | |
//-- simply add additional cases for each table desired. | |
if(tableName == EXTERNAL_TABLE_NAME){ | |
System.debug('TableSelection found within security filter:' + context.tableSelection.toString()); | |
String userId = System.UserInfo.getUserId(); | |
String externalId = null; | |
if(filter != null){ | |
String thisColumnName = filter.columnName; | |
if(thisColumnName != null && thisColumnName.equalsIgnoreCase(EXTERNAL_ID)){ | |
//-- get only the single external id | |
externalId = String.valueOf(filter.columnValue); | |
} else { | |
//-- get all rows | |
externalId = null; | |
} | |
} else { | |
//-- get all rows | |
externalId = null; | |
} | |
Public_Archived_Case_Object__b[] records = null; | |
if(externalId == null){ | |
System.debug('no filter/externId is null'); | |
records = [ | |
SELECT Id, ExternalId__c, CaseNumber__c, Subject__c, Description__c, Owner__c | |
FROM Public_Archived_Case_Object__b | |
WHERE Owner__c = :userId | |
]; | |
} else { | |
System.debug('External Id was sent'); | |
records = [ | |
SELECT Id, ExternalId__c, CaseNumber__c, Subject__c, Description__c, Owner__c | |
FROM Public_Archived_Case_Object__b | |
WHERE Owner__c = :userId | |
AND ExternalId__c = :externalId | |
]; | |
} | |
for(Public_Archived_Case_Object__b record : records){ | |
rows.add(buildRecord(record)); | |
} | |
} | |
return DataSource.TableResult.get(context, rows); | |
} | |
/** | |
* The search method is invoked by a SOSL query of an external object | |
* or when a user performs a Salesforce global search | |
* that also searches external objects. | |
* | |
* <p>Because search can be federated over multiple objects, | |
* the <b>DataSource.SearchContext</b> can have multiple tables selected. | |
* Yet in this example, however, the custom adapter knows about only one table.</p> | |
* | |
* <p>PLEASE NOTE: this includes the userId search on top of any SOSL where clauses</p> | |
* | |
* <p>ALSO NOTE: Big Objects ONLY support searches on fields used within indexes currently, | |
* but external objects are more forgiving.</p> | |
* | |
* @see https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_connector_start_connection_class.htm#section_search_method | |
* @param context (DataSource.SearchContect) | |
* @return (DataSource.TableResult[]) - colection of table results for all objects under search. | |
**/ | |
override global DataSource.TableResult[] search( | |
DataSource.SearchContext context | |
){ | |
System.debug('Beginning of Search'); | |
System.debug('TableSelection:' + context.tableSelections[0].toString()); | |
List<Map<String,Object>> rows= new List<Map<String,Object>>(); | |
//-- @TODO: extend to support subqueries/subselects | |
if(context.tableSelections[0].tableSelected == EXTERNAL_TABLE_NAME){ | |
String searchPhrase = context.searchPhrase; | |
searchPhrase = '%' + searchPhrase + '%'; | |
//-- get the current user's userId | |
String userId = System.UserInfo.getUserId(); | |
//-- please note: Fields used in search for a big object | |
//-- must be used as defined in the index left to right | |
//-- meaning that sibling searches like Subject OR Description | |
//-- would require separate indices | |
//-- FOR NOW - big objects only support a single index | |
for( Public_Archived_Case_Object__b externalRecord : [ | |
SELECT Id, ExternalId__c, CaseNumber__c, Subject__c, Description__c, Owner__c | |
FROM Public_Archived_Case_Object__b | |
WHERE Owner__c = :userId | |
//-- @TODO: include additional where clauses | |
//-- External Objects would support like the following | |
//-- and (Subject__c like :searchPhrase OR Description__c like :searchPhrase OR CaseNumber__c like :searchPhrase) | |
//-- but BigObjects CAN ONLY include fields used within the index. | |
]){ | |
rows.add(buildRecord(externalRecord)); | |
} | |
} | |
DataSource.TableResult[] result = new DataSource.TableResult[]{ | |
DataSource.TableResult.get(context.tableSelections[0], rows) | |
}; | |
return(result); | |
} | |
/** | |
* The upsertRows method is invoked when external object records | |
* are created or updated. | |
* | |
* <p>You can create or update external object records | |
* through the Salesforce user interface or DML.</p> | |
* | |
* <p>For this demonstration, we have disabled upserts within | |
* the related <b>DataSource.Provider</p> class, so this is not needed</p> | |
* | |
* @see https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_connector_start_connection_class.htm#upsertRows_section | |
* @param context (DataSource.UpsertContext context) | |
* @return DataSource.UpsertResult[] | |
**/ | |
/* | |
global override DataSource.UpsertResult[] upsertRows( | |
DataSource.UpsertContext context | |
){ | |
return(null); | |
} | |
*/ | |
/** | |
* The deleteRows method is invoked when external object records are deleted. | |
* | |
* <p>You can delete external object records through the Salesforce user interface or DML.</p> | |
* | |
* <p>For this demonstration, we have disabled deletions within | |
* the related <b>DataSource.Provider</p> class, so this is not needed</p> | |
* | |
* @see https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_connector_start_connection_class.htm#deleteRows_section | |
* @param context (DataSource.DeleteContext) | |
* @return (DataSource.DeleteResult[]) - | |
**/ | |
/* | |
global override DataSource.DeleteResult[] deleteRows( | |
DataSource.DeleteContext context | |
){ | |
return(null); | |
} | |
*/ | |
/** | |
* Builds a Map<String,Object> representation of a result object. | |
* | |
* <p>From a security wrapper point, this translates the resulting object from the original object.</p> | |
* | |
* @param externalRecord (public_todo__x) | |
* @return (map<String,Object>) | |
**/ | |
global Map<String,Object> buildRecord(Public_Archived_Case_Object__b externalRecord){ | |
Map<String,Object> result = new Map<String,Object>(); | |
result.put('ExternalId', string.valueOf(externalRecord.ExternalId__c)); | |
result.put('CaseNumber__c', string.valueOf(externalRecord.CaseNumber__c)); | |
//result.put('DisplayUrl', string.valueOf(externalRecord.DisplayUrl)); | |
result.put('Subject__c', string.valueOf(externalRecord.Subject__c)); | |
result.put('Description__c', string.valueOf(externalRecord.Description__c)); | |
result.put('Owner__c', string.valueOf(externalRecord.Owner__c)); | |
//result.put('SfId__c', string.valueOf(externalRecord.SfId__c)); | |
return(result); | |
} | |
} |
/** | |
* Example Data Provider that provides row level security | |
* for Archived BigData records. | |
* | |
* <p>Your DataSource.Provider class informs Salesforce | |
* of the functional and authentication capabilities | |
* that are supported by or required | |
* to connect to the external system.</p> | |
* | |
* For more information, please see: | |
* https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_connector_start_provider_class.htm | |
* or | |
* https://help.salesforce.com/articleView?id=apex_adapter_setup.htm&type=0 | |
* | |
* @author Paul Roth <proth@salesforce.com> | |
**/ | |
global class BigO_SecurityProvider | |
extends DataSource.Provider | |
{ | |
/** | |
* If the external system requires authentication, | |
* Salesforce can provide the authentication credentials | |
* from the external data source definition or users’ | |
* personal settings.</p> | |
* | |
* <p>For simplicity, however, this example declares | |
* that the external system doesn’t require authentication.</p> | |
* | |
* <p>To do so, it returns AuthenticationCapability.ANONYMOUS | |
* as the sole entry in the list of authentication capabilities.</p> | |
* | |
* @return DataSource.AuthenticationCapability[] - list of capabilities. | |
**/ | |
override global DataSource.AuthenticationCapability[] getAuthenticationCapabilities(){ | |
DataSource.AuthenticationCapability[] results = new DataSource.AuthenticationCapability[]{ | |
DataSource.AuthenticationCapability.ANONYMOUS | |
}; | |
return(results); | |
} | |
/** | |
* Defines the capabilities Salesforce supports for this object. | |
* | |
* <p>These capabilities include: | |
* <ul> | |
* <li>To allow SOQL, declare the <b>DataSource.Capability.ROW_QUERY</b> capability.</li> | |
* <li>To allow SOSL and Salesforce searches, declare the <b>DataSource.Capability.SEARCH</b> capability.</li> | |
* <li>To allow upserting external data, declare the | |
* <b>DataSource.Capability.ROW_CREATE</b> | |
* and <b>DataSource.Capability.ROW_UPDATE</b> capabilities.</li> | |
* <li>To allow deleting external data, declare the <b>DataSource.Capability.ROW_DELETE</b> capability.</li> | |
* </ul></p> | |
* @see https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_enum_DataSource_AuthenticationCapability.htm | |
* @return DataSource.Capability[] - list of capabilities | |
**/ | |
override global DataSource.Capability[] getCapabilities(){ | |
DataSource.Capability[] results = new DataSource.Capability[]{ | |
DataSource.Capability.ROW_QUERY | |
,DataSource.Capability.SEARCH | |
//,DataSource.Capability.ROW_CREATE, | |
//,DataSource.Capability.ROW_UPDATE, | |
//,DataSource.Capability.ROW_DELETE | |
}; | |
return(results); | |
} | |
/** | |
* Return the <b>DataSource.Connection</b> implementation | |
* to use for this adapter. | |
* | |
* <p>This is where the actual filtering will take place</p> | |
* | |
* @return DataSource.Connection - instance | |
**/ | |
override global DataSource.Connection getConnection( | |
DataSource.ConnectionParams connectionParams | |
){ | |
return(new BigO_SecurityConnection(connectionParams)); | |
} | |
} |