This is an automated email from the ASF dual-hosted git repository.
alexey pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kudu.git
The following commit(s) were added to refs/heads/master by this push:
new 4e72498 KUDU-2612 allow system user to read list of table replicas
4e72498 is described below
commit 4e724988fb9dc6eeb8cd4b91f46760a03cfa5fde
Author: Alexey Serbin <[email protected]>
AuthorDate: Sat May 29 11:49:46 2021 -0700
KUDU-2612 allow system user to read list of table replicas
It turned out that txn system client wasn't able to send BEGIN_COMMIT to
participating tablets if fine-grained authz is enabled. Its request to
get the list of tablets for a table was rejected: the system user isn't
granted the METADATA privilege on any of user tables, of course.
This patch addresses that deficiency, bypassing the fine-grained authz
for the MasterService::GetTabletLocations() RPC if the caller is a
service- or super-user. In addition, tests are added to make sure the
multi-row transaction API works as expected even in the presence of
fine-grained authorization.
Change-Id: I26f06af17e5ee85522e2ef867d41cf0f3ddbe5d5
Reviewed-on: http://gerrit.cloudera.org:8080/17529
Tested-by: Alexey Serbin <[email protected]>
Reviewed-by: Andrew Wong <[email protected]>
---
src/kudu/integration-tests/ts_authz-itest.cc | 279 ++++++++++++++++++++++++++-
src/kudu/master/catalog_manager.cc | 4 +-
2 files changed, 273 insertions(+), 10 deletions(-)
diff --git a/src/kudu/integration-tests/ts_authz-itest.cc
b/src/kudu/integration-tests/ts_authz-itest.cc
index 25aac97..a83054a 100644
--- a/src/kudu/integration-tests/ts_authz-itest.cc
+++ b/src/kudu/integration-tests/ts_authz-itest.cc
@@ -15,7 +15,9 @@
// specific language governing permissions and limitations
// under the License.
+#include <cstdint>
#include <cstdlib>
+#include <functional>
#include <memory>
#include <ostream>
#include <string>
@@ -74,6 +76,7 @@ using kudu::client::KuduSession;
using kudu::client::KuduTable;
using kudu::client::KuduTableAlterer;
using kudu::client::KuduTableCreator;
+using kudu::client::KuduTransaction;
using kudu::cluster::ExternalMiniCluster;
using kudu::cluster::ExternalMiniClusterOptions;
using kudu::master::RefreshAuthzCacheRequestPB;
@@ -158,12 +161,18 @@ RWPrivileges ComplementaryPrivileges(const
unordered_set<string>& all_cols,
// privileges in 'write_privileges', using 'prng' to determine the operation.
Status PerformWrite(const WritePrivileges& write_privileges,
ThreadSafeRandom* prng,
- KuduTable* table) {
+ KuduTable* table,
+ KuduSession* session = nullptr) {
WritePrivilegeType op_type =
SelectRandomElement<WritePrivileges, WritePrivilegeType,
ThreadSafeRandom>(
write_privileges, prng);
- shared_ptr<KuduSession> session = table->client()->NewSession();
- const auto unwrap_session_error = [&session] (Status s) {
+ shared_ptr<KuduSession> new_session;
+ if (!session) {
+ new_session = table->client()->NewSession();
+ session = new_session.get();
+ }
+ CHECK(session);
+ const auto unwrap_session_error = [session] (Status s) {
if (s.IsIOError()) {
vector<KuduError*> errors;
session->GetPendingErrors(&errors, nullptr);
@@ -205,10 +214,10 @@ Status PerformWrite(const WritePrivileges&
write_privileges,
Status PerformScan(const unordered_set<string>& columns,
ThreadSafeRandom* prng,
KuduTable* table) {
- vector<string> cols_to_scan = prng ?
- SelectRandomSubset<unordered_set<string>, string, ThreadSafeRandom>(
- columns, /*min_to_return*/1, prng) :
- vector<string>(columns.begin(), columns.end());
+ vector<string> cols_to_scan = prng
+ ? SelectRandomSubset<unordered_set<string>, string, ThreadSafeRandom>(
+ columns, /*min_to_return*/1, prng)
+ : vector<string>(columns.begin(), columns.end());
KuduScanner scanner(table);
RETURN_NOT_OK(scanner.SetTimeoutMillis(30000));
RETURN_NOT_OK(scanner.SetProjectedColumnNames(cols_to_scan));
@@ -260,7 +269,7 @@ class TSAuthzITestHarness {
public:
virtual ~TSAuthzITestHarness() {}
- ExternalMiniClusterOptions GetClusterOpts() {
+ ExternalMiniClusterOptions GetClusterOpts() const {
ExternalMiniClusterOptions opts;
opts.enable_kerberos = true;
// Set a low token timeout so we can ensure retries are working properly.
@@ -442,7 +451,7 @@ class TSAuthzITest : public ExternalMiniClusterITestBase,
}
ExternalMiniClusterITestBase::SetUp();
harness_->AddUsers();
- ExternalMiniClusterOptions opts = harness_->GetClusterOpts();
+ ExternalMiniClusterOptions opts = GetClusterOpts();
harness_->SetUpExternalMiniServiceOpts(&opts);
NO_FATALS(StartClusterWithOpts(std::move(opts)));
@@ -471,6 +480,10 @@ class TSAuthzITest : public ExternalMiniClusterITestBase,
}
protected:
+ virtual ExternalMiniClusterOptions GetClusterOpts() const {
+ return harness_->GetClusterOpts();
+ }
+
unique_ptr<TSAuthzITestHarness> harness_;
};
@@ -660,4 +673,252 @@ INSTANTIATE_TEST_SUITE_P(AuthzProviders, TSAuthzITest,
return HarnessEnumToString(info.param);
});
+
+class TSAuthzTxnOpsITest : public TSAuthzITest {
+ protected:
+ static Status CountRows(KuduTable* table, int64_t* count) {
+ KuduScanner scanner(table);
+ RETURN_NOT_OK(scanner.SetSelection(KuduClient::LEADER_ONLY));
+ RETURN_NOT_OK(scanner.SetProjectedColumnNames({}));
+ RETURN_NOT_OK(scanner.SetReadMode(KuduScanner::READ_YOUR_WRITES));
+ RETURN_NOT_OK(scanner.Open());
+
+ int64_t num_rows = 0;
+ KuduScanBatch batch;
+ while (scanner.HasMoreRows()) {
+ RETURN_NOT_OK(scanner.NextBatch(&batch));
+ num_rows += batch.NumRows();
+ }
+ *count = num_rows;
+ return Status::OK();
+ }
+
+ ExternalMiniClusterOptions GetClusterOpts() const override {
+ auto opts = TSAuthzITest::GetClusterOpts();
+ // Since txn-related functionality is disabled for a while, a couple of
+ // flags should be overriden to allow running tests scenarios in
txn-enabled
+ // environment. Also, since the replication factor of the txn status table
+ // isn't crucial for this test, set its RF=1 to work with a single tablet
+ // server in the test cluster.
+ opts.extra_master_flags.emplace_back("--txn_manager_enabled");
+
opts.extra_master_flags.emplace_back("--txn_manager_status_table_num_replicas=1");
+ opts.extra_tserver_flags.emplace_back("--enable_txn_system_client_init");
+ opts.num_tablet_servers = 3;
+
+ return opts;
+ }
+};
+
+// Make sure users can perform basic txn-related operations on 'empty'
multi-row
+// transactions.
+TEST_P(TSAuthzTxnOpsITest, BasicOpsOnEmptyTransactions) {
+ SKIP_IF_SLOW_NOT_ALLOWED();
+
+ const string user = "user0";
+ ASSERT_OK(cluster_->kdc()->CreateUserPrincipal(user));
+ ASSERT_OK(cluster_->kdc()->Kinit(user));
+ ASSERT_OK(CreateTable(Substitute("$0.table", kDb), user));
+
+ shared_ptr<KuduClient> user_client;
+ ASSERT_OK(cluster_->CreateClient(nullptr, &user_client));
+
+ // It should be possible to start and commit an empty transaction.
+ {
+ shared_ptr<KuduTransaction> txn;
+ ASSERT_OK(user_client->NewTransaction(&txn));
+ ASSERT_OK(txn->Commit());
+ Status txn_status;
+ bool is_complete = false;
+ ASSERT_OK(txn->IsCommitComplete(&is_complete, &txn_status));
+ ASSERT_TRUE(is_complete);
+ ASSERT_OK(txn_status);
+ }
+
+ // It should be possible to start and commit an empty transaction in an
+ // asynchronous way.
+ {
+ shared_ptr<KuduTransaction> txn;
+ ASSERT_OK(user_client->NewTransaction(&txn));
+ ASSERT_OK(txn->StartCommit());
+ ASSERT_EVENTUALLY([&] {
+ Status txn_status;
+ bool is_complete = false;
+ ASSERT_OK(txn->IsCommitComplete(&is_complete, &txn_status));
+ ASSERT_TRUE(is_complete);
+ ASSERT_OK(txn_status);
+ });
+ }
+
+ // It should be possible to start and rollback an empty transaction.
+ {
+ shared_ptr<KuduTransaction> txn;
+ ASSERT_OK(user_client->NewTransaction(&txn));
+ ASSERT_OK(txn->Rollback());
+ ASSERT_EVENTUALLY([&] {
+ Status txn_status;
+ bool is_complete = true;
+ ASSERT_OK(txn->IsCommitComplete(&is_complete, &txn_status));
+ ASSERT_TRUE(is_complete);
+ ASSERT_TRUE(txn_status.IsAborted()) << txn_status.ToString();
+ });
+ }
+}
+
+// Make sure authorized users can successfully perform txn-related operations
+// while inserting rows into tables. In this scenario, the user is
automatically
+// granted the 'METADATA' privilege on a test table by granting the 'INSERT'
+// privilege, so KuduTable::Open() succeeds.
+TEST_P(TSAuthzTxnOpsITest, BasicOperationsForAuthorizedUsers) {
+ SKIP_IF_SLOW_NOT_ALLOWED();
+
+ const string user = "user0";
+ ASSERT_OK(cluster_->kdc()->CreateUserPrincipal(user));
+ ASSERT_OK(cluster_->kdc()->Kinit(user));
+
+ shared_ptr<KuduClient> user_client;
+ ASSERT_OK(cluster_->CreateClient(nullptr, &user_client));
+ ThreadSafeRandom prng(SeedRandom());
+
+ // Start and then commit a transaction with a single row inserted.
+ {
+ static const string kTableName = "table_commit";
+ const string table_name = Substitute("$0.$1", kDb, kTableName);
+ ASSERT_OK(CreateTable(table_name, user));
+
+ // Grant 'INSERT' privilege, so it's possible to insert a few rows.
+ ASSERT_OK(harness_->GrantTablePrivilege(
+ kDb, kTableName, user, "INSERT", /*admin=*/false, cluster_));
+ // Grant 'SELECT' to allow for counting rows in the table.
+ ASSERT_OK(harness_->GrantTablePrivilege(
+ kDb, kTableName, user, "SELECT", /*admin=*/false, cluster_));
+ shared_ptr<KuduTable> table;
+ ASSERT_OK(user_client->OpenTable(table_name, &table));
+
+ shared_ptr<KuduTransaction> txn;
+ ASSERT_OK(user_client->NewTransaction(&txn));
+ shared_ptr<KuduSession> session;
+ ASSERT_OK(txn->CreateSession(&session));
+ ASSERT_OK(session->SetFlushMode(KuduSession::FlushMode::MANUAL_FLUSH));
+ ASSERT_OK(PerformWrite(
+ { WritePrivilegeType::INSERT }, &prng, table.get(), session.get()));
+ ASSERT_OK(txn->Commit());
+ Status txn_status;
+ bool is_complete = false;
+ ASSERT_OK(txn->IsCommitComplete(&is_complete, &txn_status));
+ ASSERT_TRUE(is_complete);
+ ASSERT_OK(txn_status);
+
+ int64_t row_count = 0;
+ ASSERT_OK(CountRows(table.get(), &row_count));
+ ASSERT_EQ(1, row_count);
+ }
+
+ // Start and then rollback a transaction with a row inserted.
+ {
+ static const string kTableName = "table_rollback";
+ const string table_name = Substitute("$0.$1", kDb, kTableName);
+ ASSERT_OK(CreateTable(table_name, user));
+
+ // Grant 'INSERT' privilege, so it's possible to insert a few rows.
+ ASSERT_OK(harness_->GrantTablePrivilege(
+ kDb, kTableName, user, "INSERT", /*admin=*/false, cluster_));
+ // Grant 'SELECT' to allow for counting rows in the table.
+ ASSERT_OK(harness_->GrantTablePrivilege(
+ kDb, kTableName, user, "SELECT", /*admin=*/false, cluster_));
+ shared_ptr<KuduTable> table;
+ ASSERT_OK(user_client->OpenTable(table_name, &table));
+
+ shared_ptr<KuduTransaction> txn;
+ ASSERT_OK(user_client->NewTransaction(&txn));
+ shared_ptr<KuduSession> session;
+ ASSERT_OK(txn->CreateSession(&session));
+ ASSERT_OK(session->SetFlushMode(KuduSession::FlushMode::MANUAL_FLUSH));
+ ASSERT_OK(PerformWrite(
+ { WritePrivilegeType::INSERT }, &prng, table.get(), session.get()));
+ ASSERT_OK(txn->Rollback());
+ ASSERT_EVENTUALLY([&] {
+ Status txn_status;
+ bool is_complete = false;
+ ASSERT_OK(txn->IsCommitComplete(&is_complete, &txn_status));
+ ASSERT_TRUE(is_complete);
+ ASSERT_TRUE(txn_status.IsAborted()) << txn_status.ToString();
+ });
+
+ int64_t row_count = 0;
+ ASSERT_OK(CountRows(table.get(), &row_count));
+ ASSERT_EQ(0, row_count);
+ }
+}
+
+// A negative test case: make sure the user API presents proper error code and
+// message upon an attempt to commit a transaction with non-authorized
+// operations (for now, only INSERT) even in MANUAL_FLUSH mode. That's to make
+// sure that the automatic flush of buffered operation upon call of
+// KuduTransaction::Commit() is performed under the proper user credentials.
+TEST_P(TSAuthzTxnOpsITest, CommitForNonauthorizedUsers) {
+ SKIP_IF_SLOW_NOT_ALLOWED();
+
+ const string user = "user0";
+ ASSERT_OK(cluster_->kdc()->CreateUserPrincipal(user));
+ ASSERT_OK(cluster_->kdc()->Kinit(user));
+
+ shared_ptr<KuduClient> user_client;
+ ASSERT_OK(cluster_->CreateClient(nullptr, &user_client));
+ ThreadSafeRandom prng(SeedRandom());
+
+ // Start and then commit a transaction with a single row inserted.
+ {
+ static const string kTableName = "table_commit_non_authorized";
+ const string table_name = Substitute("$0.$1", kDb, kTableName);
+ ASSERT_OK(CreateTable(table_name, user));
+
+ // Grant only 'SELECT' privilege, so it's not possible to insert into
+ // the test table.
+ ASSERT_OK(harness_->GrantTablePrivilege(
+ kDb, kTableName, user, "SELECT", /*admin=*/false, cluster_));
+ shared_ptr<KuduTable> table;
+ ASSERT_OK(user_client->OpenTable(table_name, &table));
+
+ shared_ptr<KuduTransaction> txn;
+ ASSERT_OK(user_client->NewTransaction(&txn));
+ shared_ptr<KuduSession> session;
+ ASSERT_OK(txn->CreateSession(&session));
+ ASSERT_OK(session->SetFlushMode(KuduSession::FlushMode::MANUAL_FLUSH));
+ ASSERT_OK(PerformWrite(
+ { WritePrivilegeType::INSERT }, &prng, table.get(), session.get()));
+ const auto s = txn->Commit();
+ ASSERT_TRUE(s.IsIOError()) << s.ToString();
+ vector<KuduError*> errors;
+ session->GetPendingErrors(&errors, nullptr);
+ ElementDeleter deleter(&errors);
+ ASSERT_EQ(1, errors.size());
+ const auto& err_status = errors[0]->status();
+ ASSERT_TRUE(err_status.IsRemoteError()) << err_status.ToString();
+ ASSERT_STR_CONTAINS(err_status.ToString(),
+ "Not authorized: not authorized to write");
+
+ // However, it should be possible to rollback the transaction: that's
+ // a no-op in this case.
+ ASSERT_OK(txn->Rollback());
+
+ ASSERT_EVENTUALLY([&] {
+ Status txn_status;
+ bool is_complete = false;
+ ASSERT_OK(txn->IsCommitComplete(&is_complete, &txn_status));
+ ASSERT_TRUE(is_complete);
+ ASSERT_TRUE(txn_status.IsAborted()) << txn_status.ToString();
+ });
+
+ int64_t row_count = 0;
+ ASSERT_OK(CountRows(table.get(), &row_count));
+ ASSERT_EQ(0, row_count);
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(AuthzProviders, TSAuthzTxnOpsITest,
+ ::testing::Values(kRanger),
+ [] (const testing::TestParamInfo<TSAuthzITest::ParamType>& info) {
+ return HarnessEnumToString(info.param);
+ });
+
} // namespace kudu
diff --git a/src/kudu/master/catalog_manager.cc
b/src/kudu/master/catalog_manager.cc
index 447126b..a922d8d 100644
--- a/src/kudu/master/catalog_manager.cc
+++ b/src/kudu/master/catalog_manager.cc
@@ -5531,7 +5531,9 @@ Status CatalogManager::GetTabletLocations(const string&
tablet_id,
return Status::NotFound(Substitute("Unknown tablet $0", tablet_id));
}
}
- if (user) {
+ // Allow service and super users to read metadata on any table, bypassing
+ // fine-grained authz restrictions, if any are in effect.
+ if (user && !master_->IsServiceUserOrSuperUser(*user)) {
// Acquire the table lock and then check that the user is authorized to
operate on
// the table that the tablet belongs to.
TableMetadataLock table_lock(tablet_info->table().get(), LockMode::READ);