Apex Security 和 共享

当您使用 Apex 时, 代码的安全性至关重要。您需要为 Apex 类添加用户权限并强制执行 共享规则。请继续阅读,了解 Apex 托管共享和 获取一些安全提示。

  • 强制执行共享规则
  • 强制执行对象和字段权限
  • 对数据库操作
    强制实施用户模式 通过使用带有特殊关键字的 SOQL 或 SOSL 查询,或者使用 DML 方法重载,可以在用户模式下运行数据库操作,而不是在默认系统模式下运行数据库操作。
  • 使用 stripInaccessible 方法强制实施安全性 使用该方法
    强制实施字段级和对象级数据保护。此方法可用于从用户无法访问的查询和子查询结果中删除字段和关系字段。该方法还可用于在 DML 操作之前删除无法访问的 sObject 字段,以避免异常,并清理已从不受信任的源反序列化的 sObject。stripInaccessible
  • 使用 WITH 筛选 SOQL 查询
    SECURITY_ENFORCED 使用该子句可以对 Apex 代码中的查询(包括子查询和跨对象关系)启用字段级和对象级安全权限检查。WITH SECURITY_ENFORCEDSOQL SELECT
  • 类安全性
  • 了解 Apex 托管共享 共享
    是授予用户或用户组对记录或一组记录执行一组操作的权限的行为。可以使用 Salesforce 用户界面和 Lightning Platform 授予共享访问权限,也可以使用 Apex 以编程方式授予共享访问权限。
  • Apex 和 Visualforce 开发的安全提示

强制执行共享规则

Apex 通常在系统上下文中运行;即当前用户的权限和 在代码执行过程中不考虑字段级安全性。共享规则, 但是,并不总是被绕过:必须使用关键字声明类,以确保共享 不强制执行规则。without sharing

注意

通过调用执行的 Apex 代码和 Apex 中的 Connect 始终执行 使用当前用户的共享规则。有关 的详细信息,请参阅匿名块。executeAnonymousexecuteAnonymous

Apex 开发人员必须注意不要无意中暴露敏感数据,而这些数据通常会暴露 通过用户权限、字段级安全性或组织范围的默认值对用户隐藏。 他们必须特别小心 Web 服务,这些服务可能会受到权限的限制。 但是,一旦启动,就要在系统上下文中执行。

大多数情况下,系统上下文为系统级操作提供正确的行为 例如需要访问组织中所有数据的触发器和 Web 服务。然而 您还可以指定特定的 Apex 类应强制执行适用的共享规则 到当前用户。

注意

使用关键字强制执行共享规则不会强制执行用户的权限和字段级安全性。Apex 总是有 访问组织中的所有字段和对象,确保代码不会运行失败 因为字段或对象对用户隐藏。with sharing

此示例有两个类,第一个类 () 强制执行共享规则,而第二个类 () 则不强制执行。该类从第一个方法调用一个方法,该方法在强制执行共享规则的情况下运行。该类包含一个内部类,其中代码 在与调用方相同的共享上下文下执行。它还包含一个扩展的类 它,它继承了它的无共享设置。CWithCWithoutCWithoutCWithout

public with sharing class CWith {
  // All code in this class operates with enforced sharing rules.

  Account a = [SELECT . . . ];

  public static void m() { . . . }
  
  static {
    . . .
  }

  {
    . . .
  }

  public void c() {
    . . .
  } 
}

public without sharing class CWithout {
  // All code in this class ignores sharing rules and operates 
  // as if the context user has the Modify All Data permission.
  Account a = [SELECT . . . ];
  . . .

  public static void m() {  
     . . . 

    // This call into CWith operates with enforced sharing rules
    // for the context user. When the call finishes, the code execution 
    // returns to without sharing mode.
    CWith.m();
  }


  public class CInner {
    // All code in this class executes with the same sharing context
    // as the code that calls it. 
    // Inner classes are separate from outer classes.
    . . .

    // Again, this call into CWith operates with enforced sharing rules
    // for the context user, regardless of the class that initially called this inner class.
    // When the call finishes, the code execution returns to the sharing mode that was used to call this inner class.
    CWith.m();
  }

  public class CInnerWithOut extends CWithout {
    // All code in this class ignores sharing rules because
    // this class extends a parent class that ignores sharing rules.
  }
}

警告

因为声明为 的类可以调用声明为 的类,您可能仍然必须实现类级安全性。此外,所有 使用 Pricebook2 的 SOQL 和 SOSL 查询将忽略该关键字。无论应用的共享如何,都将返回所有价目表 规则。with sharingwithout sharingwith sharing强制执行当前用户的共享规则可能会影响:

  • SOQL 和 SOSL 查询。查询返回的行数可能少于在系统中操作的行数 上下文。
  • DML 操作。操作可能会失败,因为当前用户没有正确的 权限。例如,如果用户指定了 组织,但当前用户无权访问。

强制执行对象和字段权限

Apex 通常在系统上下文中运行;即当前用户的权限和 在代码执行过程中不考虑字段级安全性。共享 但是,规则并不总是被绕过:必须使用关键字声明类,以确保 不强制执行共享规则。与调用一起执行的 Apex 代码,并且始终在 Apex 中连接 使用当前用户的共享规则执行。有关 的详细信息,请参阅匿名块。without sharingexecuteAnonymousexecuteAnonymous

要强制运行用户的字段级安全性 (FLS) 和对象权限,请您 可以指定数据库操作的用户模式访问权限。请参阅对数据库操作强制使用用户模式。你 还可以使用 WITH SECURITY_ENFORCED在 SOQL 查询中强制执行这些权限。 有关详细信息,请参阅使用 WITH SECURITY_ENFORCED筛选 SOQL 查询。

还可以通过显式方式在代码中强制实施对象级和字段级权限 调用 sObject describe 结果方法(Schema.DescribeSObjectResult)和字段 describe 结果方法(Schema.DescribeFieldResult),用于检查 当前用户的访问权限级别。这样,您可以验证当前用户是否 具有必要的权限,并且只有当他或她具有足够的权限时,您才能 ,然后执行特定的 DML 操作或查询。

例如,您可以调用 、 或 方法来验证 当前用户分别具有对 sObject 的读取、创建或更新访问权限。同样,暴露这些 您可以调用这些方法检查当前用户的 read、create、or 更新字段的访问权限。此外,还可以调用 提供的方法来检查当前用户是否具有权限 删除特定的 sObject。isAccessibleisCreateableisUpdateableSchema.DescribeSObjectResultSchema.DescribeFieldResultisDeletableSchema.DescribeSObjectResult

这些示例调用访问控制方法。在更新之前检查联系人电子邮件字段的字段级更新权限 它:

if (Schema.sObjectType.Contact.fields.Email.isUpdateable()) {
   // Update contact phone number
}

要在创建 新增功能 联系:

if (Schema.sObjectType.Contact.fields.Email.isCreateable()) {
   // Create new contact
}

在查询之前检查联系人电子邮件字段的字段级读取权限 这 田:

if (Schema.sObjectType.Contact.fields.Email.isAccessible()) {
   Contact c = [SELECT Email FROM Contact WHERE Id= :Id];
}

要在删除 联系:

if (Schema.sObjectType.Contact.isDeletable()) {
   // Delete contact
}

共享规则不同于对象级和字段级权限。他们可以 共存。如果在 Salesforce 中定义了共享规则,则可以在课堂上强制执行这些规则 level 通过使用关键字声明类。有关更多信息,请参阅使用共享关键字、不共享关键字和继承共享关键字。如果调用 sObject 描述结果和字段 描述结果访问控制方法,对象和字段级别的验证 除了有效的共享规则之外,还会执行权限。有时 共享规则授予的访问级别可能与对象级别或 字段级权限。with sharing

考虑

  • 启用了 Experience Cloud 站点的组织提供了各种设置来隐藏 对其他用户提供用户的个人信息(请参阅对外部隐藏个人信息 用户并在内共享个人联系信息 Experience Cloud 站点)。即使在 Apex 中也不会强制执行这些设置 具有子句或方法等安全功能。隐藏用户的特定字段 对象,请按照遵守用户个人中概述的示例代码进行操作 信息可见性设置。WITH SECURITY_ENFORCEDstripInaccessible
  • 自动化流程用户无法在自定义代码中执行对象和 FLS 检查 除非将适当的权限集显式应用于这些用户。

对数据库操作强制实施用户模式

您可以在用户模式下运行数据库操作,而不是在默认系统模式下运行 通过使用带有特殊关键字的 SOQL 或 SOSL 查询或使用 DML 方法 重载。

默认情况下,Apex 代码在系统模式下运行,这意味着它以实质 提升了对运行代码的用户的权限。增强 Apex,您可以为数据库操作指定用户模式访问。字段级安全性 (FLS) 和正在运行的用户的对象权限在用户模式下受到尊重,这与 系统模式。用户模式始终应用共享规则,但在系统模式下,它们 通过在类上共享关键字来控制。请参阅使用有共享、无共享和继承共享关键字。您可以通过使用 或 在您的 SOQL 或 SOSL 查询。此示例在 SOQL 中指定用户模式。

WITH USER_MODEWITH SYSTEM_MODE

List<Account> acc = [SELECT Id FROM Account WITH USER_MODE];

注意

此功能在启用了该功能的临时组织中可用。如果未启用该功能, 具有此功能的 Apex 代码可以编译,但不能执行。ApexUserModeWithPermset

Salesforce 建议您通过使用而不是因为这些额外的优势来强制实施字段级安全性 (FLS)。WITH USER_MODEWITH SECURITY-ENFORCED

  • WITH USER_MODE考虑多态字段 like 和 .OwnerTask.whatId
  • WITH USER_MODE处理 SOQL 语句中的所有子句,包括子句。SELECTWHERE
  • WITH USER_MODE查找 SOQL 中的所有 FLS 错误 查询,而只查找 第一个错误。此外,在用户模式下,可以使用 QueryException 上的方法检查完整的 访问错误集。WITH SECURITY ENFORCEDgetInaccessibleFields()

数据库操作可以指定用户模式或系统模式。本示例插入一个新的 用户中的帐户 模式。

Account acc = new Account(Name='test');
insert as user acc;

该类表示 哪个 Apex 运行数据库操作。使用此类将执行模式定义为 user 模式或系统模式。数据库中的可选参数 和 搜索方法 指定该方法是在系统模式 () 还是用户模式 () 下运行。使用这些重载方法可以 执行 DML 和查询操作。AccessLevelaccessLevelAccessLevel.SYSTEM_MODEAccessLevel.USER_MODE

  • Database.query 方法。请参阅动态 SOQL。
  • Database.getQueryLocator 方法
  • Database.countQuery 方法
  • Search.query 方法
  • 数据库 DML 方法(insert update upsert merge delete undelete convertLead)
    • 包括 和 方法,例如 和 。*Immediate*AsyncinsertImmediatedeleteAsync

注意

当数据库 DML 方法与 一起运行时,您可以通过 访问错误。使用 ,您可以使用该方法通过 FLS 获取字段 错误。AccessLevel.USER_MODESaveResult.getErrors().getFields()insert as userDMLExceptiongetFieldNames()

这些方法需要该参数。accessLevel

  • Database.queryWithBinds
  • Database.getQueryLocatorWithBinds
  • Database.countQueryWithBinds

使用权限集在 DML 和搜索操作中强制实施安全性(开发人员 预览)

在开发者预览版中,您可以指定用于扩充字段级别的权限集 以及数据库和搜索操作的对象级安全性。使用 指定的权限集 ID。 执行的特定用户模式 DML 操作 有了这个,请尊重 除了运行用户的权限外,还设置了指定的权限集。AccessLevel.withPermissionSetId()AccessLevel此示例运行该方法 具有指定的权限集并插入自定义 对象。

AccessLevel.withPermissionSetId()

@isTest
public with sharing class ElevateUserModeOperations_Test {
    @isTest
    static void objectCreatePermViaPermissionSet() {
        Profile p = [SELECT Id FROM Profile WHERE Name='Minimum Access - Salesforce'];
        User u = new User(Alias = 'standt', Email='standarduser@testorg.com',
            EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US',
            LocaleSidKey='en_US', ProfileId = p.Id,
            TimeZoneSidKey='America/Los_Angeles',
            UserName='standarduser' + DateTime.now().getTime() + '@testorg.com');

        System.runAs(u) {
            try { 
                Database.insert(new Account(name='foo'), AccessLevel.User_mode); 
                Assert.fail(); 
            } catch (SecurityException ex) { 
                Assert.isTrue(ex.getMessage().contains('Account'));
            }
            //Get ID of previously created permission set named 'AllowCreateToAccount'
            Id permissionSetId = [Select Id from PermissionSet 
                where Name = 'AllowCreateToAccount' limit 1].Id;

            Database.insert(new Account(name='foo'), AccessLevel.User_mode.withPermissionSetId(permissionSetId)); 

            // The elevated access level is not persisted to subsequent operations
            try { 
                Database.insert(new Account(name='foo2'), AccessLevel.User_mode); 
                Assert.fail(); 
            } catch (SecurityException ex) { 
                Assert.isTrue(ex.getMessage().contains('Account')); 
            } 
            
        } 
    } 
}

注意

Checkmarx,AppExchange Security Review 源代码 扫描仪,尚未使用此新的 Apex 功能进行更新。在更新之前, Checkmarx可以针对现场或对象级安全违规行为生成误报 需要异常文档。

使用 stripInaccessible 方法强制实施安全性

使用该方法强制执行 字段级和对象级数据保护。此方法可用于剥离字段和 用户无法访问的查询和子查询结果中的关系字段。该方法可以 也用于在 DML 操作之前删除无法访问的 sObject 字段,以避免异常和 清理已从不受信任的来源反序列化的 sObject。

stripInaccessible

重要

在可能的情况下,我们更改了非包容性条款,以与我们公司保持一致 平等的价值。我们保留了某些条款,以避免对客户产生任何影响 实现。

