雲客Drupal源碼分析之實體訪問控制處理器AccessControlHandler

實體訪問控制處理器用於判斷賬戶是否有某種實體操作權限,是整個權限系統的一部分,專門針對實體,本系列已經發布了權限系統上下集,請務必先查看再閱讀本篇內容。
   是否允許對實體執行某種操作,由該實體類型定義的訪問控制處理器來判斷,她是相對於實體類型而存在的,換句話說一個實體訪問控制器只處理該類型的實體,其屬於實體處理器,被設置在實體釋文處理器根鍵的access鍵下,採用實體處理器的實例化方法,所有實體類型都需要定義訪問控制處理器,自定義實體類型如沒有特殊邏輯可直接使用系統提供的默認基類。

使用示例代碼:
在使用上所有的實體類型遵循統一的方式,得到實體訪問控制處理器對象如下:

$accessControlHandler=\Drupal::entityTypeManager()->getAccessControlHandler($entityTypeId);

普通操作(可視爲除新建操作以外的所有操作)的檢查方法:

$accessControlHandler->access($entity, $operation, $account = NULL, $return_as_object = FALSE);

新建操作的檢查方法:

$accessControlHandler->createAccess($bundle = NULL, $account = NULL, $context = [], $return_as_object = FALSE);

如果已經存在實體對象,可在其上直接調用:

$entity->access($operation, AccountInterface $account = NULL, $return_as_object = FALSE);

實體對象上調用不用區分是新建還是其他操作,但在判斷新建權限時無法傳遞上下文參數,該方法位於以下實體基類中:

\Drupal\Core\Entity\Entity::access($operation, $account = NULL, $return_as_object = FALSE)

以上調用中$return_as_object爲false時返回布爾值,爲true時返回一個訪問結果對象(見權限系統上集),對象方式返回是爲了攜帶可緩存元數據,該對象的類是以下類的子類:
   \Drupal\Core\Access\AccessResult
各類操作的操作名,用特定的字符串表示,如下:

"view":查看
"view label":查看標題 
"update":更新 
"delete":更新
"create":新建
"edit ":編輯,通常用於實體字段
"download":下載
"use":允許使用,用於格式化器

某操作必須強制使用某個字符串嗎?比如編輯操作必須強制使用"edit"嗎?查看可以用“show”嗎?要回答這個問題必須先明白操作名是某種操作的表示,模塊依據該表示來判斷在進行何種操作,需要操作發生處和權限判斷處達成一致,從這個角度看已經在用的、存在的操作名的字符表示是強制的,是固定的,是達成一致的結果,這就好比人類世界用“10”這個符號來代表十,如果某人用其它符號就無法溝通了,但如果你正在實現某種沒有適合表示的操作,那麼可以爲其取一個操作名,一旦取定,其它模塊就必須使用這個名字來識別她代表的操作,現存的操作名也都是這樣來的,換句話說操作名由操作定義處定義;此外某種操作可能並不適用於某實體,比如節點實體就沒有下載操作,因此某些操作名並不適用於所有實體。
注意不要混淆操作名和權限名,權限名又叫做權限標識符,操作名和權限名不是同一事物,她們是對應關係
如果用戶對象爲NULL將採用當前用戶(並非指匿名用戶)
系統很少在控制器中直接調用實體訪問控制處理器,而是在路由中設置相關檢查器,由後者在進入控制器之前的路由階段調用該處理器實現權限判斷

實體訪問控制處理器默認基類:
所有實體訪問控制處理器需要實現以下接口:
  \Drupal\Core\Entity\EntityAccessControlHandlerInterface
由於以實體處理器方式實例化,所以如果存在createInstance方法,將以該方法來返回實例化對象
系統默認提供了一個內容實體和配置實體的統一基類:
  \Drupal\Core\Entity\EntityAccessControlHandler
