Devin.KR

역할별 접근 조건

160분 안팎

학습 목표

로그인 여부와 소유권·역할을 따로 판정합니다.

개념

로그인 성공이 모든 행동의 허용을 뜻하지 않습니다

신청자 U01이 자신의 A001 상태를 보는 일과 다른 신청을 승인하는 일은 목적이 다릅니다. 둘 다 로그인된 화면에서 시작하므로 “회원이면 가능”이라는 요구사항으로 묶기 쉽습니다. 그러면 신청자가 자기 신청을 스스로 승인하거나 U02가 주소의 신청 번호를 바꿔 다른 사람의 상태를 읽을 수 있습니다. 이번 레슨은 누가 어떤 자원에 어떤 행동을 할 수 있는지 표로 만든 뒤 조건문으로 대조합니다. 승인 기능은 m03 모의 API에 없으며 이번 권한 표의 후속 제안입니다.

권한 표의 축은 주체·대상·행동·범위입니다

주체는 요청하는 계정이며 userId로 표기합니다. 대상의 ownerId는 서버가 보관한 신청 소유자의 ID입니다. 역할 member는 일반 신청자, operator는 행사 운영자입니다. 행동 read는 신청 상태 조회이고 approve는 승인 판단입니다. 운영자의 범위 scope=in은 담당 행사, out은 다른 행사라는 입력입니다. 이 범위는 실제 구현에서 서버가 배정 기록으로 확인할 조건입니다. 화면이 사용자의 체크박스 선택으로 운영자 역할이나 담당 범위를 정하는 설계를 뜻하지 않습니다.

표에는 본인 신청자, 다른 신청자, 담당 운영자, 비로그인 네 관계를 행으로 두고 조회와 승인을 열로 둡니다. 본인은 조회만, 다른 신청자는 두 행동 모두 거부, 담당 운영자는 두 행동 허용, 비로그인은 두 행동 모두 재로그인 안내입니다. 이 표는 취소·수정·전체 내보내기를 허용하지 않습니다. 새 행동이 생기면 담당자와 별도의 조건을 합의합니다. 허용 행동의 이름을 “관리” 하나로 쓰면 조회 권한이 일괄 삭제 권한으로 확대될 수 있습니다.

세션을 먼저 판정하고 자원 조건을 확인합니다

판정 순서는 세션→역할과 행동→소유권 또는 담당 범위입니다. 세션 값 valid만 다음 판단으로 갑니다. expired와 none은 role이 operator여도 RELOGIN입니다. 그 다음 operator는 담당 행사 안일 때 read와 approve를 허용합니다. member는 userId와 ownerId가 같고 식별자가 “-”가 아니며 행동이 read일 때만 허용합니다. 나머지는 DENY입니다. 비로그인인데 userId=ownerId라고 쓰여 있어도 세션 단계에서 멈추므로 본인으로 인정하지 않습니다.

ALLOW는 이번 정책이 허용한다는 모형 출력이고 실제 서버 요청이 성공했다는 증거가 아닙니다. RELOGIN은 현재 주체를 확인할 수 없어 로그인 확인으로 돌아가라는 뜻입니다. DENY는 이 입력에서 허용 조건이 없다는 뜻입니다. 실제 서버의 HTTP 상태나 오류 코드는 별도의 계약으로 연결합니다. 조회 대상이 없을 때 존재 여부를 숨기는 응답을 사용하는 제품도 있으므로 이 연습의 세 문자열을 모든 서비스 응답의 규칙으로 일반화하지 않습니다.

조건문이 업무 문장을 보존하는지 봅니다

브라우저 연습의 입력은 한 줄에 session role userId ownerId action scope 여섯 값을 공백으로 나눈 형태입니다. 개수 줄은 없고 비어 있지 않은 줄마다 판정 한 줄을 출력합니다. session은 valid·expired·none, action은 read·approve, scope는 in·out이며 계정 ID는 공백 없는 가명 문자열 또는 “-”입니다. role에는 member·operator 외에 미지정 값을 넣을 수 있으며 거부합니다. 빈 입력이나 공백만 있는 입력은 출력하지 않습니다. 형식이 다른 행을 복구하는 파서는 이번 과제 범위에서 제외합니다.

