ES /docs

recipe deletion failed: {"message":"Forbidden"}

RCA: recipe deletion failed: {"message":"Forbidden"}

Overview#

What Happened#

2026-05-08 07:06:37 UTC에 cupixworks-api 서비스(us-west-2)에서 admin 사용자가 팀 1173의 assigned_customer_success_managers 그룹에서 사용자(ID: 751)를 제거하는 과정에서, 해당 사용자의 notification recipe 삭제가 3건 모두 실패했다. notification-service의 API Gateway authorizer가 user 751의 api_token을 거부하여 {"message":"Forbidden"} 응답을 반환했다.

Quick Facts#

Field Value
exception.class Cupix::NotificationService
exception.message recipe deletion failed: {"message":"Forbidden"}
top_frame lib/cupix/notification_service.rb:65
env production, us-west-2
deploy production-us-west-2-20260508T0527Z0-cc7eab2d-cupixworks

Timeline#

  1. 07:06:37Z — Admin user (daniel.kim@cupix.com)가 PUT /api/v1/admin/teams/1173/groups/assigned_customer_success_managers/remove_users 호출
  2. 07:06:37Z — User 751의 cached permissions flush 실행
  3. 07:06:37ZUserRecipeCleaner가 3개 recipe 삭제 시도 (facility_new_project, record_processing_completed, record_preview_ready) — 모두 Forbidden으로 실패
  4. 07:06:37Z — 전체 HTTP 요청은 200으로 정상 완료 (recipe 삭제 실패는 비차단 부작용)

Error Log#

Datadog Logs

text
recipe deletion failed: {"message":"Forbidden"}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 3
  • 최초 발생: 2026-05-08T07:06:37.639Z
  • 최근 발생: 2026-05-08T07:06:37.639Z
  • 사용자 영향: 없음. recipe 삭제 실패는 비차단 side-effect로, 사용자 그룹 제거 자체는 정상 완료됨. 다만 user 751의 notification recipe가 orphan 상태로 남아 불필요한 알림을 계속 수신할 수 있음.

Root Cause Summary#

Admin 사용자가 CSM 그룹에서 user 751을 제거할 때, UserRecipeCleaner PubSub subscriber가 Cupix::NotificationService#delete_user_recipe를 호출한다. 이 메서드는 user 751의 api_tokenx-cupix-auth 헤더로 사용하여 notification-service에 인증하는데, notification-service의 Lambda authorizer가 이 토큰으로 Tesla API /api/v1/me를 호출한 결과 비정상 응답(non-200)을 받아 { isAuthorized: false }를 반환했다. AWS API Gateway는 이를 403 {"message":"Forbidden"}으로 변환하여 응답했다.

근본 원인은 user 751의 api_token이 유효하지 않거나, 해당 토큰에 연결된 session이 만료/비활성 상태라 Tesla API /api/v1/me 호출이 실패한 것이다. NotificationService는 항상 대상 사용자의 토큰으로 인증하므로, 토큰이 무효한 사용자에 대해서는 recipe CRUD가 불가능한 구조적 결함이 있다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/admin/groups_controller.rb:38remove_users action
  • app/repositories/admin/group_repository.rb:55-57 — 각 사용자에 대해 @model.users.destroy(user) 호출
app/repositories/admin/group_repository.rb:55-57ruby
users.each do |user|
  @model.users.destroy(user)
end
  • GroupedUser destroy 시 PubSub destroyed 이벤트 발행
  • lib/cupix/pub_sub/subscribers/user_recipe_cleaner.rb:9-10destroyed 이벤트 수신
lib/cupix/pub_sub/subscribers/user_recipe_cleaner.rb:9-10ruby
def destroyed(event)
  send("_delete_recipes_for_#{self.namespace}", event.payload[:model])
end
  • lib/cupix/pub_sub/subscribers/user_recipe_cleaner.rb:15-21 — CSM 그룹인 경우 recipe 삭제 실행
