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

kirs 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 e2e5159b98e [feature](fe) Add information_schema role mappings table 
(#62077)
e2e5159b98e is described below

commit e2e5159b98e9b85ba1e0ec37ae93988867303c6d
Author: Calvin Kirs <[email protected]>
AuthorDate: Fri Apr 10 12:40:25 2026 +0800

    [feature](fe) Add information_schema role mappings table (#62077)
    
    [[feature](fe) Add information_schema role mappings
    
table](https://github.com/apache/doris/commit/399d8b05fc9d8f73bc9abee9a5b46dbe9165cd4f)
    https://github.com/apache/doris/issues/60361
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary: Add information_schema.role_mappings so administrators
    can inspect authentication integration role mapping definitions and
    audit fields through SQL.
    
    ### Release note
    
    Add information_schema.role_mappings for inspecting role mapping
    metadata.
    
    ### Check List (For Author)
    
    - Test: FE Unit Test; Regression test attempted
    - FE Unit Test: ./run-fe-ut.sh --run
    org.apache.doris.service.FrontendServiceImplTest
    - FE Unit Test: ./run-fe-ut.sh --run
    org.apache.doris.catalog.SchemaTableTest
    - Regression test: ./run-regression-test.sh --run -d auth_p0 -s
    test_role_mapping_system_table (blocked locally because the default
    127.0.0.1:9030 cluster did not include CREATE ROLE MAPPING syntax)
    - Behavior changed: Yes (adds information_schema.role_mappings)
    - Does this need documentation: No
---
 .../schema_role_mappings_scanner.cpp               | 145 +++++++++++++++++++++
 .../schema_role_mappings_scanner.h                 |  53 ++++++++
 be/src/information_schema/schema_scanner.cpp       |   3 +
 .../schema_role_mappings_scanner_test.cpp          |  40 ++++++
 .../org/apache/doris/analysis/SchemaTableType.java |   3 +-
 .../java/org/apache/doris/catalog/SchemaTable.java |  11 ++
 .../doris/tablefunction/MetadataGenerator.java     |  77 +++++++++++
 .../org/apache/doris/catalog/SchemaTableTest.java  |  13 ++
 .../doris/service/FrontendServiceImplTest.java     | 124 ++++++++++++++++++
 gensrc/thrift/Descriptors.thrift                   |   1 +
 gensrc/thrift/FrontendService.thrift               |   1 +
 .../auth_p0/test_role_mapping_system_table.groovy  | 123 +++++++++++++++++
 12 files changed, 593 insertions(+), 1 deletion(-)

diff --git a/be/src/information_schema/schema_role_mappings_scanner.cpp 
b/be/src/information_schema/schema_role_mappings_scanner.cpp
new file mode 100644
index 00000000000..c9582e26558
--- /dev/null
+++ b/be/src/information_schema/schema_role_mappings_scanner.cpp
@@ -0,0 +1,145 @@
+// 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.
+
+#include "information_schema/schema_role_mappings_scanner.h"
+
+#include <utility>
+
+#include "core/block/block.h"
+#include "core/data_type/data_type_factory.hpp"
+#include "core/string_ref.h"
+#include "information_schema/schema_helper.h"
+#include "runtime/exec_env.h"
+#include "runtime/runtime_state.h"
+#include "util/client_cache.h"
+#include "util/thrift_rpc_helper.h"
+
+namespace doris {
+#include "common/compile_check_begin.h"
+
+std::vector<SchemaScanner::ColumnDesc> 
SchemaRoleMappingsScanner::_s_tbls_columns = {
+        {"NAME", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"INTEGRATION_NAME", TYPE_VARCHAR, sizeof(StringRef), true},
+        {"RULES", TYPE_STRING, sizeof(StringRef), true},
+        {"COMMENT", TYPE_STRING, sizeof(StringRef), true},
+        {"CREATE_USER", TYPE_STRING, sizeof(StringRef), true},
+        {"CREATE_TIME", TYPE_STRING, sizeof(StringRef), true},
+        {"ALTER_USER", TYPE_STRING, sizeof(StringRef), true},
+        {"MODIFY_TIME", TYPE_STRING, sizeof(StringRef), true},
+};
+
+SchemaRoleMappingsScanner::SchemaRoleMappingsScanner()
+        : SchemaScanner(_s_tbls_columns, TSchemaTableType::SCH_ROLE_MAPPINGS) 
{}
+
+Status SchemaRoleMappingsScanner::start(RuntimeState* state) {
+    if (!_is_init) {
+        return Status::InternalError("used before initialized.");
+    }
+    _block_rows_limit = state->batch_size();
+    _rpc_timeout_ms = state->execution_timeout() * 1000;
+    return Status::OK();
+}
+
+Status SchemaRoleMappingsScanner::_get_role_mappings_block_from_fe() {
+    TNetworkAddress master_addr = 
ExecEnv::GetInstance()->cluster_info()->master_fe_addr;
+
+    TSchemaTableRequestParams schema_table_request_params;
+    for (int i = 0; i < _s_tbls_columns.size(); i++) {
+        schema_table_request_params.__isset.columns_name = true;
+        
schema_table_request_params.columns_name.emplace_back(_s_tbls_columns[i].name);
+    }
+    
schema_table_request_params.__set_current_user_ident(*_param->common_param->current_user_ident);
+    if (_param->common_param->frontend_conjuncts) {
+        schema_table_request_params.__set_frontend_conjuncts(
+                *_param->common_param->frontend_conjuncts);
+    }
+
+    TFetchSchemaTableDataRequest request;
+    request.__set_schema_table_name(TSchemaTableName::ROLE_MAPPINGS);
+    request.__set_schema_table_params(schema_table_request_params);
+
+    TFetchSchemaTableDataResult result;
+    RETURN_IF_ERROR(ThriftRpcHelper::rpc<FrontendServiceClient>(
+            master_addr.hostname, master_addr.port,
+            [&request, &result](FrontendServiceConnection& client) {
+                client->fetchSchemaTableData(result, request);
+            },
+            _rpc_timeout_ms));
+
+    Status status(Status::create(result.status));
+    if (!status.ok()) {
+        LOG(WARNING) << "fetch role mappings from FE failed, errmsg=" << 
status;
+        return status;
+    }
+
+    _role_mappings_block = Block::create_unique();
+    for (int i = 0; i < _s_tbls_columns.size(); ++i) {
+        auto data_type =
+                
DataTypeFactory::instance().create_data_type(_s_tbls_columns[i].type, true);
+        
_role_mappings_block->insert(ColumnWithTypeAndName(data_type->create_column(), 
data_type,
+                                                           
_s_tbls_columns[i].name));
+    }
+    _role_mappings_block->reserve(_block_rows_limit);
+
+    std::vector<TRow> result_data = std::move(result.data_batch);
+    if (!result_data.empty()) {
+        auto col_size = result_data[0].column_value.size();
+        if (col_size != _s_tbls_columns.size()) {
+            return Status::InternalError<false>("role mappings schema is not 
match for FE and BE");
+        }
+    }
+
+    for (int i = 0; i < result_data.size(); i++) {
+        const TRow& row = result_data[i];
+        for (int j = 0; j < _s_tbls_columns.size(); j++) {
+            RETURN_IF_ERROR(insert_block_column(row.column_value[j], j, 
_role_mappings_block.get(),
+                                                _s_tbls_columns[j].type));
+        }
+    }
+    return Status::OK();
+}
+
+Status SchemaRoleMappingsScanner::get_next_block_internal(Block* block, bool* 
eos) {
+    if (!_is_init) {
+        return Status::InternalError("Used before initialized.");
+    }
+
+    if (nullptr == block || nullptr == eos) {
+        return Status::InternalError("input pointer is nullptr.");
+    }
+
+    if (_role_mappings_block == nullptr) {
+        RETURN_IF_ERROR(_get_role_mappings_block_from_fe());
+        _total_rows = static_cast<int>(_role_mappings_block->rows());
+    }
+
+    if (_row_idx == _total_rows) {
+        *eos = true;
+        return Status::OK();
+    }
+
+    int current_batch_rows = std::min(_block_rows_limit, _total_rows - 
_row_idx);
+    MutableBlock mblock = MutableBlock::build_mutable_block(block);
+    RETURN_IF_ERROR(mblock.add_rows(_role_mappings_block.get(), _row_idx, 
current_batch_rows));
+    _row_idx += current_batch_rows;
+
+    *eos = _row_idx == _total_rows;
+    return Status::OK();
+}
+
+#include "common/compile_check_end.h"
+} // namespace doris
diff --git a/be/src/information_schema/schema_role_mappings_scanner.h 
b/be/src/information_schema/schema_role_mappings_scanner.h
new file mode 100644
index 00000000000..959bbd921f6
--- /dev/null
+++ b/be/src/information_schema/schema_role_mappings_scanner.h
@@ -0,0 +1,53 @@
+// 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.
+
+#pragma once
+
+#include <gen_cpp/FrontendService_types.h>
+
+#include <vector>
+
+#include "common/status.h"
+#include "information_schema/schema_scanner.h"
+
+namespace doris {
+class RuntimeState;
+class Block;
+
+class SchemaRoleMappingsScanner : public SchemaScanner {
+    ENABLE_FACTORY_CREATOR(SchemaRoleMappingsScanner);
+
+public:
+    SchemaRoleMappingsScanner();
+    ~SchemaRoleMappingsScanner() override = default;
+
+    Status start(RuntimeState* state) override;
+    Status get_next_block_internal(Block* block, bool* eos) override;
+
+    static std::vector<SchemaScanner::ColumnDesc> _s_tbls_columns;
+
+private:
+    Status _get_role_mappings_block_from_fe();
+
+    int _block_rows_limit = 4096;
+    int _row_idx = 0;
+    int _total_rows = 0;
+    int _rpc_timeout_ms = 3000;
+    std::unique_ptr<Block> _role_mappings_block = nullptr;
+};
+
+} // namespace doris
diff --git a/be/src/information_schema/schema_scanner.cpp 
b/be/src/information_schema/schema_scanner.cpp
index 25a5fd07c2f..981956330ce 100644
--- a/be/src/information_schema/schema_scanner.cpp
+++ b/be/src/information_schema/schema_scanner.cpp
@@ -67,6 +67,7 @@
 #include "information_schema/schema_partitions_scanner.h"
 #include "information_schema/schema_processlist_scanner.h"
 #include "information_schema/schema_profiling_scanner.h"
+#include "information_schema/schema_role_mappings_scanner.h"
 #include "information_schema/schema_routine_load_job_scanner.h"
 #include "information_schema/schema_rowsets_scanner.h"
 #include "information_schema/schema_schema_privileges_scanner.h"
@@ -268,6 +269,8 @@ std::unique_ptr<SchemaScanner> 
SchemaScanner::create(TSchemaTableType::type type
         return SchemaFileCacheInfoScanner::create_unique();
     case TSchemaTableType::SCH_AUTHENTICATION_INTEGRATIONS:
         return SchemaAuthenticationIntegrationsScanner::create_unique();
+    case TSchemaTableType::SCH_ROLE_MAPPINGS:
+        return SchemaRoleMappingsScanner::create_unique();
     case TSchemaTableType::SCH_TABLE_STREAMS:
         return SchemaTableStreamsScanner::create_unique();
     case TSchemaTableType::SCH_TABLE_STREAM_CONSUMPTION:
diff --git a/be/test/exec/schema_scanner/schema_role_mappings_scanner_test.cpp 
b/be/test/exec/schema_scanner/schema_role_mappings_scanner_test.cpp
new file mode 100644
index 00000000000..82e302b0204
--- /dev/null
+++ b/be/test/exec/schema_scanner/schema_role_mappings_scanner_test.cpp
@@ -0,0 +1,40 @@
+// 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.
+
+#include <gen_cpp/Descriptors_types.h>
+#include <gtest/gtest.h>
+
+#include "information_schema/schema_scanner.h"
+
+namespace doris {
+
+TEST(SchemaRoleMappingsScannerTest, test_create_role_mappings_scanner) {
+    auto scanner = SchemaScanner::create(TSchemaTableType::SCH_ROLE_MAPPINGS);
+    ASSERT_NE(nullptr, scanner);
+    EXPECT_EQ(TSchemaTableType::SCH_ROLE_MAPPINGS, scanner->type());
+    ASSERT_EQ(8, scanner->get_column_desc().size());
+    EXPECT_STREQ("NAME", scanner->get_column_desc()[0].name);
+    EXPECT_STREQ("INTEGRATION_NAME", scanner->get_column_desc()[1].name);
+    EXPECT_STREQ("RULES", scanner->get_column_desc()[2].name);
+    EXPECT_STREQ("COMMENT", scanner->get_column_desc()[3].name);
+    EXPECT_STREQ("CREATE_USER", scanner->get_column_desc()[4].name);
+    EXPECT_STREQ("CREATE_TIME", scanner->get_column_desc()[5].name);
+    EXPECT_STREQ("ALTER_USER", scanner->get_column_desc()[6].name);
+    EXPECT_STREQ("MODIFY_TIME", scanner->get_column_desc()[7].name);
+}
+
+} // namespace doris
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/analysis/SchemaTableType.java 
b/fe/fe-core/src/main/java/org/apache/doris/analysis/SchemaTableType.java
index 06d98182dd9..8807381c2cc 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/analysis/SchemaTableType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/analysis/SchemaTableType.java
@@ -121,7 +121,8 @@ public enum SchemaTableType {
     SCH_TABLE_STREAM_CONSUMPTION("TABLE_STREAM_CONSUMPTION", 
"TABLE_STREAM_CONSUMPTION",
             TSchemaTableType.SCH_TABLE_STREAM_CONSUMPTION),
     SCH_BE_COMPACTION_TASKS("BE_COMPACTION_TASKS", "BE_COMPACTION_TASKS",
-            TSchemaTableType.SCH_BE_COMPACTION_TASKS);
+            TSchemaTableType.SCH_BE_COMPACTION_TASKS),
+    SCH_ROLE_MAPPINGS("ROLE_MAPPINGS", "ROLE_MAPPINGS", 
TSchemaTableType.SCH_ROLE_MAPPINGS);
 
     private static final String dbName = "INFORMATION_SCHEMA";
 
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/SchemaTable.java 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/SchemaTable.java
index 371d43f878a..4770c780a29 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/SchemaTable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/SchemaTable.java
@@ -847,6 +847,17 @@ public class SchemaTable extends Table {
                             .column("ALTER_USER", 
ScalarType.createStringType())
                             .column("MODIFY_TIME", 
ScalarType.createStringType())
                             .build()))
+            .put("role_mappings",
+                    new SchemaTable(SystemIdGenerator.getNextId(), 
"role_mappings", TableType.SCHEMA,
+                        builder().column("NAME", ScalarType.createVarchar(256))
+                            .column("INTEGRATION_NAME", 
ScalarType.createVarchar(256))
+                            .column("RULES", ScalarType.createStringType())
+                            .column("COMMENT", ScalarType.createStringType())
+                            .column("CREATE_USER", 
ScalarType.createStringType())
+                            .column("CREATE_TIME", 
ScalarType.createStringType())
+                            .column("ALTER_USER", 
ScalarType.createStringType())
+                            .column("MODIFY_TIME", 
ScalarType.createStringType())
+                            .build()))
             .put("table_streams",
                     new SchemaTable(SystemIdGenerator.getNextId(), 
"table_streams", TableType.SCHEMA,
                             builder().column("DB_NAME", 
ScalarType.createVarchar(NAME_CHAR_LEN))
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/MetadataGenerator.java
 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/MetadataGenerator.java
index a91eac1234e..83f3252e84b 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/MetadataGenerator.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/MetadataGenerator.java
@@ -19,6 +19,7 @@ package org.apache.doris.tablefunction;
 
 import org.apache.doris.analysis.UserIdentity;
 import org.apache.doris.authentication.AuthenticationIntegrationMeta;
+import org.apache.doris.authentication.RoleMappingMeta;
 import org.apache.doris.blockrule.SqlBlockRule;
 import org.apache.doris.catalog.Column;
 import org.apache.doris.catalog.DataProperty;
@@ -44,6 +45,7 @@ import org.apache.doris.catalog.Type;
 import org.apache.doris.catalog.View;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.ClientPool;
+import org.apache.doris.common.ErrorCode;
 import org.apache.doris.common.Pair;
 import org.apache.doris.common.proc.FrontendsProcNode;
 import org.apache.doris.common.proc.PartitionsProcDir;
@@ -162,6 +164,8 @@ public class MetadataGenerator {
 
     private static final ImmutableMap<String, Integer> 
AUTHENTICATION_INTEGRATIONS_COLUMN_TO_INDEX;
 
+    private static final ImmutableMap<String, Integer> 
ROLE_MAPPINGS_COLUMN_TO_INDEX;
+
     private static final ImmutableMap<String, Integer> 
TABLE_STREAMS_COLUMN_TO_INDEX;
 
     private static final ImmutableMap<String, Integer> 
TABLE_STREAM_CONSUMPTION_COLUMN_TO_INDEX;
@@ -253,6 +257,13 @@ public class MetadataGenerator {
         }
         AUTHENTICATION_INTEGRATIONS_COLUMN_TO_INDEX = 
authenticationIntegrationsBuilder.build();
 
+        ImmutableMap.Builder<String, Integer> roleMappingsBuilder = new 
ImmutableMap.Builder();
+        List<Column> roleMappingsColList = 
SchemaTable.TABLE_MAP.get("role_mappings").getFullSchema();
+        for (int i = 0; i < roleMappingsColList.size(); i++) {
+            
roleMappingsBuilder.put(roleMappingsColList.get(i).getName().toLowerCase(), i);
+        }
+        ROLE_MAPPINGS_COLUMN_TO_INDEX = roleMappingsBuilder.build();
+
         ImmutableMap.Builder<String, Integer> tableStreamsBuilder = new 
ImmutableMap.Builder();
         List<Column> streamsBuilderColList = 
SchemaTable.TABLE_MAP.get("table_streams")
                 .getFullSchema();
@@ -386,6 +397,10 @@ public class MetadataGenerator {
                 result = 
authenticationIntegrationsMetadataResult(schemaTableParams);
                 columnIndex = AUTHENTICATION_INTEGRATIONS_COLUMN_TO_INDEX;
                 break;
+            case ROLE_MAPPINGS:
+                result = roleMappingsMetadataResult(schemaTableParams);
+                columnIndex = ROLE_MAPPINGS_COLUMN_TO_INDEX;
+                break;
             case TABLE_STREAMS:
                 result = streamMetadataResult(schemaTableParams);
                 columnIndex = TABLE_STREAMS_COLUMN_TO_INDEX;
@@ -777,6 +792,68 @@ public class MetadataGenerator {
         return result;
     }
 
+    private static TFetchSchemaTableDataResult 
roleMappingsMetadataResult(TSchemaTableRequestParams params) {
+        if (!params.isSetCurrentUserIdent()) {
+            return errorResult("current user ident is not set.");
+        }
+        UserIdentity currentUserIdentity = 
UserIdentity.fromThrift(params.getCurrentUserIdent());
+        TFetchSchemaTableDataResult result = new TFetchSchemaTableDataResult();
+        List<TRow> dataBatch = Lists.newArrayList();
+        result.setDataBatch(dataBatch);
+        result.setStatus(new TStatus(TStatusCode.OK));
+        if 
(!Env.getCurrentEnv().getAccessManager().checkGlobalPriv(currentUserIdentity, 
PrivPredicate.ADMIN)) {
+            return 
errorResult(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR.formatErrorMsg("ADMIN"));
+        }
+
+        List<Expression> conjuncts = Collections.EMPTY_LIST;
+        if (params.isSetFrontendConjuncts()) {
+            conjuncts = 
FrontendConjunctsUtils.convertToExpression(params.getFrontendConjuncts());
+        }
+        List<Expression> nameConjuncts = 
FrontendConjunctsUtils.filterBySlotName(conjuncts, "NAME");
+        List<Expression> integrationNameConjuncts =
+                FrontendConjunctsUtils.filterBySlotName(conjuncts, 
"INTEGRATION_NAME");
+
+        for (RoleMappingMeta meta : 
Env.getCurrentEnv().getRoleMappingMgr().getRoleMappings().values()) {
+            if (FrontendConjunctsUtils.isFiltered(nameConjuncts, "NAME", 
meta.getName())
+                    || FrontendConjunctsUtils.isFiltered(
+                    integrationNameConjuncts, "INTEGRATION_NAME", 
meta.getIntegrationName())) {
+                continue;
+            }
+            TRow row = new TRow();
+            row.addToColumnValue(new TCell().setStringVal(meta.getName()));
+            row.addToColumnValue(new 
TCell().setStringVal(meta.getIntegrationName()));
+            row.addToColumnValue(new 
TCell().setStringVal(formatRoleMappingRules(meta.getRules())));
+            if (meta.getComment() == null) {
+                row.addToColumnValue(new TCell());
+            } else {
+                row.addToColumnValue(new 
TCell().setStringVal(meta.getComment()));
+            }
+            row.addToColumnValue(new 
TCell().setStringVal(meta.getCreateUser()));
+            row.addToColumnValue(new 
TCell().setStringVal(meta.getCreateTimeString()));
+            row.addToColumnValue(new 
TCell().setStringVal(meta.getAlterUser()));
+            row.addToColumnValue(new 
TCell().setStringVal(meta.getModifyTimeString()));
+            dataBatch.add(row);
+        }
+        return result;
+    }
+
+    private static String 
formatRoleMappingRules(List<RoleMappingMeta.RuleMeta> rules) {
+        List<String> serializedRules = 
Lists.newArrayListWithCapacity(rules.size());
+        for (RoleMappingMeta.RuleMeta rule : rules) {
+            serializedRules.add(formatRoleMappingRule(rule));
+        }
+        return Joiner.on("; ").join(serializedRules);
+    }
+
+    private static String formatRoleMappingRule(RoleMappingMeta.RuleMeta rule) 
{
+        return "RULE (USING CEL '" + 
escapeRoleMappingCondition(rule.getCondition()) + "' GRANT ROLE "
+                + Joiner.on(", ").join(rule.getGrantedRoles()) + ")";
+    }
+
+    private static String escapeRoleMappingCondition(String condition) {
+        return condition.replace("'", "''");
+    }
+
     private static String maskAuthenticationProperties(Map<String, String> 
properties) {
         Map<String, String> maskedProperties = new LinkedHashMap<>();
         for (Map.Entry<String, String> entry : properties.entrySet()) {
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/catalog/SchemaTableTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/catalog/SchemaTableTest.java
index 9970d50a37a..8f0f875f25f 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/catalog/SchemaTableTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/catalog/SchemaTableTest.java
@@ -102,5 +102,18 @@ public class SchemaTableTest {
         Assertions.assertEquals("CREATE_TIME", 
authenticationIntegrations.getFullSchema().get(5).getName());
         Assertions.assertEquals("ALTER_USER", 
authenticationIntegrations.getFullSchema().get(6).getName());
         Assertions.assertEquals("MODIFY_TIME", 
authenticationIntegrations.getFullSchema().get(7).getName());
+
+        SchemaTable roleMappings = (SchemaTable) 
SchemaTable.TABLE_MAP.get("role_mappings");
+        Assertions.assertFalse(roleMappings.shouldFetchAllFe());
+        Assertions.assertFalse(roleMappings.shouldAddAgg());
+        Assertions.assertEquals(8, roleMappings.getFullSchema().size());
+        Assertions.assertEquals("NAME", 
roleMappings.getFullSchema().get(0).getName());
+        Assertions.assertEquals("INTEGRATION_NAME", 
roleMappings.getFullSchema().get(1).getName());
+        Assertions.assertEquals("RULES", 
roleMappings.getFullSchema().get(2).getName());
+        Assertions.assertEquals("COMMENT", 
roleMappings.getFullSchema().get(3).getName());
+        Assertions.assertEquals("CREATE_USER", 
roleMappings.getFullSchema().get(4).getName());
+        Assertions.assertEquals("CREATE_TIME", 
roleMappings.getFullSchema().get(5).getName());
+        Assertions.assertEquals("ALTER_USER", 
roleMappings.getFullSchema().get(6).getName());
+        Assertions.assertEquals("MODIFY_TIME", 
roleMappings.getFullSchema().get(7).getName());
     }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/service/FrontendServiceImplTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/service/FrontendServiceImplTest.java
index 00903892b23..90bcb41bed1 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/service/FrontendServiceImplTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/service/FrontendServiceImplTest.java
@@ -17,14 +17,17 @@
 
 package org.apache.doris.service;
 
+import org.apache.doris.analysis.UserIdentity;
 import org.apache.doris.catalog.Database;
 import org.apache.doris.catalog.Env;
 import org.apache.doris.catalog.OlapTable;
 import org.apache.doris.catalog.Partition;
 import org.apache.doris.common.Config;
+import org.apache.doris.common.ErrorCode;
 import org.apache.doris.common.FeConstants;
 import org.apache.doris.common.util.DatasourcePrintableMap;
 import org.apache.doris.nereids.parser.NereidsParser;
+import org.apache.doris.nereids.trees.plans.commands.Command;
 import org.apache.doris.nereids.trees.plans.commands.CreateDatabaseCommand;
 import org.apache.doris.nereids.trees.plans.commands.CreateTableCommand;
 import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
@@ -105,6 +108,17 @@ public class FrontendServiceImplTest {
         }
     }
 
+    private static void executeCommand(String sql) throws Exception {
+        NereidsParser nereidsParser = new NereidsParser();
+        LogicalPlan parsed = nereidsParser.parseSingle(sql);
+        StmtExecutor stmtExecutor = new StmtExecutor(connectContext, sql);
+        if (parsed instanceof Command) {
+            ((Command) parsed).run(connectContext, stmtExecutor);
+            return;
+        }
+        throw new IllegalArgumentException("Unsupported command in test: " + 
sql);
+    }
+
 
     @Test
     public void testCreatePartitionRange() throws Exception {
@@ -303,4 +317,114 @@ public class FrontendServiceImplTest {
             
Env.getCurrentEnv().getAuthenticationIntegrationMgr().dropAuthenticationIntegration(integrationName,
 true);
         }
     }
+
+    @Test
+    public void testFetchRoleMappingsSchemaTableData() throws Exception {
+        String mappingName = "test_role_mapping_system_table";
+        String integrationName = "test_role_mapping_system_table_ldap";
+        String readerRole = "test_role_mapping_reader";
+        String financeReaderRole = "test_role_mapping_fin_reader";
+        String financeWriterRole = "test_role_mapping_fin_writer";
+        String expectedRules = "RULE (USING CEL 'has_group(\"analyst\")' GRANT 
ROLE " + readerRole + "); "
+                + "RULE (USING CEL 'attr(\"department\") == \"finance\"' GRANT 
ROLE "
+                + financeReaderRole + ", " + financeWriterRole + ")";
+
+        executeCommand("DROP ROLE MAPPING IF EXISTS " + mappingName);
+        
Env.getCurrentEnv().getAuthenticationIntegrationMgr().dropAuthenticationIntegration(integrationName,
 true);
+        executeCommand("DROP ROLE IF EXISTS " + financeWriterRole);
+        executeCommand("DROP ROLE IF EXISTS " + financeReaderRole);
+        executeCommand("DROP ROLE IF EXISTS " + readerRole);
+
+        try {
+            executeCommand("CREATE ROLE " + readerRole);
+            executeCommand("CREATE ROLE " + financeReaderRole);
+            executeCommand("CREATE ROLE " + financeWriterRole);
+            executeCommand("CREATE AUTHENTICATION INTEGRATION " + 
integrationName
+                    + " PROPERTIES ('type'='ldap', 
'ldap.server'='ldap://127.0.0.1:389') "
+                    + "COMMENT 'role mapping auth'");
+            executeCommand("CREATE ROLE MAPPING " + mappingName
+                    + " ON AUTHENTICATION INTEGRATION " + integrationName
+                    + " RULE (USING CEL 'has_group(\"analyst\")' GRANT ROLE " 
+ readerRole + ")"
+                    + ", RULE (USING CEL 'attr(\"department\") == \"finance\"' 
GRANT ROLE "
+                    + financeReaderRole + ", " + financeWriterRole + ")"
+                    + " COMMENT 'role mapping comment'");
+
+            FrontendServiceImpl impl = new FrontendServiceImpl(exeEnv);
+            TFetchSchemaTableDataRequest request = new 
TFetchSchemaTableDataRequest();
+            request.setSchemaTableName(TSchemaTableName.ROLE_MAPPINGS);
+            TSchemaTableRequestParams params = new TSchemaTableRequestParams();
+            
params.setCurrentUserIdent(connectContext.getCurrentUserIdentity().toThrift());
+            request.setSchemaTableParams(params);
+
+            TFetchSchemaTableDataResult result = 
impl.fetchSchemaTableData(request);
+            Assert.assertEquals(TStatusCode.OK, 
result.getStatus().getStatusCode());
+
+            List<String> rowValues = result.getDataBatch().stream()
+                    .filter(row -> 
mappingName.equals(row.getColumnValue().get(0).getStringVal()))
+                    .map(row -> row.getColumnValue().stream()
+                            .map(cell -> cell.isSetStringVal() ? 
cell.getStringVal() : null)
+                            .collect(Collectors.toList()))
+                    .findFirst()
+                    .orElseThrow(() -> new AssertionError("role mapping row 
not found"));
+
+            Assert.assertEquals(mappingName, rowValues.get(0));
+            Assert.assertEquals(integrationName, rowValues.get(1));
+            Assert.assertEquals(expectedRules, rowValues.get(2));
+            Assert.assertEquals("role mapping comment", rowValues.get(3));
+            Assert.assertEquals(connectContext.getQualifiedUser(), 
rowValues.get(4));
+            Assert.assertNotNull(rowValues.get(5));
+            Assert.assertFalse(rowValues.get(5).isEmpty());
+            Assert.assertEquals(connectContext.getQualifiedUser(), 
rowValues.get(6));
+            Assert.assertEquals(rowValues.get(5), rowValues.get(7));
+        } finally {
+            executeCommand("DROP ROLE MAPPING IF EXISTS " + mappingName);
+            
Env.getCurrentEnv().getAuthenticationIntegrationMgr().dropAuthenticationIntegration(integrationName,
 true);
+            executeCommand("DROP ROLE IF EXISTS " + financeWriterRole);
+            executeCommand("DROP ROLE IF EXISTS " + financeReaderRole);
+            executeCommand("DROP ROLE IF EXISTS " + readerRole);
+        }
+    }
+
+    @Test
+    public void testFetchRoleMappingsSchemaTableDataWithoutAdmin() throws 
Exception {
+        String mappingName = "test_role_mapping_non_admin_system_table";
+        String integrationName = 
"test_role_mapping_non_admin_system_table_ldap";
+        String readerRole = "test_role_mapping_non_admin_reader";
+        String normalUser = "test_role_mapping_non_admin_user";
+
+        executeCommand("DROP ROLE MAPPING IF EXISTS " + mappingName);
+        
Env.getCurrentEnv().getAuthenticationIntegrationMgr().dropAuthenticationIntegration(integrationName,
 true);
+        executeCommand("DROP USER IF EXISTS " + normalUser);
+        executeCommand("DROP ROLE IF EXISTS " + readerRole);
+
+        try {
+            executeCommand("CREATE ROLE " + readerRole);
+            executeCommand("CREATE USER " + normalUser);
+            executeCommand("CREATE AUTHENTICATION INTEGRATION " + 
integrationName
+                    + " PROPERTIES ('type'='ldap', 
'ldap.server'='ldap://127.0.0.1:389') "
+                    + "COMMENT 'role mapping auth'");
+            executeCommand("CREATE ROLE MAPPING " + mappingName
+                    + " ON AUTHENTICATION INTEGRATION " + integrationName
+                    + " RULE (USING CEL 'has_group(\"analyst\")' GRANT ROLE " 
+ readerRole + ")"
+                    + " COMMENT 'role mapping comment'");
+
+            FrontendServiceImpl impl = new FrontendServiceImpl(exeEnv);
+            TFetchSchemaTableDataRequest request = new 
TFetchSchemaTableDataRequest();
+            request.setSchemaTableName(TSchemaTableName.ROLE_MAPPINGS);
+            TSchemaTableRequestParams params = new TSchemaTableRequestParams();
+            
params.setCurrentUserIdent(UserIdentity.createAnalyzedUserIdentWithIp(normalUser,
 "%").toThrift());
+            request.setSchemaTableParams(params);
+
+            TFetchSchemaTableDataResult result = 
impl.fetchSchemaTableData(request);
+            Assert.assertEquals(TStatusCode.INTERNAL_ERROR, 
result.getStatus().getStatusCode());
+            Assert.assertEquals(1, result.getStatus().getErrorMsgsSize());
+            
Assert.assertEquals(ErrorCode.ERR_SPECIFIC_ACCESS_DENIED_ERROR.formatErrorMsg("ADMIN"),
+                    result.getStatus().getErrorMsgs().get(0));
+        } finally {
+            executeCommand("DROP ROLE MAPPING IF EXISTS " + mappingName);
+            
Env.getCurrentEnv().getAuthenticationIntegrationMgr().dropAuthenticationIntegration(integrationName,
 true);
+            executeCommand("DROP USER IF EXISTS " + normalUser);
+            executeCommand("DROP ROLE IF EXISTS " + readerRole);
+        }
+    }
 }
diff --git a/gensrc/thrift/Descriptors.thrift b/gensrc/thrift/Descriptors.thrift
index 9026c21086c..432da0d49c3 100644
--- a/gensrc/thrift/Descriptors.thrift
+++ b/gensrc/thrift/Descriptors.thrift
@@ -218,6 +218,7 @@ enum TSchemaTableType {
     SCH_TABLE_STREAMS = 68;
     SCH_TABLE_STREAM_CONSUMPTION = 69;
     SCH_BE_COMPACTION_TASKS = 70;
+    SCH_ROLE_MAPPINGS = 71;
 }
 
 enum THdfsCompression {
diff --git a/gensrc/thrift/FrontendService.thrift 
b/gensrc/thrift/FrontendService.thrift
index 34f56fe2510..1d8f6e6bbd9 100644
--- a/gensrc/thrift/FrontendService.thrift
+++ b/gensrc/thrift/FrontendService.thrift
@@ -874,6 +874,7 @@ enum TSchemaTableName {
   AUTHENTICATION_INTEGRATIONS = 14,
   TABLE_STREAMS = 15,
   TABLE_STREAM_CONSUMPTION = 16,
+  ROLE_MAPPINGS = 17,
 }
 
 struct TMetadataTableRequestParams {
diff --git 
a/regression-test/suites/auth_p0/test_role_mapping_system_table.groovy 
b/regression-test/suites/auth_p0/test_role_mapping_system_table.groovy
new file mode 100644
index 00000000000..284b533a352
--- /dev/null
+++ b/regression-test/suites/auth_p0/test_role_mapping_system_table.groovy
@@ -0,0 +1,123 @@
+// 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_role_mapping_system_table", "p0,auth") {
+    String suiteName = "test_role_mapping_system_table"
+    String mappingName = "test_role_mapping_system_table_mapping"
+    String integrationName = "test_role_mapping_system_table_ldap"
+    String readerRole = "test_role_mapping_reader"
+    String financeReaderRole = "test_role_mapping_fin_reader"
+    String financeWriterRole = "test_role_mapping_fin_writer"
+    String user = "${suiteName}_user"
+    String pwd = 'C123_567p'
+    def jdbcUrlTokens = context.config.jdbcUrl.split('/')
+    String jdbcUrlWithoutSchema = jdbcUrlTokens[0] + "//" + jdbcUrlTokens[2] + 
"/?"
+    String expectedRules = (
+            """RULE (USING CEL 'has_group("analyst")' GRANT ROLE 
${readerRole}); """
+            + """RULE (USING CEL 'attr("department") == "finance"' """
+            + """GRANT ROLE ${financeReaderRole}, ${financeWriterRole})"""
+    )
+
+    try_sql("DROP ROLE MAPPING IF EXISTS ${mappingName}")
+    try_sql("DROP AUTHENTICATION INTEGRATION IF EXISTS ${integrationName}")
+    try_sql("DROP USER ${user}")
+    try_sql("DROP ROLE IF EXISTS ${financeWriterRole}")
+    try_sql("DROP ROLE IF EXISTS ${financeReaderRole}")
+    try_sql("DROP ROLE IF EXISTS ${readerRole}")
+
+    try {
+        sql """CREATE ROLE ${readerRole}"""
+        sql """CREATE ROLE ${financeReaderRole}"""
+        sql """CREATE ROLE ${financeWriterRole}"""
+
+        sql """
+            CREATE AUTHENTICATION INTEGRATION ${integrationName}
+            PROPERTIES (
+                'type'='ldap',
+                'ldap.server'='ldap://127.0.0.1:389'
+            )
+            COMMENT 'role mapping auth'
+        """
+
+        sql """CREATE USER '${user}' IDENTIFIED BY '${pwd}'"""
+
+        if (isCloudMode()) {
+            def clusters = sql " SHOW CLUSTERS; "
+            assertTrue(!clusters.isEmpty())
+            def validCluster = clusters[0][0]
+            sql """GRANT USAGE_PRIV ON CLUSTER `${validCluster}` TO ${user}"""
+        }
+
+        sql """GRANT SELECT_PRIV ON internal.information_schema.* TO ${user}"""
+
+        sql """
+            CREATE ROLE MAPPING ${mappingName}
+            ON AUTHENTICATION INTEGRATION ${integrationName}
+            RULE ( USING CEL 'has_group("analyst")' GRANT ROLE ${readerRole} ),
+            RULE (
+                USING CEL 'attr("department") == "finance"'
+                GRANT ROLE ${financeReaderRole}, ${financeWriterRole}
+            )
+            COMMENT 'role mapping comment'
+        """
+
+        def result = sql """
+            SELECT
+                NAME,
+                INTEGRATION_NAME,
+                RULES,
+                COMMENT,
+                CREATE_USER,
+                CREATE_TIME,
+                ALTER_USER,
+                MODIFY_TIME
+            FROM information_schema.role_mappings
+            WHERE NAME = '${mappingName}'
+            ORDER BY NAME
+        """
+
+        assertEquals(1, result.size())
+        assertEquals(8, result[0].size())
+        assertEquals(mappingName, result[0][0])
+        assertEquals(integrationName, result[0][1])
+        assertEquals(expectedRules, result[0][2])
+        assertEquals("role mapping comment", result[0][3])
+        assertTrue(result[0][4] != null && result[0][4].length() > 0)
+        assertTrue(result[0][5] != null && result[0][5].length() > 0)
+        assertTrue(result[0][6] != null && result[0][6].length() > 0)
+        assertEquals(result[0][5], result[0][7])
+
+        connect(user, "${pwd}", jdbcUrlWithoutSchema) {
+            test {
+                sql """
+                    SELECT NAME
+                    FROM information_schema.role_mappings
+                    WHERE NAME = '${mappingName}'
+                    ORDER BY NAME
+                """
+                exception "Access denied; you need (at least one of) the 
(ADMIN) privilege(s) for this operation"
+            }
+        }
+    } finally {
+        try_sql("DROP ROLE MAPPING IF EXISTS ${mappingName}")
+        try_sql("DROP AUTHENTICATION INTEGRATION IF EXISTS ${integrationName}")
+        try_sql("DROP USER ${user}")
+        try_sql("DROP ROLE IF EXISTS ${financeWriterRole}")
+        try_sql("DROP ROLE IF EXISTS ${financeReaderRole}")
+        try_sql("DROP ROLE IF EXISTS ${readerRole}")
+    }
+}


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


Reply via email to