字段级和对象级数据保护可通过 Security 和 SObjectAccessDecision 类进行访问。访问检查是 基于当前用户在指定上下文中的字段级权限 操作 – 创建、读取、更新或更新插入。Security.stripInaccessible() 方法检查源代码 不符合当前用户的字段级安全检查的字段的记录。这 方法还检查源记录中查找或主从关系字段到的字段 当前用户无权访问。该方法创建 sObjects 的返回列表,该列表是 与源记录相同,只是当前无法访问的字段 用户被删除。该方法返回的 sObject 包含的记录与方法参数中的 sObject 顺序相同。getRecordssourceRecordsstripInaccessible

作为开发人员预览版功能,将权限集 ID 作为参数并强制执行 根据指定的权限集进行字段级和对象级访问,此外 运行用户的权限。Security.stripInaccessible()

注意

该方法永远不会剥离 ID 字段,以避免在对结果执行 DML 时出现问题。stripInaccessible

要识别已删除的不可访问字段,可以使用 SObject.isSet() 方法。例如,返回列表 包含 Contact 对象和无法访问social_security_number__c自定义字段 用户。由于此自定义字段未通过字段级访问检查,因此未设置该字段 并返回 .isSetfalse

SObjectAccessDecision securityDecision = Security.stripInaccessible(AccessType.READABLE, sourceRecords);
Contact c = securityDecision.getRecords()[0];
System.debug(c.isSet('social_security_number__c')); // prints "false"

注意

该方法不支持 AggregateResult SObject。如果源记录为 AggregateResult SObject 类型,则 引发异常。stripInaccessible

对 User 对象强制执行对象和字段权限并隐藏 具有 Experience Cloud 站点的组织中其他用户的用户个人信息,请参阅强制执行对象和字段权限。

以下是可以使用该方法的一些示例。stripInaccessible

此示例代码从查询结果中删除无法访问的字段。展示台 对于广告系列数据,必须始终显示 . 必须仅向具有 读取该字段的权限。

BudgetedCostActualCost

SObjectAccessDecision securityDecision = 
         Security.stripInaccessible(AccessType.READABLE,
                 [SELECT Name, BudgetedCost, ActualCost FROM Campaign]                 );

    // Construct the output table
    if (securityDecision.getRemovedFields().get('Campaign').contains('ActualCost')) {
        for (Campaign c : securityDecision.getRecords()) {
        //System.debug Output: Name, BudgetedCost
        }
    } else {
        for (Campaign c : securityDecision.getRecords()) {
        //System.debug Output: Name, BudgetedCost, ActualCost
        }
}

此示例代码从子查询结果中删除不可访问的字段。用户 无权读取 接触 对象。

Phone

List<Account> accountsWithContacts =
	[SELECT Id, Name, Phone,
	    (SELECT Id, LastName, Phone FROM Account.Contacts)
	FROM Account];
  
   // Strip fields that are not readable
   SObjectAccessDecision decision = Security.stripInaccessible(
	                                   AccessType.READABLE,
	                                   accountsWithContacts);
 
// Print stripped records
   for (Integer i = 0; i < accountsWithContacts.size(); i++) 
  {
      System.debug('Insecure record access: '+accountsWithContacts[i]);
      System.debug('Secure record access: '+decision.getRecords()[i]);
   }
 
// Print modified indexes
   System.debug('Records modified by stripInaccessible: '+decision.getModifiedIndexes());
 
// Print removed fields
   System.debug('Fields removed by stripInaccessible: '+decision.getRemovedFields());

此示例代码在 DML 操作之前从 sObject 中删除不可访问的字段。这 无权为帐户创建评级的用户仍然可以创建帐户。 该方法确保未设置 Rating,并且不会引发异常。

List<Account> newAccounts = new List<Account>();
Account a = new Account(Name='Acme Corporation');
Account b = new Account(Name='Blaze Comics', Rating=’Warm’);
newAccounts.add(a);
newAccounts.add(b);

SObjectAccessDecision securityDecision = Security.stripInaccessible(
                                         AccessType.CREATABLE, newAccounts);

// No exceptions are thrown and no rating is set
insert securityDecision.getRecords();

System.debug(securityDecision.getRemovedFields().get('Account')); // Prints "Rating"
System.debug(securityDecision.getModifiedIndexes()); // Prints "1"

此示例代码清理已从不受信任的 sObject 反序列化的 sObject 源。用户无权更新 帐户。

AnnualRevenue

String jsonInput =
'[' +
'{' +
'"Name": "InGen",' +
'"AnnualRevenue": "100"' +
'},' +
'{' +
'"Name": "Octan"' +
'}' +
']';

List<Account> accounts = (List<Account>)JSON.deserializeStrict(jsonInput, List<Account>.class);
SObjectAccessDecision securityDecision = Security.stripInaccessible(
                                         AccessType.UPDATABLE, accounts);

// Secure update
update securityDecision.getRecords(); // Doesn’t update AnnualRevenue field
System.debug(String.join(securityDecision.getRemovedFields().get('Account'), ', ')); // Prints "AnnualRevenue"
System.debug(String.join(securityDecision.getModifiedIndexes(), ', ')); // Prints "0”

此示例代码从查询结果中删除不可访问的关系字段。这 用户无权插入字段,该字段是从 MyCustomObject__c 到 帐户。

Account__c

// Account__c is a lookup from MyCustomObject__c to Account
@IsTest
   public class TestCustomObjectLookupStripped {
      @IsTest static void caseCustomObjectStripped() {
         Account a = new Account(Name='foo');
         insert a;
         List<MyCustomObject__c> records = new List<MyCustomObject__c>{
            new MyCustomObject__c(Name='Custom0', Account__c=a.id)
         };
         insert records;
         records = [SELECT Id, Account__c FROM MyCustomObject__c];
         SObjectAccessDecision securityDecision = Security.stripInaccessible
                                                  (AccessType.READABLE, records);
         
         // Verify stripped records
         System.assertEquals(1, securityDecision.getRecords().size());
         for (SObject strippedRecord : securityDecision.getRecords()) {
             System.debug('Id should be set as Id fields are ignored: ' + 
                           strippedRecord.isSet('Id')); // prints true
             System.debug('Lookup field FLS is not READABLE to running user, 
                           should not be set: ' +
                           strippedRecord.isSet('Account__c')); // prints false
         }
      }
   }

使用 WITH SECURITY_ENFORCED筛选 SOQL 查询

使用子句启用 对 Apex 代码中的查询(包括子查询和跨对象)进行字段级和对象级安全权限检查 关系。

WITH SECURITY_ENFORCEDSOQL SELECT

Apex 通常在系统上下文中运行;即当前用户的权限和 在代码执行过程中不考虑字段级安全性。共享规则, 但是,并不总是被绕过:必须使用关键字声明类,以确保共享 不强制执行规则。尽管执行字段和对象级安全检查是 在早期版本中,此子句可能大大减少了冗长和技术性 查询操作的复杂性。此功能专为 Apex 开发人员量身定制,他们拥有最少的 具有安全性和优雅降级的应用程序的开发经验 权限错误不是必需的。without sharing