lib/cupix/pub_sub/subscribers/user_recipe_cleaner.rb:34-41ruby
def _delete_recipes(opts = {})
  %w[
    record_preview_ready
    record_processing_completed
    facility_new_project
  ].each do |recipe_name|
    Cupix::NotificationService.new(user: opts[:user]).delete_user_recipe(recipe_name, team_id: opts[:team_id], facility_key: opts[:facility_key])
  end
end
  • lib/cupix/notification_service.rb:48-68delete_user_recipe 메서드. 먼저 find_email_recipe GET 호출 후 DELETE 호출
lib/cupix/notification_service.rb:48-68ruby
def delete_user_recipe(recipe_name, team_id: nil, facility_key: nil)
  return if @service_url.nil?

  recipe = find_email_recipe(recipe_name, facility_key)

  return if recipe.nil?

  response = RestClient.delete(
    "#{@service_url}/api/recipes/v2/#{recipe['id']}",
    {
      content_type: :json,
      'x-cupix-auth': @user.api_token
    }
  )

  Cupix::Logger.info("recipe deleted: #{response.body}", class: self.class.name, function: __method__, recipe_name: recipe_name, facility_key: facility_key)
rescue RestClient::ExceptionWithResponse => e
  Cupix::Logger.error("recipe deletion failed: #{e.response.body}", class: self.class.name, function: __method__, recipe_name: recipe_name, facility_key: facility_key)

  false
end
  • Failure point: lib/cupix/notification_service.rb:127-133find_email_recipe의 GET 요청이 user 751의 api_token으로 인증 시도
lib/cupix/notification_service.rb:127-133ruby
response = RestClient.get(
  "#{@service_url}/api/recipes/v2?#{params.to_query}",
  {
    content_type: :json,
    'x-cupix-auth': @user.api_token
  }
)
  • find_email_recipe에는 자체 rescue 블록이 없으므로, GET 요청에서 발생한 RestClient::ExceptionWithResponse(403)가 delete_user_recipe의 rescue로 전파되어 "recipe deletion failed" 에러 로그가 기록됨.

  • Downstream: notification-service/src/lambda/authorizer.ts:11-43 — API Gateway Lambda authorizer

applications/notification-service/src/lambda/authorizer.ts:19-32typescript
const response = await CPUtils.fetchWithRetry(
  `https://api.${DOMAIN_NAME}/api/v1/me?fields=id,firstname,lastname,email,team`,
  {
    headers: {
      'x-cupix-auth': token
    }
  }
);

console.log(`Tesla API responded in ${Date.now() - startTime}ms`);
console.log(`response: ${JSON.stringify(response)}`);

if (response.status !== 200) {
  throw new Error('Unauthorized; permission denied');
}
  • 기대 동작: user 751의 api_token으로 Tesla /api/v1/me 호출 → 200 응답 → 인증 성공 → recipe 조회/삭제 진행
  • 실제 동작: user 751의 api_token으로 Tesla /api/v1/me 호출 → non-200 응답 → authorizer가 { isAuthorized: false } 반환 → API Gateway가 403 {"message":"Forbidden"} 응답

Log Evidence#

사용한 Datadog 쿼리:

text
service:cupixworks-api "recipe deletion failed" @environment:production
text
service:cupixworks-api @request_id:add3120c-c804-4bfe-a1a9-b07abd237f4d

에러 로그 (3건, 동일 request_id):

json
{
  "message": "recipe deletion failed: {\"message\":\"Forbidden\"}",
  "class": "Cupix::NotificationService",
  "function": "delete_user_recipe",
  "recipe_name": "facility_new_project",
  "host": "ip-10-1-80-134.us-west-2.compute.internal",
  "request_id": "add3120c-c804-4bfe-a1a9-b07abd237f4d",
  "timestamp": "2026-05-08T07:06:37.639Z"
}

동일 request 내 context 로그:

text
[INFO] Flush cached permissions for User 751 (function: flush_cached_permissions, class: Module)
[INFO] Deleting cached_permission on user 751 (function: flush_cached_permission, class: User)

