ES /docs

TransferManager::uploadNewPointclouds | {"stack":"HttpError: HTTP request failed

RCA: TransferManager::uploadNewPointclouds HttpError 403

Overview#

What Happened#

2026-06-26 09:53 KST, cupixworks-capture-3dreconstruction-instance 에이전트가 3D 재구성 결과의 새 pointcloud 그룹을 생성하기 위해 cupixworks-api POST /api/v1/pointclouds를 호출했으나 403 Permission Denied 응답을 받고 작업이 실패했다. 에이전트는 UploadNewPointcloudsContainer::createGroupPointcloud 단계에서 group pointcloud 생성에 실패해 업로드 task 생성 전에 reject 되었고, 동일 시각에 ThreeDReconstruction::run | end가 error 레벨로 함께 기록되며 해당 job은 updateErrorActionJob 으로 종료되었다.

Quick Facts#

Field Value
exception.class HttpError
exception.message HTTP request failed
top_frame pointcloudApi.js:874:40 (@tesla/typescript-node-sdk createPointcloud)
upstream cause Cupix::Errors::PermissionDenied / PERM10000 at PointcloudFactory#create!
policy RecordPolicy#create? returned false
runtime Node.js agent on EC2 instance (/tmp/agent/dist/...)
env production, region ap-southeast-2, tenant nswgov (cluster frontmatter)

Affected Teams#

Team / Domain Error Count Impact
sinsw / nswgov (ap-southeast-2) 1 단일 capture 의 3D 재구성 job 종료 실패 — pointcloud 미업로드

Timeline#

  1. 2026-06-26 09:53:50 KST — 에이전트가 TransferManager::uploadNewPointclouds 호출 → UploadNewPointcloudsContainer::createGroupPointcloudPOST /api/v1/pointclouds 요청.
  2. 2026-06-26 09:53:51 KST — cupixworks-api 가 [403] POST /api/v1/pointclouds (Api::V1::PointcloudsController#create) 응답. Cupix::Errors::PermissionDenied (PERM10000).
  3. 2026-06-26 09:53:52 KSTRecordPolicy#create?Permission denied on creating a model Record info 로그를 남김.
  4. 2026-06-26 09:53:50 KST — 에이전트 측에서 HttpErrorTransferManager::uploadNewPointclouds catch 로 전파되어 error 로그 기록.
  5. 2026-06-26 09:53:50 KSTThreeDReconstruction::run | end 가 error 레벨로 기록되며 jobManager.updateErrorActionJob 호출.

Error Log#

Datadog Logs

text
TransferManager::uploadNewPointclouds | {"stack":"HttpError: HTTP request failed
    at Request._callback (/tmp/agent/dist/node_modules/.pnpm/@tesla+typescript-node-sdk@1.13.3-SNAPSHOT.202605301249_1e9855b03300e7077469e8a23e5d7e36/node_modules/@tesla/typescript-node-sdk/api/pointcloudApi.js:874:40)
    at self.callback (/tmp/agent/dist/node_modules/.pnpm/request@2.88.2/node_modules/request/request.js:185:22)
    at Request.emit (node:events:524:28)
    at Request.emit (node:domain:489:12)
    at Request.<anonymous> (/tmp/agent/dist/node_modules/.pnpm/request@2.88.2/node_modules/request/request.js:1154:10)
    at Request.emit (node:events:524:28)
    at Request.emit (node:domain:489:12)
    at IncomingMessage.<anonymous> (/tmp/agent/dist/node_modules/.pnpm/request@2.88.2/node_modules/request/request.js:1076:12)
    at Object.onceWrapper (node:events:638:28)
    at IncomingMessage.emit (node:events:536:35)","message":"HTTP request failed","response":{"body":{},"statusCode":403},"body":{},"statusCode":403,"name":"HttpError"}

Impact#

  • Service: cupixworks-capture-3dreconstruction-instance
  • Team: sinsw
  • 발생 횟수: 1
  • 최초 발생: 2026-06-26 09:53 KST
  • 최근 발생: 2026-06-26 09:53 KST

영향 범위는 단일 인스턴스의 단일 3D 재구성 job. cupixworks-api 측 7일 검색 결과 POST /api/v1/pointclouds 의 403 은 본 시점 1건만 관측되어 광범위한 정책 회귀가 아닌 단일 record 의 권한 상태 문제로 보인다.

Root Cause Summary#

에이전트가 createGroupPointcloud 단계에서 cupixworks-api 의 POST /api/v1/pointclouds 를 호출하자 PointcloudFactory#create! 가 부모 Record 에 대해 Pundit.policy(current_user, parent).create? 를 평가했고, 해당 Recordapplied_permission 배열에서 write/all bit ([3], [4]) 가 모두 0 이라 RecordPolicy 가 상속한 ApplicationPolicy#create?false 를 반환했다. 그 결과 Cupix::Errors::PermissionDenied (PERM10000) 가 발생하고 API 가 403 을 응답했다. 에이전트의 CupixAuth::retryablestatusCode > 500 만 재시도하므로 403 은 즉시 reject 되고 ThreeDReconstruction::run 의 catch 블록이 실행되어 job 이 error 상태로 종료되었다. 즉 코드 결함이 아니라, 에이전트가 사용하는 자격(token)의 user 가 해당 record 에 대해 write/all 권한이 없거나, record 의 applied_permission 이 예상과 다르게 셋업된 상태가 근본 원인이다.

Technical Analysis#

Code Path#

Entry point (agent 측): three-d-reconstruction-service.ts:378uploadNewPointclouds 호출.

applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/three-d-reconstruction-service.ts:373-393typescript
private uploadPointcloudFiles = async (cpCapture: CPCapture): Promise<void> => {
    for (let index = 0; index < cpCapture.cpClusters.length; index++) {
        const cpCluster = cpCapture.cpClusters[index];
        if (cpCluster.cpPointclouds.length > 0) {
            logger.debug('ThreeDReconstruction::uploadPointcloudFiles | upload new pointclouds - cluster id: %d, pointcloud size: %d', cpCluster.id, cpCluster.cpPointclouds.length);
            await this.transferManager.uploadNewPointclouds(cpCluster, cpCluster.cpPointclouds);
            // ... 후속 업로드는 본 단계 실패 시 도달하지 않음
        }
    }
};

TransferManager::uploadNewPointclouds 는 컨테이너를 만들고 init() 단계의 reject 를 그대로 외부로 전파한다.

applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer.manager.ts:265-277typescript
uploadNewPointclouds = (cpCluster: CPCluster, items: CPPointcloud[]): Promise<void> => new Promise((resolve, reject) => {
    const container = new UploadNewPointcloudsContainer(this, cpCluster, items, resolve, reject);
    container.init()
        .then(() => {
            container.tasks.forEach(task => {
                this.addTask(task);
            });
        })
        .catch(ec => {
            logger.error('TransferManager::uploadNewPointclouds | %s', JSON.stringify(ec, Object.getOwnPropertyNames(ec)));
            reject();
        });
});

컨테이너 초기화는 group pointcloud 생성 API 호출을 포함한다 — 이 호출이 실패의 직접 원인이다.

applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/manager/transfer/upload-new-pointclouds.container.ts:26-40typescript
private createGroupPointcloud = async (): Promise<void> => {
    const cpCapture = this.cpCluster.cpCapture;
    const createPointcloudRequest = {
        kind: 'group',
        name: cpCapture?.name + '_' + this.cpCluster.name,
        pointcloud_type: TESLA.PointcloudType._3dReconstructed,
        level_id: cpCapture?.srvLevel?.id as number,
        record_id: cpCapture?.srvRecord?.id as number,
        capture_id: cpCapture?.id as number,
        cluster_id: this.cpCluster?.id as number
    };
    logger.debug('UploadNewPointcloudsContainer::createGroupPointcloud | createPointcloudRequest - %s', JSON.stringify(createPointcloudRequest));
    this._groupPointcloud = await this.cupixApi.pointcloud.create(createPointcloudRequest);

pointcloud.create 는 SDK 의 createPointcloud 를 호출하고, 응답을 unwrap 한다.

applications/agents/packages/api/src/api/pointcloud.api.ts:140-144typescript
async create(createPointcloudRequest: TESLA.CreatePointcloudRequest): Promise<TESLA.Pointcloud> {
    const api = await this.api();
    const res = await api.createPointcloud(Fields.PointcloudFields, createPointcloudRequest);
    return unwrapAttributes(res);
}

서버 측(tesla) Failure point — PointcloudFactory#create! 에서 record 에 대한 정책 검사:

app/factories/pointcloud_factory.rb:6-21ruby
def create!(params = {})
    self.model = ::Pointcloud.new kind: params[:kind]

    raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'level is required') if params[:level_id].blank?

    if params[:record_id].present?
      self.parent = RecordRepository.new(current_user: self.current_user).show(params[:record_id])
    else
      raise Cupix::Errors::Parameter.new(code: 'ARG10000', reason: 'record_id is required')
    end

    self.model.record = self.parent
    self.model.facility = self.parent.facility

    raise Cupix::Errors::PermissionDenied.new(code: 'PERM10000', reason: 'Permission denied') unless Pundit.policy(self.current_user, self.parent).create?

RecordPolicy#create? 는 별도 override 없이 ApplicationPolicy#create? 를 상속해 적용한다.

app/policies/application_policy.rb:13-36ruby
def create?
    return true if admin_administrator?
    return false if archived_entity?

    if record.has_attribute?(:applied_permission)
      if record.applied_permission[3] == 1 || record.applied_permission[4] == 1
        true
      else
        Cupix::Logger.info(
          "Permission denied on creating a model #{record.class.name}",
          class: self.class.name,
          function: __method__,
          user: { id: user.id, email: user.email }
        )
        false
      end
    else
      true
    end
end

기대 동작: 에이전트가 인증한 user 가 Record 에 대해 write(applied_permission[3]==1) 또는 all(applied_permission[4]==1) 권한을 보유해 create? 가 true 를 반환해야 한다.

실제 동작: 두 bit 모두 0(또는 user 가 admin_administrator? 아님 및 record 가 archived) 으로 create? 가 false 를 반환 → 403 PermissionDenied.

또한 에이전트의 재시도 정책은 5xx 에만 동작하므로 403 에서는 즉시 실패한다.

applications/agents/packages/api/src/authentication/cupix-auth.ts:59-77typescript
retryable = <T>(f: () => Promise<T>, retries?: number): Promise<T> => new Promise((resolve, reject) => {
    const _retries = retries != undefined ? retries : 0;
    f()
        .then(response => resolve(response))
        .catch(e => {
            if (e && e.statusCode > 500 && _retries < Constants.MaxRetries) {
                // ... retry only on >500
            } else {
                reject(e);
            }
        });
});

상위 ThreeDReconstruction::run 의 catch 가 error 로그를 남기고 job 을 error 상태로 마무리한다.

applications/agents/packages/cupix-capture-3d-reconstruction-agent/src/three-d-reconstruction-service.ts:126-132typescript
logger.info('ThreeDReconstruction::run | end');
await this.jobManager.updateCompleteActionJob(this.ActionName, TESLA.UpdateJobRequest.StateEnum.Stopped);
} catch (error: any) {
    logger.error('ThreeDReconstruction::run | end', error);
    await this.jobManager.updateErrorActionJob(this.ActionName);
} finally {
    await this.removeWorkspaceAndPointCloudDir(cpCapture!);

Log Evidence#

Datadog 쿼리 (재현용):

text
service:cupixworks-capture-3dreconstruction-instance status:error "TransferManager" "uploadNewPointclouds"
text
service:cupixworks-api "/pointclouds" @http.status_code:403
text
service:cupixworks-api "Permission denied on creating a model Record"

에이전트 측 에러 원문 (status:error, 2026-06-26 09:53:50 KST):

json
{
  "message": "HTTP request failed",
  "response": { "body": {}, "statusCode": 403 },
  "body": {},
  "statusCode": 403,
  "name": "HttpError"
}

같은 초의 cupixworks-api 응답 로그 (2026-06-26 09:53:51 KST):

json
{
  "message": "[403] POST /api/v1/pointclouds (Api::V1::PointcloudsController#create)",
  "error": {
    "reason": "Permission denied",
    "code": "PERM10000",
    "message": "Permission denied",
    "class": "Cupix::Errors::PermissionDenied"
  }
}

Pundit 정책 거부 정보 로그 (2026-06-26 09:53:52 KST):

json
{
  "message": "Permission denied on creating a model Record",
  "class": "RecordPolicy",
  "function": "create?"
}

같은 인스턴스에서 직후 동시 발생한 ThreeDReconstruction::run | end 가 error 레벨로 기록 — 동일 시각의 형제 클러스터 3d3f45ac-fde9-4c64-a226-f1b9d96030a7 와 일치.

7일 범위에서 POST /api/v1/pointclouds 403 은 본 1건만 관측 (service:cupixworks-api "[403] POST /api/v1/pointclouds" → 1 hit). 정책 회귀(regression)가 아닌 단일 record 의 권한 상태 문제임을 시사.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 cupixworks-api 가 Pundit RecordPolicy#create? 를 통해 403 PermissionDenied 를 반환 — 에이전트 user 가 해당 Record 의 applied_permission write/all bit 미보유 API 로그 [403] POST /api/v1/pointclouds … PERM10000, 직후 Permission denied on creating a model Record (RecordPolicy#create?) 로그, PointcloudFactory#create! line 20 의 명시적 raise 없음 Confirmed
H2 토큰 만료/인증 실패로 인한 403 401 이 아닌 403, 같은 인스턴스에서 직전 CupixAuth::setSession session_id: … 가 정상 기록, handleError 가 별도 인증 실패 경로를 타지 않음 Rejected
H3 광범위한 권한 정책 회귀(코드 배포) 7일간 POST /api/v1/pointclouds 403 이 1건만 발생, 같은 시각 동일 controller #index 는 200 응답 다수 Rejected
H4 record/level/capture/cluster id 불일치로 인한 ARG10000 파라미터 검증 실패가 403 으로 잘못 분류 응답 error class 가 Cupix::Errors::PermissionDenied 이며 code 가 PERM10000 으로 명시 — Parameter 검증 실패라면 ARG10000 이 와야 함 Rejected
H5 외부 의존성(S3, ELB) 일시 장애 status-board dep:* 매칭 없음, 동일 에이전트의 같은 분 다른 API 호출(setSession, JobManager) 이 정상 동작 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • 영향 받은 record (capture/cluster 의 부모 record) 의 권한 상태를 운영팀이 직접 확인. 후보:
    • Record.applied_permission 의 write/all bit 값
    • 해당 user 가 속한 team 의 role/permission 매핑
    • record 의 applied_cycle_statearchived/archiving 인지 (archived 이면 ApplicationPolicy#create? 가 즉시 false 반환)
  • 코드 변경은 불필요. 데이터/권한 설정 수정으로 해결.
  • 운영팀이 cluster frontmatter 의 regions: ap-southeast-2, tenant: nswgov, 그리고 직후 동시 시각의 형제 cluster 와 함께 capture/record id 를 확인. 동일 인스턴스의 같은 분 (09:53:50–09:53:52 KST) info 로그에 JobManager::loadJob 가 없으므로 어떤 job id 인지는 더 좁힌 검색 필요 (service:cupixworks-capture-3dreconstruction-instance "loadJob" -f "2026-06-26T00:50:00,2026-06-26T00:55:00").

단기 개선 (1주 이내)#

  • UploadNewPointcloudsContainer::createGroupPointcloudpointcloud.create 실패 시 단순 에러 외에 요청 컨텍스트(record_id, capture_id, cluster_id, level_id)를 함께 남기도록 로그 보강. 현재 JSON.stringify(ec, Object.getOwnPropertyNames(ec)) 만 남겨 어느 record 가 거부됐는지 사후 추적이 어렵다.
  • cupixworks-api Cupix::Logger.info("Permission denied on creating a model …") 가 user.id/email 외에 record.id 도 함께 남기도록 보강 — 현재 user 만 기록되어 record 식별이 불가하다 (참고: application_policy.rb:21-29read? 는 model.id 를 남기지만 create? 는 남기지 않음).
  • 운영 가시성: 403 발생 시 에이전트는 즉시 job 을 error 종료하므로, 동일 record 에 대한 재시도 차단 정책 또는 명시적 사용자 알림 경로가 필요한지 검토.

장기 개선 (재발 방지)#

  • 3D 재구성 에이전트가 사용하는 service account 가 production 환경의 record write 권한을 보장하도록 권한 모델 점검 — 특정 tenant/team 권한 변경 시 에이전트 권한이 함께 회수되지 않도록 분리.
  • 정책 거부(403/PERM10000) 메트릭을 controller 별 dashboard 로 노출해 단발성/광범위성 회귀를 빠르게 구분.
  • (Revision 1 추가) 에이전트가 user 권한과 분리된 system identity 로 동작하도록 인증 모델 변경 검토 — 아래 "Agent Permission Model" 섹션의 옵션과 비용 평가 참조.

Agent Permission Model — 변경 비용 평가#

피드백 "agent 가 뜨는 순간에는 user 권한없이 동작해야할거 같은데 이렇게 동작을 수정하려면 얼마나 어려운지 계산좀" 에 대한 분석. 현재 구조와 세 가지 변경 옵션의 영향 범위를 코드 기반으로 추산했다.

현재 구조 (As-Is)#

에이전트가 cupixworks-api 를 호출할 때 사용하는 자격은 실제 human user 의 세션이다. 에이전트 전용 user 도, 정책 우회 경로도 존재하지 않는다.

  • app/jobs/create_capture_3d_reconstruction_job.rb:57session = self.user.agent_team_session(self.jobable.team) 로 job 을 발행한 user 의 세션을 발급. Job 의 self.user 는 capture 를 생성/소유한 일반 user 이다.
  • app/models/user.rb:116-126agent_team_sessiongrant_type: :auth_agent 세션을 생성하지만, 소유자는 호출한 user (self.sessions.active.where(grant_type: :auth_agent)).
  • app/factories/session_factory.rb:38-43auth_agentapi_token 의 유일한 차이는 expires_at 만료 기간. 권한/role 차이 없음.
  • app/controllers/concerns/legacy_verification_controller.rb:83@current_user = @session.user 로 controller 는 일반 user 로 인식.
  • app/factories/pointcloud_factory.rb:20Pundit.policy(self.current_user, self.parent).create? 가 그 일반 user 의 applied_permission 만 평가.

즉 본 incident 는 "에이전트가 user 권한으로 동작" 한다는 설계 자체로 인해 user 의 권한 변동이 그대로 에이전트 실패로 이어지는 구조적 위험을 드러낸다.

변경 옵션과 비용#

옵션 접근 코드 변경 범위 복잡도 회귀 위험 보안/감사 영향
A. 운영 데이터 수정 영향 받은 record 의 applied_permission 또는 user role 보정 0 file trivial 없음 변동 없음 (현재 권장 즉시 조치와 동일)
B. Policy 레벨 우회 ApplicationPolicy 에서 @session.grant_type == :auth_agent 이면 create/update/delete 우회 약 5-15 file (Pundit context 확장 + 6개 메서드) small-medium 중간 높음 — 탈취된 agent token = tenant-wide god-mode
C. 전용 system user (per-team) team 마다 agent_system user 를 두고 agent_team_session 이 그것을 사용 약 3-7 file (user.rb, job 들의 self.user) + data migration medium 높음 (감사 추적 단절) 중간 — 감사 로그에서 human attribution 사라짐
D. 정책 모델에 "agent identity" 도입 Pundit.policy(user, record)Pundit.policy(actor, record) 시그니처 변경, actor 가 user 또는 agent role 보유 92 file / 167 call site 전수 수정 + 모든 policy class 수정 large 매우 높음 중간 — 새 권한 모델 필요

옵션별 세부 추산#

Option A — Operational only

  • 변경 파일: 0
  • 작업: 운영팀이 capture/cluster 의 parent record 권한 점검 후 user role 또는 record applied_permission 보정
  • 장점: 즉시 가능, regression risk 없음
  • 단점: 근본 원인 해결 아님 — 동일 패턴 재발 가능

Option B — Policy-level agent bypass

대상 정책 메서드: ApplicationPolicy#create?, #update?, #delete?, #share?, #archive?, #read? (6개).

proposed: app/policies/application_policy.rb#create?ruby
def create?
  return true if admin_administrator?
  return true if auth_agent_session? # NEW
  return false if archived_entity?
  # ... existing logic
end

Plumbing: 현재 policy 는 (user, record) 만 받음 — session/grant_type 접근 불가. 두 가지 방법:

  1. Pundit policy_scope 에 context 주입 — Pundit pundit_user override 로 user 대신 { user:, session: } 객체를 전달. app/controllers/api_controller.rb 의 모든 controller 에서 pundit_user 메서드 override 필요. 영향: api_controller.rb 1 file + application_policy.rb 1 file + 시그니처 변경 영향받는 Scope.resolve 들 (~92 file 중 일부).
  2. User 모델에 from_agent_session? flag 추가legacy_verification_controller.rb:83 에서 @current_user.instance_variable_set(:@from_agent_session, @session.grant_type == 'auth_agent') 셋업. 영향: legacy_verification_controller.rb 1 file + user.rb 1 file + application_policy.rb 1 file. 가장 작은 변경 경로 (3-5 file).

테스트: spec/policies/application_policy_spec.rb + 영향받는 자식 policy spec 다수.

리스크: agent 토큰이 탈취되면 해당 team 전체 record 에 대해 write 가능 — 현재는 user 권한 범위로 제한되지만, bypass 후엔 그 제한이 사라짐. 따라서 User-Agent 검증, IP allowlist, 또는 별도 mTLS 등 보강 필요. 보안 영향이 크므로 보안 리뷰 필수.

Option C — Per-team system user

proposed: change in user.rb#agent_team_sessionruby
def agent_team_session(current_team)
  system_user = current_team.agent_system_user # NEW
  system_user.sessions.active
             .where(grant_type: :auth_agent, team: current_team)
             # ...
end

대상 변경:

  • app/models/team.rbagent_system_user association + lazy create
  • app/models/user.rb:116-126agent_team_session 이 self 가 아닌 system user 사용
  • 모든 Job 클래스의 self.user.agent_team_session(...) 호출 약 30+ 위치 (Grep 결과 38건 확인됨) — 호출 자체는 그대로지만 의미가 달라짐
  • Data migration: 기존 team 마다 system user 생성, 적절한 applied_permission 또는 administrator group 매핑
  • 모든 record/facility/team 의 audit log 가 system user 로 표시 — UX 변경

변경 범위: 코어는 3-5 file 이나, migration + 감사 로그 의미 변경 으로 인한 전사 검토 필요.

리스크: 감사 추적이 "어떤 user 가 일으킨 작업인가" 정보를 잃음. job 메타데이터로 보강 필요. 또한 system user 가 administrator 그룹이면 admin_administrator? 가 true 가 되어 모든 record 우회 — Option B 보다 더 강력한 권한이 발생.

Option D — actor 시그니처 변경

가장 깨끗하나 가장 비싸다. 162개 Pundit.policy(...) 호출 + 모든 policy class 의 user 사용처를 actor 로 전수 수정. 모든 policy spec 재작성. 1-2 주 단위 작업이며 회귀 위험 크다.

권장#

  • 이번 incident 해결: Option A (운영팀 데이터 수정).
  • 재발 방지로 1순위: Option B 의 두 번째 방법(from_agent_session? flag) — 3-5 file 변경으로 작은 폭. 단 보안 리뷰 필수.
  • Option C 는 감사 로그 영향이 크므로, 보안/제품팀 합의가 선행되어야 함.
  • Option D 는 본 incident 의 비용 대비 효과가 낮음 — 보류.

미확인 항목#

  • 다른 에이전트(forge-agent, complete-agent, mask-enhancement-agent 등)도 동일하게 user 세션으로 동작하는지 — 본 분석은 create_capture_3d_reconstruction_job 만 확인. 변경 시 일관성 검증 필요.
  • production 의 auth_agent 세션이 통과하는 controller 가 Api::V1 외에 Api::V2/Api::Admin 도 있는지 — Option B 의 통합 정도 결정에 영향.

Monitoring#

403 PERM10000 발생률 모니터링 (timeseries widget 용):

text
sum:trace.rack.request.errors{service:cupixworks-api,resource_name:api/v1/pointclouds_controller#create,http.status_code:403}.as_count()

에이전트 측 uploadNewPointclouds 실패 추세:

text
logs("service:cupixworks-capture-3dreconstruction-instance status:error \"TransferManager::uploadNewPointclouds\"").index("*").rollup("count").by("@environment")

permission denied 정책 로그 추세 (record/workspace/pointcloud 등 모델별):

text
logs("service:cupixworks-api \"Permission denied on creating a model\"").index("*").rollup("count").by("@class")

Risk Assessment#

  • Risk level: low — 1건/7일, 단일 tenant, 단일 record. 광범위한 회귀 없음.
  • 예상 복잡도: trivial (운영 데이터/권한 수정). 코드 변경 시 standard (로그 보강). Agent permission 모델 분리는 Option B 기준 small-medium (3-5 file + 보안 리뷰).

Revision History#

Revision 1#

Feedback: "agent 가 뜨는 순간에는 user 권한없이 동작해야할거 같은데 이렇게 동작을 수정하려면 얼마나 어려운지 계산좀" — 에이전트가 호출 user 의 권한과 무관하게 동작하도록 바꾸는 비용 추산 요청.

판정:

피드백 항목 판정 근거
에이전트가 user 권한 없이 동작해야 한다는 문제 제기 수용 현재 에이전트는 job 발행 user 의 세션으로 동작 — app/jobs/create_capture_3d_reconstruction_job.rb:57self.user.agent_team_session(self.jobable.team), app/models/user.rb:116-126agent_team_sessionself.sessions 로 동일 user 세션 발급. app/factories/session_factory.rb:38-43 에서 auth_agent grant_type 은 만료 기간만 다를 뿐 권한 분리 없음. 본 incident 의 직접 원인이 user 의 record 권한 부재이므로 구조적 위험이 맞음.
변경 난이도 추산 수용 Pundit.policy 호출 167건/92파일 (/home/ec2-user/repos/tesla/app Grep count). 가장 작은 변경 경로(Option B 의 user flag 방식)는 legacy_verification_controller.rb, user.rb, application_policy.rb 3 file. 가장 큰 변경 경로(Option D actor 시그니처)는 92 file 전수. Option A/B/C/D 비용 추산을 본문 "Agent Permission Model — 변경 비용 평가" 섹션에 반영.

변경 사항:

  • 새 섹션 추가: ## Agent Permission Model — 변경 비용 평가 — 현재 구조, 4가지 옵션(A: 운영 데이터, B: policy 우회, C: per-team system user, D: actor 시그니처 변경) 별 코드 변경 범위/복잡도/리스크 표 + 권장.
  • Fix Recommendation장기 개선 에 Revision 1 항목 1개 추가 (새 섹션 참조).
  • Risk Assessment 에 agent permission 모델 분리의 복잡도(Option B 기준 small-medium) 추가.

추가 조사 내용:

  • /home/ec2-user/repos/tesla/app/models/user.rb:116-126agent_team_session 정의 확인.
  • /home/ec2-user/repos/tesla/app/jobs/create_capture_3d_reconstruction_job.rb:57,86 — 3D 재구성 job 의 세션 발급 경로.
  • /home/ec2-user/repos/tesla/app/factories/session_factory.rb:38-43auth_agent grant_type 의 의미 (만료 기간만 다름).
  • /home/ec2-user/repos/tesla/app/controllers/concerns/legacy_verification_controller.rb:44,83 — controller 가 @current_user = @session.user 로 세팅하는 위치, auth_agent token_type 인식 위치.
  • /home/ec2-user/repos/tesla/app/policies/application_policy.rb:1-36 — Pundit 정책의 user/record 시그니처 (session 비전달).
  • /home/ec2-user/repos/tesla/app/policies/concerns/sales_team_policy/deprecated.rb:5-7 — 유일한 명시적 우회 admin_administrator? 의 정의.
  • /home/ec2-user/repos/tesla/app/factories/user_factory.rb:18 — 시스템 내부 호출 전용 skip_permission_check 파라미터 패턴 (API 노출 안 됨).
  • /home/ec2-user/repos/cupixworks/applications/agents/packages/api/src/authentication/cupix-auth.ts:200-212,236 — 에이전트 측 세션 사용 흐름과 User-Agent: <CPX_AGENT_NAME> 헤더 송신 (서버 측 정책에 활용 가능한 신호).
  • Grep counts: Pundit\.policy 호출 167건/92파일, agent_team_session 호출 38건 (tesla app/ 기준).