注意

该条款仅 在 Apex 中可用。我们不建议在 Apex 类或触发器中使用早于 API 的版本 45.0.WITH SECURITY_ENFORCEDWITH SECURITY_ENFORCED

WITH SECURITY_ENFORCED应用字段和 对象级安全检查仅对 SOQL 子句中引用的字段和对象进行检查 而不是像 或 这样的子句。换言之,安全性是针对查询返回的内容强制实施的,而不是针对进入的所有元素 运行查询。SELECTFROMWHEREORDER BYSOQL SELECT插入子句:

WITH SECURITY_ENFORCED

  • 如果存在,则在子句之后,否则在子句之后。WHEREFROM
  • 在任何 、 、 或聚合函数之前 第。ORDER BYLIMITOFFSET

有关查询的详细信息,请参阅 SOQL 和 SOSL 中的 SOQL SELECT 语法 参考。

SOQL SELECT例如,如果用户具有 LastName 的字段访问权限,则此查询将返回 Id 和 LastName 用于 Acme 帐户条目。

List<Account> act1 = [SELECT Id, (SELECT LastName FROM Contacts)
   FROM Account WHERE Name like 'Acme' WITH SECURITY_ENFORCED]

使用 查询多态查找字段时存在一些限制。多态场是关系 可以指向多个实体的字段。

WITH SECURITY_ENFORCED

  • 在使用 的查询中不支持遍历多态字段的关系。例如,您不能在此查询中使用 返回 User 和 Calendar 实体的 Id 和 Owner 名称:。WITH SECURITY_ENFORCEDWITH SECURITY_ENFORCEDSELECT Id, What.Name FROM Event WHERE What.Type IN (’User’,’Calendar’)
  • 在使用 的查询中不支持将表达式与子句一起使用。 在 SELECT 查询中用于指定要为 给定多态关系的类型。例如,不能在此查询中使用。查询 指定要为 Account 和 Opportunity 对象返回的某些字段,以及 Name 和 要为所有其他对象返回的电子邮件字段。TYPEOFELSEWITH SECURITY_ENFORCEDTYPEOFWITH SECURITY_ENFORCEDSELECT TYPE OF What WHEN Account THEN Phone WHEN Opportunity THEN Amount ELSE Name,Email END FROM Event
  • 、 和 多态查找字段不受此限制,并且允许多态 关系遍历。OwnerCreatedByLastModifiedBy
  • 对于 AppExchange Security Review,在使用 时必须使用 API 版本 48.0 或更高版本。不能使用 API 该功能处于测试版或试点阶段的版本。WITH SECURITY_ENFORCED

如果查询中引用的任何字段或对象无法访问 用户,a 被抛出,没有 返回数据。SOQL SELECTWITH SECURITY_ENFORCEDSystem.QueryException

对 User 对象强制实施对象和字段权限并隐藏用户的个人 来自具有 Experience Cloud 站点的组织中其他用户的信息,请参阅强制执行对象和字段权限。

如果隐藏了 LastName 或 Description 的字段访问权限,则此查询将引发 指示权限不足的异常。

List<Account> act1 = [SELECT Id, (SELECT LastName FROM Contacts), 
   (SELECT Description FROM Opportunities)
   FROM Account WITH SECURITY_ENFORCED]

如果“网站”的字段访问处于隐藏状态,则此查询将引发异常,指示 权限不足。

List<Account> act2 = [SELECT Id, parent.Name, parent.Website 
   FROM Account WITH SECURITY_ENFORCED]

如果隐藏了 Type 的字段访问,则此聚合函数查询将引发异常 表示权限不足。

List<AggregateResult> agr1 = [SELECT GROUPING(Type) 
   FROM Opportunity WITH SECURITY_ENFORCED 
   GROUP BY Type]

类安全性

您可以指定哪些用户可以根据其 用户配置文件或权限集。您只能在 Apex 类上设置安全性,而不能在触发器上设置安全性。

要从类列表页面设置 Apex 类安全性,请参阅从类列表设置 Apex 类访问权限 页

要从类详细信息页面设置 Apex 类安全性,请参阅从类列表中设置 Apex 类访问权限 页

要从权限集设置 Apex 类安全性,请执行以下操作:

  1. 在“设置”中,输入“快速” “查找”框,然后选择“权限集”。Permission Sets
  2. 选择权限集。
  3. 单击 Apex 类访问
  4. 单击编辑
  5. 从“可用的 Apex 类”列表中选择要启用的 Apex 类,然后 单击“添加”,或从 “已启用 Apex 类”列表,然后单击“删除”。
  6. 点击保存

要从配置文件设置 Apex 类安全性,请执行以下操作:

  1. 在“设置”中,在“快速查找”框中输入, ,然后选择配置文件Profiles
  2. 选择配置文件。
  3. 在“Apex 类访问”页面或相关列表中,单击“编辑”。
  4. 从“可用的 Apex 类”列表中选择要启用的 Apex 类,然后 单击“添加”,或从 “已启用 Apex 类”列表,然后单击“删除”。
  5. 点击保存

了解 Apex 托管共享

共享是授予用户或用户组权限的行为 对一条记录或一组记录执行一组操作。可以使用以下命令授予共享访问权限 Salesforce 用户界面和 Lightning Platform,或以编程方式使用 顶点。

有关共享的详细信息,请参阅设置内部组织范围的共享 Salesforce 联机帮助中的默认值。

  • 了解共享 共享
    支持对所有自定义对象以及许多标准对象(如客户、联系人、商机和案例)进行记录级访问控制。管理员首先设置对象的组织范围默认共享访问级别,然后根据记录所有权、角色层次结构、共享规则和手动共享授予其他访问权限。然后,开发人员可以使用 Apex 托管共享以编程方式授予 Apex 的额外访问权限。
  • 使用 Apex 共享记录
  • 重新计算 Apex 托管共享

了解共享

共享支持对所有自定义对象进行记录级访问控制,因为 以及许多标准对象(例如 Account、Contact、Opportunity 和 Case)。 管理员首先设置对象的组织范围默认共享访问级别, 然后根据记录所有权、角色层次结构、共享授予其他访问权限 规则和手动共享。然后,开发人员可以使用 Apex 托管共享来授予其他 使用 Apex 以编程方式访问。

记录的大多数共享都维护在相关的共享对象中,类似于访问 在其他平台中找到的控制列表 (ACL)。

共享类型

