diff --git "a/TestTasks/KuanXY/openGauss\345\256\211\345\205\250\346\234\272\345\210\266\344\271\213\345\217\243\344\273\244\345\255\230\345\202\250.md" "b/TestTasks/KuanXY/openGauss\345\256\211\345\205\250\346\234\272\345\210\266\344\271\213\345\217\243\344\273\244\345\255\230\345\202\250.md" new file mode 100644 index 0000000000000000000000000000000000000000..5e297f676dd17f7332cd5ccb32273c333aaa3540 --- /dev/null +++ "b/TestTasks/KuanXY/openGauss\345\256\211\345\205\250\346\234\272\345\210\266\344\271\213\345\217\243\344\273\244\345\255\230\345\202\250.md" @@ -0,0 +1,520 @@ +### openGauss安全机制之口令存储 + +[TOC] + +#### 前言 + +本文《openGauss安全机制之口令存储》为第五届中国软件开源创新大赛——开源代码评注赛道参赛作品 + +作者:袁建硕 + +所属战队:共同学习 + +时间:2022-10-02 + +评注代码:[opengauss-mirror/openGauss-server: openGauss kernel (github.com)](https://github.com/opengauss-mirror/openGauss-server) + +*** + + + +#### 口令加密与存储概述 + +口令的加密与存储作为身份认证的重要一环,尤其是在面向庞大的价值数据面前,安全性与可靠性格外重要。本文主要分析openGauss对口令的操作与加密方式,并另外重点分析新增支持的SM3加密算法原理。 + +openGauss数据库在执行创建用户或修改用户口令操作时,会将口令通过**单向Hash**加密存储在pg_authid系统表中。口令的加密方式与加密参数配置在"password_encryption_type"中。 + +目前系统支持MD5、SHA256+MD5(同时存储两种加密值)、SHA256以及**SM3(新增)**四种方式,默认采用第三种SHA256加密方式。 + +> 这里解释为何要保留已经稍显落伍的MD5加密方式? +> +> 主要是考虑到了数据库体系结构的**向后兼容**,为了**兼容已经逐渐成熟的PostgreSQL**社区及其他工具,保留了安全性较低的MD5方式。 + + + +一张表概括各类加密类型、加密方式、认证方式、加密函数之间的关系: + +| password_encryption_type | 加密方式 | 认证方式(pg_hba.conf) | 加密函数接口 | +| ------------------------ | ---------- | --------------------- | -------------------------------------- | +| 0 | MD5 | MD5 | pg_md5_encrypt | +| 1 | SHA256+MD5 | SHA256或MD5 | calculate_encrypted_combinaed_password | +| 2(默认) | SHA256 | SHA256 | calculate_encrypted_sha256_password | +| 3(PASSWORD_TYPE_SM3) | SM3 | SM3 | gs_calculate_encrypted_sm3_password | + + + +#### 口令加密过程 + +口令加密的需求产生于**创建用户以及用户修改密码**。收到指令后,生成随机数作为**盐值Salt**。之后**检查**口令是否满足**复杂度的要求**(详情可见[补充](#补充)部分),满足口令复杂度的话,则去**执行口令加密函数**,**判断加密方式**进行加密,返回密文口令,**存储到pg_authid系统表**中,完成加密的全过程。 + +![kouling0](https://forum.gitlink.org.cn/api/attachments/398546) + + + +#### 相关代码分析 + +##### 口令加密函数 → calculate_encrypted_password + +```cpp +/* gausskernel/optimizer/commands/user.cpp:6052-6092 */ +Datum calculate_encrypted_password(bool is_encrypted, const char* password, const char* rolname, + const char* salt_string) +{ + // 空密码 + if (password == NULL || password[0] == '\0') { + ereport(ERROR, (errcode(ERRCODE_INVALID_PASSWORD), errmsg("The password could not be NULL."))); + } + errno_t rc = EOK; + char encrypted_md5_password[MD5_PASSWD_LEN + 1] = {0}; + Datum datum_value; + + // 是否已加密 + if (!is_encrypted || isPWDENCRYPTED(password)) { + return CStringGetTextDatum(password); + } + + /* + * The guc parameter of u_sess.attr.attr_security.Password_encryption_type here may be 0, 1, 2. + * if Password_encryption_type is 0, the encrypted password is md5. + * if Password_encryption_type is 1, the encrypted password is sha256 + md5. + * if Password_encryption_type is 2, the encrypted password is sha256. + * 另外新增了了SM3的加密方式Password_encryption_type is PASSWORD_TYPE_SM3(其值为3) + */ + // MD5加密 + if (u_sess->attr.attr_security.Password_encryption_type == 0) { + if (!pg_md5_encrypt(password, rolname, strlen(rolname), encrypted_md5_password)) { + rc = memset_s(encrypted_md5_password, MD5_PASSWD_LEN + 1, 0, MD5_PASSWD_LEN + 1); + securec_check(rc, "\0", "\0"); + ereport(ERROR, (errcode(ERRCODE_INVALID_PASSWORD), errmsg("password encryption failed"))); + } + + datum_value = CStringGetTextDatum(encrypted_md5_password); + rc = memset_s(encrypted_md5_password, MD5_PASSWD_LEN + 1, 0, MD5_PASSWD_LEN + 1); + securec_check(rc, "\0", "\0"); + ereport(NOTICE, (errmsg("The encrypted password contains MD5 ciphertext, which is not secure."))); + } else if (u_sess->attr.attr_security.Password_encryption_type == 1) { // sha256+MD5 + datum_value = calculate_encrypted_combined_password(password, rolname, salt_string); + } else if (u_sess->attr.attr_security.Password_encryption_type == PASSWORD_TYPE_SM3) { //SM3加密 + datum_value = gs_calculate_encrypted_sm3_password(password, salt_string); + } else { // sha256 + datum_value = calculate_encrypted_sha256_password(password, rolname, salt_string); + } + + return datum_value; +} +``` + + + + + +##### MD5加密方式 + +对于MD5加密算法,本文便不详细介绍 + +> 可参考优质博客[ MD5 加密算法详解_红月修罗的博客-CSDN博客_md5是什么算法](https://blog.csdn.net/hawinlolo/article/details/94464237) + +MD5 算法将输入的信息进行分组,每组512 位(64个 字节),顺序处理完所有分组后输出128 位结果。 +在每一组消息的处理中,都要进行4 轮、每轮16 步、总计64 步的处理。其中每步计算中含一次左循环移位,每一步结束时将计算结果进行一次右循环移位。 + +执行函数:**pg_md5_encrypt( )** + +```cpp +/* common/backend/libpq/md5.cpp:308-349 */ +bool pg_md5_encrypt(const char* passwd, const char* salt, size_t salt_len, char* buf) +{ + ... +} +``` + + + +##### MD5加密和SHA256加密方式 + +**执行函数:calculate_encrypted_combined_password( )** + +```cpp +/* gausskernel/optimizer/commands/user.cpp:5904-5958 */ +Datum calculate_encrypted_combined_password(const char* password, const char* rolname, const char* salt_string) +{ + ... +} +``` + + + +##### SHA256加密方式 + +执行函数:**calculate_encrypted_sha256_password( )** + +```cpp +/* gausskernel/optimizer/commands/user.cpp:5960-6000 */ +Datum calculate_encrypted_sha256_password(const char* password, const char* rolname, const char* salt_string) +{ + ... +} +``` + + + +##### SM3加密方式 + +执行函数:**gs_calculate_encrypted_sm3_password( )** + +```cpp +/* gausskernel/optimizer/commands/user.cpp:6002-6042 */ +static Datum gs_calculate_encrypted_sm3_password(const char* password, const char* salt_string) +{ + ... +} +``` + + + +前面三种加密方式已经耳熟能详,互联网上也有了不少优质文章介绍。也正是因为这些加密算法大家耳熟能详,关注度高,这些**算法也逐渐被攻破**。为了保障信息安全,国家密码管理局于2010年制定并公布了一系列国产密码算法,简称国密算法。 + +而openGauss也基于这一考量,引入了国密SM3加密算法,但由于引入的时间较短,相关文章也是寥寥无几。本着前人栽树后人乘凉的互联网理念,在这里详细介绍一下国密SM3。 + +*** + +#### openGauss引入国密SM3加密算法 + +**SM3密码杂凑算法**是中国国家密码管理局2010年公布的中国商用密码杂凑算法标准。该算法于2012年发布为密码行业标准(GM/T 0004-2012),2016年发布为国家密码杂凑算法标准(GB/T 32905-2016)。 + +SM3适用于商用密码应用中的数字签名和验证,是在SHA-256基础上改进实现的一种算法,其安全性和SHA-256相当。SM3和MD5的迭代过程类似,也采用Merkle-Damgard结构。消息分组长度为512位,摘要值长度为256位。 + +整个算法的执行过程可以概括成四个步骤:**消息填充、消息扩展、迭代压缩、输出结果。** + + + +##### 1. 消息填充 + +SM3的消息扩展是以512位的数据分组作为输入。 + +对数据进行填充,填充方法同MD5的加密方式,填入“1”后,填入k个“0”,使得拓展长度满足 + +```c++ +( n + 1 + k ) mod 512 = 448 +``` + +那么为什么是448呢?因为后面要再填充一段64位长的空间存储数据的长度。 + +如图所示: + +SM31 + + + +##### 2. 消息扩展 + +将512位数据划分为16个消息字,利用第一个512位数据生成的16个消息字递推生成剩余的116个消息字 + +总共迭代产生132个消息字,将前68个消息字记为`Wj`,后64个消息字记为`Wj'`。 + +生成消息字伪代码如下 + +```cpp +def CF(): +for j = 16 to 67 + Wj = P1(W_[j-16] xor W_[j-9] xor (W_[j-3] <<< 15 ) ) xor (W_[j-13] <<< 7) xor W_[j-6] + +for j = 0 to 63 + Wj' = Wj xor W_[j+4] +``` + +如下图所展示的过程递归加密 + +![](https://forum.gitlink.org.cn/api/attachments/398547) + + + +##### 3. 迭代压缩 + +然后是分析具体的压缩函数: + +`前16个消息值`与`初值IV`作为输入进入压缩函数,得到的值`result1`作为下一次的输入和下一组`16个消息字` + +得到`result2`,依次递归得到最终的杂凑值。 + + + +压缩函数的细节如下伪代码: + +```cpp +ABCDEFGH = V_i +V_(i+1) = CF(V(i), B(i)) +FOR j = 0 TO 63 + SS1 = ((A <<< 12) + E + (T_j <<< j)) <<< 7 + SS2 = SS1 xor (A <<< 12) + TT1 = FFj(A,B,C) + D + SS2 + Wj' + TT2 = GGj(E,F,G) + H + SS1 + Wj + D = C + C = B <<< 9 + B = A + A = TT1 + H = G + G = F <<< 19 + F = E + E = P0(TT2) +``` + +压缩函数的流程图如图所示: + +![](https://forum.gitlink.org.cn/api/attachments/398548) + + + +##### 4 .输出结果 + +最后由压缩函数的输出A、B、C、D、E、F、G、H八个变量拼接就可以得到最后的加密值啦! + + + +##### 5. 结合代码分析 + +在已经了解了国密SM3的加密算法原理之后,让我们结合openGauss的源码看看是如何实现算法细节的。 + +我们追踪到之前提到的sm3加密函数**gs_calculate_encrypted_sm3_password( )** + +```cpp +/* + * INPUT: 口令password,盐值salt_string + * OUTPUT: 口令密文datum_value +*/ + +static Datum gs_calculate_encrypted_sm3_password(const char* password, const char* salt_string) +{ + char encrypted_sm3_password[SM3_PASSWD_LEN + 1] = {0}; // length: 196 + char encrypted_sm3_password_complex[SM3_PASSWD_LEN + ITERATION_STRING_LEN + 1] = {0}; // length: 206 + // iteration_string_len 迭代字符串长度 + char iteration_string[ITERATION_STRING_LEN + 1] = {0}; // length: 12 + Datum datum_value; + errno_t rc = EOK; + + // 将各类参数传入核心加密函数GsSm3Encrypt中 + if (!GsSm3Encrypt(password, + salt_string, + strlen(salt_string), + encrypted_sm3_password, + NULL, + u_sess->attr.attr_security.auth_iteration_count)) { + rc = memset_s(encrypted_sm3_password, SM3_PASSWD_LEN + 1, 0, SM3_PASSWD_LEN + 1); + securec_check(rc, "\0", "\0"); + ereport(ERROR, (errcode(ERRCODE_INVALID_PASSWORD), errmsg("password encryption failed"))); + } + + ... + + return datum_value; +} +``` + + + +这里追踪一下常数,做出一些简单的计算已写入注释中: + +```CPP +SM3_PASSWD_LEN = (ENCRYPTED_STRING_LENGTH + SM3_LENGTH) = 195 + + SM3_LENGTH = 3 + ENCRYPTED_STRING_LENGTH = (HMAC_STRING_LENGTH + STORED_KEY_STRING_LENGTH + SALT_STRING_LENGTH) = 192 + HMAC_STRING_LENGTH = (HMAC_LENGTH * 2) = 64 + HMAC_LENGTH = 32 + STORED_KEY_STRING_LENGTH = (STORED_KEY_LENGTH * 2) = 64 + STORED_KEY_LENGTH = 32 + SALT_STRING_LENGTH = (SALT_LENGTH * 2) = 64 + SALT_LENGTH = 32 + + +ITERATION_STRING_LEN = 11 +``` + + + +继续追踪加密核心函数**GsSm3Encrypt()**: + +```cpp +/* + * INPUT: 口令password,盐值salt_s,盐值长度salt_len,密文缓冲区buf, + * 客户端密钥client_key_buf,迭代长度iteration_count + * OUTPUT: bool +*/ + +bool GsSm3Encrypt( + const char* password, const char* salt_s, size_t salt_len, char* buf, char* client_key_buf, int iteration_count) +{ + size_t password_len = 0; + char k[K_LENGTH + 1] = {0}; // K_LENGTH = 32 + char client_key[CLIENT_KEY_BYTES_LENGTH + 1] = {0}; // CLIENT_KEY_BYTES_LENGTH = 32 + char sever_key[HMAC_LENGTH + 1] = {0}; // HMAC_LENGTH = 32 + char stored_key[STORED_KEY_LENGTH + 1] = {0}; // STORED_KEY_LENGTH = 32 + char salt[SALT_LENGTH + 1] = {0}; // SALT_LENGTH = 32 + char serverKeyString[HMAC_LENGTH * ENCRY_LENGTH_DOUBLE + 1] = {0}; // HMAC_LENGTH = 32 + // ENCRY_LENGTH_DOUBLE = 2 + char stored_key_string[STORED_KEY_LENGTH * 2 + 1] = {0}; // STORED_KEY_LENGTH = 32 + int pkcs_ret; + int sever_ret; + int client_ret; + int hash_ret; + int hmac_length = HMAC_LENGTH; + int stored_key_length = STORED_KEY_LENGTH; + int total_encrypt_length; + char server_string[SEVER_STRING_LENGTH_SM3] = "Server Key"; + char client_string[CLIENT_STRING_LENGTH] = "Client Key"; + errno_t rc = 0; + + if (NULL == password || NULL == buf) { + return false; + } + + password_len = strlen(password); + /* Tranform string(64Bytes) to binary(32Bytes) */ + /* 转换一下格式 */ + sha_hex_to_bytes32(salt, (char*)salt_s); + /* calculate k */ + /* 这里我们之前介绍过,满足(n+1+k) mod 512 = 448 */ + // 生成消息认证码 + pkcs_ret = PKCS5_PBKDF2_HMAC((char*)password, + password_len, + (unsigned char*)salt, + SALT_LENGTH, + iteration_count, + (EVP_MD*)EVP_sha1(), + K_LENGTH, + (unsigned char*)k); + if (!pkcs_ret) { + rc = memset_s(k, K_LENGTH + 1, 0, K_LENGTH + 1); + SECUREC_CHECK(rc); + + return false; + } + + /* We have already get k ,then we calculate client key and server key, + * then calculate stored key by using client key */ + + /* calculate server rkey */ + /* 计算服务端密钥 */ + sever_ret = CRYPT_hmac(NID_hmacWithSHA256, + (GS_UCHAR*)k, + K_LENGTH, + (GS_UCHAR*)server_string, + SEVER_STRING_LENGTH_SM3 - 1, + (GS_UCHAR*)sever_key, + (GS_UINT32*)&hmac_length); + if (sever_ret) { + rc = memset_s(k, K_LENGTH + 1, 0, K_LENGTH + 1); + SECUREC_CHECK(rc); + + rc = memset_s(sever_key, HMAC_LENGTH + 1, 0, HMAC_LENGTH + 1); + SECUREC_CHECK(rc); + return false; + } + + /* calculate client key */ + /* 计算客户端密钥 */ + client_ret = CRYPT_hmac(NID_hmacWithSHA256, + (GS_UCHAR*)k, + K_LENGTH, + (GS_UCHAR*)client_string, + CLIENT_STRING_LENGTH - 1, + (GS_UCHAR*)client_key, + (GS_UINT32*)&hmac_length); + if (client_ret) { + rc = memset_s(k, K_LENGTH + 1, 0, K_LENGTH + 1); + SECUREC_CHECK(rc); + + rc = memset_s(client_key, CLIENT_KEY_BYTES_LENGTH + 1, 0, CLIENT_KEY_BYTES_LENGTH + 1); + SECUREC_CHECK(rc); + + rc = memset_s(sever_key, HMAC_LENGTH + 1, 0, HMAC_LENGTH + 1); + SECUREC_CHECK(rc); + + return false; + } + + /* 转换回64位 */ + if (NULL != client_key_buf) { + sha_bytes_to_hex64((uint8*)client_key, client_key_buf); + } + + hash_ret = EVP_Digest( + (GS_UCHAR*)client_key, HMAC_LENGTH, (GS_UCHAR*)stored_key, (GS_UINT32*)&stored_key_length, EVP_sm3(), NULL); + + if (!hash_ret) { + ... + // 安全检查 + return false; + } + + /* Mark the type in the stored string */ + ... + + /* We must clear the mem before we free it for the safe */ + /* 清理内存,以确保安全*/ + ... + + return true; +} + +``` + + + +可惜只是看到了加密的整体流程,没有追踪到算法的具体细节。 + +但根据注释 + +```cpp + /* calculate server rkey */ + sever_ret = CRYPT_hmac() + +GS_UINT32 CRYPT_hmac(GS_UINT32 ulAlgType, const GS_UCHAR* pucKey, GS_UINT32 upucKeyLen, const GS_UCHAR* pucData, + GS_UINT32 ulDataLen, GS_UCHAR* pucDigest, GS_UINT32* pulDigestLen) +{ + const EVP_MD* evp_md = get_evp_md_by_id(ulAlgType); + if (evp_md == NULL) { + return 1; + } +#ifndef WIN32 + if (!HMAC(evp_md, pucKey, (int)upucKeyLen, pucData, ulDataLen, pucDigest, pulDigestLen)) { + return 1; + } +#else + if (!HMAC(evp_md, pucKey, (int)upucKeyLen, pucData, ulDataLen, pucDigest, (unsigned int*)pulDigestLen)) { + return 1; + } +#endif + return 0; +} +``` + +大致可以猜测到函数HMAC就是加密的关键。而且自此,便无法继续追踪下去,通过查阅资料 + +[HMAC(Hash-based Message Authentication Code)实现原理 - yvivid - 博客园 (cnblogs.com)](https://www.cnblogs.com/yvivid/p/hmac_basic.html),可以确认,加密细节都在其中。 + +*** + +#### 总结 + +本文重点分析了openGauss安全机制中的口令存储部分。安全形势总是日新月异,只有不断进步、创新,才能稳稳的保护用户的数据安全。正如**openGauss2022年主要规划特性:性能,可靠,安全所描述的样子**,采用新的加密算法国密SM3是一个不错的选择,也希望openGauss可以一步一步做下去,为更多用户提供优质的开源数据库软件。 + + + +#### 补充 + +openGauss用户口令强度校验机制: + +- 包含大写字母(A-Z)的最少个数(password_min_uppercase) +- 包含小写字母(a-z)的最少个数(password_min_lowercase) +- 包含数字(0-9)的最少个数(password_min_digital) +- 包含特殊字符的最少个数(password_min_special) +- 密码的最小长度(password_min_length) +- 密码的最大长度(password_max_length) +- 至少包含上述四类字符中的三类。 +- 不能和用户名、用户名倒写相同,本要求为非大小写敏感。 +- 不能和当前密码、当前密码的倒写相同。 +- 不能是弱口令。 + +参数password_policy设置为1时表示采用密码复杂度校验,默认值为1。[点我返回](#口令加密过程) + + diff --git "a/TestTasks/KuanXY/openGauss\345\256\211\345\205\250\346\234\272\345\210\266\344\271\213\345\256\241\350\256\241\344\270\216\350\277\275\350\270\252.md" "b/TestTasks/KuanXY/openGauss\345\256\211\345\205\250\346\234\272\345\210\266\344\271\213\345\256\241\350\256\241\344\270\216\350\277\275\350\270\252.md" new file mode 100644 index 0000000000000000000000000000000000000000..c67f9507f070430a5194e83e6be94ca18f51875a --- /dev/null +++ "b/TestTasks/KuanXY/openGauss\345\256\211\345\205\250\346\234\272\345\210\266\344\271\213\345\256\241\350\256\241\344\270\216\350\277\275\350\270\252.md" @@ -0,0 +1,391 @@ +## openGauss安全机制之审计与追踪 + +[TOC] + +### 前言 + +本文《openGauss安全机制之审计与追踪》为第五届中国软件开源创新大赛——开源代码评注赛道参赛作品 + +作者:袁建硕 + +所属战队:共同学习 + +时间:2022-10-05 + +评注代码:[opengauss-mirror/openGauss-server: openGauss kernel (github.com)](https://github.com/opengauss-mirror/openGauss-server) + +*** + +### 审计与追踪概述 + +随着信息泄露事件的频发,数据库安全产品逐渐进入大众视野。而**数据库审计(DBAudit)**作为数据库安全技术之一,以安全事件为中心,以全面审计和精确审计为基础,实时记录网络上的数据库活动,对数据库操作进行细粒度审计的合规性管理,对数据库遭受到的风险行为进行实时告警。为用户带来诸多价值: + ++ 满足合规要求,加强监管能力 ++ 数据库为核心,操作全方位掌控 ++ 行为准确定位,便于追查定责 ++ 攻击精准识别,保证数据库系统安全 + +数据库的审计一定程度上起到威慑作用,将审计作为证据,依靠法律武器积极维权,也是保护数据安全的重要措施之一。 + + + +### 审计日志设计 + +首先,我们先了解审计日志的结构设计,审计日志通常是存储在数据库的表中或者系统文件中,openGauss在设计上选择了后者,更好的利用了操作系统的文件权限管理。 + +审计日志文件受操作系统权限的保护,默认只有初始化用户可以读写,保证了审计结果的安全性,避免被攻击篡改日志。 + +由源码以及命名规则不难得知,每条审计记录对应一个**AuditData**结构体 + +```cpp +typedef struct AuditData { + AuditMsgHdr header; /* 记录文件头,存储记录的表示、大小等 */ + AuditType type; /* 审计类型 */ + AuditResult result; /* 执行结果 */ + char varstr[1]; /* 二进制格式存储的具体审计信息 */ +} AuditData; +``` + +然后我们展开分析其中的数据结构 + +**AuditMsgHdr、AuditType、AuditResult** + +#### AuditMsgHdr + +```cpp +typedef struct AuditMsgHdr { + char signature[2]; /* 审计记录标识,目前固定为AUDIT前两个字符'A'和'U' */ + uint16 version; /* 版本信息,值为0 */ + uint16 fields; /* 审计记录字段数,值为13 */ + uint16 flags; /* 记录有效性标识,如果被删除则标记位DEAD */ + pg_time_t time; /* 审计记录创建时间 */ + uint32 size; /* 审计信息占字节长度 */ +} AuditMsgHdr; +``` + + + +#### AuditType + +都是一些常数,体现在日志的type字段 + +```cpp +typedef enum { + AUDIT_UNKNOWN_TYPE = 0, + AUDIT_LOGIN_SUCCESS, + AUDIT_LOGIN_FAILED, + AUDIT_USER_LOGOUT, + AUDIT_SYSTEM_START, + AUDIT_SYSTEM_STOP, + ... +} AuditType; +``` + + + +#### AuditResult + +体现在result字段中 + +```cpp +typedef enum { AUDIT_UNKNOWN = 0, AUDIT_OK, AUDIT_FAILED } AuditResult; +``` + + + +#### 示例中的其他问题 + +再回过头来看**审计日志单条信息示例** + +```sql +time | 时间 +type | 类型 +result | 结果 +userid | 用户id +username | 用户名 +database | 数据库名 +client_connifo | 客户端连接信息 +object_name | 目标名 +detail_info | 目标详细信息 +node_name | 结点名 +thread_id | 线程号 +local_port | 本地端口号 +remote_port | 远程端口号 +``` + + + +那么除了前面的三个字段,**其他字段**在哪呢? + +我们在pgaudit.cpp文件找到这一段函数 + +```cpp +static void deserialization_to_tuple(Datum (&values)[PGAUDIT_QUERY_COLS], + AuditData *adata, + const AuditMsgHdr &header) +{ + /* append timestamp info to data tuple */ + int i = 0; + values[i++] = TimestampTzGetDatum(time_t_to_timestamptz(adata->header.time)); + values[i++] = CStringGetTextDatum(AuditTypeDesc(adata->type)); + values[i++] = CStringGetTextDatum(AuditResultDesc(adata->result)); + + /* + * new format of the audit file under correct record + * the older audit file do not have userid info, so let it to be null + */ + int index_field = 0; + const char* field = NULL; + bool new_version = (header.fields == PGAUDIT_QUERY_COLS); + field = new_version ? pgaudit_string_field(adata, index_field++) : NULL; + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* user id */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* user name */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* dbname */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* client info */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* object name */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* detail info */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* node name */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* thread id */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* local port */ + field = pgaudit_string_field(adata, index_field++); + values[i++] = CStringGetTextDatum(FILED_NULLABLE(field)); /* remote port */ + + Assert(i == PGAUDIT_QUERY_COLS); +} +``` + +可以看到,后面的所有字段都是在这里被加入的 + + + +#### 日志组的设计 + +我们已经了解单条审计信息的字段格式 + +那么数据库操作中大量的日志又是**如何进行索引**的呢? + +Answer:这里采用了指针的结构。 + +首先有**索引表** + +> ​ [ 索引表 ] +> +> 索引表头|| 索引元素1 | 索引元素2 | 索引元素3 | ...... + + + +然后由**每个索引元素**链接到**对应的审计文件表**中 + +> ​ [ 审计文件表 ] +> +> 审计记录1 | 审计记录2 | 审计记录3 | ...... + + + +最后每个审计文件表中的结构就和之前示例中的相一致啦 + +> ​ [ 审计记录 ] +> +> 审计记录头 | 审计类行 | 审计结果 | 具体审计数据 + +如图所示: + +![Shenji1](https://forum.gitlink.org.cn/api/attachments/398552) + +然后让我们联系代码去看一看 + + + +#### 联系代码 + +首先看**索引表的结构体** + +```cpp +typedef struct AuditIndexTable { + uint32 maxnum; /* 表中可存储的审计文件的最大数量 */ + uint32 begidx; /* 首个审计文件的位置 */ + uint32 curidx; /* 当前操作的审计文件的位置 */ + uint32 count; /* 当前表中存储的审计文件的数量 */ + pg_time_t last_audit_time; /* 最后一次写入审计记录的时间 */ + AuditIndexItem data[1]; /* 审计文件指针 */ +} AuditIndexTable; +``` + + + + + +然后我们继续追踪**审计文件的结构体**AuditIndexItem + +```cpp +typedef struct AuditIndexItem { + pg_time_t ctime; /* 审计文件创建时间 */ + uint32 filenum; /* 审计文件的名 */ + uint32 filesize; /* 审计文件的大小 */ +} AuditIndexItem; +``` + + + +这里以审计记录的写入函数audit_report为例 + +```cpp +/* + * Brief : 向系统审计员报告审计信息 + * Description : 由后端调用,通常以一下过程进行 + * 1. 验证类型和进程,决定是否去报告审计 + * 2. 从连接中得到所有审计信息 + * 3. 添加审计信息到字符缓冲区中 + * 4. 最后,写入审计文件或者发送给审计员进程去处理 + */ + +void audit_report(AuditType type, AuditResult result, const char *object_name, const char *detail_info, + AuditClassType ctype) +{ + /* 检查进程状态 */ + if (!audit_status_check_ok() || (detail_info == NULL)) { + return; + } + + /* 检查审计类型 */ + if (!audit_type_validcheck(type)) { + return; + } + + /* 从端口中得到信息 */ + StringInfoData buf; + AuditData adata; + AuditEventInfo event_info; + if (!audit_get_clientinfo(type, object_name, event_info)) { + return; + } + char *userid = event_info.userid; // 用户id + const char* username = event_info.username; // 用户名 + const char* dbname = event_info.dbname; // 数据库名 + char* client_info = event_info.client_info; // 客户端信息 + char* threadid = event_info.threadid; // 进程号 + char* localport = event_info.localport; // 本地端口 + char* remoteport = event_info.remoteport; // 远程端口 + + /* append xid info when audit_xid_info = 1 */ + char *detail_info_xid = NULL; + bool audit_xid_info = (u_sess->attr.attr_security.audit_xid_info == 1); + if (audit_xid_info) { + uint32 len = uint64_max_len + strlen("xid=, ") + strlen(detail_info) + 1; + detail_info_xid = (char *)palloc0(len); + audit_append_xid_info(detail_info, detail_info_xid, len); + } + + /* 数据头赋值 */ + adata.header.signature[0] = 'A'; + adata.header.signature[1] = 'U'; + adata.header.version = 0; + adata.header.fields = PGAUDIT_QUERY_COLS; + adata.header.flags = AUDIT_TUPLE_NORMAL; + adata.header.time = current_timestamp(); + adata.header.size = 0; + adata.type = type; + adata.result = result; + initStringInfo(&buf); + appendBinaryStringInfo(&buf, (char*)&adata, AUDIT_HEADER_SIZE); + + /* 审计数据赋值 */ + appendStringField(&buf, userid); + appendStringField(&buf, username); + appendStringField(&buf, dbname); + appendStringField(&buf, (client_info[0] != '\0') ? client_info : NULL); + appendStringField(&buf, object_name); + appendStringField(&buf, (!audit_xid_info) ? detail_info : detail_info_xid); + appendStringField(&buf, g_instance.attr.attr_common.PGXCNodeName); + appendStringField(&buf, (threadid[0] != '\0') ? threadid : NULL); + appendStringField(&buf, (localport[0] != '\0') ? localport : NULL); + appendStringField(&buf, (remoteport[0] != '\0') ? remoteport : NULL); + + /* + * Use the chunking protocol if we know the syslogger should be + * catching stderr output, and we are not ourselves the syslogger. + * Otherwise, just do a vanilla write to stderr. + */ + if (WRITE_TO_AUDITPIPE) { + write_pipe_chunks(buf.data, buf.len, ctype); + } else if (WRITE_TO_STDAUDITFILE(ctype)) { + pgaudit_write_file(buf.data, buf.len); + } else if (WRITE_TO_UNIAUDITFILE(ctype)) { + pgaudit_write_policy_audit_file(buf.data, buf.len); + } else if (detail_info != NULL) { + ereport(LOG, (errmsg("discard audit data: %s", (!audit_xid_info) ? detail_info : detail_info_xid))); + } + + if (detail_info_xid != NULL) { + pfree(detail_info_xid); + } + pfree(buf.data); +} +``` + + + +### 审计执行的原理 + +openGauss提供对用户发起的SQL行为审计和追踪能力,支持对DDL、DML语句和关键行为的审计。 + +> 简单介绍一下DDL语句与DML语句: +> +> DML(Data Manipulation Language)数据操纵语言: +> +> 适用范围:对数据库中的数据进行一些简单操作,如insert,delete,update,select等. +> +> +> +> DDL(Data Definition Language)数据定义语言: +> +> 适用范围:对数据库中的某些对象(例如,database,table)进行管理,如Create,Alter和Drop. +> + +工作线程初始化时便加载了审计模块,审计的执行原理是将审计函数赋给SQL生命周期不同阶段的HOOK函数。 + +同样在阅读代码之后,定位到了加载审计模块的关键函数**pgaudit_agent_init( )** + +```cpp +/* + * Brief : perfstat_agent_init() + * Description : Module load callback. + * Notes : Called from postmaster. + */ +void pgaudit_agent_init(void) +{ + if (!IsPostmasterEnvironment || !u_sess->attr.attr_security.Audit_enabled || + u_sess->exec_cxt.g_pgaudit_agent_attached) { + return; + } + prev_ExecutorEnd = ExecutorEnd_hook; + ExecutorEnd_hook = pgaudit_ExecutorEnd; + set_pgaudit_prehook(ProcessUtility_hook); + ProcessUtility_hook = (ProcessUtility_hook_type)pgaudit_ProcessUtility; + u_sess->exec_cxt.g_pgaudit_agent_attached = true; +} +``` + +SQL语句在指向到ProcessUtility_hook和ExecutorEnd_hook 函数指针时,会分别进入预置好的审计流程中。 + +这两个函数指针的位置在SQL进入执行器执行之前,具体关系如图: + +![Shenji2](https://forum.gitlink.org.cn/api/attachments/398553) + +在计划期到执行器的途中,生成了计划树,此时审计模块则调用pgaudit_ExecutorEnd和pgaudit_ProcessUtility函数分别对DML语句和DDL语句进行分析。 + +*** + +### 总结 + +本文概述了openGauss安全机制中的审计与追踪,主要包括审计日志的设计以及审计执行的原理。 + +通过审计的的确确有效地维护了数据库的安全。但对于审计文件而已,过于依赖了操作系统对文件的保护,是openGuass仍然可以改进的地方。 diff --git "a/TestTasks/KuanXY/openGauss\345\256\211\345\205\250\346\234\272\345\210\266\344\271\213\350\247\222\350\211\262\345\210\233\345\273\272\343\200\201\347\256\241\347\220\206\344\270\216\350\256\244\350\257\201.md" "b/TestTasks/KuanXY/openGauss\345\256\211\345\205\250\346\234\272\345\210\266\344\271\213\350\247\222\350\211\262\345\210\233\345\273\272\343\200\201\347\256\241\347\220\206\344\270\216\350\256\244\350\257\201.md" new file mode 100644 index 0000000000000000000000000000000000000000..c76c0d584f420f560f3b42b5638828f51a752e6c --- /dev/null +++ "b/TestTasks/KuanXY/openGauss\345\256\211\345\205\250\346\234\272\345\210\266\344\271\213\350\247\222\350\211\262\345\210\233\345\273\272\343\200\201\347\256\241\347\220\206\344\270\216\350\256\244\350\257\201.md" @@ -0,0 +1,1082 @@ +## openGauss安全机制之角色创建、管理与认证 + +[TOC] + +### 前言 + +本文《openGauss安全机制之角色创建、管理与认证》为第五届中国软件开源创新大赛——开源代码评注赛道参赛作品 + +作者:袁建硕 + +所属战队:共同学习 + +时间:2022-09-28 + +评注代码:[opengauss-mirror/openGauss-server: openGauss kernel (github.com)](https://github.com/opengauss-mirror/openGauss-server) + +*** + +### 角色创建与角色管理概述 + +数据库中,用户要想访问具体的数据库对象,需要各类复杂的权限,而如果采用逐个权限授予用户的方式,则会非常麻烦并且容易出错,不利于权限的管理。因此openGauss采用基于角色进行权限的管理,这种方式则能够实现权限的高效管理。 + +>什么是角色? +> +>角色是一组权限的集合,可以被赋予指定的权限并分配给用户,拥有角色的用户也就拥有了角色对应的权限。(注意:角色没有登陆权限,无法通过角色登录数据库) + +在数据库中,各种各样的角色拥有不同的权限,不同的操作,当对角色进行权限的授予和撤销时将会影响拥有该角色的所有用户。因此,对角色的区分管理及安全管控十分重要。 + +*** + +### 角色创建 + +角色是拥有数据库对象和权限的实体,在不同的环境中角色可以是一个用户、一个组或者两者均有。 + +在openGuass上创建一个角色,可以使用SQL命令CREATE ROLE + +```sql +CREATE ROLE role_name [ [ WITH ] option [ ... ] ] [ ENCRYPTED | UNENCRYPTED ] { PASSWORD | IDENTIFIED BY } { 'password' | DISABLE}; +``` + + + +创建角色调用函数CreateRole实现 + +```cpp +/* gausskernel/optimizer/commands/user.cpp: 548-1555 */ +void CreateRole(CreateRoleStmt* stmt) +{ + ... +} +``` + + + +其传入参数的结构体为: + +```cpp +typedef struct CreateRoleStmt { + NodeTag type; + RoleStmtType stmt_type; /* ROLE/USER/GROUP 创建角色的类型 角色/用户/组用户 */ + char* role; /* role name 角色名称 */ + List* options; /* List of DefElem nodes 角色属性列表,为一个链表结构 */ +} CreateRoleStmt; +``` + + + +然后查看函数声明中出现频率最高的结构体DefElem + +```cpp +typedef struct DefElem { + NodeTag type; + char *defnamespace; /* 节点对应的命名空间 */ + char *defname; /* 节点对应的角色属性名 */ + Node *arg; /* 表示值或类型名 */ + DefElemAction defaction; /* SET/ADD/DROP等其他未指定的行为 */ + int begin_location; /* token begin location, or -1 if unknown */ + int end_location; /* token end location, or -1 if unknown */ + int location; +} DefElem; +``` + + + +#### 创建流程 + +流程如图所示: + + +![](https://forum.gitlink.org.cn/api/attachments/398614) + +##### 1.判断创建的角色类型 + +仍然是函数**CreateRole**函数中 + +```cpp +switch (stmt->stmt_type) { + case ROLESTMT_ROLE: + break; + case ROLESTMT_USER: + canlogin = true; + break; + case ROLESTMT_GROUP: + break; + default: + break; + } +``` + +当所创建的角色类型为用户时,将canlogin设置为true,这是因为用户默认具有登录权限。 + + + +##### 2. 循环获取角色属性options + +CreateRole: 657-894 + +从源码中可以看到 + +大量使用了strcmp判断各类属性的是否存在以及其value + +包括password口令、encryptedPassword加密口令、sysid系统号等字段 + +```cpp +/* 从Node tree中获取option */ +foreach (option, stmt->options) +{ + ... +} +``` + + + +##### 3.将获取的属性转换成对应的数据类型 + +CreateRole: 896-1166 + +采用了大量的if判断结构将对应的参数信息转换为需要的角色属性值类型 + +```cpp +... + if (dinherit != NULL) + inherit = intVal(dinherit->arg) != 0; + if (dcreaterole != NULL) + createrole = intVal(dcreaterole->arg) != 0; + if (dcreatedb != NULL) + createdb = intVal(dcreatedb->arg) != 0; + if (duseft != NULL) + useft = intVal(duseft->arg) != 0; + if (dcanlogin != NULL) + canlogin = intVal(dcanlogin->arg) != 0; + if (disreplication != NULL) + isreplication = intVal(disreplication->arg) != 0; +... +``` + + + +##### 4. 将要创建的角色属性值构建pg_authid元组 + +CreateRole: 1168-1178 + +```cpp +/* + * 检查pg_authid是否已经存在 + */ + Relation pg_authid_rel = heap_open(AuthIdRelationId, RowExclusiveLock); + TupleDesc pg_authid_dsc = RelationGetDescr(pg_authid_rel); + + if (OidIsValid(get_role_oid(stmt->role, true))) { + str_reset(password); + ereport(ERROR, (errcode(ERRCODE_DUPLICATE_OBJECT), errmsg("role \"%s\" already exists", stmt->role))); + } +``` + +之后就是检验时间戳是否超时以及对口令的再次检查 + +CreateRole: 1233-1371 + +创建一个插入的元组 + +```cpp +/* + * Build a tuple to insert + */ + errno_t errorno = memset_s(new_record, sizeof(new_record), 0, sizeof(new_record)); + securec_check(errorno, "\0", "\0"); + errorno = memset_s(new_record_nulls, sizeof(new_record_nulls), false, sizeof(new_record_nulls)); + securec_check(errorno, "\0", "\0"); + + new_record[Anum_pg_authid_rolname - 1] = DirectFunctionCall1(namein, CStringGetDatum(stmt->role)); + + new_record[Anum_pg_authid_rolsuper - 1] = BoolGetDatum(issuper); + new_record[Anum_pg_authid_rolinherit - 1] = BoolGetDatum(inherit); + new_record[Anum_pg_authid_rolcreaterole - 1] = BoolGetDatum(createrole); + new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(createdb); + ... +``` + + + +##### 5. 将tuple写入系统表并更新 + +CreateRole: 1372-1413 + +```cpp + HeapTuple tuple = heap_form_tuple(pg_authid_dsc, new_record, new_record_nulls); + + if (u_sess->proc_cxt.IsBinaryUpgrade && OidIsValid(u_sess->upg_cxt.binary_upgrade_next_pg_authid_oid)) { + HeapTupleSetOid(tuple, u_sess->upg_cxt.binary_upgrade_next_pg_authid_oid); + u_sess->upg_cxt.binary_upgrade_next_pg_authid_oid = InvalidOid; + } + + /* + * Insert new record in the pg_authid table + */ + roleid = simple_heap_insert(pg_authid_rel, tuple); + + /* add dependency of roleid on rpoid, no need add dependency on default_pool */ + if (IsUnderPostmaster) { + if (OidIsValid(rpoid) && (rpoid != DEFAULT_POOL_OID)) + recordDependencyOnRespool(AuthIdRelationId, roleid, rpoid); + + u_sess->wlm_cxt->wlmcatalog_update_user = true; + } + ... +``` + + + +##### 6. 将新创建的角色加入指定存在的父角色中 + +CreateRole: 1414-1430 + +```cpp +/* + * Add the new role to the specified existing roles. + */ + foreach (item, addroleto) { + char* oldrolename = strVal(lfirst(item)); + Oid oldroleid = get_role_oid(oldrolename, false); + + AddRoleMems( + oldrolename, oldroleid, list_make1(makeString(stmt->role)), list_make1_oid(roleid), GetUserId(), false); + } + + /* + * Add the specified members to this new role. adminmembers get the admin + * option, rolemembers don't. + */ + AddRoleMems(stmt->role, roleid, adminmembers, roleNamesToIds(adminmembers), GetUserId(), true); + AddRoleMems(stmt->role, roleid, rolemembers, roleNamesToIds(rolemembers), GetUserId(), false); +``` + + + +到此为止,完成了整个角色创建的过程 + + + +### 角色管理 + +角色管理主要包括以下内容: + ++ 修改角色属性 ++ 删除角色 ++ 授予和回收角色 + + + +#### 修改角色属性 + +SQL语句 ALTER ROLE修改数据库角色 + +通过调用AlterRole函数来实现角色属性的修改 + +查看关键结构体AlterRoleStmt + +```cpp +typedef struct AlterRoleStmt { + NodeTag type; + char* role; /* 角色名称 */ + List* options; /* 需要修改的属性列表 */ + int action; /* +1 = 增加成员关系, -1 = 删除成员关系 */ + RoleLockType lockstatus; /* 角色锁定状态 */ +} AlterRoleStmt; +``` + +大致流程为: + +AlterRole → 循环提取要修改的属性options → 转换为对应的数据类型 → 判断角色是否存在 → 判断角色是否有修改的权限 → 更新后的属性值同步到新元组中 → 新元组替代旧元组 → 判断成员关系action → 关闭系统表 → 结束 + + + +其结构与**角色创建**时大同小异 + +这里主要分析后两次判断: + +- 判断角色是否有修改的权限 +- 判断成员关系action + + + +**判断角色是否有修改的权限:** + +AlterRole:2258-2357 + +不同操作要求的权限不同,权限不足会报错提示 + +```cpp +/* Database Security: Support separation of privilege.*/ + if (roleid == BOOTSTRAP_SUPERUSERID) { + if (!(issuper < 0 && inherit < 0 && createrole < 0 && createdb < 0 && canlogin < 0 && isreplication < 0 && + isauditadmin < 0 && issystemadmin < 0 && ismonitoradmin < 0 && isoperatoradmin < 0 && + ispolicyadmin < 0 && isvcadmin < 0 && useft < 0 && ispersistence < 0 && dconnlimit == NULL && + rolemembers == NULL && validBegin == NULL && validUntil == NULL && drespool == NULL && + dparent == NULL && dnode_group == NULL && dspacelimit == NULL && dtmpspacelimit == NULL && + dspillspacelimit == NULL)) { + str_reset(password); + str_reset(replPasswd); + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("Permission denied to change privilege of the initial account."))); + } + } + ... +``` + + + +**判断成员关系action:** + +AlterRole:2999-3002 + +```cpp + if (stmt->action == +1) /* add members to role */ + AddRoleMems(stmt->role, roleid, rolemembers, roleNamesToIds(rolemembers), GetUserId(), false); + else if (stmt->action == -1) /* drop members from role */ + DelRoleMems(stmt->role, roleid, rolemembers, roleNamesToIds(rolemembers), false); +``` + + + + + +#### 删除角色 + +SQL命令:DROP ROLE + +通过函数DropRole实现,其对应结构体DropRoleStmt + +```cpp +typedef struct DropRoleStmt { + NodeTag type; + List* roles; /* 要删除的角色列表 */ + bool missing_ok; /* 判断角色是否存在 */ + bool is_user; /* 删除的时角色还是用户 */ + bool inherit_from_parent; /* 是否继承父角色 */ + DropBehavior behavior; /* 是否级联删除依赖对象 */ +} DropRoleStmt; +``` + +删除的大致流程为: + +判断是否有删除角色的权限 → 检查要删除的角色是否存在 → 再次检查是否有权删除此角色 → 此角色是否有依赖对象 → 删除pg_authid中对应的元组,删除pg_auth_member中相关元组 → 关闭系统表 → 结束 + + + +**注意两次删除权限的判断**,第一次是当前角色是否可以执行删除的操作,第二次是当前角色是否有权限删除目标角色 + +从逻辑上讲,也可以先循环处理删除的角色,判断要删除角色是否存在,再依次进行两次判断,但这样操作的话明显增加了复杂度,属于是没有必要 + + + +user.cpp : 3098-3383 + +函数have_createrole_privilege()检查是否有权删除 + +下面这段代码校验执行者和被删除角色的权限 + +```cpp +if ((((Form_pg_authid)GETSTRUCT(tuple))->rolsuper || ((Form_pg_authid)GETSTRUCT(tuple))->rolsystemadmin) && + !isRelSuperuser()) + ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("Permission denied."))); + + Datum datum = heap_getattr(tuple, Anum_pg_authid_roloperatoradmin, pg_authid_dsc, &isNull); + if (!isNull) { + is_opradmin = DatumGetBool(datum); + } else if (roleid == BOOTSTRAP_SUPERUSERID) { + is_opradmin = true; + } + if (is_opradmin && !initialuser()) { + ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("Permission denied."))); + } + + /* Forbid createrole holders to drop auditadmin when PrivilegesSeparate enabled. */ + if ((((Form_pg_authid)GETSTRUCT(tuple))->rolauditadmin) && + g_instance.attr.attr_security.enablePrivilegesSeparate && !isRelSuperuser()) + ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("Permission denied."))); + +``` + + + +针对级联的情况,删除该角色拥有的对象 + +```cpp + /* + * Drop the objects owned by the role if its behavior mod is CASCADE + */ + if (stmt->behavior == DROP_CASCADE) { + char* user = NULL; + + CancelQuery(role); + user = (char*)palloc(sizeof(char) * strlen(role) + 1); + errno_t errorno = strncpy_s(user, strlen(role) + 1, role, strlen(role)); + securec_check(errorno, "\0", "\0"); + drop_objectstmt.behavior = stmt->behavior; + drop_objectstmt.type = T_DropOwnedStmt; + drop_objectstmt.roles = list_make1(makeString(user)); + + DropOwnedObjects(&drop_objectstmt); + list_free_deep(drop_objectstmt.roles); + } +``` + + + +检查是否有对象依赖于该角色,如果有则提示报错 + +```cpp +/* Check for pg_shdepend entries depending on this role */ + if (checkSharedDependencies(AuthIdRelationId, roleid, &detail, &detail_log)) + ereport(ERROR, + (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST), + errmsg("role \"%s\" cannot be dropped because some objects depend on it", role), + errdetail_internal("%s", detail), + errdetail_log("%s", detail_log))); +``` + + + +#### 授予和回收角色 + +SQL命令:GRANT/REVOKE + +通过函数GrantRole来实现 + +结构体GrantRoleStmt + + + +```cpp +typedef struct GrantRoleStmt { + NodeTag type; + List* granted_roles; /* 被授予或回收的角色集合 */ + List* grantee_roles; /* 从granted_roles中增加或删除的角色集合 */ + bool is_grant; /* true = GRANT授权, false = REVOKE回收 */ + bool admin_opt; /* 是否带有admin选项 */ + char* grantor; /* 授权者 */ + DropBehavior behavior; /* 是否级联回收角色 */ +} GrantRoleStmt; +``` + +大致流程: + +检查有权添加/删除角色成员 → 循环处理要添加/删除的角色 → 判断添加的成员是否已属于此角色 → 创建/修改pg_auth_member元组 → 新元组插入系统表 → 关闭系统表 → 结束 + +对角色的操作都大差不差,这里不过多介绍 + + + + + +### 身份认证简要逻辑 + +在了解了角色创建以及角色管理的流程后,让我们继续分析openGuass是如何对身份进行认证的。 + +openGauss的访问规则在配置文件HBA(Host-Based Authentication,主机认证)中。 + +格式为: + +```sql +hostssl DATABASE USER ADDRESS METHOD [OPTIONS] +``` + +第一个字段代表套接字方法 + +第二个字段代表被允许访问的数据库 + +第三个字段代表允许访问的用户 + +第四个字段代表允许访问的IP地址 + +第五个字段代表访问的认证方式 + +第六个字段对第五个字段认证信息的补充 + +自左向右,访问需求规则的优先级逐渐降低 + +源码如下: + +```c +/* src/include/lib/hba.h: 39-66 */ +typedef struct HbaLine { + int linenumber; /* 规则行号 */ + ConnType conntype; /* 连接套接字方法 */ + List* databases; /* 允许访问的数据库集合 */ + List* roles; /* 允许访问的用户组 */ + ... + char* hostname; /* 允许访问的IP地址 */ + UserAuth auth_method; /* 认证方法 */ + ... +} HbaLine; +``` + +HBA文件在系统管理员配置完成后存放在数据库服务侧。 + +当用户通过数据库用户发起的认证请求时,信息被存放在数据结构Port中 + +源码如下: + +```C +/* src/include/libpq/libpq-be.h: 98-204 */ + SockAddr laddr; /* 本地进程IP地址信息 */ + SockAddr raddr; /* 远程客户端进程IP地址信息 */ + char* remote_host; /* 远端主机名称字符串或IP地址 */ + char* remote_hostname; /* (可选)远程主机名称字符串或IP地址 */ + + /* 发送给后端的数据包信息,包括访问的数据库名称、用户名、配置参数 */ + char* database_name; + char* user_name; + char* cmdline_options; + List* guc_options; + + /* 认证规则信息 */ + HbaLine* hba; + ... + + /* SSL, 安全套接层 */ +#ifdef USE_SSL + SSL* ssl; + X509* peer; + char* peer_cn; + unsigned long count; +#endif + .... + + /* Kerberos 认证数据结构信息 */ +#ifdef ENABLE_GSS + char* krbsrvname; /* Kerberos服务进程名称 */ + gss_ctx_id_t gss_ctx; /* GSS(Generic Security Service, 通用安全服务)数据内容 */ + gss_cred_id_t gss_cred; /* GSS 凭证信息 */ + gss_name_t gss_name; /* GSS target name */ + gss_buffer_desc gss_outbuf; /* GSS token信息 */ +#endif +} Port; +``` + + + +在得到Port信息后,后台服务线程会根据前端传入的信息与**HbaLine**中记录的信息逐一比较,完成对应的身份识别。 + +在**check_hba函数**中,可以看到身份认证的核心逻辑代码: + +```c +/* src/common/backend/libpq/hba.cpp: 1523-1716 */ +/* */ +/* 扫描HBA文件,寻找匹配连接请求的规则项 */ +static void check_hba(hbaPort* port) +{ + ... + /* 获取目标用户的ID号 */ + roleid = get_role_oid(port->user_name, true); + ... + + /* */ + foreach (line, g_instance.libpq_cxt.comm_parsed_hba_lines) { + errno_t rc = memcpy_s(hba, sizeof(HbaLine), lfirst(line), sizeof(HbaLine)); + securec_check(rc, "\0", "\0"); + /* 检查连接类型,考虑时本地连接行为还是远程连接行为 */ + if (hba->conntype == ctLocal) { + /* 对于local套接字,仅允许初始安装用户本地登录 */ + if (roleid == INITIAL_USER_ID) { + uid_t uid = 0; + gid_t gid = 0; + /* 得到当前系统用户名,基于本地uid */ + if (getpeereid(port->sock, &uid, &gid) != 0) { + pfree_ext(hba); + ... + } + + /* + * For connections from another coordinator, we could not + * get the userid. This case may also exist for other cases, + * like tools. (Usually happed when login with -h localhost + * or 127.0.0.1) + */ + if ((long)uid == USER_NULL_MASK) { + continue; + } + /* 对照用户名,如果不匹配,设置为uaTrust不信任模式 */ + isUsernameSame= IsSysUsernameSameToDB(uid, port->user_name); + if (!isUsernameSame && hba->auth_method == uaTrust) { + hba->auth_method = get_default_auth_method(port->user_name); + } + } else if (hba->auth_method == uaTrust || hba->auth_method == uaPeer) { + /* 访问用户与本地系统用户不相匹配的场景,需要提供密码 */ + hba->auth_method = get_default_auth_method(port->user_name); + } + + if (!IS_AF_UNIX(port->raddr.addr.ss_family)) + continue; + } else { /* 如果时远程访问行为,则需要逐条判断炮廓认证方式在内的信息的正确性 */ + if (IS_AF_UNIX(port->raddr.addr.ss_family)) + continue; + + /* 检查SSL状态 */ +#ifdef USE_SSL + if (port->ssl != NULL) { + if (hba->conntype == ctHostNoSSL) + continue; + } else { + if (hba->conntype == ctHostSSL) + continue; + } +#else + if (hba->conntype == ctHostSSL) + continue; +#endif + + /* IP白名单校验 */ + switch (hba->ip_cmp_method) { + case ipCmpMask: + if (hba->hostname != NULL) { + if (!check_hostname(port, hba->hostname)) + continue; + } else { + if (!check_ip(&port->raddr, (struct sockaddr*)&hba->addr, (struct sockaddr*)&hba->mask)) + continue; + } + break; + case ipCmpAll: + break; + case ipCmpSameHost: + case ipCmpSameNet: + if (!check_same_host_or_net(&port->raddr, hba->ip_cmp_method)) + continue; + break; + default: + /* shouldn't get here, but deem it no-match if so */ + continue; + } + } /* != ctLocal */ + + /* 校验数据库信息和用户信息 */ + if (!check_db(port->database_name, port->user_name, roleid, hba->databases)) + continue; + + if (!check_role(port->user_name, roleid, hba->roles)) + continue; + + /* 记录相关信息以及继续执行校验 */ + if (hba->conntype != ctLocal) { + /* + * 通常,带有信任模式的远程连接请求是被禁止的 + * 但有一些例外场景: + * 1 由协调器和内部维护工具创建的连接请求 + * -- 信任模式被允许,在process_startup_options函数中实现了对内部组的检查 + * 2 本地环回连接 + * -- 密码被增强 + * 3 连接是由到协调器的组产生的非初始用户 + * -- 密码被增强 + */ + if (hba->auth_method == uaTrust) { + if (IsConnPortFromCoord(port) || u_sess->proc_cxt.IsInnerMaintenanceTools) { + /* 第一种情况,直接跳过 */ + } else if (IsLoopBackAddr(port)) { + /* 第二种情况, 本地环回连接, hba->remote_trust 设置为false */ + hba->auth_method = get_default_auth_method(port->user_name); + hba->remoteTrust = false; + } else if (roleid != INITIAL_USER_ID && IS_PGXC_COORDINATOR && is_cluster_internal_connection(port)) { + /* + * 第三种情况 + * 因为在成功认证之前,我们不能通过 pgxc_node 节点。 + * 我们能在重新连接时得到一个过度保守的内部组判断 + * 然而,这些情况不应该时问题,因为周期性地本地cm_agent连接很亏就会在共享内存中填充节点信息 + */ + hba->auth_method = get_default_auth_method(port->user_name); + hba->remoteTrust = false; + } else { + ConnAuthMethodCorrect = false; + pfree_ext(hba); + ereport(FATAL, + (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), + errmsg("Forbid remote connection with trust method!"))); + } + } + } + +#ifdef USE_IAM + /* 当使用带有SHA256/MD5/SM3加密时地iam用户去避免冲突时时,更改内部认证方式到IAM */ + if (isIAM && (hba->auth_method == uaSHA256 || hba->auth_method == uaMD5 || hba->auth_method == uaSM3)) { + hba->auth_method = uaIAM; + } +#endif +#if defined(ENABLE_GSS) || defined(ENABLE_SSPI) + /* 对于非初始化地用户, krb authentication是不被允许的, sha256 请求将会被通过. */ + if (hba->auth_method == uaGSS && roleid != INITIAL_USER_ID && IsConnFromApp()) { + hba->auth_method = get_default_auth_method(port->user_name); + } +#endif + isMatched = true; + break; + } + + /* 无匹配规则,直接拒绝当前连接请求 */ + if (!isMatched) { + hba->auth_method = uaImplicitReject; + } + copy_hba_line(hba); + port->hba = hba; + PG_END_ENSURE_ERROR_CLEANUP(hba_rwlock_cleanup, (Datum)0); + (void)pthread_rwlock_unlock(&hba_rwlock); +} +``` + + + + + +### 认证机制核心流程 + +在梳理了身份验证的基本逻辑之后,我们再去追溯一下认证机制的核心流程 + +大致流程如图: + + +![](https://forum.gitlink.org.cn/api/attachments/398615) + +#### 线程会话初始化入口 + +**执行函数:InitSession( )** + +代码位置: + +src\gausskernel\process\threadpool\threadpool_worker.cpp: 778-892 + +**传参:session** + +接受用户的身份信息session,切换至对应的空间地址mem + +```cpp +AutoContextSwitch memSwitch(session->mcxt_group->GetMemCxtGroup(MEMORY_CONTEXT_DEFAULT)); +``` + + + +更新工作版本Working version + +```cpp +t_thrd.proc->workingVersionNum = pg_atomic_read_u32(&WorkingGrandVersionNum); +``` + + + +初始化GUC(Grand Unified Configuration),并读取相关内容 + +```cpp +InitializeGUCOptions(); +read_nondefault_variables(); +``` + + + +安全地向用户端client发送错误信息 + +```cpp +t_thrd.postgres_cxt.whereToSendOutput = DestRemote; +// 思考在781行为什么设置暂不发送? +// t_thrd.postgres_cxt.whereToSendOutput = DestNone; +// 这里猜测是输出的状态信息在验证session的过程中会通过注入等方式泄露高权限用户的信息 +``` + + + +初始化端口号和连接 + +```cpp +if (!InitPort(session->proc_cxt.MyProcPort)) { + ... + } +``` + + + +再次切换工作版本号(从端口中得到的新版本号,why?) + +```cpp +t_thrd.proc->workingVersionNum = session->proc_cxt.MyProcPort->SessionVersionNum; +``` + + + +增加进程定义模式 + +```cpp + Reset_Pseudo_CurrentUserId(); + + SetProcessingMode(InitProcessing); + + SessionSetBackendOptions(); +``` + + + +允许SIGINT的系统调用去中止初始化程序 + +``` +gs_signal_setmask(&t_thrd.libpq_cxt.UnBlockSig, NULL); +``` + + + +初始化合法消息系统、状态、文件存储读取和流管理 + +```cpp + SharedInvalBackendInit(false, true); + pgstat_initialize_session(); + InitFileAccess(); + smgrinit(); +``` + + + +初始化openguass + +```cpp + char* dbname = session->proc_cxt.MyProcPort->database_name; + char* username = session->proc_cxt.MyProcPort->user_name; + t_thrd.proc_cxt.PostInit->SetDatabaseAndUser(dbname, InvalidOid, username); + t_thrd.proc_cxt.PostInit->InitSession(); +``` + + + +多节点情况下,执行操作 + +```cpp +#ifndef ENABLE_MULTIPLE_NODES + if (u_sess->proc_cxt.MyDatabaseId != InvalidOid && DB_IS_CMPT(B_FORMAT)) { + if (!u_sess->attr.attr_sql.dolphin) { + LoadDolphinIfNeeded(); + } else { + InitBSqlPluginHookIfNeeded(); + } + } else if (u_sess->proc_cxt.MyDatabaseId != InvalidOid && DB_IS_CMPT(A_FORMAT) && u_sess->attr.attr_sql.whale) { + InitASqlPluginHookIfNeeded(); + } +#endif +``` + + + +初始化哈希hash表 + +```cpp +if (IS_PGXC_COORDINATOR) { + init_set_params_htab(); + } +``` + + + +检查存储 + +```cpp +if (t_thrd.utils_cxt.gs_mp_inited && processMemInChunks > maxChunksPerProcess) { + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_LOGICAL_MEMORY), + errmsg("memory usage reach the max_dynamic_memory"), + errdetail("current memory usage is: %u MB, max_dynamic_memory is: %u MB", + (unsigned int)processMemInChunks << (chunkSizeInBits - BITS_IN_MB), + (unsigned int)maxChunksPerProcess << (chunkSizeInBits - BITS_IN_MB)))); + } + + ReadyForQuery((CommandDest)t_thrd.postgres_cxt.whereToSendOutput); +``` + + + +#### 连接认证总入口 + +**执行函数:PerformAuthentication( )** + +代码位置 + +src\common\backend\utils\init\postinit.cpp: 287-344 + +**传参:port** + +设置时效性,以防止意外bug + +```cpp +if (!enable_sig_alarm(u_sess->attr.attr_security.AuthenticationTimeout * 1000, true)) + ereport(FATAL, (errmsg("could not set timer for authorization timeout"))); +``` + + + +解锁SIGUSR2权限使触发SIGALARM去处理超时 + +```cpp +old_sigset = gs_signal_unblock_sigusr2(); +``` + + + +认证改变并恢复信号隐藏 + +```cpp +ClientAuthentication(port); +gs_signal_recover_mask(old_sigset); +``` + + + +认证结束,恢复环境量(禁用timeout, log) + +```cpp +if (!disable_sig_alarm(true)) + ereport(FATAL, (errmsg("could not disable timer for authorization timeout"))); + + if (u_sess->attr.attr_storage.Log_connections) { + if (AM_WAL_SENDER) + ereport(LOG, (errmsg("replication connection authorized: user=%s", port->user_name))); + else + ereport(LOG, (errmsg("connection authorized: user=%s database=%s", port->user_name, port->database_name))); + } + if (AM_WAL_DB_SENDER) { + Oid userId = get_role_oid(port->user_name, false); + CheckLogicalPremissions(userId); + } +``` + + + +更新用户登录计数器 + +```cpp +if (IsUnderPostmaster && !IsBootstrapProcessingMode() && !dummyStandbyMode) + InstrUpdateUserLogCounter(true); + + set_ps_display("startup", false); + + u_sess->ClientAuthInProgress = false; /* client_min_messages is active now */ + u_sess->misc_cxt.authentication_finished = true; +``` + + + +#### 读取并加载配置信息 + +**执行函数:load_hba( )** + +src\common\backend\libpq\hba.cpp: 1729-1829 + +读取配置文件: + +```cpp + file = AllocateFile(g_instance.attr.attr_common.HbaFileName, "r"); + linecxt = tokenize_file(g_instance.attr.attr_common.HbaFileName, file, &hba_lines, &hba_line_nums); + (void)FreeFile(file); +``` + + + +解析所有行: + +```cpp +hbacxt = AllocSetContextCreate(g_instance.instance_context, + "hba parser context", + ALLOCSET_DEFAULT_MINSIZE, + ALLOCSET_DEFAULT_INITSIZE, + ALLOCSET_DEFAULT_MAXSIZE, + SHARED_CONTEXT); + oldcxt = MemoryContextSwitchTo(hbacxt); + forboth(line, hba_lines, line_num, hba_line_nums) + { + HbaLine* newline = NULL; + + if ((newline = parse_hba_line((List*)lfirst(line), lfirst_int(line_num))) == NULL) { + MemoryContextReset(hbacxt); + new_parsed_lines = NIL; + ok = false; + continue; + } + + new_parsed_lines = lappend(new_parsed_lines, newline); + } +``` + + + + + +#### 查配置信息有效性并获取认证方式 + +**执行函数:hba_getauthmethod( )** + +src\common\backend\libpq\hba.cpp: 2142-2159 + +决定何种认证方式将被使用当访问数据库时 + +```cpp +void hba_getauthmethod(hbaPort* port) +{ +#ifdef ENABLE_MULTIPLE_NODES + if (IsDSorHaWalSender() ) { +#else + if (IsDSorHaWalSender() && (is_node_internal_connection(port) || AM_WAL_HADR_SENDER)) { +#endif + check_hba_replication(port); + } else { + check_hba(port); + } +} +``` + + + +#### 根据认证方式获取认证凭证并完成认证 + +**执行函数:crypt_verify( )** + +src\common\backend\libpq\crypt.cpp: 374-714 + +对加密方式进行确认 + +包括MD5、SHA256、SM3加密方式,这里会在另一篇文章 + +《openGauss安全认证机制之口令存储》里详细介绍 + +#### 完成认证并加载信息 + +**执行函数:InitUser( )** + +src\common\backend\utils\init\postinit.cpp 2252-2268 + +```cpp +void PostgresInitializer::InitUser() +{ + InitializeSessionUserId(m_username, false, m_useroid); + m_isSuperUser = superuser(); + u_sess->misc_cxt.CurrentUserName = u_sess->proc_cxt.MyProcPort->user_name; +#ifndef ENABLE_MULTIPLE_NODES + /* + * In opengauss, we allow twophasecleaner to connect to database as superusers + * for cleaning up temporary tables. During the cleanup of temporary tables, + * m_username and u_sess->proc_cxt.MyProcPort->user_name point to the same memory + * address. We have freed and reinitialized u_sess->proc_cxt.MyProcPort->user_name + * in function InitializeSessionUserId, and we need to initialize m_username here. + */ + if (u_sess->proc_cxt.IsInnerMaintenanceTools) + m_username = u_sess->proc_cxt.MyProcPort->user_name; +#endif +} +``` + + + + + + + +### 总结 + +不难看出,角色管理的各个方面与角色创建的操作几乎一模一样,这是遵循了对角色操作的规范性,避免因为代码的逻辑问题造成一些权限混乱,角色界限不清晰的问题。 + +身份认证作为数据库访问安全的核心,防范未授权访问,实现严谨的身份验证方法。 + +本文大致描述了身份认证的全过程,但并未展现认证细节。对于认证的各个方面,我们将在其他系列文章中进行一一展示。 \ No newline at end of file diff --git "a/TestTasks/KuanXY/openGauss\345\256\211\345\205\250\350\256\244\350\257\201\346\234\272\345\210\266.md" "b/TestTasks/KuanXY/openGauss\345\256\211\345\205\250\350\256\244\350\257\201\346\234\272\345\210\266.md" new file mode 100644 index 0000000000000000000000000000000000000000..f70eee66846e7b0843f00c209f50613a94a82d0d --- /dev/null +++ "b/TestTasks/KuanXY/openGauss\345\256\211\345\205\250\350\256\244\350\257\201\346\234\272\345\210\266.md" @@ -0,0 +1,465 @@ +# openGauss安全认证机制 + +openGauss是一款开源的**自治安全数据库**,保障用户数据的安全和隐私,是数据库最基本的功能之一。特别是在大数据时代,随着互联网的发展,信息的传播和获取越来越方便,隐私泄露、数据丢失及信息篡改等问题也愈发严重,只有维护好数据库的安全,才能为用户提供安全的服务。 + +安全认证是客户端与服务端相互认证,建立信任连接,业务正常开展的基础,是数据库对外提供的的第一道防线,下面就看一下openGauss的安全认证机制。 + +*** + +#### 安全认证协议 + +**安全认证**:openGauss采用的认证方案为**RFC5802认证协议**。RFC5802认证协议实际上是SCRAM(Salted Challenge Response Authentication Mechanism,是指 `Salted质询响应身份验证机制` 或者 `基于盐值的质询响应身份验证` 机制)标准流程中的协议。SCRAM 是一套包含**服务器和客户端双向确认的用户认证体系**,配合信道绑定可以避免中间人攻击。 + +**openGauss的支持的认证方法**: + +| 认证方法 | 说明 | +| -------- | ------------------------------------------------------------ | +| trust | openGauss无条件接收连接请求,且访问请求时无须提供口令;当前仅支持超级用户本地登录采用此方式 | +| 口令认证 | 主要支持sha256加密口令认证。由于整个身份认证过程中,不需要还原明文口令,因此采用 PBKDF2单向加密算法。其中 Hash函数使用sha256算法,盐值salt则通过安全随机数生成。(迭代次数可由用户决定)。非超级用户在访问登录时必须提供口令信息(用户的口令信息被存放在系统表pg_authid中的rolpassword字段中,如果为空,则表示出现元信息错误) | +| cert认证 | openGauss支持使用SSL安全连接通道,cert认证表示使用SSL客户端进行认证,不需要提供用户密码。在该认证模式下,客户端和服务端数据经过加密处理。 | +| gss | 使用基于gssapi的kerberos认证,该认证方法依赖kerberosserver组件,一般用于支持`openGauss集群`内部通信认证和外部客户端连接认证,外部客户端仅支持gsql(openGauss提供的在命令行下运行的数据库连接工具)或JDBC连接时使用。 | + +*** + +#### 基于口令认证的密钥计算代码 + +**基于口令认证的密钥计算代码**如下所示: + +```c++ +SaltedPassword := Hi(password, salt, iteration_count) +ClientKey := HMAC(SaltedPassword, "Client Key") +StoredKey := sha256(ClientKey) +ServerKey := HMAC(SaltedPassword, "Sever Key") +ClientSignature:=HMAC(StoredKey, token) +ServerSignature:= HMAC(ServerKey, token) +ClientProof:= ClientSignature XOR ClientKey +``` + +其中 + +- **Hi()**:本质上是PBKDF2,即将salted hash进行多次重复计算(计算次数可选择)。 +- **HMAC()**:HMAC(Hash-based Message Authentication Code,散列信息认证码),HMAC运算利用hash算法,以一个消息M和一个密钥K作为输入,生成一个定长的消息摘要作为输出。HMAC能够提供消息完整性认证以及信源身份认证。 +- **sha256()**:采用sha256算法对任意长度的消息进行散列,生成一个256bit长的消息摘要(哈希值)。 + +| 对象 | 中文 | 解释 | +| --------------- | ------------ | ------------------------------------------------------------ | +| password | 密码 | 用户密码 | +| SaltedPassword | 加盐哈希密码 | 通过用户密码,盐值和迭代次数进行PBKDF2操作生成 | +| ClientKey | 客户端密钥 | SaltedPassword和字符串"Client Key"通过HMAC生成 | +| ClientSignature | 客户端签名 | 对StoredKey和随机数token进行HMAC生成 | +| StoredKey | 存储密钥 | 对ClientKey进行sha256生成,**用以验证用户身份(服务器端存储)** | +| ServerKey | 服务器密钥 | SaltedPassword和字符串"Sever Key"通过HMAC生成,**客户端验证服务端身份(服务器端存储)** | +| ServerSignature | 服务器签名 | 对ServerKey和随机数token进行HMAC生成 | +| ClientProof | 客户端证明 | 对ClientSignature和ClientKey进行异或生成 | + +生成过程图如下所示: + +![](https://forum.gitlink.org.cn/api/attachments/398616) + + +图中橙色块`ServerKey`和`StoredKey`是用户创建或修改用户属性(密码)时就会通过加密函数计算得到的,并被拼接为特定字符串存储于服务端。 + +绿色块`ClientSignature`、`ServerSignature`和`ClientProof`在每次建立连接后都需要计算,由于受随机数token的影响,在每次建立连接中值都不同。 + +*** + +#### 认证关键点 + +1. **服务器通过StoredKey验证客户端用户身份** + +ClientProof(客户端证明)由ClientSignature和ClientKey进行异或(XOR)生成。 + +而当客户端验证时则会将`ClientSignature`和客户端发送来的`ClientProof`进行异或,恢复生成`ClientKey`,即: + +```c++ +ClientKey:= ClientSignature XOR ClientProof +``` + +再将ClinetKey进行哈希运算,将得到的值与StoredKey进行对比,如果相等则客户端认证通过。 + +服务器计算出来的ClientKey验证完后直接丢弃。 + +2. **客户端通过ServerKey验证服务器身份** + +将ServerSignature与服务器端发送来的值进行对比,相等则认证通过。 + +*** + +#### 认证流程 + +认证流程图: +![](https://forum.gitlink.org.cn/api/attachments/398617) + +认证流程: + +1. 首先客户端与服务器建立连接,客户端将自己的username发送给服务端。 +2. 服务端在收到username后,生成一个随机数token,并通过username查询到用户对应盐值slat,ServerKey和迭代次数,并通过ServerKey和刚刚生成的token进行HMAC得到ServerSignature。 +3. 服务端将认证方式信息,随机数token,盐值salt,ServerSignature和迭代次数发送给客户端。 +4. 客户端利用密码password,盐值salt和迭代次数通过PBKDF2生成K(即SaltedPassword),有了K,就可以分别与字符串“Client Key”和“Sever Key”进行HMAC操作,生成对应的ClientKey和ServerKey。再将其中的ClientKey进行SHA256得到StoredKey。 +5. 客户端生成对应的ServerKey和StoredKey后,将字符串“sha256”、盐值salt、ServerKey和StoredKey拼接为Buf。从Buf中得到ServerKey并和服务端刚刚生成的随机数token进行HMAC操作,得到`客户端侧生成的ServerSignature`,与服务器发送来的ServerSignature对比,一致则**客户端认证服务器通过**。 +6. 认证完服务器后,将自己的StoredKey和token进行HMAC生成ClientSignature,并再将ClientSignature和ClientKey进行异或操作,生成ClientProof。 +7. 客户端将ClientProof发送给服务端。 +8. 服务端获取ClientProof后,通过Storedkey和token的HAMC计算出ClientSignature,通过ClientProof和ClientSignature进行异或得到ClientKey,在进行HMAC得到StoredKey,与`服务端本身存储的StoredKey`进行对比,若一致,则**服务端认证客户端通过**。 + +以上就是口令安全认证机制的具体流程。 + +对客户端认证主要通过调用`ClientAuthentication()`函数完成,下面对该函数的源码(部分省略)进行分析和注释: + +*** + +#### ClientAuthentication + +接收的参数为[Port结构体](https://forum.gitlink.org.cn/forums/8338/detail)指针,通过sendAuthRequest()函数发送请求到前端。 + +- 第3~8行定义相关变量,包括status,随机数token等。 +- 第13行获取port中的认证方法。 +- 第20行根据用户认证方法进行对应操作。 + - 第21行是无条件拒绝连接。 + - 第25行是匹配失败。 + - 第32行是MD5认证方法。 + - 第43、44行对应SHA256认证和SM3认证。可以看出在服务端SHA256和SM3的认证基本流程一样。 + - 第61行通过sha_bytes_to_hex8生成随机数token。 + - 第63行通过检查hba->auth_method从而选择SHA256还是SM3进行认证。 +- 第73~77行根据status完成身份认证。 + +>/src/common/backend/libpq/auth.cpp 350——773行 + +```c++ +void ClientAuthentication(Port* port) /* 传入Port结构体指针参数 */ +{ + int status = STATUS_ERROR; /* 设置认证初始状态为ERROR */ + /* 数据库安全:支持密码复杂性 */ + char details[PGAUDIT_MAXLENGTH] = {0}; + char token[TOKEN_LENGTH + 1] = {0}; + errno_t rc = EOK; + int retval = 0; + + /* + 获取用于此前端/数据库组合的身份验证方法。注意:此时我们不解析文件;其他地方已经这样做了。如果解析hba配置文件失败,hba.c会将错误消息放入服务器日志文件。 + */ + hba_getauthmethod(port); + + ······ + + /* + * 现在继续进行实际的身份验证检查 + */ + switch (port->hba->auth_method) { /* 根据用户认证方法执行对应操作 */ + case uaReject: /* 无条件拒绝连接 */ + + ······ + + case uaImplicitReject: + + /* + * 没有匹配条目,所以告诉用户我们失败了 +注意:这里报告的额外信息不是安全漏洞,因为所有这些信息都是在前端已知的,必须假定坏人已知。我们只是在帮助那些不那么神秘的好人。 + */ + ······ + case uaMD5: /* 数据库安全:支持MD5认证方法 */ + /* 禁止与初始用户远程连接 */ + if (isRemoteInitialUser(port)) { + ereport(FATAL, + (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), + errmsg("Forbid remote connection with initial user."))); + } + sendAuthRequest(port, AUTH_REQ_MD5);/* 发送认证请求到前端,认证码为AUTH_REQ_MD5 */ + status = recv_and_check_password_packet(port);/* 接收和检查密码包,返回状态 */ + break; + /* 数据库安全:支持SHA256认证方法 */ + case uaSHA256: + case uaSM3: /* 数据库安全:支持SHA256和SM3认证方法 */ + /* 禁止与初始用户远程连接 */ + if (isRemoteInitialUser(port)) { + ereport(FATAL, + (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), + errmsg("Forbid remote connection with initial user."))); + } + rc = memset_s(port->token, TOKEN_LENGTH * 2 + 1, 0, TOKEN_LENGTH * 2 + 1); + securec_check(rc, "\0", "\0"); + /* 分配内存的函数需要保持中断以确保安全。 */ + HOLD_INTERRUPTS(); + retval = RAND_priv_bytes((GS_UCHAR*)token, (GS_UINT32)TOKEN_LENGTH);//生成随机数token + RESUME_INTERRUPTS(); + CHECK_FOR_INTERRUPTS(); + if (retval != 1) { + ereport(ERROR, (errmsg("Failed to Generate the random number,errcode:%d", retval))); + } + sha_bytes_to_hex8((uint8*)token, port->token); /* 生成随机数token */ + port->token[TOKEN_LENGTH * 2] = '\0'; + if (port->hba->auth_method == uaSHA256) {/* 判断认证方法是SHA256还是SM3 */ + sendAuthRequest(port, AUTH_REQ_SHA256);/* 发送认证请求到前端,认证码为AUTH_REQ_SHA256 */ + } else { + sendAuthRequest(port, AUTH_REQ_SM3);/* 发送认证请求到前端,认证码为AUTH_REQ_SM3 */ + } + status = recv_and_check_password_packet(port);/* 接收和检查密码包,返回状态 */ + break; + ······ + } + + if (status == STATUS_OK) /* 根据接受检查密码包的结果状态发送身份验证请求 */ + sendAuthRequest(port, AUTH_REQ_OK);/* 认证成功,将认证成功消息发送回客户端 */ + else { + auth_failed(port, status); /* 身份验证失败 */ + } + /* 已完成身份验证,因此应关闭即时中断(参数ImmediateInterruptOK) */ + t_thrd.int_cxt.ImmediateInterruptOK = false; +} +``` + +客户端认证服务端主要通过调用`pg_password_sendauth()`函数完成,下面对该函数的源码(部分省略)进行分析和注释: + +*** + +#### pg_password_sendauth + +结合上述流程和图片分析代码,代码实现非常清晰明了(主要看SHA256和SM3的具体实现): + +- 第3~第25行初始化了一系列变量,包括上述提及的ClientProof,server_key,ClientSignature,server_signature,随机数token等。 +- 第28行switch语句根据不同的认证请求进行对应操作。(由于不支持直接发送密码的认证,代码中删除了AUTH_REQ_PASSWORD的分支) +- 第37行开始为SHA256加密的认证。 +- 第119行开始为SM3方式加密的认证。 +- 第212~225行清空变量,保证数据安全。 + +>/src/common/interfaces/libpq/fe-auth.cpp 770——1077行 + +```c++ +static int pg_password_sendauth(PGconn* conn, const char* password, AuthRequest areq) +{ /* 初始化变量 */ + int ret; + char* crypt_pwd = NULL; + int crypt_pwd_sz = 0; + const char* pwd_to_send = NULL; /* 发送给服务端的信息 */ + int hmac_length = HMAC_LENGTH; /* HMAC长度(32)*/ + char h[HMAC_LENGTH + 1] = {0}; /* ClientProof二进制形式 */ + char h_string[HMAC_LENGTH * 2 + 1] = {0}; /* ClientProof字符串形式 */ + char hmac_result[HMAC_LENGTH + 1] = {0}; /* ClientSignature */ + char client_key_bytes[HMAC_LENGTH + 1] = {0}; /* ClientKey二进制形式 */ + char buf[SHA256_PASSWD_LEN + 1] = {0}; /* 拼接字符串buf形式 */ + char sever_key_bytes[HMAC_LENGTH + 1] = {0}; /* server_key二进制形式 */ + char server_key_string[HMAC_LENGTH * 2 + 1] = {0}; /* server_key字符串形式 */ + char token[TOKEN_LENGTH + 1] = {0}; /* 随机数token */ + char client_sever_signature_bytes[HMAC_LENGTH + 1] = {0}; /* 客户端生成的server_signature二进制形式 */ + char client_sever_signature_string[HMAC_LENGTH * 2 + 1] = {0}; /* 客户端生成的server_signature字符串形式 */ + char salt[SALT_LENGTH + 1] = {0}; /* 盐值,SALT_LENGTH=32 */ + char stored_key_bytes[STORED_KEY_LENGTH + 1] = {0}; /* storedKey二进制形式 */ + char stored_key_string[STORED_KEY_LENGTH * 2 + 1] = {0}; /* storedKey字符串形式 */ + char client_key_buf[CLIENT_KEY_STRING_LENGTH + 1] = {0}; /* ClientKey字符串形式 */ + char fail_info[] = "sever_signature_failed"; /* 失败信息 */ + int CRYPT_hmac_ret1; /* 状态:生成客户端server_signature是否成功 */ + int CRYPT_hmac_ret2; /* 状态:生成H(ClientProof)是否成功 */ + errno_t rc = 0; + + /* 加密密码 */ + switch (areq) { + case AUTH_REQ_MD5: { + /* pg_md5_encrypt()通过MD5Salt进行MD5加密 */ + ······ + + case AUTH_REQ_MD5_SHA256: { + + ······ + + case AUTH_REQ_SHA256: { + char* crypt_pwd2 = NULL; + + ······ + +#else + if (SHA256_PASSWORD == conn->password_stored_method || PLAIN_PASSWORD == conn->password_stored_method) { + /* 通过SHA256方式加密,生成sha256加密字符串 + pg_sha_encrypt输入密码,盐值salt,迭代次数,得到buf*/ + if (!pg_sha256_encrypt( + password, conn->salt, strlen(conn->salt), (char*)buf, client_key_buf, conn->iteration_count)) + return STATUS_ERROR; /* 加密失败则返回错误 */ +#endif + rc = strncpy_s(server_key_string, /* 将buf内容的ServerKey部分复制到server_key_string中 */ + sizeof(server_key_string), + &buf[SHA256_LENGTH + SALT_STRING_LENGTH], + sizeof(server_key_string) - 1); + securec_check_c(rc, "\0", "\0"); + rc = strncpy_s(stored_key_string, /* 将buf内容的StoredKey部分复制到stroed_key_stirng中 */ + sizeof(stored_key_string), + &buf[SHA256_LENGTH + SALT_STRING_LENGTH + HMAC_STRING_LENGTH], + sizeof(stored_key_string) - 1); + securec_check_c(rc, "\0", "\0"); + server_key_string[sizeof(server_key_string) - 1] = '\0'; /* 转C字符串 */ + stored_key_string[sizeof(stored_key_string) - 1] = '\0'; /* 转C字符串 */ + + + sha_hex_to_bytes32(sever_key_bytes, server_key_string);/* 将server_key_string字符串(64字节)转换为二进制(32字节)*/ + sha_hex_to_bytes4(token, conn->token); /* token将字符串(8字节)转换为二进制(4字节) */ + /* 通过server_key和token调用HMAC算法计算,得到client_server_signature_bytes,将该变量转为字符串变量,用来验证与服务端传来的server_signature是否相等*/ + CRYPT_hmac_ret1 = CRYPT_hmac(NID_hmacWithSHA256, + (GS_UCHAR*)sever_key_bytes, + HMAC_LENGTH, + (GS_UCHAR*)token, + TOKEN_LENGTH, + (GS_UCHAR*)client_sever_signature_bytes, + (GS_UINT32*)&hmac_length); + if (CRYPT_hmac_ret1) { + return STATUS_ERROR; + } + + sha_bytes_to_hex64((uint8*)client_sever_signature_bytes, client_sever_signature_string); /* 将客户端生成的server_signature二进制(32字节)转换为字符串(64字节)*/ + + /* + * 在检查客户端签名不安全之前,请检查服务器签名。 + * 未来:rfc5802认证协议需要增强。 + * 调用函数strncmp判断计算的client_server_signature_string和服务端传来的server_signature值是否相等 + */ + if (PG_PROTOCOL_MINOR(conn->pversion) < PG_PROTOCOL_GAUSS_BASE && + 0 != strncmp(conn->sever_signature, client_sever_signature_string, HMAC_STRING_LENGTH)) { /* 如果客户端生成的server_signature与服务器发送过来的server_signature不相等则给pwd_to_send赋值失败信息 */ + pwd_to_send = fail_info; + } else { + /* 计算H(ClientProof),H=hmac(storedkey,token)XOR ClientKey*/ + sha_hex_to_bytes32(stored_key_bytes, stored_key_string); /* 将storedKey字符串(64字节)转换为二进制(32字节)*/ + CRYPT_hmac_ret2 = CRYPT_hmac(NID_hmacWithSHA256, + (GS_UCHAR*)stored_key_bytes, + STORED_KEY_LENGTH, + (GS_UCHAR*)token, + TOKEN_LENGTH, + (GS_UCHAR*)hmac_result, + (GS_UINT32*)&hmac_length); + + if (CRYPT_hmac_ret2) { /* H计算错误返回失败状态 */ + return STATUS_ERROR; + } + + sha_hex_to_bytes32(client_key_bytes, client_key_buf); /*将client_key_buf字符串(64字节)转换为client_key_bytes二进制(32字节) */ + /* hmac_result和client_key_bytes异或得到h,然后将其发送给服务端,用于验证客户端 */ + if (XOR_between_password(hmac_result, client_key_bytes, h, HMAC_LENGTH)) { + return STATUS_ERROR; + } + + sha_bytes_to_hex64((uint8*)h, h_string); /* 将h(ClientProof)二进制(32字节)转换为字符串(64字节) */ + + /*将H(ClientProof)发送到服务器*/ + pwd_to_send = h_string; + } + } + +······ + break; + } + case AUTH_REQ_SM3: { + /* 通过SM3方式加密 */ + if (conn->password_stored_method == SM3_PASSWORD) { + if (!GsSm3Encrypt( + password, conn->salt, strlen(conn->salt), (char*)buf, client_key_buf, conn->iteration_count)) + return STATUS_ERROR; /* 加密失败则返回错误 */ + /* 将buf内容的ServerKey部分复制到server_key_string中 */ + rc = strncpy_s(server_key_string, + sizeof(server_key_string), + &buf[SM3_LENGTH + SALT_STRING_LENGTH], + sizeof(server_key_string) - 1); + securec_check_c(rc, "\0", "\0"); + /* 将buf内容的StoredKey部分复制到stroed_key_stirng中 */ + rc = strncpy_s(stored_key_string, + sizeof(stored_key_string), + &buf[SM3_LENGTH + SALT_STRING_LENGTH + HMAC_STRING_LENGTH], + sizeof(stored_key_string) - 1); + securec_check_c(rc, "\0", "\0"); + server_key_string[sizeof(server_key_string) - 1] = '\0'; /* 转C字符串 */ + stored_key_string[sizeof(stored_key_string) - 1] = '\0'; /* 转C字符串 */ + /* 将server_key_string字符串(64字节)转换为二进制(32字节)*/ + sha_hex_to_bytes32(sever_key_bytes, server_key_string); + /* token将字符串(8字节)转换为二进制(4字节) */ + sha_hex_to_bytes4(token, conn->token); + /* 通过server_key和token调用HMAC算法计算,得到client_server_signature_bytes,将该变量转为字符串变量,用来验证与服务端传来的server_signature是否相等*/ + CRYPT_hmac_ret1 = CRYPT_hmac(NID_hmacWithSHA256, + (GS_UCHAR*)sever_key_bytes, + HMAC_LENGTH, + (GS_UCHAR*)token, + TOKEN_LENGTH, + (GS_UCHAR*)client_sever_signature_bytes, + (GS_UINT32*)&hmac_length); + if (CRYPT_hmac_ret1) { + return STATUS_ERROR; + } + /* 将客户端生成的server_signature二进制(32字节)转换为字符串(64字节)*/ + sha_bytes_to_hex64((uint8*)client_sever_signature_bytes, client_sever_signature_string); + + /* + * 在检查客户端签名不安全之前,请检查服务器签名。 + * 计算H(ClientProof),H=hmac(storedkey,token)XOR ClientKey + */ + if (PG_PROTOCOL_MINOR(conn->pversion) < PG_PROTOCOL_GAUSS_BASE && + 0 != strncmp(conn->sever_signature, client_sever_signature_string, HMAC_STRING_LENGTH)) {/* 如果客户端生成的server_signature与服务器发送过来的server_signature不相等则给pwd_to_send赋值失败信息 */ + pwd_to_send = fail_info; + } else { + /* 计算 H(ClientProof), H = hmac(storedkey, token) XOR ClientKey */ + sha_hex_to_bytes32(stored_key_bytes, stored_key_string); /* 将storedKey字符串(64字节)转换为二进制(32字节)*/ + CRYPT_hmac_ret2 = CRYPT_hmac(NID_hmacWithSHA256, + (GS_UCHAR*)stored_key_bytes, + STORED_KEY_LENGTH, + (GS_UCHAR*)token, + TOKEN_LENGTH, + (GS_UCHAR*)hmac_result, + (GS_UINT32*)&hmac_length); + if (CRYPT_hmac_ret2) { + return STATUS_ERROR; /* H计算错误返回失败状态 */ + } + + sha_hex_to_bytes32(client_key_bytes, client_key_buf); + // hmac_result和client_key_bytes异或得到h,然后将其发送给服务端,用于验证客户端 + if (XOR_between_password(hmac_result, client_key_bytes, h, HMAC_LENGTH)) { + return STATUS_ERROR; + } + /* 将h转化为string */ + sha_bytes_to_hex64((uint8*)h, h_string); + + /* 将H发送到服务器 */ + pwd_to_send = h_string; + } + } else { + pwd_to_send = password; + } + + break; + } +/* + * 注意:目前不支持直接发送密码的身份验证。 + * 需要:出于默认和安全原因,我们在这里删除AUTH_REQ_PASSWORD分支。 + */ + default: + return STATUS_ERROR; + } + /* 从协议3.0开始,数据包具有消息类型 */ + if (PG_PROTOCOL_MAJOR(conn->pversion) >= 3) + ret = pqPacketSend(conn, 'p', pwd_to_send, strlen(pwd_to_send) + 1); + else + ret = pqPacketSend(conn, 0, pwd_to_send, strlen(pwd_to_send) + 1); + if (crypt_pwd != NULL) { + erase_mem(crypt_pwd, crypt_pwd_sz); + libpq_free(crypt_pwd); + } + /* 清空变量,保证安全 */ + erase_arr(h_string); + erase_arr(h); + erase_arr(hmac_result); + erase_arr(client_key_bytes); + erase_arr(buf); + erase_arr(sever_key_bytes); + erase_arr(server_key_string); + erase_arr(token); + erase_arr(client_sever_signature_bytes); + erase_arr(client_sever_signature_string); + erase_arr(salt); + erase_arr(stored_key_bytes); + erase_arr(stored_key_string); + erase_arr(client_key_buf); + + return ret; +} +``` + +> 对于**SM3国密算法**的解析参见我们的另一篇文章——《[openGauss新增国密SM3加密算法解析](https://forum.gitlink.org.cn/forums/8337/detail)》 + +*** + +#### 总结 + +本文介绍了openGauss的安全认证机制,openGauss采用的是**基于盐值的质询响应身份验证**机制进行安全认证。认证过程中客户端和服务器需要双向认证,服务端不会获取和保存密码明文,盐值与多重哈希算法保证了用户密码不会泄露,防止彩虹攻击,随机数token保证了每次认证中发送的证明都不一样,从而防止重放攻击。 + +从源码中我们可以清晰地了解到认证机制的具体实现,其中涉及一些二进制与字符串格式的转换,通过源码也了解到认证过程中openGauss支持SM3国密算法的使用,openGauss的安全性正在不断增强。 \ No newline at end of file diff --git "a/TestTasks/KuanXY/openGauss\346\235\203\351\231\220\347\256\241\347\220\206.md" "b/TestTasks/KuanXY/openGauss\346\235\203\351\231\220\347\256\241\347\220\206.md" new file mode 100644 index 0000000000000000000000000000000000000000..bc1c14906b212115c9483b319323b4a8d1a0c424 --- /dev/null +++ "b/TestTasks/KuanXY/openGauss\346\235\203\351\231\220\347\256\241\347\220\206.md" @@ -0,0 +1,1038 @@ +# openGauss权限管理 +## 权限管理概述 + +> 什么是权限?为什么需要权限? + +在各类系统中,权限都非常重要,它定义了每个用户可以进行的行为及范围。在openGauss数据库中,用户执行任何命令都需拥有对应的权限,通过合理地分配和回收权限保护数据库的正常使用,防止用户越级滥用权限。 + +openGauss数据库权限可以分为**系统权限**和**对象权限**两种。 + +数据库安装过程生成的**初始用户**拥有最高权限(即超级用户),可以执行所有操作。(初始用户会绕过所有权限检查,因此openGauss建议仅将此初始用户作为DBA管理用途,而非业务应用。) + +### 系统权限 + +| 系统权限 |
对应权限
| | +| :--------- | :--------------------------------------- | ------------------------------------------------------------ | +| SYSADMIN | 系统管理员 | 默认安装情况下具有与对象所有者相同的权限,但不包括dbe_perf模式的对象权限(SYSADMIN权限可以通过GRANT/REVOKE ALL PRIVILEGE授予或撤销) | +| CREATEDB | 创建数据库权限 | 允许创建数据库 | +| CREATEROLE | 创建角色权限 | 允许创建角色 | +| AUDITADMIN | 审计管理员 | 查看、删除审计日志 | +| MONADMIN | 监控管理员 | 具有查看dbe_perf模式下视图和函数的权限,亦可以对dbe_perf模式的对象权限进行授予或收回 | +| OPRADMIN | 运维管理员 | 具有使用Roach工具执行备份恢复的权限 | +| POLADMIN | 安全策略管理员 | 具有创建资源标签、脱敏策略和统一审计策略的权限 | +| LOGIN | 登陆权限 | 允许登录 | + +> 系统权限无法通过ROLE和USER的权限被继承,也无法授予PUBLIC + +### 对象权限 + +对象权限是将数据库对象(表和视图、指定字段、数据库、函数、模式、表空间等)的相关权限授予特定角色或用户。 + +openGauss中授予和撤销权限的命令为GRANT和REVOKE: + +GRANT 命令的基本语法如下 + +```sql +GRANT privilege [, ...] +ON object [, ...] +TO { PUBLIC | GROUP group | username } +``` + +- privilege − 值可以为:SELECT,INSERT,UPDATE,DELETE, RULE,ALL。 + +- object − 要授予访问权限的对象名称。可能的对象有: table, view,sequence。 + +- PUBLIC − 表示所有用户。 + +- GROUP group − 为用户组授予权限。 + +- username − 要授予权限的用户名。PUBLIC 是代表所有用户的简短形式。 + +- **WITH GRANT OPTION** + + 如果声明了WITH GRANT OPTION,则被授权的用户也可以将此权限赋予他人,否则就不能授权给他人。这个选项不能赋予PUBLIC。 + +REVOKE 命令取消权限,REVOKE 语法: + +```sql +REVOKE privilege [, ...] +ON object [, ...] +FROM { PUBLIC | GROUP groupname | username } +``` + +openGauss权限的简单实操可参看我们的另一篇博客——[openGauss权限管理基本操作](https://forum.gitlink.org.cn/forums/8258/detail):https://forum.gitlink.org.cn/forums/8258/detail + +## ACL访问控制列表 + +**ACL**(AccessControl list,访问控制列表)是openGauss进行对象权限管理和权限检查的基础。 + +在openGauss中,每个数据库对象都有对应的ACL,在该 ACL上存储了此对象的所有授权信息,进行操作时数据库会查询ACL来检查用户是否拥有对应权限。当用户访问对象时,只有它在对象的 ACL 中并且具有所需的权限时才能访问该对象。当用户对对象的访问权限发生变更时,只需要在 ACL 上更新对应的权限即可。 + +>数据库对象的ACL保存在对应的**系统表**中,当被授予或回收对象权限时,系统表中保存的ACL权限位会被更新。进行权限检查时也是通过系统表获取对象权限。 + +**每个ACL是由1个或多个`AclItem`构成的链表**,每1个AclItem由`授权者`、`被授权者`和`权限位`3部分构成,记录着可在对象上进行操作的用户及其权限。 +数据结构AclItem的代码如下: + +### AclItem + +>src/include/utils/acl.h 45——49 + +```c++ +typedef struct AclItem { + Oid ai_grantee; /* 被授权者的OID */ + Oid ai_grantor; /* 授权者的OID */ + AclMode ai_privs; /* 权限位:32位的比特位 */ +} AclItem; +``` + +其中 ai_privs是AclMode类型,AclMode是一个32位的比特位 + +>```c++ +>typedef uint32 AclMode; /* 特权位的位掩码 */ +>``` + +AclMode结构解释 + +| AclMode位 | 位含义 | 解释 | +| --------- | ---------- | ------------------------------------------------------------ | +| 高16位 | 权限选项位 | 比特位为1则ai_grantee拥有此对象相应操作的**授权权限**(否则无授权) | +| 低16位 | 操作权限位 | 比特位为1则ai_grantee拥有此对象的相应**操作权限**(否则无权限) | + +数据库的SQL语句通常分为以下几类操作: + +>**DML、DDL和DCL类操作** +> +>- DML(Data Manipulation Language)数据操纵语言: +> +> +>用于对数据库表中的数据进行操作。如:insert,delete,update,select等。 +> +>- DDL(Data Definition Language)数据定义语言: +> +>用于定义或修改数据库中的对象,如Create,Alter和Drop等。 +> +>- DCL(Data Control Language)数据控制语句: +> +>是用来创建用户角色、设置或更改数据库用户或角色权限的语句,如grant,revoke等。 + +openGauss分别将DML和DDL操作的权限记录于两个AclMode结构中,以第15位的值(0~31位)来区分两类AclMode,如下图所示: + +![](https://forum.gitlink.org.cn/api/attachments/398618) + +在后续的源码中我们也会看到,在ExecuteGrantStmt函数中,将数据结构GrantStmt转换为InternalGrant时,就是把权限列表提取为DML和DDL两类权限进行操作。下面给出AclMode中对应的权限参数及含义列表。 + +### 权限参数 + +>权限参数在具体代码中的定义参见我们的另一篇博文——[源码中的对象类型及权限定义](https://forum.gitlink.org.cn/forums/8284/detail):https://forum.gitlink.org.cn/forums/8284/detail + +| 参数 | 对应权限 | | +| ---- | ---------- | ------------------------------------------------------------ | +| a | INSERT | 允许对指定的表执行INSERT命令。 | +| r | SELECT | 允许对指定的表、视图、序列执行SELECT命令,
update或delete时也需要对应字段上的select权限。 | +| w | UPDATE | 允许对声明的表中任意字段执行UPDATE命令。
通常,update命令也需要select权限来查询出哪些行需要更新。
SELECT… FOR UPDATE和SELECT… FOR SHARE除了需要SELECT权限外,还需要UPDATE权限。 | +| d | DELETE | 允许执行DELETE命令删除指定表中的数据。
通常,delete命令也需要select权限来查询出哪些行需要删除。 | +| D | TRUNCATE | 允许执行TRUNCATE语句删除指定表中的所有记录。 | +| x | REFERENCES | 创建一个外键约束,必须拥有参考表和被参考表的REFERENCES权限。 | +| t | TRIGGER | 允许创建触发器。 | +| X | EXECUTE | 允许使用指定的函数,以及利用这些函数实现的操作符。 | +| U | USAGE | 对于过程语言,允许用户在创建函数的时候指定过程语言。
对于模式,USAGE允许访问包含在指定模式中的对象,若没有该权限,则只能看到这些对象的名称。
对于序列,USAGE允许使用nextval函数。
对于Data Source对象,USAGE是指访问权限,也是可赋予的所有权限,即USAGE与ALL PRIVILEGES等价。 | +| C | CREATE | 对于数据库,允许在数据库里创建新的模式。
对于模式,允许在模式中创建新的对象。如果要重命名一个对象,用户除了必须是该对象的所有者外,还必须拥有该对象所在模式的CREATE权限。
对于表空间,允许在表空间中创建表,允许在创建数据库和模式的时候把该表空间指定为缺省表空间。 | +| T | TEMPORARY | 允许创建临时表。 | +| c | CONNECT | 允许用户连接到指定的数据库。 | +| p | COMPUTE | | +| R | READ | | +| W | WRITE | | +| A | ALTER | 允许用户修改指定对象的属性,但不包括修改对象的所有者和修改对象所在的模式。 | +| P | DROP | 允许用户删除指定的对象。 | +| m | COMMENT | 允许用户定义或修改指定对象的注释。 | +| i | INDEX | 允许用户在指定表上创建索引,并管理指定表上的索引,还允许用户对指定表执行REINDEX和CLUSTER操作。 | +| v | VACUUM | 允许用户对指定的表执行ANALYZE和VACUUM操作。 | + +- **ALL PRIVILEGES** + + 一次性给指定用户/角色赋予所有可赋予的权限。只有系统管理员有权执行GRANT ALL PRIVILEGES。 + +## 权限管理源码解析 + +数据库对象权限管理主要通过使用SQL命令“GRANT/REVOKE”授予或回收一个或多个角色在对象上的权限。SQL引擎将用户的GRANT/REVOKE指令解析优化为可执行的计划,然后由执行器执行。**“GRANT/REVOKE”命令都由函数`ExecuteGrantStmt`实现**,该函数的输入为一个`GrantStmt类型`的参数,基本执行流程如图所示。 + +![](https://forum.gitlink.org.cn/api/attachments/398619) + +**函数ExecuteGrantStmt首先将GrantStmt结构转换为InternalGrant结构,并将权限列表转换为内部的AclMode表示形式。**当privileges 取值为**NIL**时,表示授予或回收所有的权限,此时置InternalGrant的all_privs字段为true,privileges字段为ACL_NO_RIGHTS。下面是涉及的主要数据结构及注释: + +### 数据结构 + +#### GrantStmt + +> /src/include/nodes/parsenodes.h 622——634行 + +```c++ +typedef struct GrantStmt { + NodeTag type; + bool is_grant; /* true = 授权, false = 回收 */ + GrantTargetType targtype; /* 操作目标的类型 */ + GrantObjectType objtype; /* 被操作对象的类型:表、数据库、模式、函数等 */ + List* objects; /* 被操作对象的集合:RangeVar节点、FuncWithArgs节点或纯名称(作为值字符串)的列表*/ + List* privileges; /* 要操作权限列表:privileges指向数据结构AccessPriv节点列表 */ + /* privileges == NIL 表示所有特权 */ + List* grantees; /* 被授权者的集合:PrivGrantee节点列表 */ + bool grant_option; /* 授予或撤销授予权限, true = 再授予权限 */ + DropBehavior behavior; /* 回收权限的行为 drop behavior (for REVOKE) */ +} GrantStmt; +``` + +#### InternalGrant + +>/src/common/backend/catalog/aclchk.cpp 104——116行 + +```c++ +typedef struct { + bool is_grant; /* true=授权, false=回收 */ + GrantObjectType objtype; /* 被操作对象的类型:表、数据库、模式、函数等 */ + List* objects; /* 被操作对象的集合 */ + bool all_privs; /* 是否授予或回收所有的权限 */ + AclMode privileges; /* AclMode形式表示的DML类操作对应的权限 */ + AclMode ddl_privileges; /* AclMode形式表示的DDL类操作对应的权限 */ + List* col_privs; /* 对列执行的DML类操作对应的权限 */ + List* col_ddl_privs; /* 对列执行的DDL类操作对应的权限 */ + List* grantees; /* 被授权者的集合 */ + bool grant_option; /* true=再授予权限 */ + DropBehavior behavior; /* 回收权限的行为 */ +} InternalGrant; +``` + +对比二者,InternalGrant将权限列表细分为了对应的DML、DDL类操作权限: + +| **GrantStmt** | **InternalGrant** | 注释 | +| ------------------------ | ----------------------- | ------------------------------------------ | +| NodeTag type | | | +| bool is_grant | bool is_grant | true=授权, false=回收 | +| GrantTargetType targtype | | 操作目标的类型 | +| GrantObjectType objtype | GrantObjectType objtype | 被操作对象的类型:表、数据库、模式、函数等 | +| List* objects | List* objects | 被操作对象的集合 | +| List* privileges | | 指向要操作权限列表 | +| List* grantees | List* grantees | 被授权者的集合 | +| bool grant_option | bool grant_option | true=再授予权限 | +| DropBehavior behavior | DropBehavior behavior | 回收权限的行为 | +| | bool all_privs | 是否授予或回收所有的权限 | +| | AclMode privileges | AclMode形式表示的DML类操作对应的权限 | +| | AclMode ddl_privileges | AclMode形式表示的DDL类操作对应的权限 | +| | List* col_privs | 对列执行的DML类操作对应的权限 | +| | List* col_ddl_privs | 对列执行的DDL类操作对应的权限 | + +#### PrivGrantee + +```c++ +typedef struct PrivGrantee { + NodeTag type; + char* rolname; /* 如果为NULL,则为PUBLIC */ +} PrivGrantee; +``` + +#### AccessPriv + +```C++ +/* + * 具有可选列名列表的访问权限 + * priv_name==NULL表示所有特权(仅用于列列表) + * cols==NIL表示“所有列” + * 注意:简单的“ALL PRIVILEGES”表示为一个NIL列表,而不是两个字段都为空的AccessPriv。 + */ +typedef struct AccessPriv { + NodeTag type; + char* priv_name; /* 特权名 */ + List* cols; /* 值字符串列表 */ +} AccessPriv; +``` + +### 主要函数 + +#### ExecuteGrantStmt + +ExecuteGrantStm主要进行结构检查和转换,将GrantStmt类型的参数stmt转化为InternalGrant类型的参数istmt,并将istmt传递给ExecGrantStmt_oids进行权限管理。 + +>/src/common/backend/catalog/aclchk.cpp 521——677行 + +```c++ +/* + * 调用ExecuteGrantStmt以执行命令GRANT和REVOKE + */ +void ExecuteGrantStmt(GrantStmt* stmt) +{ /* 定义初始数据结构 */ + InternalGrant istmt;//生成InternalGrant,后面将stmt参数转化到istmt中 + ListCell* cell = NULL; + const char* errormsg = NULL; + AclMode all_privileges; + AclMode all_ddl_privileges; + bool type_flag = false; + + /* + * 将常规GrantStmt转换为InternalGrant格式 + */ + istmt.is_grant = stmt->is_grant; + istmt.objtype = stmt->objtype; + + /* 收集目标对象的OID */ + istmt.objects = GrantStmtGetObjectOids(stmt); + + /* all_privs在下方赋予 */ + /* DML和DDL权限在下方赋予 */ + istmt.col_privs = NIL; /* 可能在下方填写 */ + istmt.col_ddl_privs = NIL; /* 可能在下方填写 */ + istmt.grantees = NIL; /* 在下面填写 */ + istmt.grant_option = stmt->grant_option; //授予或撤销权限 + istmt.behavior = stmt->behavior; //回收权限行为 + + /* +将PrivGrantee列表转换为Oid列表。 +注意:如果检测到角色名为空,则将向列表中插入ACL_ID_PUBLIC(如果找到PUBLIC,语法将使用该名称),因此下游不需要任何额外的工作来处理此类情况。 + */ + foreach (cell, stmt->grantees) { // + PrivGrantee* grantee = (PrivGrantee*)lfirst(cell); + + if (grantee->rolname == NULL) { //若角色名为空(即PUBLIC) + /* 在安全模块中,禁止授权public操作 */ + if (stmt->is_grant && isSecurityMode && !IsInitdb && !u_sess->attr.attr_common.IsInplaceUpgrade && + !u_sess->exec_cxt.extension_is_valid) { + ereport(ERROR, (errmodule(MOD_SEC), errcode(ERRCODE_INVALID_GRANT_OPERATION), + errmsg("invalid grant operation"), + errdetail("Grant to public operation is forbidden in security mode."), + errcause("Grant to public operation is forbidden in security mode."), + erraction("Don't grant to public in security mode."))); + } + istmt.grantees = lappend_oid(istmt.grantees, ACL_ID_PUBLIC); + } else + istmt.grantees = lappend_oid(istmt.grantees, get_role_oid(grantee->rolname, false)); + } + + /* + * 转换 stmt->privileges(一系列访问权限节点) 为一个AclMode + * bitmask. 注意: 对象类型不能为 ACL_OBJECT_COLUMN. + */ + for (size_t idx = 0; idx < sizeof(acl_object_type) / sizeof(acl_object_type[0]); idx++) {//找到所属对象类型 + if (acl_object_type[idx].objtype == stmt->objtype) { //给对象赋予对应权限 + all_privileges = acl_object_type[idx].all_privileges; //记录该对象DML标准权限,为后续对比做准备 + all_ddl_privileges = acl_object_type[idx].all_ddl_privileges; //记录该对象DDL标准权限,为后续对比做准备 + errormsg = acl_object_type[idx].errormsg; //赋予错误信息 + type_flag = true; //找到合法对象break退出 + break; + } + } + if (!type_flag) { //如果对象没找到对应的合法类型则报错 + ereport(ERROR, (errmodule(MOD_SEC), errcode(ERRCODE_UNRECOGNIZED_NODE_TYPE), + errmsg("unrecognized object type"), errdetail("unrecognized GrantStmt.objtype: %d", (int)stmt->objtype), + errcause("The object type is not supported for GRANT/REVOKE."), + erraction("Check GRANT/REVOKE syntax to obtain the supported object types."))); + /* 编译无效 */ + all_privileges = ACL_NO_RIGHTS; + all_ddl_privileges = ACL_NO_DDL_RIGHTS; + errormsg = NULL; + } + + if (stmt->privileges == NIL) { //拥有所有特权 + istmt.all_privs = true; + + /* + * 将由内部例程根据对象类型转换为ACL_ALL_RIGHTS_* + */ + istmt.privileges = ACL_NO_RIGHTS; + istmt.ddl_privileges = ACL_NO_DDL_RIGHTS; + } else { + istmt.all_privs = false; + istmt.privileges = ACL_NO_RIGHTS; + istmt.ddl_privileges = ACL_NO_DDL_RIGHTS; + + foreach (cell, stmt->privileges) { + AccessPriv* privnode = (AccessPriv*)lfirst(cell); //获取AccessPriv的属性 + AclMode priv; + + /* + * 如果是列级规范,我们暂时将其放在col_privs中;但坚持认为这是为了关系。 + */ + if (privnode->cols) { //列级权限处理 + if (stmt->objtype != ACL_OBJECT_RELATION) //列权限若对应的不是关系类型则报错 + ereport(ERROR, (errmodule(MOD_SEC), errcode(ERRCODE_INVALID_GRANT_OPERATION), + errmsg("invalid grant/revoke operation"), + errdetail("Column privileges are only valid for relations."), + errcause("Column privileges are only valid for relations in GRANT/REVOKE."), + erraction("Use the column privileges only for relations."))); + + if (privnode->priv_name == NULL) { //若特权名为空,将privnode加入col_ddl_privs与col_ddl_privs + istmt.col_ddl_privs = lappend(istmt.col_ddl_privs, privnode); + istmt.col_privs = lappend(istmt.col_privs, privnode); + } else { + priv = string_to_privilege(privnode->priv_name);//将特权名转换为aclmode + if (ACLMODE_FOR_DDL(priv)) {//(判断)该特权是DDL类型 + istmt.col_ddl_privs = lappend(istmt.col_ddl_privs, privnode);//将privnode的权限加入col_ddl_privs + } else { //特权不是DDL类型 + istmt.col_privs = lappend(istmt.col_privs, privnode);//将privnode的权限加入col_privs + } + } + + continue; //继续处理下一权限 + } + + if (privnode->priv_name == NULL) { /* 特权名为空则报错*/ + ereport(ERROR, (errmodule(MOD_SEC), errcode(ERRCODE_INVALID_GRANT_OPERATION), + errmsg("invalid AccessPriv node"), errdetail("AccessPriv node must specify privilege or columns"), + errcause("System error."), erraction("Contact engineer to support."))); + } + + priv = string_to_privilege(privnode->priv_name);//将特权名转换为aclmode + if (stmt->objtype == ACL_OBJECT_NODEGROUP && strcmp(privnode->priv_name, "create") == 0) { + priv = ACL_ALL_RIGHTS_NODEGROUP; + } + + if (ACLMODE_FOR_DDL(priv)) { //(判断)该特权是DDL类型 + if (priv & ~((AclMode)all_ddl_privileges)) { + ereport(ERROR, (errmodule(MOD_SEC), errcode(ERRCODE_INVALID_GRANT_OPERATION), + errmsg(errormsg, privilege_to_string(priv)), errdetail("N/A"), + errcause("The privilege type is not supported for the object."), + erraction("Check GRANT/REVOKE syntax to obtain the supported privilege types " + "for the object type."))); + } + + istmt.ddl_privileges |= priv; //通过或运算保存权限 + } else {//该特权不是DDL类型 + if (priv & ~((AclMode)all_privileges)) { + ereport(ERROR, (errmodule(MOD_SEC), errcode(ERRCODE_INVALID_GRANT_OPERATION), + errmsg(errormsg, privilege_to_string(priv)), errdetail("N/A"), + errcause("The privilege type is not supported for the object."), + erraction("Check GRANT/REVOKE syntax to obtain the supported privilege types " + "for the object type."))); + } + + istmt.privileges |= priv; //通过或运算保存权限 + } + } + } + + ExecGrantStmt_oids(&istmt);//调用权限管理函数 +} +``` + +函数ExecuteGrantStmt在完成结构转换之后,调用函数ExecGrantStmt_oids。 + +*** + +#### ExecGrantStmt_oids + +函数ExecGrantStmt_oids根据对象类型(GrantObjectType objtype 参数)分别调用相应对象的权限管理函数。 + +>/src/common/backend/catalog/aclchk.cpp 684——743 + +```c++ +static void ExecGrantStmt_oids(InternalGrant* istmt) +{ /* 通过不同对象类型执行对应权限管理函数 */ + switch (istmt->objtype) { + case ACL_OBJECT_RELATION: + case ACL_OBJECT_SEQUENCE: + ExecGrant_Relation(istmt); //表类型 + break; + case ACL_OBJECT_DATABASE: + ExecGrant_Database(istmt);//数据库类型 + break; +······ //省略一系列对象的权限管理调用 + default://若不是以上类型就报错 +······ + } +} +``` + +对象类型定义在枚举类型GrantObjectType中。 + +函数ExecGrant_Relation用来处理表对象权限的授予或回收操作,入参为InternalGrant类型的变量,存储着授权或回收操作的操作对象信息、被授权者信息和权限信息。具体流程如下: + +![](https://forum.gitlink.org.cn/api/attachments/398620) + + +下面分析ExecGrant_Relation的源码,函数中发生错误会以Ereport()函数进行报告。 + +系统表pg_class存储数据库对象信息及其之间的关系,系统表pg_attribute存储关于表字段的信息。 + +以下是截取的pg_class字段(详细字段请查看[官方手册](https://opengauss.org/zh/docs/3.1.0/docs/Developerguide/PG_CLASS.html)) + +| 名称 | 类型 | 描述 | +| -------- | -------- | ------------------------------------------------------------ | +| oid | oid | 行标识符(隐含属性,必须明确选择) | +| relname | name | 表、索引、视图等对象的名称 | +| reltype | oid | 对应这个表的行类型的数据类型(索引为零,因为索引没有pg_type记录) | +| relowner | oid | 关系所有者 | +| relkind | “char” | r:表示普通表。i:表示索引。I:表示分区表GLOBAL索引。S:表示序列。L:表示Large序列。v:表示视图。c:表示复合类型。t:表示TOAST表。f:表示外表。m:表示物化视图。 | +| relnatts | smallint | 关系中用户字段数目(除了系统字段以外)。在pg_attribute里肯定有相同数目对应行。 | +| ··· | ··· | ··· | + +*** + +#### ExecGrant_Relation + +>/src/common/backend/catalog/aclchk.cpp 2100——2387 + +下列代码中: + +- 第3~5行定义了relation,attRelation和cell。 + +- 第7~8行打开系统表pg_class和系统表pg_attribute。 + +- 第10~254行内循环处理每一个istmt中的表对象。 + +- 第35行获取表的pg_class_tuple。 + +- 第68~90行从系统表pg_class中获取旧ACL值: + - 不存在旧ACL则通过acldefault函数新建一个具有默认权限的ACL。 + - 存在旧ACL将其存储为一个副本。 + +- 第98~178行处理表级别权限: + - 第111行通过调用select_best_grantor函数获取授权者对操作对象所拥有的授权权限。 + - 第135行通过restrict_and_check_grant函数计算得到实际需要授予或回收的权限。 + - 第142行通过merge_acl_with_grant函数生成新的ACL。 + - 第165行通过simple_heap_update函数更新系统表pg_class对应元组的ACL字段。 + +- 第184行开始处理列级权限: + - 第234~244行,若存在列级授权或回收,则调用ExecGrant_Attribute 函数处理。 + +- 第256~257行,关闭系统表pg_class和系统表pg_attribute。 + +```c++ +static void ExecGrant_Relation(InternalGrant* istmt) +{ + Relation relation = NULL; + Relation attRelation = NULL; + ListCell* cell = NULL; +/* 通过heap_open函数读取系统表pg_class和pg_attribute */ + relation = heap_open(RelationRelationId, RowExclusiveLock); //打开系统表pg_class + attRelation = heap_open(AttributeRelationId, RowExclusiveLock);//打开系统表pg_attribute + /* 循环处理每一个表对象 */ + foreach (cell, istmt->objects) { + Oid relOid = lfirst_oid(cell); //表对象oid + Datum aclDatum; + Form_pg_class pg_class_tuple = NULL; //Form_pg_class对应一个指向pg_class关系格式元组的指针 + bool isNull = false; + AclMode this_privileges[PRIVS_ATTR_NUM]; //此次处理的权限 + AclMode privileges, ddl_privileges; //定义DML和DDL的ACLMODE结构 + AclMode* col_privileges = NULL; //定义列级的DML权限 + AclMode* col_ddl_privileges = NULL; //定义列级的DDL权限 + int num_col_privileges; + bool have_col_privileges = false; + Acl* old_acl = NULL; + Acl* old_rel_acl = NULL; + int noldmembers; + Oid* oldmembers = NULL; + Oid ownerId; + HeapTuple tuple = NULL; + ListCell* cell_colprivs = NULL; + + /* 判断所要操作的表对象是否存在,若不存在则提示报错 */ + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid)); + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, (errmodule(MOD_SEC), errcode(ERRCODE_CACHE_LOOKUP_FAILED), + errmsg("cache lookup failed for relation %u", relOid), errdetail("N/A"), + errcause("System error."), erraction("Contact engineer to support."))); + pg_class_tuple = (Form_pg_class)GETSTRUCT(tuple);//获取待操作表的指针 + + ExecGrantRelationTypeCheck(pg_class_tuple, istmt, &privileges, &ddl_privileges);//执行授权关系类型检查,调整权限privileges和ddl_privileges + + /* + GRANT TABLE语法可以用于序列和非序列,因此必须查看relkind来确定支持的权限。 + */ + if (istmt->objtype == ACL_OBJECT_RELATION) {//如果对象类型为表或视图,则进行表权限检查 + ExecGrantRelationPrivilegesCheck(pg_class_tuple, &privileges, &ddl_privileges); + } + + /* + 设置数组,我们将在其中累积需要修改的任何列特权位。对数组进行索引,使条目[0]对应于FirstLowInvalidHeapAttributeNumber。 + */ + CatalogRelationBuildParam catalogParam = GetCatalogParam(relOid);//通过relOid获取参数 + if (catalogParam.oid != InvalidOid) + pg_class_tuple->relnatts = catalogParam.natts; //关系中用户字段数目 + num_col_privileges = pg_class_tuple->relnatts - FirstLowInvalidHeapAttributeNumber + 1; + col_privileges = (AclMode*)palloc0(num_col_privileges * sizeof(AclMode)); + col_ddl_privileges = (AclMode*)palloc0(num_col_privileges * sizeof(AclMode)); + have_col_privileges = false; + + /* + * 如果要撤销同时也是列特权的关系特权,也必须根据SQL规范从每个列中隐式撤销它们。(不需要在GRANT期间隐式添加列特权,因为权限检查代码总是检查关系和每列特权。) + */ + if (!istmt->is_grant && ((privileges & ACL_ALL_RIGHTS_COLUMN) != 0 || + (REMOVE_DDL_FLAG(ddl_privileges) & ACL_ALL_DDL_RIGHTS_COLUMN) != 0)) { + ······ + } + + /* + 系统表pg_class中获取旧ACL。若不存在旧的ACL,则新建一个ACL,若存在旧的ACL,则将旧的ACL存储为一个副本 + */ + ownerId = pg_class_tuple->relowner; //获得关系所有者ID + aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl, &isNull);//SysCacheGetAttr调用了heap_getattr操作接口 + if (isNull) { //不存在旧acl + switch (pg_class_tuple->relkind) { + case RELKIND_SEQUENCE: + case RELKIND_LARGE_SEQUENCE: + old_acl = acldefault(ACL_OBJECT_SEQUENCE, ownerId);//创建描述默认访问权限的ACL + break; + default: + old_acl = acldefault(ACL_OBJECT_RELATION, ownerId);//创建描述默认访问权限的ACL + break; + } + /* 根据目录,没有旧成员角色 */ + noldmembers = 0; + oldmembers = NULL; + } else {//存在旧acl + old_acl = DatumGetAclPCopy(aclDatum); + /* 获取现有ACL中提到的角色 */ + noldmembers = aclmembers(old_acl, &oldmembers); + } + + /* 需要原始rel ACL的额外副本来处理列 */ + old_rel_acl = aclcopy(old_acl); + + /* + * 处理表级别的权限 + */ + if (privileges != ACL_NO_RIGHTS || REMOVE_DDL_FLAG(ddl_privileges) != ACL_NO_RIGHTS) { + AclMode avail_goptions, avail_ddl_goptions; + Acl* new_acl = NULL; + Oid grantorId; + HeapTuple newtuple = NULL; + Datum values[Natts_pg_class]; + bool nulls[Natts_pg_class] = {false}; + bool replaces[Natts_pg_class] = {false}; + int nnewmembers; + Oid* newmembers = NULL; + AclObjectKind aclkind; + + /* 确定授予身份的ID,以及可用的授予选项 */ + /* 需要对模式dbe_perf和模式快照中的关系进行特殊处理 */ + Oid namespaceId = pg_class_tuple->relnamespace; + /* 获取授权者grantorId和授权者对该操作对象所拥有的授权权限avail_goptions */ + select_best_grantor(GetUserId(), privileges, ddl_privileges, old_acl, ownerId, + &grantorId, &avail_goptions, &avail_ddl_goptions, IsMonitorSpace(namespaceId)); + + /* 操作模式下的opradmin特殊处理 */ + if (isOperatoradmin(GetUserId()) && u_sess->attr.attr_security.operation_mode) { + grantorId = ownerId; + avail_goptions = ACL_GRANT_OPTION_FOR(privileges); + avail_ddl_goptions = ACL_GRANT_OPTION_FOR(REMOVE_DDL_FLAG(ddl_privileges)); + } + + switch (pg_class_tuple->relkind) { + case RELKIND_SEQUENCE: + case RELKIND_LARGE_SEQUENCE: + aclkind = ACL_KIND_SEQUENCE; + break; + default: + aclkind = ACL_KIND_CLASS; + break; + } + + /* + 将特权限制为我们可以实际授予的权限,并发出标准强制的警告和错误消息。 + 结合参数avail_goptions和SQL命令中给出的操作权限,计算出实际需要授予或回收的权限 + */ + restrict_and_check_grant(this_privileges, istmt->is_grant, + avail_goptions, avail_ddl_goptions, istmt->all_privs, privileges, ddl_privileges, relOid, grantorId, + aclkind, NameStr(pg_class_tuple->relname), 0, NULL); + + /* + * 生成新的ACL,并更新到系统表pg_class对应元组的ACL字段 + */ + new_acl = merge_acl_with_grant(old_acl, + istmt->is_grant, istmt->grant_option, istmt->behavior, istmt->grantees, + this_privileges, grantorId, ownerId); + + /* + * 我们需要新旧ACL的成员,以便更正共享的依赖项信息。 + */ + nnewmembers = aclmembers(new_acl, &newmembers); + + /* 已完成新ACL值的构建,现在将其插入 */ + errno_t rc = EOK; + rc = memset_s(values, sizeof(values), 0, sizeof(values)); + securec_check(rc, "\0", "\0"); + rc = memset_s(nulls, sizeof(nulls), false, sizeof(nulls)); + securec_check(rc, "\0", "\0"); + rc = memset_s(replaces, sizeof(replaces), false, sizeof(replaces)); + securec_check(rc, "\0", "\0"); + + replaces[Anum_pg_class_relacl - 1] = true; + values[Anum_pg_class_relacl - 1] = PointerGetDatum(new_acl); + + newtuple = heap_modify_tuple(tuple, RelationGetDescr(relation), values, nulls, replaces); + + simple_heap_update(relation, &newtuple->t_self, newtuple); + + /* 使目录索引保持最新 */ + CatalogUpdateIndexes(relation, newtuple); + + /* 更新共享依赖项ACL信息 */ + updateAclDependencies( + RelationRelationId, relOid, 0, ownerId, noldmembers, oldmembers, nnewmembers, newmembers); + + /* 重新编码授予关系的时间。 */ + recordRelationMTime(relOid, pg_class_tuple->relkind); + + pfree_ext(new_acl); + } + + /* + * 处理列级权限(如果指定或暗示了)。 + * 我们首先将用户指定的列特权扩展到数组中,然后遍历所有非空数组项。 + */ + foreach (cell_colprivs, istmt->col_privs) { + AccessPriv* col_privs = (AccessPriv*)lfirst(cell_colprivs); + + if (col_privs->priv_name == NULL) + privileges = ACL_ALL_RIGHTS_COLUMN; + else + privileges = string_to_privilege(col_privs->priv_name); + + if (privileges & ~((AclMode)ACL_ALL_RIGHTS_COLUMN)) + ereport(ERROR, (errmodule(MOD_SEC), errcode(ERRCODE_INVALID_GRANT_OPERATION), + errmsg("invalid privilege type %s for column", privilege_to_string(privileges)), errdetail("N/A"), + errcause("The privilege type is not supported for column object."), + erraction("Check GRANT/REVOKE syntax to obtain the supported privilege types " + "for column object."))); + + if (RELKIND_IS_SEQUENCE(pg_class_tuple->relkind) && (privileges & ~((AclMode)ACL_SELECT))) { + /* + * 序列上唯一允许的列权限是SELECT。 + * 这是一个警告而不是错误,因为我们这样做是为了关系级特权。 + */ + ereport(WARNING, (errcode(ERRCODE_INVALID_GRANT_OPERATION), + errmsg("sequence \"%s\" only supports SELECT column privileges", + NameStr(pg_class_tuple->relname)))); + + privileges &= (AclMode)ACL_SELECT; + } + + expand_col_privileges(col_privs->cols, relOid, privileges, col_privileges, num_col_privileges); + have_col_privileges = true; + } + + foreach (cell_colprivs, istmt->col_ddl_privs) { + AccessPriv* col_privs = (AccessPriv*)lfirst(cell_colprivs); + + if (col_privs->priv_name == NULL) + ddl_privileges = ACL_ALL_DDL_RIGHTS_COLUMN; //赋予列级所有DDL权限 + else + ddl_privileges = string_to_privilege(col_privs->priv_name); //根据priv_name赋予列级对应DDL权限 + + if (ddl_privileges & ~((AclMode)ACL_ALL_DDL_RIGHTS_COLUMN)) + ereport(ERROR, (errmodule(MOD_SEC), errcode(ERRCODE_INVALID_GRANT_OPERATION), + errmsg("invalid privilege type %s for column", privilege_to_string(ddl_privileges)), + errdetail("N/A"), errcause("The privilege type is not supported for column object."), + erraction("Check GRANT/REVOKE syntax to obtain the supported privilege types " + "for column object."))); + + expand_col_privileges(col_privs->cols, relOid, ddl_privileges, col_ddl_privileges, num_col_privileges); + have_col_privileges = true; + } +/* 若存在列级授权或回收,则调用ExecGrant_Attribute 函数处理 */ + if (have_col_privileges) { + AttrNumber i; + + for (i = 0; i < num_col_privileges; i++) { + if (col_privileges[i] == ACL_NO_RIGHTS && REMOVE_DDL_FLAG(col_ddl_privileges[i]) == ACL_NO_RIGHTS) + continue; + ExecGrant_Attribute(istmt, relOid, NameStr(pg_class_tuple->relname), + i + FirstLowInvalidHeapAttributeNumber, ownerId, col_privileges[i], col_ddl_privileges[i], + attRelation, old_rel_acl); + } + } + + pfree_ext(old_rel_acl); + pfree_ext(col_privileges); + pfree_ext(col_ddl_privileges); + + ReleaseSysCache(tuple);//释放系统缓存 + + /* 防止处理重复对象时出错 */ + CommandCounterIncrement(); + } + + heap_close(attRelation, RowExclusiveLock);//关闭系统表pg_attribute + heap_close(relation, RowExclusiveLock);//关闭系统表pg_class +} +``` + +>ExecGrant_Relation中涉及的主要函数可参见我们的另一篇博客——[权限管理——ExecGrant_Relation相关函数](https://forum.gitlink.org.cn/forums/8283/detail):https://forum.gitlink.org.cn/forums/8283/detail + + + +## 权限检查 + +### 数据库对象函数对应关系 + +用户在对数据库对象进行访问操作时,数据库会检查用户是否拥有该对象的操作权限。数据库通过查询数据库对象的访问控制列表检查用户对数据库对象的访问权限,**数据库对象的ACL保存在对应的系统表中,当被授予或回收对象权限时,系统表中保存的ACL权限位会被更新**。常用的数据库对象**权限检查函数**、**ACL检查函数**对应关系如下表所示。 + +| 对象 | 权限检查 | ACL检查 | +| -------------------- | -------------------------------- | ------------------------------- | +| table | pg_class_aclcheck | pg_class_aclmask | +| column | pg_attribute_aclcheck | pg_attribute_aclmask | +| database | pg_database_aclcheck | pg_database_aclmask | +| function | pg_proc_aclcheck | pg_proc_aclmask | +| language | pg_language_aclcheck | pg_language_aclmask | +| largeobject | pg_largeobject_aclcheck_snapshot | pg_largeobject_aclmask_snapshot | +| namespace | pg_namespace_aclcheck | pg_namespace_aclmask | +| tablespace | pg_tablespace_aclcheck | pg_tablespace_aclmask | +| foreign data wrapper | pg_foreign_data_wrapper_aclcheck | pg_foreign_data_wrapper_aclmask | +| foreign server | pg_foreign_server_aclcheck | pg_foreign_server_aclmask | +| type | pg_type_aclcheck | pg_type_aclmask | + +数据库对象与对应**ACL所在系统表**、**对象所有者检查函数**关系如下表所示: + +| 对象 | 系统表 | 所有者检查 | +| -------------------- | ------------------------------------------------------------ | ---------------------------------- | +| table | [PG_CLASS ](https://opengauss.org/zh/docs/3.1.0/docs/Developerguide/PG_CLASS.html) | pg_class_ownercheck | +| column | [PG_ATTRIBUTE ](https://opengauss.org/zh/docs/3.1.0/docs/Developerguide/PG_ATTRIBUTE.html) | NA | +| database | [PG_DATABASE](https://opengauss.org/zh/docs/3.1.0/docs/Developerguide/PG_DATABASE.html) | pg_database_ownercheck | +| function | [PG_PROC](https://opengauss.org/zh/docs/3.1.0/docs/Developerguide/PG_PROC.html) | pg_proc_ownercheck | +| language | [PG_LANGUAGE](https://opengauss.org/zh/docs/3.1.0/docs/Developerguide/PG_LANGUAGE.html) | pg_language_ownercheck | +| largeobject | [PG_LARGEOBJECT_METADATA](https://opengauss.org/zh/docs/3.1.0/docs/Developerguide/PG_LARGEOBJECT_METADATA.html) | pg_largeobject_ownercheck | +| namespace | [PG_NAMESPACE](https://opengauss.org/zh/docs/3.1.0/docs/Developerguide/PG_NAMESPACE.html) | pg_namespace_ownercheck | +| tablespace | [PG_TABLESPACE](https://opengauss.org/zh/docs/3.1.0/docs/Developerguide/PG_TABLESPACE.html) | pg_tablespace_ownercheck | +| foreign data wrapper | [PG_FOREIGN_DATA_WRAPPER](https://opengauss.org/zh/docs/3.1.0/docs/Developerguide/PG_FOREIGN_DATA_WRAPPER.html) | pg_foreign_data_wrapper_ownercheck | +| foreign server | [PG_FOREIGN_SERVER](https://opengauss.org/zh/docs/3.1.0/docs/Developerguide/PG_FOREIGN_SERVER.html) | pg_foreign_data_wrapper_ownercheck | +| type | [PG_TYPE](https://opengauss.org/zh/docs/3.1.0/docs/Developerguide/PG_TYPE.html) | pg_type_ownercheck | + +> 系统表是openGauss存放结构元数据的地方,它是openGauss数据库系统运行控制信息的来源,是数据库系统的核心组成部分。 + +下面以表对象的权限管理为例检查过程说明。 + +table对象对应的系统表为pg_class,在系统表pg_class中 + +- **relname**表示表、索引、视图等对象的名称 +- **relowner**表示关系所有者 +- **relacl**是aclitem[]类型,表示了访问权限 + +```shell +rolename=xxxx/yyyy —赋予一个角色的权限 +=xxxx/yyyy —赋予public的权限(xxxx表示赋予的权限,yyyy表示授予这个权限的角色) +``` + +以Tryme平台为例查看系统表pg_class详情 + +![](https://forum.gitlink.org.cn/api/attachments/398621) + + +我们可以看到pg_class中对应的每个对象的信息。 + +### 表对象权限检查 + +#### pg_class_aclcheck + +> /src/common/backend/catalog/alchk.cpp 6338——6344 + +```c++ +/* + * 用于检查用户对表的访问权限的导出例程 + * + * 如果用户具有“mode”标识的任何特权,则返回ACLCHECK_OK;否则将返回适当的错误代码(实际上,总是ACLCHECK_NO_PRIV)。 + */ +AclResult pg_class_aclcheck(Oid table_oid, Oid roleid, AclMode mode, bool check_nodegroup) +{ + if (pg_class_aclmask(table_oid, roleid, mode, ACLMASK_ANY, check_nodegroup) != 0) + return ACLCHECK_OK; + else + return ACLCHECK_NO_PRIV; +} +``` + +pg_class_aclcheck函数有4个输入参数 + +| 参数 | 含义 | +| --------------- | ------------------------------------------------------------ | +| table_oid | 表示待检查的表 | +| roleid | 表示待检查的用户或角色 | +| mode | 待检查的权限
(此权限可以是一种权限也可以是多种权限的组合) | +| check_nodegroup | 表示是否检查nodegroup逻辑集群权限
(如果调用时不给此参数赋值则默认为true) | + +函数返回值为枚举类型AclResult,如果检查结果**有权限**返回`ACLCHECK_OK`,**无权限**则返回`ACLCHECK_NO_PRIV` + +#### pg_class_aclmask + +`pg_class_aclmask`比`pg_class_aclcheck`多了一个输入参数,即第四个参数`AclMaskHow how`,how有两种取值,分别是`ACLMASK_ALL`和`ACLMASK_ANY`。其中: + +- ACLMASK_ALL表示需要满足待检查权限mode中的所有权限 +- ACLMASK_ANY表示只需满足待检查权限mode中的一种权限即可 + +> /src/common/backend/catalog/alchk.cpp 5066——5240 + +```c++ +/* + * 用于检查用户对表的权限的导出例程 + */ +AclMode pg_class_aclmask(Oid table_oid, Oid roleid, AclMode mask, AclMaskHow how, bool check_nodegroup) +{ + AclMode result; + HeapTuple tuple = NULL; + Form_pg_class classForm; + Datum aclDatum; + bool isNull = false; + Acl* acl = NULL; + Oid ownerId; + + bool is_ddl_privileges = ACLMODE_FOR_DDL(mask); + /* 从Aclitem中删除ddl特权标志 */ + mask = REMOVE_DDL_FLAG(mask); + + /* + * 必须从pg_class获取关系的元组 + */ + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(table_oid));// 判断所要操作的表对象是否存在,若不存在则提示报错 + if (!HeapTupleIsValid(tuple)) + ereport(ERROR, (errmodule(MOD_SEC), errcode(ERRCODE_UNDEFINED_TABLE), + errmsg("relation with OID %u does not exist", table_oid), errdetail("N/A"), + errcause("System error."), erraction("Contact engineer to support."))); + classForm = (Form_pg_class)GETSTRUCT(tuple); + + /* 检查当前用户是否具有此组的权限 */ + if (IS_PGXC_COORDINATOR && !IsInitdb && check_nodegroup && is_pgxc_class_table(table_oid) && + roleid != classForm->relowner) { + Oid group_oid = get_pgxc_class_groupoid(table_oid); + AclResult aclresult = ACLCHECK_OK; + if (InvalidOid == group_oid) { + ereport(ERROR, (errmodule(MOD_SEC), errcode(ERRCODE_DATA_EXCEPTION), errmsg("invalid group"), + errdetail("computing nodegroup is not a valid group."), + errcause("System error."), erraction("Contact engineer to support."))); + } else { + aclresult = pg_nodegroup_aclcheck(group_oid, roleid, ACL_USAGE); + if (aclresult != ACLCHECK_OK) { + aclcheck_error(aclresult, ACL_KIND_NODEGROUP, get_pgxc_groupname(group_oid)); + } + } + } + + /* + 拒绝任何人更新系统目录的权限,除非pg_authid。已设置rolcatupdate。(这是为了让超级用户保护自己。)如果g_instance.attr.attr_common.allowSystemTableMods,也允许这样做。 +从7.4开始,我们有一些可更新的系统视图;那些不应该这样保护。假设视图规则可以自行处理。ACL_USAGE是指我们是否有系统序列。 + */ + if (!is_ddl_privileges && (mask & (ACL_INSERT | ACL_UPDATE | ACL_DELETE | ACL_TRUNCATE | ACL_USAGE)) + && IsSystemClass(classForm) && + classForm->relkind != RELKIND_VIEW && classForm->relkind != RELKIND_CONTQUERY && !has_rolcatupdate(roleid) && + !g_instance.attr.attr_common.allowSystemTableMods) { +#ifdef ACLDEBUG + elog(DEBUG2, "permission denied for system catalog update"); +#endif + mask &= ~(ACL_INSERT | ACL_UPDATE | ACL_DELETE | ACL_TRUNCATE | ACL_USAGE); + } + + /* + 对于模式dbe_perf和模式快照中的关系,初始用户和monitorsdmin将绕过所有权限检查。 + */ + Oid namespaceId = classForm->relnamespace; + if (IsMonitorSpace(namespaceId) && (roleid == INITIAL_USER_ID || isMonitoradmin(roleid))) { + ReleaseSysCache(tuple); + return mask; + } + + /* 无法修改区块链历史表 */ + if (table_oid == GsGlobalChainRelationId || classForm->relnamespace == PG_BLOCKCHAIN_NAMESPACE) { + if (isRelSuperuser() || isAuditadmin(roleid)) { + mask &= ~(ACL_INSERT | ACL_UPDATE | ACL_DELETE | ACL_TRUNCATE | ACL_USAGE | ACL_REFERENCES); + } else { + mask &= ACL_NO_RIGHTS; + } + ReleaseSysCache(tuple); + return mask; + } + + /* 否则,超级用户将绕过所有权限检查,访问独立角色的对象除外。 */ + /* 数据库安全:支持特权分离。 */ + if (!is_ddl_privileges && !IsMonitorSpace(namespaceId) && (superuser_arg(roleid) || systemDBA_arg(roleid)) && + ((classForm->relowner == roleid) || !is_role_independent(classForm->relowner) || + independent_priv_aclcheck(mask, classForm->relkind))) { +#ifdef ACLDEBUG + elog(DEBUG2, "OID %u is system admin, home free", roleid); +#endif + ReleaseSysCache(tuple); + return mask; + } + + if (is_security_policy_relation(table_oid) && isPolicyadmin(roleid)) { + ReleaseSysCache(tuple); + return mask; + } + /* 当满足pg_catalog时。gs_global_config,用户具有CREATEROLE权限,绕过所有权限检查 */ + if (table_oid == GsGlobalConfigRelationId && has_createrole_privilege(roleid)) { + ReleaseSysCache(tuple); + return mask; + } + + /* + * 当operation_mode=on时,允许opradmin更新pgxc_node并选择所有系统类; + * 还允许opradmin访问用户的表 + */ + if (!is_ddl_privileges && isOperatoradmin(roleid) && u_sess->attr.attr_security.operation_mode) { + if (table_oid == PgxcNodeRelationId) { + mask |= ACL_UPDATE; + ReleaseSysCache(tuple); + return mask; + } else if (IsSystemClass(classForm)) { + ReleaseSysCache(tuple); + return mask; + } else if ((mask & (ACL_SELECT | ACL_INSERT | ACL_TRUNCATE | ACL_USAGE)) && + !is_role_independent(classForm->relowner)) { + mask &= ~(ACL_UPDATE | ACL_DELETE); + ReleaseSysCache(tuple); + return mask; + } + } + + /* + * 正常情况:从pg_class获取关系的ACL + */ + ownerId = classForm->relowner; + + aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl, &isNull); + if (isNull) { //判断是否有ACL + /* 没有ACL,因此生成默认ACL */ + switch (classForm->relkind) { + case RELKIND_SEQUENCE: + case RELKIND_LARGE_SEQUENCE: + acl = acldefault(ACL_OBJECT_SEQUENCE, ownerId); + break; + default: + acl = acldefault(ACL_OBJECT_RELATION, ownerId); + break; + } + aclDatum = (Datum)0; + } else { + /* 必要时绕开rel的ACL */ + acl = DatumGetAclP(aclDatum); + } + + if (check_nodegroup) { + check_nodegroup_privilege(roleid, ownerId, ACL_USAGE); + } + + if (is_ddl_privileges) { + mask = ADD_DDL_FLAG(mask); + } + if (IsMonitorSpace(namespaceId)) { + result = aclmask_without_sysadmin(acl, roleid, ownerId, mask, how); + } else { + result = aclmask(acl, roleid, ownerId, mask, how);//权限位校验 + } + + /* 如果我们有一份废弃的拷贝,释放它 */ + FREE_DETOASTED_ACL(acl, aclDatum); + + if ((how == ACLMASK_ANY && result != 0) || IsSysSchema(namespaceId)) { + ReleaseSysCache(tuple); + return result; + } + if (is_ddl_privileges) { + result = check_ddl_privilege(classForm->relkind, mask, roleid, result); + } else { + result = check_dml_privilege(classForm, mask, roleid, result); + } + ReleaseSysCache(tuple); + return result; +} +``` + + + +## 总结 + +权限是主动控制数据库安全的重要手段。openGauss通过ACL访问控制列表进行权限管理,用户可以通过GRANT/REVOKE指令进行权限的授予与撤回。在权限管理的规则中,需要确定授权人和被授权人,以及授予的权限是授权权限还是操作权限(这些都在ACL中定义,详细的规则可参考官方手册)。从权限管理的代码中,我们可以看到权限管理的具体流程(不同对象有不同的处理函数),包括acl数据结构的转换,对系统表的更新等,通过学习源码可以提高我们对权限的理解和管理水平。 \ No newline at end of file diff --git "a/TestTasks/KuanXY/\345\215\232\345\256\242\344\273\243\347\240\201\346\240\207\346\263\250\346\200\273\347\273\223-\345\205\261\345\220\214\345\255\246\344\271\240-openGauss.md" "b/TestTasks/KuanXY/\345\215\232\345\256\242\344\273\243\347\240\201\346\240\207\346\263\250\346\200\273\347\273\223-\345\205\261\345\220\214\345\255\246\344\271\240-openGauss.md" new file mode 100644 index 0000000000000000000000000000000000000000..726de8e188fb47eb0a45a29fbcbb0a92247b3a6f --- /dev/null +++ "b/TestTasks/KuanXY/\345\215\232\345\256\242\344\273\243\347\240\201\346\240\207\346\263\250\346\200\273\347\273\223-\345\205\261\345\220\214\345\255\246\344\271\240-openGauss.md" @@ -0,0 +1,142 @@ +# 博客/代码标注总结-共同学习-openGauss + +[TOC] + +## 一、工作概述 + +**团队成员**: + +- 关梓豪 +- 袁建硕 +- 吴昱良 + +本次比赛我们团队聚焦于openGauss**安全管理模块**进行学习和分析注释,主要包括以下及部分内容: + +![](https://forum.gitlink.org.cn/api/attachments/398626) + +## 二、openGauss安全简介 + +随着大数据,网络等的发展,当今人们获取数据的方式愈加便捷,数据也成为了数字经济时代的重要生产要素。数据库作为大规模存储数据的基础软件,是确保信息系统稳定运行的基石,保护数据库的数据安全则是重中之重。针对安全,openGauss提出了一系列解决方案,实现了对数据的全生命周期进行安全保护。 + +![](https://forum.gitlink.org.cn/api/attachments/398625) + +针对安全的特性,可以主要归纳出几个**安全目标**: + +![](https://forum.gitlink.org.cn/api/attachments/398624) + +为了达到以上的安全目标,安全管理机制主要可以分为主动控制与事后追责的两类方式。 + +**主动控制**:可以采用**数据加密、访问控制**等方式在数据库处理数据的过程中进行主动控制,从而达到保障数据隐私,限制用户权限等目的。 + +**事后追责**:可以采用**安全审计、完整性验证**等方式进行事后追责。从而达到恢复数据安全状态的目的,同时通过恶意行为的追踪,也可形成一定的威慑效果,间接保护数据安全。 + +我们所学习分析的**安全管理部分**就主要涉及了以上两方面。 + +下图为openGauss逻辑结构图。 + +![](https://forum.gitlink.org.cn/api/attachments/398623) + + +可以看到,openGauss涉及的技术非常广泛,而**安全管理**是数据库最基础也是最必不可少的模块之一。 + +安全管理模块是openGauss安全能力的重要组成部分,其定义了相关的**安全认证机制**,保障客户端与服务端的身份认证,建立可信连接,是对外的第一道防线;通过**角色和权限管理机制**,限制用户的数据库权限,保障数据库的行为安全;通过**审计追踪机制**,对数据库行为进行审计追踪,实现恶意行为检测和追责,保障数据库的安全运维;通过多种**数据加密**方式,实现数据库数据存储和计算安全。 + + + +## 三、代表博客 + +本队针对openGauss**安全管理模块**进行了学习和源码分析,并将学习分析和源码注释结果凝聚为五篇博客,他们分别为: + +1. **安全认证机制解析** + +《openGauss安全机制之安全认证机制》:https://forum.gitlink.org.cn/forums/8360/detail + +*** + +> 主要内容: + + 安全认证机制是openGauss服务端与客户端互相确认对方的真实身份,进行可信连接与业务正常开展的基础。openGauss采用的认证机制是**RFC5802认证协议**——即SCRAM(Salted Challenge Response Authentication Mechanism,是指 `Salted质询响应身份验证机制` 或者 `基于盐值的质询响应身份验证` 机制)标准流程中的协议。 + + 认证过程中客户端和服务器需要双向认证,openGauss提供多种认证方式,可以选择使用不同的加密算法(如MD5,SHA256,SM3算法等)。认证过程中设计多次加密,通过多次密钥的生成达到,密码明文不会发送到服务端,用户可以指定PBKDF2的迭代次数,盐值与哈希算法保证了用户密码不会泄露(防止彩虹攻击),同时随机数token保证了每次认证中发送的证明都不一样,从而有效防止重放攻击。 + + 在实现安全认证的同时,要确保认证过程中真实用户的隐私身份信息不被泄露。 + + + +2. **身份认证与口令存储解析** + +《openGauss安全机制之口令存储》:https://forum.gitlink.org.cn/forums/8308/detail + +*** + +> 主要内容: + + 在openGauss中,数据库访问者只有完成身份识别并通过认证校验机制,才可以建立访问通道从事数据库管理活动。身份认证机制要解决的核心问题是谁可以访问数据。因此,在定义身份时,除了描述访问用户,还要清晰定义整个过程中以何种方法访问、从何处访问、访问哪个数据库的问题。 + + 在解决了身份认证的问题,我们要探讨身份认证的关键凭证:**口令**。口令的加密与存储作为身份认证的重要一环,尤其是在面向庞大的价值数据面前,安全性与可靠性格外重要。我们将在本文介绍openGauss支持的三种加密方式,并重点分析新支持的加密算法SM3。 + + + +3. **角色创建与管理解析** + +《openGauss安全机制之角色创建、管理与认证》:https://forum.gitlink.org.cn/forums/8357/detail + +*** + +> 主要内容: + + 在数据库中,各种各样的角色拥有不同的权限,不同的操作。对角色的区分管理及安全管控十分重要。角色是拥有数据库对象和权限的实体,在不同的环境中角色可以是一个用户、一个组或者两者均有。不同角色对应的角色属性options自然也不同,而不同的角色属性也是区分不同身份的重要特征。 + + 同时,我们要对不同的角色进行管理。包括修改角色属性、删除角色、授予和回收角色。而每一次对角色的管理操作,都要进行一套系列规范的流程来确保和维护角色权限,避免越级操作和非法操作。 + + + +4. **权限管理与检查解析** + +《openGauss安全机制之权限管理与检查》:https://forum.gitlink.org.cn/forums/8363/detail + +*** + +> 主要内容: + + 在openGauss数据库中,用户执行任何命令都需拥有对应的权限(包括`系统权限`和`对象权限`),通过合理地分配和回收权限保护数据库的正常使用,防止用户越级滥用权限。权限在数据库中通过`ACL(访问控制列表)`定义了用户的授权权限及操作权限。 + + 对象权限存储于系统表中,用户可以通过GRANT/REVOKE语句进行权限的授予或撤回,openGauss通过ExecuteGrantStmt函数实现GRANT/REVOKE语句,该函数对权限数据结构进行转化,然后送入具体对象的执行函数中进行权限的管理,对系统表相关字段进行更新。 + + 通过权限管理与检查,主动控制了用户的行为,从而维护数据库安全。 + + + +5. **审计与追踪解析** + +《openGauss安全机制之审计与追踪》:https://forum.gitlink.org.cn/forums/8311/detail + +*** + +> 主要内容: + + 审计机制和审计追踪机制能够对用户的日常行为进行记录和分析,从而规避风险,提高安全性。同时,数据库的审计一定程度上起到威慑作用,将审计作为证据,依靠法律武器积极维权,也是保护数据安全的重要措施之一。当然,审计员通过查询审计记录也可以及时定位到风险存在处,及时修复漏洞。 + + 本文将介绍审计日志的设计、对审计日志文件的权限管理以及审计执行的原理。结合具象化的例子去分析审计条例的产生和审计执行的全流程。 + + + +**以上就是我们的代表博客** + +*** + +除以上博文外,还有一些相关博文供大家参考,希望对安全管理模块的学习理解有所帮助: + +>- [源码中的对象类型及权限定义](https://forum.gitlink.org.cn/forums/8284/detail):https://forum.gitlink.org.cn/forums/8284/detail +>- [权限管理——ExecGrant_Relation相关函数](https://forum.gitlink.org.cn/forums/8283/detail):https://forum.gitlink.org.cn/forums/8283/detail +>- [特权名转换函数——string_to_privilege](https://forum.gitlink.org.cn/forums/8282/detail):https://forum.gitlink.org.cn/forums/8282/detail +>- [基于docker搭建openGauss及简单操作](https://forum.gitlink.org.cn/forums/8267/detail):https://forum.gitlink.org.cn/forums/8267/detail +>- [openGauss权限管理基本操作](https://forum.gitlink.org.cn/forums/8258/detail):https://forum.gitlink.org.cn/forums/8258/detail + + + +## 四、总结与展望 + + 此次比赛是我们的一次难忘经历,极大地提升了我们的学习分析能力。万事开头难,最开始比赛有点难以下手,不过经过调整静下心来阅读和学习,终于有所收获。在博客撰写和源码阅读分析过程中,我们查阅了很多资料,结合自己的理解对源码进行注释,也学习到了很多安全方面的知识,“路漫漫其修远兮,吾将上下而求索”,未来我们依旧会在安全领域继续学习探索。 + + 这次比赛是我们的起点,相信未来无论是我们还是openGauss都会蓬勃发展,展现无限活力。 \ No newline at end of file