3. 리소스 권한 검사 추가
특정 리소스에 대해 업데이트(또는 다른 작업)를 수행할 때 권한 프레임워크는 결정이 리소스 자체의 특성에 기반하도록 할 수 있어요. 이는 예를 들어 리소스를 소유한 사용자에게는 작업을 허용하고 그 외에는 거부하는 정책을 작성할 수 있다는 뜻이에요.
출처: 문서
본문
특정 리소스에 대해 업데이트(또는 다른 작업)를 수행할 때 권한 프레임워크는 결정이 리소스 자체의 특성에 기반하도록 할 수 있어요. 이는 예를 들어 리소스를 소유한 사용자에게는 작업을 허용하고 그 외에는 거부하는 정책을 작성할 수 있다는 뜻이에요.
업데이트 권한 만들기
이전 섹션의 plugins/todo-list-common/src/permissions.ts 파일에 새 권한을 추가해 보겠어요.
plugins/todo-list-common/src/permissions.ts
import { createPermission } from '@backstage/plugin-permission-common';export const TODO_LIST_RESOURCE_TYPE = 'todo-item';export const todoListCreatePermission = createPermission({ name: 'todo.list.create', attributes: { action: 'create' },});export const todoListUpdatePermission = createPermission({ name: 'todo.list.update', attributes: { action: 'update' }, resourceType: TODO_LIST_RESOURCE_TYPE,});export const todoListPermissions = [todoListCreatePermission];export const todoListPermissions = [ todoListCreatePermission, todoListUpdatePermission,];
todoListCreatePermission과 달리 todoListUpdatePermission 권한에 resourceType 필드가 포함되어 있음을 주목하세요. 이 필드는 권한 프레임워크에 이 권한이 'todo-item' 타입의 리소스 맥락에서 인가되도록 의도되었음을 알려줘요. 각 리소스 타입에 대해 같은 값을 일관되게 사용하는 한 리소스 타입으로 원하는 문자열을 사용할 수 있어요.
업데이트 권한에 대한 인가 설정
먼저 plugins/todo-list-backend/src/plugin.ts를 편집해 새 권한을 플러그인에 추가해 보겠어요.
plugins/todo-list-backend/src/plugin.ts
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';import { todoListCreatePermission, todoListUpdatePermission,} from '@internal/plugin-todo-list-common';// ...permissionsRegistry.addPermissions([todoListCreatePermission]);permissionsRegistry.addPermissions([ todoListCreatePermission, todoListUpdatePermission,]);// ...
그럼 이전 섹션에서 했던 것과 같은 방식으로 plugins/todo-list-backend/src/service/router.ts를 편집해 보겠어요.
plugins/todo-list-backend/src/service/router.ts
import { todoListCreatePermission } from '@internal/plugin-todo-list-common';import { todoListCreatePermission, todoListUpdatePermission,} from '@internal/plugin-todo-list-common';router.put('/todos', async (req, res) => { const credentials = await httpAuth.credentials(req, { allow: ['user'] }); if (!isTodoUpdateRequest(req.body)) { throw new InputError('Invalid payload'); } const decision = ( await permissions.authorize( [{ permission: todoListUpdatePermission, resourceRef: req.body.id }], { credentials }, ) )[0]; if (decision.result !== AuthorizeResult.ALLOW) { throw new NotAllowedError('Unauthorized'); } res.json(update(req.body));});
중요: todo 항목의 id를 값으로 하는 추가 resourceRef 필드를 전달하고 있음을 주목하세요.
이를 통해 리소스 특성에 기반한 결정이 가능해지지만, 정책 작성자는 권한 정책 안에서 resource ref에 접근할 수 없다는 점을 알아두는 것이 중요해요. 대신 정책은 조건부 결정을 반환하며, 우리는 이제 그것을 플러그인에서 지원해야 해요.
조건부 결정 지원 추가
누락된 모듈을 설치하세요.
$ yarn workspace @internal/plugin-todo-list-backend add zod@4
새 plugins/todo-list-backend/src/service/rules.ts 파일을 만들고 다음 코드를 추가하세요.
plugins/todo-list-backend/src/service/rules.ts
import { createPermissionResourceRef, createPermissionRule,} from '@backstage/plugin-permission-node';import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common';import * as z from 'zod';import { Todo, TodoFilter } from './todos';export const todoListPermissionResourceRef = createPermissionResourceRef< Todo, TodoFilter>().with({ pluginId: 'todolist', resourceType: TODO_LIST_RESOURCE_TYPE,});export const isOwner = createPermissionRule({ name: 'IS_OWNER', description: 'Should allow only if the todo belongs to the user', resourceRef: todoListPermissionResourceRef, paramsSchema: z.object({ userId: z.string().describe('User ID to match on the resource'), }), apply: (resource: Todo, { userId }) => { return resource.author === userId; }, toQuery: ({ userId }) => { return { property: 'author', values: [userId], }; },});export const rules = { isOwner };
todoListPermissionResourceRef는 리소스 타입과 관련된 타입과 상수를 캡슐화하는 유틸리티예요. 이 리소스에 대해 생성된 모든 규칙에서 리소스와 쿼리 타입이 일관되도록 보장해요.
참고
Backstage 통합자가 정의한 사용자 지정 규칙을 지원하려면, 백엔드 패키지에서 todoListPermissionResourceRef를 내보내야 해요. 서드파티 모듈 생성을 활성화하려면 *-node 패키지에서 내보내야 해요.
우리는 새 isOwner 규칙을 만들었어요. 이 규칙은 resourceRef가 붙은 인가 요청에 대한 응답으로 조건부 응답이 반환될 때마다 권한 프레임워크가 자동으로 사용할 거예요. 구체적으로 apply 함수는 전달된 리소스가 인가되어야 하는지 이해하는 데 사용돼요.
지금은 toQuery 함수를 건너뛰고, 다음 섹션에서 다시 돌아올 거예요.
이제 PermissionsRegistryService를 통해 새 리소스 타입을 권한 시스템에 추가해 보겠어요. 다음을 제공해야 해요.
getResources:authorize에 전달될 것으로 기대하는 것과 같은 형식의resourceRefs배열을 받아 해당 리소스 배열을 반환하는 함수.resourceType: 위 권한 규칙에서 사용한 것과 같은 값.permissions: 플러그인이 받아들이는 권한 목록.rules: 조건부 결정에서 지원하려는 모든 권한 규칙의 배열.
plugins/todo-list-backend/src/plugin.ts
// ...import { coreServices, createBackendPlugin,} from '@backstage/backend-plugin-api';import { createRouter } from './service/router';import { todoListCreatePermission, todoListUpdatePermission,} from '@internal/plugin-todo-list-common';import { getTodo } from './todos';import { todoListPermissionResourceRef, rules } from './rules';// ...permissionsRegistry.addPermissions([ todoListCreatePermission, todoListUpdatePermission,]);permissionsRegistry.addResourceType({ resourceRef: todoListPermissionResourceRef, permissions: [todoListCreatePermission, todoListUpdatePermission], rules: Object.values(rules), getResources: async resourceRefs => { return Promise.all(resourceRefs.map(getTodo)); },});
정책 작성자를 위한 유틸리티 제공
이제 새 리소스 타입과 해당 규칙이 있으니, 정책 작성자가 그것들을 참조할 수 있도록 일부 유틸리티를 내보내야 해요.
새 plugins/todo-list-backend/src/conditionExports.ts 파일을 만들고 다음 코드를 추가하세요.
plugins/todo-list-backend/src/conditionExports.ts
import { TODO_LIST_RESOURCE_TYPE } from '@internal/plugin-todo-list-common';import { createConditionExports } from '@backstage/plugin-permission-node';import { todoListPermissionResourceRef, rules } from './service/rules';const { conditions, createConditionalDecision } = createConditionExports({ resourceRef: todoListPermissionResourceRef, rules,});export const todoListConditions = conditions;export const createTodoListConditionalDecision = createConditionalDecision;
plugins/todo-list-backend/src/index.ts를 편집해 todoListConditions와 createTodoListConditionalDecision이 todo-list-backend 패키지에서 내보내지도록 확인하세요.
plugins/todo-list-backend/src/index.ts
export * from './service/router';export * from './conditionExports';export { exampleTodoListPlugin } from './plugin';
인가된 업데이트 엔드포인트 테스트
권한 정책의 handle 함수로 돌아가 isOwner 조건으로 새 권한을 인가해 보겠어요. Getting Started 단계에서 만든 권한 정책 모듈의 CustomPolicy 클래스를 갱신하세요.
import { PermissionPolicy, PolicyQuery, PolicyQueryUser,} from '@backstage/plugin-permission-node';import { AuthorizeResult, PolicyDecision, isPermission,} from '@backstage/plugin-permission-common';import { UserInfoService } from '@backstage/backend-plugin-api';import { todoListCreatePermission } from '@internal/plugin-todo-list-common';import { todoListCreatePermission, todoListUpdatePermission,} from '@internal/plugin-todo-list-common';import { todoListConditions, createTodoListConditionalDecision,} from '@internal/plugin-todo-list-backend';export class CustomPolicy implements PermissionPolicy { constructor(private readonly userInfo: UserInfoService) {} async handle( request: PolicyQuery, _user?: PolicyQueryUser, user?: PolicyQueryUser, ): Promise<PolicyDecision> { if (isPermission(request.permission, todoListCreatePermission)) { return { result: AuthorizeResult.ALLOW, }; } if (isPermission(request.permission, todoListUpdatePermission)) { const userEntityRef = user ? (await this.userInfo.getUserInfo(user.credentials)).userEntityRef : ''; return createTodoListConditionalDecision( request.permission, todoListConditions.isOwner({ userId: userEntityRef, }), ); } return { result: AuthorizeResult.ALLOW, }; }}
들어오는 업데이트 요청에 대해 이제 조건부 결정을 반환해요. 우리는 이렇게 말하는 거예요.
권한 프레임워크야, 나 혼자 결정할 수 없어. id가 todolist인 플러그인에 가서 이 조건들을 적용하라고 요청해 줘.
모든 것이 예상대로 작동하는지 확인하려면, 자신이 만들지 않은 항목을 편집하려고 할 때 UI에 오류가 표시되는 것을 볼 수 있어야 해요. 성공이에요!