默認安裝中提供的所有實體類型的訪問控制處理器都繼承或直接使用了該默認基類,自定義實體類型如無特別需求可直接使用該基類,其中的方法解釋如下:
public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE)
判斷除新建操作以外的操作是否具備權限;默認情況下“view label”操作將被當做“view”操作,但$viewLabelOperation屬性被設置爲true時將獨立判斷,訪問控制可以區分版本、語言;權限判斷來自兩個方面:模塊和實體通用判斷,該方法首先派發以下鉤子收集模塊的判斷意見:

entity_access
$EntityTypeId. '_access'

有三個參數:$entity, $operation, $account,其中$account已做過處理(如未傳遞已被賦值當前賬戶)一定存在,這兩個鉤子沒有優先級順序(沒有覆寫關係),同等對待,鉤子函數簽名如下:
   hook_entity_access($entity, $operation, $account)
鉤子必須返回訪問結果對象,如果沒有模塊實現鉤子則系統以中立結果爲準,模塊返回的結果對象以orIf方法合併,如果結果爲禁止則權限判斷邏輯結束,以此爲準,反之繼續執行實體通用檢查,其結果依然以orIf合併,通用檢查邏輯如下:
如果是“delete”操作,但實體對象卻是新的則返回禁止,如果實體釋文中定義了管理權限標識符,釋文鍵爲“admin_permission”則檢查賬戶是否具備該權限,具備則允許,不具備則返回中立,沒有定義管理權限標識符時也將返回中立結果
通過以上邏輯得出的檢查結果作爲最終結果並緩存返回。
注意在該方法中是採用orIf方法合併,結果如果是中立那麼是否允許由調用者決定,但在路由的入站檢查中,各檢查器的結果是以andIf方法合併,其結果如果是中立將當做禁止看待。

public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE)
檢查實體的創建權限,參數解釋如下:
$entity_bundle:實體的bundle,如果支持bundle則需要傳遞,否則默認爲null
$account:賬戶對象,如傳遞值等效爲false將採用當前賬戶
$context:額外上下文參數,見後
$return_as_object:是否以對象形式返回
該方法和access大同小異,但是隻處理創建操作,有時創建操作需要很細化的判斷,需要一些額外信息,因此該方法採用上下文參數以傳遞額外信息,上下文參數是一個數組,默認鍵名有:
   entity_type_id:實體類型id,通常不需要指定,系統自動補充
   langcode:正在創建的實體的語言id,默認爲“x-default”,表示第一個版本首次創建
用戶可根據需求傳遞更多額外信息,創建權限判斷也來自兩個方面:模塊和實體通用判斷,該方法首先派發以下鉤子收集模塊的判斷意見:

entity_create_access
$entityTypeId . '_create_access'

有三個參數:$account(已做處理的賬戶對象,如未傳遞已被賦值當前賬戶), $context(前文所講上下文參數), $entity_bundle(字符串值實體bundle,不支持則爲NULL),這兩個鉤子沒有優先級順序(沒有覆寫關係)鉤子函數簽名如下:
   hook_entity_create_access($account, array $context, $entity_bundle);
鉤子必須返回訪問結果對象,如果沒有實現鉤子的模塊則系統以中立結果爲準,模塊返回的結果對象以orIf方法合併,如果結果爲禁止則權限判斷邏輯結束,以此爲準,反之繼續執行實體通用創建檢查,其結果依然以orIf合併,通用創建檢查邏輯如下:
如果實體釋文中定義了管理權限標識符,釋文鍵爲“admin_permission”則檢查賬戶是否具備該權限,具備則允許,不具備則返回中立,沒有定義管理權限標識符時也將返回中立結果

protected function getCache($cid, $operation, $langcode, AccountInterface $account)
protected function setCache($access, $cid, $operation, $langcode, AccountInterface $account)
public function resetCache()

由於權限計算是一個很耗費資源的問題,爲了提高性能,採用了靜態屬性緩存,結構如下:
    $this->accessCache[$account->id()][$cid][$langcode][$operation]
這裏需要調用者注意緩存失效問題,如果在訪問控制處理器對象的生命週期內,出現兩次相同的權限判斷,但其間實體或權限改變了,那麼緩存應該失效,但該緩存並不失效,所以調用者應該主動調用重置緩存方法

