Skip to content

CUBRID Authentication and Authorization — Users, Passwords, Privileges, and System Catalogs

Contents:

Every multi-user RDBMS owes its users two related guarantees, and they almost always come from two distinct subsystems sharing the same catalog tables. Database System Concepts (Silberschatz, Korth, Sudarshan) splits them in Chapter 4 (“Intermediate SQL”) on GRANT/REVOKE and Chapter 5 (“Advanced SQL”)‘s authorization section: authentication answers who is the caller? and authorization answers what may that caller do? The first problem reduces to verifying credentials against a stored secret (canonically a salted hash); the second reduces to checking a privilege bitmap keyed on (principal, object, action) and returning a yes/no decision before the action proceeds.

Three implementation choices the model leaves open shape every real engine and frame the rest of this document:

  1. Where do user records live? A purpose-built secret table (PostgreSQL’s pg_authid, MySQL’s mysql.user) keeps passwords out of the regular catalog and lets the engine keep them locked in a non-replicated way; an alternative is to model users as ordinary objects in the same catalog as everything else (CUBRID’s db_user/db_password). The tradeoff is uniformity (everything is an object, everything is queryable) versus the option of storing secrets with a different lock or replication policy.
  2. What is the granularity of a privilege? SQL standard stops at table/view/column/procedure. Real engines either stay at table granularity (most), expose column-level grants (PostgreSQL, Oracle), or extend further to row-level security (PostgreSQL’s RLS, Oracle VPD). CUBRID 11.3 keeps grants at the class (table/view) granularity for _db_class only; other object types (trigger, serial, synonym, server, procedure) live under owner-only or owner-plus-DBA rules with no per-grantee bitmap. The tradeoff is policy expressiveness against the size of the privilege catalog and the cost of a privilege check on the read path.
  3. When is the privilege check evaluated? Eagerly at every tuple touch (slow but accurate under fast-changing grants) or lazily by computing an authorization bitmap once per (user, class) pair and caching it in the class metadata until a GRANT/REVOKE invalidates it. Almost every modern engine chooses the cache approach because the read path runs millions of times more often than the grant path. CUBRID’s AU_CLASS_CACHE makes the cache an array indexed by the user’s cache slot, so a context switch between users (a stored-procedure setuid, for example) is one indexed read.

The DBA superuser is the standard escape hatch: a single distinguished principal that bypasses authorization checks because it owns the means of bootstrap. Both the SQL standard (_SYSTEM) and the textbook treat it as out-of-band; in CUBRID the distinguished DBA user is created by au_install and is recognised at every check site by au_is_dba_group_member.

After these are named, every CUBRID-specific structure in this document either implements one of them or makes the resulting catalog state durable.

Despite very different SQL dialects, the leading RDBMS engines converge on the same five-piece design for user management. The pieces and CUBRID’s mapping appear below; the rest of the doc walks the CUBRID side in detail.

PostgreSQL stores users (and roles, since 8.1 they are unified) in pg_authid, with the password hash in the same row. MySQL keeps mysql.user with one row per (user, host) pair and a hashed authentication_string. Oracle’s SYS.USER$ underpins the DBA_USERS view and again carries the hash in the same row. CUBRID is unusual in splitting the user record (db_user) from the password record (db_password) into two classes joined by a password reference attribute, so a row in db_user literally holds an object pointer to a db_password instance.

Modern engines store passwords as one of (random-salt, hash, algorithm-id) triples. PostgreSQL stamps SCRAM-SHA-256 or MD5 prefixes onto the stored string; MySQL prefixes *<HEX-of-SHA1(SHA1(pw))> for mysql_native_password. CUBRID tags the encoded password with a single-byte algorithm prefix at offset 0: \x01 for the legacy DES, \x02 for SHA-1, \x03 for SHA-2/512. The hash is unsalted in 11.5 — crypt_sha_two takes only the password and returns the hex digest; matches are done by string comparison after both sides are canonicalised to the same algorithm tag (see match_password).

PostgreSQL keeps aclitem[] arrays in the per-object catalog row (pg_class.relacl, etc.), MySQL has mysql.db, mysql.tables_priv, mysql.columns_priv, Oracle has SYS.OBJAUTH$. The encoding varies but the shape is the same: a many-to-many (grantor, grantee, object, privilege_bits, with_grant_option) relation. CUBRID has two such tables — _db_auth (the fully-normalised relation, used by unloaddb and information schema queries) and db_authorization (a denormalised per-grantee row with all that grantee’s grants packed into a single sequence attribute, used by the runtime privilege check). The two are kept in sync inside au_grant/au_revoke.

