콘텐츠로 이동

[KO] CUBRID 인증과 권한 — 사용자, 비밀번호, 권한 그리고 시스템 카탈로그

목차

다중 사용자 RDBMS는 자기 사용자에게 두 가지 보장을 빚지고 있고, 그 두 보장은 거의 언제나 같은 카탈로그 테이블을 공유하는 서로 다른 두 서브시스템에서 흘러 나온다. Database System Concepts (Silberschatz, Korth, Sudarshan) 4장 (“Intermediate SQL) 의 GRANT/REVOKE 절과 5장 (Advanced SQL”) 의 authorization 절이 이 둘을 가른다. 인증 (authentication) 은 호출자가 누구인가? 라는 질문에 답하고, 권한 (authorization) 은 그 호출자가 무엇을 해도 되는가? 라는 질문에 답한다. 첫 번째 문제는 저장된 비밀 (정석은 salt 가 들어간 해시) 과 자격증명을 대조해 검증하는 것으로 환원되고, 두 번째 문제는 (주체, 객체, 동작) 으로 keyed 된 권한 비트맵을 조회해 동작이 시작되기 전에 yes/no 결정을 내리는 것으로 환원된다.

이 모델이 열어 둔 세 가지 구현 선택이 모든 실제 엔진의 모양을 잡고, 본 문서의 골격을 만든다.

  1. 사용자 레코드는 어디에 사는가. 전용 비밀 테이블 (PostgreSQL 의 pg_authid, MySQL 의 mysql.user) 은 비밀 번호를 일반 카탈로그 바깥으로 빼서 별도의 lock 또는 비복제 정책 아래 보관할 수 있게 해 준다. 다른 길은 사용자를 다른 모든 것과 같은 카탈로그 안의 평범한 객체로 모델링하는 것이다 (CUBRID 의 db_user/db_password 가 그 길이다). 균일성 (모든 것이 객체이고 모든 것이 query 가능하다) 과, 비밀을 다른 lock 이나 복제 정책으로 둘 수 있는 옵션 사이의 절충이다.
  2. 권한의 입자 (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 에서의 권한 검사 비용 사이의 절충이다.
  3. 권한 검사는 언제 평가되는가. 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 고유 구조는 둘 중 하나를 구현하거나 그로부터 유래된 카탈로그 상태를 내구성 있게 만드는 일을 한다는 점이 분명해진다.

서로 다른 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 안에서 동기화된다.

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 하게 채워 넣는다. GRANTREVOKE 는 단일 (user, class) 슬롯을 reset_cache_for_user_and_class 로 무효화하거나, 더 광범위한 변경을 모든 캐시 비트를 AU_CACHE_INVALID = 0x80000000 으로 뒤집는다.

이론적 개념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)
권한 rootdb_root (단일 인스턴스, MOP 캐시 Au_root)
DBA superuserDBA 사용자 (Au_dba_user); au_is_dba_group_member 로 인지
Public rolePUBLIC 사용자 (Au_public_user); 모든 신규 사용자가 기본으로 멤버
(사용자, 객체) 별 권한 비트맵AU_TYPE_MASK = 0x7F, grant cache 의 하위 7비트 (AU_SELECT…AU_EXECUTE)
WITH GRANT OPTIONcache 의 상위 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 유틸리티가 사용)
비밀번호의 알고리즘 tagENCODE_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 에서 두 가지뿐)

