This is an automated email from the ASF dual-hosted git repository.

starocean999 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 35cbbaa4dfa [feature](show) support show create user (#51845)
35cbbaa4dfa is described below

commit 35cbbaa4dfa5d02a9ad9a5a86b52f238e3d01b47
Author: lsy3993 <[email protected]>
AuthorDate: Fri Jun 27 09:56:26 2025 +0800

    [feature](show) support show create user (#51845)
    
    Problem Summary:
    We can create user like this `CREATE USER IF NOT EXISTS 'xxxxxxx'
    IDENTIFIED BY '12345' PASSWORD_EXPIRE INTERVAL 10 DAY
    FAILED_LOGIN_ATTEMPTS 3 PASSWORD_LOCK_TIME 1 DAY;`
    
    But we can not show the users' create stmt, it's hard for admin or root
    to manage all users.
    So this PR supports 'show create user identify' by the SQL below:
    `show create user xxxxxxx` (show create user via userIdentity)
    
    this is the result, it has two columns:
    | User Identity | Create Stmt |
    | --- | --- |
    | xxxxxxx | CREATE USER 'xxxxxxx'@'%' IDENTIFIED BY *** PASSWORD_HISTORY
    DEFAULT PASSWORD_EXPIRE INTERVAL 864000 SECOND FAILED_LOGIN_ATTEMPTS 3
    PASSWORD_LOCK_TIME 86400 SECOND COMMENT "" |
---
 .../antlr4/org/apache/doris/nereids/DorisParser.g4 |   1 +
 .../doris/nereids/parser/LogicalPlanBuilder.java   |  10 ++
 .../apache/doris/nereids/trees/plans/PlanType.java |   1 +
 .../plans/commands/ShowCreateUserCommand.java      | 177 +++++++++++++++++++++
 .../trees/plans/visitor/CommandVisitor.java        |   5 +
 .../plans/commands/ShowCreateUserCommandTest.java  |  99 ++++++++++++
 .../show/test_nereids_show_create_user.groovy      |  47 ++++++
 7 files changed, 340 insertions(+)

diff --git a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4 
b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
index 8141838c924..f630d61bba2 100644
--- a/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
+++ b/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
@@ -346,6 +346,7 @@ supportedShowStatement
     | SHOW GLOBAL FULL? FUNCTIONS (LIKE STRING_LITERAL)?                       
     #showGlobalFunctions
     | SHOW ALL? GRANTS                                                         
     #showGrants
     | SHOW GRANTS FOR userIdentify                                             
     #showGrantsForUser
+    | SHOW CREATE USER userIdentify                                            
     #showCreateUser
     | SHOW SNAPSHOT ON repo=identifier wildWhere?                              
     #showSnapshot
     | SHOW LOAD PROFILE loadIdPath=STRING_LITERAL? limitClause?                
     #showLoadProfile
     | SHOW CREATE REPOSITORY FOR identifier                                    
     #showCreateRepository
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
index e0d761ea05d..04c442eb181 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
@@ -734,6 +734,7 @@ import 
org.apache.doris.nereids.trees.plans.commands.ShowCreateMaterializedViewC
 import 
org.apache.doris.nereids.trees.plans.commands.ShowCreateProcedureCommand;
 import 
org.apache.doris.nereids.trees.plans.commands.ShowCreateRepositoryCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowCreateTableCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowCreateUserCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowCreateViewCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowDataCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowDataSkewCommand;
@@ -5018,6 +5019,15 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
         return new ShowGrantsCommand(userIdent, false);
     }
 
+    @Override
+    public LogicalPlan visitShowCreateUser(DorisParser.ShowCreateUserContext 
ctx) {
+        UserIdentity userIdent = null;
+        if (ctx.userIdentify() != null) {
+            userIdent = visitUserIdentify(ctx.userIdentify());
+        }
+        return new ShowCreateUserCommand(userIdent);
+    }
+
     @Override
     public LogicalPlan visitShowRowPolicy(ShowRowPolicyContext ctx) {
         UserIdentity user = null;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
index 3ff3968f50d..cf3c559d7fc 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/PlanType.java
@@ -261,6 +261,7 @@ public enum PlanType {
     SHOW_CREATE_REPOSITORY_COMMAND,
     SHOW_CREATE_TABLE_COMMAND,
     SHOW_CREATE_VIEW_COMMAND,
+    SHOW_CREATE_USER_COMMAND,
     SHOW_DATABASES_COMMAND,
     SHOW_DATABASE_ID_COMMAND,
     SHOW_DATA_COMMAND,
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowCreateUserCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowCreateUserCommand.java
new file mode 100644
index 00000000000..c68880d1e6d
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowCreateUserCommand.java
@@ -0,0 +1,177 @@
+// 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.nereids.trees.plans.commands;
+
+import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.ScalarType;
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.nereids.trees.plans.PlanType;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.ShowResultSet;
+import org.apache.doris.qe.ShowResultSetMetaData;
+import org.apache.doris.qe.StmtExecutor;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * show create user command
+ */
+public class ShowCreateUserCommand extends ShowCommand {
+    private static final ShowResultSetMetaData META_DATA =
+            ShowResultSetMetaData.builder()
+                .addColumn(new Column("User Identity", 
ScalarType.createVarchar(512)))
+                .addColumn(new Column("Create Stmt", 
ScalarType.createVarchar(1024)))
+                .build();
+    private UserIdentity userIdent;
+
+    /**
+     * constructor for show create user
+     */
+    public ShowCreateUserCommand(UserIdentity userIdent) {
+        super(PlanType.SHOW_CREATE_USER_COMMAND);
+        this.userIdent = userIdent;
+    }
+
+    @VisibleForTesting
+    protected ShowResultSet handleShowCreateUser(ConnectContext ctx, 
StmtExecutor executor) throws Exception {
+        if (userIdent == null) {
+            userIdent = ConnectContext.get().getCurrentUserIdentity();
+        }
+        if (userIdent != null) {
+            userIdent.analyze();
+        }
+        Preconditions.checkState(userIdent != null);
+        UserIdentity self = ConnectContext.get().getCurrentUserIdentity();
+
+        // need global GRANT priv.
+        if (!self.equals(userIdent)) {
+            if 
(!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(ConnectContext.get(), 
PrivPredicate.GRANT)) {
+                
ErrorReport.reportAnalysisException(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR, 
"GRANT");
+            }
+        }
+        if (userIdent != null && 
!Env.getCurrentEnv().getAccessManager().getAuth().doesUserExist(userIdent)) {
+            throw new AnalysisException(String.format("User: %s does not 
exist", userIdent));
+        }
+
+        // get all user identity
+        List<List<String>> infos = new ArrayList<>();
+
+        // get user's create stmt
+        List<String> userInfo = new ArrayList<>();
+        userInfo.add(userIdent.getQualifiedUser());
+        userInfo.add(toSql(userIdent));
+        infos.add(userInfo);
+
+        return new ShowResultSet(getMetaData(), infos);
+    }
+
+    @VisibleForTesting
+    protected String toSql(UserIdentity user) {
+        if (user == null) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder("CREATE USER ");
+
+        // user identity
+        sb.append(user);
+
+        // user password
+        sb.append(" IDENTIFIED BY *** ");
+
+        // password policy
+        if (Env.getCurrentEnv().getAuth().getPasswdPolicyManager() != null) {
+            // policies are : <expirePolicy, historyPolicy, failedLoginPolicy>
+            // expirePolicy has two lists: <EXPIRATION_SECONDS>, 
<PASSWORD_CREATION_TIME>
+            // historyPolicy has two lists: <HISTORY_NUM>, <HISTORY_PASSWORDS>
+            // failedLoginPolicy has four lists :
+            // <NUM_FAILED_LOGIN>, <PASSWORD_LOCK_SECONDS>, 
<FAILED_LOGIN_COUNTER>, <LOCK_TIME>
+            List<List<String>> policies = 
Env.getCurrentEnv().getAuth().getPasswdPolicyManager().getPolicyInfo(user);
+            if (policies != null) {
+                // historyPolicy: <HISTORY_NUM>, <HISTORY_PASSWORDS>; use 
HISTORY_NUM only
+                if (policies.size() > 3) {
+                    List<String> history = policies.get(2);
+                    String historyValue = 
history.get(1).equalsIgnoreCase("DEFAULT")
+                            || 
history.get(1).equalsIgnoreCase("NO_RESTRICTION") ? "DEFAULT" : history.get(1);
+                    sb.append(" PASSWORD_HISTORY ").append(historyValue);
+                }
+
+                // expirePolicy: <EXPIRATION_SECONDS>, 
<PASSWORD_CREATION_TIME>; use EXPIRATION_SECONDS only
+                if (policies.size() > 1) {
+                    List<String> expire = policies.get(0);
+                    String expireValue = expire.get(1);
+                    if (expireValue.equalsIgnoreCase("DEFAULT") || 
expireValue.equalsIgnoreCase("NEVER")) {
+                        sb.append(" PASSWORD_EXPIRE ").append(expireValue);
+                    } else {
+                        sb.append(" PASSWORD_EXPIRE INTERVAL 
").append(expireValue).append(" SECOND");
+                    }
+                }
+
+                // failedLoginPolicy: <NUM_FAILED_LOGIN>, 
<PASSWORD_LOCK_SECONDS>, <FAILED_LOGIN_COUNTER>, <LOCK_TIME>
+                // use NUM_FAILED_LOGIN and PASSWORD_LOCK_SECONDS only
+                if (policies.size() > 7) {
+                    List<String> failedAttempts = policies.get(4);
+                    String attemptValue = 
failedAttempts.get(1).equalsIgnoreCase("DISABLED") ? "0"
+                            : failedAttempts.get(1);
+                    sb.append(" FAILED_LOGIN_ATTEMPTS ").append(attemptValue);
+
+                    List<String> lockTime = policies.get(5);
+                    String lockValue = 
lockTime.get(1).equalsIgnoreCase("DISABLED") ? "UNBOUNDED" : lockTime.get(1);
+                    if (lockValue.equalsIgnoreCase("DISABLED") || 
lockValue.equalsIgnoreCase("UNBOUNDED")) {
+                        sb.append(" PASSWORD_LOCK_TIME UNBOUNDED");
+                    } else {
+                        sb.append(" PASSWORD_LOCK_TIME 
").append(lockValue).append(" SECOND");
+                    }
+                }
+            }
+        }
+
+        // comment
+        if 
(Env.getCurrentEnv().getAuth().getUserManager().getUserByUserIdentity(user) != 
null) {
+            String comment = 
Env.getCurrentEnv().getAuth().getUserManager().getUserByUserIdentity(user).getComment();
+            if (comment != null) {
+                sb.append(" COMMENT 
").append("\"").append(comment).append("\"");
+            }
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public ShowResultSet doRun(ConnectContext ctx, StmtExecutor executor) 
throws Exception {
+        return handleShowCreateUser(ctx, executor);
+    }
+
+    @Override
+    public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
+        return visitor.visitShowCreateUserCommand(this, context);
+    }
+
+    @Override
+    public ShowResultSetMetaData getMetaData() {
+        return META_DATA;
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
index bc4e7384267..dae23b0ce00 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/visitor/CommandVisitor.java
@@ -178,6 +178,7 @@ import 
org.apache.doris.nereids.trees.plans.commands.ShowCreateMaterializedViewC
 import 
org.apache.doris.nereids.trees.plans.commands.ShowCreateProcedureCommand;
 import 
org.apache.doris.nereids.trees.plans.commands.ShowCreateRepositoryCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowCreateTableCommand;
+import org.apache.doris.nereids.trees.plans.commands.ShowCreateUserCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowCreateViewCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowDataCommand;
 import org.apache.doris.nereids.trees.plans.commands.ShowDataSkewCommand;
@@ -751,6 +752,10 @@ public interface CommandVisitor<R, C> {
         return visitCommand(showCreateFunctionCommand, context);
     }
 
+    default R visitShowCreateUserCommand(ShowCreateUserCommand 
showCreateUserCommand, C context) {
+        return visitCommand(showCreateUserCommand, context);
+    }
+
     default R visitShowCreateViewCommand(ShowCreateViewCommand 
showCreateViewCommand, C context) {
         return visitCommand(showCreateViewCommand, context);
     }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowCreateUserCommandTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowCreateUserCommandTest.java
new file mode 100644
index 00000000000..abcc0af5485
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowCreateUserCommandTest.java
@@ -0,0 +1,99 @@
+// 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.nereids.trees.plans.commands;
+
+import org.apache.doris.analysis.UserDesc;
+import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.catalog.DomainResolver;
+import org.apache.doris.common.AnalysisException;
+import org.apache.doris.common.UserException;
+import org.apache.doris.mysql.privilege.Auth;
+import org.apache.doris.nereids.trees.plans.commands.info.CreateUserInfo;
+import org.apache.doris.utframe.TestWithFeService;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+public class ShowCreateUserCommandTest extends TestWithFeService {
+    @Override
+    protected void runBeforeAll() throws Exception {
+        createDatabase("test");
+    }
+
+    private static final class MockDomainResolver extends DomainResolver {
+        public MockDomainResolver(Auth auth) {
+            super(auth);
+        }
+
+        @Override
+        public boolean resolveWithBNS(String domainName, Set<String> 
resolvedIPs) {
+            switch (domainName) {
+                case "palo.domain1":
+                    resolvedIPs.add("10.1.1.1");
+                    resolvedIPs.add("10.1.1.2");
+                    resolvedIPs.add("10.1.1.3");
+                    break;
+                case "palo.domain2":
+                    resolvedIPs.add("20.1.1.1");
+                    resolvedIPs.add("20.1.1.2");
+                    resolvedIPs.add("20.1.1.3");
+                    break;
+                default:
+                    break;
+            }
+            return true;
+        }
+    }
+
+    @Test
+    void testHandleShowCreateUser() throws Exception {
+        ShowCreateUserCommand sc = new ShowCreateUserCommand(null);
+        sc.handleShowCreateUser(connectContext, null);
+
+        UserIdentity user = new UserIdentity("test", "127.0.0.1");
+        sc = new ShowCreateUserCommand(user);
+        ShowCreateUserCommand finalSc = sc;
+        Assertions.assertThrows(AnalysisException.class, () -> 
finalSc.handleShowCreateUser(connectContext, null));
+
+        sc = new ShowCreateUserCommand(user);
+        ShowCreateUserCommand finalSc1 = sc;
+        Assertions.assertThrows(AnalysisException.class, () -> 
finalSc1.handleShowCreateUser(connectContext, null));
+
+        sc = new ShowCreateUserCommand(null);
+        sc.handleShowCreateUser(connectContext, null);
+
+        // test domain user
+        UserIdentity userIdentity = new UserIdentity("zhangsan", 
"palo.domain1", true);
+        UserDesc userDesc = new UserDesc(userIdentity, "12345", true);
+        CreateUserInfo info = new CreateUserInfo(true, userDesc, null, null, 
"");
+        CreateUserCommand create = new CreateUserCommand(info);
+        try {
+            create.run(connectContext, null);
+        } catch (UserException e) {
+            e.printStackTrace();
+        }
+        sc = new ShowCreateUserCommand(userIdentity);
+        sc.handleShowCreateUser(connectContext, null);
+        userIdentity = new UserIdentity("zhangsan", "10.1.1.1", false);
+        sc = new ShowCreateUserCommand(userIdentity);
+        ShowCreateUserCommand finalSc2 = sc;
+        Assertions.assertThrows(AnalysisException.class, () -> 
finalSc2.handleShowCreateUser(connectContext, null));
+    }
+}
diff --git 
a/regression-test/suites/nereids_p0/show/test_nereids_show_create_user.groovy 
b/regression-test/suites/nereids_p0/show/test_nereids_show_create_user.groovy
new file mode 100644
index 00000000000..c5abb982a08
--- /dev/null
+++ 
b/regression-test/suites/nereids_p0/show/test_nereids_show_create_user.groovy
@@ -0,0 +1,47 @@
+// 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.
+
+suite("test_nereids_show_create_user") {
+    sql "DROP USER IF EXISTS 'xxxxxxx'"
+    sql "CREATE USER IF NOT EXISTS 'xxxxxxx' IDENTIFIED BY '12345' 
PASSWORD_EXPIRE INTERVAL 10 DAY FAILED_LOGIN_ATTEMPTS 3 PASSWORD_LOCK_TIME 1 
DAY;"
+
+    sql "DROP USER IF EXISTS 'yyyyyy'@'192.168.%'"
+    sql "CREATE USER IF NOT EXISTS 'yyyyyy'@'192.168.%' IDENTIFIED BY '123456' 
PASSWORD_EXPIRE INTERVAL 10 DAY FAILED_LOGIN_ATTEMPTS 3 PASSWORD_LOCK_TIME 1 
DAY;"
+
+    sql "DROP USER IF EXISTS 'xyxyxy_abc'@'192.168.%'"
+
+    checkNereidsExecute("SHOW CREATE USER xxxxxxx")
+    checkNereidsExecute("SHOW CREATE USER 'xxxxxxx'")
+    checkNereidsExecute("SHOW CREATE USER 'yyyyyy'@'192.168.%'")
+    checkNereidsExecute("SHOW CREATE USER 'yyyyyy'@'192.168.%'")
+
+    def res2 = sql """SHOW CREATE USER xxxxxxx"""
+    assertEquals('xxxxxxx', res2.get(0).get(0))
+
+    // test create stmt can be reused
+    def res3 = sql """SHOW CREATE USER 'yyyyyy'@'192.168.%'"""
+    def createStmt = res3.get(0).get(1)
+    def reusedStmt = createStmt.toString().replace("***", 
"'654321'").replace("yyyyyy", "xyxyxy_abc")
+    sql "${reusedStmt}"
+    def res4 = sql """SHOW CREATE USER 'xyxyxy_abc'@'192.168.%'"""
+    assertEquals(true, res4.size() > 0)
+    assertEquals("xyxyxy_abc", res4.get(0).get(0))
+
+    sql "DROP USER IF EXISTS 'xxxxxxx'"
+    sql "DROP USER IF EXISTS 'yyyyyy'@'192.168.%'"
+    sql "DROP USER IF EXISTS 'xyxyxy_abc'@'192.168.%'"
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to