protected function processAccessHookResults(array $access)
合併模塊檢查結果,參數爲數組,鍵名通常爲整數,無意義不重要,鍵值是檢查結果對象,如果爲空將當做中立處理,否則以orIf方法合併

protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account)
執行實體通用檢查,如果是“delete”操作,但實體對象卻是新的則返回禁止,如果實體釋文中定義了管理權限標識符,釋文鍵爲“admin_permission”則檢查賬戶是否具備該權限,都不是將返回中立結果

protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL)
執行實體通用創建檢查,如果實體釋文中定義了管理權限標識符,釋文鍵爲“admin_permission”則檢查賬戶是否具備該權限,具備則允許,不具備則返回中立,沒有定義管理權限標識符時也將返回中立結果

protected function prepareUser(AccountInterface $account = NULL)
如果不傳入賬戶對象,將默認採用當前賬戶

public function fieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account = NULL, FieldItemListInterface $items = NULL, $return_as_object = FALSE)
檢查是否具備在實體字段對象上執行某操作的權限,參數依次爲:操作名、字段定義對象、賬戶對象、字段對象、指示結果標識,針對字段的權限檢查來自4個方面:
1、字段對象上的默認檢查:如果傳遞了字段對象將執行其defaultAccess方法,否則該部分默認允許 ,注意字段定義對象上不涉及權限檢查相關內容
2、實體級別的檢查:如果實體對象不是新的,在執行UUID或非字符串類型的實體ID的編輯edit操作,將直接拒絕,這兩者是不允許編輯的
3、字段對象上的通用檢查:由訪問控制處理器依據該實體類型情況設定,默認爲允許,其和字段對象上的默認檢查結果以andIf方式合併,合併結果這裏稱爲默認結果對象
4、派發鉤子執行來自模塊的檢查
派發的鉤子名如下:
    entity_field_access
該鉤子依次接收四個參數:$operation, $field_definition, $account, $items,返回訪問結果對象
執行完畢後系統繼續派發該鉤子的修改鉤,允許模塊對總結果進行修正,修改鉤函數簽名如下:
    function hook_entity_field_access_alter(array &$grants, array $context)
參數$grants是一個數組,鍵名爲模塊名,鍵值爲該模塊返回的訪問結果對象,包含默認結果對象,其鍵名爲“:default”,參數$context爲上下文數組,有如下鍵名:

    $context = [
      'operation' => $operation,
      'field_definition' => $field_definition,
      'items' => $items,
      'account' => $account,
    ];

修改鉤需要保持$grants是一維數組形式,可以增加、刪除、修改元素,鍵名隨意,但須保證鍵值爲訪問結果對象,可以爲空數組,經過修改鉤子處理後的$grants數組將以orIf方式合併各元素(如爲空數組返回中立),合併的結果將作爲該字段對象的訪問檢查結果返回
如果存在字段對象,則可在其上直接調用以下方法:
   access($operation = 'view', AccountInterface $account = NULL, $return_as_object = FALSE)
這將等同於調用了本方法,相當於快捷方式,因此往往在沒有字段對象的情況下才直接調用本方法

protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL)
執行通用字段檢查,此處默認爲允許,各實體類型可具體控制通用邏輯

各實體類型的訪問控制處理器:
查看各實體類型的處理器類可在控制器中運行以下代碼:

        $definitions = \Drupal::entityTypeManager()->getDefinitions();
        $data = [];
        foreach ($definitions as $entityTypeID => $entity_type) {
            $data[$entityTypeID] = $entity_type->getAccessControlClass();
        }
        print_r($data);
        die;

默認安裝如下:


Array
(
    [block] => Drupal\block\BlockAccessControlHandler
    [block_content] => Drupal\block_content\BlockContentAccessControlHandler
    [block_content_type] => Drupal\Core\Entity\EntityAccessControlHandler
    [comment] => Drupal\comment\CommentAccessControlHandler
    [comment_type] => Drupal\Core\Entity\EntityAccessControlHandler
    [contact_form] => Drupal\contact\ContactFormAccessControlHandler
    [contact_message] => Drupal\contact\ContactMessageAccessControlHandler
    [editor] => Drupal\editor\EditorAccessControlHandler
    [field_config] => Drupal\field\FieldConfigAccessControlHandler
    [field_storage_config] => Drupal\field\FieldStorageConfigAccessControlHandler
    [file] => Drupal\file\FileAccessControlHandler
    [filter_format] => Drupal\filter\FilterFormatAccessControlHandler
    [image_style] => Drupal\Core\Entity\EntityAccessControlHandler
    [configurable_language] => Drupal\language\LanguageAccessControlHandler
    [language_content_settings] => Drupal\Core\Entity\EntityAccessControlHandler
    [node] => Drupal\node\NodeAccessControlHandler
    [node_type] => Drupal\node\NodeTypeAccessControlHandler
    [rdf_mapping] => Drupal\Core\Entity\EntityAccessControlHandler
    [search_page] => Drupal\search\SearchPageAccessControlHandler
    [shortcut] => Drupal\shortcut\ShortcutAccessControlHandler
    [shortcut_set] => Drupal\shortcut\ShortcutSetAccessControlHandler
    [action] => Drupal\Core\Entity\EntityAccessControlHandler
    [menu] => Drupal\system\MenuAccessControlHandler
    [taxonomy_term] => Drupal\taxonomy\TermAccessControlHandler
    [taxonomy_vocabulary] => Drupal\taxonomy\VocabularyAccessControlHandler
    [tour] => Drupal\tour\TourAccessControlHandler
    [user_role] => Drupal\user\RoleAccessControlHandler
    [user] => Drupal\user\UserAccessControlHandler
    [menu_link_content] => Drupal\menu_link_content\MenuLinkContentAccessControlHandler
    [view] => Drupal\Core\Entity\EntityAccessControlHandler
    [date_format] => Drupal\system\DateFormatAccessControlHandler
    [entity_form_display] => \Drupal\Core\Entity\Entity\Access\EntityFormDisplayAccessControlHandler
    [entity_form_mode] => Drupal\Core\Entity\EntityAccessControlHandler
    [entity_view_display] => \Drupal\Core\Entity\Entity\Access\EntityViewDisplayAccessControlHandler
    [entity_view_mode] => Drupal\Core\Entity\EntityAccessControlHandler
    [base_field_override] => Drupal\Core\Field\BaseFieldOverrideAccessControlHandler
)

自定義訪問控制處理器時,通常繼承以上默認基類:
   \Drupal\Core\Entity\EntityAccessControlHandler
覆寫以下的訪問控制方法:
  checkAccess(EntityInterface $entity, $operation, AccountInterface $account)
  checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL)
參考塊訪問控制處理器:
  \Drupal\block\BlockAccessControlHandler

操作細化問題:
你可曾想過操作可以細化,從而產生層級,比如“edit”操作,可以細化爲編輯版本、編輯翻譯,那麼是否有必要爲這些細化操作專門定義操作名呢?細化可以有很多層級,或者多種條件,比如編輯有多個翻譯的版本,因此如果細化將產生大量操作名,操作名變的像權限名了,在軟件架構上違反了封裝原則,細化檢查應該在權限判斷邏輯中檢查,而不應該通過定義操作名的方式進行,因此係統並沒有“edit revision”“create translation”這樣的操作名
實際上翻譯的創建需要考慮很多問題,比如是否允許被翻譯成目標語言id,是否已存在翻譯等,因此翻譯創建權限並不在權限訪問控制器中判斷,而在內容翻譯處理器中,詳見:
   \Drupal\content_translation\ContentTranslationHandler::getTranslationAccess
同時也見權限檢查標識“_access_content_translation_manage”
能否創建新版本可通過版本字段的更新權限實現:
   $entity->get($entity_type->getKey('revision'))->access('update');


實體訪問的路由相關控制:
在路由定義中可以在requirements鍵下使用一些權限檢查標識來控制實體相關的訪問(權限檢查標識見本系列權限系統上集),如:

_entity_access: 'block_content.update'
_entity_create_access: 'menu'

注意權限檢查標識和“_entity_view、_entity_list 、_form、_entity_form”等不一樣,後者是在設置控制器,在路由定義中的defaults鍵下進行,而權限檢查標識是在設置權限需求。
系統默認定義了一些和實體相關的權限檢查標識,她們大多調用了實體訪問控制處理器,權限檢查標識及其相應的權限檢查器如下:

權限檢查標識:_entity_access
檢查器服務id:access_check.entity
檢查器類:Drupal\Core\Entity\EntityAccessCheck
檢查方法:access
對實體的通用操作進行權限檢查,檢查標識的值格式爲“實體類型ID.操作名”,操作名如:'view', 'update', 'create', 'delete'等,注意這包括了創建create操作,操作名不可省略,許多時候就是實體釋文中執行該操作的表單處理器鍵名,通常也做表單模式名,該檢查器需要路由在經過參數轉化後參數中存在實體對象,且參數名爲實體類型ID,否則該檢查器返回中立結果,實際的檢查邏輯在實體的訪問控制處理器中完成。

權限檢查標識:_entity_create_access
檢查器服務id:access_check.entity_create
檢查器類:Drupal\Core\Entity\EntityCreateAccessCheck
檢查方法:access
檢查實體的創建權限,檢查標識的值格式爲“實體類型ID :bundle”,其中bundle部分可以是一個變量(這也是通常的情況),此時bundle部分應寫作“{name}”,name爲路由原始參數中bundle的變量名,其值爲bundle值,在獲取bundle時會進行替換,如:“node:{node_type}”,此時路由原始參數中應存在$node_type,其值爲bundle值;如果實體類型不支持bundle,則bundle部分可以省略,實際的檢查邏輯在實體的訪問控制處理器中完成

權限檢查標識:_entity_create_any_access
檢查器服務id:access_check.entity_create_any
檢查器類:Drupal\Core\Entity\EntityCreateAnyAccessCheck
檢查方法:access
檢查標識的值是實體類型ID,如果該實體類型不支持bundle則該檢查標識完全等同於_entity_create_access,反之只要具備任何一個bundle的創建權限就允許訪問,如果有創建其bundle實體本身的權限則放行,以節點實體說明,如果有任何一個內容類型的創建權限即允許訪問,或者有新建內容類型的權限也允許訪問;該檢查標識通常用在實體類型的內容添加頁。
但目前(V8.6.9)該檢查器有bug,實現邏輯不正確,比如在無權創建bundle實體本身或無權創建屬於第一個bundle的實體時結果將禁止,而不管屬於其他bundle的實體是否有創建權限,已提交,見:
https://www.drupal.org/project/drupal/issues/3039629

權限檢查標識:_entity_delete_multiple_access
檢查器服務id:access_check.entity_delete_multiple
檢查器類:Drupal\Core\Entity\EntityDeleteMultipleAccessCheck
檢查方法:access
判斷是否允許刪除多個實體,其值爲實體類型id(但該值並不重要,沒被使用),被刪除的實體須爲同一實體類型,因此在內部判斷時只要有一個實體的刪除權限即被允許,如果沒Session,也沒有被刪除的實體,則返回中立,如果需要將權限具體到單個實體對象,則需要在控制器中做進一步檢查,由於該檢查器用到了$entity_type_id參數,因此路由中需要有該參數,其值爲實體類型id

權限檢查標識:_node_add_access
檢查器服務id:access_check.node.add
檢查器類:Drupal\node\Access\NodeAddAccessCheck
檢查方法:access
功能和_entity_create_any_access相同,只不過是專門用於節點實體,值爲“node:{node_type}”,由於是節點專用,所以其值並不重要(檢查器沒有用到該值),開發社區中已在討論採用更泛化的_entity_create_any_access代替,在目前(V8.6.9)的代碼中尚未聲明棄用。有內容類型創建權限、當前bundle創建權限、任意bundle節點創建權限時均允許訪問,否則返回中立
有開發者認爲該檢查器存在bug,不應該有內容類型創建權限就允許,關於此見:
https://www.drupal.org/project/drupal/issues/2744381
現在的邏輯是能創建內容類型就一定能添加內容,你是否認爲這合理呢?