인증 모듈에는 네 개의 이동 부품이 있다. 카탈로그 스키마 (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_authdb_authorization 양쪽을 갱신한 뒤 캐시를 무효화한다.

au_install 이 설치하는 카탈로그 스키마

섹션 제목: “au_install 이 설치하는 카탈로그 스키마”

스키마는 authenticate_context::install (옛 이름 au_install) 안에서 db_create_classsmt_add_attribute 의 직접 호출로 만들어진다. 의도가 자명하지 않은 자리에는 주석을 곁들였다.

// authenticate_context::install — src/object/authenticate_context.cpp
// db_root: single-instance class, holds the authorization root
smt_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 record
smt_add_attribute (def, "name", "string", ...); // unique-indexed
smt_add_attribute (def, "id", "integer", ...);
smt_add_attribute (def, "password", AU_PASSWORD_CLASS_NAME, ...); // → db_password
smt_add_attribute (def, "direct_groups", "set of (db_user)", ...);
smt_add_attribute (def, "groups", "set of (db_user)", ...); // flattened
smt_add_attribute (def, "authorization", AU_AUTH_CLASS_NAME, ...); // → db_authorization
smt_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 marker
smt_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 string
smt_add_attribute (def, "password", "string", ...); // [tag-byte][hash-hex]\0
// db_authorization: one row per grantee, denormalised
smt_add_attribute (def, AU_AUTH_ATTR_OWNER, AU_USER_CLASS_NAME, ...); // grantee
smt_add_attribute (def, AU_AUTH_ATTR_GRANTS, "sequence", ...); // [type, obj, grantor, cache]*

_db_authschema_system_catalog_install.cpp 에서 별도로 기술된다. (grantor, grantee, object, auth_type) 마다 한 행을 들고 다니며, unloaddbGRANT 문을 생성할 때 읽는 표가 바로 이것이다.

부트스트랩이 발급하는 두 가지 grant 는 짚어 둘 만하다. PUBLICdb_rootdb_userSELECT|EXECUTE 를 받는다 (그 래서 어떤 사용자든 사용자 목록을 introspect 할 수 있다). 그러나 db_authorization 에 대해서는 SELECT 만 받고, db_password 에는 어떤 grant 도 주어지지 않는다. 권한 없는 사용자는 다른 사용자의 비밀번호 해시를 select * from db_password 로조차 읽을 수 없다는 뜻이다. 행이 fetch 되기 전에 권한 검사가 ER_AU_SELECT_FAILURE 로 실패한다.

CUBRID 은 인증 상태를 호출 사슬을 따라 내려보내지 않는다. 그 상태는 au_ctx() (authenticate.c 안) 로 접근하는 싱글턴 안에 산다. 헤더는 글로벌 변수처럼 보이지만 실제로는 싱글턴의 필드로 펼쳐지는 매크로 집합을 정의한다. pre-class C 시절 코드와의 호환을 위해서다.

authenticate.h
#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 를 채우고, PUBLICDBAau_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.cppauthenticate_context::login 이다. 흥미로운 줄은 제공된 비밀번호의 세 겹 해싱이다.

// authenticate_context::login — src/object/authenticate_context.cpp
if (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.cppmatch_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.cpp
if (!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.cpp
void
encrypt_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_twosrc/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_internalencode == 1 일 때 encrypt_password_sha2_512 를 무조건적으로 호출한다).

매칭 함수는 저장 측의 prefix 바이트로 알고리즘을 인지한 뒤, 제공된 비밀번호를 매칭되는 알고리즘으로 미리 해싱해 비교를 단순 strcmp 로 만든다.

// match_password — src/object/authenticate_password.cpp (condensed)
bool
match_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 위의 권한 검사다. 질의 실행기는 자기에게 필요한 모든 클래스를 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.cpp
if (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.cpp
if (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_authorizationAU_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 grants
au_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_grantGRANT 문의 진입점이다. CUBRID 11.5 는 두 객체 타입을 지원한다.

// au_grant — src/object/authenticate_grant.cpp
int
au_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_authupdate_auth 를 통한 _db_auth 행 insert / update, (5) 비정규화된 db_authorization.grants sequence 갱신과 Au_cache.reset_cache_for_user_and_class 를 통한 (사용자, 클래스) 캐시 슬롯 reset.

_db_authdb_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.cpp
int
au_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)
MOP
au_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_usersdb_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_NAMEDBAPUBLIC.
  • 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_AUTH enum (dbtype_def.h) — AU_NONE, AU_SELECT, AU_INSERT, AU_UPDATE, AU_DELETE, AU_ALTER, AU_INDEX, AU_EXECUTE.
  • AU_FETCHMODE enum (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_loginis_loginable / is_system_created attribute 기계.
  • 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_passwordsignore_passwords = 1 설정.
  • 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 래퍼.
  • au_grant, au_revoke — public 진입점 둘.
  • au_grant_class, au_grant_procedure — 객체 타입별 일꾼.
  • au_revoke_class, au_revoke_procedure — 대칭.
  • apply_grantsclass_mop 에 대한 모든 grant 를 *bits 에 OR 한다.
  • get_grantsauth MOP 에 대한 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_entrydb_authorization.grants 위의 sequence 원소 op.
  • appropriate_error — 비트→ER_AU_*_FAILURE 매핑.
  • au_print_grantsau_dump 용.
  • check_grant_option (static) — 현재 사용자가 grant 되는 type 를 WITH GRANT OPTION 을 보유하는지 검증.
  • au_force_write_new_auth (SA_MODE 전용) — unloaddbdb_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_auth au_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_privilegesau_change_class_owner 가 권한을 atomic 하게 이전할 때 사용.

사용자 CRUD (authenticate_access_user.cpp)

섹션 제목: “사용자 CRUD (authenticate_access_user.cpp)”
  • au_find_user — 이름으로 조회 (먼저 대문자화).
  • au_find_user_to_dropau_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_namename 읽기 (사용자가 현재 사용자일 때 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 을 카탈로그 클래스 이름으로 매핑하는 표 (오류 메시지가 사용).
  • 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.h57
AU_DISABLE/AU_ENABLE 매크로authenticate.h104
au_login (free function)authenticate.h142
au_grant (signature)authenticate.h148
au_revoke (signature)authenticate.h149
au_check_class_authorization (sig.)authenticate.h196
au_check_serial_authorizationauthenticate.h197
au_check_server_authorizationauthenticate.h198
au_check_procedure_authorizationauthenticate.h199
AU_TYPE_MASK, AU_GRANT_MASK, AU_FULL_AUTHORIZATIONauthenticate_constants.h82
AU_GRANT_SHIFTauthenticate_constants.h92
AU_CACHE_INVALIDauthenticate_constants.h95
AU_MAX_PASSWORD_CHARS, AU_MAX_PASSWORD_BUFauthenticate_constants.h97
class authenticate_contextauthenticate_context.hpp38
AU_USER_ATTR_IS_LOGINABLE, _IS_SYSTEM_CREATEDauthenticate_context.hpp35
au_login (body)authenticate.c89
au_ctxauthenticate.c95
au_check_serial_authorizationauthenticate.c461
au_check_server_authorizationauthenticate.c486
au_disable_passwordsauthenticate.c515
authenticate_context::resetauthenticate_context.cpp42
authenticate_context::init_ctx / final_ctxauthenticate_context.cpp68/75
authenticate_context::startauthenticate_context.cpp95
authenticate_context::loginauthenticate_context.cpp218
authenticate_context::installauthenticate_context.cpp283
authenticate_context::perform_loginauthenticate_context.cpp555
authenticate_context::set_userauthenticate_context.cpp687
authenticate_context::set_password (2 ovr.)authenticate_context.cpp747/759
authenticate_context::disable_passwordsauthenticate_context.cpp769
authenticate_context::check_userauthenticate_context.cpp781
authenticate_context::get_current_user_nameauthenticate_context.cpp832
authenticate_context::push_user / pop_userauthenticate_context.cpp917/932
authenticate_context::is_loginable_userauthenticate_context.cpp991
ENCODE_PREFIX_* 매크로authenticate_password.hpp34
IS_ENCODED_* 매크로authenticate_password.hpp38
encrypt_passwordauthenticate_password.cpp60
encrypt_password_sha1authenticate_password.cpp89
encrypt_password_sha2_512authenticate_password.cpp116
match_passwordauthenticate_password.cpp159
au_set_password_internalauthenticate_password.cpp248
class authenticate_cacheauthenticate_cache.hpp86
AU_CLASS_CACHEauthenticate_cache.hpp50
AU_USER_CACHEauthenticate_cache.hpp71
authenticate_cache::initauthenticate_cache.cpp58
authenticate_cache::flushauthenticate_cache.cpp75
authenticate_cache::updateauthenticate_cache.cpp118
authenticate_cache::free_authorization_cacheauthenticate_cache.cpp317
authenticate_cache::make_class_cacheauthenticate_cache.cpp353
class au_auth_accessorauthenticate_access_auth.hpp45
au_auth_accessor::insert_auth / etc. (sig.)authenticate_access_auth.hpp71
au_change_class_owner_including_partitionsauthenticate_access_class.cpp67
au_check_class_authorizationauthenticate_access_class.cpp328
fetch_classauthenticate_access_class.cpp362
au_fetch_class_internalauthenticate_access_class.cpp529
au_fetch_classauthenticate_access_class.cpp614
au_fetch_class_by_instancemopauthenticate_access_class.cpp630
au_fetch_class_by_classmopauthenticate_access_class.cpp646
au_fetch_class_forceauthenticate_access_class.cpp663
au_fetch_instanceauthenticate_access_class.cpp699
au_fetch_instance_forceauthenticate_access_class.cpp746
fetch_instanceauthenticate_access_class.cpp761
check_authorizationauthenticate_access_class.cpp876
is_protected_classauthenticate_access_class.cpp953
au_grantauthenticate_grant.cpp87
au_grant_classauthenticate_grant.cpp117
au_grant_procedureauthenticate_grant.cpp314
au_revokeauthenticate_grant.cpp487
au_revoke_classauthenticate_grant.cpp525
au_revoke_procedureauthenticate_grant.cpp726
appropriate_errorauthenticate_grant.cpp983
add_grant_entryauthenticate_grant.cpp1164
drop_grant_entryauthenticate_grant.cpp1196
apply_grantsauthenticate_grant.cpp1532
collect_class_grantsauthenticate_grant.cpp1584
propagate_revokeauthenticate_grant.cpp1751
au_compare_grantor_and_returnauthenticate_grant.cpp1915
au_print_grantsauthenticate_grant.cpp2014
au_check_procedure_authorizationauthenticate_grant.cpp2058
au_force_write_new_auth (SA_MODE)authenticate_grant.cpp2110
au_find_userauthenticate_access_user.cpp62
au_find_user_to_dropauthenticate_access_user.cpp213
au_get_user_nameauthenticate_access_user.cpp296
au_make_userauthenticate_access_user.cpp348
au_is_dba_group_memberauthenticate_access_user.cpp470
au_is_user_group_memberauthenticate_access_user.cpp506
au_add_userauthenticate_access_user.cpp577
au_set_user_commentauthenticate_access_user.cpp680
au_set_user_timestampsauthenticate_access_user.cpp722
au_add_member_internalauthenticate_access_user.cpp952
au_add_memberauthenticate_access_user.cpp1058
au_drop_memberauthenticate_access_user.cpp1086
au_drop_userauthenticate_access_user.cpp1193
au_check_ownerauthenticate_owner.cpp38
au_change_serial_ownerauthenticate_owner.cpp70
au_change_trigger_ownerauthenticate_owner.cpp264
au_get_class_ownerauthenticate_owner.cpp392
au_change_class_ownerauthenticate_owner.cpp417
au_change_sp_ownerauthenticate_owner.cpp534
au_change_sp_owner_with_transfer_privilegesauthenticate_owner.cpp640
au_export_usersauthenticate_migration.cpp110
au_export_grantsauthenticate_migration.cpp581
au_dump_authauthenticate.c249
au_dump_userauthenticate.c297
au_dump_to_fileauthenticate.c375
au_dumpauthenticate.c449

원본 분석 (raw/code-analysis/cubrid/common/[code_analysis] Authenticate.pdf) 은 CUBRID 11.3 기준이며 명시적으로 다음을 적어 둔다. “11.4 버전에서는 테이블, 뷰 외의 객체에 대해서도 권한 부여를 확장할 예정이다.” 현재 소스 (자기를 11.5 로 식별) 를 읽으 면 그 확장이 부분적으로 끝나 있고 부분적으로 여전히 보류 중이다.

  • au_grantDB_OBJECT_PROCEDURE 도 받는다. DB_OBJECT_CLASS 뿐이 아니다. authenticate_grant.cpp 안의 au_grant switch 로 검증. 본체에 au_grant_classau_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 루틴은 더 이상 단일 함수로 존재하지 않는다.

  1. 저장 해시에 salt 없음. crypt_sha_twoencrypt_password_sha2_512 안에서 NULL salt 로 호출된다. 그래서 같은 비밀번호를 가진 두 사용자가 db_password 안에 바이트가 동일한 해시로 끝난다. 이는 행 단위 사전 공격 방어를 배제한다. 추적 경로 — db_password 에 행 단위 salt attribute 를 추가하고 첫 로그인 시 기존 행을 마이그레이션할 것인가? crypt_sha_two 의 첫 인자에 그 배관은 이미 있다.
  2. DBA 가 비밀번호를 우회한다. perform_login현재 세션 이 이미 DBA 일 때 비밀번호 검사를 건너뛴다. 이를 허용 가능 하다고 기록한 위협 모델이 있는가, 아니면 CSQL 이 DBA 전용이던 시기의 레거시인가? authenticate_context::perform_login!au_is_dba_group_member (current_user) || ignore_dba_privilege 가드 참조.
  3. db_user.id attribute. au_installdb_user 위에 id integer attribute 를 선언하지만, au_make_user 는 그것을 설정하지 않는다 (사용자의 정체성은 MOP / OID 다). 미래의 “안정 적인 사용자 id” 기능을 위한 예약인가, 아니면 제거 가능한가?
  4. Recursive revoke 와 partitioned 클래스. au_revoke_classau_grant_class 가 가진 sm_partitioned_class_type fan out 을 갖지 않는다 (au_revoke_class 본체로 검증). partitioned 클래스에 grant 된 권한을 revoke 하는 일은 모든 파티션에서 회수되는가, 아니면 부모만인가? 집중 테스트가 필요하다.
  5. 클래스 별 캐시 메모리 증가. cache_max 가 새 사용자에게 slot 이 할당될 때마다 cache_increment = 4 만큼 확장되며, 모든 클래스 캐시가 새 크기로 자란다. 클래스 N개와 사용자 M명 를 이 메모리는 O(N·M) 이다. 단명하는 사용자가 많은 시스템 (CAS broker pool) 에서 이는 무한 증가할 수 있다. remove_user_cache 가 슬롯을 회수하기는 하는데, 클래스 별 배열 을 줄이기도 하는가? 추적 경로 — extend_class_cachesremove_user_cache 를 함께 읽기.
  6. AU_INDEX 의 의미론. GRANT INDEXdb_authorization 에 쓰지만, 그 비트가 read path 에서 조회되는가? is_protected_classAU_INDEX 를 시스템 카탈로그 위에서 금지한다고 나열한다. 그 비트가 또 어디에서 게이트하는가?
  7. is_protected_classBOOT_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.mdau_grant/au_revokedb_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.cau_login shim, 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.cppau_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.cppunloaddb export.
  • src/object/schema_system_catalog_constants.hCT_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).