Salesforce 具有以下类型的共享:托管共享托管共享涉及 Lightning 平台授予的共享访问权限 基于记录所有权、角色层次结构和共享规则:记录所有权每条记录都由一个用户拥有,或者由一个队列(可选)拥有 自定义对象、案例和潜在客户。记录 所有者被自动授予完全访问权限, 允许他们查看、编辑、传输、共享和删除 记录。角色层次结构角色层次结构允许上述用户 层次结构中的另一个用户具有相同级别的 访问以下用户拥有或与用户共享的记录。 因此,角色中高于记录所有者的用户 层次结构也被隐式授予对 记录,但可以禁用此行为 特定的自定义对象。角色层次结构不是 通过共享记录进行维护。相反,角色层次结构 访问是在运行时派生的。有关更多信息,请参阅 。 “使用层次结构控制访问”中的 Salesforce 联机帮助。共享规则管理员使用共享规则来 自动授予给定组或角色中的用户权限 访问特定用户组拥有的记录。无法将共享规则添加到 package,不能用于支持共享逻辑 对于从 AppExchange 安装的应用程序。

共享 规则可以基于记录所有权或其他 标准。您不能使用 Apex 创建 基于条件的共享规则。此外,基于标准 无法使用 Apex 测试共享。

Force.com 托管共享添加的所有隐式共享都不能是 使用 Salesforce 用户界面、SOAP API 直接更改,或者 顶点。用户管理共享,也称为手动共享用户管理的共享允许记录所有者或具有“完整”的任何用户 访问记录以与用户或用户组共享记录。 对于单个记录,这通常由最终用户完成。只有 授予记录所有者和角色层次结构中所有者之上的用户 对记录的完全访问权限。无法授予其他用户“完全”权限 访问。具有“全部修改”对象级别的用户 给定对象或“修改所有数据”的权限 权限也可以手动共享记录。用户管理的共享是 当记录所有者发生更改或在 共享不会授予对象以外的其他访问权限 组织范围的共享默认访问级别。Apex 托管共享Apex 托管共享使开发人员能够支持 以编程方式满足应用程序的特定共享要求 通过 Apex 或 SOAP API。这种类型的 共享类似于托管共享。仅限具有 “修改所有数据”权限可以添加或更改 Apex 记录上的托管共享。维护 Apex 托管共享 跨记录所有者更改。

注意

Apex 共享原因和 Apex 托管 共享重新计算仅适用于自定义对象。

分享原因 田

在 Salesforce 用户界面,自定义对象上的“原因”字段 指定用于记录的共享类型。此字段在 Apex 或 API 中调用。rowCause每个 以下列表项是一种用于记录的共享类型。这些表显示“原因”字段值和相关值。

rowCause

  • 托管共享Reason Field ValuerowCause Value (Used in Apex or the API)Account SharingImplicitChildAssociated record owner or sharingImplicitParentOwnerOwnerOpportunity TeamTeamSharing RuleRule区域分配规则TerritoryRule
  • 用户管理共享原因字段价值rowCause值(在 Apex 或 API 中使用)手动共享Manual领地手册TerritoryManual注意使用企业 API 版本 45.0 及更高版本中的 Territory Management 取代了 .Territory2AssociationManualTerritoryManual
  • Apex 托管共享原因字段价值rowCause值(在 Apex 或 API 中使用)由开发人员定义由开发人员定义

Apex 托管共享的显示原因由 开发 人员。

访问 水平

在确定用户对 记录,则使用最宽松的访问级别。大多数共享对象都支持 以下访问级别:

访问级别API 名称描述
私人没有只有记录所有者和记录所有者之上的用户 角色层次结构可以查看和编辑记录。仅限此访问级别 应用于 AccountShare 对象。
只读指定的用户或组只能查看记录。
读/写编辑指定的用户或组可以查看和编辑记录。
完全访问权限指定的用户或组可以查看、编辑、传输、共享和 删除记录。注意此访问级别只能授予 托管共享。

分享注意事项

Apex 触发器和用户记录共享如果触发器更改了记录的所有者,则正在运行的用户必须已读取 如果触发器是通过 以后:

  • 应用程序接口
  • 标准用户界面
  • 标准 Visualforce 控制器
  • 使用关键字定义的类with sharing

如果触发器是通过未使用关键字定义的类启动的,则触发器将在 系统模式。在这种情况下,触发器不需要正在运行的用户 具有特定访问权限。with sharing

使用 Apex 共享记录

重要

在可能的情况下,我们更改了非包容性条款,以符合我们的 平等的公司价值观。我们保留了某些条款,以避免对 客户实施。

若要以编程方式访问共享,必须使用与 要共享的标准对象或自定义对象。例如,AccountShare 是 Account 对象的共享对象,ContactShare 是 Contact 对象。此外,所有自定义对象共享对象的命名方式如下: 其中 是自定义对象的名称:MyCustomObject

MyCustomObject__Share

主从关系的细节端的对象没有关联的 共享对象。明细记录的访问权限由主节点的 共享对象和关系的共享设置。有关更多信息,请参阅 。 Salesforce 联机帮助中的“自定义对象安全性”。

共享对象包括支持所有三种共享类型的记录:托管共享、 用户托管共享和 Apex 托管共享。隐式授予用户的共享 通过组织范围的默认值、角色层次结构和权限,例如 给定的“查看全部”和“全部修改”权限 对象,“查看所有数据”和“修改所有数据”不是 使用此对象进行跟踪。

每个共享对象都具有以下属性:

属性名称描述
objectNameAccessLevel已授予指定用户或组的访问级别 用于共享 sObject。属性的名称将追加到对象 名字。例如,LeadShare 对象的属性名称为 。有效值为:AccessLevelLeadShareAccessLevelEditReadAll注意访问级别 是内部值,不能授予。All此字段必须是 设置为高于组织的访问级别 父对象的默认访问级别。有关详细信息,请参阅了解共享。
ParentID对象的 ID。此字段无法更新。
RowCause向用户或组授予访问权限的原因。原因 确定共享类型,控制谁可以更改共享 记录。此字段无法更新。
UserOrGroupId要向其授予访问权限的用户或组 ID。组可以是:与角色关联的公共组或共享组。区域组。此字段无法更新。注意您无法授予对 使用 Apex 的未经身份验证的来宾用户。

您可以与用户或组共享标准或自定义对象。更多信息 有关可以与之共享对象的用户和组的类型,请参阅对象参考中的“用户和组” Salesforce的。

使用创建用户管理的共享 顶点

可以使用 Apex 或 SOAP 手动将记录共享给用户或组 应用程序接口。如果记录的所有者发生更改,则会自动删除共享。这 以下示例类包含一个共享作业指定的作业的方法 具有指定用户 ID 的 ID 或具有读取访问权限的组 ID。它还包括一个测试 验证此方法的方法。在保存此示例类之前,请创建一个 名为 Job 的自定义对象。

注意

默认情况下,使用 Apex 编写的手动共享包含。只 当所有权发生更改时,将删除具有此条件的共享。RowCause=”Manual”

public class JobSharing {
   