The SQL standard requires recursive revoke: revoking a grant withdraws every grant the grantee subsequently issued under the authority of that first grant. Both PostgreSQL and Oracle implement this; MySQL does not (revokes are non-cascading until 8.0’s role overhaul). CUBRID does — propagate_revoke walks the grant graph from the owner and marks every grant unreachable without the just-revoked edge as legal == 0, then deletes those.

Looking up the privilege catalog on every row read is prohibitive. PostgreSQL maintains a per-relation, per-role ACL cache; Oracle has the dictionary cache for object privileges. CUBRID materialises this as AU_CLASS_CACHE: one bitmap (unsigned int *data) per class, indexed by the user’s cache_index, populated lazily by authenticate_cache::update on the first au_fetch_class for a given user and class. A GRANT or REVOKE either invalidates a single (user, class) slot via reset_cache_for_user_and_class or flips every cache bit to AU_CACHE_INVALID = 0x80000000 for a broader change.

Theoretical conceptCUBRID name
User catalog tabledb_user (system class, MOP cached as Au_user_class)
Password catalog tabledb_password (separate class, joined from db_user.password)
Privilege catalog (normalised)_db_auth (one row per (grantor, grantee, object, auth_type))
Privilege catalog (denormalised)db_authorization (one row per grantee, grants is a sequence of (type,obj,grantor,cache) tuples)
Authorization rootdb_root (single instance, MOP cached as Au_root)
DBA superuserDBA user (Au_dba_user); recognised by au_is_dba_group_member
Public rolePUBLIC user (Au_public_user); every new user is a member by default
Per-(user, object) privilege bitmapAU_TYPE_MASK = 0x7F, lower 7 bits of grant cache (AU_SELECT…AU_EXECUTE)
WITH GRANT OPTIONUpper 7 bits of cache, shifted by AU_GRANT_SHIFT = 8 (AU_GRANT_MASK = 0x7F00)
Runtime cacheAU_CLASS_CACHE per class, cache_index-keyed; AU_CACHE_INVALID = 0x80000000
Login disableAU_DISABLE/AU_ENABLE macros set disable_auth_check
Password ignoreignore_passwords (set by au_disable_passwords, used by SA-mode utilities)
Algorithm tag in passwordENCODE_PREFIX_DES (1), ENCODE_PREFIX_SHA1 (2), ENCODE_PREFIX_SHA2_512 (3)
Privilege typesAU_SELECT 0x01, AU_INSERT 0x02, AU_UPDATE 0x04, AU_DELETE 0x08, AU_ALTER 0x10, AU_INDEX 0x20, AU_EXECUTE 0x40
Object types under au_grantDB_OBJECT_CLASS, DB_OBJECT_PROCEDURE (only two, in 11.5)

The authentication module has four moving parts: the catalog schema (db_root, db_user, db_password, _db_auth, db_authorization), the in-memory context object (authenticate_context) that pins the system MOPs and the current-user MOP, the privilege cache (authenticate_cache) that turns a grant lookup into a bitmask test, and the entry points that wire the rest of the engine to all of the above (au_login, au_fetch_class, au_grant, au_revoke, au_change_*_owner). We walk them in that order.

flowchart LR
  subgraph CONSUMERS["Engine entry points"]
    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["System catalog (regular CUBRID classes)"]
    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

The figure encodes three boundaries. (client / catalog) the authentication module is client-side: every translation unit listed above is compiled into cubridcs (CS-mode client) and cubridsa (standalone client+server in-process), but never into cub_server. The header opens with #if defined (SERVER_MODE) #error Does not belong to server module #endif. (install / start) au_install runs once per database lifetime (inside createdb); au_start runs once per client connection (inside boot_restart_client). (read / write) au_fetch_class is on the read path and goes through the cache; au_grant/au_revoke/au_change_*_owner are on the write path and update both _db_auth and db_authorization and then invalidate the cache.

The schema is created by direct calls to db_create_class and smt_add_attribute inside authenticate_context::install (legacy name au_install). The shape, with comments where the intent is non-obvious:

// 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_auth is described separately in schema_system_catalog_install.cpp; it carries one row per (grantor, grantee, object, auth_type) and is the table unloaddb reads when generating GRANT statements.

