[KO] CUBRID 인증과 권한 — 사용자, 비밀번호, 권한 그리고 시스템 카탈로그
목차
학술적 배경
섹션 제목: “학술적 배경”다중 사용자 RDBMS는 자기 사용자에게 두 가지 보장을 빚지고
있고, 그 두 보장은 거의 언제나 같은 카탈로그 테이블을 공유하는
서로 다른 두 서브시스템에서 흘러 나온다. Database System
Concepts (Silberschatz, Korth, Sudarshan) 4장 (“Intermediate
SQL) 의 GRANT/REVOKE 절과 5장 (Advanced SQL”) 의
authorization 절이 이 둘을 가른다. 인증 (authentication) 은
호출자가 누구인가? 라는 질문에 답하고, 권한 (authorization)
은 그 호출자가 무엇을 해도 되는가? 라는 질문에 답한다. 첫 번째
문제는 저장된 비밀 (정석은 salt 가 들어간 해시) 과 자격증명을
대조해 검증하는 것으로 환원되고, 두 번째 문제는
(주체, 객체, 동작) 으로 keyed 된 권한 비트맵을 조회해 동작이
시작되기 전에 yes/no 결정을 내리는 것으로 환원된다.
이 모델이 열어 둔 세 가지 구현 선택이 모든 실제 엔진의 모양을 잡고, 본 문서의 골격을 만든다.
- 사용자 레코드는 어디에 사는가. 전용 비밀 테이블
(PostgreSQL 의
pg_authid, MySQL 의mysql.user) 은 비밀 번호를 일반 카탈로그 바깥으로 빼서 별도의 lock 또는 비복제 정책 아래 보관할 수 있게 해 준다. 다른 길은 사용자를 다른 모든 것과 같은 카탈로그 안의 평범한 객체로 모델링하는 것이다 (CUBRID 의db_user/db_password가 그 길이다). 균일성 (모든 것이 객체이고 모든 것이 query 가능하다) 과, 비밀을 다른 lock 이나 복제 정책으로 둘 수 있는 옵션 사이의 절충이다. - 권한의 입자 (granularity) 가 어디까지 가는가. SQL 표준은
table/view/column/procedure 까지 간다. 실제 엔진은 보통 테이블
입자에서 멈추거나 (대부분), column 단위 grant 를 노출하거나
(PostgreSQL, Oracle), 더 나아가 row-level security 까지 확장
한다 (PostgreSQL RLS, Oracle VPD). CUBRID 11.3 은 grant 를 오직
_db_class에 대해서만 클래스 (table/view) 입자로 유지한다. 다른 객체 타입 (trigger, serial, synonym, server, procedure) 은 소유자 전용 또는 소유자+DBA 규칙 아래 살며 grantee 별 비트맵 이 없다. 정책 표현력과 권한 카탈로그의 크기, 그리고 read path 에서의 권한 검사 비용 사이의 절충이다. - 권한 검사는 언제 평가되는가. eager 하게 매 tuple 접근
시점에 (느리지만 빠르게 변하는 grant 아래에서 정확하다), 혹은
lazy 하게
(user, class)쌍 당 한 번 권한 비트맵을 계산해GRANT/REVOKE가 무효화할 때까지 클래스 메타데이터에 캐시 해 둔다. 거의 모든 현대 엔진은 캐시 방식을 고른다. read path 가 grant path 보다 백만 배 더 자주 흐르기 때문이다. CUBRID 의AU_CLASS_CACHE는 그 캐시를 사용자의 cache slot 으로 인덱스 되는 배열로 구현한다. 사용자 사이의 context switch (예: stored procedure 의 setuid) 가 indexed read 한 번이 된다.
DBA superuser 는 표준 탈출구다. 하나의 구별된 주체로서 권한
검사를 우회한다. bootstrap 의 수단을 그가 소유하기 때문이다. SQL
표준 (_SYSTEM) 도 교과서도 이를 out-of-band 처리한다. CUBRID
에서는 구별된 DBA 사용자가 au_install 에 의해 만들어지고,
모든 검사 지점에서 au_is_dba_group_member 로 인지된다.
이 두 보장과 세 선택이 짚히고 나면, 본 문서의 모든 CUBRID 고유 구조는 둘 중 하나를 구현하거나 그로부터 유래된 카탈로그 상태를 내구성 있게 만드는 일을 한다는 점이 분명해진다.
DBMS 공통 설계 패턴
섹션 제목: “DBMS 공통 설계 패턴”서로 다른 SQL 방언에도 불구하고, 주요 RDBMS 엔진들은 사용자 관리에서 같은 다섯 조각 설계로 수렴한다. 조각들과 그에 대한 CUBRID 의 매핑은 아래에 있다. 이 다음 절부터는 CUBRID 쪽을 자세히 따라간다.
사용자 카탈로그 테이블
섹션 제목: “사용자 카탈로그 테이블”PostgreSQL 은 사용자 (와 8.1 부터는 통합된 role) 를 pg_authid
에 저장하며, 비밀번호 해시도 같은 행에 둔다. MySQL 은
mysql.user 에 (user, host) 쌍 당 한 행을 두고 해시된
authentication_string 을 함께 보관한다. Oracle 의 SYS.USER$
가 DBA_USERS 뷰의 바탕이 되며, 역시 해시를 같은 행에 들고
다닌다. CUBRID 은 사용자 레코드 (db_user) 와 비밀번호 레코드
(db_password) 를 두 개의 클래스로 쪼갠다는 점에서 특이하다.
두 클래스는 password 참조 attribute 로 연결되며, db_user 의
한 행은 문자 그대로 db_password 인스턴스에 대한 객체 포인터를
들고 있다.
Salt 와 알고리즘 prefix 가 붙은 비밀번호 해시
섹션 제목: “Salt 와 알고리즘 prefix 가 붙은 비밀번호 해시”현대 엔진은 비밀번호를 (random-salt, hash, algorithm-id) 세
쌍의 하나로 저장한다. PostgreSQL 은 SCRAM-SHA-256 또는 MD5
prefix 를 저장 문자열에 찍는다. MySQL 은
mysql_native_password 를
*<HEX-of-SHA1(SHA1(pw))> prefix 를 붙인다. CUBRID 은 인코딩된
비밀번호의 offset 0 위치에 1바이트 알고리즘 prefix 를 태깅한다.
레거시 DES 는 \x01, SHA-1 은 \x02, SHA-2/512 는 \x03.
11.5 에서 해시는 unsalted 다. crypt_sha_two 는 비밀번호만
받아 hex digest 를 돌려 주고, 매칭은 양쪽이 같은 알고리즘 tag
로 정규화된 뒤 문자열 비교로 끝난다 (match_password 참조).
권한 카탈로그
섹션 제목: “권한 카탈로그”PostgreSQL 은 객체별 카탈로그 행 (pg_class.relacl 등) 안에
aclitem[] 배열을 둔다. MySQL 은 mysql.db,
mysql.tables_priv, mysql.columns_priv 를 두며, Oracle 은
SYS.OBJAUTH$ 를 둔다. 인코딩은 다르지만 모양은 같다. (grantor, grantee, object, privilege_bits, with_grant_option)
로 이루어진 다대다 관계다. CUBRID 은 그런 테이블을 둘 가진다.
_db_auth (완전 정규화된 관계, unloaddb 와 information
schema 질의가 사용한다) 와 db_authorization (grantee 별 한 행
으로 비정규화되어 그 grantee 의 모든 grant 를 단일 sequence
attribute 에 묶어 둔, 런타임 권한 검사가 사용하는 행). 두 표는
au_grant/au_revoke 안에서 동기화된다.
GRANT / REVOKE 의미론
섹션 제목: “GRANT / REVOKE 의미론”SQL 표준은 recursive revoke 를 요구한다. 하나의 grant 를
취소하면, 그 첫 번째 grant 의 권위 아래 grantee 가 발급한 모든
grant 도 함께 거둬들여진다. PostgreSQL 과 Oracle 은 이를 구현
한다. MySQL 은 그렇지 않다 (8.0 의 role 개편 전까지 revoke 는
non-cascading 이다). CUBRID 은 구현한다. propagate_revoke 가
소유자에서 출발해 grant 그래프를 걷고, 방금 취소된 edge 없이는
도달 불가능한 모든 grant 에 legal == 0 을 표시한 뒤, 그 grant
들을 삭제한다.
런타임 캐시
섹션 제목: “런타임 캐시”매 행 read 마다 권한 카탈로그를 lookup 하는 것은 감당할 수 없다.
PostgreSQL 은 relation 별, role 별 ACL 캐시를 유지한다. Oracle
은 객체 권한을 dictionary cache 에 둔다. CUBRID 은 이를
AU_CLASS_CACHE 로 구체화한다. 클래스 당 한 비트맵
(unsigned int *data) 이 사용자의 cache_index 로 인덱스되며,
주어진 (사용자, 클래스) 에 대한 첫 번째 au_fetch_class 시점에
authenticate_cache::update 가 lazy 하게 채워 넣는다. GRANT
나 REVOKE 는 단일 (user, class) 슬롯을
reset_cache_for_user_and_class 로 무효화하거나, 더 광범위한
변경을 모든 캐시 비트를 AU_CACHE_INVALID = 0x80000000
으로 뒤집는다.
이론 ↔ CUBRID 명칭 매핑
섹션 제목: “이론 ↔ CUBRID 명칭 매핑”| 이론적 개념 | CUBRID 명칭 |
|---|---|
| 사용자 카탈로그 테이블 | db_user (시스템 클래스, MOP 캐시 Au_user_class) |
| 비밀번호 카탈로그 테이블 | db_password (별도 클래스, db_user.password 로부터 join) |
| 권한 카탈로그 (정규화) | _db_auth ((grantor, grantee, object, auth_type) 마다 한 행) |
| 권한 카탈로그 (비정규화) | db_authorization (grantee 마다 한 행, grants 는 (type,obj,grantor,cache) tuple 의 sequence) |
| 권한 root | db_root (단일 인스턴스, MOP 캐시 Au_root) |
| DBA superuser | DBA 사용자 (Au_dba_user); au_is_dba_group_member 로 인지 |
| Public role | PUBLIC 사용자 (Au_public_user); 모든 신규 사용자가 기본으로 멤버 |
| (사용자, 객체) 별 권한 비트맵 | AU_TYPE_MASK = 0x7F, grant cache 의 하위 7비트 (AU_SELECT…AU_EXECUTE) |
WITH GRANT OPTION | cache 의 상위 7비트, AU_GRANT_SHIFT = 8 로 시프트 (AU_GRANT_MASK = 0x7F00) |
| 런타임 캐시 | 클래스 당 AU_CLASS_CACHE, cache_index keyed; AU_CACHE_INVALID = 0x80000000 |
| 로그인 비활성화 | AU_DISABLE/AU_ENABLE 매크로가 disable_auth_check 를 설정 |
| 비밀번호 무시 | ignore_passwords (au_disable_passwords 가 설정, SA-mode 유틸리티가 사용) |
| 비밀번호의 알고리즘 tag | ENCODE_PREFIX_DES (1), ENCODE_PREFIX_SHA1 (2), ENCODE_PREFIX_SHA2_512 (3) |
| 권한 타입 | AU_SELECT 0x01, AU_INSERT 0x02, AU_UPDATE 0x04, AU_DELETE 0x08, AU_ALTER 0x10, AU_INDEX 0x20, AU_EXECUTE 0x40 |
au_grant 가 받는 객체 타입 | DB_OBJECT_CLASS, DB_OBJECT_PROCEDURE (11.5 에서 두 가지뿐) |
CUBRID의 구현
섹션 제목: “CUBRID의 구현”인증 모듈에는 네 개의 이동 부품이 있다. 카탈로그 스키마
(db_root, db_user, db_password, _db_auth,
db_authorization), 시스템 MOP 들과 현재 사용자 MOP 를 핀하고
있는 인메모리 컨텍스트 객체 (authenticate_context),
권한 lookup 을 비트마스크 검사로 바꿔 주는 권한 캐시
(authenticate_cache), 그리고 엔진의 나머지를 위 모든 것에 잇는
진입점들 (au_login, au_fetch_class, au_grant,
au_revoke, au_change_*_owner). 이 순서로 본다.
모듈 모양과 진입점
섹션 제목: “모듈 모양과 진입점”flowchart LR
subgraph CONSUMERS["엔진 진입점"]
CDB["createdb<br/>(SA_ONLY)"]
UDB["unloaddb<br/>(SA_ONLY)"]
CSQL["csql / CAS<br/>(CS / SA)"]
end
subgraph AUTH["src/object/authenticate*"]
AUI["au_install<br/>(authenticate_context.cpp)"]
AUS["au_start<br/>(authenticate_context.cpp)"]
AUL["au_login<br/>(authenticate.c)"]
AUF["au_fetch_class<br/>(authenticate_access_class.cpp)"]
AUG["au_grant / au_revoke<br/>(authenticate_grant.cpp)"]
AUO["au_change_∗_owner<br/>(authenticate_owner.cpp)"]
AUE["au_export_users<br/>au_export_grants<br/>(authenticate_migration.cpp)"]
CACHE["authenticate_cache<br/>(authenticate_cache.cpp)"]
end
subgraph CATALOG["시스템 카탈로그 (일반 CUBRID 클래스)"]
ROOT[("db_root")]
USR[("db_user")]
PWD[("db_password")]
DBA[("db_authorization")]
AUT[("_db_auth")]
end
CDB --> AUI
UDB --> AUE
CSQL --> AUL
CSQL --> AUF
CSQL --> AUG
CSQL --> AUO
AUI --> ROOT
AUI --> USR
AUI --> PWD
AUI --> DBA
AUI --> AUT
AUL --> USR
AUL --> PWD
AUF --> CACHE
AUG --> DBA
AUG --> AUT
AUG --> CACHE
이 그림이 인코딩하는 경계가 셋이다. (client / catalog) 인증
모듈은 클라이언트 측 이다. 위에 나열된 모든 translation unit
은 cubridcs (CS-mode 클라이언트) 와 cubridsa (in-process
독립 모드 클라이언트+서버) 에는 컴파일되지만, cub_server 에는
들어가지 않는다. 헤더는 #if defined (SERVER_MODE) #error Does not belong to server module #endif 로 시작한다. (install /
start) au_install 은 데이터베이스 라이프타임 당 한 번 돈다
(createdb 안에서). au_start 는 클라이언트 연결 당 한 번 돈다
(boot_restart_client 안에서). (read / write)
au_fetch_class 는 read path 위에 있고 캐시를 거친다.
au_grant/au_revoke/au_change_*_owner 는 write path 위에
있고 _db_auth 와 db_authorization 양쪽을 갱신한 뒤 캐시를
무효화한다.
au_install 이 설치하는 카탈로그 스키마
섹션 제목: “au_install 이 설치하는 카탈로그 스키마”스키마는 authenticate_context::install (옛 이름 au_install)
안에서 db_create_class 와 smt_add_attribute 의 직접 호출로
만들어진다. 의도가 자명하지 않은 자리에는 주석을 곁들였다.
// authenticate_context::install — src/object/authenticate_context.cpp// db_root: single-instance class, holds the authorization rootsmt_add_attribute (def, "triggers", "sequence of (string, object)", ...);smt_add_attribute (def, "charset", "integer", ...);smt_add_attribute (def, "lang", "string", ...);smt_add_attribute (def, "timezone_checksum", "string", ...);// + class methods: add_user, drop_user, find_user, change_owner, ...
// db_user: one row per user, the public-facing user recordsmt_add_attribute (def, "name", "string", ...); // unique-indexedsmt_add_attribute (def, "id", "integer", ...);smt_add_attribute (def, "password", AU_PASSWORD_CLASS_NAME, ...); // → db_passwordsmt_add_attribute (def, "direct_groups", "set of (db_user)", ...);smt_add_attribute (def, "groups", "set of (db_user)", ...); // flattenedsmt_add_attribute (def, "authorization", AU_AUTH_CLASS_NAME, ...); // → db_authorizationsmt_add_attribute (def, "triggers", "sequence of object", ...);smt_add_attribute (def, AU_USER_ATTR_IS_LOGINABLE, "integer", NULL); // can this user log in?smt_add_attribute (def, AU_USER_ATTR_IS_SYSTEM_CREATED, "integer", NULL); // DBA / PUBLIC markersmt_add_attribute (def, "comment", "varchar(1024)", NULL);smt_add_attribute (def, "created_time", "datetime", NULL);smt_add_attribute (def, "updated_time", "datetime", NULL);// + instance methods: set_password, add_member, drop_member, ...
// db_password: holds the actual encoded password stringsmt_add_attribute (def, "password", "string", ...); // [tag-byte][hash-hex]\0
// db_authorization: one row per grantee, denormalisedsmt_add_attribute (def, AU_AUTH_ATTR_OWNER, AU_USER_CLASS_NAME, ...); // granteesmt_add_attribute (def, AU_AUTH_ATTR_GRANTS, "sequence", ...); // [type, obj, grantor, cache]*_db_auth 는 schema_system_catalog_install.cpp 에서 별도로
기술된다. (grantor, grantee, object, auth_type) 마다 한 행을
들고 다니며, unloaddb 가 GRANT 문을 생성할 때 읽는 표가
바로 이것이다.
부트스트랩이 발급하는 두 가지 grant 는 짚어 둘 만하다. PUBLIC
은 db_root 와 db_user 를 SELECT|EXECUTE 를 받는다 (그
래서 어떤 사용자든 사용자 목록을 introspect 할 수 있다). 그러나
db_authorization 에 대해서는 SELECT 만 받고, db_password
에는 어떤 grant 도 주어지지 않는다. 권한 없는 사용자는 다른
사용자의 비밀번호 해시를 select * from db_password 로조차
읽을 수 없다는 뜻이다. 행이 fetch 되기 전에 권한 검사가
ER_AU_SELECT_FAILURE 로 실패한다.
authenticate_context — 글로벌 메모
섹션 제목: “authenticate_context — 글로벌 메모”CUBRID 은 인증 상태를 호출 사슬을 따라 내려보내지 않는다. 그
상태는 au_ctx() (authenticate.c 안) 로 접근하는 싱글턴 안에
산다. 헤더는 글로벌 변수처럼 보이지만 실제로는 싱글턴의 필드로
펼쳐지는 매크로 집합을 정의한다. pre-class C 시절 코드와의
호환을 위해서다.
#define Au_root au_ctx ()->root // db_root instance MOP#define Au_user au_ctx ()->current_user // db_user instance MOP for the current user#define Au_dba_user au_ctx ()->dba_user // db_user instance MOP for DBA#define Au_public_user au_ctx ()->public_user // db_user instance MOP for PUBLIC#define Au_disable au_ctx ()->disable_auth_check#define Au_user_class au_ctx ()->user_class // db_user class MOP#define Au_password_class au_ctx ()->password_class // db_password class MOP#define Au_authorization_class au_ctx ()->authorization_class // db_authorization class MOP#define Au_cache au_ctx ()->caches // authenticate_cache이 싱글턴은 thread-safe 하지 않다. 인증 모듈은 클라이언트 측
이고 CUBRID 클라이언트 (cubridcs) 는 연결 당 단일 스레드 구조
이므로 이 점은 문제가 되지 않는다. 원본 분석 문서 첫머리 주석이
명시적으로 적어 둔다. “authenticate 모듈이 속해있는 cubridcs는
single-thread 구조이므로 호출한 API는 대부분 thread-safety 가
아니다.”
라이프사이클은 다음과 같다.
au_init ──▶ au_install ──▶ au_start ──▶ tasks* ──▶ au_final (createdb) (per-conn) (shutdown) ▲ │ au_login (csql)au_init 은 사실상 au_ctx 다 (authenticate.h 안의 매크로) —
첫 호출이 싱글턴을 구성하고 init_ctx 가 모든 MOP 를 0으로
초기화한다. au_install 은 오직 createdb 만 호출한다.
au_start 는 데이터베이스가 열린 뒤 boot_restart_client 에서
호출된다. sm_find_class 를 네 번 호출해 root_class,
authorization_class, user_class, password_class 를 채우고,
PUBLIC 과 DBA 를 au_find_user 를 호출한 뒤, 마지막으로
au_login 이 미리 캐시해 둔 자격증명으로 perform_login 을
호출한다.
로그인 흐름
섹션 제목: “로그인 흐름”로그인은 두 단계로 쪼개진다. 사용자가 보통 데이터베이스가 열리기
전 에 자격증명을 제공하기 때문이다 (csql -u dba mydb 를 떠올려
보라. 자격증명 파싱은 argv 단계에서 일어나는데 데이터베이스 boot
은 그 뒤에 일어난다).
sequenceDiagram
participant U as 사용자<br/>(csql / CAS / 유틸리티)
participant L as au_login
participant CTX as authenticate_context
participant BO as boot_restart_client
participant START as au_start
participant PL as perform_login
participant DB as db_user / db_password
U->>L: au_login("u1", "secret", false)
L->>CTX: not BOOT_IS_CLIENT_RESTARTED?<br/>이름 + 3개 해시 저장
CTX-->>L: NO_ERROR
Note over CTX: user_name="U1"<br/>user_password_des_oldstyle = "\x01" + DES("secret")<br/>user_password_sha1 = "\x02" + SHA1("secret")<br/>user_password_sha2_512 = "\x03" + SHA512("secret")
U->>BO: db_restart()
BO->>START: au_start()
START->>DB: sm_find_class(db_user/...)
START->>DB: au_find_user("PUBLIC"), au_find_user("DBA")
START->>PL: perform_login(name, sha2_512, false)
PL->>DB: au_find_user("U1")
PL->>DB: obj_get(user, "password") → db_password row
PL->>DB: obj_get(pass, "password") → "\x03..."
PL->>PL: match_password(supplied, stored)
PL-->>U: NO_ERROR or ER_AU_INVALID_PASSWORD
첫 단계는 authenticate_context.cpp 의
authenticate_context::login 이다. 흥미로운 줄은 제공된
비밀번호의 세 겹 해싱이다.
// authenticate_context::login — src/object/authenticate_context.cppif (root == NULL || !BOOT_IS_CLIENT_RESTARTED ()) { if (name != NULL) strcpy (user_name, name);
if (password == NULL || strlen (password) == 0) { // empty password: zero out all three buffers } else { /* store the password encrypted (DES and SHA1 both) so we * don't have buffers lying around with the obvious * passwords in it. */ encrypt_password (password, 1, user_password_des_oldstyle); encrypt_password_sha1 (password, 1, user_password_sha1); encrypt_password_sha2_512 (password, user_password_sha2_512); } }else { /* Change users within an active database. */ AU_DISABLE (save); error = perform_login (name, password, ignore_dba_privilege); AU_ENABLE (save); }CUBRID 은 같은 평문을 세 가지 인코딩을 보관한다. 데이터
베이스가 메이저 버전을 가로지르며 업그레이드된 레거시 DB 일 수
있고, 디스크 위 행이 세 포맷 중 어느 것이든 될 수 있기 때문이다.
매칭 함수 (authenticate_password.cpp 의 match_password) 는
저장된 해시의 prefix 바이트를 검사해 매칭되는 미리 해싱된 버퍼를
골라 비교한다.
두 번째 단계가 perform_login 이다. 사용자 MOP 를 검증하고,
is_loginable attribute (사용자별 disable 플래그) 를 검사하고,
password attribute 를 fetch 하고, 그것을 db_password 행으로
역참조한 뒤, 그 행의 password attribute 를 fetch 해
match_password 를 돌린다.
// authenticate_context::perform_login — src/object/authenticate_context.cppif (!ignore_passwords && (!au_is_dba_group_member (current_user) || ignore_dba_privilege)) { pass = NULL; if (!DB_IS_NULL (&value) && db_get_object (&value) != NULL) { if (obj_get (db_get_object (&value), "password", &value)) ... if (DB_IS_STRING (&value)) pass = db_get_string (&value); }
if (pass != NULL && strlen (pass)) { if ((dbpassword == NULL) || (strlen (dbpassword) == 0) || !match_password (dbpassword, db_get_string (&value))) { error = ER_AU_INVALID_PASSWORD; er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, error, 0); } } // ... condensed: empty-stored-password vs empty-supplied-password ... }
if (error == NO_ERROR) { error = set_user (user); sm_bump_local_schema_version (); // flush any cached vclasses for the old user }자명하지 않은 사실 두 가지. 첫째,
!au_is_dba_group_member (current_user) || ignore_dba_privilege
가드는 이미 DBA 로 로그인된 세션이 다른 어떤 사용자로든 비밀
번호 없이 전환할 수 있게 한다는 뜻이다. 호출자가
ignore_dba_privilege == true 를 넘기지 않는 한. 이것이 csql -u dba 후에 ;set 으로 다른 사용자의 비밀번호 없이 사용자를 바꿀
수 있게 해 주는 메커니즘이다. 유틸리티에 편리하지만, DBA 계정
자체가 침해되면 위험하다. 둘째, 성공 시 함수는
sm_bump_local_schema_version 을 호출해 캐시된 vclass 들을 무효
화한다. 그렇지 않으면 이전 사용자가 활성일 때 컴파일된 vclass 가
새 사용자에게 보이지 않아야 할 행을 누설할 수 있다.
비밀번호 인코딩 — encrypt_password_* 와 match_password
섹션 제목: “비밀번호 인코딩 — encrypt_password_* 와 match_password”encrypt_password* 뒤에 세 인코더가 앉아 있고, 각각 호출자가
제공한 2 KB 버퍼에 쓴다.
// encrypt_password_sha2_512 — src/object/authenticate_password.cppvoidencrypt_password_sha2_512 (const char *pass, char *dest){ int error_status = NO_ERROR; char *result_strp = NULL; int result_len = 0;
if (pass == NULL) { strcpy (dest, ""); } else { error_status = crypt_sha_two (NULL, pass, strlen (pass), 512, &result_strp, &result_len); if (error_status == NO_ERROR) { memcpy (dest + 1, result_strp, result_len); dest[result_len + 1] = '\0'; dest[0] = ENCODE_PREFIX_SHA2_512; /* '\x03' */ db_private_free_and_init (NULL, result_strp); } else { strcpy (dest, ""); } }}crypt_sha_two 는 src/base/crypt_opfunc.c 의 SHA-2/512 루틴
이다. 11.5 에서는 unsalted 다. 첫 번째 NULL 인자가 salt
자리고, CUBRID 은 항상 NULL 을 넘긴다. 출력은 길이 128 의 hex
문자열이고, \x03 prefix 가 붙어 총 129 + NUL 길이가 된다.
DES 경로 (encrypt_password) 는 authenticate_password.hpp 에
박혀 있는 고정 seed PASSWORD_ENCRYPTION_SEED = U9a$y1@zw~a0%
를 쓴다. SHA-1 경로 (encrypt_password_sha1) 도 unsalted 다. 두
레거시 경로는 오직 — 옛 CUBRID 설치에서 업그레이드된 데이터베이스
가 여전히 인증할 수 있도록 — 존재한다. 새로운 비밀번호 설정은
언제나 SHA-2/512 경로를 거친다 (set_password_internal 이
encode == 1 일 때 encrypt_password_sha2_512 를 무조건적으로
호출한다).
매칭 함수는 저장 측의 prefix 바이트로 알고리즘을 인지한 뒤,
제공된 비밀번호를 매칭되는 알고리즘으로 미리 해싱해 비교를 단순
strcmp 로 만든다.
// match_password — src/object/authenticate_password.cpp (condensed)boolmatch_password (const char *user, const char *database){ char buf1[AU_MAX_PASSWORD_BUF + 4]; // user-side, post-encoding char buf2[AU_MAX_PASSWORD_BUF + 4]; // database-side, copied verbatim
if (IS_ENCODED_DES (database)) { strcpy (buf2, database); /* user side: DES-encode if not already */ } else if (IS_ENCODED_SHA1 (database)) { strcpy (buf2, database); /* user side: SHA1-encode if not already */ } else if (IS_ENCODED_SHA2_512 (database)) { strcpy (buf2, database); if (IS_ENCODED_ANY (user)) strcpy (buf1, Au_user_password_sha2_512); // already pre-hashed at au_login else encrypt_password_sha2_512 (user, buf1); // encode now } else { /* unencoded legacy DB: SHA2-encode the DB side too */ }
return strcmp (buf1, buf2) == 0;}au_login 이 평문 비밀번호로 호출되었을 때, 그 함수는 비밀번호를
세 버퍼 (Au_user_password_des_oldstyle, Au_user_password_sha1,
Au_user_password_sha2_512) 모두에 미리 해싱해 둔 상태였다. 그래서
match_password 는 사용자 측에서 *다시 해싱하지 않는다. 알맞은
미리 해싱된 버퍼를 고를 뿐이다.
SELECT 시점의 권한 평가
섹션 제목: “SELECT 시점의 권한 평가”모듈을 가장 자주 흐르는 경로는 SELECT 위의 권한 검사다. 질의
실행기는 자기에게 필요한 모든 클래스를 au_fetch_class 를
호출한다. 그 루틴은 클래스별 캐시를 조회하고, 현재 사용자를
아직 채워지지 않았다면 authenticate_cache::update 로 fall back
하여 비트맵을 계산한다.
flowchart TD
S["query_executor:<br/>SELECT ∗ FROM t"] --> F["au_fetch_class(t, AU_FETCH_READ, AU_SELECT)"]
F --> AU{"Au_user == NULL?"}
AU -- yes --> ERR1["ER_AU_INVALID_USER"]
AU -- no --> FC["fetch_class<br/>(locator + lock)"]
FC --> CHK["check_authorization<br/>(class, AU_SELECT)"]
CHK --> DIS{"Au_disable<br/>· 카탈로그 아님<br/>· admin client 아님?"}
DIS -- yes --> OK1["return NO_ERROR"]
DIS -- no --> CACHE["Au_cache.get_cache_bits(class)"]
CACHE --> HIT{"bits & AU_SELECT?"}
HIT -- yes --> OK2["return NO_ERROR"]
HIT -- "AU_CACHE_INVALID" --> UPD["Au_cache.update(class, sm_class)"]
UPD --> CACHE2["bits = get_cache_bits(class)"]
CACHE2 --> HIT2{"bits & AU_SELECT?"}
HIT2 -- yes --> OK3["return NO_ERROR"]
HIT2 -- no --> ERR2["appropriate_error<br/>→ ER_AU_SELECT_FAILURE"]
HIT -- no --> ERR3["appropriate_error<br/>→ ER_AU_SELECT_FAILURE"]
이를 끌고 가는 두 au_fetch_class 본문 발췌가
authenticate_access_class.cpp 안에 있다. 바깥 wrapper 는 다음
과 같다.
// au_fetch_class_internal — src/object/authenticate_access_class.cppif (Au_user == NULL && !Au_disable) { error = ER_AU_INVALID_USER; er_set (ER_WARNING_SEVERITY, ARG_FILE_LINE, error, 1, ""); return error; }
// ... condensed: locator_fetch_class (or skip if already in workspace) ...
if ((Au_disable && type != DB_AUTH_ALTER) || ! (error = check_authorization (classmop, class_, type))) { if (class_ptr != NULL) *class_ptr = class_; }
return error;비트마스크 검사는 check_authorization 안에 있다.
// check_authorization — src/object/authenticate_access_class.cppif (Au_disable) { int client_type = db_get_client_type (); if (!BOOT_ADMIN_CSQL_CLIENT_TYPE (client_type) || ! (sm_class->flags & SM_CLASSFLAG_SYSTEM)) return NO_ERROR; }
/* try to catch attempts by even the DBA to update a protected class */if ((sm_class->flags & SM_CLASSFLAG_SYSTEM) && is_protected_class (classobj, sm_class, type)) { error = appropriate_error (0, type); er_set (ER_WARNING_SEVERITY, ARG_FILE_LINE, error, 0); }else { bits = Au_cache.get_cache_bits (sm_class); if ((*bits & type) != type) { if (*bits == AU_CACHE_INVALID) { error = Au_cache.update (DB_OBJECT_CLASS, classobj, sm_class); // re-fetch and re-check bits = Au_cache.get_cache_bits (sm_class); if ((*bits & type) != type) error = appropriate_error (*bits, type); } else error = appropriate_error (*bits, type); } }쥐고 갈 만한 점이 두 가지 있다. 첫째, Au_disable 이 모든
것을 비활성화하지는 않는다. is_protected_class 가지가 시스템
카탈로그 클래스에 대한 직접 ALTER/INSERT/UPDATE/DELETE/
INDEX 를 DBA 라도 막는다. 의도는 카탈로그 변형이 전용 API
(au_grant, au_change_class_owner) 를 거치고 맨 DML 을 거치지
않게 하는 것이다. 둘째, 캐시는 클래스 별 이지만 사용자 별
이기도 하다. 각 클래스는 길이가 cache_max (extend_class_caches
가 할당한 크기) 인 unsigned int *data 를 가지며, 그 배열로의
오프셋이 사용자의 cache_index (make_user_cache 가 할당) 다.
authenticate_cache::update — grant 가 비트로 변하는 곳
섹션 제목: “authenticate_cache::update — grant 가 비트로 변하는 곳”check_authorization 이 AU_CACHE_INVALID 를 발견하면
Au_cache.update 를 호출하는데, 이것이 카탈로그 상태를 비트마스크
로 바꿔 주는 함수다. 살짝 압축한 로직은 다음과 같다.
// authenticate_cache::update — src/object/authenticate_cache.cpp*bits = AU_NO_AUTHORIZATION; // start at zero
if (au_is_dba_group_member (Au_user)) // shortcut for DBA / DBA member { *bits = FULL_AUTH; // 0x7F7F (everything + grant option) goto end; }
if (ws_is_same_object (Au_user, owner) || ws_is_same_object (Au_public_user, owner)) { *bits = FULL_AUTH; // owner → all rights, PUBLIC-owned classes too goto end; }
// Non-owner, non-DBA path: walk grantsau_get_set (Au_user, "groups", &groups); // user's groups
if (set_ismember (groups, &owner)) // user is a group member of owner *bits = FULL_AUTH;else { // 1) apply grants directly held on this class au_get_object (Au_user, "authorization", &auth); apply_grants (auth, mop, bits);
// 2) apply grants held by every group the user is a member of card = set_cardinality (groups); for (i = 0; i < card; i++) { au_set_get_obj (groups, i, &group); if (ws_is_same_object (group, Au_dba_user)) { *bits = FULL_AUTH; } // group is DBA → all rights else { au_get_object (group, "authorization", &auth); apply_grants (auth, mop, bits); } } }apply_grants (authenticate_grant.cpp 안) 는
db_authorization.grants sequence 를 걸어, GRANT_ENTRY_CLASS
원소가 mop 와 일치하는 항목을 찾고, 그 GRANT_ENTRY_CACHE 원소
를 *bits 에 OR 한다. 총 비용은 그룹 당 grantee 의 grants
sequence 한 번 + 사용자 자신을 위한 한 번이다. 그룹이 O(1) 인
일반적인 사용자에게 이 비용은 그가 보유한 grant 수 k 를 O(k)
가 된다.
이 함수는 Au_disable == true 아래에서 호출된다. 캐시 walk
도중에는 인증 컨텍스트가 일시적으로 비활성화되어 있다. 그렇지 않
으면 db_authorization 을 읽는 일 자체가 또 다른 au_fetch_class
를 트리거해 무한 재귀에 빠진다.
au_grant 와 au_revoke
섹션 제목: “au_grant 와 au_revoke”au_grant 는 GRANT 문의 진입점이다. CUBRID 11.5 는 두 객체
타입을 지원한다.
// au_grant — src/object/authenticate_grant.cppintau_grant (DB_OBJECT_TYPE obj_type, MOP user, MOP class_mop, DB_AUTH type, bool grant_option){ int error = NO_ERROR; switch (obj_type) { case DB_OBJECT_CLASS: error = au_grant_class (user, class_mop, type, grant_option); break; case DB_OBJECT_PROCEDURE: error = au_grant_procedure (user, class_mop, type, grant_option); break; default: error = ER_FAILED; assert (false); break; } return error;}au_grant_class 가 일꾼이다. 다섯 가지를 순서대로 한다. (1)
sm_partitioned_class_type 을 통한 partition-aware 재귀 grant
(그래서 GRANT SELECT ON tbl_partitioned 이 모든 파티션에 fan
out 한다), (2) au_compare_grantor_and_return 을 통한 유효
grantor 결정 (현재 사용자가 DBA 이거나 owner 그룹의 멤버이면 그
사람, 그렇지 않으면 요청된 권한을 WITH GRANT OPTION 과 함께
들고 있는 사용자), (3) write lock 으로 grantee 의
db_authorization 행 fetch, (4)
au_auth_accessor::insert_auth 와 update_auth 를 통한 _db_auth
행 insert / update, (5) 비정규화된 db_authorization.grants
sequence 갱신과
Au_cache.reset_cache_for_user_and_class 를 통한 (사용자, 클래스)
캐시 슬롯 reset.
_db_auth 와 db_authorization 양쪽으로의 이중 쓰기가 grant
정확성의 hot spot 이다. 두 표는 일관성을 유지해야 한다. 순서는
정규화 먼저, 비정규화 다음 이다. 그래서 두 사이에서 트랜잭션이
중단되면 둘이 함께 롤백된다.
au_revoke 는 진입 모양에서 대칭이지만 recursive revoke 로직이
덧붙는다. 요청된 grant 를 떨어뜨린 뒤,
collect_class_grants 를 호출해 class_mop 를 존재하는
type 의 모든 grant 를 열거하고, propagate_revoke 가 결과
그래프를 class_owner 에서 출발해 걸으며, 방금 취소된 edge 없이
도달 가능한 모든 grant 를 legal == 1 로 표시한 뒤,
legal == 0 인 모든 grant 를 삭제한다. 구현은 그 비용에 정직
하다. “Since we don’t keep the grants associated with the class
object, we have to visit every user object and collect the grants
for that class” 라는 collect_class_grants 의 주석이 있다.
Owner 변경 — au_change_class_owner 와 친구들
섹션 제목: “Owner 변경 — au_change_class_owner 와 친구들”CUBRID 의 객체별 권한은 객체 타입 당 하나씩 여러 owner change
루틴으로 쪼개져 있다. 비대칭은 의도된 것이다. 오직
db_class (au_change_class_owner) 와 procedure
(au_change_sp_owner) 만이 그 객체를 보유된 권한 을 함께
이전할 수 있다. 나머지는 owner attribute 만 다시 쓰고, 권한 카탈
로그가 나중에 따라잡도록 둔다.
// au_check_owner — src/object/authenticate_owner.cppintau_check_owner (DB_VALUE *creator_val){ MOP creator; DB_SET *groups; int ret_val = ER_FAILED;
creator = db_get_object (creator_val); if (ws_is_same_object (creator, Au_user) || au_is_dba_group_member (Au_user)) { ret_val = NO_ERROR; } else if (au_get_set (Au_user, "groups", &groups) == NO_ERROR) { if (set_ismember (groups, creator_val)) ret_val = NO_ERROR; set_free (groups); } return ret_val;}이 세 갈래 검사 — 내가 owner 인가?, 내가 DBA 그룹의 멤버인가?,
owner 가 그 멤버인 그룹에 내가 속하는가? — 가 11.5 에서 serial,
server, trigger, synonym, stored procedure 접근의 유일한 권한
게이트다. au_check_serial_authorization 은 owner attribute 를
fetch 한 뒤 au_check_owner 를 한 번 호출한다.
au_check_server_authorization 도 같은 패턴이다.
au_check_procedure_authorization 만이 _db_class 가 아닌 객체
타입 중 유일하게 완전한 _db_auth / db_authorization 기계를
거친다 (procedure 가 GRANT EXECUTE 를 지원하기 때문이다).
사용자 CRUD — au_add_user, au_drop_user
섹션 제목: “사용자 CRUD — au_add_user, au_drop_user”사용자 CRUD 는 모든 진입점에서 DBA 멤버십으로 게이트된다.
// au_add_user — src/object/authenticate_access_user.cpp (condensed)MOPau_add_user (const char *name, int *exists){ MOP user = NULL; int save;
if (Au_dba_user != NULL && !au_is_dba_group_member (Au_user)) { er_set (ER_WARNING_SEVERITY, ARG_FILE_LINE, ER_AU_DBA_ONLY, 1, "add_user"); return NULL; } // ... AU_DISABLE (save); ... user = au_find_user (name); if (user != NULL) { *exists = 1; } // already exists else { user = au_make_user (name); // every new user is automatically a member of PUBLIC au_add_member_internal (Au_public_user, user, 1); au_set_user_timestamps (user); } return user;}PUBLIC 멤버십 단계는 구조적이다. PUBLIC 에게 발급된 모든
grant 는 모든 사용자에게 보여야 하는데, 권한 검사가 그 lookup 을
특수 케이스로 만들기보다 모든 사용자가 글자 그대로 “사용자 겸
그룹” 인 PUBLIC 의 멤버가 된다. 그 결과
authenticate_cache::update 안의 그룹 walk 가 이미 PUBLIC 보유
grant 를 추가 코드 경로 없이 포함하게 된다.
au_drop_user 는 더 복잡하다. 정리할 일이 많기 때문이다. 사용자
가 보유한 모든 권한을 회수 (au_user_revoke_all_privileges) 하고,
사용자를 참조하는 모든 _db_auth 행을 삭제
(au_delete_auth_of_dropping_user) 하며, 다른 모든 사용자의
direct_groups 안에서 그 사용자의 멤버십을 null 로 만들고, 마지
막으로 db_user 행을 삭제한다.
log_does_active_user_exist 도 호출해 사용자가 진행 중인 트랜잭션
을 갖고 있으면 drop 을 거절한다. 원본 분석 문서가 이를
ER_AU_NOT_ALLOW_TO_DROP_ACTIVE_USER 로 지목한다.
unloaddb 마이그레이션 — au_export_users, au_export_grants
섹션 제목: “unloaddb 마이그레이션 — au_export_users, au_export_grants”마이그레이션은 카탈로그를 직접 읽고 쓰면서 권한 검사를 거치지 않는
유일한 소비자다 (DBA 로서 Au_disable 가 켜진 상태로 돈다).
au_export_users 는 db_user 의 모든 행을 걸어
CALL add_user('U1') ON CLASS db_user; 와 매칭되는
set_password_encoded 호출을 (prefix 가 태깅된 해시와 함께, 그
래서 import 측이 어느 알고리즘이 쓰였는지 안다) 발행한다.
au_export_grants 는 한 번에 한 클래스씩 _db_auth 를 걸어 행
당 하나의 GRANT type ON class TO grantee [WITH GRANT OPTION];
을 발행한다. import 측은 이를 평범한 CSQL 로 파싱한다.
이 분할은 의도적이다. unloaddb 는 사용자와 grant 를 따로
export 해, 운영자가 마이그레이션 계획에 맞는 입자 (사용자 별,
테이블 별) 를 고를 수 있게 한다.
소스 코드 가이드
섹션 제목: “소스 코드 가이드”anchor 는 심볼명 이다. 라인은 흘러간다. 끝의 위치 표는 본 문서의
updated:날짜에 scope 되어 있다.
상수와 타입
섹션 제목: “상수와 타입”AU_ROOT_CLASS_NAME,AU_USER_CLASS_NAME,AU_PASSWORD_CLASS_NAME,AU_AUTH_CLASS_NAME(authenticate_constants.h) — 시스템 클래스 이름.AU_DBA_USER_NAME,AU_PUBLIC_USER_NAME—DBA와PUBLIC.AU_TYPE_MASK 0x7F,AU_GRANT_MASK 0x7F00,AU_FULL_AUTHORIZATION 0x7F7F,AU_GRANT_SHIFT 8,AU_CACHE_INVALID 0x80000000— 비트맵 레이아웃.AU_MAX_PASSWORD_CHARS 31,AU_MAX_PASSWORD_BUF 2048— 비밀번호 문자열 사이즈.ENCODE_PREFIX_DES 1,ENCODE_PREFIX_SHA1 2,ENCODE_PREFIX_SHA2_512 3— 알고리즘 tag 바이트.PASSWORD_ENCRYPTION_SEED U9a$y1@zw~a0%— (고정된) DES seed.DB_AUTHenum (dbtype_def.h) —AU_NONE,AU_SELECT,AU_INSERT,AU_UPDATE,AU_DELETE,AU_ALTER,AU_INDEX,AU_EXECUTE.AU_FETCHMODEenum (class_object.h) —AU_FETCH_READ,AU_FETCH_SCAN,AU_FETCH_EXCLUSIVE_SCAN,AU_FETCH_WRITE,AU_FETCH_UPDATE.
싱글턴 컨텍스트 (authenticate_context.{cpp,hpp})
섹션 제목: “싱글턴 컨텍스트 (authenticate_context.{cpp,hpp})”class authenticate_context— 싱글턴.au_ctx(authenticate.c) — 접근자. 첫 호출에서 구성한다.Au_root,Au_user,Au_dba_user,Au_public_user,Au_disable,Au_user_class,Au_password_class,Au_authorization_class(authenticate.h안의 매크로) —au_ctx()->...의 sugar.authenticate_context::init_ctx(au_init) — 상태 0초기화.authenticate_context::install(au_install) —createdb부트스트랩. 네 시스템 클래스를 만들고 attribute 를 추가하고 DBA 와 PUBLIC 을 만들고 PUBLIC 에게 browse 권한을 grant 한다.authenticate_context::start(au_start) — 연결별 부트스트랩. 시스템 클래스 MOP 들을 해소하고, DBA 와 PUBLIC 을 찾고,perform_login을 호출한다.authenticate_context::login(au_login의 본체) — DB 가 아직 열리지 않았을 때 비밀번호를 세 버퍼 모두에 미리 해싱한다. 열려 있을 때는perform_login으로 위임한다.authenticate_context::perform_login— 자격증명 검증과set_user.authenticate_context::set_user— 현재 사용자 전환.sc_set_current_schema로 schema version 을 bump 한다.authenticate_context::set_password(au_set_password_encrypt)au_set_password_internal을 감싼다.authenticate_context::push_user,pop_user— stored procedure 가 사용하는 실행 권한 스택.authenticate_context::is_loginable_user,authenticate_context::set_system_user,authenticate_context::disable_login—is_loginable/is_system_createdattribute 기계.au_login(authenticate.c) — 싱글턴으로 위임하는 얇은 래퍼.
비밀번호 (authenticate_password.{cpp,hpp})
섹션 제목: “비밀번호 (authenticate_password.{cpp,hpp})”encrypt_password— DES (레거시).encrypt_password_sha1— SHA-1 (레거시).encrypt_password_sha2_512— SHA-2/512 (현행).match_password— 알고리즘 인지 비교. 저장 측 prefix 로 미리 해싱된 버퍼를 고른다.au_set_password_internal— 사용자의db_password.password를 쓴다. DBA-bypass 가드 (!au_is_dba_group_member→ 다른 사람의 비밀번호 설정 거부) 가 들어있다.au_disable_passwords—ignore_passwords = 1설정.
캐시 (authenticate_cache.{cpp,hpp})
섹션 제목: “캐시 (authenticate_cache.{cpp,hpp})”class authenticate_cache— 컨텍스트별 캐시 보유자.AU_CLASS_CACHE— 클래스 당 한 비트맵 배열, 사용자cache_index로 인덱스.AU_USER_CACHE— 등록된 사용자 당 한 항목.cache_index를 들고 다닌다.authenticate_cache::init,flush— 라이프사이클.authenticate_cache::update— 카탈로그 상태를(user, class)비트맵으로 바꾸는 그 함수.authenticate_cache::get_cache_bits,get_procedure_cache_bits— 접근자.authenticate_cache::make_user_cache,find_user_cache_by_name,find_user_cache_by_mop,get_user_cache_index,remove_user_cache,reset_user_cache사용자 측 op.authenticate_cache::install_class_cache,make_class_cache,free_class_cache,extend_class_caches,free_authorization_cache— 클래스 측 op.authenticate_cache::reset_cache_for_user_and_class,reset_cache_for_user_and_procedure,reset_authorization_caches— grant/revoke 가 호출하는 무효 화 hook.
클래스 access (authenticate_access_class.cpp)
섹션 제목: “클래스 access (authenticate_access_class.cpp)”au_fetch_class— 주된 read 진입점.au_fetch_class_internal을 호출한다.au_fetch_class_by_classmop,au_fetch_class_by_instancemop,au_fetch_class_force— 변종.au_fetch_class_internal— locator fetch 와check_authorization을 결합한다.au_fetch_instance,au_fetch_instance_force— 인스턴스 변종. 클래스를 먼저 lock 하고 인스턴스를 fetch 한다.au_check_class_authorization(옛 이름au_check_authorization) — 권한만 검사 (fetch 없음).Au_disable을 우회한다.check_authorization(static) — 비트마스크 검사. 캐시를 조회하고 miss 시update를 호출한다.is_protected_class(static) — 시스템 카탈로그 위 DML 을 DBA 라도 거절한다.au_change_class_owner_including_partitions— partition 인지 외부 wrapper.au_change_class_owner를 감싼다.au_get_class_privilege— 비트맵의 public 접근자.fetch_class,fetch_instance(static) — locator 래퍼.
Grants (authenticate_grant.{cpp,hpp})
섹션 제목: “Grants (authenticate_grant.{cpp,hpp})”au_grant,au_revoke— public 진입점 둘.au_grant_class,au_grant_procedure— 객체 타입별 일꾼.au_revoke_class,au_revoke_procedure— 대칭.apply_grants—class_mop에 대한 모든 grant 를*bits에 OR 한다.get_grants—authMOP 에 대한db_authorization.grants를 읽는다.collect_class_grants(static) — 한 클래스를 모든 사용자 사이의 한 type grant 를 모은다.propagate_revoke(static) — recursive revoke 를_db_auth삭제 집합으로 바꾸는 그래프 walk.map_grant_list(static) —propagate_revoke의 DFS 마킹 패스.au_compare_grantor_and_return(static) — 유효 grantor 결정 (DBA / owner / grant-option-holder).find_grant_entry(static),add_grant_entry,drop_grant_entry—db_authorization.grants위의 sequence 원소 op.appropriate_error— 비트→ER_AU_*_FAILURE매핑.au_print_grants—au_dump용.check_grant_option(static) — 현재 사용자가 grant 되는 type 를WITH GRANT OPTION을 보유하는지 검증.au_force_write_new_auth(SA_MODE전용) —unloaddb가db_authorization으로부터_db_auth를 구체화할 때 사용.
Auth-table 접근자 (authenticate_access_auth.{cpp,hpp})
섹션 제목: “Auth-table 접근자 (authenticate_access_auth.{cpp,hpp})”class au_auth_accessor—_db_auth위 행 단위 CRUD 의 래퍼.au_auth_accessor::insert_auth,update_auth,delete_authau_grant/au_revoke가 사용하는 세 primitive.au_delete_auth_of_dropping_user,au_delete_authorizartion_of_dropping_user— drop-user 청소.au_delete_auth_of_dropping_database_object— drop-class 청소.au_object_revoke_all_privileges,au_user_revoke_all_privileges— 일괄 회수 항목.au_object_owner_change_privileges—au_change_class_owner가 권한을 atomic 하게 이전할 때 사용.
사용자 CRUD (authenticate_access_user.cpp)
섹션 제목: “사용자 CRUD (authenticate_access_user.cpp)”au_find_user— 이름으로 조회 (먼저 대문자화).au_find_user_to_drop—au_find_user와 같지만 활성 사용자 검사 (log_does_active_user_exist) 가 붙는다.au_make_user(static) —obj_create (db_user)에 attribute 설정 boilerplate.au_add_user— public 진입점. DBA 전용. PUBLIC 으로 자동 추가.au_drop_user— public 진입점.au_user_revoke_all_privileges,au_delete_auth_of_dropping_user, 그룹 정리로 cascade 한다.au_add_member,au_drop_member,au_add_member_internal,au_compute_groups,au_add_direct_groups— 그룹 멤버십 유지.au_set_user_comment,au_set_user_timestamps,au_update_user_timestamp— attribute 헬퍼.au_get_user_name—name읽기 (사용자가 현재 사용자일 때sc_current_schema_name으로 short-circuit).au_is_dba_group_member,au_is_user_group_member— 곳곳에 쓰이는 그룹 술어. DBA-bypass / owner-bypass 용.AU_OBJECT_CLASS_NAME[]—DB_OBJECT_TYPE을 카탈로그 클래스 이름으로 매핑하는 표 (오류 메시지가 사용).
Owner 변경 (authenticate_owner.cpp)
섹션 제목: “Owner 변경 (authenticate_owner.cpp)”au_check_owner— 세 갈래 owner / DBA / 그룹 검사.au_change_class_owner— 카탈로그 재작성 + 권한 이전 결합.au_change_serial_owner— serial 전용 owner 변경.au_change_trigger_owner— trigger 전용 owner 변경.au_change_sp_owner— stored procedure owner 변경.au_change_sp_owner_with_transfer_privileges— grant 도 옮기는 SP 변종.au_get_class_owner—_db_class.owner읽기.
마이그레이션 (authenticate_migration.cpp)
섹션 제목: “마이그레이션 (authenticate_migration.cpp)”au_export_users— 모든db_user행을CALL add_user(...)와set_password_encoded호출을 발행.au_export_grants— 주어진 클래스를 참조하는 모든_db_auth행을GRANT ... ON ... TO ...발행.- (헬퍼)
emit_grant,emit_user,emit_member— 내부 포매터.
디버깅
섹션 제목: “디버깅”au_dump,au_dump_to_file,au_dump_user,au_dump_auth— 카탈로그 상태 출력.db_user/db_authorization을 걷고, 메시지 카탈로그 (MSGCAT_SET_AUTHORIZATION) 로 포맷한다.
이 개정 시점의 위치 힌트 (2026-04-30)
섹션 제목: “이 개정 시점의 위치 힌트 (2026-04-30)”| 심볼 | 파일 | 라인 |
|---|---|---|
Au_root (그리고 친구들, 매크로) | authenticate.h | 57 |
AU_DISABLE/AU_ENABLE 매크로 | authenticate.h | 104 |
au_login (free function) | authenticate.h | 142 |
au_grant (signature) | authenticate.h | 148 |
au_revoke (signature) | authenticate.h | 149 |
au_check_class_authorization (sig.) | authenticate.h | 196 |
au_check_serial_authorization | authenticate.h | 197 |
au_check_server_authorization | authenticate.h | 198 |
au_check_procedure_authorization | authenticate.h | 199 |
AU_TYPE_MASK, AU_GRANT_MASK, AU_FULL_AUTHORIZATION | authenticate_constants.h | 82 |
AU_GRANT_SHIFT | authenticate_constants.h | 92 |
AU_CACHE_INVALID | authenticate_constants.h | 95 |
AU_MAX_PASSWORD_CHARS, AU_MAX_PASSWORD_BUF | authenticate_constants.h | 97 |
class authenticate_context | authenticate_context.hpp | 38 |
AU_USER_ATTR_IS_LOGINABLE, _IS_SYSTEM_CREATED | authenticate_context.hpp | 35 |
au_login (body) | authenticate.c | 89 |
au_ctx | authenticate.c | 95 |
au_check_serial_authorization | authenticate.c | 461 |
au_check_server_authorization | authenticate.c | 486 |
au_disable_passwords | authenticate.c | 515 |
authenticate_context::reset | authenticate_context.cpp | 42 |
authenticate_context::init_ctx / final_ctx | authenticate_context.cpp | 68/75 |
authenticate_context::start | authenticate_context.cpp | 95 |
authenticate_context::login | authenticate_context.cpp | 218 |
authenticate_context::install | authenticate_context.cpp | 283 |
authenticate_context::perform_login | authenticate_context.cpp | 555 |
authenticate_context::set_user | authenticate_context.cpp | 687 |
authenticate_context::set_password (2 ovr.) | authenticate_context.cpp | 747/759 |
authenticate_context::disable_passwords | authenticate_context.cpp | 769 |
authenticate_context::check_user | authenticate_context.cpp | 781 |
authenticate_context::get_current_user_name | authenticate_context.cpp | 832 |
authenticate_context::push_user / pop_user | authenticate_context.cpp | 917/932 |
authenticate_context::is_loginable_user | authenticate_context.cpp | 991 |
ENCODE_PREFIX_* 매크로 | authenticate_password.hpp | 34 |
IS_ENCODED_* 매크로 | authenticate_password.hpp | 38 |
encrypt_password | authenticate_password.cpp | 60 |
encrypt_password_sha1 | authenticate_password.cpp | 89 |
encrypt_password_sha2_512 | authenticate_password.cpp | 116 |
match_password | authenticate_password.cpp | 159 |
au_set_password_internal | authenticate_password.cpp | 248 |
class authenticate_cache | authenticate_cache.hpp | 86 |
AU_CLASS_CACHE | authenticate_cache.hpp | 50 |
AU_USER_CACHE | authenticate_cache.hpp | 71 |
authenticate_cache::init | authenticate_cache.cpp | 58 |
authenticate_cache::flush | authenticate_cache.cpp | 75 |
authenticate_cache::update | authenticate_cache.cpp | 118 |
authenticate_cache::free_authorization_cache | authenticate_cache.cpp | 317 |
authenticate_cache::make_class_cache | authenticate_cache.cpp | 353 |
class au_auth_accessor | authenticate_access_auth.hpp | 45 |
au_auth_accessor::insert_auth / etc. (sig.) | authenticate_access_auth.hpp | 71 |
au_change_class_owner_including_partitions | authenticate_access_class.cpp | 67 |
au_check_class_authorization | authenticate_access_class.cpp | 328 |
fetch_class | authenticate_access_class.cpp | 362 |
au_fetch_class_internal | authenticate_access_class.cpp | 529 |
au_fetch_class | authenticate_access_class.cpp | 614 |
au_fetch_class_by_instancemop | authenticate_access_class.cpp | 630 |
au_fetch_class_by_classmop | authenticate_access_class.cpp | 646 |
au_fetch_class_force | authenticate_access_class.cpp | 663 |
au_fetch_instance | authenticate_access_class.cpp | 699 |
au_fetch_instance_force | authenticate_access_class.cpp | 746 |
fetch_instance | authenticate_access_class.cpp | 761 |
check_authorization | authenticate_access_class.cpp | 876 |
is_protected_class | authenticate_access_class.cpp | 953 |
au_grant | authenticate_grant.cpp | 87 |
au_grant_class | authenticate_grant.cpp | 117 |
au_grant_procedure | authenticate_grant.cpp | 314 |
au_revoke | authenticate_grant.cpp | 487 |
au_revoke_class | authenticate_grant.cpp | 525 |
au_revoke_procedure | authenticate_grant.cpp | 726 |
appropriate_error | authenticate_grant.cpp | 983 |
add_grant_entry | authenticate_grant.cpp | 1164 |
drop_grant_entry | authenticate_grant.cpp | 1196 |
apply_grants | authenticate_grant.cpp | 1532 |
collect_class_grants | authenticate_grant.cpp | 1584 |
propagate_revoke | authenticate_grant.cpp | 1751 |
au_compare_grantor_and_return | authenticate_grant.cpp | 1915 |
au_print_grants | authenticate_grant.cpp | 2014 |
au_check_procedure_authorization | authenticate_grant.cpp | 2058 |
au_force_write_new_auth (SA_MODE) | authenticate_grant.cpp | 2110 |
au_find_user | authenticate_access_user.cpp | 62 |
au_find_user_to_drop | authenticate_access_user.cpp | 213 |
au_get_user_name | authenticate_access_user.cpp | 296 |
au_make_user | authenticate_access_user.cpp | 348 |
au_is_dba_group_member | authenticate_access_user.cpp | 470 |
au_is_user_group_member | authenticate_access_user.cpp | 506 |
au_add_user | authenticate_access_user.cpp | 577 |
au_set_user_comment | authenticate_access_user.cpp | 680 |
au_set_user_timestamps | authenticate_access_user.cpp | 722 |
au_add_member_internal | authenticate_access_user.cpp | 952 |
au_add_member | authenticate_access_user.cpp | 1058 |
au_drop_member | authenticate_access_user.cpp | 1086 |
au_drop_user | authenticate_access_user.cpp | 1193 |
au_check_owner | authenticate_owner.cpp | 38 |
au_change_serial_owner | authenticate_owner.cpp | 70 |
au_change_trigger_owner | authenticate_owner.cpp | 264 |
au_get_class_owner | authenticate_owner.cpp | 392 |
au_change_class_owner | authenticate_owner.cpp | 417 |
au_change_sp_owner | authenticate_owner.cpp | 534 |
au_change_sp_owner_with_transfer_privileges | authenticate_owner.cpp | 640 |
au_export_users | authenticate_migration.cpp | 110 |
au_export_grants | authenticate_migration.cpp | 581 |
au_dump_auth | authenticate.c | 249 |
au_dump_user | authenticate.c | 297 |
au_dump_to_file | authenticate.c | 375 |
au_dump | authenticate.c | 449 |
소스 검증 (2026-04-30 기준)
섹션 제목: “소스 검증 (2026-04-30 기준)”원본 분석 (raw/code-analysis/cubrid/common/[code_analysis] Authenticate.pdf) 은 CUBRID 11.3 기준이며 명시적으로 다음을
적어 둔다. “11.4 버전에서는 테이블, 뷰 외의 객체에 대해서도 권한
부여를 확장할 예정이다.” 현재 소스 (자기를 11.5 로 식별) 를 읽으
면 그 확장이 부분적으로 끝나 있고 부분적으로 여전히 보류
중이다.
au_grant는DB_OBJECT_PROCEDURE도 받는다.DB_OBJECT_CLASS뿐이 아니다.authenticate_grant.cpp안의au_grantswitch 로 검증. 본체에au_grant_class와au_grant_procedure두 arm 이 있다. 따라서 procedure 도GRANT EXECUTE를 받을 수 있다. procedure read path 의 권한 검사는au_check_procedure_authorization인데, 이 또한 클래스와 같은_db_auth/db_authorization기계를 거친다. 그래서 원본 분석 의 stored procedure 에 대한 “11.3 현재 호출자 권한으로 실행 한다” 는 진술은 시대가 지났다. 11.5 는 grantee 별로 grant-EXECUTE-then-execute-as-owner 를 지원한다.- trigger, index, serial, synonym, server 는 여전히 owner
전용 모델을 거친다.
au_check_serial_authorization(authenticate.c) 와au_check_server_authorization(같은 파일) 로 검증. 둘 다 권한 비트맵 조회 없이au_check_owner를 직접 호출한다. 원본 분석의 “소유자가 객체 변경/삭제가 가능하다” 는 그 객체 타입들을 11.5 에서도 여전히 일치한다. - 인덱스 권한 (
AU_INDEX = 0x20) 은 비트맵 안에 존재하고au_grant_class가 받아 들인다. 그래서GRANT INDEX ON tbl TO u1이 권한 행을 쓰지만, 이것은 테이블 단위 로 그 테이블에 인덱스를 만들 수 있는 권한이지 인덱스 별 grant 가 아니다. 원본 분석의 종속된 테이블에 의한 접근 권한 언급은 여전히 유효하다. 별도의_db_index권한 표는 없다.
두 번째 drift 점 — 원본 분석은 사용자 비밀번호에 대한 4-버퍼
체계 (Au_user_password, Au_user_password_des_oldstyle,
Au_user_password_sha1, Au_user_password_sha2_512) 를 기술
한다. authenticate_context.hpp 를 읽으면 네 버퍼는 여전히 정의
되어 있지만, Au_user_password 에는 이제 // unused 주석이
붙는다. 맨 평문 버퍼는 더 이상 채워지지 않는다 (au_login 은
세 인코딩된 버퍼에만 쓴다). 설계의 의도 (“don’t have buffers
lying around with the obvious passwords in it” —
authenticate_context::login 의 주석) 는 완전히 실현되었다.
세 번째 drift 점 — 원본 분석의 authenticate_owner 기술은
데이터베이스 객체에 대한 소유자 변경 을 단일 통합 루틴으로
이야기한다. 현재 소스는 이를 객체 타입 당 한 루틴으로 쪼갰다
(au_change_class_owner, au_change_serial_owner,
au_change_trigger_owner, au_change_sp_owner,
au_change_sp_owner_with_transfer_privileges). 기능적으로는 같다.
다만 원본 분석의 “au_change_owner DB_RESOURCE_TYPE” 같은 generic
dispatch 루틴은 더 이상 단일 함수로 존재하지 않는다.
미해결 질문
섹션 제목: “미해결 질문”- 저장 해시에 salt 없음.
crypt_sha_two가encrypt_password_sha2_512안에서NULLsalt 로 호출된다. 그래서 같은 비밀번호를 가진 두 사용자가db_password안에 바이트가 동일한 해시로 끝난다. 이는 행 단위 사전 공격 방어를 배제한다. 추적 경로 —db_password에 행 단위 salt attribute 를 추가하고 첫 로그인 시 기존 행을 마이그레이션할 것인가?crypt_sha_two의 첫 인자에 그 배관은 이미 있다. - DBA 가 비밀번호를 우회한다.
perform_login은 현재 세션 이 이미 DBA 일 때 비밀번호 검사를 건너뛴다. 이를 허용 가능 하다고 기록한 위협 모델이 있는가, 아니면 CSQL 이 DBA 전용이던 시기의 레거시인가?authenticate_context::perform_login의!au_is_dba_group_member (current_user) || ignore_dba_privilege가드 참조. db_user.idattribute.au_install이db_user위에idinteger attribute 를 선언하지만,au_make_user는 그것을 설정하지 않는다 (사용자의 정체성은 MOP / OID 다). 미래의 “안정 적인 사용자 id” 기능을 위한 예약인가, 아니면 제거 가능한가?- Recursive revoke 와 partitioned 클래스.
au_revoke_class는au_grant_class가 가진sm_partitioned_class_typefan out 을 갖지 않는다 (au_revoke_class본체로 검증). partitioned 클래스에 grant 된 권한을 revoke 하는 일은 모든 파티션에서 회수되는가, 아니면 부모만인가? 집중 테스트가 필요하다. - 클래스 별 캐시 메모리 증가.
cache_max가 새 사용자에게 slot 이 할당될 때마다cache_increment = 4만큼 확장되며, 모든 클래스 캐시가 새 크기로 자란다. 클래스 N개와 사용자 M명 를 이 메모리는 O(N·M) 이다. 단명하는 사용자가 많은 시스템 (CAS broker pool) 에서 이는 무한 증가할 수 있다.remove_user_cache가 슬롯을 회수하기는 하는데, 클래스 별 배열 을 줄이기도 하는가? 추적 경로 —extend_class_caches와remove_user_cache를 함께 읽기. AU_INDEX의 의미론.GRANT INDEX가db_authorization에 쓰지만, 그 비트가 read path 에서 조회되는가?is_protected_class가AU_INDEX를 시스템 카탈로그 위에서 금지한다고 나열한다. 그 비트가 또 어디에서 게이트하는가?is_protected_class와BOOT_ADMIN_CSQL_CLIENT_TYPE.check_authorization의 early-return 은 시스템 클래스에 대한 admin CSQL 클라이언트에 대해서만 발동한다. 의도가 — 비-admin CSQL 세션이db_user를 읽을 때 권한 캐시를 거치게 (그래서PUBLIC에게 grant 된 것만 보게) 하고, admin CSQL 세션은 raw 상태로 보게 하는 것인가? 비-DBA 사용자를 두 세션을 모두 돌려 검증.
원본 분석 (raw/code-analysis/cubrid/common/)
섹션 제목: “원본 분석 (raw/code-analysis/cubrid/common/)”[code_analysis] Authenticate.pdf— Hyung-Gyu Ryoo 의 deep dive 자료._converted/authenticate.pdf.md— pdftotext 로 추출한 자료. 자료 내용의 작업 초안으로 사용.
형제 문서
섹션 제목: “형제 문서”knowledge/code-analysis/cubrid/cubrid-mvcc.md— 인증은 MVCC 가시성 전 에 동작한다. 권한 검사는au_fetch_class시점에, MVCC 가시성은mvcc_satisfies_snapshot시점에 일어 나며 둘은 독립이다.knowledge/code-analysis/cubrid/cubrid-lock-manager.md—au_grant/au_revoke는db_authorization행을 변형하기 전에 write lock 을 잡는다. lock 매니저와의 상호작용은 본 문서 범위 밖이다.
교재 챕터
섹션 제목: “교재 챕터”- Database System Concepts, 7판 (Silberschatz, Korth,
Sudarshan) — 4장 §4.7 (Authorization):
GRANT/REVOKE의미론, with-grant-option, recursive revoke. 5장 §5.2 (Security and Authorization): DBA 의 역할, 권한 모델, audit trail. - Database Internals (Petrov), 11장 (“Replication and Consistency”) — 본 주제와는 비스듬하게 닿지만, Spanner / CockroachDB 의 role 기반 access 기술이 CUBRID 의 그룹-as- 사용자 모델에 좋은 대조점을 제공한다.
CUBRID 소스 (/data/hgryoo/references/cubrid/)
섹션 제목: “CUBRID 소스 (/data/hgryoo/references/cubrid/)”src/object/authenticate.h— public C/C++ 표면.src/object/authenticate.c—au_loginshim,au_dump*,au_check_serial_authorization,au_check_server_authorization.src/object/authenticate_constants.h— 상수와 비트맵 레이아웃.src/object/authenticate_context.{cpp,hpp}— 싱글턴 컨텍스트, install / start / login / set_user.src/object/authenticate_password.{cpp,hpp}— 인코딩과 매칭.src/object/authenticate_cache.{cpp,hpp}—(user, class)비트맵 캐시.src/object/authenticate_grant.{cpp,hpp}—au_grant,au_revoke,apply_grants,propagate_revoke.src/object/authenticate_access_auth.{cpp,hpp}—_db_auth행 단위 CRUD (au_auth_accessor).src/object/authenticate_access_class.cpp—au_fetch_class,check_authorization,is_protected_class.src/object/authenticate_access_user.cpp— 사용자 CRUD, 그룹 멤버십,au_is_dba_group_member.src/object/authenticate_owner.cpp— 객체 타입별 owner 변경.src/object/authenticate_migration.cpp—unloaddbexport.src/object/schema_system_catalog_constants.h—CT_ROOT_NAME,CT_USER_NAME,CT_PASSWORD_NAME,CT_AUTHORIZATION_NAME매크로.src/base/crypt_opfunc.{c,h}—crypt_sha_two,crypt_encrypt_printable,crypt_encrypt_sha1_printable.src/base/encryption.{c,h}— DES seed 처리 (crypt_seed).