blob: cb3bbe1d702e620ac544304dbafc391d09ff2647 [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/sentry/mini_sentry.h"
#include <algorithm>
#include <csignal>
#include <map>
#include <memory>
#include <ostream>
#include <string>
#include <glog/logging.h>
#include "kudu/gutil/strings/substitute.h"
#include "kudu/util/env.h"
#include "kudu/util/monotime.h"
#include "kudu/util/path_util.h"
#include "kudu/util/slice.h"
#include "kudu/util/status.h"
#include "kudu/util/stopwatch.h"
#include "kudu/util/subprocess.h"
#include "kudu/util/test_util.h"
using std::map;
using std::string;
using std::unique_ptr;
using strings::Substitute;
static constexpr int kSentryStartTimeoutMs = 120000;
namespace kudu {
namespace sentry {
MiniSentry::MiniSentry() {
}
MiniSentry::~MiniSentry() {
WARN_NOT_OK(Stop(), "Failed to stop MiniSentry");
}
void MiniSentry::EnableKerberos(string krb5_conf,
string service_principal,
string keytab_file) {
CHECK(!sentry_process_);
CHECK(!krb5_conf.empty());
CHECK(!service_principal.empty());
CHECK(!keytab_file.empty());
krb5_conf_ = std::move(krb5_conf);
service_principal_ = std::move(service_principal);
keytab_file_ = std::move(keytab_file);
}
void MiniSentry::EnableHms(string hms_uris) {
CHECK(!sentry_process_);
hms_uris_ = std::move(hms_uris);
}
void MiniSentry::SetDataRoot(string data_root) {
CHECK(!sentry_process_);
data_root_ = std::move(data_root);
}
void MiniSentry::SetAddress(const HostPort& address) {
CHECK(!sentry_process_);
ip_ = address.host();
port_ = address.port();
}
Status MiniSentry::Start() {
SCOPED_LOG_TIMING(INFO, "Starting Sentry");
CHECK(!sentry_process_);
VLOG(1) << "Starting Sentry";
Env* env = Env::Default();
string exe;
RETURN_NOT_OK(env->GetExecutablePath(&exe));
const string bin_dir = DirName(exe);
string hadoop_home;
string sentry_home;
string java_home;
RETURN_NOT_OK(FindHomeDir("hadoop", bin_dir, &hadoop_home));
RETURN_NOT_OK(FindHomeDir("sentry", bin_dir, &sentry_home));
RETURN_NOT_OK(FindHomeDir("java", bin_dir, &java_home));
if (data_root_.empty()) {
data_root_ = GetTestDataDirectory();
}
RETURN_NOT_OK(CreateSentryConfigs(data_root_));
// List of JVM environment options to pass to the Sentry service.
string java_options;
if (!krb5_conf_.empty()) {
java_options += Substitute(" -Djava.security.krb5.conf=$0", krb5_conf_);
}
if (IsHmsEnabled()) {
java_options += Substitute(" -Dhive.metastore.uris=$0"
" -Dhive.metastore.sasl.enabled=$1"
" -Dhive.metastore.kerberos.principal=hive/127.0.0.1@KRBTEST.COM",
hms_uris_, IsKerberosEnabled());
}
const map<string, string> env_vars {
{ "JAVA_HOME", java_home },
{ "HADOOP_HOME", hadoop_home },
{ "JAVA_TOOL_OPTIONS", java_options },
};
// Start Sentry.
sentry_process_.reset(new Subprocess({
Substitute("$0/bin/sentry", sentry_home),
"--log4jConf", JoinPathSegments(data_root_, "log4j.properties"),
"--command", "service",
"--conffile", JoinPathSegments(data_root_, "sentry-site.xml"),
}));
sentry_process_->SetEnvVars(env_vars);
RETURN_NOT_OK(sentry_process_->Start());
// Wait for Sentry to start listening on its ports and commencing operation.
VLOG(1) << "Waiting for Sentry ports";
uint16_t orig_port = port_;
Status wait = WaitForTcpBind(sentry_process_->pid(), &port_, ip_,
MonoDelta::FromMilliseconds(kSentryStartTimeoutMs));
// Check that the port number only changed if the original port was 0
// (i.e. if we asked to bind to an ephemeral port)
CHECK(orig_port == 0 || port_ == orig_port);
if (!wait.ok()) {
WARN_NOT_OK(sentry_process_->Kill(SIGQUIT), "failed to send SIGQUIT to Sentry");
}
return wait;
}
Status MiniSentry::Stop() {
if (sentry_process_) {
VLOG(1) << "Stopping Sentry";
unique_ptr<Subprocess> proc = std::move(sentry_process_);
RETURN_NOT_OK_PREPEND(proc->KillAndWait(SIGTERM), "failed to stop the Sentry service");
}
return Status::OK();
}
Status MiniSentry::Pause() {
CHECK(sentry_process_);
VLOG(1) << "Pausing Sentry";
RETURN_NOT_OK_PREPEND(sentry_process_->Kill(SIGSTOP),
"failed to pause the Sentry service");
return Status::OK();
}
Status MiniSentry::Resume() {
CHECK(sentry_process_);
VLOG(1) << "Resuming Sentry";
RETURN_NOT_OK_PREPEND(sentry_process_->Kill(SIGCONT),
"failed to unpause the Sentry service");
return Status::OK();
}
Status MiniSentry::CreateSentryConfigs(const string& tmp_dir) const {
// - sentry.store.jdbc.url
// - sentry.store.jdbc.password
// Configures Sentry to use a local in-process Derby instance with a dummy
// password value.
//
// - datanucleus.schema.autoCreateAll
// - sentry.verify.schema.version
// Allow Sentry to startup and run without first running the schemaTool.
//
// - sentry.store.group.mapping
// sentry.store.group.mapping.resource
// Production Sentry instances use Hadoop's UGI infrastructure to map users
// to groups, but that's difficult to mock for tests, so we configure a
// simpler static user/group mapping based on a generated INI file.
//
// - sentry.service.admin.group
// Set up admin groups which have unrestricted privileges in Sentry.
//
// - sentry.service.allow.connect
// Set of Kerberos principals which is allowed to connect to Sentry when
// the Kerberos security mode is enabled.
//
// - sentry.service.server.rpc-port
// Port number that the Sentry service starts with.
//
// - sentry.service.server.rpc-address
// IP address that the Sentry service starts with.
//
// - sentry.db.policy.store.owner.as.privilege
// Configures Sentry to enable owner privileges feature which automatically
// derives OWNER/ALL privileges from object's ownership. 'all' indicates an
// object owner has OWNER/ALL privilege on the object, but cannot transfer
// owner privileges to another user or role.
//
// - sentry.store.clean.period.seconds
// The interval to run the "store-cleaner" Sentry's thread. Setting to a
// negative value means Sentry will not run the "store-cleaner" thread
// at all and that allows for faster start-up times of the Sentry service.
//
// - hive.sentry.server
// Server namespace the HMS instance belongs to for defining server-level
// privileges in Sentry. Sentry uses it to synchronize privileges upon
// receipt of HMS events (such as table rename). Must match with Kudu
// master's flag 'server_name'.
static const string kFileTemplate = R"(
<configuration>
<property>
<name>sentry.service.security.mode</name>
<value>$0</value>
</property>
<property>
<name>sentry.service.server.principal</name>
<value>$1</value>
</property>
<property>
<name>sentry.service.server.keytab</name>
<value>$2</value>
</property>
<property>
<name>sentry.store.jdbc.url</name>
<value>jdbc:derby:$3/sentry;create=true</value>
</property>
<property>
<name>sentry.store.jdbc.password</name>
<value>_</value>
</property>
<property>
<name>datanucleus.schema.autoCreateAll</name>
<value>true</value>
</property>
<property>
<name>sentry.verify.schema.version</name>
<value>false</value>
</property>
<property>
<name>sentry.store.group.mapping</name>
<value>org.apache.sentry.provider.file.LocalGroupMappingService</value>
</property>
<property>
<name>sentry.store.group.mapping.resource</name>
<value>$4</value>
</property>
<property>
<name>sentry.service.server.rpc-port</name>
<value>$5</value>
</property>
<property>
<name>sentry.service.server.rpc-address</name>
<value>$6</value>
</property>
<property>
<name>sentry.service.admin.group</name>
<value>admin</value>
</property>
<property>
<name>sentry.service.allow.connect</name>
<value>kudu,hive</value>
</property>
<property>
<name>sentry.db.policy.store.owner.as.privilege</name>
<value>all</value>
</property>
<property>
<name>sentry.store.clean.period.seconds</name>
<value>-1</value>
</property>
<property>
<name>hive.sentry.server</name>
<value>server1</value>
</property>
</configuration>
)";
string users_ini_path = JoinPathSegments(tmp_dir, "users.ini");
string file_contents = Substitute(
kFileTemplate,
IsKerberosEnabled() ? "kerberos" : "none",
service_principal_,
keytab_file_,
tmp_dir,
users_ini_path,
port_,
ip_);
RETURN_NOT_OK(WriteStringToFile(Env::Default(),
file_contents,
JoinPathSegments(tmp_dir, "sentry-site.xml")));
// Simple file format containing mapping of user to groups in INI syntax, see
// the LocalGroupMappingService class for more information.
static const string kUsers = R"(
[users]
test-admin=admin
test-user=user
kudu=admin
joe-interloper=""
user0=group0
user1=group1
user2=group2
)";
RETURN_NOT_OK(WriteStringToFile(Env::Default(), kUsers, users_ini_path));
// Configure the Sentry service to output WARN messages to the stderr
// console, and INFO and above to sentry.log in the data root. The console
// messages have a special 'SENTRY' tag included to disambiguate them from other
// Java component logs.
static const string kLogPropertiesTemplate = R"(
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern = %d{HH:mm:ss.SSS} [SENTRY - %p - %t] (%F:%L) %m%n
log4j.appender.console.Threshold = WARN
log4j.appender.file = org.apache.log4j.FileAppender
log4j.appender.file.File = $0
log4j.appender.file.layout = org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern = %d{HH:mm:ss.SSS} [%p - %t] (%F:%L) %m%n
log4j.rootLogger = INFO, console, file
)";
string log_properties = Substitute(
kLogPropertiesTemplate,
JoinPathSegments(tmp_dir, "sentry.log"));
RETURN_NOT_OK(WriteStringToFile(Env::Default(),
log_properties,
JoinPathSegments(tmp_dir, "log4j.properties")));
return Status::OK();
}
} // namespace sentry
} // namespace kudu