The two distinct grants the bootstrap issues are worth calling out: PUBLIC gets SELECT|EXECUTE on db_root and on db_user (so any user can introspect users) but only SELECT on db_authorization and no grant at all on db_password. That means an unprivileged user cannot read another user’s password hash even via select * from db_password — the privilege check fails with ER_AU_SELECT_FAILURE before the row is fetched.

CUBRID does not pass the authentication state down a call chain; it lives in a singleton accessed through au_ctx() (in authenticate.c). The header defines a set of macros that look like global variables but expand to fields of the singleton, for backward compatibility with the pre-class C code:

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

The singleton is not thread-safe. The authentication module is client-side and the CUBRID client (cubridcs) is single-threaded per connection, so this is fine; the comment at the top of the deck explicitly notes “authenticate 모듈이 속해있는 cubridcs는 single-thread 구조이므로 호출한 API는 대부분 thread-safety 가 아니다.”

The lifecycle is:

au_init ──▶ au_install ──▶ au_start ──▶ tasks* ──▶ au_final
(createdb) (per-conn) (shutdown)
au_login
(csql)

au_init is in fact au_ctx (the macro in authenticate.h): the first call constructs the singleton and init_ctx zeroes every MOP. au_install is called only by createdb. au_start is called from boot_restart_client after the database is open; it calls sm_find_class four times to populate root_class, authorization_class, user_class, and password_class, then calls au_find_user for PUBLIC and DBA, and finally calls perform_login with the credentials previously cached by au_login.

The login is split across two phases because the user typically provides credentials before the database is open (think csql -u dba mydb, where the credential parsing happens in argv but the database boot happens later):

sequenceDiagram
  participant U as User<br/>(csql / CAS / utility)
  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/>save name + 3 hashes
  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

The first phase is authenticate_context::login in authenticate_context.cpp. The interesting line is the triple hashing of the supplied password:

// 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 stores three distinct encodings of the same plaintext because the database may be a legacy DB upgraded across major versions and the on-disk row may be in any of the three formats. The match function (match_password in authenticate_password.cpp) inspects the prefix byte of the stored hash and picks the matching pre-hashed buffer to compare.

The second phase is perform_login. It validates the user MOP, checks the is_loginable attribute (a per-user disable flag), fetches the password attribute, dereferences it to a db_password row, fetches that row’s password attribute, and runs 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
}

Two non-obvious facts. First, the !au_is_dba_group_member (current_user) || ignore_dba_privilege guard means that a session already logged in as DBA can switch to any other user without supplying a password unless the caller passes ignore_dba_privilege == true. This is the mechanism that lets csql -u dba then ;set to another user without the user’s password — handy for utilities, dangerous if the DBA account itself is compromised. Second, on success the function calls sm_bump_local_schema_version to invalidate cached vclasses; otherwise a vclass compiled while the previous user was active could leak rows the new user is not authorized to see.

Password encoding — encrypt_password_* and match_password

Section titled “Password encoding — encrypt_password_* and match_password”

Three encoders sit behind encrypt_password*, each writing into a 2 KB caller-provided buffer:

// 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_two is the SHA-2/512 routine in src/base/crypt_opfunc.c. It is unsalted in 11.5 — the NULL first argument is the salt position, and CUBRID always passes NULL. The output is a hex string of length 128, prefixed with \x03, total length 129 + NUL.

The DES path (encrypt_password) uses a fixed seed PASSWORD_ENCRYPTION_SEED = "U9a$y1@zw~a0%" baked into authenticate_password.hpp. The SHA-1 path (encrypt_password_sha1) is also unsalted. Both legacy paths exist solely so that databases upgraded from older CUBRID installs can still authenticate; new password sets always go through the SHA-2/512 path (set_password_internal calls encrypt_password_sha2_512 unconditionally when encode == 1).

The match function recognises the algorithm by the prefix byte on the stored side, then pre-hashes the supplied password with the matching algorithm so the comparison is a simple 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;
}

When au_login was called with a plaintext password it pre-hashed into all three buffers (Au_user_password_des_oldstyle, Au_user_password_sha1, Au_user_password_sha2_512) so match_password does not re-hash on the user side; it just selects the right pre-hashed buffer.

The most-trafficked path through the module is the privilege check on a SELECT. The query executor calls au_fetch_class for every class it needs; that routine consults the per-class cache and falls back to authenticate_cache::update to compute the bitmap if it is not yet populated for the current user.

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/>· not catalog<br/>· not 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"]

The two au_fetch_class body excerpts that drive this are in authenticate_access_class.cpp. The outer 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;

