Modify the logic of setting password (#798)

* Modify the logic of setting password
1. User can set password for current_user() or if it has GRANT priv
2. And USER() function support
This commit is contained in:
Mingyu Chen
2019-03-25 09:27:40 +08:00
committed by ZHAO Chun
parent 504fbc3627
commit d47600ed84
19 changed files with 295 additions and 24 deletions

View File

@ -8,9 +8,9 @@ Syntax:
user_identity:
'user_name'@'host'
CREATE USER 命令用于创建一个 Palo 用户。在 Palo 中,一个 user_identity 唯一标识一个用户。user_identity 由两部分组成,user_name 和 host,其中 username 为用户名。host 标识用户端连接所在的主机地址。host 部分可以使用 % 进行模糊匹配。如果不指定 host,默认为 '%',即表示该用户可以从任意 host 连接到 Palo
CREATE USER 命令用于创建一个 Doris 用户。在 Doris 中,一个 user_identity 唯一标识一个用户。user_identity 由两部分组成,user_name 和 host,其中 username 为用户名。host 标识用户端连接所在的主机地址。host 部分可以使用 % 进行模糊匹配。如果不指定 host,默认为 '%',即表示该用户可以从任意 host 连接到 Doris
host 部分也可指定为 domain,语法为:'user_name'@['domain'],即使用中括号包围,则 Palo 会认为这个是一个 domain,并尝试解析其 ip 地址。目前仅支持百度内部的 BNS 解析。
host 部分也可指定为 domain,语法为:'user_name'@['domain'],即使用中括号包围,则 Doris 会认为这个是一个 domain,并尝试解析其 ip 地址。目前仅支持百度内部的 BNS 解析。
如果指定了角色(ROLE),则会自动将该角色所拥有的权限赋予新创建的这个用户。如果不指定,则该用户默认没有任何权限。指定的 ROLE 必须已经存在。
@ -54,7 +54,7 @@ Syntax:
DROP USER 'user_name'
DROP USER 命令会删除一个 palo 用户。这里 Palo 不支持删除指定的 user_identity。当删除一个指定用户后,该用户所对应的所有 user_identity 都会被删除。比如之前通过 CREATE USER 语句创建了 jack@'192.%' 以及 jack@['domain'] 两个用户,则在执行 DROP USER 'jack' 后,jack@'192.%' 以及 jack@['domain'] 都将被删除。
DROP USER 命令会删除一个 palo 用户。这里 Doris 不支持删除指定的 user_identity。当删除一个指定用户后,该用户所对应的所有 user_identity 都会被删除。比如之前通过 CREATE USER 语句创建了 jack@'192.%' 以及 jack@['domain'] 两个用户,则在执行 DROP USER 'jack' 后,jack@'192.%' 以及 jack@['domain'] 都将被删除。
## example
@ -105,7 +105,7 @@ Syntax:
GRANT privilege_list ON db_name[.tbl_name] TO user_identity [ROLE role_name]
privilege_list 是需要赋予的权限列表,以逗号分隔。当前Palo支持如下权限:
privilege_list 是需要赋予的权限列表,以逗号分隔。当前 Doris 支持如下权限:
NODE_PRIV:集群节点操作权限,包括节点上下线等操作,只有 root 用户有该权限,不可赋予其他用户。
ADMIN_PRIV:除 NODE_PRIV 以外的所有权限。

View File

@ -3281,6 +3281,8 @@ non_pred_expr ::=
{: RESULT = new FunctionCallExpr(new FunctionName(null, id), params); :}
| KW_DATABASE LPAREN RPAREN
{: RESULT = new InformationFunction("DATABASE"); :}
| KW_USER LPAREN RPAREN
{: RESULT = new InformationFunction("USER"); :}
| KW_CURRENT_USER LPAREN RPAREN
{: RESULT = new InformationFunction("CURRENT_USER"); :}
| KW_CONNECTION_ID LPAREN RPAREN

View File

@ -1440,6 +1440,14 @@ public class Analyzer {
return globalState.context.getQualifiedUser();
}
public String getUserIdentity(boolean currentUser) {
if (currentUser) {
return "";
} else {
return getQualifiedUser() + "@" + ConnectContext.get().getRemoteIP();
}
}
public String getSchemaDb() {
return schemaDb;
}

View File

@ -17,15 +17,13 @@
package org.apache.doris.analysis;
import org.apache.doris.catalog.PrimitiveType;
import org.apache.doris.catalog.Type;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.thrift.TExprNode;
import org.apache.doris.thrift.TExprNodeType;
import org.apache.doris.thrift.TInfoFunc;
/**
*/
public class InformationFunction extends Expr {
private final String funcType;
private long intValue;
@ -55,10 +53,10 @@ public class InformationFunction extends Expr {
strValue = analyzer.getDefaultDb();
} else if (funcType.equalsIgnoreCase("USER")) {
type = Type.VARCHAR;
strValue = analyzer.getQualifiedUser();
strValue = ConnectContext.get().getUserIdentity().toString();
} else if (funcType.equalsIgnoreCase("CURRENT_USER")) {
type = Type.VARCHAR;
strValue = analyzer.getQualifiedUser();
strValue = ConnectContext.get().getCurrentUserIdentity().toString();
} else if (funcType.equalsIgnoreCase("CONNECTION_ID")) {
type = Type.BIGINT;
intValue = analyzer.getConnectId();

View File

@ -57,12 +57,15 @@ public class SetPassVar extends SetVar {
boolean isSelf = false;
ConnectContext ctx = ConnectContext.get();
if (userIdent == null) {
// set userIdent as itself
userIdent = new UserIdentity(ClusterNamespace.getNameFromFullName(analyzer.getQualifiedUser()),
ctx.getRemoteIP());
// set userIdent as what current_user() returns
userIdent = ctx.getCurrentUserIdentity();
isSelf = true;
} else {
userIdent.analyze(analyzer.getClusterName());
if (userIdent.equals(ctx.getCurrentUserIdentity())) {
isSelf = true;
}
}
userIdent.analyze(analyzer.getClusterName());
// Check password
passwdBytes = MysqlPassword.checkPassword(passwdParam);
@ -92,6 +95,6 @@ public class SetPassVar extends SetVar {
@Override
public String toSql() {
return "SET PASSWORD FOR " + userIdent + " = '*XXX'";
return "SET PASSWORD FOR " + userIdent.toString() + " = '*XXX'";
}
}

View File

@ -160,6 +160,9 @@ public class UserIdentity implements Writable {
return false;
}
UserIdentity other = (UserIdentity) obj;
if (this.isDomain != other.isDomain) {
return false;
}
return user.equals(other.getQualifiedUser()) && host.equals(other.getHost());
}
@ -168,6 +171,7 @@ public class UserIdentity implements Writable {
int result = 17;
result = 31 * result + user.hashCode();
result = 31 * result + host.hashCode();
result = 31 * result + Boolean.valueOf(isDomain).hashCode();
return result;
}

View File

@ -17,6 +17,7 @@
package org.apache.doris.mysql;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.cluster.ClusterNamespace;
import org.apache.doris.common.Config;
@ -28,12 +29,14 @@ import org.apache.doris.qe.ConnectContext;
import org.apache.doris.system.SystemInfoService;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
// MySQL protocol util
public class MysqlProto {
@ -92,12 +95,14 @@ public class MysqlProto {
String qualifiedUser = ClusterNamespace.getFullName(clusterName, tmpUser);
String remoteIp = context.getMysqlChannel().getRemoteIp();
List<UserIdentity> currentUserIdentity = Lists.newArrayList();
if (!Catalog.getCurrentCatalog().getAuth().checkPassword(qualifiedUser, remoteIp,
scramble, randomString)) {
scramble, randomString, currentUserIdentity)) {
ErrorReport.report(ErrorCode.ERR_ACCESS_DENIED_ERROR, qualifiedUser, usePasswd);
return false;
}
context.setCurrentUserIdentitfy(currentUserIdentity.get(0));
context.setQualifiedUser(qualifiedUser);
return true;
}

View File

@ -26,6 +26,9 @@ import org.apache.logging.log4j.Logger;
import java.io.DataOutput;
import java.io.IOException;
/*
* DbPrivTable saves all database level privs
*/
public class DbPrivTable extends PrivTable {
private static final Logger LOG = LogManager.getLogger(DbPrivTable.class);

View File

@ -198,7 +198,8 @@ public class PaloAuth implements Writable {
}
}
public boolean checkPassword(String remoteUser, String remoteHost, byte[] remotePasswd, byte[] randomString) {
public boolean checkPassword(String remoteUser, String remoteHost, byte[] remotePasswd, byte[] randomString,
List<UserIdentity> currentUser) {
if (!Config.enable_auth_check) {
return true;
}
@ -209,7 +210,7 @@ public class PaloAuth implements Writable {
readLock();
try {
return userPrivTable.checkPassword(remoteUser, remoteHost, remotePasswd, randomString);
return userPrivTable.checkPassword(remoteUser, remoteHost, remotePasswd, randomString, currentUser);
} finally {
readUnlock();
}

View File

@ -25,6 +25,9 @@ import com.google.common.base.Preconditions;
import java.io.DataOutput;
import java.io.IOException;
/*
* TablePrivTable saves all table level privs
*/
public class TablePrivTable extends PrivTable {
public void getPrivs(String host, String db, String user, String tbl, PrivBitSet savedPrivs) {

View File

@ -27,7 +27,11 @@ import org.apache.logging.log4j.Logger;
import java.io.DataOutput;
import java.io.IOException;
import java.util.List;
/*
* UserPrivTable saves all global privs and also password for users
*/
public class UserPrivTable extends PrivTable {
private static final Logger LOG = LogManager.getLogger(UserPrivTable.class);
@ -61,7 +65,9 @@ public class UserPrivTable extends PrivTable {
// validate the connection by host, user and password.
// return true if this connection is valid, and 'savedPrivs' save all global privs got from user table.
public boolean checkPassword(String remoteUser, String remoteHost, byte[] remotePasswd, byte[] randomString) {
// if currentUser is not null, save the current user identity
public boolean checkPassword(String remoteUser, String remoteHost, byte[] remotePasswd, byte[] randomString,
List<UserIdentity> currentUser) {
LOG.debug("check password for user: {} from {}, password: {}, random string: {}",
remoteUser, remoteHost, remotePasswd, randomString);
@ -87,6 +93,9 @@ public class UserPrivTable extends PrivTable {
&& (remotePasswd.length == 0
|| MysqlPassword.checkScramble(remotePasswd, randomString, saltPassword))) {
// found the matched entry
if (currentUser != null) {
currentUser.add(entry.getUserIdent());
}
return true;
} else {
continue;

View File

@ -17,6 +17,7 @@
package org.apache.doris.qe;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.cluster.ClusterNamespace;
import org.apache.doris.mysql.MysqlCapability;
@ -63,8 +64,9 @@ public class ConnectContext {
private volatile String currentDb = "";
// cluster name
private volatile String clusterName = "";
// User
// user
private volatile String qualifiedUser;
private volatile UserIdentity currentUserIdentity;
// Serializer used to pack MySQL packet.
private volatile MysqlSerializer serializer;
// Variables belong to this session.
@ -164,6 +166,19 @@ public class ConnectContext {
this.qualifiedUser = qualifiedUser;
}
// for USER() function
public UserIdentity getUserIdentity() {
return new UserIdentity(qualifiedUser, remoteIP);
}
public UserIdentity getCurrentUserIdentity() {
return currentUserIdentity;
}
public void setCurrentUserIdentitfy(UserIdentity currentUserIdentity) {
this.currentUserIdentity = currentUserIdentity;
}
public SessionVariable getSessionVariable() {
return sessionVariable;
}

View File

@ -47,6 +47,9 @@ public class SetPassVarTest {
analyzer = AccessTestUtil.fetchAdminAnalyzer(true);
MockedAuth.mockedAuth(auth);
MockedAuth.mockedConnectContext(ctx, "root", "192.168.1.1");
UserIdentity currentUser = new UserIdentity("root", "192.168.1.1");
currentUser.setIsAnalyzed();
ctx.setCurrentUserIdentitfy(currentUser);
}
@Test
@ -70,7 +73,7 @@ public class SetPassVarTest {
// empty password
stmt = new SetPassVar(null, null);
stmt.analyze(analyzer);
Assert.assertEquals("SET PASSWORD FOR 'testCluster:testUser'@'192.168.1.1' = '*XXX'", stmt.toString());
Assert.assertEquals("SET PASSWORD FOR 'root'@'192.168.1.1' = '*XXX'", stmt.toString());
}
@Test(expected = AnalysisException.class)

View File

@ -17,14 +17,13 @@
package org.apache.doris.mysql;
import org.junit.Assert;
import org.easymock.EasyMock;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

View File

@ -17,6 +17,7 @@
package org.apache.doris.mysql;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.catalog.Database;
import org.apache.doris.common.DdlException;
@ -40,6 +41,7 @@ import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.List;
@RunWith(PowerMockRunner.class)
@PowerMockIgnore({ "org.apache.log4j.*", "javax.management.*" })
@ -56,9 +58,20 @@ public class MysqlProtoTest {
// mock auth
PaloAuth auth = EasyMock.createMock(PaloAuth.class);
EasyMock.expect(auth.checkGlobalPriv(EasyMock.anyObject(ConnectContext.class),
EasyMock.anyObject(PrivPredicate.class))).andReturn(true).anyTimes();
EasyMock.anyObject(PrivPredicate.class))).andReturn(true).anyTimes();
EasyMock.expect(auth.checkPassword(EasyMock.anyString(), EasyMock.anyString(), (byte[]) EasyMock.anyObject(),
(byte[]) EasyMock.anyObject())).andReturn(true).anyTimes();
(byte[]) EasyMock.anyObject(), (List<UserIdentity>) EasyMock.anyObject())).andDelegateTo(
new WrappedAuth() {
@Override
public boolean checkPassword(String remoteUser, String remoteHost, byte[] remotePasswd,
byte[] randomString,
List<UserIdentity> currentUser) {
UserIdentity userIdentity = new UserIdentity("defaut_cluster:user", "192.168.1.1");
currentUser.add(userIdentity);
return true;
}
}).anyTimes();
EasyMock.replay(auth);
// Mock catalog
@ -141,6 +154,7 @@ public class MysqlProtoTest {
mockAccess();
ConnectContext context = new ConnectContext(null);
context.setCatalog(catalog);
context.setThreadLocalInfo();
Assert.assertTrue(MysqlProto.negotiate(context));
}

View File

@ -0,0 +1,36 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.mysql;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.mysql.privilege.PaloAuth;
import java.util.List;
/*
* Author: Chenmingyu
* Date: Mar 24, 2019
*/
public class WrappedAuth extends PaloAuth {
@Override
public boolean checkPassword(String remoteUser, String remoteHost, byte[] remotePasswd, byte[] randomString,
List<UserIdentity> currentUser) {
return true;
}
}

View File

@ -17,6 +17,7 @@
package org.apache.doris.mysql.privilege;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.QueryState;
@ -53,6 +54,11 @@ public class MockedAuth {
ctx.getState();
result = new QueryState();
ctx.getCurrentUserIdentity();
UserIdentity userIdentity = new UserIdentity(user, ip);
userIdentity.setIsAnalyzed();
result = userIdentity;
}
};
}

View File

@ -0,0 +1,158 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.doris.mysql.privilege;
import org.apache.doris.analysis.Analyzer;
import org.apache.doris.analysis.CreateUserStmt;
import org.apache.doris.analysis.SetPassVar;
import org.apache.doris.analysis.UserDesc;
import org.apache.doris.analysis.UserIdentity;
import org.apache.doris.catalog.Catalog;
import org.apache.doris.common.AnalysisException;
import org.apache.doris.common.DdlException;
import org.apache.doris.mysql.MysqlPassword;
import org.apache.doris.persist.EditLog;
import org.apache.doris.persist.PrivInfo;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.system.SystemInfoService;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import mockit.Mocked;
import mockit.NonStrictExpectations;
import mockit.internal.startup.Startup;
/*
* Author: Chenmingyu
* Date: Mar 24, 2019
*/
public class SetPasswordTest {
private PaloAuth auth;
@Mocked
public Catalog catalog;
@Mocked
private Analyzer analyzer;
@Mocked
private EditLog editLog;
static {
Startup.initializeIfPossible();
}
@Before
public void setUp() throws NoSuchMethodException, SecurityException, AnalysisException {
auth = new PaloAuth();
new NonStrictExpectations() {
{
analyzer.getClusterName();
minTimes = 0;
result = SystemInfoService.DEFAULT_CLUSTER;
Catalog.getCurrentCatalog();
minTimes = 0;
result = catalog;
catalog.getAuth();
minTimes = 0;
result = auth;
catalog.getEditLog();
minTimes = 0;
result = editLog;
editLog.logCreateUser((PrivInfo) any);
minTimes = 0;
MysqlPassword.checkPassword(anyString);
minTimes = 0;
result = new byte[10];
}
};
}
@Test
public void test() throws DdlException {
UserIdentity userIdentity = new UserIdentity("default_cluster:cmy", "%");
userIdentity.setIsAnalyzed();
CreateUserStmt stmt = new CreateUserStmt(new UserDesc(userIdentity));
auth.createUser(stmt);
ConnectContext ctx = new ConnectContext(null);
// set password for 'cmy'@'%'
UserIdentity currentUser1 = new UserIdentity("default_cluster:cmy", "%");
currentUser1.setIsAnalyzed();
ctx.setCurrentUserIdentitfy(currentUser1);
ctx.setThreadLocalInfo();
UserIdentity user1 = new UserIdentity("default_cluster:cmy", "%");
user1.setIsAnalyzed();
SetPassVar setPassVar = new SetPassVar(user1, null);
try {
setPassVar.analyze(analyzer);
} catch (AnalysisException e) {
e.printStackTrace();
Assert.fail();
}
// set password without for
SetPassVar setPassVar2 = new SetPassVar(null, null);
try {
setPassVar2.analyze(analyzer);
} catch (AnalysisException e) {
e.printStackTrace();
Assert.fail();
}
// create user cmy2@'192.168.1.1'
UserIdentity userIdentity2 = new UserIdentity("default_cluster:cmy2", "192.168.1.1");
userIdentity2.setIsAnalyzed();
stmt = new CreateUserStmt(new UserDesc(userIdentity2));
auth.createUser(stmt);
UserIdentity currentUser2 = new UserIdentity("default_cluster:cmy2", "192.168.1.1");
currentUser2.setIsAnalyzed();
ctx.setCurrentUserIdentitfy(currentUser2);
ctx.setThreadLocalInfo();
// set password without for
SetPassVar setPassVar3 = new SetPassVar(null, null);
try {
setPassVar3.analyze(analyzer);
} catch (AnalysisException e) {
e.printStackTrace();
Assert.fail();
}
// set password for cmy2@'192.168.1.1'
UserIdentity user2 = new UserIdentity("default_cluster:cmy2", "192.168.1.1");
user2.setIsAnalyzed();
SetPassVar setPassVar4 = new SetPassVar(user2, null);
try {
setPassVar4.analyze(analyzer);
} catch (AnalysisException e) {
e.printStackTrace();
Assert.fail();
}
}
}

View File

@ -60,6 +60,10 @@ public class SetExecutorTest {
ctx.setCatalog(AccessTestUtil.fetchAdminCatalog());
ctx.setQualifiedUser("root");
ctx.setRemoteIP("192.168.1.1");
UserIdentity currentUser = new UserIdentity("root", "192.168.1.1");
currentUser.setIsAnalyzed();
ctx.setCurrentUserIdentitfy(currentUser);
ctx.setThreadLocalInfo();
new NonStrictExpectations() {
{