Devin.KR

소유자와 역할 접근표

55분 안팎

학습 목표

인증 여부와 자원 권한을 구분합니다.

개념

먼저 업무 규칙을 문장으로 정합니다

자료 보관 앱의 로그인 화면을 통과했다고 모든 자료를 읽어도 되는 것은 아닙니다. 신입 점검자는 누가 요청했는지와 무엇을 해도 되는지를 나누어 기록해야 합니다. 인증은 요청 주체를 확인하는 과정이고 인가는 그 주체가 특정 자료에 특정 동작을 수행할 수 있는지 결정하는 과정입니다. 다른 계정으로 로그인한 사용자가 내 메모를 읽을 수 있다면 로그인 기능의 성공이 접근 통제의 성공을 뜻하지 않습니다.

이번 정책의 범위

우리 앱에는 demo-alice와 demo-bob이라는 회원, demo-admin이라는 관리자가 있습니다. 자료에는 owner가 있고 각 요청에는 읽기·수정·삭제라는 동작이 있습니다. 회원은 자기 자료의 세 동작만 수행합니다. 관리자는 타인 자료를 읽어 점검할 수 있으나 수정하거나 삭제하지 않습니다. 이는 이 실습의 업무 결정이며 모든 서비스 관리자가 같은 권한을 갖는다는 규칙이 아닙니다. 역할을 추가할 때 허용 이유를 문서로 남깁니다.

실습 준비

이 모듈은 Python 3와 Node.js 표준 모듈로 실행합니다. browser 문제는 표준 입력 한 줄의 JSON을 읽고 표준 출력에 판정 한 줄을 씁니다. local ZIP은 별도 폴더에 풀어 루트에서 bash check.sh를 실행합니다. 외부 서비스와 실제 계정은 사용하지 않습니다. 이전 미션은 본인 소유 컨테이너만 점검했습니다. 이 범위를 유지하고 합성 자료·더미 비밀번호만 사용합니다. 실행 명령을 운영 서버 주소로 바꾸지 않습니다.

표의 행과 열

접근표의 행에는 익명·소유자인 회원·타인인 회원·타인인 관리자를 적고 열에는 read·update·delete를 적습니다. 익명은 모두 거절, 소유자인 회원은 모두 허용, 타인인 회원은 모두 거절, 타인인 관리자는 read만 허용합니다. 관리자가 자기 자료를 다루면 소유자 규칙도 적용됩니다. 사용자 이름을 특정 값으로 비교하기보다 요청 주체의 id와 자료 owner의 관계를 비교하면 새 회원도 같은 규칙으로 처리됩니다.

입력 계약을 닫습니다

브라우저 판정기의 JSON에는 user, owner, action이 있습니다. user가 null이면 익명입니다. user는 id와 role을 가진 객체이고 역할은 member 또는 admin입니다. owner와 id는 비어 있지 않은 문자열이며 action은 read·update·delete 중 하나입니다. 알 수 없는 역할이나 동작, 빈 소유자, 잘못된 자료형, 깨진 JSON은 DENY를 출력합니다. 정의되지 않은 경우를 알아서 허용하면 새 기능을 붙이는 순간 권한이 넓어질 수 있습니다.

기본 거절을 코드로 표현

먼저 입력이 계약을 만족하는지 확인하고 그다음 소유자 관계와 읽기 전용 관리자 조건을 계산합니다. 두 허용 조건에 들어오지 않는 요청은 거절합니다. 빈 문자열끼리 같다는 이유로 소유자라고 판단하지 않습니다. 역할 문자열이 있다는 이유만으로 관리자라고 판단하지도 않습니다. OWASP는 기본 거절과 요청마다 권한 검사를 권고합니다. 인가 지침의 원칙을 우리 접근표에 연결합니다.

정책과 신원 출처

브라우저 문제의 user는 판정 연습을 위해 이미 신뢰된 값이라고 가정합니다. 실제 서버에서는 클라이언트가 보낸 user를 그대로 신뢰하지 않습니다. body.role을 admin으로 바꿔 전송해도 서버가 저장한 계정 역할이 변해서는 안 됩니다. 다음 레슨의 세션 저장소가 신뢰할 사용자 정보를 제공하고 그다음 레슨의 라우터가 실제 자료 소유자를 읽습니다. 이번 함수는 그 두 출처가 준비된 뒤 적용할 정책만 다룹니다.

허용 결과를 작은 출력으로 남기기

판정 결과는 ALLOW 또는 DENY이며 토큰이나 자료 본문은 출력하지 않습니다. 접근표를 실행해 보는 목적은 화면 예쁘게 만들기가 아니라 정책의 빠진 칸을 발견하는 것입니다. 정상 소유자 read만 입력하면 관리자 delete가 잘못 허용되어도 알아채지 못합니다. 익명, 타인, 관리자 변경, 정의되지 않은 역할을 함께 입력합니다. 기대값은 구현의 현재 행동이 아니라 정해 둔 업무 규칙에서 얻습니다.