The bitmask test is inside 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);
}
}

Two points worth holding on to. First, Au_disable does not disable everything: the is_protected_class arm makes sure even DBA cannot directly ALTER/INSERT/UPDATE/DELETE/INDEX on a system catalog class. The intent is that catalog mutations go through dedicated APIs (au_grant, au_change_class_owner), not through bare DML. Second, the cache is per-class but also per-user: each class has an unsigned int *data whose length is cache_max (the size allocated by extend_class_caches), and the offset into that array is the user’s cache_index (allocated by make_user_cache).

authenticate_cache::update — where grants become bits

Section titled “authenticate_cache::update — where grants become bits”

When check_authorization finds AU_CACHE_INVALID, it calls Au_cache.update, which is the function that turns the catalog state into a bitmask. The logic, slightly condensed:

// 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 (in authenticate_grant.cpp) walks the db_authorization.grants sequence, finds entries whose GRANT_ENTRY_CLASS element matches mop, and OR-s the GRANT_ENTRY_CACHE element into *bits. The total cost is one walk of the grantee’s grants sequence per group plus one walk for the user himself; for a typical user with O(1) groups this is O(k) where k is the number of grants the user holds.

The function is called under Au_disable == true — the authentication context is temporarily disabled during the cache walk, because reading db_authorization would otherwise itself trigger an au_fetch_class and recurse infinitely.

au_grant is the entry point for a GRANT statement. CUBRID 11.5 supports two object types:

// 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 is the workhorse. It does five things in sequence: (1) recursive partition-aware grant via sm_partitioned_class_type so a GRANT SELECT ON tbl_partitioned fans out to every partition; (2) determine the effective grantor via au_compare_grantor_and_return (the current user if it is DBA or a member of the owner’s group, otherwise the user that itself has the requested right with WITH GRANT OPTION); (3) fetch the grantee’s db_authorization row with a write lock; (4) insert / update the row in _db_auth via au_auth_accessor::insert_auth and update_auth; (5) update the denormalised db_authorization.grants sequence and reset the (user, class) cache slot via Au_cache.reset_cache_for_user_and_class.

The dual write to both _db_auth and db_authorization is the hot spot for grant correctness — the two tables must stay consistent. The order is normalised first, denormalised second, so a transaction abort between them rolls both back together.

au_revoke is symmetric in entry shape but adds the recursive revoke logic. After dropping the requested grant, it calls collect_class_grants to enumerate every grant of type that exists for class_mop, then propagate_revoke walks the resulting graph from class_owner, marking every grant reachable without the just-revoked edge as legal == 1 and deleting every grant where legal == 0. The implementation is honest about the cost (“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” — comment at collect_class_grants).

Owner change — au_change_class_owner & friends

Section titled “Owner change — au_change_class_owner & friends”

CUBRID’s per-object authorization is split across several “owner change” routines, one per object type. The asymmetry is intentional: only db_class (au_change_class_owner) and procedures (au_change_sp_owner) can also transfer the privileges held against the object; the rest just rewrite the owner attribute and let the privilege catalog catch up later.

// 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;
}

This three-way check — am I the owner?, am I in the DBA group?, am I in a group whose membership includes the owner? — is the only authorization gate for serial, server, trigger, synonym, and stored procedure access in 11.5. au_check_serial_authorization is one direct call to au_check_owner after fetching the owner attribute; au_check_server_authorization is the same pattern. au_check_procedure_authorization is the only object type other than _db_class that goes through the full _db_auth / db_authorization machinery (because procedures support GRANT EXECUTE).

User CRUD is gated on DBA membership at every entry point:

// 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;
}

The PUBLIC-membership step is structural: every grant made to PUBLIC must be visible to every user, and rather than having the privilege check special-case the lookup, every user is literally a member of the PUBLIC user-as-group. This means the group-walk inside authenticate_cache::update already includes PUBLIC-held grants without any additional code path.

au_drop_user is more involved because it has to clean up: revoke every privilege the user holds (au_user_revoke_all_privileges), delete every _db_auth row referencing the user (au_delete_auth_of_dropping_user), null out the user’s membership in every other user’s direct_groups, and then delete the db_user row. It also calls log_does_active_user_exist to refuse the drop if the user has an in-progress transaction; the deck flags this as ER_AU_NOT_ALLOW_TO_DROP_ACTIVE_USER.

unloaddb migration — au_export_users, au_export_grants

Section titled “unloaddb migration — au_export_users, au_export_grants”

