ES /docs

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:38PERM10000 (/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#

  1. 2025-06-24 17:30 KST — 최초 발생 (first_seen).
  2. 2026-07-31 09:49 KST — 최근 발생 (last_seen, PUT /api/v1/me 403).
  3. 2026-08-06 09:11 KST — retention 창 내 마지막 관측 (PUT /api/v1/me 403, 조사 시점).

Error Log#

Datadog Logs

text
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:26rescue_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}/membersbefore_action :set_reviewReviewRepository#showBaseRepository.showreadable_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

app/controllers/api/v1/me_controller.rb:12-17ruby
def update
  @model = repository_instance.update(params)
  @session.show_option = true

  super
end

@modelbefore_action :set_user@current_user 자기 자신이 설정된다 (me_controller.rb:63-65). UserRepository#updatesuper (BaseRepository#update) 를 먼저 호출한 뒤 set_parameters 를 실행한다.

app/repositories/user_repository.rb:123-139ruby
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 필드 게이트다.

app/concerns/parameter/user.rb:52-55ruby
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)

app/controllers/api/v1/reviews_controller.rb:118-126ruby
def set_review
  # ...
  @model = repository_instance.show(params[:key], draft: _draft)
end

ReviewRepository#show (review_repository.rb:179) → superBaseRepository#show (base_repository.rb:121-129) → 클래스 메서드 self.class.showreadable_by? 로 read 권한을 검증한다. Failure point:

app/models/concerns/authenticated.rb:7-11ruby
def readable_by?(user)
  return nil if user.nil?

  Pundit.policy(user, self).read?
end
app/policies/application_policy.rb:38-55ruby
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 → showPERM10000 raise (review 열람 권한 없는 사용자의 멤버 조회) = 예상된 인가 거부.

공통 매핑 — PERM10000 → HTTP 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

기대 동작 = 권한 없는 요청을 403 으로 거부하고 서버 결함으로 계상하지 않음. 실제 동작 = 정확히 403 으로 매핑됨. 즉 서버 로직에 갭이 없다.

Log Evidence#

Datadog 쿼리 (재현용):

text
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건.

json
{
  "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"
  }
}
json
{
  "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:54application_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) PermissionDeniedclient_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 용):

text
service:cupixvista-api "PERM10000" "Api::V1::MeController#update"
text
service:cupixvista-api "PERM10000" "ReviewsController#members"

두 쿼리의 count 가 특정 시간대에 급증(burst)하면 프런트엔드 배포 회귀나 정책 변경으로 인한 광범위 거부 가능성을 시사한다. 평상시에는 저빈도 산발 유지가 정상이다.

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial (코드 변경 없음, ET ignore 권장)

Noise Verdict#

noise — 권한 없는 사용자의 요청이 이미 HTTP 403(PERM10000)으로 정상 매핑되는 예상된 인가 거부이므로 서버 코드 결함이 아니다.