blob: 71a087b4b5f9b01cd4399ee619d939f6dcca0513 [file] [log] [blame]
// 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 "kudu/master/sentry_authz_provider.h"
#include <algorithm>
#include <cstdint>
#include <functional>
#include <initializer_list>
#include <memory>
#include <ostream>
#include <string>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include <gflags/gflags.h>
#include <glog/logging.h>
#include <google/protobuf/stubs/port.h>
#include <google/protobuf/util/message_differencer.h>
#include <gtest/gtest.h>
#include "kudu/common/common.pb.h"
#include "kudu/common/schema.h"
#include "kudu/common/wire_protocol.h"
#include "kudu/gutil/integral_types.h"
#include "kudu/gutil/macros.h"
#include "kudu/gutil/map-util.h"
#include "kudu/gutil/ref_counted.h"
#include "kudu/gutil/strings/substitute.h"
#include "kudu/gutil/sysinfo.h"
#include "kudu/master/sentry_authz_provider-test-base.h"
#include "kudu/master/sentry_privileges_fetcher.h"
#include "kudu/security/token.pb.h"
#include "kudu/sentry/mini_sentry.h"
#include "kudu/sentry/sentry-test-base.h"
#include "kudu/sentry/sentry_action.h"
#include "kudu/sentry/sentry_authorizable_scope.h"
#include "kudu/sentry/sentry_client.h"
#include "kudu/sentry/sentry_policy_service_types.h"
#include "kudu/util/barrier.h"
#include "kudu/util/flag_tags.h"
#include "kudu/util/hdr_histogram.h"
#include "kudu/util/logging.h"
#include "kudu/util/metrics.h"
#include "kudu/util/net/net_util.h"
#include "kudu/util/pb_util.h"
#include "kudu/util/random.h"
#include "kudu/util/random_util.h"
#include "kudu/util/status.h"
#include "kudu/util/stopwatch.h"
#include "kudu/util/test_macros.h"
#include "kudu/util/test_util.h"
#include "kudu/util/ttl_cache.h"
DEFINE_int32(num_table_privileges, 100,
"Number of table privileges to use in testing");
TAG_FLAG(num_table_privileges, hidden);
DEFINE_int32(num_databases, 10,
"Number of databases to use in testing");
TAG_FLAG(num_databases, hidden);
DEFINE_int32(num_tables_per_db, 10,
"Number of tables to use per database to use in testing");
TAG_FLAG(num_tables_per_db, hidden);
DEFINE_bool(has_db_privileges, true,
"Whether the user should have db-level privileges in testing");
TAG_FLAG(has_db_privileges, hidden);
DECLARE_bool(sentry_require_db_privileges_for_list_tables);
DECLARE_int32(sentry_service_recv_timeout_seconds);
DECLARE_int32(sentry_service_send_timeout_seconds);
DECLARE_uint32(sentry_privileges_cache_capacity_mb);
DECLARE_string(sentry_service_rpc_addresses);
DECLARE_string(server_name);
DECLARE_string(trusted_user_acl);
METRIC_DECLARE_counter(sentry_client_tasks_successful);
METRIC_DECLARE_counter(sentry_client_tasks_failed_fatal);
METRIC_DECLARE_counter(sentry_client_tasks_failed_nonfatal);
METRIC_DECLARE_counter(sentry_client_reconnections_succeeded);
METRIC_DECLARE_counter(sentry_client_reconnections_failed);
METRIC_DECLARE_histogram(sentry_client_task_execution_time_us);
METRIC_DECLARE_counter(sentry_privileges_cache_evictions);
METRIC_DECLARE_counter(sentry_privileges_cache_evictions_expired);
METRIC_DECLARE_counter(sentry_privileges_cache_hits);
METRIC_DECLARE_counter(sentry_privileges_cache_hits_expired);
METRIC_DECLARE_counter(sentry_privileges_cache_inserts);
METRIC_DECLARE_counter(sentry_privileges_cache_lookups);
METRIC_DECLARE_counter(sentry_privileges_cache_misses);
METRIC_DECLARE_gauge_uint64(sentry_privileges_cache_memory_usage);
using kudu::pb_util::SecureDebugString;
using kudu::security::ColumnPrivilegePB;
using kudu::security::TablePrivilegePB;
using kudu::sentry::AuthorizableScopesSet;
using kudu::sentry::SentryAction;
using kudu::sentry::SentryActionsSet;
using kudu::sentry::SentryTestBase;
using kudu::sentry::SentryAuthorizableScope;
using google::protobuf::util::MessageDifferencer;
using sentry::TSentryAuthorizable;
using sentry::TSentryGrantOption;
using sentry::TSentryPrivilege;
using std::string;
using std::thread;
using std::unique_ptr;
using std::unordered_map;
using std::unordered_set;
using std::vector;
using strings::Substitute;
namespace kudu {
namespace master {
TEST(SentryAuthzProviderStaticTest, TestTrustedUserAcl) {
FLAGS_trusted_user_acl = "impala,hive,hdfs";
SentryAuthzProvider authz_provider;
ASSERT_TRUE(authz_provider.IsTrustedUser("impala"));
ASSERT_TRUE(authz_provider.IsTrustedUser("hive"));
ASSERT_TRUE(authz_provider.IsTrustedUser("hdfs"));
ASSERT_FALSE(authz_provider.IsTrustedUser("untrusted"));
}
// Basic unit test for validations on ill-formed privileges.
TEST(SentryPrivilegesFetcherStaticTest, TestPrivilegesWellFormed) {
const string kDb = "db";
const string kTable = "table";
TSentryAuthorizable requested_authorizable;
requested_authorizable.__set_server(FLAGS_server_name);
requested_authorizable.__set_db(kDb);
requested_authorizable.__set_table(kTable);
TSentryPrivilege real_privilege = GetTablePrivilege(kDb, kTable, "ALL");
{
// Privilege with a bogus action set.
TSentryPrivilege privilege = real_privilege;
privilege.__set_action("NotAnAction");
ASSERT_FALSE(SentryPrivilegesFetcher::SentryPrivilegeIsWellFormed(
privilege, requested_authorizable, /*scope=*/nullptr, /*action=*/nullptr));
}
{
// Privilege with a bogus authorizable scope set.
TSentryPrivilege privilege = real_privilege;
privilege.__set_privilegeScope("NotAnAuthorizableScope");
ASSERT_FALSE(SentryPrivilegesFetcher::SentryPrivilegeIsWellFormed(
privilege, requested_authorizable, /*scope=*/nullptr, /*action=*/nullptr));
}
{
// Privilege with a valid, but unexpected scope for the set fields.
TSentryPrivilege privilege = real_privilege;
privilege.__set_privilegeScope("COLUMN");
ASSERT_FALSE(SentryPrivilegesFetcher::SentryPrivilegeIsWellFormed(
privilege, requested_authorizable, /*scope=*/nullptr, /*action=*/nullptr));
}
{
// Privilege with a messed up scope field at a higher scope than that
// requested.
TSentryPrivilege privilege = real_privilege;
privilege.__set_dbName("NotTheActualDb");
ASSERT_FALSE(SentryPrivilegesFetcher::SentryPrivilegeIsWellFormed(
privilege, requested_authorizable, /*scope=*/nullptr, /*action=*/nullptr));
}
{
// Privilege with an field set that isn't meant to be set at its scope.
TSentryPrivilege privilege = real_privilege;
privilege.__set_columnName("SomeColumn");
ASSERT_FALSE(SentryPrivilegesFetcher::SentryPrivilegeIsWellFormed(
privilege, requested_authorizable, /*scope=*/nullptr, /*action=*/nullptr));
}
{
// Privilege with a missing field for its scope.
TSentryPrivilege privilege = real_privilege;
privilege.__isset.tableName = false;
ASSERT_FALSE(SentryPrivilegesFetcher::SentryPrivilegeIsWellFormed(
privilege, requested_authorizable, /*scope=*/nullptr, /*action=*/nullptr));
}
{
// Finally, the correct table-level privilege.
SentryAuthorizableScope::Scope granted_scope;
SentryAction::Action granted_action;
real_privilege.printTo(LOG(INFO));
ASSERT_TRUE(SentryPrivilegesFetcher::SentryPrivilegeIsWellFormed(
real_privilege, requested_authorizable, &granted_scope, &granted_action));
ASSERT_EQ(SentryAuthorizableScope::TABLE, granted_scope);
ASSERT_EQ(SentryAction::ALL, granted_action);
}
}
class SentryAuthzProviderTest : public SentryTestBase {
public:
static const char* const kTestUser;
static const char* const kTrustedUser;
static const char* const kUserGroup;
static const char* const kRoleName;
void SetUp() override {
SentryTestBase::SetUp();
metric_entity_ = METRIC_ENTITY_server.Instantiate(
&metric_registry_, "sentry_auth_provider-test");
// Configure the SentryAuthzProvider flags.
FLAGS_sentry_privileges_cache_capacity_mb = CachingEnabled() ? 1 : 0;
FLAGS_sentry_service_rpc_addresses = sentry_->address().ToString();
FLAGS_trusted_user_acl = kTrustedUser;
sentry_authz_provider_.reset(new SentryAuthzProvider(metric_entity_));
ASSERT_OK(sentry_authz_provider_->Start());
}
bool KerberosEnabled() const override {
// The returned value corresponds to the actual setting of the
// --sentry_service_security_mode flag; now it's "kerberos" by default.
return true;
}
virtual bool CachingEnabled() const {
return true;
}
Status StopSentry() {
RETURN_NOT_OK(sentry_client_->Stop());
RETURN_NOT_OK(sentry_->Stop());
return Status::OK();
}
Status StartSentry() {
RETURN_NOT_OK(sentry_->Start());
RETURN_NOT_OK(sentry_client_->Start());
return Status::OK();
}
Status DropRole() {
RETURN_NOT_OK(kudu::master::DropRole(sentry_client_.get(), kRoleName));
return sentry_authz_provider_->fetcher_.ResetCache();
}
Status CreateRoleAndAddToGroups() {
RETURN_NOT_OK(kudu::master::CreateRoleAndAddToGroups(
sentry_client_.get(), kRoleName, kUserGroup));
return sentry_authz_provider_->fetcher_.ResetCache();
}
Status AlterRoleGrantPrivilege(const TSentryPrivilege& privilege) {
RETURN_NOT_OK(kudu::master::AlterRoleGrantPrivilege(
sentry_client_.get(), kRoleName, privilege));
return sentry_authz_provider_->fetcher_.ResetCache();
}
#define GET_GAUGE_READINGS(func_name, counter_name_suffix) \
int64_t func_name() { \
scoped_refptr<Counter> gauge(metric_entity_->FindOrCreateCounter( \
&METRIC_sentry_client_##counter_name_suffix)); \
CHECK(gauge); \
return gauge->value(); \
}
GET_GAUGE_READINGS(GetTasksSuccessful, tasks_successful)
GET_GAUGE_READINGS(GetTasksFailedFatal, tasks_failed_fatal)
GET_GAUGE_READINGS(GetTasksFailedNonFatal, tasks_failed_nonfatal)
GET_GAUGE_READINGS(GetReconnectionsSucceeded, reconnections_succeeded)
GET_GAUGE_READINGS(GetReconnectionsFailed, reconnections_failed)
#undef GET_GAUGE_READINGS
#define GET_GAUGE_READINGS(func_name, counter_name_suffix) \
int64_t func_name() { \
scoped_refptr<Counter> gauge(metric_entity_->FindOrCreateCounter( \
&METRIC_sentry_privileges_##counter_name_suffix)); \
CHECK(gauge); \
return gauge->value(); \
}
GET_GAUGE_READINGS(GetCacheEvictions, cache_evictions)
GET_GAUGE_READINGS(GetCacheEvictionsExpired, cache_evictions_expired)
GET_GAUGE_READINGS(GetCacheHitsExpired, cache_hits_expired)
GET_GAUGE_READINGS(GetCacheHits, cache_hits)
GET_GAUGE_READINGS(GetCacheInserts, cache_inserts)
GET_GAUGE_READINGS(GetCacheLookups, cache_lookups)
GET_GAUGE_READINGS(GetCacheMisses, cache_misses)
#undef GET_GAUGE_READINGS
int64_t GetCacheUsage() {
scoped_refptr<AtomicGauge<uint64>> gauge(metric_entity_->FindOrCreateGauge(
&METRIC_sentry_privileges_cache_memory_usage, static_cast<uint64>(0)));
CHECK(gauge);
return gauge->value();
}
protected:
MetricRegistry metric_registry_;
scoped_refptr<MetricEntity> metric_entity_;
unique_ptr<SentryAuthzProvider> sentry_authz_provider_;
};
const char* const SentryAuthzProviderTest::kTestUser = "test-user";
const char* const SentryAuthzProviderTest::kTrustedUser = "trusted-user";
const char* const SentryAuthzProviderTest::kUserGroup = "user";
const char* const SentryAuthzProviderTest::kRoleName = "developer";
namespace {
const SentryActionsSet kAllActions({
SentryAction::ALL,
SentryAction::METADATA,
SentryAction::SELECT,
SentryAction::INSERT,
SentryAction::UPDATE,
SentryAction::DELETE,
SentryAction::ALTER,
SentryAction::CREATE,
SentryAction::DROP,
SentryAction::OWNER,
});
} // anonymous namespace
namespace {
// Indicates different invalid privilege response types to be injected.
enum class InvalidPrivilege {
// No error is injected.
NONE,
// The action string is set to something other than the expected action.
INCORRECT_ACTION,
// The scope string is set to something other than the expected scope.
INCORRECT_SCOPE,
// The 'serverName' field is set to something other than the authorizable's
// server name. Why just the server? This guarantees that a request for any
// authorizable scope will ignore such invalid privileges. E.g. say we
// instead granted an incorrect 'tableName'; assuming the dbName were still
// correct, a request at the database scope would correctly _not_ ignore the
// privilege. So to ensure that these InvalidPrivileges always yield
// privileges that are ignored, we exclusively butcher the 'server' field.
INCORRECT_SERVER,
// One of the scope fields (e.g. serverName, dbName, etc.) is unexpectedly
// missing or unexpectedly set. Note: Sentry servers don't allow an empty
// 'server' scope; if erasing the 'server' field, we'll instead set it to
// something other than the expected server.
FLIPPED_FIELD,
};
constexpr const char* kDb = "db";
constexpr const char* kTable = "table";
constexpr const char* kColumn = "column";
} // anonymous namespace
// Benchmark to test the time it takes to evaluate privileges when requesting
// privileges for braod authorization scopes (e.g. SERVER, DATABASE).
TEST_F(SentryAuthzProviderTest, BroadAuthzScopeBenchmark) {
const char* kLongDb = "DbWithLongName";
const char* kLongTable = "TableWithLongName";
ASSERT_OK(CreateRoleAndAddToGroups());
// Create a database with a bunch tables in it.
int kNumTables = FLAGS_num_table_privileges;
for (int i = 0; i < kNumTables; i++) {
KLOG_EVERY_N_SECS(INFO, 3) << Substitute("num tables granted: $0", i);
ASSERT_OK(AlterRoleGrantPrivilege(
GetTablePrivilege(kLongDb, Substitute("$0_$1", kLongTable, i), "OWNER")));
}
// Time how long it takes to get the database privileges via authorizing a
// create table request.
Status s;
LOG_TIMING(INFO, "Getting database privileges") {
s = sentry_authz_provider_->AuthorizeCreateTable(
Substitute("$0.$1_$2", kLongDb, kLongTable, 0) , kTestUser, kTestUser);
}
ASSERT_TRUE(s.IsNotAuthorized());
}
// Benchmark to test the time it takes to evaluate privileges when listing
// tables.
TEST_F(SentryAuthzProviderTest, ListTablesBenchmark) {
ASSERT_OK(CreateRoleAndAddToGroups());
unordered_set<string> tables;
for (int d = 0; d < FLAGS_num_databases; d++) {
const string db_name = Substitute("$0_$1", kDb, d);
// Regardless of whether the user has database-level privileges on the
// database, make sure there's at least one privilege for a database to
// keep the benchmark consistent when toggling this flag.
const string dummy_name = Substitute("$0_$1", "foo", d);
ASSERT_OK(AlterRoleGrantPrivilege(
GetDatabasePrivilege(FLAGS_has_db_privileges ? db_name : dummy_name, "METADATA")));
for (int t = 0; t < FLAGS_num_tables_per_db; t++) {
const string table_name = Substitute("$0_$1", kTable, t);
const string table_ident = Substitute("$0.$1", db_name, table_name);
KLOG_EVERY_N_SECS(INFO, 3) << "Granted privilege on table: " << table_ident;
ASSERT_OK(AlterRoleGrantPrivilege(GetTablePrivilege(db_name, table_name, "METADATA")));
EmplaceOrDie(&tables, table_ident);
}
}
bool checked_table_names = false;
LOG_TIMING(INFO, "Listing tables") {
ASSERT_OK(sentry_authz_provider_->AuthorizeListTables(
kTestUser, &tables, &checked_table_names));
}
ASSERT_TRUE(checked_table_names);
ASSERT_EQ(FLAGS_num_databases * FLAGS_num_tables_per_db, tables.size());
}
TEST_F(SentryAuthzProviderTest, TestListTables) {
ASSERT_OK(CreateRoleAndAddToGroups());
const int kNumDbs = 2;
const int kNumTablesPerDb = 5;
const int kNumNonHiveTables = 3;
unordered_set<string> tables;
for (int d = 0; d < kNumDbs; d++) {
const string db_name = Substitute("$0_$1", kDb, d);
for (int t = 0; t < kNumTablesPerDb; t++) {
const string table_name = Substitute("$0_$1", kTable, t);
// To test the absence of privileges, only grant privileges on one table
// per database.
if (t == 0) {
ASSERT_OK(AlterRoleGrantPrivilege(GetTablePrivilege(db_name, table_name, "METADATA")));
}
EmplaceOrDie(&tables, Substitute("$0.$1", db_name, table_name));
}
}
// Add some tables that don't conform to Hive's naming convention.
for (int i = 0; i < kNumNonHiveTables; i++) {
EmplaceOrDie(&tables, Substitute("badname_$0!", i));
}
bool checked_table_names = false;
// List tables as a trusted user. All tables, including non-Hive-conformant
// ones, should be visible.
ASSERT_OK(sentry_authz_provider_->AuthorizeListTables(
kTrustedUser, &tables, &checked_table_names));
ASSERT_FALSE(checked_table_names);
ASSERT_EQ(kNumDbs * kNumTablesPerDb + kNumNonHiveTables, tables.size());
// Now try as a regular user. Only the tables with Hive-conformant names that
// the user has privileges on should be visible.
ASSERT_OK(sentry_authz_provider_->AuthorizeListTables(kTestUser, &tables, &checked_table_names));
ASSERT_TRUE(checked_table_names);
ASSERT_EQ(kNumDbs, tables.size());
// When requires database level privileges for list tables, user shouldn't see
// any tables with only table level privileges.
FLAGS_sentry_require_db_privileges_for_list_tables = true;
ASSERT_OK(sentry_authz_provider_->AuthorizeListTables(kTestUser, &tables, &checked_table_names));
ASSERT_TRUE(checked_table_names);
ASSERT_EQ(0, tables.size());
}
class SentryAuthzProviderFilterPrivilegesTest : public SentryAuthzProviderTest {
public:
SentryAuthzProviderFilterPrivilegesTest()
: prng_(SeedRandom()) {
}
void SetUp() override {
SentryAuthzProviderTest::SetUp();
ASSERT_OK(CreateRoleAndAddToGroups());
full_authorizable_.server = FLAGS_server_name;
full_authorizable_.db = kDb;
full_authorizable_.table = kTable;
full_authorizable_.column = kColumn;
}
// Creates a Sentry privilege for the user based on the given action,
// the given scope, and the given authorizable that has all scope fields set.
// With all of the scope fields set in the authorizable, and a given scope,
// we can return an appropriate privilege for it, with tweaks indicated by
// 'invalid_privilege' to make the privilege invalid if desired.
TSentryPrivilege CreatePrivilege(const TSentryAuthorizable& full_authorizable,
const SentryAuthorizableScope& scope, const SentryAction& action,
InvalidPrivilege invalid_privilege = InvalidPrivilege::NONE) {
DCHECK(!full_authorizable.server.empty() && !full_authorizable.db.empty() &&
!full_authorizable.table.empty() && !full_authorizable.column.empty());
TSentryPrivilege privilege;
privilege.__set_action(invalid_privilege == InvalidPrivilege::INCORRECT_ACTION ?
"foobar" : ActionToString(action.action()));
privilege.__set_privilegeScope(invalid_privilege == InvalidPrivilege::INCORRECT_SCOPE ?
"foobar" : ScopeToString(scope.scope()));
// Select a scope at which we'll mess up the privilege request's field.
AuthorizableScopesSet nonempty_fields =
SentryPrivilegesFetcher::ExpectedNonEmptyFields(scope.scope());
if (invalid_privilege == InvalidPrivilege::FLIPPED_FIELD) {
static const AuthorizableScopesSet kMessUpCandidates = {
SentryAuthorizableScope::SERVER,
SentryAuthorizableScope::DATABASE,
SentryAuthorizableScope::TABLE,
SentryAuthorizableScope::COLUMN,
};
SentryAuthorizableScope::Scope field_to_mess_up =
SelectRandomElement<AuthorizableScopesSet, SentryAuthorizableScope::Scope, Random>(
kMessUpCandidates, &prng_);
if (ContainsKey(nonempty_fields, field_to_mess_up)) {
// Since Sentry servers don't allow empty 'server' fields in requests,
// rather flipping the empty status of the field, inject an incorrect
// value for the field.
if (field_to_mess_up == SentryAuthorizableScope::SERVER) {
invalid_privilege = InvalidPrivilege::INCORRECT_SERVER;
} else {
nonempty_fields.erase(field_to_mess_up);
}
} else {
InsertOrDie(&nonempty_fields, field_to_mess_up);
}
}
// Fill in any fields we may need.
for (const auto& field : nonempty_fields) {
switch (field) {
case SentryAuthorizableScope::SERVER:
privilege.__set_serverName(invalid_privilege == InvalidPrivilege::INCORRECT_SERVER ?
"foobar" : full_authorizable.server);
break;
case SentryAuthorizableScope::DATABASE:
privilege.__set_dbName(full_authorizable.db);
break;
case SentryAuthorizableScope::TABLE:
privilege.__set_tableName(full_authorizable.table);
break;
case SentryAuthorizableScope::COLUMN:
privilege.__set_columnName(full_authorizable.column);
break;
default:
LOG(FATAL) << "not a valid scope field: " << field;
}
}
return privilege;
}
protected:
// Authorizable that has all scope fields set; useful for generating
// privilege requests.
TSentryAuthorizable full_authorizable_;
private:
mutable Random prng_;
};
TEST_F(SentryAuthzProviderFilterPrivilegesTest, TestTablePrivilegePBParsing) {
constexpr int kNumColumns = 10;
SchemaBuilder schema_builder;
schema_builder.AddKeyColumn("col0", DataType::INT32);
vector<string> column_names = { "col0" };
for (int i = 1; i < kNumColumns; i++) {
const string col = Substitute("col$0", i);
schema_builder.AddColumn(ColumnSchema(col, DataType::INT32),
/*is_key=*/false);
column_names.emplace_back(col);
}
SchemaPB schema_pb;
ASSERT_OK(SchemaToPB(schema_builder.Build(), &schema_pb));
unordered_map<string, ColumnId> col_name_to_id;
for (const auto& col_pb : schema_pb.columns()) {
EmplaceOrDie(&col_name_to_id, col_pb.name(), ColumnId(col_pb.id()));
}
// First, grant some privileges at the table authorizable scope or higher.
Random prng(SeedRandom());
vector<SentryAuthorizableScope::Scope> scope_to_grant_that_implies_table =
SelectRandomSubset<vector<SentryAuthorizableScope::Scope>,
SentryAuthorizableScope::Scope, Random>({ SentryAuthorizableScope::SERVER,
SentryAuthorizableScope::DATABASE,
SentryAuthorizableScope::TABLE }, 0, &prng);
unordered_map<SentryAuthorizableScope::Scope, SentryActionsSet, std::hash<int>>
granted_privileges;
SentryActionsSet table_privileges;
TSentryAuthorizable table_authorizable;
table_authorizable.__set_server(FLAGS_server_name);
table_authorizable.__set_db(kDb);
table_authorizable.__set_table(kTable);
table_authorizable.__set_column(column_names[0]);
for (const auto& granted_scope : scope_to_grant_that_implies_table) {
for (const auto& action : SelectRandomSubset<SentryActionsSet, SentryAction::Action, Random>(
kAllActions, 0, &prng)) {
// Grant the privilege to the user.
TSentryPrivilege table_privilege = CreatePrivilege(table_authorizable,
SentryAuthorizableScope(granted_scope), SentryAction(action));
ASSERT_OK(AlterRoleGrantPrivilege(table_privilege));
// All of the privileges imply the table-level action.
InsertIfNotPresent(&table_privileges, action);
}
}
// Grant some privileges at the column scope.
vector<string> columns_to_grant =
SelectRandomSubset<vector<string>, string, Random>(column_names, 0, &prng);
unordered_set<ColumnId> scannable_columns;
for (const auto& column_name : columns_to_grant) {
for (const auto& action : SelectRandomSubset<SentryActionsSet, SentryAction::Action, Random>(
kAllActions, 0, &prng)) {
// Grant the privilege to the user.
TSentryPrivilege column_privilege =
GetColumnPrivilege(kDb, kTable, column_name, ActionToString(action));
ASSERT_OK(AlterRoleGrantPrivilege(column_privilege));
if (SentryAction(action).Implies(SentryAction(SentryAction::SELECT))) {
InsertIfNotPresent(&scannable_columns, FindOrDie(col_name_to_id, column_name));
}
}
}
// Make sure that any implied privileges make their way to the token.
const string kTableId = "table-id";
TablePrivilegePB expected_pb;
expected_pb.set_table_id(kTableId);
for (const auto& granted_table_action : table_privileges) {
if (SentryAction(granted_table_action).Implies(SentryAction(SentryAction::INSERT))) {
expected_pb.set_insert_privilege(true);
}
if (SentryAction(granted_table_action).Implies(SentryAction(SentryAction::UPDATE))) {
expected_pb.set_update_privilege(true);
}
if (SentryAction(granted_table_action).Implies(SentryAction(SentryAction::DELETE))) {
expected_pb.set_delete_privilege(true);
}
if (SentryAction(granted_table_action).Implies(SentryAction(SentryAction::SELECT))) {
expected_pb.set_scan_privilege(true);
}
}
// If any of the table-level privileges imply privileges on scan, we
// shouldn't expect per-column scan privileges. Otherwise, we should expect
// the columns privileges that implied SELECT to have scan privileges.
if (!expected_pb.scan_privilege()) {
ColumnPrivilegePB scan_col_privilege;
scan_col_privilege.set_scan_privilege(true);
for (const auto& id : scannable_columns) {
InsertIfNotPresent(expected_pb.mutable_column_privileges(), id, scan_col_privilege);
}
}
// Validate the privileges went through.
TablePrivilegePB privilege_pb;
privilege_pb.set_table_id(kTableId);
ASSERT_OK(sentry_authz_provider_->FillTablePrivilegePB(Substitute("$0.$1", kDb, kTable),
kTestUser, schema_pb, &privilege_pb));
ASSERT_TRUE(MessageDifferencer::Equals(expected_pb, privilege_pb))
<< Substitute("$0 vs $1", SecureDebugString(expected_pb), SecureDebugString(privilege_pb));
}
// Parameterized on the scope at which the privilege will be granted.
class SentryAuthzProviderFilterPrivilegesScopeTest :
public SentryAuthzProviderFilterPrivilegesTest,
public ::testing::WithParamInterface<SentryAuthorizableScope::Scope> {};
// Attempt to grant privileges for various actions on a single scope of an
// authorizable, injecting various invalid privileges, and checking that Kudu
// ignores them.
TEST_P(SentryAuthzProviderFilterPrivilegesScopeTest, TestFilterInvalidResponses) {
const string& table_ident = Substitute("$0.$1", full_authorizable_.db, full_authorizable_.table);
static constexpr InvalidPrivilege kInvalidPrivileges[] = {
InvalidPrivilege::INCORRECT_ACTION,
InvalidPrivilege::INCORRECT_SCOPE,
InvalidPrivilege::INCORRECT_SERVER,
InvalidPrivilege::FLIPPED_FIELD,
};
SentryAuthorizableScope granted_scope(GetParam());
for (const auto& action : kAllActions) {
for (const auto& ip : kInvalidPrivileges) {
TSentryPrivilege privilege = CreatePrivilege(full_authorizable_, granted_scope,
SentryAction(action), ip);
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
}
}
for (const auto& requested_scope : { SentryAuthorizableScope::SERVER,
SentryAuthorizableScope::DATABASE,
SentryAuthorizableScope::TABLE }) {
SentryPrivilegesBranch privileges_info;
ASSERT_OK(sentry_authz_provider_->fetcher_.GetSentryPrivileges(
requested_scope, table_ident, kTestUser,
SentryCaching::ALL, &privileges_info));
// Kudu should ignore all of the invalid privileges.
ASSERT_TRUE(privileges_info.privileges().empty());
}
}
// Grants privileges for various actions on a single scope of an authorizable.
TEST_P(SentryAuthzProviderFilterPrivilegesScopeTest, TestFilterValidResponses) {
const string& table_ident = Substitute("$0.$1", full_authorizable_.db, full_authorizable_.table);
SentryAuthorizableScope granted_scope(GetParam());
// Send valid requests and verify that we can get it back through the
// SentryAuthzProvider.
for (const auto& action : kAllActions) {
TSentryPrivilege privilege = CreatePrivilege(full_authorizable_, granted_scope,
SentryAction(action));
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
}
for (const auto& requested_scope : { SentryAuthorizableScope::SERVER,
SentryAuthorizableScope::DATABASE,
SentryAuthorizableScope::TABLE }) {
SentryPrivilegesBranch privileges_info;
ASSERT_OK(sentry_authz_provider_->fetcher_.GetSentryPrivileges(
requested_scope, table_ident, kTestUser,
SentryCaching::ALL, &privileges_info));
ASSERT_EQ(1, privileges_info.privileges().size());
const auto& authorizable_privileges = *privileges_info.privileges().cbegin();
ASSERT_EQ(GetParam(), authorizable_privileges.scope)
<< ScopeToString(authorizable_privileges.scope);
ASSERT_FALSE(authorizable_privileges.allowed_actions.empty());
}
}
INSTANTIATE_TEST_CASE_P(GrantedScopes, SentryAuthzProviderFilterPrivilegesScopeTest,
::testing::Values(SentryAuthorizableScope::SERVER,
SentryAuthorizableScope::DATABASE,
SentryAuthorizableScope::TABLE,
SentryAuthorizableScope::COLUMN));
// Test to create tables requiring ALL ON DATABASE with the grant option. This
// is parameterized on the ALL scope and OWNER actions, which behave
// identically.
class CreateTableAuthorizationTest :
public SentryAuthzProviderTest,
public ::testing::WithParamInterface<string> {
};
TEST_P(CreateTableAuthorizationTest, TestAuthorizeCreateTable) {
// Don't authorize create table on a non-existent user.
Status s = sentry_authz_provider_->AuthorizeCreateTable("db.table",
"non-existent-user",
"non-existent-user");
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
// Don't authorize create table on a user without any privileges.
s = sentry_authz_provider_->AuthorizeCreateTable("db.table", kTestUser, kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
// Don't authorize create table on a user without required privileges.
ASSERT_OK(CreateRoleAndAddToGroups());
TSentryPrivilege privilege = GetDatabasePrivilege("db", "DROP");
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
s = sentry_authz_provider_->AuthorizeCreateTable("db.table", kTestUser, kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
// Authorize create table on a user with proper privileges.
privilege = GetDatabasePrivilege("db", "CREATE");
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
ASSERT_OK(sentry_authz_provider_->AuthorizeCreateTable("db.table", kTestUser, kTestUser));
// Table creation with a different owner than the user
// requires the creating user have 'ALL on DATABASE' with grant.
s = sentry_authz_provider_->AuthorizeCreateTable("db.table", kTestUser, "diff-user");
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
const auto& all = GetParam();
privilege = GetDatabasePrivilege("db", all);
s = sentry_authz_provider_->AuthorizeCreateTable("db.table", kTestUser, "diff-user");
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
privilege = GetDatabasePrivilege("db", all, TSentryGrantOption::ENABLED);
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
ASSERT_OK(sentry_authz_provider_->AuthorizeCreateTable("db.table", kTestUser, "diff-user"));
}
INSTANTIATE_TEST_CASE_P(AllOrOwner, CreateTableAuthorizationTest,
::testing::Values("ALL", "OWNER"));
TEST_F(SentryAuthzProviderTest, TestAuthorizeDropTable) {
// Don't authorize delete table on a user without required privileges.
ASSERT_OK(CreateRoleAndAddToGroups());
TSentryPrivilege privilege = GetDatabasePrivilege("db", "SELECT");
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
Status s = sentry_authz_provider_->AuthorizeDropTable("db.table", kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
// Authorize delete table on a user with proper privileges.
privilege = GetDatabasePrivilege("db", "DROP");
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
ASSERT_OK(sentry_authz_provider_->AuthorizeDropTable("db.table", kTestUser));
}
TEST_F(SentryAuthzProviderTest, TestAuthorizeAlterTable) {
// Don't authorize alter table on a user without required privileges.
ASSERT_OK(CreateRoleAndAddToGroups());
TSentryPrivilege db_privilege = GetDatabasePrivilege("db", "SELECT");
ASSERT_OK(AlterRoleGrantPrivilege(db_privilege));
Status s = sentry_authz_provider_->AuthorizeAlterTable("db.table", "db.table", kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
// Authorize alter table without rename on a user with proper privileges.
db_privilege = GetDatabasePrivilege("db", "ALTER");
ASSERT_OK(AlterRoleGrantPrivilege(db_privilege));
ASSERT_OK(sentry_authz_provider_->AuthorizeAlterTable("db.table", "db.table", kTestUser));
// Table alteration with rename requires 'ALL ON TABLE <old-table>' and
// 'CREATE ON DATABASE <new-database>'
s = sentry_authz_provider_->AuthorizeAlterTable("db.table", "new_db.new_table", kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
// Authorize alter table without rename on a user with proper privileges.
db_privilege = GetDatabasePrivilege("new_db", "CREATE");
ASSERT_OK(AlterRoleGrantPrivilege(db_privilege));
TSentryPrivilege table_privilege = GetTablePrivilege("db", "table", "ALL");
ASSERT_OK(AlterRoleGrantPrivilege(table_privilege));
ASSERT_OK(sentry_authz_provider_->AuthorizeAlterTable("db.table",
"new_db.new_table",
kTestUser));
}
TEST_F(SentryAuthzProviderTest, TestAuthorizeGetTableStatistics) {
// Don't authorize getting statistics of a table for a user without required
// privileges.
ASSERT_OK(CreateRoleAndAddToGroups());
Status s = sentry_authz_provider_->AuthorizeGetTableStatistics("db.table", kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
// Authorize get table statistics on a user with proper privileges.
TSentryPrivilege privilege = GetDatabasePrivilege("db", "SELECT");
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
ASSERT_OK(sentry_authz_provider_->AuthorizeGetTableStatistics("db.table", kTestUser));
}
TEST_F(SentryAuthzProviderTest, TestAuthorizeGetTableMetadata) {
// Don't authorize getting metadata on a table for a user without required
// privileges.
ASSERT_OK(CreateRoleAndAddToGroups());
Status s = sentry_authz_provider_->AuthorizeGetTableMetadata("db.table", kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
// Authorize getting metadata on a table for a user with proper privileges.
TSentryPrivilege privilege = GetDatabasePrivilege("db", "SELECT");
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
ASSERT_OK(sentry_authz_provider_->AuthorizeGetTableMetadata("db.table", kTestUser));
}
TEST_F(SentryAuthzProviderTest, TestInvalidAction) {
ASSERT_OK(CreateRoleAndAddToGroups());
TSentryPrivilege privilege = GetDatabasePrivilege("db", "invalid");
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
// User has privileges with invalid action cannot operate on the table.
Status s = sentry_authz_provider_->AuthorizeCreateTable("DB.table", kTestUser, kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
}
TEST_F(SentryAuthzProviderTest, TestInvalidAuthzScope) {
ASSERT_OK(CreateRoleAndAddToGroups());
TSentryPrivilege privilege = GetDatabasePrivilege("db", "ALL");
privilege.__set_privilegeScope("invalid");
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
// User has privileges with invalid authorizable scope cannot operate
// on the table.
Status s = sentry_authz_provider_->AuthorizeCreateTable("DB.table", kTestUser, kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
}
// Ensures Sentry privileges are case insensitive.
TEST_F(SentryAuthzProviderTest, TestPrivilegeCaseSensitivity) {
ASSERT_OK(CreateRoleAndAddToGroups());
TSentryPrivilege privilege = GetDatabasePrivilege("db", "create");
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
ASSERT_OK(sentry_authz_provider_->AuthorizeCreateTable("DB.table", kTestUser, kTestUser));
}
// Verify the behavior of the SentryAuthzProvider's cache upon fetching
// privilege information on authorizables of the TABLE scope in
// 'adjacent branches' of the authz hierarchy.
TEST_F(SentryAuthzProviderTest, CacheBehaviorScopeHierarchyAdjacentBranches) {
ASSERT_OK(CreateRoleAndAddToGroups());
ASSERT_OK(AlterRoleGrantPrivilege(GetDatabasePrivilege("db", "METADATA")));
ASSERT_OK(AlterRoleGrantPrivilege(GetTablePrivilege("db", "t0", "ALTER")));
ASSERT_OK(AlterRoleGrantPrivilege(GetTablePrivilege("db", "t1", "ALTER")));
// ALTER TABLE, if not renaming the table itself, requires ALTER privilege
// on the table, but nothing is required on the database that contains
// the table.
ASSERT_EQ(0, GetTasksSuccessful());
ASSERT_OK(sentry_authz_provider_->AuthorizeAlterTable(
"db.t0", "db.t0", kTestUser));
ASSERT_EQ(1, GetTasksSuccessful());
// The cache was empty. The query was for TABLE scope privileges, so the
// cache was examined for both DATABASE and TABLE scope entries, and both
// were missing. After fetching information on privileges granted to the user
// on table 'db.t0', the information received from Sentry was split and put
// into DATABASE and TABLE scope entries.
ASSERT_EQ(2, GetCacheLookups());
ASSERT_EQ(2, GetCacheMisses());
ASSERT_EQ(0, GetCacheHits());
ASSERT_EQ(2, GetCacheInserts());
ASSERT_OK(sentry_authz_provider_->AuthorizeAlterTable(
"db.t1", "db.t1", kTestUser));
// Information on the user's privileges granted on 'db.t1' was not present
// in the cache: there was an RPC request sent to Sentry.
ASSERT_EQ(2, GetTasksSuccessful());
// One more cache miss: TABLE scope entry for 'db.t1' was absent.
ASSERT_EQ(3, GetCacheMisses());
// One more cache hit: DATABASE scope entry was already present.
ASSERT_EQ(1, GetCacheHits());
// Updated already existing DATABASE and inserted new TABLE entry.
ASSERT_EQ(4, GetCacheInserts());
// METADATA requires corresponding privilege granted on the table, but nothing
// is required on the database.
ASSERT_OK(sentry_authz_provider_->AuthorizeGetTableMetadata(
"db.other_table", kTestUser));
ASSERT_EQ(3, GetTasksSuccessful());
// One more cache miss: TABLE scope entry for 'db.other_table' was absent.
ASSERT_EQ(4, GetCacheMisses());
// One more cache hit: DATABASE scope entry was already present.
ASSERT_EQ(2, GetCacheHits());
// Updated already existing DATABASE and inserted new TABLE entry.
ASSERT_EQ(6, GetCacheInserts());
// Repeat all the requests above: not a single new RPC to Sentry should be
// sent since all authz queries must hit the cache: that's about repeating
// the same requests.
ASSERT_OK(sentry_authz_provider_->AuthorizeAlterTable(
"db.t0", "db.t0", kTestUser));
ASSERT_EQ(4, GetCacheHits());
ASSERT_OK(sentry_authz_provider_->AuthorizeAlterTable(
"db.t1", "db.t1", kTestUser));
ASSERT_EQ(6, GetCacheHits());
ASSERT_OK(sentry_authz_provider_->AuthorizeGetTableMetadata(
"db.other_table", kTestUser));
ASSERT_EQ(8, GetCacheHits());
// No new cache misses.
ASSERT_EQ(4, GetCacheMisses());
// No additional inserts, of course.
ASSERT_EQ(6, GetCacheInserts());
// No additional RPC requests to Sentry.
ASSERT_EQ(3, GetTasksSuccessful());
// All the requests below should also hit the cache since the information on
// the privileges granted on each of the tables in the requests below
// is in the cache. In the Sentry's privileges model for Kudu, DROP TABLE
// requires privileges on the table itself, but nothing is required on the
// database the table belongs to.
Status s = sentry_authz_provider_->AuthorizeDropTable("db.t0", kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
ASSERT_EQ(10, GetCacheHits());
s = sentry_authz_provider_->AuthorizeDropTable("db.t1", kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
ASSERT_EQ(12, GetCacheHits());
s = sentry_authz_provider_->AuthorizeDropTable("db.other_table", kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
ASSERT_EQ(14, GetCacheHits());
// No new cache misses.
ASSERT_EQ(4, GetCacheMisses());
// No additional inserts, of course.
ASSERT_EQ(6, GetCacheInserts());
// No additional RPC requests to Sentry.
ASSERT_EQ(3, GetTasksSuccessful());
// A sanity check: verify no failed requests are registered.
ASSERT_EQ(0, GetTasksFailedFatal());
ASSERT_EQ(0, GetTasksFailedNonFatal());
}
// Ensure requests to authorize CreateTables and AlterTables hit cache once
// the information was fetched from Sentry for an authorizable of the TABLE
// scope in the same hierarchy branch. A bit of context: Sentry sends all
// available information for the branch up the authz scope hierarchy.
TEST_F(SentryAuthzProviderTest, CacheBehaviorForCreateAndAlter) {
ASSERT_OK(CreateRoleAndAddToGroups());
ASSERT_OK(AlterRoleGrantPrivilege(GetDatabasePrivilege("db0", "ALTER")));
ASSERT_OK(AlterRoleGrantPrivilege(GetDatabasePrivilege("db1", "CREATE")));
ASSERT_EQ(0, GetTasksSuccessful());
// ALTER TABLE, if not renaming the table itself, requires privileges on the
// table only.
ASSERT_OK(sentry_authz_provider_->AuthorizeAlterTable(
"db0.t0", "db0.t0", kTestUser));
ASSERT_EQ(1, GetTasksSuccessful());
// The cache was empty. The query was for TABLE scope privileges, so the
// cache was examined for both DATABASE and TABLE scope entries, and both
// were missing. After fetching information on privileges granted to the user
// on table 'db.t0', the information received from Sentry was split and put
// into DATABASE and TABLE scope entries.
ASSERT_EQ(2, GetCacheLookups());
ASSERT_EQ(2, GetCacheMisses());
ASSERT_EQ(0, GetCacheHits());
ASSERT_EQ(2, GetCacheInserts());
// The CREATE privileges is not granted on the 'db0', so the request must
// not be authorized.
auto s = sentry_authz_provider_->AuthorizeCreateTable(
"db0.t1", kTestUser, kTestUser);
ASSERT_TRUE(s.IsNotAuthorized());
// CREATE TABLE requires privileges on the database only, and those should
// have been cached already due to the prior request.
ASSERT_EQ(1, GetTasksSuccessful());
// One more cache lookup of the corresponding DATABASE scope key.
ASSERT_EQ(3, GetCacheLookups());
// No new cache misses.
ASSERT_EQ(2, GetCacheMisses());
// Single cache lookup turned to be a cache hit.
ASSERT_EQ(1, GetCacheHits());
// No new RPCs to Sentry should be issued: the information on privileges
// on 'db1' authorizable of the DATABASE scope should be fetched and cached
// while fetching the information privileges on 'db1.t0' authorizable of the
// TABLE scope.
for (int idx = 0; idx < 10; ++idx) {
const auto table_name = Substitute("db1.t$0", idx);
ASSERT_OK(sentry_authz_provider_->AuthorizeCreateTable(
table_name, kTestUser, kTestUser));
}
// Only a single new RPC should be issued to Sentry: to get information
// for "db1" authorizable of the DATABASE scope while authorizing the creation
// of table "db1.t0". All other requests must hit the cache.
ASSERT_EQ(2, GetTasksSuccessful());
// Ten more cache lookups of the corresponding DATABASE scope key: one turned
// to be a miss and other nine hits after the information was fetched
// from Sentry and inserted into the cache.
ASSERT_EQ(13, GetCacheLookups());
ASSERT_EQ(3, GetCacheMisses());
ASSERT_EQ(10, GetCacheHits());
// One more insert: adding an entry for the DATABASE scope key for 'db1'.
ASSERT_EQ(3, GetCacheInserts());
// Same story for requests for 'db1.t0', ..., 'db1.t19'.
for (int idx = 0; idx < 20; ++idx) {
const auto table_name = Substitute("db1.t$0", idx);
ASSERT_OK(sentry_authz_provider_->AuthorizeCreateTable(
table_name, kTestUser, kTestUser));
}
ASSERT_EQ(2, GetTasksSuccessful());
// All twenty lookups hit the cache, no new misses.
ASSERT_EQ(33, GetCacheLookups());
ASSERT_EQ(30, GetCacheHits());
ASSERT_EQ(3, GetCacheMisses());
// A sanity check: verify no failed requests are registered.
ASSERT_EQ(0, GetTasksFailedFatal());
ASSERT_EQ(0, GetTasksFailedNonFatal());
}
// A scenario where a TABLE-scope privilege on a table is granted to a user,
// but there isn't any DATABASE-scope privilege granted on the database
// the table belongs to. After authorizing an operation on the table, there
// should not be another RPC to Sentry issued while authorizing an operation
// on the database itself.
TEST_F(SentryAuthzProviderTest, CacheBehaviorHybridLookups) {
ASSERT_OK(CreateRoleAndAddToGroups());
ASSERT_OK(AlterRoleGrantPrivilege(GetTablePrivilege("db", "t", "ALL")));
ASSERT_EQ(0, GetTasksSuccessful());
// In the Sentry's authz model for Kudu, DROP TABLE requires only privileges
// on the table itself.
ASSERT_OK(sentry_authz_provider_->AuthorizeDropTable("db.t", kTestUser));
ASSERT_EQ(1, GetTasksSuccessful());
// The cache was empty. The query was for TABLE scope privileges, so the
// cache was examined for both DATABASE and TABLE scope entries, and both
// were missing. After fetching information on privileges granted to the user
// on table 'db.t', the information received from Sentry was split and put
// into DATABASE and TABLE scope entries.
ASSERT_EQ(0, GetCacheHits());
ASSERT_EQ(2, GetCacheLookups());
ASSERT_EQ(2, GetCacheMisses());
ASSERT_EQ(2, GetCacheInserts());
// CREATE TABLE requires privileges only on the database itself. No privileges
// are granted on the database, so the request must not be authorized.
auto s = sentry_authz_provider_->AuthorizeCreateTable(
"db.t", kTestUser, kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
// No extra RPC should be sent to Sentry: the information on the privileges
// granted on relevant authorizables of the DATABASE scope in corresponding
// branch should have been fetched and cached.
ASSERT_EQ(1, GetTasksSuccessful());
// One more lookup in the cache that turned to a cache hit: it was necessary
// to lookup only DATABASE scope entry in the cache.
ASSERT_EQ(3, GetCacheLookups());
ASSERT_EQ(1, GetCacheHits());
// ALTER TABLE, if renaming the table, requires privileges both on the
// database and the table. Even if ALL is granted on the table itself, there
// isn't any privilege granted on the database, so the request to rename
// the table must not be authorized.
s = sentry_authz_provider_->AuthorizeAlterTable(
"db.t", "db.t_renamed", kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
// No extra RPCs are expected in this case.
ASSERT_EQ(1, GetTasksSuccessful());
// Three more lookups; three more cache hits: DATABASE and TABLE lookups
// are 'db.t'-related, and DATABASE lookup is 'db.t_renamed' related.
ASSERT_EQ(6, GetCacheLookups());
ASSERT_EQ(4, GetCacheHits());
// A sanity check: verify no failed requests are registered.
ASSERT_EQ(0, GetTasksFailedFatal());
ASSERT_EQ(0, GetTasksFailedNonFatal());
}
// Verify that information on TABLE-scope privileges are fetched from Sentry,
// but not cached when SentryPrivilegeFetcher receives a ListPrivilegesByUser
// response for a DATABASE-scope authorizable for CreateTables or AlterTables.
TEST_F(SentryAuthzProviderTest, CacheBehaviorNotCachingTableInfo) {
ASSERT_OK(CreateRoleAndAddToGroups());
ASSERT_OK(AlterRoleGrantPrivilege(GetDatabasePrivilege("db", "CREATE")));
ASSERT_OK(AlterRoleGrantPrivilege(GetTablePrivilege("db", "t0", "ALL")));
ASSERT_OK(AlterRoleGrantPrivilege(GetTablePrivilege("db", "t1", "ALTER")));
ASSERT_EQ(0, GetTasksSuccessful());
// In the Sentry's authz model for Kudu, CREATE TABLE requires only privileges
// on the database.
ASSERT_OK(sentry_authz_provider_->AuthorizeCreateTable(
"db.table", kTestUser, kTestUser));
ASSERT_EQ(1, GetTasksSuccessful());
ASSERT_EQ(1, GetCacheInserts());
ASSERT_EQ(1, GetCacheLookups());
ASSERT_EQ(1, GetCacheMisses());
// Examine the entry that has just been cached: it should not contain
// any information on authorizables of the TABLE scope under the 'db':
// the cache chops off everything of the TABLE and narrower scope from
// Sentry response before adding corresponding entry into the cache.
auto* cache = sentry_authz_provider_->fetcher_.cache_.get();
ASSERT_NE(nullptr, cache);
{
auto handle = cache->Get(
Substitute("$0/$1/$2", kTestUser, FLAGS_server_name, "db"));
ASSERT_TRUE(handle);
ASSERT_EQ(2, GetCacheLookups());
ASSERT_EQ(1, GetCacheHits());
const auto& value = handle.value();
for (const auto& privilege : value.privileges()) {
ASSERT_NE(SentryAuthorizableScope::TABLE, privilege.scope);
ASSERT_NE(SentryAuthorizableScope::COLUMN, privilege.scope);
}
}
// ALTER TABLE, if not renaming the table, requires privileges only on the
// table itself.
ASSERT_OK(sentry_authz_provider_->AuthorizeAlterTable(
"db.t0", "db.t0", kTestUser));
// Here an RPC request should be sent to Sentry to fetch information on
// privileges granted at the table 'db.t0'. That information was fetched
// from Sentry upon prior call to AuthorizeCreateTable(), but it was not
// cached deliberately: that way the cache avoids storing information on
// non-Kudu tables, if any, under an authorizable of the DATABASE scope.
ASSERT_EQ(2, GetTasksSuccessful());
ASSERT_EQ(2, GetCacheMisses());
// Two more lookups: one DATABASE scope and another TABLE scope lookup.
ASSERT_EQ(4, GetCacheLookups());
// Inserted DATABASE and TABLE entries.
ASSERT_EQ(3, GetCacheInserts());
// The same as above stays valid for the 'db.t1' table.
ASSERT_OK(sentry_authz_provider_->AuthorizeAlterTable(
"db.t1", "db.t1", kTestUser));
ASSERT_EQ(3, GetTasksSuccessful());
ASSERT_EQ(3, GetCacheMisses());
// Two more lookups: one DATABASE scope and another TABLE scope lookup.
ASSERT_EQ(6, GetCacheLookups());
// Updated already existing DATABASE and inserted new TABLE entry.
ASSERT_EQ(5, GetCacheInserts());
// A sanity check: verify no failed requests are registered.
ASSERT_EQ(0, GetTasksFailedFatal());
ASSERT_EQ(0, GetTasksFailedNonFatal());
}
// Whether the authz information received from Sentry is cached or not.
enum class AuthzCaching {
Disabled,
Enabled,
};
// Tests to ensure SentryAuthzProvider enforces access restrictions as expected.
// Parameterized by whether caching is enabled.
class SentryAuthzProviderReconnectionTest :
public SentryAuthzProviderTest,
public ::testing::WithParamInterface<AuthzCaching> {
public:
bool CachingEnabled() const override {
return GetParam() == AuthzCaching::Enabled;
}
};
INSTANTIATE_TEST_CASE_P(
, SentryAuthzProviderReconnectionTest,
::testing::Values(AuthzCaching::Disabled, AuthzCaching::Enabled));
// Checks that the SentryAuthzProvider handles reconnecting to Sentry
// after a connection failure, or service being too busy.
TEST_P(SentryAuthzProviderReconnectionTest, ConnectionFailureOrTooBusy) {
// Restart SentryAuthzProvider with configured timeout to reduce the run time
// of this test.
NO_FATALS(sentry_authz_provider_->Stop());
FLAGS_sentry_service_rpc_addresses = sentry_->address().ToString();
FLAGS_sentry_service_send_timeout_seconds = AllowSlowTests() ? 5 : 2;
FLAGS_sentry_service_recv_timeout_seconds = AllowSlowTests() ? 5 : 2;
sentry_authz_provider_.reset(new SentryAuthzProvider);
ASSERT_OK(sentry_authz_provider_->Start());
ASSERT_OK(CreateRoleAndAddToGroups());
TSentryPrivilege privilege = GetDatabasePrivilege("db", "METADATA");
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
ASSERT_OK(sentry_authz_provider_->AuthorizeGetTableMetadata("db.table", kTestUser));
// Shutdown Sentry and try a few operations.
ASSERT_OK(StopSentry());
Status s = sentry_authz_provider_->AuthorizeDropTable("db.table", kTestUser);
if (CachingEnabled()) {
EXPECT_TRUE(s.IsNotAuthorized()) << s.ToString();
} else {
EXPECT_TRUE(s.IsNetworkError()) << s.ToString();
}
s = sentry_authz_provider_->AuthorizeCreateTable("db.table", kTestUser, "diff-user");
if (CachingEnabled()) {
EXPECT_TRUE(s.IsNotAuthorized()) << s.ToString();
} else {
EXPECT_TRUE(s.IsNetworkError()) << s.ToString();
}
// Start Sentry back up and ensure that the same operations succeed.
ASSERT_OK(StartSentry());
ASSERT_EVENTUALLY([&] {
ASSERT_OK(sentry_authz_provider_->AuthorizeGetTableMetadata(
"db.table", kTestUser));
});
privilege = GetDatabasePrivilege("db", "DROP");
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
ASSERT_OK(sentry_authz_provider_->AuthorizeDropTable("db.table", kTestUser));
// Pause Sentry and try a few operations.
ASSERT_OK(sentry_->Pause());
s = sentry_authz_provider_->AuthorizeDropTable("db.table", kTestUser);
if (CachingEnabled()) {
EXPECT_TRUE(s.ok()) << s.ToString();
} else {
EXPECT_TRUE(s.IsTimedOut()) << s.ToString();
}
s = sentry_authz_provider_->AuthorizeGetTableMetadata("db.table", kTestUser);
if (CachingEnabled()) {
EXPECT_TRUE(s.ok()) << s.ToString();
} else {
EXPECT_TRUE(s.IsTimedOut()) << s.ToString();
}
// Resume Sentry and ensure that the same operations succeed.
ASSERT_OK(sentry_->Resume());
ASSERT_EVENTUALLY([&] {
ASSERT_OK(sentry_authz_provider_->AuthorizeDropTable(
"db.table", kTestUser));
});
}
// Test to ensure the authorization hierarchy rule of SentryAuthzProvider
// works as expected.
class TestAuthzHierarchy :
public SentryAuthzProviderTest,
public ::testing::WithParamInterface<SentryAuthorizableScope::Scope> {
};
TEST_P(TestAuthzHierarchy, TestAuthorizableScope) {
const SentryAuthorizableScope::Scope scope = GetParam();
const string action = "ALL";
const string db = "database";
const string tbl = "table";
const string col = "col";
vector<TSentryPrivilege> lower_hierarchy_privs;
vector<TSentryPrivilege> higher_hierarchy_privs;
const TSentryPrivilege column_priv = GetColumnPrivilege(db, tbl, col, action);
const TSentryPrivilege table_priv = GetTablePrivilege(db, tbl, action);
const TSentryPrivilege db_priv = GetDatabasePrivilege(db, action);
const TSentryPrivilege server_priv = GetServerPrivilege(action);
switch (scope) {
case SentryAuthorizableScope::Scope::TABLE:
higher_hierarchy_privs.emplace_back(table_priv);
FALLTHROUGH_INTENDED;
case SentryAuthorizableScope::Scope::DATABASE:
higher_hierarchy_privs.emplace_back(db_priv);
FALLTHROUGH_INTENDED;
case SentryAuthorizableScope::Scope::SERVER:
higher_hierarchy_privs.emplace_back(server_priv);
break;
default:
break;
}
switch (scope) {
case SentryAuthorizableScope::Scope::SERVER:
lower_hierarchy_privs.emplace_back(db_priv);
FALLTHROUGH_INTENDED;
case SentryAuthorizableScope::Scope::DATABASE:
lower_hierarchy_privs.emplace_back(table_priv);
FALLTHROUGH_INTENDED;
case SentryAuthorizableScope::Scope::TABLE:
lower_hierarchy_privs.emplace_back(column_priv);
break;
default:
break;
}
// Privilege with higher scope on the hierarchy can imply privileges
// with lower scope on the hierarchy.
for (const auto& privilege : higher_hierarchy_privs) {
ASSERT_OK(CreateRoleAndAddToGroups());
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
ASSERT_OK(sentry_authz_provider_->Authorize(scope, SentryAction::Action::ALL,
Substitute("$0.$1", db, tbl), kTestUser));
ASSERT_OK(DropRole());
}
// Privilege with lower scope on the hierarchy cannot imply privileges
// with higher scope on the hierarchy.
for (const auto& privilege : lower_hierarchy_privs) {
ASSERT_OK(CreateRoleAndAddToGroups());
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
Status s = sentry_authz_provider_->Authorize(scope, SentryAction::Action::ALL,
Substitute("$0.$1", db, tbl), kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
ASSERT_OK(DropRole());
}
}
INSTANTIATE_TEST_CASE_P(AuthzCombinations, TestAuthzHierarchy,
// Scope::COLUMN is excluded since column scope for table
// authorizable doesn't make sense.
::testing::Values(SentryAuthorizableScope::Scope::SERVER,
SentryAuthorizableScope::Scope::DATABASE,
SentryAuthorizableScope::Scope::TABLE));
// Test to verify the functionality of metrics in HA Sentry client used in
// SentryAuthzProvider to communicate with Sentry.
class TestSentryClientMetrics : public SentryAuthzProviderTest {
public:
bool CachingEnabled() const override {
// For simplicity, scenarios of this test doesn't use caching. The scenarios
// track updates of HaClient metrics upon issuing RPCs to Sentry.
return false;
}
};
TEST_F(TestSentryClientMetrics, Basic) {
ASSERT_EQ(0, GetTasksSuccessful());
ASSERT_EQ(0, GetTasksFailedFatal());
ASSERT_EQ(0, GetTasksFailedNonFatal());
ASSERT_EQ(0, GetReconnectionsSucceeded());
ASSERT_EQ(0, GetReconnectionsFailed());
Status s = sentry_authz_provider_->AuthorizeCreateTable("db.table",
kTestUser, kTestUser);
// The call should be counted as successful, and the client should connect
// to Sentry (counted as reconnect).
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
ASSERT_EQ(1, GetTasksSuccessful());
ASSERT_EQ(0, GetTasksFailedFatal());
ASSERT_EQ(0, GetTasksFailedNonFatal());
ASSERT_EQ(1, GetReconnectionsSucceeded());
ASSERT_EQ(0, GetReconnectionsFailed());
// Stop Sentry, and try the same call again. There should be a fatal error
// reported, and then there should be failed reconnection attempts.
ASSERT_OK(StopSentry());
s = sentry_authz_provider_->AuthorizeCreateTable("db.table",
kTestUser, kTestUser);
ASSERT_TRUE(s.IsNetworkError()) << s.ToString();
ASSERT_EQ(1, GetTasksSuccessful());
ASSERT_EQ(1, GetTasksFailedFatal());
ASSERT_EQ(0, GetTasksFailedNonFatal());
ASSERT_LE(1, GetReconnectionsFailed());
ASSERT_EQ(1, GetReconnectionsSucceeded());
ASSERT_OK(StartSentry());
s = sentry_authz_provider_->AuthorizeCreateTable("db.table",
kTestUser, kTestUser);
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
ASSERT_EQ(2, GetTasksSuccessful());
ASSERT_EQ(1, GetTasksFailedFatal());
ASSERT_EQ(0, GetTasksFailedNonFatal());
ASSERT_EQ(2, GetReconnectionsSucceeded());
// NotAuthorized() from Sentry itself considered as a fatal error.
// TODO(KUDU-2769): clarify whether it is a bug in HaClient or Sentry itself?
s = sentry_authz_provider_->AuthorizeCreateTable("db.table",
"nobody", "nobody");
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
ASSERT_EQ(2, GetTasksSuccessful());
ASSERT_EQ(2, GetTasksFailedFatal());
ASSERT_EQ(0, GetTasksFailedNonFatal());
// Once a new fatal error is registered, the client should reconnect.
ASSERT_EQ(3, GetReconnectionsSucceeded());
// Shorten the default timeout parameters: make timeout interval shorter.
NO_FATALS(sentry_authz_provider_->Stop());
FLAGS_sentry_service_rpc_addresses = sentry_->address().ToString();
FLAGS_sentry_service_send_timeout_seconds = 2;
FLAGS_sentry_service_recv_timeout_seconds = 2;
sentry_authz_provider_.reset(new SentryAuthzProvider(metric_entity_));
ASSERT_OK(sentry_authz_provider_->Start());
ASSERT_OK(CreateRoleAndAddToGroups());
const auto privilege = GetDatabasePrivilege("db", "create");
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
// Pause Sentry and try to send an RPC, expecting it to time out.
ASSERT_OK(sentry_->Pause());
s = sentry_authz_provider_->AuthorizeCreateTable(
"db.table", kTestUser, kTestUser);
ASSERT_TRUE(s.IsTimedOut());
ASSERT_OK(sentry_->Resume());
scoped_refptr<Histogram> hist(metric_entity_->FindOrCreateHistogram(
&METRIC_sentry_client_task_execution_time_us));
ASSERT_LT(0, hist->histogram()->MinValue());
// Change the threshold to 1900000 in case of very unstable system clock
// and other scheduler anomalies of the OS scheduler.
ASSERT_LT(1900000, hist->histogram()->MaxValue());
ASSERT_LE(5, hist->histogram()->TotalCount());
ASSERT_LT(1900000, hist->histogram()->TotalSum());
}
enum class ThreadsNumPolicy {
CloseToCPUsNum,
MoreThanCPUsNum,
};
// Test to ensure concurrent requests to Sentry with the same set of parameters
// are accumulated by SentryAuthzProvider, so in total there is less RPC
// requests sent to Sentry than the total number of concurrent requests to
// the provider (ideally, there should be just single request to Sentry).
class TestConcurrentRequests :
public SentryAuthzProviderTest,
public ::testing::WithParamInterface<std::tuple<ThreadsNumPolicy,
AuthzCaching>> {
public:
bool CachingEnabled() const override {
return std::get<1>(GetParam()) == AuthzCaching::Enabled;
}
};
// Verify how multiple concurrent requests are handled when Sentry responds
// with success.
TEST_P(TestConcurrentRequests, SuccessResponses) {
const auto kNumRequestThreads =
std::get<0>(GetParam()) == ThreadsNumPolicy::CloseToCPUsNum
? std::min(base::NumCPUs(), 4) : base::NumCPUs() * 3;
ASSERT_OK(CreateRoleAndAddToGroups());
const auto privilege = GetDatabasePrivilege("db", "METADATA");
ASSERT_OK(AlterRoleGrantPrivilege(privilege));
Barrier barrier(kNumRequestThreads);
vector<thread> threads;
vector<Status> thread_status(kNumRequestThreads);
for (auto i = 0; i < kNumRequestThreads; ++i) {
const auto thread_idx = i;
threads.emplace_back([&, thread_idx] () {
barrier.Wait();
thread_status[thread_idx] = sentry_authz_provider_->
AuthorizeGetTableMetadata("db.table", kTestUser);
});
}
for (auto& thread : threads) {
thread.join();
}
for (const auto& s : thread_status) {
ASSERT_TRUE(s.ok()) << s.ToString();
}
const auto sentry_rpcs_num = GetTasksSuccessful();
// Ideally all requests should result in a single RPC sent to Sentry, but some
// scheduling anomalies might occur so even the current threshold of maximum
// (kNumRequestThreads / 2) of actual RPC requests to Sentry might be reached
// and the assertion below would be triggered. For example, the OS scheduler
// might de-schedule the majority of the threads spawned above for a time
// longer than it takes to complete an RPC to Sentry, and that de-scheduling
// might happen exactly prior the point when the 'earlier-running' thread
// added itself into a queue designed to track concurrent requests.
// Essentially, that's about 'freezing' all incoming requests just before the
// queueing point, and then awakening them one by one, so no more than one
// thread is registered in the queue at any time.
//
// However, those anomalies are expected to be exceptionally rare. In fact,
// (kNumRequestThreads / 2) seems to be a good enough threshold even for TSAN
// builds while running the test scenario with --stress_cpu_threads=16.
ASSERT_GE(kNumRequestThreads / 2, sentry_rpcs_num);
// Issue the same request once more. If caching is enabled, there should be
// no additional RPCs sent to Sentry.
ASSERT_OK(sentry_authz_provider_->AuthorizeGetTableMetadata(
"db.table", kTestUser));
ASSERT_EQ(CachingEnabled() ? sentry_rpcs_num : sentry_rpcs_num + 1,
GetTasksSuccessful());
}
// Verify how multiple concurrent requests are handled when Sentry responds
// with errors.
TEST_P(TestConcurrentRequests, FailureResponses) {
const auto kNumRequestThreads =
std::get<0>(GetParam()) == ThreadsNumPolicy::CloseToCPUsNum
? std::min(base::NumCPUs(), 4) : base::NumCPUs() * 3;
Barrier barrier(kNumRequestThreads);
vector<thread> threads;
vector<Status> thread_status(kNumRequestThreads);
for (auto i = 0; i < kNumRequestThreads; ++i) {
const auto thread_idx = i;
threads.emplace_back([&, thread_idx] () {
barrier.Wait();
thread_status[thread_idx] = sentry_authz_provider_->
AuthorizeCreateTable("db.table", "nobody", "nobody");
});
}
for (auto& thread : threads) {
thread.join();
}
for (const auto& s : thread_status) {
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
}
ASSERT_EQ(0, GetTasksSuccessful());
ASSERT_EQ(0, GetTasksFailedNonFatal());
const auto sentry_rpcs_num = GetTasksFailedFatal();
// See the TestConcurrentRequests.SuccessResponses scenario above for details
// on setting the threshold for 'sentry_rpcs_num'.
ASSERT_GE(kNumRequestThreads / 2, sentry_rpcs_num);
// The cache does not store negative responses/errors, so in both caching and
// non-caching case there should be one extra RPC sent to Sentry.
auto s = sentry_authz_provider_->AuthorizeCreateTable(
"db.table", "nobody", "nobody");
ASSERT_TRUE(s.IsNotAuthorized()) << s.ToString();
ASSERT_EQ(sentry_rpcs_num + 1, GetTasksFailedFatal());
}
INSTANTIATE_TEST_CASE_P(QueueingConcurrentRequests, TestConcurrentRequests,
::testing::Combine(::testing::Values(ThreadsNumPolicy::CloseToCPUsNum,
ThreadsNumPolicy::MoreThanCPUsNum),
::testing::Values(AuthzCaching::Disabled,
AuthzCaching::Enabled)));
} // namespace master
} // namespace kudu