Migration is the only consumer that talks to the catalog without going through the privilege check (it runs as DBA with Au_disable set). au_export_users walks every row of db_user and emits CALL add_user('U1') ON CLASS db_user; and the matching set_password_encoded call (with the prefix-tagged hash, so the import side knows which algorithm was used). au_export_grants walks _db_auth for one class at a time and emits one GRANT type ON class TO grantee [WITH GRANT OPTION]; per row. The import side parses these as ordinary CSQL.

The split is deliberate: unloaddb exports users and grants separately so the operator can choose a granularity (per-user, per-table) that matches their migration plan.

Anchor on symbol names, not line numbers. The CUBRID source moves; the position table at the end is scoped to the doc’s updated: date.

  • AU_ROOT_CLASS_NAME, AU_USER_CLASS_NAME, AU_PASSWORD_CLASS_NAME, AU_AUTH_CLASS_NAME (authenticate_constants.h) — system-class names.
  • AU_DBA_USER_NAME, AU_PUBLIC_USER_NAME"DBA" and "PUBLIC".
  • AU_TYPE_MASK 0x7F, AU_GRANT_MASK 0x7F00, AU_FULL_AUTHORIZATION 0x7F7F, AU_GRANT_SHIFT 8, AU_CACHE_INVALID 0x80000000 — bitmap layout.
  • AU_MAX_PASSWORD_CHARS 31, AU_MAX_PASSWORD_BUF 2048 — password-string sizing.
  • ENCODE_PREFIX_DES 1, ENCODE_PREFIX_SHA1 2, ENCODE_PREFIX_SHA2_512 3 — algorithm tag bytes.
  • PASSWORD_ENCRYPTION_SEED "U9a$y1@zw~a0%" — the (fixed) DES seed.
  • DB_AUTH enum (in dbtype_def.h) — AU_NONE, AU_SELECT, AU_INSERT, AU_UPDATE, AU_DELETE, AU_ALTER, AU_INDEX, AU_EXECUTE.
  • AU_FETCHMODE enum (in class_object.h) — AU_FETCH_READ, AU_FETCH_SCAN, AU_FETCH_EXCLUSIVE_SCAN, AU_FETCH_WRITE, AU_FETCH_UPDATE.

Singleton context (authenticate_context.{cpp,hpp})

Section titled “Singleton context (authenticate_context.{cpp,hpp})”
  • class authenticate_context — the singleton.
  • au_ctx (in authenticate.c) — accessor; constructs on first call.
  • Au_root, Au_user, Au_dba_user, Au_public_user, Au_disable, Au_user_class, Au_password_class, Au_authorization_class (macros in authenticate.h) — sugar for au_ctx()->....
  • authenticate_context::init_ctx (au_init) — zero state.
  • authenticate_context::install (au_install) — createdb bootstrap; creates all four system classes, adds attributes, creates DBA + PUBLIC, grants browse privileges to PUBLIC.
  • authenticate_context::start (au_start) — per-connection bootstrap; resolves system-class MOPs, finds DBA + PUBLIC, calls perform_login.
  • authenticate_context::login (au_login’s body) — pre-hashes password into all three buffers when the DB is not yet open; delegates to perform_login when it is.
  • authenticate_context::perform_login — credential validation and set_user.
  • authenticate_context::set_user — switch current user; bumps schema version via sc_set_current_schema.
  • authenticate_context::set_password (au_set_password_encrypt) — wraps au_set_password_internal.
  • authenticate_context::push_user, pop_user — execution rights stack used by stored procedures.
  • authenticate_context::is_loginable_user, authenticate_context::set_system_user, authenticate_context::disable_login — the is_loginable / is_system_created attribute machinery.
  • au_login (authenticate.c) — thin wrapper that defers to the singleton.

Password (authenticate_password.{cpp,hpp})

