CUBRID Authentication and Authorization — Users, Passwords, Privileges, and System Catalogs
Contents:
- Theoretical Background
- Common DBMS Design
- CUBRID’s Approach
- Source Walkthrough
- Cross-check Notes
- Open Questions
- Sources
Theoretical Background
Section titled “Theoretical Background”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:
- Where do user records live? A purpose-built secret table
(PostgreSQL’s
pg_authid, MySQL’smysql.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’sdb_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. - 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_classonly; 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. - 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/REVOKEinvalidates it. Almost every modern engine chooses the cache approach because the read path runs millions of times more often than the grant path. CUBRID’sAU_CLASS_CACHEmakes 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.
Common DBMS Design
Section titled “Common DBMS Design”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.
A user catalog table
Section titled “A user catalog table”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.
Salted, prefix-tagged password hashes
Section titled “Salted, prefix-tagged password hashes”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).
A privilege catalog
Section titled “A privilege catalog”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.
GRANT / REVOKE semantics
Section titled “GRANT / REVOKE semantics”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.
A runtime cache
Section titled “A runtime cache”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.
Theory ↔ CUBRID mapping
Section titled “Theory ↔ CUBRID mapping”| Theoretical concept | CUBRID name |
|---|---|
| User catalog table | db_user (system class, MOP cached as Au_user_class) |
| Password catalog table | db_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 root | db_root (single instance, MOP cached as Au_root) |
| DBA superuser | DBA user (Au_dba_user); recognised by au_is_dba_group_member |
| Public role | PUBLIC user (Au_public_user); every new user is a member by default |
| Per-(user, object) privilege bitmap | AU_TYPE_MASK = 0x7F, lower 7 bits of grant cache (AU_SELECT…AU_EXECUTE) |
WITH GRANT OPTION | Upper 7 bits of cache, shifted by AU_GRANT_SHIFT = 8 (AU_GRANT_MASK = 0x7F00) |
| Runtime cache | AU_CLASS_CACHE per class, cache_index-keyed; AU_CACHE_INVALID = 0x80000000 |
| Login disable | AU_DISABLE/AU_ENABLE macros set disable_auth_check |
| Password ignore | ignore_passwords (set by au_disable_passwords, used by SA-mode utilities) |
| Algorithm tag in password | ENCODE_PREFIX_DES (1), ENCODE_PREFIX_SHA1 (2), ENCODE_PREFIX_SHA2_512 (3) |
| Privilege types | AU_SELECT 0x01, AU_INSERT 0x02, AU_UPDATE 0x04, AU_DELETE 0x08, AU_ALTER 0x10, AU_INDEX 0x20, AU_EXECUTE 0x40 |
Object types under au_grant | DB_OBJECT_CLASS, DB_OBJECT_PROCEDURE (only two, in 11.5) |
CUBRID’s Approach
Section titled “CUBRID’s Approach”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.
Module shape and entry points
Section titled “Module shape and entry points”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.
Catalog schema as installed by au_install
Section titled “Catalog schema as installed by au_install”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 rootsmt_add_attribute (def, "triggers", "sequence of (string, object)", ...);smt_add_attribute (def, "charset", "integer", ...);smt_add_attribute (def, "lang", "string", ...);smt_add_attribute (def, "timezone_checksum", "string", ...);// + class methods: add_user, drop_user, find_user, change_owner, ...
// db_user: one row per user, the public-facing user recordsmt_add_attribute (def, "name", "string", ...); // unique-indexedsmt_add_attribute (def, "id", "integer", ...);smt_add_attribute (def, "password", AU_PASSWORD_CLASS_NAME, ...); // → db_passwordsmt_add_attribute (def, "direct_groups", "set of (db_user)", ...);smt_add_attribute (def, "groups", "set of (db_user)", ...); // flattenedsmt_add_attribute (def, "authorization", AU_AUTH_CLASS_NAME, ...); // → db_authorizationsmt_add_attribute (def, "triggers", "sequence of object", ...);smt_add_attribute (def, AU_USER_ATTR_IS_LOGINABLE, "integer", NULL); // can this user log in?smt_add_attribute (def, AU_USER_ATTR_IS_SYSTEM_CREATED, "integer", NULL); // DBA / PUBLIC markersmt_add_attribute (def, "comment", "varchar(1024)", NULL);smt_add_attribute (def, "created_time", "datetime", NULL);smt_add_attribute (def, "updated_time", "datetime", NULL);// + instance methods: set_password, add_member, drop_member, ...
// db_password: holds the actual encoded password stringsmt_add_attribute (def, "password", "string", ...); // [tag-byte][hash-hex]\0
// db_authorization: one row per grantee, denormalisedsmt_add_attribute (def, AU_AUTH_ATTR_OWNER, AU_USER_CLASS_NAME, ...); // granteesmt_add_attribute (def, AU_AUTH_ATTR_GRANTS, "sequence", ...); // [type, obj, grantor, cache]*_db_auth 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.
authenticate_context — the global memo
Section titled “authenticate_context — the global memo”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:
#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_cacheThe 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.
Login flow
Section titled “Login flow”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.cppif (root == NULL || !BOOT_IS_CLIENT_RESTARTED ()) { if (name != NULL) strcpy (user_name, name);
if (password == NULL || strlen (password) == 0) { // empty password: zero out all three buffers } else { /* store the password encrypted (DES and SHA1 both) so we * don't have buffers lying around with the obvious * passwords in it. */ encrypt_password (password, 1, user_password_des_oldstyle); encrypt_password_sha1 (password, 1, user_password_sha1); encrypt_password_sha2_512 (password, user_password_sha2_512); } }else { /* Change users within an active database. */ AU_DISABLE (save); error = perform_login (name, password, ignore_dba_privilege); AU_ENABLE (save); }CUBRID 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.cppif (!ignore_passwords && (!au_is_dba_group_member (current_user) || ignore_dba_privilege)) { pass = NULL; if (!DB_IS_NULL (&value) && db_get_object (&value) != NULL) { if (obj_get (db_get_object (&value), "password", &value)) ... if (DB_IS_STRING (&value)) pass = db_get_string (&value); }
if (pass != NULL && strlen (pass)) { if ((dbpassword == NULL) || (strlen (dbpassword) == 0) || !match_password (dbpassword, db_get_string (&value))) { error = ER_AU_INVALID_PASSWORD; er_set (ER_ERROR_SEVERITY, ARG_FILE_LINE, error, 0); } } // ... condensed: empty-stored-password vs empty-supplied-password ... }
if (error == NO_ERROR) { error = set_user (user); sm_bump_local_schema_version (); // flush any cached vclasses for the old user }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.cppvoidencrypt_password_sha2_512 (const char *pass, char *dest){ int error_status = NO_ERROR; char *result_strp = NULL; int result_len = 0;
if (pass == NULL) { strcpy (dest, ""); } else { error_status = crypt_sha_two (NULL, pass, strlen (pass), 512, &result_strp, &result_len); if (error_status == NO_ERROR) { memcpy (dest + 1, result_strp, result_len); dest[result_len + 1] = '\0'; dest[0] = ENCODE_PREFIX_SHA2_512; /* '\x03' */ db_private_free_and_init (NULL, result_strp); } else { strcpy (dest, ""); } }}crypt_sha_two 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)boolmatch_password (const char *user, const char *database){ char buf1[AU_MAX_PASSWORD_BUF + 4]; // user-side, post-encoding char buf2[AU_MAX_PASSWORD_BUF + 4]; // database-side, copied verbatim
if (IS_ENCODED_DES (database)) { strcpy (buf2, database); /* user side: DES-encode if not already */ } else if (IS_ENCODED_SHA1 (database)) { strcpy (buf2, database); /* user side: SHA1-encode if not already */ } else if (IS_ENCODED_SHA2_512 (database)) { strcpy (buf2, database); if (IS_ENCODED_ANY (user)) strcpy (buf1, Au_user_password_sha2_512); // already pre-hashed at au_login else encrypt_password_sha2_512 (user, buf1); // encode now } else { /* unencoded legacy DB: SHA2-encode the DB side too */ }
return strcmp (buf1, buf2) == 0;}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.
Privilege evaluation on a SELECT
Section titled “Privilege evaluation on a SELECT”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.cppif (Au_user == NULL && !Au_disable) { error = ER_AU_INVALID_USER; er_set (ER_WARNING_SEVERITY, ARG_FILE_LINE, error, 1, ""); return error; }
// ... condensed: locator_fetch_class (or skip if already in workspace) ...
if ((Au_disable && type != DB_AUTH_ALTER) || ! (error = check_authorization (classmop, class_, type))) { if (class_ptr != NULL) *class_ptr = class_; }
return error;The bitmask test is inside check_authorization:
// check_authorization — src/object/authenticate_access_class.cppif (Au_disable) { int client_type = db_get_client_type (); if (!BOOT_ADMIN_CSQL_CLIENT_TYPE (client_type) || ! (sm_class->flags & SM_CLASSFLAG_SYSTEM)) return NO_ERROR; }
/* try to catch attempts by even the DBA to update a protected class */if ((sm_class->flags & SM_CLASSFLAG_SYSTEM) && is_protected_class (classobj, sm_class, type)) { error = appropriate_error (0, type); er_set (ER_WARNING_SEVERITY, ARG_FILE_LINE, error, 0); }else { bits = Au_cache.get_cache_bits (sm_class); if ((*bits & type) != type) { if (*bits == AU_CACHE_INVALID) { error = Au_cache.update (DB_OBJECT_CLASS, classobj, sm_class); // re-fetch and re-check bits = Au_cache.get_cache_bits (sm_class); if ((*bits & type) != type) error = appropriate_error (*bits, type); } else error = appropriate_error (*bits, type); } }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 grantsau_get_set (Au_user, "groups", &groups); // user's groups
if (set_ismember (groups, &owner)) // user is a group member of owner *bits = FULL_AUTH;else { // 1) apply grants directly held on this class au_get_object (Au_user, "authorization", &auth); apply_grants (auth, mop, bits);
// 2) apply grants held by every group the user is a member of card = set_cardinality (groups); for (i = 0; i < card; i++) { au_set_get_obj (groups, i, &group); if (ws_is_same_object (group, Au_dba_user)) { *bits = FULL_AUTH; } // group is DBA → all rights else { au_get_object (group, "authorization", &auth); apply_grants (auth, mop, bits); } } }apply_grants (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 and au_revoke
Section titled “au_grant and au_revoke”au_grant is the entry point for a GRANT statement. CUBRID
11.5 supports two object types:
// au_grant — src/object/authenticate_grant.cppintau_grant (DB_OBJECT_TYPE obj_type, MOP user, MOP class_mop, DB_AUTH type, bool grant_option){ int error = NO_ERROR; switch (obj_type) { case DB_OBJECT_CLASS: error = au_grant_class (user, class_mop, type, grant_option); break; case DB_OBJECT_PROCEDURE: error = au_grant_procedure (user, class_mop, type, grant_option); break; default: error = ER_FAILED; assert (false); break; } return error;}au_grant_class 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.cppintau_check_owner (DB_VALUE *creator_val){ MOP creator; DB_SET *groups; int ret_val = ER_FAILED;
creator = db_get_object (creator_val); if (ws_is_same_object (creator, Au_user) || au_is_dba_group_member (Au_user)) { ret_val = NO_ERROR; } else if (au_get_set (Au_user, "groups", &groups) == NO_ERROR) { if (set_ismember (groups, creator_val)) ret_val = NO_ERROR; set_free (groups); } return ret_val;}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 — au_add_user, au_drop_user
Section titled “User CRUD — au_add_user, au_drop_user”User CRUD is gated on DBA membership at every entry point:
// au_add_user — src/object/authenticate_access_user.cpp (condensed)MOPau_add_user (const char *name, int *exists){ MOP user = NULL; int save;
if (Au_dba_user != NULL && !au_is_dba_group_member (Au_user)) { er_set (ER_WARNING_SEVERITY, ARG_FILE_LINE, ER_AU_DBA_ONLY, 1, "add_user"); return NULL; } // ... AU_DISABLE (save); ... user = au_find_user (name); if (user != NULL) { *exists = 1; } // already exists else { user = au_make_user (name); // every new user is automatically a member of PUBLIC au_add_member_internal (Au_public_user, user, 1); au_set_user_timestamps (user); } return user;}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.
Source Walkthrough
Section titled “Source Walkthrough”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.
Constants and types
Section titled “Constants and types”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_AUTHenum (indbtype_def.h) —AU_NONE,AU_SELECT,AU_INSERT,AU_UPDATE,AU_DELETE,AU_ALTER,AU_INDEX,AU_EXECUTE.AU_FETCHMODEenum (inclass_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(inauthenticate.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 inauthenticate.h) — sugar forau_ctx()->....authenticate_context::init_ctx(au_init) — zero state.authenticate_context::install(au_install) —createdbbootstrap; 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, callsperform_login.authenticate_context::login(au_login’s body) — pre-hashes password into all three buffers when the DB is not yet open; delegates toperform_loginwhen it is.authenticate_context::perform_login— credential validation andset_user.authenticate_context::set_user— switch current user; bumps schema version viasc_set_current_schema.authenticate_context::set_password(au_set_password_encrypt) — wrapsau_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— theis_loginable/is_system_createdattribute 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— writedb_password.passwordfor a user, with DBA-bypass guard (!au_is_dba_group_member→ refuse to set someone else’s password).au_disable_passwords— setignore_passwords = 1.
Cache (authenticate_cache.{cpp,hpp})
Section titled “Cache (authenticate_cache.{cpp,hpp})”class authenticate_cache— the per-context cache holder.AU_CLASS_CACHE— one bitmap array per class, indexed by usercache_index.AU_USER_CACHE— one entry per registered user; carries thecache_index.authenticate_cache::init,flush— lifecycle.authenticate_cache::update— the 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; callsau_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 withcheck_authorization.au_fetch_instance,au_fetch_instance_force— instance variant; locks the class first, then fetches the instance.au_check_class_authorization(legacy nameau_check_authorization) — privilege-only check (no fetch), bypassesAu_disable.check_authorization(static) — the bitmask test; consults the cache, callsupdateon miss.is_protected_class(static) — refuses DML on system catalogs even for DBA.au_change_class_owner_including_partitions— partition-aware outer wrapper aroundau_change_class_owner.au_get_class_privilege— public accessor for the bitmap.fetch_class,fetch_instance(static) — locator wrappers.
Grants (authenticate_grant.{cpp,hpp})
Section titled “Grants (authenticate_grant.{cpp,hpp})”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 forclass_mopinto*bits.get_grants— readdb_authorization.grantsfor anauthMOP.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_authdeletes.map_grant_list(static) — DFS marking pass forpropagate_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 ondb_authorization.grants.appropriate_error— bit→ER_AU_*_FAILUREmapping.au_print_grants— forau_dump.check_grant_option(static) — verify the current user holdsWITH GRANT OPTIONfor the type being granted.au_force_write_new_auth(SA_MODEonly) — used byunloaddbto materialise_db_authfromdb_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 primitivesau_grant/au_revokeuse.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 byau_change_class_ownerto transfer privileges atomically.
User CRUD (authenticate_access_user.cpp)
Section titled “User CRUD (authenticate_access_user.cpp)”au_find_user— lookup by name (uppercases first).au_find_user_to_drop— likeau_find_userbut 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 throughau_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— readname(also short-circuits viasc_current_schema_namewhen 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 mappingDB_OBJECT_TYPEto catalog class name (used by error messages).
Owner change (authenticate_owner.cpp)
Section titled “Owner change (authenticate_owner.cpp)”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.
Migration (authenticate_migration.cpp)
Section titled “Migration (authenticate_migration.cpp)”au_export_users— emitCALL add_user(...)andset_password_encodedcalls for everydb_userrow.au_export_grants— emitGRANT ... ON ... TO ...for every_db_authrow referencing the given class.- (helpers)
emit_grant,emit_user,emit_member— internal formatters.
Debugging
Section titled “Debugging”au_dump,au_dump_to_file,au_dump_user,au_dump_auth— print the catalog state, walkdb_user/db_authorization, format with the message catalog (MSGCAT_SET_AUTHORIZATION).
Position hints as of 2026-04-30
Section titled “Position hints as of 2026-04-30”| Symbol | File | Line |
|---|---|---|
Au_root (and friends, macros) | authenticate.h | 57 |
AU_DISABLE/AU_ENABLE macros | authenticate.h | 104 |
au_login (free function) | authenticate.h | 142 |
au_grant (signature) | authenticate.h | 148 |
au_revoke (signature) | authenticate.h | 149 |
au_check_class_authorization (sig.) | authenticate.h | 196 |
au_check_serial_authorization | authenticate.h | 197 |
au_check_server_authorization | authenticate.h | 198 |
au_check_procedure_authorization | authenticate.h | 199 |
AU_TYPE_MASK, AU_GRANT_MASK, AU_FULL_AUTHORIZATION | authenticate_constants.h | 82 |
AU_GRANT_SHIFT | authenticate_constants.h | 92 |
AU_CACHE_INVALID | authenticate_constants.h | 95 |
AU_MAX_PASSWORD_CHARS, AU_MAX_PASSWORD_BUF | authenticate_constants.h | 97 |
class authenticate_context | authenticate_context.hpp | 38 |
AU_USER_ATTR_IS_LOGINABLE, _IS_SYSTEM_CREATED | authenticate_context.hpp | 35 |
au_login (body) | authenticate.c | 89 |
au_ctx | authenticate.c | 95 |
au_check_serial_authorization | authenticate.c | 461 |
au_check_server_authorization | authenticate.c | 486 |
au_disable_passwords | authenticate.c | 515 |
authenticate_context::reset | authenticate_context.cpp | 42 |
authenticate_context::init_ctx / final_ctx | authenticate_context.cpp | 68/75 |
authenticate_context::start | authenticate_context.cpp | 95 |
authenticate_context::login | authenticate_context.cpp | 218 |
authenticate_context::install | authenticate_context.cpp | 283 |
authenticate_context::perform_login | authenticate_context.cpp | 555 |
authenticate_context::set_user | authenticate_context.cpp | 687 |
authenticate_context::set_password (2 ovr.) | authenticate_context.cpp | 747/759 |
authenticate_context::disable_passwords | authenticate_context.cpp | 769 |
authenticate_context::check_user | authenticate_context.cpp | 781 |
authenticate_context::get_current_user_name | authenticate_context.cpp | 832 |
authenticate_context::push_user / pop_user | authenticate_context.cpp | 917/932 |
authenticate_context::is_loginable_user | authenticate_context.cpp | 991 |
ENCODE_PREFIX_* macros | authenticate_password.hpp | 34 |
IS_ENCODED_* macros | authenticate_password.hpp | 38 |
encrypt_password | authenticate_password.cpp | 60 |
encrypt_password_sha1 | authenticate_password.cpp | 89 |
encrypt_password_sha2_512 | authenticate_password.cpp | 116 |
match_password | authenticate_password.cpp | 159 |
au_set_password_internal | authenticate_password.cpp | 248 |
class authenticate_cache | authenticate_cache.hpp | 86 |
AU_CLASS_CACHE | authenticate_cache.hpp | 50 |
AU_USER_CACHE | authenticate_cache.hpp | 71 |
authenticate_cache::init | authenticate_cache.cpp | 58 |
authenticate_cache::flush | authenticate_cache.cpp | 75 |
authenticate_cache::update | authenticate_cache.cpp | 118 |
authenticate_cache::free_authorization_cache | authenticate_cache.cpp | 317 |
authenticate_cache::make_class_cache | authenticate_cache.cpp | 353 |
class au_auth_accessor | authenticate_access_auth.hpp | 45 |
au_auth_accessor::insert_auth / etc. (sig.) | authenticate_access_auth.hpp | 71 |
au_change_class_owner_including_partitions | authenticate_access_class.cpp | 67 |
au_check_class_authorization | authenticate_access_class.cpp | 328 |
fetch_class | authenticate_access_class.cpp | 362 |
au_fetch_class_internal | authenticate_access_class.cpp | 529 |
au_fetch_class | authenticate_access_class.cpp | 614 |
au_fetch_class_by_instancemop | authenticate_access_class.cpp | 630 |
au_fetch_class_by_classmop | authenticate_access_class.cpp | 646 |
au_fetch_class_force | authenticate_access_class.cpp | 663 |
au_fetch_instance | authenticate_access_class.cpp | 699 |
au_fetch_instance_force | authenticate_access_class.cpp | 746 |
fetch_instance | authenticate_access_class.cpp | 761 |
check_authorization | authenticate_access_class.cpp | 876 |
is_protected_class | authenticate_access_class.cpp | 953 |
au_grant | authenticate_grant.cpp | 87 |
au_grant_class | authenticate_grant.cpp | 117 |
au_grant_procedure | authenticate_grant.cpp | 314 |
au_revoke | authenticate_grant.cpp | 487 |
au_revoke_class | authenticate_grant.cpp | 525 |
au_revoke_procedure | authenticate_grant.cpp | 726 |
appropriate_error | authenticate_grant.cpp | 983 |
add_grant_entry | authenticate_grant.cpp | 1164 |
drop_grant_entry | authenticate_grant.cpp | 1196 |
apply_grants | authenticate_grant.cpp | 1532 |
collect_class_grants | authenticate_grant.cpp | 1584 |
propagate_revoke | authenticate_grant.cpp | 1751 |
au_compare_grantor_and_return | authenticate_grant.cpp | 1915 |
au_print_grants | authenticate_grant.cpp | 2014 |
au_check_procedure_authorization | authenticate_grant.cpp | 2058 |
au_force_write_new_auth (SA_MODE) | authenticate_grant.cpp | 2110 |
au_find_user | authenticate_access_user.cpp | 62 |
au_find_user_to_drop | authenticate_access_user.cpp | 213 |
au_get_user_name | authenticate_access_user.cpp | 296 |
au_make_user | authenticate_access_user.cpp | 348 |
au_is_dba_group_member | authenticate_access_user.cpp | 470 |
au_is_user_group_member | authenticate_access_user.cpp | 506 |
au_add_user | authenticate_access_user.cpp | 577 |
au_set_user_comment | authenticate_access_user.cpp | 680 |
au_set_user_timestamps | authenticate_access_user.cpp | 722 |
au_add_member_internal | authenticate_access_user.cpp | 952 |
au_add_member | authenticate_access_user.cpp | 1058 |
au_drop_member | authenticate_access_user.cpp | 1086 |
au_drop_user | authenticate_access_user.cpp | 1193 |
au_check_owner | authenticate_owner.cpp | 38 |
au_change_serial_owner | authenticate_owner.cpp | 70 |
au_change_trigger_owner | authenticate_owner.cpp | 264 |
au_get_class_owner | authenticate_owner.cpp | 392 |
au_change_class_owner | authenticate_owner.cpp | 417 |
au_change_sp_owner | authenticate_owner.cpp | 534 |
au_change_sp_owner_with_transfer_privileges | authenticate_owner.cpp | 640 |
au_export_users | authenticate_migration.cpp | 110 |
au_export_grants | authenticate_migration.cpp | 581 |
au_dump_auth | authenticate.c | 249 |
au_dump_user | authenticate.c | 297 |
au_dump_to_file | authenticate.c | 375 |
au_dump | authenticate.c | 449 |
Cross-check Notes
Section titled “Cross-check Notes”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_grantacceptsDB_OBJECT_PROCEDURE, not justDB_OBJECT_CLASS. Verified at theau_grantswitch (inauthenticate_grant.cpp): the body has two arms,au_grant_classandau_grant_procedure. Procedures can therefore receiveGRANT EXECUTE. The privilege check on the procedure read path isau_check_procedure_authorization, which does go through the same_db_auth/db_authorizationmachinery 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(inauthenticate.c) andau_check_server_authorization(same file): both callau_check_ownerdirectly, 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 andau_grant_classaccepts them, so aGRANT INDEX ON tbl TO u1writes 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_indexprivilege 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.
Open Questions
Section titled “Open Questions”- No salt on stored hashes.
crypt_sha_twois invoked withNULLsalt inencrypt_password_sha2_512, so two users with the same password end up with byte-identical hashes indb_password. This precludes per-row dictionary-attack defence. Investigation path: should we add a per-row salt attribute todb_passwordand migrate existing rows on first login? The plumbing already exists incrypt_sha_two’s first parameter. - DBA-bypasses-password.
perform_loginskips 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? Seeauthenticate_context::perform_login— the!au_is_dba_group_member (current_user) || ignore_dba_privilegeguard. db_user.idattribute.au_installdeclares anidinteger attribute ondb_user, butau_make_userdoes 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?- Recursive revoke and partitioned classes.
au_revoke_classdoes not have thesm_partitioned_class_typefan-out thatau_grant_classdoes (verified atau_revoke_classbody). Revoking a privilege that was granted on a partitioned class — does it revoke from every partition, or only the parent? Worth a focused test. - Per-class cache memory growth.
cache_maxextends bycache_increment = 4whenever 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_cachereclaims the slot, but does it shrink the per-class arrays? Investigation path: readextend_class_cachesandremove_user_cachetogether. AU_INDEXsemantics. AGRANT INDEXwrites todb_authorization, but is the bit ever consulted on the read path?is_protected_classlistsAU_INDEXas forbidden on system catalogs. Where else does the bit gate?is_protected_classandBOOT_ADMIN_CSQL_CLIENT_TYPE. Thecheck_authorizationearly-return only fires for admin CSQL clients on system classes. Is the intent that a non-admin CSQL session readingdb_usershould hit the privilege cache (and therefore see only whatPUBLICwas granted), and an admin CSQL session sees it raw? Verify by running both sessions against a non-DBA user.
Sources
Section titled “Sources”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.
Sibling docs
Section titled “Sibling docs”knowledge/code-analysis/cubrid/cubrid-mvcc.md— authentication runs before MVCC visibility; the privilege check happens atau_fetch_classtime, MVCC visibility atmvcc_satisfies_snapshottime, and the two are independent.knowledge/code-analysis/cubrid/cubrid-lock-manager.md—au_grant/au_revoketake a write lock on thedb_authorizationrow before mutating; lock interactions with the lock manager are out of scope here.
Textbook chapters
Section titled “Textbook chapters”- Database System Concepts, 7th ed. (Silberschatz, Korth,
Sudarshan) — Ch. 4 §4.7 (“Authorization”):
GRANT/REVOKEsemantics, 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.c—au_loginshim,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_authrow-level CRUD (au_auth_accessor).src/object/authenticate_access_class.cpp—au_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.cpp—unloaddbexport.src/object/schema_system_catalog_constants.h—CT_ROOT_NAME,CT_USER_NAME,CT_PASSWORD_NAME,CT_AUTHORIZATION_NAMEmacros.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).