こんにちは。
内部開発者ポータル(IDP)のBackstageでは組織内で開発する様々なサービス、アプリケーション、インフラストラクチャリソースをソフトウェアカタログとして登録し、利用者間でそうした情報を共有したりすることができます。
Backstageではデフォルトではすべてのリソースを誰もが自由にアクセスし更新できるようになっていますが、アクセス制限を設けることもできます。それが以前ご紹介したPermission Frameworkです。
前回はその概要部分について解説しましたが、実際にどのような処理が行われているのでしょうか。 今回はPermission Frameworkで実現するRBAC(Role Based Access Control)について、実際のコードを参照しながらもう少し詳しく見ていきたいと思います。
Catalog EntityのPermission
Permission Frameworkは、それを利用するPluginごとに定義しています。今回はCatalog Entityを対象に調べてみたいと思います。
Permission
BackstageのPermissionは createPermission()を使用して定義します。
Catalog EntityのPermissionは次のものが用意されています。
これらは次のコード上で定義しています。
export const RESOURCE_TYPE_CATALOG_ENTITY = 'catalog-entity'; export type CatalogEntityPermission = ResourcePermission< typeof RESOURCE_TYPE_CATALOG_ENTITY >; export const catalogEntityReadPermission = createPermission({ name: 'catalog.entity.read', attributes: { action: 'read', }, resourceType: RESOURCE_TYPE_CATALOG_ENTITY, }); export const catalogEntityCreatePermission = createPermission({ name: 'catalog.entity.create', attributes: { action: 'create', }, }); // 〜〜 catalog.entity系 以下略〜〜 export const catalogLocationReadPermission = createPermission({ name: 'catalog.location.read', attributes: { action: 'read', }, }); export const catalogLocationCreatePermission = createPermission({ name: 'catalog.location.create', attributes: { action: 'create', }, }); // 〜〜 catalog.location系 以下略〜〜
CatalogのPermissionは以下のものが定義されています。
Permission名 | Rule利用可否 |
---|---|
catalog.entity.read | ◯ |
catalog.entity.create | ✗ |
catalog.entity.delete | ◯ |
catalog.entity.refresh | ◯ |
catalog.location.read | ✗ |
catalog.location.create | ✗ |
catalog.location.create | ✗ |
Resource Rule利用可否で◯がついているPermissionは Allow/Deny以外に後述のRuleを使ったConditionalなPolicyを指定することが可能です。 Resource Rule利用可否で✗のものはRule指定ができず、PolicyはAllow/Denyのいずれかを指定します。
catalog.entityPermissionは、catalog entityの作成・削除・読み取り処理実行の際にチェックするものです。refreshについては、データの再取得をする際に利用するものです。
catalog.location Permissionは、catalog entity登録の際に使用するデータソースを示すLocation Entityでチェックするものです。catalog.location Permissionについては Ruleの利用ができません。参照のみを許可する場合は「catalog.entity」と合わせて「catalog.location.read」を、Catalogの追加・削除を許可する場合はそれぞれ「catalog.location.create」「catalog.location.delete」を許可する必要があります。
Rule
Catalog EntityのPolicyはALLOW/DENYの他、条件式(CONDITIONAL)で示すことができます。このときに使用するのがRuleです。 Catalog Entityで利用可能なルールは以下で定義されています。
たとえば一番利用するであろう IS_ENTITY_OWNER は次のように定義されています。
export const isEntityOwner = createCatalogPermissionRule({ name: 'IS_ENTITY_OWNER', description: 'Allow entities owned by a specified claim', resourceType: RESOURCE_TYPE_CATALOG_ENTITY, paramsSchema: z.object({ claims: z .array(z.string()) .describe( `List of claims to match at least one on within ${RELATION_OWNED_BY}`, ), }), apply: (resource, { claims }) => { if (!resource.relations) { return false; } return resource.relations .filter(relation => relation.type === RELATION_OWNED_BY) .some(relation => claims.includes(relation.targetRef)); }, toQuery: ({ claims }) => ({ key: 'relations.ownedBy', values: claims, }), });
チェックロジック
API
Backend側のAPI呼び出しの際、それぞれのPermissionのPolicyをチェックしています。以下の図の②の部分です。
これらのチェックはcatalog-backend/src/serviceにあるAuthorizeXXX.tsというファイルで記述されています。permissionApi.authorizeCondition()またはpermissionApi.authorize()という処理が該当部分です。
例えば、entity一覧取得APIの場合は以下のようなものです。
async entities(request: EntitiesRequest): Promise<EntitiesResponse> { const authorizeDecision = ( await this.permissionApi.authorizeConditional( [{ permission: catalogEntityReadPermission }], { credentials: request.credentials }, ) )[0]; if (authorizeDecision.result === AuthorizeResult.DENY) { return { entities: [], pageInfo: { hasNextPage: false }, }; } if (authorizeDecision.result === AuthorizeResult.CONDITIONAL) { const permissionFilter: EntityFilter = this.transformConditions( authorizeDecision.conditions, ); return this.entitiesCatalog.entities({ ...request, filter: request?.filter ? { allOf: [permissionFilter, request.filter] } : permissionFilter, }); } return this.entitiesCatalog.entities(request); }
Locationの作成の場合は次のようなものです。
async createLocation( spec: LocationInput, dryRun: boolean, options: { credentials: BackstageCredentials; }, ): Promise<{ location: Location; entities: Entity[]; exists?: boolean | undefined; }> { const authorizationResponse = ( await this.permissionApi.authorize( [{ permission: catalogLocationCreatePermission }], { credentials: options.credentials }, ) )[0]; if (authorizationResponse.result === AuthorizeResult.DENY) { throw new NotAllowedError(); } return this.locationService.createLocation(spec, dryRun, options); }
UI
Backend APIによるPermission Policyのチェック以外に、一部UI側で事前にPermission Policyをチェックしています。 チェックにはusePermission()またはuseEntityPermission()で行われています。
(1)catalog.entity.create
catalog.entity.create 権限が許可の場合、Createボタンが表示されません。(Create new componentの画面に遷移可能)
これに対し、catalog.entity.create 権限が不許可の場合、Createボタンが表示されません。
(2)catalog.entity.delete
catalog.entity.delete 権限が許可の場合、「unregister entity」が実行可能となります。catalog.entity.delete 権限が不許可の場合は、「unregister entity」がグレーアウトし実行できません。
(3)catalog.entity.refresh
catalog.entity.refresh 権限が許可の場合、About cardのリフレッシュボタンが実行可能となります。
catalog.entity.refresh 権限が不許可の場合、About cardのリフレッシュボタンが表示されず、実行できません。
まとめ
いかがだったでしょうか。実際にやってみるとわかるのですが、Permission Frameworkは一見簡単なようだけれども、実際Policyを定義しようと思うといろいろとわからないことが出てきます。Permission/RBACを指定する場合に、「どこで」「どのような」チェックが行われているのか、それぞれのPermissionが何を意味するものなのかが理解できると、Permissionそのものの設計もより想像しやすくなると思います。また、APIだけでなくUI側でもチェックが行われる部分があるということも今回ご理解いただけたかと思います。
最後に弊社ではBackstageの導入や運用をManagedで行うサービスを提供しています。Permission FrameworkのPolicyの定義方法のガイドなどもあわせて提供していきたいと考えておりますので、ご興味のある方はぜひご連絡ください。
また、これまでに紹介してきたBackstageの記事はこちらになります。ぜひご参考にしていただければと思います。
Backstageを簡単にお試しいただける「ちょこっとBackstage」というものも用意しております。Permission Frameworkは有効にしていませんが、ソースコードもありますので、ご自身でPluginを足してご利用いただくことが可能です。ぜひご活用ください。
次回は「Scaffolder Template」のPermissionについて見ていきたいと思います。それでは、またの機会にお会いしましょう。