Section titled “Password (authenticate_password.{cpp,hpp})”
  • encrypt_password — DES (legacy).
  • encrypt_password_sha1 — SHA-1 (legacy).
  • encrypt_password_sha2_512 — SHA-2/512 (current).
  • match_password — algorithm-aware compare; selects pre-hashed buffer based on stored-side prefix.
  • au_set_password_internal — write db_password.password for a user, with DBA-bypass guard (!au_is_dba_group_member → refuse to set someone else’s password).
  • au_disable_passwords — set ignore_passwords = 1.
  • class authenticate_cache — the per-context cache holder.
  • AU_CLASS_CACHE — one bitmap array per class, indexed by user cache_index.
  • AU_USER_CACHE — one entry per registered user; carries the cache_index.
  • authenticate_cache::init, flush — lifecycle.
  • authenticate_cache::updatethe function that turns catalog state into a (user, class) bitmap.
  • authenticate_cache::get_cache_bits, get_procedure_cache_bits — accessors.
  • 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 — user-side ops.
  • authenticate_cache::install_class_cache, make_class_cache, free_class_cache, extend_class_caches, free_authorization_cache — class-side ops.
  • authenticate_cache::reset_cache_for_user_and_class, reset_cache_for_user_and_procedure, reset_authorization_caches — invalidation hooks called by grant/revoke.

Class access (authenticate_access_class.cpp)

Section titled “Class access (authenticate_access_class.cpp)”
  • au_fetch_class — primary read entry; calls au_fetch_class_internal.
  • au_fetch_class_by_classmop, au_fetch_class_by_instancemop, au_fetch_class_force — variants.
  • au_fetch_class_internal — combines locator fetch with check_authorization.
  • au_fetch_instance, au_fetch_instance_force — instance variant; locks the class first, then fetches the instance.
  • au_check_class_authorization (legacy name au_check_authorization) — privilege-only check (no fetch), bypasses Au_disable.
  • check_authorization (static) — the bitmask test; consults the cache, calls update on miss.
  • is_protected_class (static) — refuses DML on system catalogs even for DBA.
  • au_change_class_owner_including_partitions — partition-aware outer wrapper around au_change_class_owner.
  • au_get_class_privilege — public accessor for the bitmap.
  • fetch_class, fetch_instance (static) — locator wrappers.
  • au_grant, au_revoke — the two public entries.
  • au_grant_class, au_grant_procedure — per-object-type workers.
  • au_revoke_class, au_revoke_procedure — symmetric.
  • apply_grants — OR every grant for class_mop into *bits.
  • get_grants — read db_authorization.grants for an auth MOP.
  • collect_class_grants (static) — gather every grant of a type for a class across every user.
  • propagate_revoke (static) — graph walk that turns recursive revoke into a set of _db_auth deletes.
  • map_grant_list (static) — DFS marking pass for propagate_revoke.
  • au_compare_grantor_and_return (static) — picks the effective grantor (DBA / owner / grant-option-holding user).
  • find_grant_entry (static), add_grant_entry, drop_grant_entry — sequence-element ops on db_authorization.grants.
  • appropriate_error — bit→ER_AU_*_FAILURE mapping.
  • au_print_grants — for au_dump.
  • check_grant_option (static) — verify the current user holds WITH GRANT OPTION for the type being granted.
  • au_force_write_new_auth (SA_MODE only) — used by unloaddb to materialise _db_auth from db_authorization.

Auth-table accessor (authenticate_access_auth.{cpp,hpp})

Section titled “Auth-table accessor (authenticate_access_auth.{cpp,hpp})”
  • class au_auth_accessor — wraps row-level CRUD on _db_auth.
  • au_auth_accessor::insert_auth, update_auth, delete_auth — the three primitives au_grant/au_revoke use.
  • au_delete_auth_of_dropping_user, au_delete_authorizartion_of_dropping_user — drop-user cleanup.
  • au_delete_auth_of_dropping_database_object — drop-class cleanup.
  • au_object_revoke_all_privileges, au_user_revoke_all_privileges — bulk revoke entries.
  • au_object_owner_change_privileges — used by au_change_class_owner to transfer privileges atomically.
  • au_find_user — lookup by name (uppercases first).
  • au_find_user_to_drop — like au_find_user but with the active-user check (log_does_active_user_exist).
  • au_make_user (static) — obj_create (db_user) plus attribute-set boilerplate.
  • au_add_user — public entry; DBA-only; auto-adds user to PUBLIC.
  • au_drop_user — public entry; cascades through au_user_revoke_all_privileges, au_delete_auth_of_dropping_user, group fixup.
  • au_add_member, au_drop_member, au_add_member_internal, au_compute_groups, au_add_direct_groups — group membership maintenance.
  • au_set_user_comment, au_set_user_timestamps, au_update_user_timestamp — attribute helpers.
  • au_get_user_name — read name (also short-circuits via sc_current_schema_name when the user is the current user).
  • au_is_dba_group_member, au_is_user_group_member — the group predicates used everywhere as DBA-bypass / owner-bypass.
  • AU_OBJECT_CLASS_NAME[] — table mapping DB_OBJECT_TYPE to catalog class name (used by error messages).
  • au_check_owner — the three-way owner / DBA / group test.
  • au_change_class_owner — combined catalog rewrite + privilege transfer.
  • au_change_serial_owner — serial-only owner change.
  • au_change_trigger_owner — trigger-only owner change.
  • au_change_sp_owner — stored-procedure owner change.
  • au_change_sp_owner_with_transfer_privileges — SP variant that also moves grants.
  • au_get_class_owner — read _db_class.owner.
  • au_export_users — emit CALL add_user(...) and set_password_encoded calls for every db_user row.
  • au_export_grants — emit GRANT ... ON ... TO ... for every _db_auth row referencing the given class.
  • (helpers) emit_grant, emit_user, emit_member — internal formatters.
  • au_dump, au_dump_to_file, au_dump_user, au_dump_auth — print the catalog state, walk db_user/db_authorization, format with the message catalog (MSGCAT_SET_AUTHORIZATION).