관련 요청 정보:

text
[200] PUT /api/v1/admin/teams/1173/groups/assigned_customer_success_managers/remove_users
Controller: Api::V1::Admin::GroupsController#remove_users
User: daniel.kim@cupix.com (id: 43867)
User-Agent: python-httpx/0.28.1
Duration: 3680.01ms

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 User 751의 api_token이 무효하거나 session이 만료됨 API Gateway authorizer가 Tesla /api/v1/me 호출 시 non-200을 받아 isAuthorized: false 반환 (authorizer.ts:31-32). 응답이 {"message":"Forbidden"} — AWS API Gateway의 표준 unauthorized 응답 형식. Confirmed
H2 notification-service 자체의 recipe 권한 검사 실패 notification-service의 proxy.ts:100-103ItemNotFoundException을 403으로 매핑하지만 그 body는 {"message":"Item not found"}이지 {"message":"Forbidden"}이 아님. {"message":"Forbidden"}은 API Gateway authorizer 거부의 표준 응답. Rejected
H3 notification-service URL이 잘못되어 다른 서비스가 403 반환 다른 recipe 관련 호출(find_email_recipe)이 동일 시간대에 다른 사용자에 대해서는 정상 동작 중 (info 로그에서 "recipe found: null" 확인). URL 자체는 정상. Rejected
H4 fetchWithRetry timeout(1초)으로 인한 실패 authorizer의 timeout이 1000ms로 매우 짧아 Tesla API 지연 시 실패 가능 timeout 시 abort error가 발생하며, 이는 catch에서 isAuthorized: false로 처리되지만, 동일 시간대 다른 요청에서는 authorizer가 정상 동작하므로 network 문제는 아님. 3건이 모두 동일하게 실패한 것은 token 자체의 문제를 시사. Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • lib/cupix/notification_service.rb:48-68: delete_user_recipe에서 find_email_recipe 호출을 별도 rescue로 감싸거나, find_email_recipe 메서드 자체에 rescue 블록을 추가하여 인증 실패 시 에러 대신 nil을 반환하도록 처리. 이렇게 하면 인증 실패가 "recipe deletion failed"가 아닌 적절한 메시지로 로깅됨.
  • 에러 레벨을 warn으로 낮출 것을 권장. recipe 삭제 실패는 비차단 side-effect이며 메인 비즈니스 로직(사용자 그룹 제거)에 영향을 주지 않으므로 error 레벨은 과도함.

단기 개선 (1주 이내)#

  • NotificationService가 대상 사용자의 api_token 대신 서비스 계정 토큰 또는 admin token으로 인증하도록 변경. 현재 구조에서는 대상 사용자의 토큰이 무효한 경우 해당 사용자의 recipe를 관리할 수 없는 구조적 한계가 있음.
  • notification-service authorizer에 service-to-service 인증 방식(예: API key 또는 IAM role 기반)을 추가하여, 서버 간 내부 호출이 개별 사용자 토큰에 의존하지 않도록 개선.

장기 개선 (재발 방지)#

  • notification-service의 recipe lifecycle 관리를 이벤트 기반으로 전환. cupixworks-api가 직접 HTTP 호출하는 대신, 이벤트(SNS/SQS)를 발행하고 notification-service가 소비하여 자체 권한으로 recipe를 정리하는 방식.
  • api_token 기반 인증의 취약점(토큰 만료, 사용자 비활성화 등) 대응을 위한 token refresh 또는 fallback 메커니즘 검토.

Monitoring#

  • Datadog 쿼리로 recipe deletion 실패 추이 모니터링:
text
service:cupixworks-api "recipe deletion failed" @environment:production
  • notification-service authorizer 실패 모니터링:
text
service:notification-service "Error authorizing" @environment:production
  • user 751의 token 상태 확인 후, 필요 시 token refresh 수동 실행

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: standard
  • 사용자 영향 없음 (메인 작업은 정상 완료). orphan recipe로 인한 불필요 알림 가능성은 있으나 즉각적 장애는 아님.