조건식 읽는 순서

and는 필요한 조건을 함께 묶고 or는 별개의 허용 근거를 연결합니다. 관리자이고 읽기라는 조건은 두 조건이 동시에 만족해야 합니다. 관리자인가 또는 읽기인가로 바꾸면 모든 회원의 타인 읽기와 관리자의 삭제가 열립니다. 괄호로 소유자 조건과 관리자 읽기 조건을 분리해 검토합니다. 앞부분 입력 검사가 실패한 경우 바로 반환하면 None에서 id를 꺼내는 오류도 피할 수 있습니다.

실패 메시지에서 계약 찾기

JSONDecodeError는 정책 위반이 아니라 입력 문자열을 해석하지 못한 경우입니다. 이번 채점 계약은 이런 경우도 DENY로 처리합니다. KeyError는 없는 키를 직접 참조한 단서이며 get으로 기본값을 읽고 자료형을 확인합니다. AttributeError가 발생하면 객체라고 가정한 값이 실제로는 문자열이나 배열일 수 있습니다. 예외를 모두 ALLOW로 바꾸는 수정은 결함을 숨깁니다. 어떤 입력이 실패했는지 최소 JSON으로 재현합니다.

거절과 기능 실패 구분

DENY가 많이 나온다는 이유로 안전하다고 결론 내리지 않습니다. 모든 요청을 거절하는 함수도 타인 접근 시험은 통과할 수 있습니다. 소유자의 수정과 삭제, 관리자의 타인 읽기까지 허용되는지 확인해야 앱 기능을 보존했다고 말할 수 있습니다. 특히 본인 id와 owner가 같은 경우에도 role이 unknown이면 거절합니다. 계정의 역할을 해석하지 못했는데 소유 관계만으로 통과시키면 잘못된 계정 데이터가 권한으로 이어집니다.

리뷰에서 설명할 산출물

완료 후 접근표와 각 칸의 대표 JSON을 남깁니다. 관리자의 읽기 권한은 왜 필요한지, 타인 삭제를 왜 제외했는지 한 문장씩 설명합니다. 이 함수 통과는 쿠키 위조나 실제 HTTP의 우회까지 검증한 결과가 아닙니다. 정책 계산의 범위를 분명히 적고 신원 획득·라우터 적용을 후속 검사로 연결합니다. PHP 서재는 다른 런타임의 비밀번호·세션 구현을 더 읽기로 제공하며 이 레슨에서는 정책 판단에 집중합니다.

따라하기

신원과 자원을 분리하기

아래 코드를 실행하여 이 단계의 판단 결과를 확인합니다.

user = {'id': 'demo-alice', 'role': 'member'}
item = {'owner': 'demo-bob'}
print('authenticated=', user is not None)
print('owner=', user['id'] == item['owner'])

실행 결과

authenticated= True
owner= False

관리자 읽기 조건 비교

아래 코드를 실행하여 이 단계의 판단 결과를 확인합니다.

for action in ['read', 'update', 'delete']:
    print(action, 'ALLOW' if action == 'read' else 'DENY')

실행 결과

read ALLOW
update DENY
delete DENY

빈 식별자 검사

아래 코드를 실행하여 이 단계의 판단 결과를 확인합니다.

for identity in ['', 'demo-alice', None]:
    print(bool(isinstance(identity, str) and identity.strip()))

실행 결과

False
True
False

실습의 실패와 수정 대조

브라우저에 starter를 입력하고 접근표를 완성합니다. 익명·타인·관리자 변경·빈 식별자 사례를 실행한 뒤 기대와 비교합니다.

확인 문제

실습

user·owner·action으로 ALLOW 또는 DENY 한 줄을 출력합니다. member/admin만 인정하고 유효한 소유자는 read/update/delete를 허용합니다. admin은 타인 read만 허용합니다. 빈 id·owner, 정의 밖 동작·역할·자료형, 깨진 JSON은 DENY입니다. user는 정책 연습용 신뢰 입력이며 서버에서는 세션에서 얻어야 합니다.

모범 답안
import sys, json

def allowed(data):
    if not isinstance(data, dict): return False
    user = data.get('user')
    owner = data.get('owner')
    action = data.get('action')
    if not isinstance(user, dict): return False
    uid = user.get('id')
    role = user.get('role')
    if not isinstance(uid, str) or not uid.strip(): return False
    if not isinstance(owner, str) or not owner.strip(): return False
    if role not in ('member', 'admin'): return False
    if action not in ('read', 'update', 'delete'): return False
    return uid == owner or (role == 'admin' and action == 'read')

try:
    data = json.loads(sys.stdin.read())
    print('ALLOW' if allowed(data) else 'DENY')
except (ValueError, TypeError):
    print('DENY')

더 읽기

면접 질문

  • 로그인한 사용자가 다른 사람의 자료를 읽는 문제를 설명합니다.