Cupix::Errors::PermissionDenied: Permission denied
RCA: Cupix::Errors::PermissionDenied: Permission denied
Overview#
What Happened#
cupixvista-api (= tesla 의 vista 배포) 에서 Cupix::Errors::PermissionDenied (code PERM10000, message Permission denied) 가 산발적으로 발생했다. 최근 14일 기준 실측 로그는 두 엔드포인트 — PUT /api/v1/me (Api::V1::MeController#update) 와 GET /api/v1/reviews/{key}/members (Api::V1::ReviewsController#members) — 에 집중되며, 전부 이미 HTTP 403 으로 정상 매핑되어 있다. 권한 없는 사용자가 자신이 변경할 수 없는 필드/리소스에 접근했을 때 발생하는 예상된 인가(authorization) 거부로, 서버 결함이 아니다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Cupix::Errors::PermissionDenied |
| exception.message | Permission denied |
| error.code | PERM10000 |
| top_frame | app/concerns/parameter/user.rb:54 (PUT /me), app/policies/application_policy.rb:38 → PERM10000 (/reviews/members) |
| runtime | Ruby on Rails (tesla, CUPIXVISTA launch mode) |
| env | dev + production (혼재) |
| http_status | 403 |
Affected Teams#
에러 로그에는 team domain 이 노출되지 않았으나, 발생 엔드포인트로 영향 범위를 정리한다.
| Endpoint | Error Count (14d) | Impact |
|---|---|---|
PUT /api/v1/me (MeController#update) |
5 | 권한 없는 state 등 보호 필드 변경 시도가 403 으로 거부 (예상 동작) |
GET /api/v1/reviews/{key}/members (ReviewsController#members) |
3 | review 열람 권한 없는 사용자의 멤버 목록 조회가 403 으로 거부 (예상 동작) |
Timeline#
- 2025-06-24 17:30 KST — 최초 발생 (
first_seen). - 2026-07-31 09:49 KST — 최근 발생 (
last_seen,PUT /api/v1/me403). - 2026-08-06 09:11 KST — retention 창 내 마지막 관측 (
PUT /api/v1/me403, 조사 시점).
Error Log#
Permission denied
Impact#
- Service:
cupixvista-api(= tesla vista 배포) - 발생 횟수: 10 (occurrence_count; 14d 로그 실측 8건)
- 최초 발생: 2025-06-24 17:30 KST
- 최근 발생: 2026-07-31 09:49 KST
Root Cause Summary#
이 에러는 tesla 의 인가 계층이 권한 없는 요청을 정상적으로 차단하면서 발생하는 예상된 client 거부다. Cupix::Errors::PermissionDenied (PERM10000) 는 client_error_controller.rb:26 의 rescue_from Cupix::Errors::PermissionDenied, with: :permission_denied_403_error 에 의해 이미 HTTP 403 으로 매핑된다. 실측 두 경로 모두 Pundit policy 게이트에서 발생한다: (1) PUT /api/v1/me 는 사용자가 자신의 프로필을 수정하면서 params[:state] 같은 보호 필드를 포함시켰으나 UserPolicy#update_state? (= delete?) 권한이 없을 때 parameter/user.rb:54 에서 raise; (2) GET /api/v1/reviews/{key}/members 는 before_action :set_review → ReviewRepository#show → BaseRepository.show 의 readable_by? 검사에서 ApplicationPolicy#read? 가 review 의 applied_permission 부재로 false 를 반환할 때 raise. 두 경우 모두 서버 로직 결함이 아니라 인가 정책의 정상 동작이므로 코드 변경이 불필요하다.
Technical Analysis#
Code Path#
Path A — PUT /api/v1/me (dominant, 5/8):
Entry: app/controllers/api/v1/me_controller.rb:12
def update
@model = repository_instance.update(params)
@session.show_option = true
super
end
@model 은 before_action :set_user 로 @current_user 자기 자신이 설정된다 (me_controller.rb:63-65). UserRepository#update 는 super (BaseRepository#update) 를 먼저 호출한 뒤 set_parameters 를 실행한다.
def update(params = {})
super
set_parameters(params)
# ...
end
BaseRepository#update 의 첫 게이트(updatable_by? → UserPolicy#update?)는 record.id == user.id 이면 true 를 반환하므로 자기 프로필 수정은 통과한다 (app/policies/user_policy.rb:14). Failure point 는 set_parameters 내부의 state 필드 게이트다.
if params[:state].present?
unless Pundit.policy(current_user, @model).update_state?
raise Cupix::Errors::PermissionDenied.new(code: 'PERM10000', reason: 'Permission denied')
end
# ...
UserPolicy#update_state? 는 delete? 를 위임하며, delete? 는 admin_administrator / sales_team / administrators·super_admin 그룹 소속을 요구한다 (user_policy.rb:18,28-33). 일반 사용자가 자신의 프로필 PUT 에 state 를 실어 보내면 이 게이트에서 거부된다 = 예상된 인가 거부.
Path B — GET /api/v1/reviews/{key}/members (3/8):
Entry: app/controllers/api/v1/reviews_controller.rb:10 (before_action :set_review)
def set_review
# ...
@model = repository_instance.show(params[:key], draft: _draft)
end
ReviewRepository#show (review_repository.rb:179) → super → BaseRepository#show (base_repository.rb:121-129) → 클래스 메서드 self.class.show 가 readable_by? 로 read 권한을 검증한다. Failure point:
def readable_by?(user)
return nil if user.nil?
Pundit.policy(user, self).read?
end
def read?
return true if user.member_of_admin_groups?(%w[administrator senior_editing_engineers])
if record.has_attribute?(:applied_permission)
if record.applied_permission[1] == 1 || record.applied_permission[0] == 1 || record.applied_permission[4] == 1
true
else
Cupix::Logger.info(
"Permission denied on reading a model #{record.class.name}",
# ...
)
false
end
read 권한이 없으면 false → show 가 PERM10000 raise (review 열람 권한 없는 사용자의 멤버 조회) = 예상된 인가 거부.
공통 매핑 — PERM10000 → HTTP 403:
rescue_from Cupix::Errors::PermissionDenied, with: :permission_denied_403_error
# ...
def permission_denied_403_error(exception)
raise_error(403, exception)
end
기대 동작 = 권한 없는 요청을 403 으로 거부하고 서버 결함으로 계상하지 않음. 실제 동작 = 정확히 403 으로 매핑됨. 즉 서버 로직에 갭이 없다.
Log Evidence#
Datadog 쿼리 (재현용):
service:cupixvista-api "Permission denied"
14일 창(now-14d) 실측 8건, 전부 status:info 의 [403] 요청 로그이며 error.code=PERM10000, error.class=Cupix::Errors::PermissionDenied, error.message=Permission denied 로 일치한다. 엔드포인트 분포: PUT /api/v1/me 5건, GET /api/v1/reviews/{key}/members 3건.
{
"timestamp": "2026-08-06 00:11:37",
"status": "info",
"message": "[403] PUT /api/v1/me (Api::V1::MeController#update)",
"error": {
"reason": "Permission denied",
"code": "PERM10000",
"message": "Permission denied",
"class": "Cupix::Errors::PermissionDenied"
}
}
{
"timestamp": "2026-08-03 15:19:06",
"status": "info",
"message": "[403] GET /api/v1/reviews/iw8g86/members (Api::V1::ReviewsController#members)",
"error": {
"reason": "Permission denied",
"code": "PERM10000",
"message": "Permission denied",
"class": "Cupix::Errors::PermissionDenied"
}
}
모든 로그가 status:info [403] 이므로 status:error 키워드 검색은 0건이다 — 메시지 문자열로 검색해야 한다. status-board 조회 결과 scope svc:cupixvista-api::unknown, active/recent 인시던트 없음. occurrence_count 10, 14개월(2025-06-24 ~ 2026-07-31) 에 걸친 매우 저빈도 산발 발생.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 권한 없는 사용자의 요청이 인가 게이트에서 정상 거부되어 403 매핑됨 (noise) | 14d 로그 8/8 이 [403] PERM10000; client_error_controller.rb:26 가 403 매핑; parameter/user.rb:54 및 application_policy.rb:38 정책 게이트 확인 |
— | Confirmed |
| H2 | Representative Permission denied 가 stale, 실제 현재는 다른 메시지 |
— | 최근 로그(2026-08-06, 2026-08-03, 2026-07-31)의 message 가 모두 Permission denied/PERM10000 로 대표값과 정확히 일치. PERM10000 reason 은 고정 코드 상수 |
Rejected |
| H3 | c99b03ce (PERM10000 "Permission denied to publish") 와 동일 이슈 | 둘 다 PERM10000 Cupix::Errors::PermissionDenied |
본 이슈 reason 은 Permission denied (generic), 엔드포인트는 /me update·/reviews/members. c99b03ce 는 Permission denied to publish (publish 경로). et_issue_id 상이 → 별개 ET 이슈, cross-merge 금지 |
Rejected |
| H4 | 서버 결함으로 잘못 매핑된 인가 오류 (예: 500) | — | PermissionDenied 는 client_error_controller.rb:69-71 에서 명시적으로 403 처리; 로그도 전부 [403] |
Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
없음. Cupix::Errors::PermissionDenied (PERM10000) 는 이미 HTTP 403 으로 정상 매핑되어 있고, 발생 원인은 권한 없는 client 요청이다. 코드 변경이 필요하지 않다. Error Tracking 에서 본 이슈(8300d796)를 IGNORE 처리 권장.
단기 개선 (1주 이내)#
선택 사항. 관측성 개선 차원에서, PUT /api/v1/me 에서 일반 사용자가 state 를 포함해 보내는 것이 프런트엔드의 의도치 않은 payload 인지 확인할 수 있다. 만약 프런트가 프로필 저장 시 항상 state 를 함께 전송하고 있다면, 프런트 payload 를 정리하여 불필요한 403 발생을 줄일 수 있다 (프런트엔드 담당자와 협의 필요 — 자동 code-fix 대상 아님).
장기 개선 (재발 방지)#
특별한 조치 불필요. 인가 거부는 정상적인 보안 동작이며, 403 은 클라이언트가 처리해야 할 응답이다. 다만 이런 "정상 4xx" 를 Error Tracking 이 error 로 집계하는 패턴(PERM10000/ARG 계열 다수 episode 와 동형)을 줄이려면, 인가 거부 로그를 error tracking pipeline 에서 필터링하는 규칙을 상위에서 고려할 수 있다.
Monitoring#
권한 거부 추이를 확인하는 timeseries 쿼리 (dashboard widget 용):
service:cupixvista-api "PERM10000" "Api::V1::MeController#update"
service:cupixvista-api "PERM10000" "ReviewsController#members"
두 쿼리의 count 가 특정 시간대에 급증(burst)하면 프런트엔드 배포 회귀나 정책 변경으로 인한 광범위 거부 가능성을 시사한다. 평상시에는 저빈도 산발 유지가 정상이다.
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial (코드 변경 없음, ET ignore 권장)
Noise Verdict#
noise — 권한 없는 사용자의 요청이 이미 HTTP 403(PERM10000)으로 정상 매핑되는 예상된 인가 거부이므로 서버 코드 결함이 아니다.