blob: e8ce7232c83464451bcc6ed9cc3c2580a76179b2 [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/ranger/mini_ranger.h"
#include <csignal>
#include <ostream>
#include <string>
#include <vector>
#include <glog/logging.h>
#include "kudu/gutil/map-util.h"
#include "kudu/gutil/strings/join.h"
#include "kudu/gutil/strings/substitute.h"
#include "kudu/postgres/mini_postgres.h"
#include "kudu/ranger/mini_ranger_configs.h"
#include "kudu/ranger/ranger.pb.h"
#include "kudu/util/curl_util.h"
#include "kudu/util/easy_json.h"
#include "kudu/util/env.h"
#include "kudu/util/faststring.h"
#include "kudu/util/monotime.h"
#include "kudu/util/net/net_util.h"
#include "kudu/util/path_util.h"
#include "kudu/util/slice.h"
#include "kudu/util/stopwatch.h"
#include "kudu/util/subprocess.h"
#include "kudu/util/test_util.h"
using std::string;
using std::vector;
using strings::Substitute;
static constexpr int kRangerStartTimeoutMs = 90000;
namespace kudu {
namespace ranger {
Status MiniRanger::Start() {
if (!mini_pg_->IsStarted()) {
RETURN_NOT_OK_PREPEND(mini_pg_->Start(), "Failed to start Postgres");
}
return StartRanger();
}
MiniRanger::~MiniRanger() {
WARN_NOT_OK(Stop(), "Failed to stop Ranger");
}
Status MiniRanger::Stop() {
if (process_) {
LOG(INFO) << "Stopping Ranger...";
RETURN_NOT_OK(process_->KillAndWait(SIGTERM));
LOG(INFO) << "Stopped Ranger";
process_.reset();
}
return mini_pg_->Stop();
}
Status MiniRanger::InitRanger(const string& admin_home, bool* fresh_install) {
if (env_->FileExists(admin_home)) {
*fresh_install = false;
return Status::OK();
}
*fresh_install = true;
RETURN_NOT_OK(env_->CreateDir(admin_home));
RETURN_NOT_OK(mini_pg_->AddUser("miniranger", /*super=*/ false));
LOG(INFO) << "Created miniranger Postgres user";
RETURN_NOT_OK(mini_pg_->CreateDb("ranger", "miniranger"));
LOG(INFO) << "Created ranger Postgres database";
return Status::OK();
}
Status MiniRanger::CreateConfigs() {
// Ranger listens on 2 ports:
//
// - port_ is the RPC port (REST API) that the Ranger subprocess and
// EasyCurl can talk to
// - ranger_shutdown_port is the port which Ranger listens on for a shutdown
// command. We're not using this shutdown port as we simply send a SIGTERM,
// but it's necessary to set it to a random value to avoid collisions in
// parallel testing.
if (port_ == 0) {
RETURN_NOT_OK(GetRandomPort(host_, &port_));
}
uint16_t ranger_shutdown_port;
RETURN_NOT_OK(GetRandomPort(host_, &ranger_shutdown_port));
string admin_home = ranger_admin_home();
ranger_admin_url_ = Substitute("http://$0:$1", host_, port_);
// Write config files
RETURN_NOT_OK(WriteStringToFile(
env_, GetRangerInstallProperties(bin_dir(), host_, mini_pg_->bound_port()),
JoinPathSegments(admin_home, "install.properties")));
RETURN_NOT_OK(WriteStringToFile(
env_, GetRangerAdminSiteXml(host_, port_, host_, mini_pg_->bound_port(),
admin_ktpath_, lookup_ktpath_,
spnego_ktpath_),
JoinPathSegments(admin_home, "ranger-admin-site.xml")));
RETURN_NOT_OK(WriteStringToFile(
env_, GetRangerAdminDefaultSiteXml(
JoinPathSegments(bin_dir(), "postgresql.jar"),
ranger_shutdown_port),
JoinPathSegments(admin_home, "ranger-admin-default-site.xml")));
RETURN_NOT_OK(WriteStringToFile(env_, GetRangerCoreSiteXml(kerberos_),
JoinPathSegments(admin_home, "core-site.xml")));
RETURN_NOT_OK(WriteStringToFile(env_, GetRangerLog4jProperties("info"),
JoinPathSegments(admin_home, "log4j.properties")));
return Status::OK();
}
Status MiniRanger::DbSetup(const string& admin_home, const string& ews_dir,
const string& web_app_dir) {
RETURN_NOT_OK(env_->CreateDir(ews_dir));
RETURN_NOT_OK(env_->CreateSymLink(JoinPathSegments(ranger_home_, "ews/webapp").c_str(),
web_app_dir.c_str()));
// Much of this encapsulates setup.sh from apache/ranger[1], excluding some of
// the system-level configs.
//
// [1] https://github.com/apache/ranger/blob/master/security-admin/scripts/setup.sh
//
// TODO(abukor): load a db dump instead as this is very slow
Subprocess db_setup(
{ "python", JoinPathSegments(ranger_home_, "db_setup.py")});
db_setup.SetEnvVars({
{ "JAVA_HOME", java_home_ },
{ "RANGER_ADMIN_HOME", ranger_home_ },
{ "RANGER_ADMIN_CONF", admin_home },
{ "XAPOLICYMGR_DIR", admin_home },
{ "RANGER_PID_DIR_PATH", admin_home },
});
db_setup.SetCurrentDir(admin_home);
RETURN_NOT_OK(db_setup.Start());
return db_setup.WaitAndCheckExitCode();
}
Status MiniRanger::StartRanger() {
bool fresh_install;
if (!mini_pg_->IsStarted()) {
return Status::IllegalState("Postgres is not running");
}
LOG_TIMING(INFO, "starting Ranger") {
LOG(INFO) << "Starting Ranger...";
string exe;
RETURN_NOT_OK(env_->GetExecutablePath(&exe));
const string bin_dir = DirName(exe);
RETURN_NOT_OK(FindHomeDir("hadoop", bin_dir, &hadoop_home_));
RETURN_NOT_OK(FindHomeDir("ranger", bin_dir, &ranger_home_));
RETURN_NOT_OK(FindHomeDir("java", bin_dir, &java_home_));
const string kAdminHome = ranger_admin_home();
const string kEwsDir = JoinPathSegments(kAdminHome, "ews");
const string kWebAppDir = JoinPathSegments(kEwsDir, "webapp");
const string kWebInfDir = JoinPathSegments(kWebAppDir, "WEB-INF");
const string kClassesDir = JoinPathSegments(kWebInfDir, "classes");
const string kConfDir = JoinPathSegments(kClassesDir, "conf");
RETURN_NOT_OK(InitRanger(kAdminHome, &fresh_install));
LOG(INFO) << "Starting Ranger out of " << kAdminHome;
RETURN_NOT_OK(CreateConfigs());
if (fresh_install) {
RETURN_NOT_OK(DbSetup(kAdminHome, kEwsDir, kWebAppDir));
}
// Encapsulates ranger-admin-services.sh[1]
//
// 1. https://github.com/apache/ranger/blob/f37f5407eee8d2627a4306a25938b151f8e2ba31/embeddedwebserver/scripts/ranger-admin-services.sh
string classpath = ranger_classpath();
LOG(INFO) << "Using Ranger class path: " << classpath;
LOG(INFO) << "Using host: " << host_;
std::vector<string> args({
JoinPathSegments(java_home_, "bin/java"),
"-Dproc_rangeradmin",
Substitute("-Dhostname=$0", host_),
Substitute("-Dlog4j.configuration=file:$0",
JoinPathSegments(kAdminHome, "log4j.properties")),
"-Duser=miniranger",
Substitute("-Dranger.service.host=$0", host_),
"-Dservername=miniranger",
Substitute("-Dcatalina.base=$0", kEwsDir),
Substitute("-Dlogdir=$0", JoinPathSegments(kAdminHome, "logs")),
"-Dranger.audit.solr.bootstrap.enabled=false",
});
if (kerberos_) {
args.emplace_back(Substitute("-Djava.security.krb5.conf=$0", krb5_config_));
}
args.emplace_back("-cp");
args.emplace_back(classpath);
args.emplace_back("org.apache.ranger.server.tomcat.EmbeddedServer");
process_.reset(new Subprocess(args));
process_->SetEnvVars({
{ "XAPOLICYMGR_DIR", kAdminHome },
{ "XAPOLICYMGR_EWS_DIR", kEwsDir },
{ "RANGER_JAAS_LIB_DIR", JoinPathSegments(kWebAppDir, "ranger_jaas") },
{ "RANGER_JAAS_CONF_DIR", JoinPathSegments(kConfDir, "ranger_jaas") },
{ "JAVA_HOME", java_home_ },
{ "RANGER_PID_DIR_PATH", JoinPathSegments(data_root_, "tmppid") },
{ "RANGER_ADMIN_PID_NAME", "rangeradmin.pid" },
{ "RANGER_ADMIN_CONF_DIR", kAdminHome },
{ "RANGER_USER", "miniranger" },
});
RETURN_NOT_OK(process_->Start());
uint16_t port;
RETURN_NOT_OK(WaitForTcpBind(process_->pid(),
&port,
{ "0.0.0.0", "127.0.0.1", },
MonoDelta::FromMilliseconds(kRangerStartTimeoutMs)));
LOG(INFO) << "Ranger bound to " << port;
LOG(INFO) << "Ranger admin URL: " << ranger_admin_url_;
}
if (fresh_install) {
RETURN_NOT_OK(CreateKuduService());
}
return Status::OK();
}
Status MiniRanger::CreateKuduService() {
EasyJson service;
service.Set("name", "kudu");
service.Set("type", "kudu");
// The below config authorizes "kudu" to download the list of authorized users
// for policies and tags respectively.
EasyJson configs = service.Set("configs", EasyJson::kObject);
configs.Set("policy.download.auth.users", "kudu");
configs.Set("tag.download.auth.users", "kudu");
RETURN_NOT_OK_PREPEND(PostToRanger("service/plugins/services", service),
"Failed to create Kudu service");
LOG(INFO) << "Created Kudu service";
return Status::OK();
}
Status MiniRanger::AddPolicy(AuthorizationPolicy policy) {
string policy_name = JoinStrings<vector<string>>({JoinStrings(policy.databases, ","),
JoinStrings(policy.tables, ","),
JoinStrings(policy.columns, ",")}, ";");
PolicyKey policy_key(policy.databases, policy.tables, policy.columns);
vector<PolicyItem>* items = FindOrNull(policies_, policy_key);
if (!items) {
policies_.emplace(policy_key, policy.items);
items = &policy.items;
} else {
for (const auto& item : policy.items) {
items->emplace_back(item);
}
}
EasyJson policy_json;
policy_json.Set("service", "kudu");
policy_json.Set("name", policy_name);
policy_json.Set("isEnabled", true);
EasyJson resources = policy_json.Set("resources", EasyJson::kObject);
if (!policy.databases.empty()) {
EasyJson databases = resources.Set("database", EasyJson::kObject);
EasyJson database_values = databases.Set("values", EasyJson::kArray);
for (const string& database : policy.databases) {
database_values.PushBack(database);
}
}
if (!policy.tables.empty()) {
EasyJson tables = resources.Set("table", EasyJson::kObject);
EasyJson table_values = tables.Set("values", EasyJson::kArray);
for (const string& table : policy.tables) {
table_values.PushBack(table);
}
}
if (!policy.columns.empty()) {
EasyJson columns = resources.Set("column", EasyJson::kArray);
EasyJson column_values = columns.Set("values", EasyJson::kArray);
for (const string& column : policy.columns) {
column_values.PushBack(column);
}
}
EasyJson policy_items = policy_json.Set("policyItems", EasyJson::kArray);
for (const auto& policy_item : *items) {
EasyJson item = policy_items.PushBack(EasyJson::kObject);
EasyJson users = item.Set("users", EasyJson::kArray);
for (const string& user : std::get<0>(policy_item)) {
users.PushBack(user);
}
item.Set("delegateAdmin", std::get<2>(policy_item));
EasyJson accesses = item.Set("accesses", EasyJson::kArray);
for (const ActionPB& action : std::get<1>(policy_item)) {
EasyJson access = accesses.PushBack(EasyJson::kObject);
access.Set("type", ActionPB_Name(action));
access.Set("isAllowed", true);
}
}
RETURN_NOT_OK_PREPEND(PostToRanger("service/plugins/policies?deleteIfExists=true",
policy_json), "Failed to add policy");
return Status::OK();
}
Status MiniRanger::PostToRanger(const string& url, const EasyJson& payload, bool secure) {
EasyCurl curl;
if (secure) {
curl.set_auth(CurlAuthType::SPNEGO);
} else {
curl.set_auth(CurlAuthType::BASIC, "admin", "admin");
}
faststring result;
RETURN_NOT_OK_PREPEND(curl.PostToURL(JoinPathSegments(ranger_admin_url_, url),
payload.ToString(), &result,
{"Content-Type: application/json"}),
Substitute("Error received from Ranger: $0", result.ToString()));
return Status::OK();
}
Status MiniRanger::CreateClientConfig(const string& client_config_dir) {
auto policy_cache = JoinPathSegments(client_config_dir, "policy-cache");
if (!env_->FileExists(client_config_dir)) {
RETURN_NOT_OK(env_->CreateDir(client_config_dir));
RETURN_NOT_OK(env_->CreateDir(policy_cache));
}
RETURN_NOT_OK(WriteStringToFile(env_, GetRangerCoreSiteXml(kerberos_),
JoinPathSegments(client_config_dir, "core-site.xml")));
RETURN_NOT_OK(WriteStringToFile(env_, GetRangerKuduSecurityXml(policy_cache, "kudu",
ranger_admin_url_,
policy_poll_interval_ms_),
JoinPathSegments(client_config_dir,
"ranger-kudu-security.xml")));
return Status::OK();
}
} // namespace ranger
} // namespace kudu