   public static boolean manualShareRead(Id recordId, Id userOrGroupId){
      // Create new sharing object for the custom object Job.
      Job__Share jobShr  = new Job__Share();
   
      // Set the ID of record being shared.
      jobShr.ParentId = recordId;
        
      // Set the ID of user or group being granted access.
      jobShr.UserOrGroupId = userOrGroupId;
        
      // Set the access level.
      jobShr.AccessLevel = 'Read';
        
      // Set rowCause to 'manual' for manual sharing.
      // This line can be omitted as 'manual' is the default value for sharing objects.
      jobShr.RowCause = Schema.Job__Share.RowCause.Manual;
        
      // Insert the sharing record and capture the save result. 
      // The false parameter allows for partial processing if multiple records passed 
      // into the operation.
      Database.SaveResult sr = Database.insert(jobShr,false);

      // Process the save results.
      if(sr.isSuccess()){
         // Indicates success
         return true;
      }
      else {
         // Get first save result error.
         Database.Error err = sr.getErrors()[0];
         
         // Check if the error is related to trival access level.
         // Access level must be more permissive than the object's default.
         // These sharing records are not required and thus an insert exception is acceptable. 
         if(err.getStatusCode() == StatusCode.FIELD_FILTER_VALIDATION_EXCEPTION  &&  
                  err.getMessage().contains('AccessLevel')){
            // Indicates success.
            return true;
         }
         else{
            // Indicates failure.
            return false;
         }
       }
   }
   
}
@isTest
private class JobSharingTest {
   // Test for the manualShareRead method
   static testMethod void testManualShareRead(){
      // Select users for the test.
      List<User> users = [SELECT Id FROM User WHERE IsActive = true LIMIT 2];
      Id User1Id = users[0].Id;
      Id User2Id = users[1].Id;
   
      // Create new job.
      Job__c j = new Job__c();
      j.Name = 'Test Job';
      j.OwnerId = user1Id;
      insert j;    
                
      // Insert manual share for user who is not record owner.
      System.assertEquals(JobSharing.manualShareRead(j.Id, user2Id), true);
   
      // Query job sharing records.
      List<Job__Share> jShrs = [SELECT Id, UserOrGroupId, AccessLevel, 
         RowCause FROM job__share WHERE ParentId = :j.Id AND UserOrGroupId= :user2Id];
      
      // Test for only one manual share on job.
      System.assertEquals(jShrs.size(), 1, 'Set the object\'s sharing model to Private.');
      
      // Test attributes of manual share.
      System.assertEquals(jShrs[0].AccessLevel, 'Read');
      System.assertEquals(jShrs[0].RowCause, 'Manual');
      System.assertEquals(jShrs[0].UserOrGroupId, user2Id);
      
      // Test invalid job Id.
      delete j;   
   
      // Insert manual share for deleted job id. 
      System.assertEquals(JobSharing.manualShareRead(j.Id, user2Id), false);
   }  
}

重要

对象的组织范围默认值 访问级别不得设置为最宽松的访问级别。对于定制 对象,此级别为公共读/写。有关详细信息,请参阅了解共享。

创建 Apex 托管共享

Apex 托管共享使开发人员能够以编程方式操作共享,以 通过 Apex 或 SOAP API 支持其应用程序的行为。此类型 的共享类似于托管共享。仅具有“全部修改”的用户 “数据”权限可以在记录上添加或更改 Apex 托管共享。顶点 在记录所有者更改后保留托管共享。

Apex 托管共享必须使用 Apex 共享原因Apex 共享原因是开发人员的一种方式 跟踪他们与用户或用户组共享记录的原因。使用多个 Apex 共享原因简化了进行更新和 删除共享记录。它们还使开发人员能够与相同的 用户或组多次使用不同的原因。Apex 共享原因在对象的详细信息页面上定义。每个 Apex 共享原因 具有标签和名称:

  • 查看时,标签将显示在“原因”列中 用户界面中记录的共享。此标签允许用户和 管理员了解共享的来源。标签也是 启用通过 Translation Workbench 进行翻译。
  • 在 API 和 Apex 中引用原因时,将使用该名称。

所有 Apex 共享原因名称都采用以下格式:

MyReasonName__c

Apex 共享原因可以通过编程方式引用为 遵循:

Schema.CustomObject__Share.rowCause.SharingReason__c

例如,对于名为 Job 的对象,名为 Recruiter 的 Apex 共享原因可以是 引用为 遵循:

Schema.Job__Share.rowCause.Recruiter__c

有关更多信息,请参见 System.Schema 类。要创建 Apex 共享原因,请执行以下操作:

  1. 在自定义对象的管理设置中,单击“Apex 共享原因”中的“新建”相关 列表。
  2. 输入 Apex 共享原因的标签。查看记录的共享时,标签将显示在“原因”列中 在用户界面中。该标签还可通过 翻译工作台。
  3. 输入 Apex 共享原因的名称。引用时使用该名称 API 和 Apex 中的原因。此名称只能包含下划线和 字母数字字符,并且在您的组织中必须是唯一的。它必须以 字母,不包含空格,不以下划线结尾,也不包含 连续两个下划线。
  4. 点击保存

注意

Apex 共享原因和 Apex 托管共享重新计算仅适用于 自定义对象。

Apex 托管共享示例

在此示例中,假设您正在构建一个招聘应用程序,并且有一个 名为 Job 的对象。您想要验证招聘人员和招聘经理是否列出了 在工作中可以访问记录。以下触发器授予招聘人员和 创建作业记录时的招聘经理访问权限。此示例需要自定义 名为 Job 的对象,其中两个查找字段与名为 Hiring_Manager和招聘人员。此外,作业自定义对象应具有两个共享 添加了称为Hiring_Manager和招聘人员的原因。

trigger JobApexSharing on Job__c (after insert) {
    
    if(trigger.isInsert){
        // Create a new list of sharing objects for Job
        List<Job__Share> jobShrs  = new List<Job__Share>();
        
        // Declare variables for recruiting and hiring manager sharing
        Job__Share recruiterShr;
        Job__Share hmShr;
        
        for(Job__c job : trigger.new){
            // Instantiate the sharing objects
            recruiterShr = new Job__Share();
            hmShr = new Job__Share();
            
            // Set the ID of record being shared
            recruiterShr.ParentId = job.Id;
            hmShr.ParentId = job.Id;
            
            // Set the ID of user or group being granted access
            recruiterShr.UserOrGroupId = job.Recruiter__c;
            hmShr.UserOrGroupId = job.Hiring_Manager__c;
            
            // Set the access level
            recruiterShr.AccessLevel = 'edit';
            hmShr.AccessLevel = 'read';
            
            // Set the Apex sharing reason for hiring manager and recruiter
            recruiterShr.RowCause = Schema.Job__Share.RowCause.Recruiter__c;
            hmShr.RowCause = Schema.Job__Share.RowCause.Hiring_Manager__c;
            
            // Add objects to list for insert
            jobShrs.add(recruiterShr);
            jobShrs.add(hmShr);
        }
        
        // Insert sharing records and capture save result 
        // The false parameter allows for partial processing if multiple records are passed 
        // into the operation 
        Database.SaveResult[] lsr = Database.insert(jobShrs,false);
        
        // Create counter
        Integer i=0;
        
        // Process the save results
        for(Database.SaveResult sr : lsr){
            if(!sr.isSuccess()){
                // Get the first save result error
                Database.Error err = sr.getErrors()[0];
                
                // Check if the error is related to a trivial access level
                // Access levels equal or more permissive than the object's default 
                // access level are not allowed. 
                // These sharing records are not required and thus an insert exception is 
                // acceptable. 
                if(!(err.getStatusCode() == StatusCode.FIELD_FILTER_VALIDATION_EXCEPTION  
                                               &&  err.getMessage().contains('AccessLevel'))){
                    // Throw an error when the error is not related to trivial access level.
                    trigger.newMap.get(jobShrs[i].ParentId).
                      addError(
                       'Unable to grant sharing access due to following exception: '
                       + err.getMessage());
                }
            }
            i++;
        }   
    }
    
}

