ES /docs

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#

  1. 2025-02-04 22:53 KST — 이슈 first_seen (Error Tracking 최초 집계).
  2. 2026-07-28 22:59 KST — 이슈 last_seen. 이 시각 전후 로그가 Representative Error(PERM10000 Permission denied)와 정확히 일치.
  3. 2026-08-04 — RCA 수행. 지난 7일 5,294건 전부 HTTP 403 으로 정상 종결됨을 확인.

Error Log#

Datadog Logs

text
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 에 동일 패턴 존재). 이 예외는 ClientErrorControllerrescue_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 여부를 먼저 확인한다.
app/controllers/api/v1/admin/api_controller.rb:1-7ruby
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 전반에 존재.
app/repositories/admin/team_repository.rb:232-236ruby
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
app/factories/base_factory.rb:87ruby
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.
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
  • raise_error 는 403 JSON 을 렌더할 뿐 Cupix::Logger.error 를 호출하지 않는다 → error 레벨 로그가 남지 않고 request 로그(status:info [403] ...)만 남는다.
app/controllers/concerns/error_base_controller.rb:5-33ruby
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 과 분리):

text
service:cupixworks-api @error.class:Cupix\:\:Errors\:\:PermissionDenied

지난 7일 결과: count = 5,294, 전부 http.status_code:403. 코드/사유 분포:

text
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 와 정확히 일치:

json
{"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 스팬으로 태깅되지 않기 때문이다:

text
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#

정상 인가 거부의 추세만 참고용으로 관찰 (알람 임계값 설정 불필요):

text
service:cupixworks-api @error.class:Cupix\:\:Errors\:\:PermissionDenied

특정 엔드포인트로의 비정상 급증 감지 (예: 인증 회귀로 정상 사용자까지 403 을 받는 경우):

text
service:cupixworks-api @error.class:Cupix\:\:Errors\:\:PermissionDenied @http.url_details.path:*

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (코드 변경 없음, ignore/필터 구성만)

Noise Verdict#

noise — 권한 없는 사용자가 보호된 리소스에 접근할 때 발생하는 의도적 인가 가드로, 전부 HTTP 403 클라이언트 응답으로 정상 처리되며 코드 결함이 아니다.