SymbolFileLine
Au_root (and friends, macros)authenticate.h57
AU_DISABLE/AU_ENABLE macrosauthenticate.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_* macrosauthenticate_password.hpp34
IS_ENCODED_* macrosauthenticate_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

The raw analysis (raw/code-analysis/cubrid/common/[code_analysis] Authenticate.pdf) is dated against CUBRID 11.3 and explicitly notes “11.4 버전에서는 테이블, 뷰 외의 객체에 대해서도 권한 부여를 확장할 예정이다.” Reading the current source (which identifies as 11.5) shows the expansion is partly done and partly still pending:

  • au_grant accepts DB_OBJECT_PROCEDURE, not just DB_OBJECT_CLASS. Verified at the au_grant switch (in authenticate_grant.cpp): the body has two arms, au_grant_class and au_grant_procedure. Procedures can therefore receive GRANT EXECUTE. The privilege check on the procedure read path is au_check_procedure_authorization, which does go through the same _db_auth / db_authorization machinery as classes. So the deck’s “11.3 현재 호출자 권한으로 실행한다” statement for stored procedures is out of date: 11.5 supports grant-EXECUTE-then-execute-as-owner on a per-grantee basis.
  • Triggers, indexes, serials, synonyms, servers still go through the owner-only model. Verified at au_check_serial_authorization (in authenticate.c) and au_check_server_authorization (same file): both call au_check_owner directly, with no privilege bitmap consulted. The deck’s claim that “소유자가 객체 변경/삭제가 가능하다” for those object types still matches 11.5.
  • Index privileges (AU_INDEX = 0x20) exist in the bitmap and au_grant_class accepts them, so a GRANT INDEX ON tbl TO u1 writes a privilege row, but this is treated as a table-level permission to create indexes on the table, not a per-index grant. The deck’s “종속된 테이블에 의한 접근 권한” remark still holds: there is no separate _db_index privilege table.

A second drift point: the deck describes a four-buffer scheme for the user password (Au_user_password, Au_user_password_des_oldstyle, Au_user_password_sha1, Au_user_password_sha2_512). Reading authenticate_context.hpp confirms the four buffers are still defined, but the comment // unused is now attached to Au_user_password — the bare plaintext buffer is no longer populated (au_login writes only to the three encoded buffers). The intent of the design (“don’t have buffers lying around with the obvious passwords in it” — comment in authenticate_context::login) has been fully realised.

