직원·담당자·대표라는 이름만으로는 누가 어떤 자료에 무엇을 할 수 있는지 정해지지 않는다. 나는 역할과 회사 범위, 담당과 문서 상태를 함께 확인하는 작은 정책 모듈을 만들었다.

이것은 로그인 서비스나 배포된 ERP 보안 계층이 아니다. 신뢰할 수 있는 서버 문맥이 이미 있다는 전제 아래 규칙과 반환 필드를 시험하는 독립 합성 예제다. 데모의 역할 선택을 실제 권한 부여로 사용하면 안 된다.

가장 먼저 지켜야 할 입력의 신뢰 경계

decideAccess는 principal, action, record를 받는다. principal은 실제 서비스라면 서버가 인증한 사용자에서 만들어야 한다. 브라우저의 선택 상자, 요청 본문이나 임의 헤더의 역할 값을 그대로 넣어서는 안 된다. 이 함수가 참을 돌려주어도 입력 사용자가 위조됐다면 실제 권한 판단으로 쓸 수 없다. 로컬 데모는 그 조건을 보안으로 강제하는 프로그램이 아니다.

예시에서는 active가 참이고 알려진 역할·행동과 유효한 회사·사용자·자료 식별자가 있어야 한다. 문서 상태는 draft 또는 sent다. 회사가 다르거나 조건을 알 수 없으면 먼저 거절한다. 거절 결과는 같은 형태로 반환하고 자료 내용을 붙이지 않는다. 회사와 담당 범위는 기록의 현재 값을 서버에서 읽어 확인해야 한다는 전제를 문서에 남겼다.

역할과 행동의 관계를 명시했다

직원은 자기 담당 자료를 읽고 초안의 수량을 바꿀 수 있다. 담당자는 같은 회사의 자료를 읽고 초안을 수정하거나 확인할 수 있다. 대표는 같은 회사 자료의 읽기와 내보내기만 허용한다. 대표라는 이름이 수정·확인·복구까지 자동으로 포함하지 않는다. 이 선택은 회사별 정책을 논의하기 위한 합성 예시이며 보편적인 조직 규칙은 아니다.

restore 행동은 모든 예시 역할에 거절된다. 별도의 복구 책임과 승인을 구현하지 않았기 때문이다. 알 수 없는 delete 같은 행동을 넣어도 허용하지 않는다. 새 기능을 추가했을 때 이름이 익숙하다는 이유로 권한이 열리지 않게 기본 거절로 두었다. 운영 서비스에서는 각 API의 실행 지점에 이런 검사를 연결해야 한다.

응답과 수정 필드도 허용 목록으로 나눈다

projectOrder는 읽기를 허용한 뒤 식별자·상태·수량만 새 객체로 반환한다. 대표 예시에는 금액을 더하되 유효한 음수 아닌 안전 정수인지 확인한다. 원본의 내부 메모나 추가 필드는 반환하지 않는다. 원본 객체를 펼쳐 전부 응답한 뒤 화면에서 숨기는 방식과 다르다. 입력 계약이 변해도 공개할 필드를 의식적으로 정하게 만들었다.

editableOrderFields는 허용된 수정의 대상이 수량뿐이라는 목록을 돌려준다. 이 함수가 실제 요청 본문을 검사하거나 저장하지는 않는다. 실무에서는 서버가 이 목록을 적용하고 각 값과 문서 상태를 다시 확인해야 한다. 일괄 내보내기도 별도 조회 범위와 행별 규칙이 필요하다. 한 자료의 허용을 전체 자료의 허용으로 넓혀 적용하지 않는 것이 중요하다.

코드와 구현 살펴보기

독립 합성 모듈 전체 구현

