권한 판정 함수
80분 안팎
학습 목표
읽기 권한의 조건을 코드로 표현합니다.
개념
운영체제 규칙의 일부를 함수로 만들기
파일 권한을 눈으로 읽을 수 있어도 조건을 코드로 옮기면 다른 실수가 나타납니다. 소유자·그룹·기타를 모두 더해 보거나 그룹과 일치하면 항상 읽기를 주는 구현이 대표적입니다. 이번 레슨은 합성 UID·GID·권한을 받아 일반 파일 읽기 비트만 판정하는 함수를 만듭니다. Linux 파일을 직접 열거나 권한을 변경하지 않는 결정 모형이며 앞 레슨의 실제 접근 시험을 대신하지 않습니다.
함수는 입력 값을 받아 결과를 돌려주는 작은 판단 단위입니다. can_read는 불리언을 반환하고 표준 입력·출력은 바깥 코드가 맡습니다. 결과를 반환하면 같은 입력을 여러 테스트에 넣기 쉽습니다. 함수 안에서 안내 문구를 출력하면 채점 출력과 섞이고 호출자도 판단 결과를 사용하기 어렵습니다. 업무 규칙과 출력 형식을 분리하는 이유입니다.
정확한 입력 계약
한 줄은 euid, 파일 소유 UID, 파일 GID, 프로세스 그룹 목록, mode 순서의 다섯 필드입니다. 예시는 10001 0 10001 10001 640입니다. 그룹 목록은 기본 그룹과 추가 그룹을 합친 숫자를 쉼표로 연결하고, 그룹이 없으면 대시 하나를 씁니다. 이 형식은 합성 문제의 계약입니다. 실제 Linux 프로세스의 그룹을 자동 수집하는 프로그램은 아닙니다.
UID·GID는 0 이상의 십진 정수이고 mode는 000부터 777까지 세 자리 8진수입니다. 실제 root 동작을 모형에 넣지 않으므로 euid가 0이면 INVALID를 출력합니다. mode 888이나 0640, 음수 UID, 빈 쉼표 원소, 다섯 개가 아닌 필드도 INVALID입니다. 빈 줄은 건너뜁니다. 여러 줄이면 각 줄마다 ALLOW, DENY, INVALID 중 하나만 출력합니다.
INVALID는 정책상 거절과 다릅니다. 입력이 계약에 맞지 않아 계산하지 않았다는 뜻입니다. DENY는 유효한 일반 파일 모형에서 선택된 클래스에 읽기 비트가 없다는 뜻입니다. ALLOW는 그 비트가 있다는 뜻일 뿐 실제 경로 접근 성공이 아닙니다. 이 세 가지를 구분하면 자료 형식 오류를 권한 정책의 결과로 오해하지 않게 됩니다.
우선순위로 클래스 하나 선택하기
먼저 euid가 소유 UID와 같은지 봅니다. 같으면 소유자 자릿수를 선택하고 다른 클래스는 보지 않습니다. 다르면 파일 GID가 그룹 목록에 있는지 봅니다. 포함되면 그룹 자릿수를 선택하고 그렇지 않으면 기타 자릿수를 선택합니다. 이 분기는 한 번만 선택됩니다. 세 클래스의 읽기 비트를 OR로 합치는 것은 해당 규칙을 넓혀 버리는 잘못입니다.
소유자가 자신의 파일을 읽을 수 없게 040으로 설정한 예를 생각합니다. 그룹에는 읽기가 있지만 소유자 비트는 0입니다. euid와 owner가 같으면 소유자 클래스 0을 선택하므로 DENY입니다. 소유자 검사 뒤 그룹 검사까지 계속해 읽기를 얻으면 잘못입니다. 테스트에는 이 우선순위 함정을 넣어 단순히 그룹 포함 여부만 확인하는 구현을 잡습니다.
소유자가 아니고 파일 GID가 추가 그룹에 있는 경우도 그룹 클래스를 사용합니다. 그룹 목록의 첫 원소만 비교하면 뒤에 있는 그룹의 읽기를 놓칠 수 있습니다. 정수 목록 전체에 대한 in 검사를 씁니다. 소유자가 아니며 어느 그룹도 일치하지 않을 때에만 기타가 적용됩니다. 그룹 클래스가 선택됐는데 읽기가 없어도 기타 비트로 다시 기회를 주지 않습니다.
읽기 비트 계산하기
선택한 자릿수는 0에서 7 사이입니다. 읽기 비트는 4이므로 digit & 4가 0이 아닌지 계산합니다. 4는 읽기만, 5는 읽기·실행, 6은 읽기·쓰기, 7은 셋 모두를 포함합니다. digit이 4와 같은지만 비교하면 5·6·7의 읽기를 놓칩니다. 비트 AND는 값 안에 특정 비트가 포함됐는지 확인하는 연산입니다.
이 문제는 mode를 문자열로 보관해 각 문자를 int로 바꿉니다. 처음에 세 자리와 0~7만 있는지 확인했으므로 int(mode[0])처럼 읽어도 자릿수 값이 유지됩니다. 전체 mode를 십진 정수로 해석해 640을 연산하는 방식은 권한 비트 구조와 다릅니다. 전체 값을 사용하고 싶다면 int(mode, 8)로 변환하고 시프트 규칙을 명확히 해야 합니다.
can_read에는 euid, owner_uid, file_gid, groups, mode를 순서대로 전달합니다. 이 함수는 문법 검사가 끝난 입력만 받는다는 전제를 둡니다. 입력 파서에서 계약을 검사하고 판단 함수에서는 클래스 선택에 집중합니다. 중간 return은 해당 분기를 끝내므로 소유자 결과 뒤에 다른 클래스로 넘어가지 않습니다. 읽기 비트는 bool로 바꿔 호출자에게 돌려줍니다.
파서의 실패를 드러내기
parse_line은 공백으로 다섯 필드를 나누고 숫자 문자열을 먼저 검사합니다. UID·GID는 0 또는 0으로 시작하지 않는 양의 십진 표기를 허용합니다. 그룹 목록은 같은 형식의 숫자들을 쉼표로 나눕니다. 대시의 경우 빈 목록으로 만듭니다. 정해진 형식을 통과한 뒤 정수 변환을 수행하면 ValueError를 정상 출력처럼 숨길 필요가 없습니다.
빈 줄은 출력 줄을 만들지 않습니다. 반면 10001 0 10001 - 600은 유효한 한 줄이므로 DENY를 출력합니다. 그룹이 비어 있다는 것과 입력이 없다는 것이 다릅니다. 같은 줄이 반복되면 각각 결과를 출력합니다. 집합으로 줄을 제거하면 입력 건수와 결과 건수가 어긋납니다. 순서도 입력 순서를 유지하여 오류 위치를 쉽게 찾도록 합니다.
starter를 완성하고 경계 확인하기
starter는 파서를 제공하고 can_read가 항상 False를 반환합니다. 소유자·그룹·기타 선택과 읽기 비트 포함 판정을 완성합니다. 출력 문자열이나 입력 계약은 바꾸지 않습니다. 정상 읽기 사례가 통과하면 소유자 우선순위와 그룹 우선순위, 비어 있는 그룹, 읽기 없는 000, 기타 읽기 004를 순서대로 확인합니다. 경계 테스트를 통과하려고 결과를 입력별로 하드코딩하지 않습니다.
NameError는 변수 이름이나 정의 순서를, IndentationError는 분기의 들여쓰기를 확인하라는 단서입니다. 모든 사례가 DENY이면 반환값이 계속 False인지 봅니다. 040 소유자 사례만 ALLOW이면 여러 클래스를 합친 부분을 찾습니다. 그룹 목록 뒤쪽 사례만 실패하면 첫 그룹만 비교했는지 살핍니다. 예상 결과를 손으로 먼저 계산하면 코드가 틀린 지점을 빠르게 찾을 수 있습니다.
모형 밖의 권한을 남기기
이 함수는 부모 디렉터리 탐색, ACL, capabilities, 보안 모듈, 파일 시스템 상태, 이미 열린 파일 디스크립터를 검사하지 않습니다. Linux의 root는 능력에 따라 기본 비트 제약을 우회할 수 있으므로 이 문제는 root를 INVALID로 처리합니다. “Linux 권한 검사기 완성”이라고 이름 붙이지 않고 “일반 파일 읽기 비트 모형”이라고 설명합니다. 실제 접근은 앞의 컨테이너 시험으로 확인합니다.
비트 클래스 선택의 근거는 Linux 경로 해석 문서에 대조했습니다. 완료 후에는 선택한 클래스와 읽기 비트 판정을 말로 설명할 수 있어야 합니다. 함수의 상세 문법은 더 읽기로 보내고 이번에는 정책 우선순위, 유효하지 않은 입력, 모형의 한계를 테스트와 함께 남기는 데 집중합니다.
따라하기
함수의 한 클래스 선택 확인
소유자 040은 그룹 읽기가 있어도 DENY이고 그룹 640은 ALLOW입니다. 아래 결과는 함수 모형 계산입니다.
def can_read(euid, owner_uid, file_gid, groups, mode):
if euid == owner_uid:
digit = int(mode[0])
elif file_gid in groups:
digit = int(mode[1])
else:
digit = int(mode[2])
return bool(digit & 4)
print('ALLOW' if can_read(10001, 10001, 10001, [10001], '040') else 'DENY')
print('ALLOW' if can_read(10002, 0, 10001, [10002,10001], '640') else 'DENY')
실행 결과
DENY ALLOW
읽기 비트 포함 확인
4와의 같음 대신 비트 포함을 확인하면 5·6·7에서도 읽기가 유지됩니다.
for digit in [0, 2, 4, 5, 6, 7]:
print(digit, bool(digit & 4))
실행 결과
0 False 2 False 4 True 5 True 6 True 7 True
입력 오류 분리하기
파서가 root·잘못된 mode·필드 부족을 판단 함수로 넘기지 않는지 확인합니다.
import sys
import re
def parse_line(line):
fields = line.split()
if len(fields) != 5:
return None
euid, owner, gid, group_text, mode = fields
def number(text):
return re.fullmatch(r"0|[1-9][0-9]*", text) is not None
if not all(number(x) for x in (euid, owner, gid)) or euid == "0":
return None
if re.fullmatch(r"[0-7]{3}", mode) is None:
return None
groups = [] if group_text == "-" else group_text.split(",")
if not all(number(x) for x in groups):
return None
return int(euid), int(owner), int(gid), [int(x) for x in groups], mode
for line in ['0 0 0 0 777', '10001 0 0 - 888', 'missing', '10002 0 10001 10001 640']:
print('INVALID' if parse_line(line) is None else 'VALID')
실행 결과
INVALID INVALID INVALID VALID
기타와 그룹 우선순위 비교
그룹이 없으면 기타 004를 사용하지만 그룹 일치 때 그룹 0이 선택되면 읽기가 없습니다.
def can_read(euid, owner_uid, file_gid, groups, mode):
if euid == owner_uid:
digit = int(mode[0])
elif file_gid in groups:
digit = int(mode[1])
else:
digit = int(mode[2])
return bool(digit & 4)
for groups in [[], [10001]]:
print('ALLOW' if can_read(10002, 0, 10001, groups, '004') else 'DENY')
실행 결과
ALLOW DENY
확인 문제
실습
일반 파일 읽기 비트 모형의 can_read를 완성합니다. 한 줄은 euid owner_uid file_gid 그룹목록 mode 다섯 필드입니다. 그룹은 쉼표로 연결하고 없으면 -입니다. 소유자→그룹→기타 중 한 클래스만 선택하여 읽기 비트 4를 검사합니다. 유효한 결과는 ALLOW 또는 DENY, 계약 밖 입력과 euid 0은 INVALID, 빈 줄은 생략합니다. mode는 세 자리 0~7이며 숫자 식별자는 0 또는 선행 0 없는 양의 십진수입니다. 실제 파일을 열거나 root·ACL·부모 경로를 판정하지 않습니다.
모범 답안
import sys
import re
def parse_line(line):
fields = line.split()
if len(fields) != 5:
return None
euid, owner, gid, group_text, mode = fields
def number(text):
return re.fullmatch(r"0|[1-9][0-9]*", text) is not None
if not all(number(x) for x in (euid, owner, gid)) or euid == "0":
return None
if re.fullmatch(r"[0-7]{3}", mode) is None:
return None
groups = [] if group_text == "-" else group_text.split(",")
if not all(number(x) for x in groups):
return None
return int(euid), int(owner), int(gid), [int(x) for x in groups], mode
def can_read(euid, owner_uid, file_gid, groups, mode):
if euid == owner_uid:
digit = int(mode[0])
elif file_gid in groups:
digit = int(mode[1])
else:
digit = int(mode[2])
return bool(digit & 4)
for line in sys.stdin.read().splitlines():
if not line.strip():
continue
values = parse_line(line)
if values is None:
print("INVALID")
else:
print("ALLOW" if can_read(*values) else "DENY")
더 읽기
면접 질문
- 일반 파일 읽기에서 소유자·그룹·기타 클래스의 우선순위와 모형의 한계를 설명합니다.