權限檢查標識:_access_node_revision
檢查器服務id:access_check.node.revision
檢查器類:Drupal\node\Access\NodeRevisionAccessCheck
檢查方法:access
檢查是否具備節點實體某版本的某個操作權限,其值爲操作名,是view、update、delete三者之一,如果傳入其他操作名將直接拒絕

權限檢查標識:_node_preview_access
檢查器服務id:access_check.node.preview
檢查器類:Drupal\node\Access\NodePreviewAccessCheck
檢查方法:access
判斷是否能夠進入節點預覽頁,其值爲“{node_preview}”,該值並不重要(檢查器沒有用到該值),如果節點是新的則以是否有新建權限爲結果,否則以是否具備更新權限爲結果

權限檢查標識:_access_quickedit_entity_field
檢查器服務id:access_check.quickedit.entity_field
檢查器類:Drupal\quickedit\Access\QuickEditEntityFieldAccessCheck
檢查方法:access
判斷是否具備編輯實體某字段的權限,檢查標識的值不被使用,因此不重要,通常寫作true即可,該檢查器會驗證這些信息:實體是否具備該字段、實體是否有傳遞的語言參數所示的翻譯、實體是否有更新權限、實體字段是否有編輯權限,這些檢查如果有一個爲否,即禁止通過;該檢查器往往用於前端的就地編輯

權限檢查標識:_field_ui_view_mode_access
檢查器服務id:access_check.field_ui.view_mode
檢查器類:Drupal\field_ui\Access\ViewModeAccessCheck
檢查方法:access
判斷能否訪問視圖模式配置管理頁面(該頁面可爲字段指定格式化器等),檢查標識值爲視圖模式配置管理的權限標識符,如“administer node display”,該檢查器首先檢查該視圖模式是否被啓用,其次檢查當前用戶是否具備前文所示的權限,任一回答爲否即禁止;如果路由使用了該檢查標識,那麼路由參數中應該以這些參數:entity_type_id(實體類型id)、view_mode_name(視圖模式名,默認爲default)、bundle(或者通過bundle實體類型id指定)。

權限檢查標識:_field_ui_form_mode_access
檢查器服務id:access_check.field_ui.form_mode
檢查器類:Drupal\field_ui\Access\FormModeAccessCheck
檢查方法:access
和_field_ui_view_mode_access完全相同,只不過該檢查標識用於實體表單模式

補充:
1、bug(D8.6.9):\Drupal\Core\Access\AccessResult::andIf方法實現有問題,在A爲中立,B爲允許時,結果沒有合併B的緩存元數據,這就導致在緩存有效期內B變爲禁止時,結果還是中立
此外\Drupal\Core\Access\AccessResult::orIf方法也有問題,也是出現在緩存上,已報告官方
2、實體權限檢查分實體對象層面和字段對象層面,實體層面允許不代表字段層面也允許,反之亦然。
3、權限檢查是內聚的,只要任何一個模塊拒絕,那麼結果將是拒絕
4、節點實體在系統中佔有非常重要的地位,除本篇所涉知識外還涉及授權控制表,本系列將在下一篇獨立講解其訪問控制處理器
5、本文權限檢查鉤子:

entity_access
$EntityTypeId. '_access'
entity_create_access
$entityTypeId . '_create_access'

必須返回訪問檢查結果對象:
   \Drupal\Core\Access\AccessResultInterface
不能返回布爾或NULL值,一般就是以下三者之一:

return \Drupal\Core\Access\AccessResult::neutral();
return \Drupal\Core\Access\AccessResult::forbidden();
return \Drupal\Core\Access\AccessResult::allowed();

 

我是雲客,【雲遊天下,做客四方】,聯繫方式見主頁,歡迎轉載,但須註明出處

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章