// Teaching policy only. Principal must come from a trusted server session, never request-body roles.
const idOK=v=>typeof v==='string'&&/^[A-Za-z0-9_-]{1,40}$/.test(v);
const roles=['staff','coordinator','owner'],actions=['read','edit','approve','export','restore'];
export function decideAccess(principal,action,record) {
  const deny=()=>({allowed:false,reason:'denied'});
  if(!principal||!record||principal.active!==true||!roles.includes(principal.role)||!actions.includes(action)||!idOK(principal.companyId)||!idOK(principal.userId)||!idOK(record.companyId)||!idOK(record.assignedTo)||!idOK(record.id)||!['draft','sent'].includes(record.status))return deny();
  if(principal.companyId!==record.companyId)return deny();
  const own=principal.userId===record.assignedTo,draft=record.status==='draft';
  const allowed=principal.role==='staff'?(own&&(action==='read'||(action==='edit'&&draft))):principal.role==='coordinator'?(action==='read'||(draft&&['edit','approve'].includes(action))):['read','export'].includes(action);
  return allowed?{allowed:true,reason:'allowed'}:deny();
}
export function projectOrder(principal,record) {
  if(!decideAccess(principal,'read',record).allowed)return null;
  if(!Number.isSafeInteger(record.quantity)||record.quantity<1||record.quantity>99)return null;
  const result={id:record.id,status:record.status,quantity:record.quantity};
  if(principal.role==='owner'){
    if(!Number.isSafeInteger(record.amount)||record.amount<0)return null;
    result.amount=record.amount;
  }
  return result;
}
export function editableOrderFields(principal,record){return decideAccess(principal,'edit',record).allowed?['quantity']:[];}
export function syntheticAccess(){return {principal:{active:true,companyId:'ORG_A',userId:'USER_A',role:'staff'},record:{companyId:'ORG_A',id:'S01',assignedTo:'USER_A',status:'draft',quantity:6,amount:600000,internalNote:'not projected'}};}

데모에서 바뀌는 것은 합성 판단이다

아래에서 역할, 회사 일치 여부, 담당, 문서 상태와 요청 행동을 바꿀 수 있다. 읽기가 허용되면 제한된 반환 자료가 보이고 수정 행동에서는 허용 필드를 확인할 수 있다. 다른 회사를 고르면 대표라도 거절된다. 실제 로그인이나 쿠키, 서버 권한은 만들지 않는다. 이 입력을 마음대로 바꿀 수 있다는 점 자체가 클라이언트 선택을 신뢰하면 안 되는 이유다.

모든 값은 고정 합성 자료이며 실제 회사와 계정을 포함하지 않는다. 한영 데모는 같은 코드로 판단하고 결과의 기계용 키도 같게 유지한다. 실제 업무 화면에서는 거절 이유를 사용자가 이해할 문장으로 안내하되 다른 회사의 민감한 정보나 존재 여부를 흘리지 않도록 정해야 한다. 무엇을 보여 줄지도 권한 설계의 일부다.

독립 합성 예시 · 실제 회사 자료·서버 연결 없음

검증한 것과 서버에 추가해야 할 것

압축 파일을 풀고 node test.mjs로 독립 테스트를 실행할 수 있다. 코드·한영 HTML 데모·범위와 재사용 허용 문서가 포함되며 수정·재배포·상업적 사용이 가능하고 무보증으로 제공한다. 패키지 테스트 9개 그룹과 전체 정책 조합 120개를 대조했고 비활성 사용자·필드 제한·알 수 없는 행동도 확인했다. 원래 업체 프로그램에서 가져온 코드는 아니다.

한영 데모 이벤트 연결은 코드로 확인했지만 인증 서비스나 실브라우저, 운영 보안 검수는 수행하지 않았다. 회사에 적용하려면 실제 사용자 확인, 권한 회수, 서버의 회사 범위 조회, 모든 행동의 검사와 트랜잭션 경계, 요청 본문 필드 검증, 기록 정책을 별도로 구현해야 한다. 규칙을 읽기 쉽게 공유하는 것과 보호되는 서비스를 완성하는 일을 구별해 둔다.

구현·테스트·한영 데모 ZIP 받기
재사용 허용과 적용 범위 문서 포함