Cupix::Errors::PermissionDenied: Permission denied to publish
RCA: Cupix::Errors::PermissionDenied: Permission denied to publish
Overview#
What Happened#
cupixworks-api (tesla) 에서 편집 권한이 없는 사용자가 리소스 publish 엔드포인트(PUT /api/v1/captures/{id}/publish 등)를 호출하자 Pundit 정책 검사가 실패하여 Cupix::Errors::PermissionDenied (PERM10000) 가 발생했다. 이 예외는 client_error_controller.rb 에서 HTTP 403 으로 정상 매핑되며, 서버 결함이 아니라 예상된 인가 실패다. retention 창(now-14d) 내 26건 중 대부분이 dev/qa 테스트 트래픽(tg-not-editing 팀)이었다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Cupix::Errors::PermissionDenied |
| exception.message | Permission denied to publish |
| error.code | PERM10000 |
| top_frame | app/repositories/concerns/publishable_repository.rb:5 |
| http.status | 403 |
| runtime | Ruby on Rails (tesla) |
| env | dev / qa / production, region us-west-2 |
Affected Teams#
로그 team.domain 기준 분포 (now-14d, 26건):
| Team / Domain | Error Count | Impact |
|---|---|---|
tg-not-editing |
16 | 편집 권한 없는 QA 테스트 팀 — 인가 거부 테스트로 추정 |
hani |
6 | 비편집 사용자 publish 시도 → 403 |
ikukbo |
2 | 비편집 사용자 publish 시도 → 403 |
cupix |
1 | 비편집 사용자 publish 시도 → 403 |
qatest |
1 | QA 테스트 트래픽 |
환경 분포: dev 18, qa 6, production 2.
Timeline#
- 2025-02-24 14:21 KST — 최초 발생 (first_seen, ET 집계 기준)
- 2026-07-22 12:48 KST — retention 창 내 첫 로그 확인 (
captures/78908/publish, teamtg-not-editing) - 2026-07-27 10:09 KST — 클러스터 last_seen
- 2026-08-04 16:06 KST — retention 창 내 최신 발생 (여전히 진행 중)
Error Log#
Permission denied to publish
실제 request 로그 (status:info, 403):
[403] PUT /api/v1/captures/78908/publish (Api::V1::CapturesController#publish)
Impact#
- Service:
cupixworks-api(tesla) - 발생 횟수: 12 (클러스터), retention 창 내 실측 26건
- 최초 발생: 2025-02-24 14:21 KST
- 최근 발생: 2026-07-27 10:09 KST (last_seen); 실측 최신 2026-08-04 16:06 KST
Root Cause Summary#
편집 권한이 없는 사용자가 capture/review/floorplan 의 publish 엔드포인트를 호출하면, PublishableRepository#publish! 첫 줄에서 Pundit.policy(current_user, model).publish? 가 false 를 반환하여 Cupix::Errors::PermissionDenied (PERM10000, reason Permission denied to publish) 를 raise 한다. publish? 정책은 editor? (admin-team editor) 또는 delete? 권한을 요구하는데 (publishable_policy.rb:5-9), 두 조건을 모두 만족하지 못하는 viewer/비편집자가 publish 를 시도한 것이 원인이다. 이 예외는 client_error_controller.rb:26 의 rescue_from Cupix::Errors::PermissionDenied → permission_denied_403_error 로 HTTP 403 에 정확히 매핑된다. 즉 클라이언트 인가 실패가 올바르게 처리된 정상 동작이며, 서버 코드 결함이 아니다.
Technical Analysis#
Code Path#
- Entry point:
PublishableController#publish(app/controllers/concerns/publishable_controller.rb:4-9)
def publish
repository_instance.publish!
render_api Renderable.new({
contents: @model
})
end
- Failure point:
PublishableRepository#publish!(app/repositories/concerns/publishable_repository.rb:4-11)
def publish!
raise Cupix::Errors::PermissionDenied.new(code: 'PERM10000', reason: 'Permission denied to publish') unless Pundit.policy(self.current_user, self.model).publish?
return if self.model.published?
self.model.check_publishable
_publish
end
- 정책 검사:
PublishablePolicy#publish?(app/policies/concerns/publishable_policy.rb:4-9)
def publish?
return true if editor?
delete?
end
editor?는 admin 팀의 editor 만 true (application_policy.rb:255-261):
def editor?
return false unless user.team&.domain == 'admin'
return false unless record.respond_to?(:editor)
record.editor_id == user.id || record.try(:editing).try(:editor_id) == user.id
end
- HTTP 매핑:
ClientErrorController(app/controllers/concerns/client_error_controller.rb:26,69-71)
rescue_from Cupix::Errors::PermissionDenied, with: :permission_denied_403_error
# ...
def permission_denied_403_error(exception)
raise_error(403, exception)
end
기대 동작 vs 실제 동작: 편집 권한 없는 사용자의 publish 요청은 인가 거부(403)로 반환되어야 하고, 실제로도 그렇게 처리된다. publish! (bang) 은 컨트롤러에서 별도 rescue 하지 않으므로 예외가 controller rescue chain 까지 전파되어 403 으로 렌더된다. 동일 reason 문자열은 siteinsights_publishable_repository.rb:5 와 publishable_repository/review.rb:50 에도 존재하지만, retention 창 로그는 모두 일반 captures/reviews/floorplans publish 경로였다.
Log Evidence#
Datadog 쿼리 (재현용):
service:cupixworks-api "Permission denied to publish"
- LOG 26건 (now-14d). 모두
status:info[403]request 로그.status:error/ span 검색은 0건 (403 은 error 로 로깅되지 않음).
대표 로그 원문 (attributes 발췌):
{
"timestamp": "2026-07-22T03:48:24.918Z",
"status": "info",
"message": "[403] PUT /api/v1/captures/78908/publish (Api::V1::CapturesController#publish)",
"attributes": {
"error": {
"reason": "Permission denied to publish",
"code": "PERM10000",
"class": "Cupix::Errors::PermissionDenied"
},
"action": "publish",
"controller": "Api::V1::CapturesController",
"team": { "domain": "tg-not-editing", "id": 377 },
"environment": "dev"
}
}
엔드포인트 분포 (message 기준):
19 PUT /api/v1/captures/{id}/publish (Api::V1::CapturesController#publish)
2 PUT /api/v1/reviews/{key}/publish (Api::V1::ReviewsController#publish)
1 PUT /api/v1/floorplans/{id}/publish (Api::V1::FloorplansController#publish)
환경/팀 분포:
env: dev 18, qa 6, production 2
team: tg-not-editing 16, hani 6, ikukbo 2, cupix 1, qatest 1
status: info 26 (전부 [403], error 0)
tg-not-editing (팀명이 문자 그대로 "편집 불가") + qatest 가 22/26 을 차지 → 인가 거부 동작을 검증하는 dev/qa 자동화 테스트가 대부분. distinct capture ID 가 매번 다르고(45942, 78908, 79106, 80701 …) 특정 리소스 재진입 패턴이 아니라 저빈도 산발 → 클라이언트 인가 실패의 자연스러운 분포.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 편집 권한 없는 사용자의 publish 시도가 403 으로 정상 매핑되는 예상된 인가 실패 (noise) | 로그 26건 전부 status:info [403]; error.code=PERM10000; client_error_controller.rb:26 가 403 으로 매핑; 팀 tg-not-editing/qatest 가 대부분; publish? 정책은 editor/delete 권한 요구 |
— | Confirmed |
| H2 | publish! 예외가 rescue 되지 않아 500 으로 새는 매핑 버그 (bug) | publish! 은 bang 메서드로 controller 에서 개별 rescue 없음 |
PermissionDenied < BaseError 가 client_error_controller.rb:26 rescue_from 에 등록되어 확정적으로 403; 로그 status 도 전부 info/403, 500 로그 0 |
Rejected |
| H3 | 정책 로직 회귀로 정당한 editor 도 거부되는 결함 (bug) | 발생이 16개월 지속 | editor?/delete? 정책은 정상; 트래픽이 비편집 테스트 팀(tg-not-editing)에 집중, editor 팀의 거부 로그 없음; production 은 2건뿐 |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 코드 변경 불필요.
Cupix::Errors::PermissionDenied→ HTTP 403 매핑은 이미 정상(client_error_controller.rb:26,69-71)이며, publish 정책(publishable_policy.rb)도 의도대로 동작한다. 예상된 클라이언트 인가 실패이므로 Error Tracking 에서 IGNORE 권장.
단기 개선 (1주 이내)#
- 알람 노이즈 감소가 목적이라면, 프런트엔드에서 비편집(viewer) 사용자에게 publish 버튼을 비활성화/숨김 처리하여 사전 차단하는 UX 개선을 검토 (백엔드 변경 아님, 프런트엔드 담당자 조율 필요).
장기 개선 (재발 방지)#
- QA 자동화(
tg-not-editing/qatest)가 생성하는 인가 거부 트래픽은 정상 테스트 산출물이므로, ET 필터에서 dev/qa env 또는PERM10000계열 4xx 를 노이즈로 분류하는 규칙을 유지한다.
Monitoring#
Publish 인가 거부 추이 (403 request 로그):
service:cupixworks-api "Permission denied to publish"
production 환경만 별도 추적 (테스트 트래픽 제외):
service:cupixworks-api "Permission denied to publish" env:production
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial (코드 변경 없음 — ET IGNORE)
Noise Verdict#
noise — 편집 권한 없는 사용자의 publish 시도가 PERM10000 로 HTTP 403 에 이미 정상 매핑되는 예상된 클라이언트 인가 실패이며, 트래픽 대부분이 dev/qa 테스트 팀(tg-not-editing/qatest)이라 서버 코드 결함이 아니다.