===는 두 값이 같은지 비교하고 &&는 조건들이 함께 만족하는지, ||는 둘 중 하나가 만족하는지 표현합니다. if 뒤의 괄호가 참이면 그 분기를 실행하고 return은 그 판정을 반환하여 다음 분기로 내려가지 않게 합니다. role과 소유권을 OR 하나로 합치면 본인 신청이라는 이유만으로 approve까지 허용할 수 있습니다. 업무 조건을 짧게 압축하기보다 허용되는 두 길을 각각 적고 그 외에는 거부하는 형태로 시작합니다. 문법의 더 자세한 설명은 제어문 장을 읽습니다.

표를 테스트 사례로 바꿉니다

본인 read 허용과 본인 approve 거부를 붙여 테스트하면 행동 축 누락을 찾을 수 있습니다. U02가 U01 신청을 읽는 입력은 소유권 축 검사입니다. valid operator의 in과 out을 비교하면 담당 행사 범위를 검사합니다. expired operator in approve는 세션보다 역할을 먼저 확인한 실수를 잡습니다. guest가 본인 ID를 갖는 입력은 미지정 역할이 소유권만으로 통과하지 않는지 확인합니다. 성공 사례만 여럿 넣으면 허용 범위가 넓어진 결함을 발견하기 어렵습니다.

“-”와 “-”를 같은 계정이라고 비교하는 실수도 경계 사례로 둡니다. 이 실습에서 “-”는 주체를 식별할 수 없는 표식이므로 member의 본인 조회 조건에서 배제합니다. 반면 운영자는 별도로 인증된 주체라는 입력 전제를 쓰며 대상 소유자와 ID가 같을 필요는 없습니다. 실제 제품에서는 유효한 세션에 인증된 계정이 존재하는지도 서버가 확인합니다. 테스트 데이터의 문자열 비교를 인증 구현 자체로 설명하지 않는 것이 이 모형을 읽는 기준입니다.

화면 숨김과 서버 검사를 구별합니다

신청자 화면에서 승인 버튼을 숨기면 실수를 줄일 수 있지만 주소나 요청을 직접 만들었을 때의 권한은 서버가 확인해야 합니다. 개발자에게는 조회와 승인 각각 어느 요청에서 역할·소유권·담당 행사를 검증하는지 묻습니다. 서버는 요청 본문의 ownerUserId를 그대로 믿기보다 인증된 주체와 저장된 소유권을 대조하는 설계를 검토합니다. 권한 표는 UI 표시와 서버 검증의 같은 근거가 되어야 하며 버튼 숨김만 완료 기준으로 삼지 않습니다.

흔한 코딩 오류로 ReferenceError가 나오면 role 같은 변수 이름이 선언된 이름과 일치하는지 봅니다. SyntaxError는 괄호·중괄호·문자열 따옴표를 먼저 확인합니다. 실행은 성공하지만 기대값과 다르면 해당 입력의 세션, 행동, 소유권, 범위를 순서대로 손으로 평가합니다. DENY가 필요한 본인 approve가 ALLOW라면 소유권 뒤에 행동 조건이 빠졌을 가능성이 있습니다. 출력 줄에 설명을 덧붙이면 채점 출력이 달라지므로 진단 설명은 답안 코드 밖에 적습니다.

허용을 넓히기 전에 결정 근거를 남깁니다

권한을 추가해 달라는 요청에는 업무 목적과 담당 범위를 묻습니다. 운영자에게 모든 행사 조회를 주자는 제안은 편리할 수 있지만 현재 담당 행사 권한보다 넓습니다. 이번 제출에서는 확대하지 않고 후속 검토 항목에 배정 기록·필요 필드·기간을 적습니다. 동료는 표의 거부 사례를 읽고 화면에서 어떻게 안내하는지 확인합니다. 모형 통과 뒤 실제 서버에서 같은 사례가 거부되는지 검증하는 일은 개발 담당자와 합의할 별도 인수 기준입니다.

