권한 정책 작성
이전 섹션에서 yarn new를 사용해 권한 정책 모듈을 스캐폴딩하고 정책이 올바르게 연결되었음을 확인했어요.
출처: 문서
본문
이전 섹션에서 yarn new를 사용해 권한 정책 모듈을 스캐폴딩하고 정책이 올바르게 연결되었음을 확인했어요.
생성된 정책 클래스는 이렇게 생겼어요.
export class CustomPolicy implements PermissionPolicy { constructor(private readonly userInfo: UserInfoService) {} async handle( _request: PolicyQuery, user?: PolicyQueryUser, ): Promise<PolicyDecision> { const _ownershipEntityRefs = user ? (await this.userInfo.getUserInfo(user.credentials)).ownershipEntityRefs : []; return { result: AuthorizeResult.ALLOW }; }}
기본으로 이 클래스는 사용자의 소유권 클레임을 해석하지만 모든 요청을 그냥 허용해요. 특정 권한에 대한 검사를 추가해 확장해 보겠어요. 정책 클래스를 다음으로 갱신하세요.
import { AuthorizeResult, PolicyDecision,} from '@backstage/plugin-permission-common';import { PermissionPolicy, PolicyQuery, PolicyQueryUser,} from '@backstage/plugin-permission-node';export class CustomPolicy implements PermissionPolicy { async handle( request: PolicyQuery, _user?: PolicyQueryUser, ): Promise<PolicyDecision> { if (request.permission.name === 'catalog.entity.delete') { return { result: AuthorizeResult.DENY, }; } return { result: AuthorizeResult.ALLOW }; }}
이제 이 정책이 있으면 누구도 Catalog에서 엔티티를 삭제할 수 없어요. 다음 섹션들은 여기서 사용된 개념을 확장할 거예요.
정책에는 무엇이 있나요?
조금 더 자세히 살펴보겠어요. PolicyQuery 타입의 request 객체는 Permission 객체를 감싸는 단순한 래퍼예요. 이 permission 객체는 사용자가 수행하려는 액션에 대한 정보를 캡슐화해요 (자세한 내용은 Concepts 페이지 참고).
위 정책에서 우리는 제공된 액션이 카탈로그 엔티티 삭제 액션인지 확인하고 있어요. 이는 catalog 플러그인 작성자가 카탈로그 엔티티 등록 취소 액션을 나타내기 위해 만든 권한이에요. 그렇다면 Definitive Policy Decision(확정 정책 결정)인 DENY를 반환해요. 다른 모든 경우에는 ALLOW를 반환해요(기본 허용 동작).
이전 섹션에서 확인했듯이, 이제 이것이 카탈로그 구성 요소 등록 취소를 방지한다는 것을 알 수 있어요. 만세! 하지만 이것이 누구든 구성 요소 등록 취소를 방지하는데, 이는 그다지 현실적인 정책이 아니라는 점을 알아차릴 수 있어요. 이 구성 요소의 소유자가 아닌 한 등록 취소 액션을 비활성화해 이 정책을 개선해 보겠어요.
조건부 결정
정책을 다음으로 변경해 보겠어요.
import { AuthorizeResult, PolicyDecision, isPermission,} from '@backstage/plugin-permission-common';import { catalogConditions, createCatalogConditionalDecision,} from '@backstage/plugin-catalog-backend/alpha';import { catalogEntityDeletePermission,} from '@backstage/plugin-catalog-common/alpha';import { PermissionPolicy, PolicyQuery, PolicyQueryUser,} from '@backstage/plugin-permission-node';import { UserInfoService } from '@backstage/backend-plugin-api';export class CustomPolicy implements PermissionPolicy { constructor(private readonly userInfo: UserInfoService) {} async handle( request: PolicyQuery, user?: PolicyQueryUser, ): Promise<PolicyDecision> { if (request.permission.name === 'catalog.entity.delete') { if (isPermission(request.permission, catalogEntityDeletePermission)) { return { result: AuthorizeResult.DENY, }; const ownershipRefs = user ? (await this.userInfo.getUserInfo(user.credentials)).ownershipEntityRefs : []; return createCatalogConditionalDecision( request.permission, catalogConditions.isEntityOwner({ claims: ownershipRefs, }), ); } return { result: AuthorizeResult.ALLOW }; }}
UserInfoService는 생성자를 통해 정책 클래스에서 이미 사용할 수 있어요 — 스캐폴딩된 모듈이 자동으로 연결해 줘요.
방금 추가한 새 코드를 살펴보겠어요.
Definitive Policy Decision을 반환하는 대신 팩토리 메서드를 사용해 Conditional Policy Decision(조건부 정책 결정)을 구성해요 (자세한 내용은 Concepts 페이지 참고). 정책에는 user가 엔티티 소유자인지 결정할 충분한 정보가 없으므로, 이 기준은 조건부 결정 안에 캡슐화돼요. 그러나 request.permission이 카탈로그 엔티티 ResourcePermission이 아니면 createCatalogConditionalDecision이 컴파일되지 않아요. 이 타입 제약은 정책이 요청된 권한과 호환되는 조건부 결정을 반환하도록 보장해요. 이를 해결하기 위해 isPermission을 사용해 request.permission의 타입을 ResourcePermission<'catalog-entity'>로 "좁힙니다". 이는 이전에 있었던 런타임 동작과 일치하지만, 그 if 문의 범위 안에서 request.permission의 타입이 바뀐 것을 알아차릴 거예요.
catalogConditions 객체는 catalog 플러그인이 정의한 모든 규칙을 담아요. 이 규칙들을 결합해 PermissionCriteria 객체를 만들 수 있지만, 이 경우에는 isEntityOwner 규칙만 사용하면 돼요. 이 규칙은 소유권을 결정하는 데 사용되는 User 신원과 Group 구성원을 나타내는 엔티티 참조 목록을 받아요. UserInfoService를 사용해 자격 증명에서 사용자의 ownershipEntityRefs를 조회해요. 사용자가 익명일 수 있으므로 빈 배열을 대체값으로 제공해요.
이제 Backstage 앱에서 자신이 소유한 엔티티에 대해서는 엔티티 등록 취소 버튼이 활성화되고, 다른 모든 엔티티에 대해서는 비활성화되는 것을 볼 수 있을 거예요!
리소스 타입
이제 소유자가 수행하지 않는 한 카탈로그 엔티티에 대한 모든 액션을 방지하고 싶다고 해 보겠어요. 이를 달성하는 한 가지 방법은 if 문을 갱신해 각 권한을 확인하는 것이에요. 이런 방식으로 정책을 작성하기로 선택한다면 분명히 작동할 거예요! 그러나 정책이 커질수록 유지하기 어려울 수 있고, 특정 권한이 빠졌는지 명확하지 않을 수 있어요. 요청된 권한의 리소스 타입을 확인해 같은 정책을 더 확장 가능한 방식으로 작성할 수 있어요.
import { AuthorizeResult, PolicyDecision, isPermission, isResourcePermission,} from '@backstage/plugin-permission-common';import { catalogConditions, createCatalogConditionalDecision,} from '@backstage/plugin-catalog-backend/alpha';import { catalogEntityDeletePermission,} from '@backstage/plugin-catalog-common/alpha';import { PermissionPolicy, PolicyQuery, PolicyQueryUser,} from '@backstage/plugin-permission-node';import { UserInfoService } from '@backstage/backend-plugin-api';export class CustomPolicy implements PermissionPolicy { constructor(private readonly userInfo: UserInfoService) {} async handle( request: PolicyQuery, user?: PolicyQueryUser, ): Promise<PolicyDecision> { if (isPermission(request.permission, catalogEntityDeletePermission)) { if (isResourcePermission(request.permission, 'catalog-entity')) { const ownershipRefs = user ? (await this.userInfo.getUserInfo(user.credentials)).ownershipEntityRefs : []; return createCatalogConditionalDecision( request.permission, catalogConditions.isEntityOwner({ claims: ownershipRefs, }), ); } return { result: AuthorizeResult.ALLOW }; }}
이 예시에서는 isResourcePermission을 사용해 리소스 타입이 catalog-entity인 모든 권한을 일치시켜요. isPermission과 마찬가지로 이 헬퍼는 request.permission의 타입을 "좁히고" createCatalogConditionalDecision의 사용을 가능하게 해요. 이전에 관찰한 동작에 더해, 소유자가 아닌 한 카탈로그 엔티티가 더 이상 보이지 않는 것도 볼 수 있어야 해요 — 성공이에요!
참고
catalogEntityCreatePermission 같은 일부 카탈로그 권한은 'catalog-entity' 리소스 타입을 갖지 않아요. 그런 경우 아직 존재하지 않는 엔티티에는 조건을 적용할 수 없으므로 확정 결정(definitive decision)이 필요해요.