在某些情况下,插入共享行会导致 现有共享行。请看以下示例:

  • 手动共享访问级别设置为“读取”,并插入一个新级别,设置为 写。原始共享行将更新为“写入”,指示较高的 访问级别。
  • 用户可以访问帐户,因为他们可以访问其子记录 (联系人、案例、商机等)。如果帐户共享规则是 created,共享规则行原因(这是更高的访问级别) 替换父级隐式共享行原因,指示 访问。

重要

对象的组织范围的默认访问级别必须 未设置为最宽松的访问级别。对于自定义对象,此级别为 公共读/写。有关详细信息,请参阅了解共享。

为 Customer Community Plus 用户创建 Apex 托管共享

Customer Community Plus 用户以前称为 Customer Portal 用户。共享 这些用户无法使用对象,例如 和 。如果 您必须以Customer Community Plus用户身份使用共享对象,请考虑使用一个 trigger,默认情况下使用关键字进行操作。否则,请使用具有相同 关键字使 DML 操作能够成功运行。单独的实用程序类 也可用于启用此访问。AccountShareContactSharewithout sharing

支持通过写入共享对象的手动/顶点共享授予可见性 但对象本身对Customer Community Plus用户不可用。 但是,其他用户可以添加授予对Customer Community Plus访问权限的共享 用户。

警告

启用数字体验后,角色和 通过 Apex 托管共享的下属会自动提供给角色, 内部和门户下属。要保护外部用户的访问权限,请更新您的 Apex 代码,以便创建与 Role 和 Internal Subordinates 组的共享。因为 此转换是大规模操作,请考虑使用批处理 Apex。

重新计算 Apex 托管共享

Salesforce 会自动重新计算对象上所有记录的共享,当其 组织范围的共享默认访问级别更改。重新计算添加托管 在适当的时候分享。此外,如果访问 他们授予被认为是多余的。例如,手动共享,它授予只读 对用户的访问权限,当对象的共享模型从 Private 更改时,将删除对用户的访问权限 更改为公共只读。

若要重新计算 Apex 托管共享,必须编写一个实现 Salesforce 提供的用于重新计算的界面。然后,您必须关联该类 与自定义对象一起使用,在自定义对象的详细信息页面上,在 Apex 共享中 重新计算相关列表。

注意

Apex 共享原因和 Apex 托管共享重新计算仅适用于 自定义对象。

您可以从自定义对象详细信息页面执行此类,其中 Apex 共享 指定原因。管理员可能需要重新计算 Apex 托管共享 如果锁定问题阻止 Apex 代码向用户授予访问权限,则对象为 由应用程序的逻辑定义。还可以使用 Database.executeBatch 方法以编程方式调用 Apex 托管共享重新计算。

注意

每次自定义对象的组织范围共享默认访问级别为 更新时,为关联的自定义对象定义的任何 Apex 重新计算类也会 执行。

要监视或停止 Apex 重新计算的执行,请从“设置”中输入“快速查找”框,然后 选择 Apex JobsApex Jobs

创建用于重新计算的 Apex 类 共享

要重新计算 Apex 托管共享,您必须编写一个 Apex 类来执行 重新计算。此类必须实现 Salesforce 提供的接口。Database.Batchable

该接口用于 所有批处理 Apex 进程,包括重新计算 Apex 托管共享。您可以 在组织中多次实现此接口。更多信息 有关必须实现的方法,请参阅使用 Batch Apex。Database.Batchable

在创建 Apex 托管共享重新计算类之前,还要考虑最佳做法。

重要

对象的组织范围的默认访问级别必须 未设置为最宽松的访问级别。对于自定义对象,此级别为 公共读/写。有关详细信息,请参阅了解共享。

Apex 托管共享重新计算 例

在此示例中,假设您正在构建一个 招聘应用程序,并有一个名为 Job 的对象。您想要验证这一点 招聘中列出的招聘人员和招聘经理可以访问 记录。以下 Apex 类执行此验证。此示例需要一个名为 Job 的自定义对象,其中包含两个查找字段 与名为 Hiring_Manager 和 Recruiter 的用户记录相关联。此外,工作 自定义对象应添加两个共享原因,称为 Hiring_Manager 和 招聘。在运行此示例之前,请将电子邮件地址替换为 要向其发送错误通知和作业完成的有效电子邮件地址 通知。

