[fix](auth) fix losing global priv bug and refactor default role name (#16966)
This PR mainly changes: When upgrading from old version to master, the ADMIN_PRIV for normal user may be lost. This may only happen if: Create a user with ADMIN_PRIV privilege. Upgrade Doris to v1.2.x or master before the meta image which contains the edit log in step 1 is generate. And the ADMIN_PRIV will be lost in Global Privileges This PR will rectify this bug and set ADMIN_PRIV to the right place Refactor the user's implicit role name In [feature](auth)Implementing privilege management with rbac model #16091, we refactor the Doris auth model by introducing RBAC. And each user will have an implicit role, named with prefix default_role_rbac_. But it has wrong format like: default_role_rbac_'default_cluster:user1'@'%' This PR change the role name's format, like: default_role_rbac_user1@% default_role_rbac_user2@[domain] NOTICE: this change may cause incompatible metadata, but since [feature](auth)Implementing privilege management with rbac model #16091 is not released, we should fix it soon. Add a new session variable show_user_default_role When set to true, it will show implicit role of user in the result of show roles stmt. Default is false
This commit is contained in:
@ -121,7 +121,7 @@ public class PaloFe {
|
||||
// check command line options
|
||||
checkCommandLineOptions(cmdLineOpts);
|
||||
|
||||
LOG.info("Palo FE starting...");
|
||||
LOG.info("Doris FE starting...");
|
||||
|
||||
FrontendOptions.init();
|
||||
|
||||
@ -196,7 +196,7 @@ public class PaloFe {
|
||||
|
||||
/*
|
||||
* -v --version
|
||||
* Print the version of Palo Frontend
|
||||
* Print the version of Doris Frontend
|
||||
* -h --helper
|
||||
* Specify the helper node when joining a bdb je replication group
|
||||
* -i --image
|
||||
@ -222,7 +222,7 @@ public class PaloFe {
|
||||
private static CommandLineOptions parseArgs(String[] args) {
|
||||
CommandLineParser commandLineParser = new DefaultParser();
|
||||
Options options = new Options();
|
||||
options.addOption("v", "version", false, "Print the version of Palo Frontend");
|
||||
options.addOption("v", "version", false, "Print the version of Doris Frontend");
|
||||
options.addOption("h", "helper", true, "Specify the helper node when joining a bdb je replication group");
|
||||
options.addOption("i", "image", true, "Check if the specified image is valid");
|
||||
options.addOption("b", "bdb", false, "Run bdbje debug tools");
|
||||
|
||||
@ -27,6 +27,7 @@ import org.apache.doris.common.PatternMatcherWrapper;
|
||||
import org.apache.doris.common.io.Text;
|
||||
import org.apache.doris.common.io.Writable;
|
||||
import org.apache.doris.mysql.privilege.Auth;
|
||||
import org.apache.doris.mysql.privilege.RoleManager;
|
||||
import org.apache.doris.persist.gson.GsonPostProcessable;
|
||||
import org.apache.doris.persist.gson.GsonUtils;
|
||||
import org.apache.doris.thrift.TUserIdentity;
|
||||
@ -210,6 +211,20 @@ public class UserIdentity implements Writable, GsonPostProcessable {
|
||||
return tUserIdent;
|
||||
}
|
||||
|
||||
// return default_role_rbac_username@host or default_role_rbac_username@[domain]
|
||||
public String toDefaultRoleName() {
|
||||
StringBuilder sb = new StringBuilder(
|
||||
RoleManager.DEFAULT_ROLE_PREFIX + ClusterNamespace.getNameFromFullName(user) + "@");
|
||||
if (isDomain) {
|
||||
sb.append("[");
|
||||
}
|
||||
sb.append(host);
|
||||
if (isDomain) {
|
||||
sb.append("]");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static UserIdentity read(DataInput in) throws IOException {
|
||||
// Use Gson in the VERSION_109
|
||||
if (Env.getCurrentEnvJournalVersion() < FeMetaVersion.VERSION_109) {
|
||||
|
||||
@ -1351,6 +1351,8 @@ public class Env {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auth.rectifyPrivs();
|
||||
}
|
||||
|
||||
// start all daemon threads only running on Master
|
||||
@ -1822,10 +1824,10 @@ public class Env {
|
||||
return checksum;
|
||||
}
|
||||
|
||||
public long loadPaloAuth(DataInputStream dis, long checksum) throws IOException {
|
||||
public long loadAuth(DataInputStream dis, long checksum) throws IOException {
|
||||
// CAN NOT use Auth.read(), cause this auth instance is already passed to DomainResolver
|
||||
auth.readFields(dis);
|
||||
LOG.info("finished replay paloAuth from image");
|
||||
LOG.info("finished replay auth from image");
|
||||
return checksum;
|
||||
}
|
||||
|
||||
@ -2096,7 +2098,7 @@ public class Env {
|
||||
return checksum;
|
||||
}
|
||||
|
||||
public long savePaloAuth(CountingDataOutputStream dos, long checksum) throws IOException {
|
||||
public long saveAuth(CountingDataOutputStream dos, long checksum) throws IOException {
|
||||
auth.write(dos);
|
||||
return checksum;
|
||||
}
|
||||
|
||||
@ -84,7 +84,7 @@ public class Auth implements Writable {
|
||||
private static final Logger LOG = LogManager.getLogger(Auth.class);
|
||||
|
||||
// root user's role is operator.
|
||||
// each Palo system has only one root user.
|
||||
// each Doris system has only one root user.
|
||||
public static final String ROOT_USER = "root";
|
||||
public static final String ADMIN_USER = "admin";
|
||||
// unknown user does not have any privilege, this is just to be compatible with old version.
|
||||
@ -1222,13 +1222,13 @@ public class Auth implements Writable {
|
||||
.getNameFromFullName(user.getUserIdentity().getQualifiedUser()))
|
||||
.concat("\'@\'").concat(user.getUserIdentity().getHost()).concat("\'");
|
||||
String isGrantable = tablePrivEntry.getPrivSet().get(2) ? "YES" : "NO"; // GRANT_PRIV
|
||||
for (Privilege paloPriv : tablePrivEntry.getPrivSet().toPrivilegeList()) {
|
||||
if (!Privilege.privInDorisToMysql.containsKey(paloPriv)) {
|
||||
for (Privilege priv : tablePrivEntry.getPrivSet().toPrivilegeList()) {
|
||||
if (!Privilege.privInDorisToMysql.containsKey(priv)) {
|
||||
continue;
|
||||
}
|
||||
TPrivilegeStatus status = new TPrivilegeStatus();
|
||||
status.setTableName(tblName);
|
||||
status.setPrivilegeType(Privilege.privInDorisToMysql.get(paloPriv));
|
||||
status.setPrivilegeType(Privilege.privInDorisToMysql.get(priv));
|
||||
status.setGrantee(grantee);
|
||||
status.setSchema(dbName);
|
||||
status.setIsGrantable(isGrantable);
|
||||
@ -1273,12 +1273,12 @@ public class Auth implements Writable {
|
||||
.getNameFromFullName(user.getUserIdentity().getQualifiedUser()))
|
||||
.concat("\'@\'").concat(user.getUserIdentity().getHost()).concat("\'");
|
||||
String isGrantable = dbPrivEntry.getPrivSet().get(2) ? "YES" : "NO"; // GRANT_PRIV
|
||||
for (Privilege paloPriv : dbPrivEntry.getPrivSet().toPrivilegeList()) {
|
||||
if (!Privilege.privInDorisToMysql.containsKey(paloPriv)) {
|
||||
for (Privilege priv : dbPrivEntry.getPrivSet().toPrivilegeList()) {
|
||||
if (!Privilege.privInDorisToMysql.containsKey(priv)) {
|
||||
continue;
|
||||
}
|
||||
TPrivilegeStatus status = new TPrivilegeStatus();
|
||||
status.setPrivilegeType(Privilege.privInDorisToMysql.get(paloPriv));
|
||||
status.setPrivilegeType(Privilege.privInDorisToMysql.get(priv));
|
||||
status.setGrantee(grantee);
|
||||
status.setSchema(dbName);
|
||||
status.setIsGrantable(isGrantable);
|
||||
@ -1318,8 +1318,8 @@ public class Auth implements Writable {
|
||||
.concat(ClusterNamespace.getNameFromFullName(user.getUserIdentity().getQualifiedUser()))
|
||||
.concat("\'@\'").concat(user.getUserIdentity().getHost()).concat("\'");
|
||||
String isGrantable = privEntry.getPrivSet().get(2) ? "YES" : "NO"; // GRANT_PRIV
|
||||
for (Privilege paloPriv : privEntry.getPrivSet().toPrivilegeList()) {
|
||||
if (paloPriv == Privilege.ADMIN_PRIV) {
|
||||
for (Privilege globalPriv : privEntry.getPrivSet().toPrivilegeList()) {
|
||||
if (globalPriv == Privilege.ADMIN_PRIV) {
|
||||
// ADMIN_PRIV includes all privileges of table and resource.
|
||||
for (String priv : Privilege.privInDorisToMysql.values()) {
|
||||
TPrivilegeStatus status = new TPrivilegeStatus();
|
||||
@ -1330,11 +1330,11 @@ public class Auth implements Writable {
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!Privilege.privInDorisToMysql.containsKey(paloPriv)) {
|
||||
if (!Privilege.privInDorisToMysql.containsKey(globalPriv)) {
|
||||
continue;
|
||||
}
|
||||
TPrivilegeStatus status = new TPrivilegeStatus();
|
||||
status.setPrivilegeType(Privilege.privInDorisToMysql.get(paloPriv));
|
||||
status.setPrivilegeType(Privilege.privInDorisToMysql.get(globalPriv));
|
||||
status.setGrantee(grantee);
|
||||
status.setIsGrantable(isGrantable);
|
||||
userPrivResult.add(status);
|
||||
@ -1403,7 +1403,6 @@ public class Auth implements Writable {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//tmp for current user can only has one role
|
||||
private void setRoleToUser(UserIdentity userIdent, String role) throws DdlException {
|
||||
// 1. check if role exist
|
||||
@ -1416,10 +1415,18 @@ public class Auth implements Writable {
|
||||
userRoleManager.addUserRole(userIdent, roleManager.getUserDefaultRoleName(userIdent));
|
||||
}
|
||||
|
||||
public static Auth read(DataInput in) throws IOException {
|
||||
Auth auth = new Auth();
|
||||
auth.readFields(in);
|
||||
return auth;
|
||||
|
||||
/**
|
||||
* This is a bug that if created a normal user and grant it with ADMIN_PRIV/RESOURCE_PRIV/NODE_PRIV
|
||||
* before v1.2, and then upgrade to v1.2, these privileges will be set in catalog level, but it should be
|
||||
* in global level.
|
||||
* This method will rectify this bug. And it's logic is same with userPrivTable.degradeToInternalCatalogPriv(),
|
||||
* but userPrivTable.degradeToInternalCatalogPriv() only handle the info in images, not in edit log.
|
||||
* This rectifyPrivs() will be called after image and edit log replayed.
|
||||
* So it will rectify the bug in both images and edit log.
|
||||
*/
|
||||
public void rectifyPrivs() {
|
||||
roleManager.rectifyPrivs();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1450,7 +1457,7 @@ public class Auth implements Writable {
|
||||
catalogPrivTable = (CatalogPrivTable) PrivTable.read(in);
|
||||
} else {
|
||||
catalogPrivTable = userPrivTable.degradeToInternalCatalogPriv();
|
||||
LOG.info("Load PaloAuth from meta version < {}, degrade UserPrivTable to CatalogPrivTable",
|
||||
LOG.info("Load auth from meta version < {}, degrade UserPrivTable to CatalogPrivTable",
|
||||
FeMetaVersion.VERSION_111);
|
||||
}
|
||||
DbPrivTable dbPrivTable = (DbPrivTable) PrivTable.read(in);
|
||||
|
||||
@ -532,6 +532,45 @@ public class Role implements Writable, GsonPostProcessable {
|
||||
tablePrivTable.revoke(entry, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* eg:
|
||||
* Before, tblPatternToPrivs has 1 entry: ctl.*.* -> SELECT_PRIV, ADMIN_PRIV, ALTER_PRIV
|
||||
* After, tblPatternToPrivs has 2 entries:
|
||||
* *.*.* -> ADMIN_PRIV
|
||||
* ctl.*.* -> SELECT_PRIV, ALTER_PRIV
|
||||
*/
|
||||
public void rectifyPrivs() {
|
||||
PrivBitSet modifiedGlobalPrivs = new PrivBitSet();
|
||||
for (Map.Entry<TablePattern, PrivBitSet> entry : tblPatternToPrivs.entrySet()) {
|
||||
TablePattern tblPattern = entry.getKey();
|
||||
PrivBitSet privs = entry.getValue();
|
||||
if (privs.containsPrivs(Privilege.ADMIN_PRIV, Privilege.NODE_PRIV, Privilege.USAGE_PRIV)
|
||||
&& tblPattern.getPrivLevel() != PrivLevel.GLOBAL) {
|
||||
LOG.debug("rectify privs {}: {} -> {}", roleName, tblPattern, privs);
|
||||
PrivBitSet copiedPrivs = privs.copy();
|
||||
copiedPrivs.and(PrivBitSet.of(Privilege.ADMIN_PRIV, Privilege.NODE_PRIV, Privilege.USAGE_PRIV));
|
||||
modifiedGlobalPrivs.or(copiedPrivs);
|
||||
// remove these privs from non global table pattern's priv set
|
||||
privs.unset(Privilege.USAGE_PRIV.getIdx());
|
||||
privs.unset(Privilege.NODE_PRIV.getIdx());
|
||||
privs.unset(Privilege.ADMIN_PRIV.getIdx());
|
||||
LOG.debug("alter rectify privs {}: {} -> {}, modified global priv: {}",
|
||||
roleName, tblPattern, privs, modifiedGlobalPrivs);
|
||||
}
|
||||
}
|
||||
if (!modifiedGlobalPrivs.isEmpty()) {
|
||||
PrivBitSet privBitSet = tblPatternToPrivs.get(TablePattern.ALL);
|
||||
if (privBitSet == null) {
|
||||
tblPatternToPrivs.put(TablePattern.ALL, modifiedGlobalPrivs);
|
||||
} else {
|
||||
privBitSet.or(modifiedGlobalPrivs);
|
||||
}
|
||||
}
|
||||
|
||||
// rebuild these priv tables
|
||||
rebuildPrivTables();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@ -586,7 +625,16 @@ public class Role implements Writable, GsonPostProcessable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void gsonPostProcess() throws IOException {
|
||||
public void gsonPostProcess() {
|
||||
rebuildPrivTables();
|
||||
}
|
||||
|
||||
private void rebuildPrivTables() {
|
||||
globalPrivTable = new GlobalPrivTable();
|
||||
catalogPrivTable = new CatalogPrivTable();
|
||||
dbPrivTable = new DbPrivTable();
|
||||
tablePrivTable = new TablePrivTable();
|
||||
resourcePrivTable = new ResourcePrivTable();
|
||||
for (Entry<TablePattern, PrivBitSet> entry : tblPatternToPrivs.entrySet()) {
|
||||
try {
|
||||
grantPrivs(entry.getKey(), entry.getValue().copy());
|
||||
|
||||
@ -30,6 +30,7 @@ import org.apache.doris.common.io.Text;
|
||||
import org.apache.doris.common.io.Writable;
|
||||
import org.apache.doris.mysql.privilege.Auth.PrivLevel;
|
||||
import org.apache.doris.persist.gson.GsonUtils;
|
||||
import org.apache.doris.qe.ConnectContext;
|
||||
import org.apache.doris.system.SystemInfoService;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
@ -122,11 +123,12 @@ public class RoleManager implements Writable {
|
||||
return existingRole;
|
||||
}
|
||||
|
||||
|
||||
public void getRoleInfo(List<List<String>> results) {
|
||||
for (Role role : roles.values()) {
|
||||
if (role.getRoleName().startsWith(DEFAULT_ROLE_PREFIX)) {
|
||||
continue;
|
||||
if (ConnectContext.get() == null || !ConnectContext.get().getSessionVariable().showUserDefaultRole) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
List<String> info = Lists.newArrayList();
|
||||
info.add(role.getRoleName());
|
||||
@ -185,13 +187,19 @@ public class RoleManager implements Writable {
|
||||
}
|
||||
|
||||
public String getUserDefaultRoleName(UserIdentity userIdentity) {
|
||||
return DEFAULT_ROLE_PREFIX + userIdentity.toString();
|
||||
return userIdentity.toDefaultRoleName();
|
||||
}
|
||||
|
||||
public Map<String, Role> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
public void rectifyPrivs() {
|
||||
for (Map.Entry<String, Role> entry : roles.entrySet()) {
|
||||
entry.getValue().rectifyPrivs();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder("Roles: ");
|
||||
|
||||
@ -129,9 +129,9 @@ public class MetaPersistMethod {
|
||||
break;
|
||||
case "paloAuth":
|
||||
metaPersistMethod.readMethod =
|
||||
Env.class.getDeclaredMethod("loadPaloAuth", DataInputStream.class, long.class);
|
||||
Env.class.getDeclaredMethod("loadAuth", DataInputStream.class, long.class);
|
||||
metaPersistMethod.writeMethod =
|
||||
Env.class.getDeclaredMethod("savePaloAuth", CountingDataOutputStream.class, long.class);
|
||||
Env.class.getDeclaredMethod("saveAuth", CountingDataOutputStream.class, long.class);
|
||||
break;
|
||||
case "transactionState":
|
||||
metaPersistMethod.readMethod =
|
||||
|
||||
@ -271,6 +271,9 @@ public class SessionVariable implements Serializable, Writable {
|
||||
public static final String DROP_TABLE_IF_CTAS_FAILED = "drop_table_if_ctas_failed";
|
||||
|
||||
public static final String MAX_TABLE_COUNT_USE_CASCADES_JOIN_REORDER = "max_table_count_use_cascades_join_reorder";
|
||||
public static final int MIN_JOIN_REORDER_TABLE_COUNT = 2;
|
||||
|
||||
public static final String SHOW_USER_DEFAULT_ROLE = "show_user_default_role";
|
||||
|
||||
// session origin value
|
||||
public Map<Field, String> sessionOriginValue = new HashMap<Field, String>();
|
||||
@ -720,11 +723,12 @@ public class SessionVariable implements Serializable, Writable {
|
||||
@VariableMgr.VarAttr(name = DROP_TABLE_IF_CTAS_FAILED, needForward = true)
|
||||
public boolean dropTableIfCtasFailed = true;
|
||||
|
||||
|
||||
@VariableMgr.VarAttr(name = MAX_TABLE_COUNT_USE_CASCADES_JOIN_REORDER)
|
||||
public int maxTableCountUseCascadesJoinReorder = 10;
|
||||
|
||||
public static final int MIN_JOIN_REORDER_TABLE_COUNT = 2;
|
||||
// If this is true, the result of `show roles` will return all user default role
|
||||
@VariableMgr.VarAttr(name = SHOW_USER_DEFAULT_ROLE, needForward = true)
|
||||
public boolean showUserDefaultRole = false;
|
||||
|
||||
// If this fe is in fuzzy mode, then will use initFuzzyModeVariables to generate some variables,
|
||||
// not the default value set in the code.
|
||||
@ -1513,6 +1517,10 @@ public class SessionVariable implements Serializable, Writable {
|
||||
: maxTableCountUseCascadesJoinReorder;
|
||||
}
|
||||
|
||||
public boolean isShowUserDefaultRole() {
|
||||
return showUserDefaultRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize to thrift object.
|
||||
* Used for rest api.
|
||||
|
||||
@ -168,6 +168,8 @@ public class AuthTest {
|
||||
|
||||
try {
|
||||
auth.createUser(createUserStmt);
|
||||
Set<UserIdentity> roleUsers = auth.getRoleUsers(userIdentity.toDefaultRoleName());
|
||||
Assert.assertTrue(roleUsers.contains(userIdentity));
|
||||
} catch (DdlException e) {
|
||||
Assert.fail();
|
||||
}
|
||||
@ -295,6 +297,8 @@ public class AuthTest {
|
||||
}
|
||||
try {
|
||||
auth.createUser(createUserStmt);
|
||||
Set<UserIdentity> roleUsers = auth.getRoleUsers(userIdentity.toDefaultRoleName());
|
||||
Assert.assertTrue(roleUsers.contains(userIdentity));
|
||||
} catch (DdlException e) {
|
||||
Assert.fail();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user