A third drift point: the deck’s authenticate_owner description talks about “데이터베이스 객체에 대한 소유자 변경” as a single unified routine. The current source has split this into one routine per object type (au_change_class_owner, au_change_serial_owner, au_change_trigger_owner, au_change_sp_owner, au_change_sp_owner_with_transfer_privileges). Functionally equivalent — but the deck’s “au_change_owner DB_RESOURCE_TYPE” generic-dispatch routine no longer exists as a single function.

  1. No salt on stored hashes. crypt_sha_two is invoked with NULL salt in encrypt_password_sha2_512, so two users with the same password end up with byte-identical hashes in db_password. This precludes per-row dictionary-attack defence. Investigation path: should we add a per-row salt attribute to db_password and migrate existing rows on first login? The plumbing already exists in crypt_sha_two’s first parameter.
  2. DBA-bypasses-password. perform_login skips the password check when the current session is already DBA. Is there a recorded threat model that says this is acceptable, or is this a legacy from an era when CSQL was DBA-only? See authenticate_context::perform_login — the !au_is_dba_group_member (current_user) || ignore_dba_privilege guard.
  3. db_user.id attribute. au_install declares an id integer attribute on db_user, but au_make_user does not set it (the user’s identity is the MOP / OID). Is this a reservation for a future “stable user id” feature, or can it be removed?
  4. Recursive revoke and partitioned classes. au_revoke_class does not have the sm_partitioned_class_type fan-out that au_grant_class does (verified at au_revoke_class body). Revoking a privilege that was granted on a partitioned class — does it revoke from every partition, or only the parent? Worth a focused test.
  5. Per-class cache memory growth. cache_max extends by cache_increment = 4 whenever a new user is allocated a slot, and every class cache is grown to the new size. With N classes and M users this is O(N·M) memory. For systems with many short-lived users (CAS broker pools), this may be unbounded; remove_user_cache reclaims the slot, but does it shrink the per-class arrays? Investigation path: read extend_class_caches and remove_user_cache together.
  6. AU_INDEX semantics. A GRANT INDEX writes to db_authorization, but is the bit ever consulted on the read path? is_protected_class lists AU_INDEX as forbidden on system catalogs. Where else does the bit gate?
  7. is_protected_class and BOOT_ADMIN_CSQL_CLIENT_TYPE. The check_authorization early-return only fires for admin CSQL clients on system classes. Is the intent that a non-admin CSQL session reading db_user should hit the privilege cache (and therefore see only what PUBLIC was granted), and an admin CSQL session sees it raw? Verify by running both sessions against a non-DBA user.

Raw analyses (raw/code-analysis/cubrid/common/)

Section titled “Raw analyses (raw/code-analysis/cubrid/common/)”
  • [code_analysis] Authenticate.pdf — Hyung-Gyu Ryoo’s deep- dive deck.
  • _converted/authenticate.pdf.md — pdftotext extract used as the working draft of the deck content.
  • knowledge/code-analysis/cubrid/cubrid-mvcc.md — authentication runs before MVCC visibility; the privilege check happens at au_fetch_class time, MVCC visibility at mvcc_satisfies_snapshot time, and the two are independent.
  • knowledge/code-analysis/cubrid/cubrid-lock-manager.mdau_grant/au_revoke take a write lock on the db_authorization row before mutating; lock interactions with the lock manager are out of scope here.
  • Database System Concepts, 7th ed. (Silberschatz, Korth, Sudarshan) — Ch. 4 §4.7 (“Authorization”): GRANT/REVOKE semantics, with-grant-option, recursive revoke. Ch. 5 §5.2 (“Security and Authorization”): the role of the DBA, privilege models, audit trails.
  • Database Internals (Petrov), Ch. 11 (“Replication and Consistency”) — only tangentially relevant, but its description of role-based access in Spanner / CockroachDB is a useful counterpoint to CUBRID’s group-as-user model.

CUBRID source (/data/hgryoo/references/cubrid/)

Section titled “CUBRID source (/data/hgryoo/references/cubrid/)”
  • src/object/authenticate.h — public C/C++ surface.
  • src/object/authenticate.cau_login shim, au_dump*, au_check_serial_authorization, au_check_server_authorization.
  • src/object/authenticate_constants.h — constants and bitmap layout.
  • src/object/authenticate_context.{cpp,hpp} — singleton context, install / start / login / set_user.
  • src/object/authenticate_password.{cpp,hpp} — encoding and matching.
  • src/object/authenticate_cache.{cpp,hpp}(user, class) bitmap cache.
  • src/object/authenticate_grant.{cpp,hpp}au_grant, au_revoke, apply_grants, propagate_revoke.
  • src/object/authenticate_access_auth.{cpp,hpp}_db_auth row-level CRUD (au_auth_accessor).
  • src/object/authenticate_access_class.cppau_fetch_class, check_authorization, is_protected_class.
  • src/object/authenticate_access_user.cpp — user CRUD, group membership, au_is_dba_group_member.
  • src/object/authenticate_owner.cpp — owner change per object type.
  • src/object/authenticate_migration.cppunloaddb export.
  • src/object/schema_system_catalog_constants.hCT_ROOT_NAME, CT_USER_NAME, CT_PASSWORD_NAME, CT_AUTHORIZATION_NAME macros.
  • src/base/crypt_opfunc.{c,h}crypt_sha_two, crypt_encrypt_printable, crypt_encrypt_sha1_printable.
  • src/base/encryption.{c,h} — DES seed handling (crypt_seed).