global class JobSharingRecalc implements Database.Batchable<sObject> {
    
    // String to hold email address that emails will be sent to. 
    // Replace its value with a valid email address.
    static String emailAddress = 'admin@yourcompany.com';
    
    // The start method is called at the beginning of a sharing recalculation.
    // This method returns a SOQL query locator containing the records 
    // to be recalculated. 
    global Database.QueryLocator start(Database.BatchableContext BC){
        return Database.getQueryLocator([SELECT Id, Hiring_Manager__c, Recruiter__c 
                                         FROM Job__c]);  
    }
    
    // The executeBatch method is called for each chunk of records returned from start.  
    global void execute(Database.BatchableContext BC, List<sObject> scope){
       // Create a map for the chunk of records passed into method.
        Map<ID, Job__c> jobMap = new Map<ID, Job__c>((List<Job__c>)scope);  
        
        // Create a list of Job__Share objects to be inserted.
        List<Job__Share> newJobShrs = new List<Job__Share>();
               
        // Locate all existing sharing records for the Job records in the batch.
        // Only records using an Apex sharing reason for this app should be returned. 
        List<Job__Share> oldJobShrs = [SELECT Id FROM Job__Share WHERE ParentId IN 
             :jobMap.keySet() AND 
            (RowCause = :Schema.Job__Share.rowCause.Recruiter__c OR
            RowCause = :Schema.Job__Share.rowCause.Hiring_Manager__c)]; 
        
        // Construct new sharing records for the hiring manager and recruiter 
        // on each Job record.
        for(Job__c job : jobMap.values()){
            Job__Share jobHMShr = new Job__Share();
            Job__Share jobRecShr = new Job__Share();
            
            // Set the ID of user (hiring manager) on the Job record being granted access.
            jobHMShr.UserOrGroupId = job.Hiring_Manager__c;
            
            // The hiring manager on the job should always have 'Read Only' access.
            jobHMShr.AccessLevel = 'Read';
            
            // The ID of the record being shared
            jobHMShr.ParentId = job.Id;
            
            // Set the rowCause to the Apex sharing reason for hiring manager.
            // This establishes the sharing record as Apex managed sharing.
            jobHMShr.RowCause = Schema.Job__Share.RowCause.Hiring_Manager__c;
            
            // Add sharing record to list for insertion.
            newJobShrs.add(jobHMShr);
            
            // Set the ID of user (recruiter) on the Job record being granted access.
            jobRecShr.UserOrGroupId = job.Recruiter__c;
            
            // The recruiter on the job should always have 'Read/Write' access.
            jobRecShr.AccessLevel = 'Edit';
            
            // The ID of the record being shared
            jobRecShr.ParentId = job.Id;
            
            // Set the rowCause to the Apex sharing reason for recruiter.
            // This establishes the sharing record as Apex managed sharing.
            jobRecShr.RowCause = Schema.Job__Share.RowCause.Recruiter__c;
            
         // Add the sharing record to the list for insertion.            
            newJobShrs.add(jobRecShr);
        }
        
        try {
           // Delete the existing sharing records.
           // This allows new sharing records to be written from scratch.
            Delete oldJobShrs;
            
           // Insert the new sharing records and capture the save result. 
           // The false parameter allows for partial processing if multiple records are 
           // passed into operation. 
           Database.SaveResult[] lsr = Database.insert(newJobShrs,false);
           
           // Process the save results for insert.
           for(Database.SaveResult sr : lsr){
               if(!sr.isSuccess()){
                   // Get the first save result error.
                   Database.Error err = sr.getErrors()[0];
                   
                   // Check if the error is related to trivial access level.
                   // Access levels equal or more permissive than the object's default 
                   // access level are not allowed. 
                   // These sharing records are not required and thus an insert exception 
                   // is acceptable. 
                   if(!(err.getStatusCode() == StatusCode.FIELD_FILTER_VALIDATION_EXCEPTION  
                                     &&  err.getMessage().contains('AccessLevel'))){
                       // Error is not related to trivial access level.
                       // Send an email to the Apex job's submitter.
                     Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
                     String[] toAddresses = new String[] {emailAddress}; 
                     mail.setToAddresses(toAddresses); 
                     mail.setSubject('Apex Sharing Recalculation Exception');
                     mail.setPlainTextBody(
                       'The Apex sharing recalculation threw the following exception: ' + 
                             err.getMessage());
                     Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
                   }
               }
           }   
        } catch(DmlException e) {
           // Send an email to the Apex job's submitter on failure.
            Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
            String[] toAddresses = new String[] {emailAddress}; 
            mail.setToAddresses(toAddresses); 
            mail.setSubject('Apex Sharing Recalculation Exception');
            mail.setPlainTextBody(
              'The Apex sharing recalculation threw the following exception: ' + 
                        e.getMessage());
            Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
        }
    }
    
    // The finish method is called at the end of a sharing recalculation.
    global void finish(Database.BatchableContext BC){  
        // Send an email to the Apex job's submitter notifying of job completion.
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        String[] toAddresses = new String[] {emailAddress}; 
        mail.setToAddresses(toAddresses); 
        mail.setSubject('Apex Sharing Recalculation Completed.');
        mail.setPlainTextBody
                      ('The Apex sharing recalculation finished processing');
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
    }
    
}

测试 Apex 托管共享 重新计算

此示例插入 5 个作业记录并调用已实现的批处理作业 在上一示例的 Batch 类中。此示例需要一个自定义对象 称为 Job,其中有两个与用户记录关联的查找字段,称为 Hiring_Manager和招聘人员。此外,作业自定义对象应具有两个共享 添加了称为Hiring_Manager和招聘人员的原因。在运行此测试之前,请将 作业到专用的组织范围默认共享。请注意,由于电子邮件 不是从测试发送的,并且因为批处理类是由测试调用的 方法,在这种情况下不会发送电子邮件通知。

@isTest
private class JobSharingTester {
   
    // Test for the JobSharingRecalc class    
    static testMethod void testApexSharing(){
       // Instantiate the class implementing the Database.Batchable interface.     
        JobSharingRecalc recalc = new JobSharingRecalc();
        
        // Select users for the test.
        List<User> users = [SELECT Id FROM User WHERE IsActive = true LIMIT 2];
        ID User1Id = users[0].Id;
        ID User2Id = users[1].Id;
        
        // Insert some test job records.                 
        List<Job__c> testJobs = new List<Job__c>();
        for (Integer i=0;i<5;i++) {
        Job__c j = new Job__c();
            j.Name = 'Test Job ' + i;
            j.Recruiter__c = User1Id;
            j.Hiring_Manager__c = User2Id;
            testJobs.add(j);
        }
        insert testJobs;
        
        Test.startTest();
        
        // Invoke the Batch class.
        String jobId = Database.executeBatch(recalc);
        
        Test.stopTest();
        
        // Get the Apex job and verify there are no errors.
        AsyncApexJob aaj = [Select JobType, TotalJobItems, JobItemsProcessed, Status, 
                            CompletedDate, CreatedDate, NumberOfErrors 
                            from AsyncApexJob where Id = :jobId];
        System.assertEquals(0, aaj.NumberOfErrors);
      
        // This query returns jobs and related sharing records that were inserted       
        // by the batch job's execute method.     
        List<Job__c> jobs = [SELECT Id, Hiring_Manager__c, Recruiter__c, 
            (SELECT Id, ParentId, UserOrGroupId, AccessLevel, RowCause FROM Shares 
            WHERE (RowCause = :Schema.Job__Share.rowCause.Recruiter__c OR 
            RowCause = :Schema.Job__Share.rowCause.Hiring_Manager__c))
            FROM Job__c];       
        
        // Validate that Apex managed sharing exists on jobs.     
        for(Job__c job : jobs){
            // Two Apex managed sharing records should exist for each job
            // when using the Private org-wide default. 
            System.assert(job.Shares.size() == 2);
            
            for(Job__Share jobShr : job.Shares){
               // Test the sharing record for hiring manager on job.             
                if(jobShr.RowCause == Schema.Job__Share.RowCause.Hiring_Manager__c){
                    System.assertEquals(jobShr.UserOrGroupId,job.Hiring_Manager__c);
                    System.assertEquals(jobShr.AccessLevel,'Read');
                }
                // Test the sharing record for recruiter on job.
                else if(jobShr.RowCause == Schema.Job__Share.RowCause.Recruiter__c){
                    System.assertEquals(jobShr.UserOrGroupId,job.Recruiter__c);
                    System.assertEquals(jobShr.AccessLevel,'Edit');
                }
            }
        }
    }
}

关联用于 重新计算

用于重新计算的 Apex 类必须与自定义对象相关联。要将 Apex 托管共享重新计算类与自定义对象相关联,请执行以下操作:

  1. 从自定义对象的管理设置中,转到 Apex Sharing 重新计算。
  2. 选择重新计算此对象的 Apex 共享的 Apex 类。 您选择的类必须实现接口。您不能关联相同的 Apex 类多次使用同一个自定义对象。Database.Batchable
  3. 点击保存

ref