Loading

Content Object Access By Functions Using Programmatic Share on ContentDocumentLink

Julkaisupäivä: Oct 13, 2022
Kuvaus

Terminology

Content Document: Represents a document that has been uploaded to a library in Salesforce CRM Content or Salesforce Files.

Content Version: Represents a specific version of a document in Salesforce CRM Content or Salesforce Files. One Content Document can have multiple content versions.

Content Document Link:
Represents the link between a Salesforce CRM Content document or Salesforce file and where it’s shared. A file can be shared with other users, groups, records, and Salesforce CRM Content libraries.
 

Document Link Creation and Content Access

Two types of Content access are possible:
  1. Function accessing its own Content
  2. Function accessing another user’s content
When a function creates new Content the Cloud Integration User will own the Content so functions will have access to it. This is the simpler scenario and the more common scenario is where Content already exists and needs to be access by a function.
 

Function accessing its own Content

A function can create content and insert it using the REST API endpoint at:
services/data/vXX.X/sobjects/ContentVersion

This will automatically make the Cloud Integration User as the owner of the content and will allow the function (as well as other Users) to access this content. Document Links are automatically inserted so no additional steps are necessary.

Function accessing another user’s content

We need to create a record in ContentDocumentLink that links the Content with the user with whom it is being shared. In this case, the User will be the Cloud Integration User. This can be done via Apex after the content is created or via DataLoader after the content is uploaded.

Below is an example of Apex code that is used to create content (i.e. ContentVersion record) and link it with ContentDocumentLink (i.e. ContentDocumentLink record).
 
//Create Document
//Note: This example creates the content in Apex
//We could also load content via Data Loader.
ContentVersion cv = new ContentVersion();
cv.Title = 'Test Document';
cv.PathOnClient = 'TestDocument.pdf';
cv.VersionData = Blob.valueOf('Test Content');
cv.IsMajorVersion = true;
Insert cv;
 
//Get Content Documents
Id conDocId = [SELECT ContentDocumentId FROM ContentVersion WHERE Id =:cv.Id].ContentDocumentId;
 
//Create ContentDocumentLink 
ContentDocumentLink cdl = new ContentDocumentLink();
cdl.ContentDocumentId = conDocId;
//Label would contain the user id of the cloud integration user (obtained separately)
cdl.LinkedEntityId = Label.Platform_User_Id;
//V - Viewer permission. C - Collaborator permission. I - Inferred permission
cdl.ShareType = 'V';
Insert cdl;

Here the ContentDocumentId is obtained from the ContentVersion (the version of the document that was created earlier - not shown here)

The LinkedEntityId refers to the Cloud Integration User Id. This has to be obtained ahead of time and passed into this code, or be accessible from this code as a global variable (or via the platform cache).

Once we have created an entry in ContentDocumentLink (as above), a function will be able to access the Content normally via the SDK.

Obtaining the Cloud Integration User ID
Since the Cloud Integration User is not exposed in Salesforce like other users, the ID of this user can be obtained by one of the following ways:

1. We could query the User object and figure out the username for CloudIntegrationUser.The cloud integration user name is cloud@ + 18 character orgId in lower case. See SOQL query below 
SELECT Id, UserName from User WHERE username like 'cloud@00d%'

2. By an insert of a record by the function. The ID of the User who inserted this record will be that of the Cloud Integration User.

Once the ID has been obtained, it can be stored for use by the Apex code shown above.
The code below shows a function accessing a specific content document after the sharing was established as shown above.
// Purposefully selecting another file NOT owned by the platform integration user
// (Hardcoded platform integration user Id - to be replaced with a more dynamic ID 
// merge in the query)
let contentQuery = "SELECT Id 
                    FROM ContentVersion 
                    WHERE OwnerId != '0055f0000057ROYAA2' 
                    ORDER BY CreatedDate DESC LIMIT 1";
let queryResults = await context.org.dataApi.query(contentQuery);
let contId = "";
for (const downItem of queryResults.records) {
    contId = downItem.fields["Id"];
    break;
}

// File Id for download
logger.info("contId: " + JSON.parse(JSON.stringify(contId)));

// Print
logger.info("DOWNLOAD LATEST FILE NOT OWNED BY FUNCTIONS USER");

await request
    .get(
        context.org.baseUrl +
        "/services/data/v54.0/sobjects/ContentVersion/" +
        contId +
        "/VersionData"
    )
    .auth(null, null, true, context.org.dataApi.accessToken)
    .on("error", function (err) {
        logger.info("Exception: " + err);
    })
    .pipe(
        fs.createWriteStream("./tempFiles/downFile.csv", { encoding: "utf8" })
    )
    .on("finish", doSomethingAfterDownload);    
}

Note 1

The code purposefully tries to access content created by a user other than the Cloud Integration User. If the content was shared successfully in ContentDocumentLink (as shown earlier), this function would succeed in accessing the content.

Note 2

If the function code were to access content that was created by the Cloud Integration User, then it is not necessary to share it via ContentDocumentLink (as this would have been automatically created). Accessing content would be straightforward in this case.

Automated Document Link creation

It is possible to automate the creation of the Document Links records whenever a new Content is created. The following example shows how this might be done.

Sample Trigger Code

The code below is the trigger that creates a ContentDocumentLink record when a ContentVersion record is created (requires the Platform_User_Id from the previous example that is necessary for document link creation).
 
trigger ContentVersionTrigger on ContentVersion (after insert) {
    ContentVersionTriggerHelper.triggerHelper(
        Trigger.operationType, 
        Trigger.new, 
        Trigger.oldMap);
}
public with sharing class ContentVersionTriggerHelper {
    public static void triggerHelper(
        System.TriggerOperation operationType,
        List<ContentVersion> newList,
        Map<Id, ContentVersion> oldMap
    ) {
        switch on operationType {
            when AFTER_INSERT{
                List<ContentVersion> docsToProcess = getDocsToShare(newList);
                if(!docsToProcess.isEmpty()){
                    shareWithIntegrationUser(docsToProcess);
                }
            }
        }
    }

    public static List<ContentVersion> getDocsToShare(List<ContentVersion> allDocuments){
        List<ContentVersion> docsToProcess = new List<ContentVersion>();
        for(ContentVersion cVer : allDocuments){
            if(cDoc.OwnerId != Label.Platform_User_Id){
                docsToProcess.add(cVer);
            }
        }
        return docsToProcess;
    } 

    public static void shareWithIntegrationUser(List<ContentVersion> documents){
        List<ContentDocumentLink> shareLinks = new List<ContentDocumentLink>();
        for(ContentVersion cVer : documents){
            ContentDocumentLink cdl = new ContentDocumentLink();
            cdl.ContentDocumentId = cVer.ContentDocumentId;
            cdl.LinkedEntityId = Label.Platform_User_Id;
            cdl.ShareType = 'V';
            shareLinks.add(cdl);
        }
        Database.insert(shareLinks,false);
    }
}
 

Considerations and Risks

  • Once a content document link is shared, it can be accessed by any function. To prevent access to the document in the future, it has to be explicitly unshared from ContentDocumentLink. This approach gives access to the document for all functions or none because all functions run as the Cloud Integration User
  • The User ID of Cloud Integration User has to be obtained manually and injected into the Apex trigger. This requires the ID to be stored in a custom setting, object, or label. Or, it could be stored and accessed from the Platform Cache. Further, the User ID could change after an org migration and will require recreation of shares with the new ID.
  • Documents have to be shared individually by creating a record share for each document. This could pose scalability problems when we are dealing with a large number of documents.
Knowledge-artikkelin numero

000393095

 
Ladataan
Salesforce Help | Article