Cupix::Errors::PermissionDenied: Permission denied
RCA: Cupix::Errors::PermissionDenied: Permission denied
Overview#
What Happened#
cupixworks-api (tesla) 에서 Cupix::Errors::PermissionDenied (PERM10000, reason Permission denied) 예외가 반복 집계된 Error Tracking 이슈다. 이 예외는 tesla 전반의 factory/repository/operation 에 심어진 의도적인 인가(authorization) 가드로, 권한 없는 사용자가 보호된 리소스나 admin 엔드포인트에 접근할 때 발생한다. 예외는 ClientErrorController 에서 HTTP 403 클라이언트 응답으로 완전히 rescue 되며, 서버 500 이나 미처리 크래시가 아니다. 지난 7일간 5,294건이 발생했고 전부 403 으로 종결됐다.
Quick Facts#
| Field | Value |
|---|---|
| exception.class | Cupix::Errors::PermissionDenied |
| exception.message | Permission denied |
| exception.code | PERM10000 (대부분), 소수 PERM32000 / PERM33000 / PERM34000 |
| http.status_code | 403 |
| top_frame | app/controllers/concerns/client_error_controller.rb:26 (rescue_from → permission_denied_403_error) |
| env | production, us-west-2 |
Affected Teams#
지난 7일 Datadog 로그(@error.class:Cupix::Errors::PermissionDenied)에서 확인된 엔드포인트별 분포. 다수의 서로 다른 team/tenant 에 산발적으로 발생하며 특정 팀 장애가 아니다.
| Endpoint | Error Count (7d) | Impact |
|---|---|---|
Api::V1::Admin::TeamsController#show |
4,839 | 비-admin 사용자가 admin 전용 팀 조회 접근 → 403 |
Api::V1::TeamsController#show |
134 | 권한 없는 팀 조회 → 403 |
Api::V1::EditingsController#show |
51 | 권한 없는 editing 조회 → 403 |
Api::V1::TeamsController#entity_parameters |
47 | 권한 없는 파라미터 조회 → 403 |
| 기타 (create/update/publish 등 다수) | ~220 | 정상 인가 거부 → 403 |
Timeline#
- 2025-02-04 22:53 KST — 이슈 first_seen (Error Tracking 최초 집계).
- 2026-07-28 22:59 KST — 이슈 last_seen. 이 시각 전후 로그가 Representative Error(
PERM10000 Permission denied)와 정확히 일치. - 2026-08-04 — RCA 수행. 지난 7일 5,294건 전부 HTTP 403 으로 정상 종결됨을 확인.
Error Log#
Permission denied
Impact#
- Service:
cupixworks-api(tesla) - 발생 횟수: 77 (Error Tracking 집계) / 로그 기준 최근 7일 5,294건
- 최초 발생: 2025-02-04 22:53 KST
- 최근 발생: 2026-07-28 22:59 KST
Root Cause Summary#
Cupix::Errors::PermissionDenied 는 코드 결함이 아니라 tesla 전반에 의도적으로 심어진 인가 실패 가드다. Pundit.policy(...).create? / updatable_by? / creatable_by? 등의 정책 검사가 false 를 반환하면 raise Cupix::Errors::PermissionDenied.new(code: 'PERM10000', reason: 'Permission denied') 로 요청을 거부한다(약 40개 factory/repository/operation 에 동일 패턴 존재). 이 예외는 ClientErrorController 의 rescue_from Cupix::Errors::PermissionDenied, with: :permission_denied_403_error 로 잡혀 HTTP 403 JSON 응답으로 렌더된다. 즉 권한 없는 사용자가 보호된 리소스(특히 비-admin 이 admin 엔드포인트 접근)에 접근한 정상적인 클라이언트 인가 거부이며, 서버 오류가 아니다. Error Tracking 이 rescue 되는 예외까지 APM 레벨에서 집계하기 때문에 이슈로 노출됐을 뿐이다.
Technical Analysis#
Code Path#
- Entry point: 다수 (예:
Api::V1::Admin::TeamsController#show). admin 엔드포인트는 admin team 여부를 먼저 확인한다.
class Api::V1::Admin::ApiController < Api::V1::ApiController
def authenticate!
super
raise ActionController::RoutingError, 'Not Found' if @current_team != TeamRepository.admin_team
end
end
- 인가 가드(대표 패턴): Pundit 정책 실패 시
PermissionDenied를 raise. 동일한 형태가 repository/factory/operation 전반에 존재.
def authorize_bulk_migrate!
unless Pundit.policy(current_user, ::Team.new).bulk_migrate?
raise Cupix::Errors::PermissionDenied.new(code: 'PERM10000', reason: 'Permission denied')
end
end
raise Cupix::Errors::PermissionDenied.new(code: 'PERM10000', reason: 'Permission denied') if !(Pundit.policy(self.current_user, self.parent).create? rescue false) && !(Pundit.policy(self.current_user, self.review).create? rescue false)
- Handling point (top frame): 예외가 컨트롤러까지 전파되면
ClientErrorController가 403 으로 rescue.
rescue_from Cupix::Errors::PermissionDenied, with: :permission_denied_403_error
# ...
def permission_denied_403_error(exception)
raise_error(403, exception)
end
raise_error는 403 JSON 을 렌더할 뿐Cupix::Logger.error를 호출하지 않는다 → error 레벨 로그가 남지 않고 request 로그(status:info [403] ...)만 남는다.
def raise_error(status_code, exception, code: nil, type: nil, reason: nil, message: nil)
# ...
render status: status_code, json: {
result: { code: code, type: type, reason: reason, message: message }
}
end
- 기대 동작 vs 실제 동작: 코드는 권한 없는 요청을 403 으로 거부하도록 설계됨(SHOULD). 실제로도 모든 발생이 403 으로 종결됨(DID). Gap 없음 → 코드 결함 아님. Error Tracking 이 rescue 되는 예외까지 집계하는 것이 유일한 노출 경로다.
Log Evidence#
사용한 Datadog 쿼리 (@error.class 로 필터해 NotFound→403 등 다른 403 과 분리):
service:cupixworks-api @error.class:Cupix\:\:Errors\:\:PermissionDenied
지난 7일 결과: count = 5,294, 전부 http.status_code:403. 코드/사유 분포:
by code: PERM10000=5222, PERM32000=56, PERM33000=15, PERM34000=1
by reason: "Permission denied"=5194, "Archived entity"=56,
"Creating entity on billing expired team/workspace..."=15,
"Permission denied to unpublish"=12, "Permission denied to publish"=9
last_seen(2026-07-28 22:59 KST) 전후 실제 로그 — Representative Error 와 정확히 일치:
{"t":"2026-07-28T13:59:33.034Z","ctrl":"Api::V1::Admin::AccessCodesController","act":"create","http":403,"err":{"reason":"Permission denied","code":"PERM10000","message":"Permission denied","class":"Cupix::Errors::PermissionDenied"},"team":{"domain":"admin","id":133},"tenant":"cupix"}
{"t":"2026-07-28T13:06:15.267Z","ctrl":"Api::V1::TeamsController","act":"entity_parameters","http":403,"err":{"reason":"Permission denied","code":"PERM10000","class":"Cupix::Errors::PermissionDenied"},"team":{"domain":"trane","id":1075},"tenant":"cupix"}
에러 스팬 검색은 0건이었다 — 예외가 403 으로 handled 되어 status:error 스팬으로 태깅되지 않기 때문이다:
service:cupixworks-api status:error "PermissionDenied" → 0 spans
service:cupixworks-api "PermissionDenied" → 0 logs (error 레벨 미기록)
즉 발생 증거는 request 로그의 중첩 @error 객체(status:info [403] ...)에만 존재한다. 다수의 distinct team/tenant/endpoint 에 산발적으로 나타나며 특정 레코드 재진입이 아니라 광범위한 정상 인가 거부다.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | 의도적 인가 가드가 정상적으로 403 을 반환하며, Error Tracking 이 rescue 되는 예외까지 집계해 노출된 것 (noise) | ~40개 factory/repository 의 동일 raise PermissionDenied 패턴; client_error_controller.rb:26 이 403 으로 rescue; 7일 5,294건 전부 http:403; 다수 team/tenant 산발 |
— | Confirmed |
| H2 | 미처리 500 로 이어지는 코드 결함 | — | client_error_controller.rb:26 rescue_from 이 명시적으로 403 매핑; 모든 로그 http.status_code:403; status:error 스팬 0건 |
Rejected |
| H3 | Representative Error 가 stale 하고 실제 현재 메시지가 다름 | — | last_seen(2026-07-28 13:59:33Z) 로그가 rep PERM10000 "Permission denied" 와 정확히 일치; 메시지가 고정 코드 상수 |
Rejected |
| H4 | 특정 사용자/엔드포인트의 회귀(regression)로 인한 급증 | Admin::TeamsController#show 가 4,839건으로 편중 |
admin 엔드포인트에 비-admin 접근 시 정상 403; 여러 team/tenant 분산 → 클라이언트 접근 패턴, 코드 회귀 아님 | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 코드 변경 불필요. 이 이슈는 정상 인가 거부(403)이며 서버 결함이 아니다. Error Tracking 에서 Ignore 처리 권장.
단기 개선 (1주 이내)#
- (선택) Error Tracking 필터/샘플링에서
error.handling:handled또는 4xx client 예외(Cupix::Errors::PermissionDenied,Cupix::Errors::NotFound등)를 집계 대상에서 제외해 알람 노이즈를 줄인다. tesla 코드 변경이 아니라 Datadog 측 구성 변경이다.
장기 개선 (재발 방지)#
- 인가 거부(4xx client error)와 서버 오류(5xx)를 Error Tracking 레벨에서 명확히 분리하는 정책 수립. handled 4xx 예외는 기본적으로 이슈 집계에서 제외하도록 표준화.
Monitoring#
정상 인가 거부의 추세만 참고용으로 관찰 (알람 임계값 설정 불필요):
service:cupixworks-api @error.class:Cupix\:\:Errors\:\:PermissionDenied
특정 엔드포인트로의 비정상 급증 감지 (예: 인증 회귀로 정상 사용자까지 403 을 받는 경우):
service:cupixworks-api @error.class:Cupix\:\:Errors\:\:PermissionDenied @http.url_details.path:*
Risk Assessment#
- Risk level: low
- 예상 복잡도: trivial (코드 변경 없음, ignore/필터 구성만)
Noise Verdict#
noise — 권한 없는 사용자가 보호된 리소스에 접근할 때 발생하는 의도적 인가 가드로, 전부 HTTP 403 클라이언트 응답으로 정상 처리되며 코드 결함이 아니다.