權(quán)限控制:從 permission 對(duì)象到 DocumentRBAC 組件)
Strapi Content Manager 的 RBAC 字段級(jí)權(quán)限控制從 permission 對(duì)象到 DocumentRBAC 組件【免費(fèi)下載鏈接】strapi Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/st/strapi本文基于 Strapi 官方文檔 03-RBAC.md深入講解內(nèi)容管理器Content Manager中文檔級(jí) 字段級(jí)的角色訪問控制RBAC機(jī)制包括 permission 對(duì)象中properties.fields的命名規(guī)則、DocumentRBAC組件提供的權(quán)限上下文、useRBAC鉤子對(duì)權(quán)限條件的校驗(yàn)以及編輯視圖中字段被隱藏或禁用的完整落地鏈路。讀完本文你可以掌握如何在 Strapi 管理面板中實(shí)現(xiàn)按角色控制用戶可創(chuàng)建/更新/讀取哪些字段的完整原理與代碼實(shí)現(xiàn)。需要說明的是本文不是對(duì) Strapi 整體權(quán)限系統(tǒng)的全面拆解。如果你還想了解 Strapi 權(quán)限體系的整體設(shè)計(jì)角色、動(dòng)作、條件等建議參考 Permissions Intro。文檔權(quán)限模型properties.fields 決定字段級(jí)操作范圍Strapi 的每一個(gè) permission 對(duì)象都包含一個(gè)properties.fields屬性——一個(gè)字符串?dāng)?shù)組。系統(tǒng)通過它來判斷當(dāng)前用戶針對(duì)某個(gè)主體subject可以對(duì)哪些字段執(zhí)行 create / read / update 等操作。官方文檔給出了如下示例// An example permission object { id: 666, action: plugin::content-manager.explorer.create, actionParameters: {}, subject: api::article.article, properties: { fields: [short_text, blocks, single_compo.name, single_compo.test, dynamiczone] }, conditions: [] }上面的權(quán)限表示用戶可以在article這個(gè)內(nèi)容類型上創(chuàng)建文檔且僅可操作fields數(shù)組中列出的字段。理解這個(gè)數(shù)組時(shí)需要記住官方文檔給出的四條命名規(guī)則字段名是 schema 中的名字不是顯示標(biāo)簽label——label 可以在 EditViewSettings 中被覆蓋而權(quán)限校驗(yàn)只認(rèn) schema 名組件字段使用點(diǎn)號(hào)分隔的路徑路徑的第一部分是組件名如single_compo.name可重復(fù)組件repeatable component的路徑中不包含索引即component.0.field中的0不會(huì)出現(xiàn)在權(quán)限路徑里這一點(diǎn)在源碼匹配算法中有專門處理下文詳述動(dòng)態(tài)區(qū)dynamic zone中的字段始終允許——權(quán)限數(shù)組里只需出現(xiàn)dynamiczone這類頂層字段名即可。這些字段權(quán)限能夠掛到 permission 上根源在于服務(wù)端權(quán)限注冊時(shí)的聲明。從 permission.ts 的registerPermissions可以看到content-manager 插件為每個(gè)內(nèi)容類型 UID 注冊了五個(gè)動(dòng)作并且create/read/update三個(gè)動(dòng)作顯式聲明了options: { applyToProperties: [fields] }正是這個(gè)聲明讓管理面板在配置角色時(shí)可以勾選字段級(jí)權(quán)限const actions [ { section: contentTypes, displayName: Create, uid: explorer.create, pluginName: content-manager, subjects: contentTypesUids, options: { applyToProperties: [fields] }, }, { section: contentTypes, displayName: Read, uid: explorer.read, pluginName: content-manager, subjects: allContentTypesUids, options: { applyToProperties: [fields] }, }, { section: contentTypes, displayName: Update, uid: explorer.update, pluginName: content-manager, subjects: contentTypesUids, options: { applyToProperties: [fields] }, }, { section: contentTypes, displayName: Delete, uid: explorer.delete, ... }, { section: contentTypes, displayName: Publish, uid: explorer.publish, ... }, ... ];注意細(xì)節(jié)explorer.read的 subjects 是allContentTypesUids包含未在管理面板中顯示的內(nèi)容類型而create/update/delete/publish只針對(duì)isDisplayed為真的內(nèi)容類型。DocumentRBAC 組件為列表頁與編輯頁提供權(quán)限上下文DocumentRBAC是 content-manager 前端的核心特性組件實(shí)現(xiàn)位于 DocumentRBAC.tsx。它包裹 ListView 與 EditView 頁面向下提供三樣?xùn)|西用戶是否能create / read / update / delete / publish一個(gè)文檔布爾值每個(gè)動(dòng)作對(duì)應(yīng)的可操作字段列表string[]一個(gè)判斷用戶對(duì)單個(gè)字段能否執(zhí)行某動(dòng)作的工具函數(shù)canUserAction。它暴露的上下文類型如下與文檔中DocumentRBACContextValue一致interface DocumentRBACContextValue { canCreate?: boolean; canCreateFields: string[]; canDelete?: boolean; canPublish?: boolean; canRead?: boolean; canReadFields: string[]; canUpdate?: boolean; canUpdateFields: string[]; canUserAction: ( fieldName: string, fieldsUserCanAction: string[], fieldType: Attribute.Kind ) boolean; isLoading: boolean; }其中字段數(shù)組的語義很關(guān)鍵canReadFields為空數(shù)組時(shí)意味著用戶一個(gè)字段都讀不到——即使他擁有讀取整個(gè)文檔的權(quán)限也可能一個(gè)字段都看不見。組件在頁面中的接入點(diǎn)通過搜索DocumentRBAC可以確認(rèn)它實(shí)際包裹了 content-manager 前端的所有文檔級(jí)頁面頁面接入位置列表頁ListViewPage.tsx#L733-L735編輯頁EditViewPage.tsx#L343-L347歷史版本頁History.tsx#L281-L283預(yù)覽頁P(yáng)review.tsx#L514-L516關(guān)聯(lián)文檔彈窗嵌套使用傳入 modelRelations.tsx#L656其中關(guān)系字段彈窗的嵌套用法值得注意它在父文檔的編輯視圖中打開了另一個(gè)內(nèi)容類型的編輯界面此時(shí) URL 中的 slug 已經(jīng)不對(duì)應(yīng)被編輯的文檔因此組件提供了model屬性來顯式指定內(nèi)容類型 UID。DocumentRBAC的取值邏輯是contentTypeUid model ?? slug——優(yōu)先用傳入的model否則從路由參數(shù)里取slug兩者都沒有時(shí)會(huì)直接拋出錯(cuò)誤。組件源碼的注釋也聲明了這一點(diǎn)它依賴 URL 參數(shù)或model屬性來識(shí)別內(nèi)容類型屬于content-manager 插件內(nèi)部使用的組件在上下文之外使用時(shí)所有動(dòng)作默認(rèn)返回false。內(nèi)部實(shí)現(xiàn)從用戶權(quán)限到字段數(shù)組DocumentRBAC的核心處理分為三步見 DocumentRBAC.tsx#L68-L111按 subject 過濾從useAuth狀態(tài)中取出當(dāng)前用戶的全部權(quán)限過濾出permission.subject contentTypeUid的部分按動(dòng)作分組把權(quán)限按action的最后一段permission.action.split(.).slice(-1)聚合成{ create: [...], read: [...], update: [...] }結(jié)構(gòu)再交給useRBAC提取字段列表只有當(dāng)useRBAC判定該動(dòng)作被允許時(shí)才調(diào)用extractAndDedupeFields從該動(dòng)作的所有權(quán)限對(duì)象上合并出字段數(shù)組否則置為空數(shù)組。字段提取函數(shù)有一個(gè)重要約定const extractAndDedupeFields (permissions: Permission[] []) { const allFields permissions.flatMap((permission) permission.properties?.fields); // An undefined entry means this permission grants access to all fields // (no field-level restriction). Returning [] signals no restriction to callers. if (allFields.some((field) field undefined)) return []; // Deduplicate fields return Array.from(new Set(allFields as string[])); };也就是說當(dāng)某個(gè)權(quán)限的字段屬性為 undefined即管理員沒有配置字段級(jí)限制extractAndDedupeFields返回空數(shù)組語義是不限制而非不允許任何字段。這里空數(shù)組的含義與過濾后一個(gè)字段都沒有不同——它表示該動(dòng)作對(duì)所有字段開放調(diào)用方據(jù)此放行。useRBAC 鉤子動(dòng)作級(jí)判定與 conditions 校驗(yàn)useRBAC定義在 useRBAC.ts是管理面板通用的 RBAC 鉤子DocumentRBAC對(duì)它的調(diào)用是const { isLoading, allowedActions } useRBAC( contentTypePermissions, permissions ?? undefined, rawQuery );它做了三件事動(dòng)作命名轉(zhuǎn)換從 permission 的action中取最后一段去掉連字符并首字母大寫加上can前綴。例如plugin::content-manager.explorer.create得到canCreate帶連字符的動(dòng)作如admin::roles.create-draft會(huì)得到canCreateDraft。轉(zhuǎn)換邏輯見 useRBAC.ts#L156-L159 的getActionName異步條件校驗(yàn)內(nèi)部調(diào)用 auth 狀態(tài)中的checkUserHasPermissions把待校驗(yàn)的權(quán)限、服務(wù)端返回的permissions以及原始查詢串rawQuery發(fā)給后端由 RBAC 中間件檢查權(quán)限上掛載的conditionsJSON Logic 條件例如僅當(dāng)status為draft時(shí)才可更新。這就是文檔中特別強(qiáng)調(diào)isLoading字段存在的原因因?yàn)閡seRBAC鉤子需要向 API 發(fā)起請(qǐng)求來對(duì)照權(quán)限的conditions做檢查所以可選地返回isLoading以便組件在必要時(shí)等待結(jié)果。在DocumentRBAC中isLoading為真時(shí)直接渲染Page.Loading /避免在權(quán)限未定下時(shí)渲染受控字段默認(rèn)拒絕在校驗(yàn)完成前所有can{Action}均為falsedefaultAllowedActions校驗(yàn)成功后逐個(gè)置為true實(shí)現(xiàn)未確認(rèn)即拒絕的保守策略。canUserAction字段匹配算法的三種分支canUserAction是給定一個(gè)表單字段名用戶能否對(duì)它執(zhí)行動(dòng)作的最終判定函數(shù)實(shí)現(xiàn)見 DocumentRBAC.tsx#L117-L145。它處理了權(quán)限路徑命名規(guī)則中比較微妙的兩個(gè)問題——可重復(fù)組件的索引和組件嵌套const canUserAction (fieldName, fieldsUserCanAction, fieldType) { const name removeNumericalStrings(fieldName.split(.)); const componentFieldNames fieldsUserCanAction // filter out fields that arent components (components are dot separated) .filter((field) field.split(.).length 1); if (fieldType component) { // check if the field name is within any of those arrays return componentFieldNames.some((field) { return field.includes(name.join(.)); }); } /** * The field is within a component. */ if (name.length 1) { return componentFieldNames.includes(name.join(.)); } /** * just a regular field */ return fieldsUserCanAction.includes(fieldName); };配合去索引函數(shù)// const name a.0.b; // removeNumericalStrings(name.split(.)) [a, b] const removeNumericalStrings (arr: string[]) arr.filter((item) isNaN(Number(item)));三個(gè)分支對(duì)應(yīng)三種情況字段本身是組件fieldType component在權(quán)限數(shù)組中找所有點(diǎn)號(hào)路徑長度大于 1 的項(xiàng)只要任一路徑包含當(dāng)前組件名即放行。這與組件整體可讀可編輯、內(nèi)部字段另判的語義一致字段位于組件內(nèi)部路徑去索引后仍有多段用去掉了數(shù)字索引的規(guī)范路徑如single_compo.name做精確匹配——這正好消化了權(quán)限規(guī)則中可重復(fù)組件不帶索引的約定無論表單中渲染的是第幾個(gè)重復(fù)項(xiàng)hero.0.title還是hero.1.title都會(huì)歸一化成hero.title再去比對(duì)權(quán)限路徑普通頂層字段直接在fieldsUserCanAction中做includes匹配。該組件的行為有專門的測試覆蓋見 DocumentRBAC.test.tsx測試用例覆蓋了無權(quán)限時(shí)動(dòng)作與字段列表全為拒絕、以及按角色授予字段權(quán)限后的放行場景。編輯視圖的落地字段級(jí)隱藏、禁用與發(fā)布攔截有了上下文編輯視圖的字段渲染器 InputRenderer.tsx 就能完成文檔所說的根據(jù)用戶權(quán)限禁用或隱藏字段const canCreateFields useDocumentRBAC(InputRenderer, (rbac) rbac.canCreateFields); const canReadFields useDocumentRBAC(InputRenderer, (rbac) rbac.canReadFields); const canUpdateFields useDocumentRBAC(InputRenderer, (rbac) rbac.canUpdateFields); const canUserAction useDocumentRBAC(InputRenderer, (rbac) rbac.canUserAction); // 新建文檔時(shí)沒有 documentId用 create 權(quán)限集已有文檔則區(qū)分 read / update const editableFields idToCheck ? canUpdateFields : canCreateFields; const readableFields idToCheck ? canReadFields : canCreateFields; const canUserReadField canUserAction(props.name, readableFields, props.type); const canUserEditField canUserAction(props.name, editableFields, props.type);注意新建 vs 編輯的權(quán)限集切換新建文檔尚無documentId時(shí)同時(shí)用canCreateFields做可讀/可寫判斷編輯已有文檔時(shí)則可讀看canReadFields、可寫看canUpdateFields。判定結(jié)果有兩級(jí)效果不可讀 → 隱藏canUserReadField為假時(shí)該字段整體不渲染真實(shí)輸入框而是渲染 NotAllowedInput——一個(gè)禁用的只讀輸入框占位文案為 No permissions to see this field并帶一個(gè)劃線眼睛圖標(biāo)向編輯者明確提示這里有一個(gè)你無權(quán)查看的字段可讀但不可寫 → 禁用fieldIsDisabled由!canUserEditField、字段自身disabled或整個(gè)表單isFormDisabled三者之一觸發(fā)字段可見但不可編輯。源碼中還有兩處與動(dòng)態(tài)區(qū)字段始終允許相關(guān)的豁免字段位于 Dynamic Zone 內(nèi)、或位于預(yù)覽彈窗preview popover內(nèi)時(shí)shouldIgnorePermissions為真RBAC 判定被跳過。這與文檔中動(dòng)態(tài)區(qū)中所有字段始終允許的規(guī)則相互印證。字段權(quán)限還會(huì)反過來影響發(fā)布流程。在 DocumentActions.tsx 的PublishAction中發(fā)布前的校驗(yàn)若失敗會(huì)進(jìn)一步檢查是否存在必填但不可讀的字段組件類型則檢查canReadFields中是否有以該組件名開頭的路徑普通字段則檢查canReadFields是否包含該必填字段。若存在則給出專門的錯(cuò)誤提示Your current permissions prevent access to certain required fields. Please request access from an administrator to proceed.這防止了用戶因?yàn)榭床坏侥硞€(gè)必填字段而無法發(fā)布、卻只得到一條籠統(tǒng)的校驗(yàn)失敗信息。小結(jié)Strapi Content Manager 的文檔級(jí) RBAC 可以概括為一條完整的鏈路服務(wù)端注冊permission.ts 為每個(gè)內(nèi)容類型注冊explorer.create/read/update/delete/publish動(dòng)作其中 create/read/update 通過applyToProperties: [fields]支持字段級(jí)授權(quán)權(quán)限對(duì)象permission 的properties.fields以 schema 字段名為基準(zhǔn)組件用點(diǎn)號(hào)路徑、可重復(fù)組件不含索引、動(dòng)態(tài)區(qū)字段始終放行前端上下文DocumentRBAC 按 subject 過濾、按動(dòng)作分組結(jié)合 useRBAC 的conditions異步校驗(yàn)產(chǎn)出can{Action}布爾值、字段數(shù)組與canUserAction匹配函數(shù)視圖落地InputRenderer 據(jù)此隱藏?zé)o權(quán)讀取的字段NotAllowedInput、禁用無權(quán)編輯的字段并在發(fā)布時(shí)攔截必填但不可讀的場景。理解了這條鏈路你就能在 Strapi 中配置出精確到某個(gè)角色只能創(chuàng)建文章的標(biāo)題和摘要字段、只能讀取正文級(jí)別的文檔權(quán)限并預(yù)判管理面板中字段會(huì)被隱藏還是禁用。更完整的權(quán)限體系角色、條件、其他插件動(dòng)作請(qǐng)參考 Permissions Intro?!久赓M(fèi)下載鏈接】strapi Strapi is the leading open-source headless CMS. It’s 100% JavaScript/TypeScript, fully customizable, and developer-first.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/st/strapi創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考