전편에서 API Key를 통한 인증과 기본 권한 설정을 완료했다. 하지만 현재는 인증된 사용자라면 누구나 모든 티켓을 수정하고 삭제할 수 있는 상태다. 이번에는 자신이 생성한 리소스만 수정/삭제할 수 있도록 소유권 기반 권한을 구현해보자.
목표
- 티켓 생성 시 자동으로 생성자(owner) 기록
- 자신이 생성한 티켓만 삭제 가능
- 다른 사람의 티켓 삭제 시도는 차단
Owner Relationship 추가
먼저 티켓 리소스에 소유자를 나타내는 relationship을 추가한다.
# lib/helpdesk/support/ticket.ex
defmodule Helpdesk.Support.Ticket do
use Ash.Resource,
domain: Helpdesk.Support,
data_layer: AshPostgres.DataLayer,
extensions: [AshJsonApi.Resource],
authorizers: [Ash.Policy.Authorizer]
# ... 기존 코드 ...
relationships do
belongs_to :representative, Helpdesk.Support.Representative # 기존
belongs_to :owner, Helpdesk.Accounts.User # 추가
end
end
relationship을 추가했으므로 마이그레이션이 필요하다.
mix ash.codegen "add owner relationship to tickets"
mix ash.migrate
생성자 자동 지정
티켓 생성 시 현재 인증된 사용자를 자동으로 owner로 설정하도록 한다.
actions do
defaults [:read]
create :open do
accept [:subject]
change relate_actor(:owner) # 이 줄 추가
end
# ... 기존 액션들
end
relate_actor(:owner)는 현재 actor(인증된 사용자)를 owner relationship에 자동으로 연결한다.
삭제 액션 추가
actions do
# ... 기존 액션들
destroy :destroy do
primary? true
end
end
정책 설정
이제 권한 정책을 설정할 차례다. 읽기와 생성은 모든 인증된 사용자에게 허용하고, 수정과 삭제는 소유자만 가능하도록 한다.
policies do
# 읽기와 생성은 모든 인증된 사용자
policy action_type([:read, :create]) do
authorize_if actor_present()
end
# 수정과 삭제는 소유자만
policy action_type([:update, :destroy]) do
authorize_if relates_to_actor_via(:owner)
end
end
relates_to_actor_via(:owner)는 현재 사용자와 리소스의 owner가 일치하는지 확인한다.
API 라우트 추가
Domain 설정에 delete 엔드포인트를 추가한다.
# lib/helpdesk/support.ex
defmodule Helpdesk.Support do
use Ash.Domain, extensions: [AshJsonApi.Domain]
json_api do
routes do
base_route "/tickets", Helpdesk.Support.Ticket do
get :read
index :read
post :open
delete :destroy # 이 줄 추가
end
end
end
resources do
resource Helpdesk.Support.Ticket
end
end
테스트
자신의 티켓 삭제는 성공한다.
# 티켓 생성
% curl -X POST http://localhost:4000/api/json/tickets \
-H "Authorization: Bearer helpdesk_FlhUEreQrABNZ2XvyDIC5bUeJljjHiOGL9jeiwHPpjeMSmYosY4TupfzhIOF339E_ckbhkf" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"data": {
"type": "ticket",
"attributes": {
"subject": "My test ticket"
}
}
}'
# 응답에서 ID 확인 (예: abc-123-def)
# 자신의 티켓 삭제 (성공)
% curl -X DELETE http://localhost:4000/api/json/tickets/abc-123-def \
-H "Authorization: Bearer helpdesk_FlhUEreQrABNZ2XvyDIC5bUeJljjHiOGL9jeiwHPpjeMSmYosY4TupfzhIOF339E_ckbhkf"
# 결과: 200 OK
다른 사용자의 티켓을 삭제하려고 하면 차단된다. 테스트를 위해 새 사용자와 API Key를 생성한다(방법은 전편 참조).
# 다른 사용자의 API Key로 삭제 시도
% curl -X DELETE http://localhost:4000/api/json/tickets/abc-123-def \
-H "Authorization: Bearer helpdesk_NewUserApiKeyHere..."
# 결과: 403 Forbidden
{"errors":[{"code":"forbidden",...}]}
서버 로그를 보면 정책 평가 과정을 확인할 수 있다.
Policy Breakdown
user: %{id: "62ff21cf-d1c4-4fd5-8af1-b4cbc160a4c1"}
Policy | 🔎:
condition: action.type in [:update, :destroy]
authorize if: record.owner == actor | owner.id == "62ff21cf-d1c4-4fd5-8af1-b4cbc160a4c1" | ? | 🔎
SAT Solver statement:
"action.type in [:update, :destroy]" and
(("action.type in [:update, :destroy]" and "record.owner == actor") or
not "action.type in [:update, :destroy]")
동작 원리
티켓 생성 시 relate_actor(:owner)가 현재 인증된 사용자의 ID를 owner_id 컬럼에 저장한다. 삭제 요청이 들어오면 relates_to_actor_via(:owner) 정책이 ticket.owner_id == current_user.id를 확인하고, 일치하면 허용하고 다르면 차단한다.
주의사항
이미 존재하는 티켓들은 owner_id가 NULL이다. 필요하다면 relationship을 nullable로 설정할 수 있다.
belongs_to :owner, Helpdesk.Accounts.User do
allow_nil? true
end
또는 기존 데이터에 대한 마이그레이션 스크립트를 작성해야 할 수도 있다.
마무리
Ash의 relationship과 정책 시스템을 활용하면 소유권 기반 권한 제어를 선언적으로 구현할 수 있다. relate_actor로 자동으로 소유자를 설정하고, relates_to_actor_via로 권한을 확인하는 것만으로 충분하다. 이 패턴은 삭제뿐 아니라 수정, 조회 등 모든 액션에 적용 가능하다.