기본 거부와 요청별 권한 검사의 원칙은 OWASP 권한 지침에서 확인할 수 있습니다. 이 모듈의 담당 행사와 승인 조건은 그 지침이 정해 주는 업무 규칙이 아니라 우리가 검토할 제품 정책입니다. 보안 원칙의 출처와 동아리 업무 조건의 결정권자를 구분해서 남깁니다.

따라하기

본인 행동 두 가지를 실행합니다

같은 두 ID에서 행동만 바뀌었을 때 결과가 다른지 실행합니다.

function decide(session, role, userId, ownerId, action, scope) {
  if (session !== 'valid') return 'RELOGIN';
  if (role === 'operator' && scope === 'in' && (action === 'read' || action === 'approve')) return 'ALLOW';
  if (role === 'member' && userId !== '-' && userId === ownerId && action === 'read') return 'ALLOW';
  return 'DENY';
}
console.log(decide('valid','member','U01','U01','read','in'));
console.log(decide('valid','member','U01','U01','approve','in'));

실행 결과

ALLOW
DENY

운영자 범위와 만료를 비교합니다

역할이 같아도 담당 범위와 유효 세션이 달라지면 결과가 달라집니다.

function decide(session, role, userId, ownerId, action, scope) {
  if (session !== 'valid') return 'RELOGIN';
  if (role === 'operator' && scope === 'in' && (action === 'read' || action === 'approve')) return 'ALLOW';
  if (role === 'member' && userId !== '-' && userId === ownerId && action === 'read') return 'ALLOW';
  return 'DENY';
}
console.log(decide('valid','operator','O01','U01','approve','in'));
console.log(decide('valid','operator','O01','U01','approve','out'));
console.log(decide('expired','operator','O01','U01','approve','in'));

실행 결과

ALLOW
DENY
RELOGIN

미지정 역할과 빈 ID 표식을 확인합니다

본인 ID만 같게 두어 허용되는 오류가 없는지 확인합니다.

function decide(session, role, userId, ownerId, action, scope) {
  if (session !== 'valid') return 'RELOGIN';
  if (role === 'operator' && scope === 'in' && (action === 'read' || action === 'approve')) return 'ALLOW';
  if (role === 'member' && userId !== '-' && userId === ownerId && action === 'read') return 'ALLOW';
  return 'DENY';
}
console.log(decide('valid','guest','U01','U01','read','in'));
console.log(decide('valid','member','-','-','read','in'));

실행 결과

DENY
DENY

표를 시작 코드에 반영합니다

브라우저 실습의 decide 함수를 완성하고 빈 입력·공백 입력·여러 줄을 실행합니다. 실패 사례를 세션→역할→행동→자원 범위 순서로 손으로 판정합니다.

확인 문제

실습

여섯 열 session role userId ownerId action scope를 받아 행마다 RELOGIN·DENY·ALLOW 하나를 출력합니다. session은 valid·expired·none, action은 read·approve, scope는 in·out입니다. role은 member·operator와 미지정 문자열, ID는 공백 없는 문자열이며 “-”는 식별 불가 표식입니다. valid가 아니면 RELOGIN, valid operator는 in에서 두 행동 허용, valid member는 “-”가 아닌 본인 ID의 read만 허용, 나머지는 DENY입니다. 빈 입력과 공백 줄은 출력하지 않습니다. 개수 줄과 해설 출력은 없습니다. 파싱 코드는 제공되며 decide만 수정합니다.

모범 답안
function decide(session, role, userId, ownerId, action, scope) {
  if (session !== 'valid') return 'RELOGIN';
  if (role === 'operator' && scope === 'in' && (action === 'read' || action === 'approve')) return 'ALLOW';
  if (role === 'member' && userId !== '-' && userId === ownerId && action === 'read') return 'ALLOW';
  return 'DENY';
}
const raw = require('fs').readFileSync(0, 'utf8').trim();
const lines = raw === '' ? [] : raw.split(/\r?\n/).filter(line => line.trim() !== '');
for (const line of lines) {
  const args = line.trim().split(/\s+/);
  console.log(decide(...args));
}

더 읽기

면접 질문

  • 사용자가 로그인이 안 된다고 할 때 확인할 순서를 설명해 주시면 됩니다.