Skip to content

Instantly share code, notes, and snippets.

@paulroth3d
Last active March 8, 2019 05:57
Show Gist options
  • Save paulroth3d/e05bb43ef70e680596ff520dacc6a771 to your computer and use it in GitHub Desktop.
Save paulroth3d/e05bb43ef70e680596ff520dacc6a771 to your computer and use it in GitHub Desktop.
Salesforce Custom Adapter for External Object Row-Level Security
/**
* 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));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment