ES /docs

Cupix::Errors::PermissionDenied: Permission denied

RCA: Cupix::Errors::PermissionDenied: Permission denied

Overview#

What Happened#

cupixworks-api에서 Cupix::Errors::PermissionDenied (code PERM10000) 예외가 반복 발생하며 Error Tracking 이슈로 묶였다. 최근 로그를 확인하면 거의 전부가 GET /api/v1/admin/teams/{id} (Api::V1::Admin::TeamsController#show) 요청에서 발생하며, API가 권한이 없는 요청자에게 HTTP 403을 정상 반환한 케이스다. 로그 레벨은 모두 info이고 status:error 로그는 존재하지 않는다. 이는 인가(authorization) 정책이 의도대로 동작한 결과이며 코드 결함이 아니다.

Quick Facts#

Field Value
exception.class Cupix::Errors::PermissionDenied
exception.message Permission denied
error.code PERM10000
top_frame app/repositories/base_repository.rb:359-360
http_status 403
log_level info (error 로그 없음)
env production

Affected Teams#

Team / Domain Error Count Impact
admin (/api/v1/admin/teams/*) 대다수 (샘플 50건 중 45+건) 권한 없는 team 상세 조회 요청이 403으로 정상 거부됨. 사용자 데이터/상태 영향 없음
facilities / captures publish 각 1건 trash/publish/unpublish 시 권한 없는 요청 403 거부

Timeline#

  1. 2025-01-21 12:16 KST — 최초 발생 (first_seen). Representative Error 는 이 시점의 오래된 샘플.
  2. 2026-07-29 13:07 KST — 클러스터 기준 최근 발생 (last_seen).
  3. 2026-08-04 (RCA 시점) — 최근 14일 로그에서도 동일 패턴 지속 확인 (GET /api/v1/admin/teams/{id} 403 반복).

Error Log#

Datadog Logs

text
Permission denied

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 20 (클러스터 기준)
  • 최초 발생: 2025-01-21 12:16 KST
  • 최근 발생: 2026-07-29 13:07 KST

Root Cause Summary#

이 예외는 코드 버그가 아니라 정상적인 인가 거부다. Api::V1::Admin::TeamsController#showset_team before_action 이 Admin::TeamRepository#show(params[:id]) 를 호출하고, 이는 BaseRepository.show (base_repository.rb:359-360) 에서 Pundit.policy(current_user, model).read?false 이면 Cupix::Errors::PermissionDenied (PERM10000) 를 raise 한다. 이 예외는 ClientErrorController (client_error_controller.rb:26,69-71) 에서 permission_denied_403_error 로 rescue 되어 HTTP 403 으로 렌더링되고 info 레벨로 기록된다. 즉 권한 없는 사용자가 자신이 접근할 수 없는 team(예: team 469, 830, 691)을 조회하려 할 때 API가 접근을 올바르게 차단한 것이다. Datadog Error Tracking 은 응답에 exception class 가 실려 있어 이를 이슈로 묶었을 뿐, 서버 오류(5xx)나 nil reference 같은 결함은 없다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/admin/teams_controller.rb:8before_action :set_team
  • set_team 이 repository show 를 호출:
app/controllers/api/v1/admin/teams_controller.rb:91-93ruby
def set_team
  @model = repository_instance.show(params[:id])
end
  • Repository 위임 후 클래스 메서드 BaseRepository.show 로 진입하여 Pundit read? 정책을 검사:
app/repositories/base_repository.rb:359-361ruby
if !skip_permission && current_user.present? && !Pundit.policy(current_user, model).read?
  raise Cupix::Errors::PermissionDenied.new(code: 'PERM10000', reason: 'Permission denied')
end
  • Failure point (의도된 raise): app/repositories/base_repository.rb:360
  • 이 예외는 4xx client-error 핸들러에서 rescue 되어 403 으로 변환:
app/controllers/concerns/client_error_controller.rb:26,69-71ruby
rescue_from Cupix::Errors::PermissionDenied, with: :permission_denied_403_error
# ...
def permission_denied_403_error(exception)
  raise_error(403, exception)
end
  • 기대 동작 vs 실제 동작: 기대 동작 = 권한 없는 요청은 403 으로 거부. 실제 동작 = 정확히 403 으로 거부됨. 둘이 일치하므로 결함 없음. 유일한 부작용은 exception class 가 응답 메타에 실려 Error Tracking 이 이를 "error" 처럼 묶는다는 점.

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api "Permission denied"
text
service:cupixworks-api "Permission denied" status:error

첫 번째 쿼리는 최근 14일 내 다수 결과 반환, 두 번째 쿼리(status:error)는 0건 — 즉 error 레벨 로그가 전혀 없다.

최근(last_seen 이후에도 지속) 발생 로그 원문 — 모두 status:info, HTTP 403:

json
{
  "timestamp": "2026-08-04 16:42:18",
  "status": "info",
  "message": "[403] GET /api/v1/admin/teams/469 (Api::V1::Admin::TeamsController#show)",
  "error": {
    "reason": "Permission denied",
    "code": "PERM10000",
    "message": "Permission denied",
    "class": "Cupix::Errors::PermissionDenied"
  }
}

엔드포인트 분포 (샘플 50건):

text
  8  [403] GET /api/v1/admin/teams/469 (Api::V1::Admin::TeamsController#show)
  7  [403] GET /api/v1/admin/teams/830 (Api::V1::Admin::TeamsController#show)
  7  [403] GET /api/v1/admin/teams/691 (Api::V1::Admin::TeamsController#show)
  6  [403] GET /api/v1/admin/teams/32  (Api::V1::Admin::TeamsController#show)
  ...
  1  [403] PUT /api/v1/facilities/po00en/trash (Api::V1::FacilitiesController#trash)
  1  [403] PUT /api/v1/captures/82636/publish (Api::V1::CapturesController#publish)
  1  [403] DELETE /api/v1/captures/82636/publish (Api::V1::CapturesController#unpublish)

압도적 다수가 admin/teams/{id} 조회이며 소수의 facilities/captures 권한 거부가 섞여 있다. 모두 read?/권한 정책에 따른 정상적 접근 차단이다.

Representative Error 불일치 참고: 클러스터의 Representative Error 는 Permission denied 문자열뿐이며, 이는 first_seen(2025-01-21) 시점의 오래된 샘플이다. last_seen 이후의 실제 최근 발생 메시지도 동일한 Permission denied / PERM10000 이므로 근본 성격(정상 403 인가 거부)에는 변화가 없다.

Status Board#

bun run cli/incident-board.ts for-cluster 결과 scope 는 svc:cupixworks-api::unknown, active: null. 최근 resolved 인시던트(2026-07-29, 2026-07-30 "service degraded")는 이 클러스터를 포함하지 않으며 별개 사건이다. 외부 의존성 outage 아님.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 정상적인 인가 거부(권한 없는 요청에 대한 403) — 코드 결함 아님 모든 발생이 status:info/HTTP 403; base_repository.rb:359-360 Pundit read? 실패 시 의도적 raise; client_error_controller.rb:26 이 4xx client error 로 분류 status:error 로그 0건, 5xx 없음, nil/logic 오류 흔적 없음 Confirmed
H2 서버 측 결함(nil reference, 잘못된 정책 로직)으로 인해 잘못 403 반환 error 레벨 로그 없음; 요청이 특정 team id(469, 830, 691)에 반복 집중되어 요청자별 권한 부재 패턴에 부합; show 는 model 을 정상 조회한 뒤 정책 검사에서만 거부 Rejected
H3 외부 의존성 outage 로 인한 대량 발생 status-board active: null, dep scope 아님; 인증/DB 오류가 아니라 인가 로직 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 코드 변경 불필요. 이 예외는 의도된 인가 거부이며 API 동작은 정확하다.

단기 개선 (1주 이내)#

  • Error Tracking 노이즈 억제: Cupix::Errors::PermissionDenied (및 유사 4xx client error 인 Cupix::Errors::NotFound 403) 는 이미 info 레벨로 기록되고 있으므로, Datadog Error Tracking 에서 이 exception class 를 error 로 수집하지 않도록 이슈를 Ignore/Archive 처리하거나 error-tracking exclusion filter(예: @error.code:PERM10000 제외)를 추가하는 것을 권장한다.
  • 프런트엔드/클라이언트가 권한 없는 team id 로 반복 조회하는 패턴(team 469/830/691 등)이 UX 버그인지 별도 확인은 선택 사항이나, 이는 API 결함이 아닌 클라이언트 조회 로직 이슈다.

장기 개선 (재발 방지)#

  • Error Tracking 수집 파이프라인에서 HTTP 4xx (특히 403 인가 거부) 응답에 실린 exception class 를 자동으로 error 클러스터에서 제외하는 규칙을 추가하면 동일 유형의 노이즈 이슈 생성을 방지할 수 있다.

Monitoring#

정상 동작이므로 알림은 불필요하나, 403 비율이 비정상적으로 급증하면(정책 회귀 또는 클라이언트 버그 신호) 파악할 수 있도록 추이 위젯을 둘 수 있다.

text
sum:trace.rack.request.hits{service:cupixworks-api,http.status_code:403}.as_count()
text
sum:trace.rack.request.hits{service:cupixworks-api,resource_name:api/v1/admin/teams/show}.as_count()

Risk Assessment#

  • Risk level: low (정상 인가 동작, 사용자 영향 없음)
  • 예상 복잡도: trivial (코드 수정 없음, Error Tracking 노이즈 정리만)

Noise Verdict#

noise — 권한 없는 요청에 대해 API가 의도대로 HTTP 403(PERM10000, info 레벨)을 반환한 정상적인 인가 거부이므로 코드 수정이 필요 없는 노이즈다.