blob: f9df0adc5dff89ca9a61b60d0c0a75bf0e97c09d [file] [log] [blame]
// Copyright 2016 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "authpolicy/samba_interface.h"
#include <map>
#include <string>
#include <utility>
#include <vector>
#include <base/files/file.h>
#include <base/files/file_util.h>
#include <base/memory/ptr_util.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include "authpolicy/constants.h"
#include "authpolicy/process_executor.h"
#include "authpolicy/samba_interface_internal.h"
#include "bindings/authpolicy_containers.pb.h"
namespace ac = authpolicy::constants;
namespace ai = authpolicy::internal;
namespace ap = authpolicy::protos;
namespace authpolicy {
namespace {
// Must match Chromium AccountId::kKeyAdIdPrefix.
const char kActiveDirectoryPrefix[] = "a-";
// Temporary data. Note: Use a #define, so we can concat strings below.
#define AUTHPOLICY_TMP_DIR "/tmp/authpolicyd"
// Persisted samba data.
#define STATE_DIR "/var/lib/authpolicyd"
// Temporary samba data.
#define SAMBA_TMP_DIR AUTHPOLICY_TMP_DIR "/samba"
// Kerberos configuration file.
#define KRB5_FILE_PATH AUTHPOLICY_TMP_DIR "/krb5.conf"
// Temp machine keytab file.
#define MACHINE_KT_TMP_FILE_PATH SAMBA_TMP_DIR "/krb5_machine.keytab"
// Persistent machine keytab file.
#define MACHINE_KT_STATE_FILE_PATH STATE_DIR "/krb5_machine.keytab"
// Samba configuration file data.
const char kSmbConfData[] =
"[global]\n"
"\tnetbios name = %s\n"
"\tsecurity = ADS\n"
"\tworkgroup = %s\n"
"\trealm = %s\n"
"\tlock directory = " SAMBA_TMP_DIR "/lock\n"
"\tcache directory = " SAMBA_TMP_DIR "/cache\n"
"\tstate directory = " SAMBA_TMP_DIR "/state\n"
"\tprivate directory = " SAMBA_TMP_DIR "/private\n"
"\tkerberos method = secrets and keytab\n"
"\tkerberos encryption types = strong\n"
"\tclient signing = mandatory\n"
"\tclient min protocol = SMB2\n"
// TODO(ljusten): Remove this line once crbug.com/662440 is resolved.
"\tclient max protocol = SMB3\n"
"\tclient ipc min protocol = SMB2\n"
"\tclient schannel = yes\n"
"\tclient ldap sasl wrapping = sign\n";
// Kerberos configuration file data.
const char kKrb5ConfData[] =
"[libdefaults]\n"
// Only allow AES. (No DES, no RC4.)
"\tdefault_tgs_enctypes = aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96\n"
"\tdefault_tkt_enctypes = aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96\n"
"\tpermitted_enctypes = aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96\n"
// Prune weak ciphers from the above list. With current settings it’s a
// no-op, but still.
"\tallow_weak_crypto = false\n"
// Default is 300 seconds, but we might add a policy for that in the future.
"\tclockskew = 300\n"
// Required for password change.
"\tdefault_realm = %s\n";
const int kFileMode_rwr = base::FILE_PERMISSION_READ_BY_USER |
base::FILE_PERMISSION_WRITE_BY_USER |
base::FILE_PERMISSION_READ_BY_GROUP;
const int kFileMode_rwxrx = kFileMode_rwr |
base::FILE_PERMISSION_EXECUTE_BY_USER |
base::FILE_PERMISSION_EXECUTE_BY_GROUP;
const int kFileMode_rwxrwx =
kFileMode_rwxrx | base::FILE_PERMISSION_WRITE_BY_GROUP;
// Directories to create (Samba fails to do it on its own). AUTHPOLICY_TMP_DIR
// needs group rx access to read smb.conf and krb5.conf and to access
// SAMBA_TMP_DIR, but no write access. SAMBA_TMP_DIR needs full group rwx access
// since samba reads and writes files there.
std::pair<const char*, int> kSambaDirsAndMode[] = {
{AUTHPOLICY_TMP_DIR, kFileMode_rwxrx },
{SAMBA_TMP_DIR, kFileMode_rwxrwx},
{SAMBA_TMP_DIR "/lock", kFileMode_rwxrwx},
{SAMBA_TMP_DIR "/cache", kFileMode_rwxrwx},
{SAMBA_TMP_DIR "/state", kFileMode_rwxrwx},
{SAMBA_TMP_DIR "/private", kFileMode_rwxrwx}};
// Location to download GPOs to.
const char kSambaTmpDir[] = SAMBA_TMP_DIR;
const char kGpoLocalDir[] = SAMBA_TMP_DIR "/cache/gpo_cache";
// Directory / filenames for user and device policy.
const char kPRegUserDir[] = "User";
const char kPRegDeviceDir[] = "Machine";
const char kPRegFileName[] = "Registry.pol";
// File paths.
const char kSmbFilePath[] = AUTHPOLICY_TMP_DIR "/smb.conf";
const char kKrb5FilePath[] = KRB5_FILE_PATH;
const char kMachineKtTmpFilePath[] = MACHINE_KT_TMP_FILE_PATH;
const char kMachineKtStateFilePath[] = MACHINE_KT_STATE_FILE_PATH;
const char kConfigFilePath[] = STATE_DIR "/config.dat";
// Flags. Write kFlag* strings to kFlagsFilePath to toggle flags.
const char kFlagsFilePath[] = "/etc/authpolicyd_flags";
const char kFlagDisableSeccomp[] = "disable_seccomp";
const char kFlagLogSeccomp[] = "log_seccomp";
static bool s_disable_seccomp_filters = false;
static bool s_log_seccomp_filters = false;
// Size limit when loading the config file (4 MB).
const size_t kConfigSizeLimit = 4 * 1024 * 1024;
// Env variable for krb5.conf file.
const char kKrb5ConfEnvKey[] = "KRB5_CONFIG";
const char kKrb5ConfEnvValue[] = "FILE:" KRB5_FILE_PATH;
// Env variable for machine keytab (machine password for getting device policy).
const char kMachineKTEnvKey[] = "KRB5_KTNAME";
const char kMachineKTEnvValueTmp[] = "FILE:" MACHINE_KT_TMP_FILE_PATH;
const char kMachineKTEnvValueState[] = "FILE:" MACHINE_KT_STATE_FILE_PATH;
// Executable paths. For security reasons use absolute file paths!
const char kNetPath[] = "/usr/bin/net";
const char kKInitPath[] = "/usr/bin/kinit";
const char kSmbClientPath[] = "/usr/bin/smbclient";
// Keys for interpreting kinit output.
const char kKeyBadUserName[] =
"Client not found in Kerberos database while getting initial credentials";
const char kKeyBadPassword[] =
"Preauthentication failed while getting initial credentials";
const char kKeyPasswordExpiredStdout[] =
"Password expired. You must change it now.";
const char kKeyPasswordExpiredStderr[] =
"Cannot read password while getting initial credentials";
const char kKeyCannotResolve[] =
"Cannot resolve network address for KDC in realm";
// Keys for interpreting net output.
const char kKeyJoinAccessDenied[] = "NT_STATUS_ACCESS_DENIED";
const char kKeyBadMachineName[] = "Improperly formed account name";
const char kKeyMachineNameTooLong[] = "Our netbios name can be at most";
const char kKeyUserHitJoinQuota[] =
"Insufficient quota exists to complete the operation";
const char kKeyJoinFailedToFindDC[] = "failed to find DC";
const char kKeyNoLogonServers[] = "No logon servers";
const char kKeyJoinLogonFailure[] = "Logon failure";
// Keys for interpreting smbclient output.
const char kKeyNetworkTimeout[] = "NT_STATUS_IO_TIMEOUT";
const char kKeyNoSuchFile[] = "NT_STATUS_NO_SUCH_FILE listing ";
#undef MACHINE_KT_STATE_FILE_PATH
#undef MACHINE_KT_TMP_FILE_PATH
#undef KRB5_FILE_PATH
#undef SAMBA_TMP_DIR
#undef STATE_DIR
#undef AUTHPOLICY_TMP_DIR
// Seccomp filters.
#define SECCOMP_DIR "/usr/share/policy/"
const char kParserSeccompFilter[] =
SECCOMP_DIR "authpolicy_parser-seccomp.policy";
const char kKInitSeccompFilter[] = SECCOMP_DIR "kinit-seccomp.policy";
const char kNetAdsSeccompFilter[] =
SECCOMP_DIR "net_ads-seccomp.policy";
const char kSmbClientSeccompFilter[] = SECCOMP_DIR "smbclient-seccomp.policy";
#undef SECCOMP_DIR
// Switches to the authpolicyd-exec user in the constructor and back to
// authpolicyd in the destructor.
class ScopedAuthpolicyExecSwitch {
public:
ScopedAuthpolicyExecSwitch() {
// Keep kAuthPolicydUid as saved uid, so we can switch back.
CHECK_EQ(0, setresuid(ac::kAuthPolicyExecUid, ac::kAuthPolicyExecUid,
ac::kAuthPolicydUid));
}
~ScopedAuthpolicyExecSwitch() {
// Keep kAuthPolicyExecUid as saved uid, so we can switch back.
CHECK_EQ(0, setresuid(ac::kAuthPolicydUid, ac::kAuthPolicydUid,
ac::kAuthPolicyExecUid));
}
};
bool SetupJailAndRun(ProcessExecutor* cmd, const char* seccomp_filter) {
// Limit the system calls that the process can do.
DCHECK(cmd);
if (!s_disable_seccomp_filters) {
if (s_log_seccomp_filters)
cmd->LogSeccompFilterFailures();
cmd->SetSeccompFilter(seccomp_filter);
}
// Required since we don't have the caps to wipe supplementary groups.
cmd->KeepSupplementaryGroups();
// Allows us to drop setgroups, setresgid and setresuid from seccomp filters.
cmd->SetNoNewPrivs();
// Execute as authpolicyd exec user. Don't use minijail to switch user. This
// would force us to run without preload library since saved uids are wiped by
// execve and the executed code wouldn't be able to switch user. Running with
// preload library has two main advantages:
// 1) Tighter seccomp filters, no need to allow execve and others.
// 2) Ability to log seccomp filter failures. Without this, it is hard to
// know which syscall has to be added to the filter policy file.
ScopedAuthpolicyExecSwitch switch_scope;
return cmd->Execute();
}
// Returns true if the string contains the given substring.
bool Contains(const std::string& str, const std::string& substr) {
return str.find(substr) != std::string::npos;
}
ErrorType GetKinitError(const ProcessExecutor& kinit_cmd) {
// Handle different error cases
const std::string& kinit_out = kinit_cmd.GetStdout();
const std::string& kinit_err = kinit_cmd.GetStderr();
if (Contains(kinit_err, kKeyBadUserName)) {
LOG(ERROR) << "kinit failed - bad user name";
return ERROR_BAD_USER_NAME;
}
if (Contains(kinit_err, kKeyBadPassword)) {
LOG(ERROR) << "kinit failed - bad password";
return ERROR_BAD_PASSWORD;
}
// Check both stderr and stdout here since any kinit error in the change-
// password-workflow would otherwise be interpreted as 'password expired'.
if (Contains(kinit_out, kKeyPasswordExpiredStdout) &&
Contains(kinit_err, kKeyPasswordExpiredStderr)) {
LOG(ERROR) << "kinit failed - password expired";
return ERROR_PASSWORD_EXPIRED;
}
if (Contains(kinit_err, kKeyCannotResolve)) {
LOG(ERROR) << "kinit failed - cannot resolve KDC realm";
return ERROR_NETWORK_PROBLEM;
}
LOG(ERROR) << "kinit failed with exit code " << kinit_cmd.GetExitCode();
return ERROR_KINIT_FAILED;
}
ErrorType GetNetError(const ProcessExecutor& executor,
const std::string& net_command) {
// Handle different error cases
const std::string& net_out = executor.GetStdout();
const std::string& net_err = executor.GetStderr();
const std::string error_msg("net ads " + net_command + " failed: ");
if (Contains(net_out, kKeyJoinFailedToFindDC) ||
Contains(net_err, kKeyNoLogonServers)) {
LOG(ERROR) << error_msg << "network failure";
return ERROR_NETWORK_PROBLEM;
}
if (Contains(net_out, kKeyJoinLogonFailure)) {
LOG(ERROR) << error_msg << "logon failure";
return ERROR_BAD_PASSWORD;
}
if (Contains(net_out, kKeyJoinAccessDenied)) {
LOG(ERROR) << error_msg << "user is not permitted to join the domain";
return ERROR_JOIN_ACCESS_DENIED;
}
if (Contains(net_out, kKeyBadMachineName)) {
LOG(ERROR) << error_msg << "incorrect machine name";
return ERROR_BAD_MACHINE_NAME;
}
if (Contains(net_out, kKeyMachineNameTooLong)) {
LOG(ERROR) << error_msg << "machine name is too long";
return ERROR_MACHINE_NAME_TOO_LONG;
}
if (Contains(net_out, kKeyUserHitJoinQuota)) {
LOG(ERROR) << error_msg << "user joined max number of machines";
return ERROR_USER_HIT_JOIN_QUOTA;
}
LOG(ERROR) << error_msg << "failed with exit code "
<< executor.GetExitCode();
return ERROR_NET_FAILED;
}
ErrorType GetSmbclientError(const ProcessExecutor& smb_client_cmd) {
const std::string& smb_client_out = smb_client_cmd.GetStdout();
if (Contains(smb_client_out, kKeyNetworkTimeout)) {
LOG(ERROR) << "smbclient failed - network failure";
return ERROR_NETWORK_PROBLEM;
}
LOG(ERROR) << "smbclient failed with exit code "
<< smb_client_cmd.GetExitCode();
return ERROR_SMBCLIENT_FAILED;
}
// Retrieves the name of the domain controller. If the full server name is
// 'server.realm', |domain_controller_name| is set to 'server'. Since the domain
// controller name is expected to change very rarely, this function earlies out
// and returns true if called with a non-empty |domain_controller_name|. The
// domain controller name is required for proper kerberized authentication.
bool UpdateDomainControllerName(std::string* domain_controller_name,
ErrorType* out_error) {
if (!domain_controller_name->empty())
return true;
authpolicy::ProcessExecutor net_cmd(
{kNetPath, "ads", "info", "-s", kSmbFilePath});
if (!SetupJailAndRun(&net_cmd, kNetAdsSeccompFilter)) {
*out_error = GetNetError(net_cmd, "info");
return false;
}
const std::string& net_out = net_cmd.GetStdout();
// Parse the output to find the domain controller name. Enclose in a sandbox
// for security considerations.
ProcessExecutor parse_cmd({ac::kParserPath, ac::kCmdParseDcName});
parse_cmd.SetInputString(net_out);
if (!SetupJailAndRun(&parse_cmd, kParserSeccompFilter)) {
LOG(ERROR) << "authpolicy_parser parse_dc_name failed with exit code "
<< parse_cmd.GetExitCode();
*out_error = ERROR_PARSE_FAILED;
return false;
}
*domain_controller_name = parse_cmd.GetStdout();
LOG(INFO) << "Found DC name = '" << *domain_controller_name << "'";
return true;
}
// Retrieves the name of the workgroup.
bool GetWorkgroup(const ap::SambaConfig* config, std::string* workgroup,
ErrorType* out_error) {
ProcessExecutor net_cmd(
{kNetPath, "ads", "workgroup", "-s", kSmbFilePath});
if (!SetupJailAndRun(&net_cmd, kNetAdsSeccompFilter)) {
*out_error = GetNetError(net_cmd, "workgroup");
return false;
}
const std::string& net_out = net_cmd.GetStdout();
// Parse the output to find the workgroup. Enclose in a sandbox for security
// considerations.
ProcessExecutor parse_cmd({ac::kParserPath, ac::kCmdParseWorkgroup});
parse_cmd.SetInputString(net_out);
if (!SetupJailAndRun(&parse_cmd, kParserSeccompFilter)) {
LOG(ERROR) << "authpolicy_parser parse_workgroup failed with exit code "
<< parse_cmd.GetExitCode();
*out_error = ERROR_PARSE_FAILED;
return false;
}
DCHECK(workgroup);
*workgroup = parse_cmd.GetStdout();
return true;
}
// Creates the given directory recursively and sets error message on failure.
bool CreateDirectory(const base::FilePath& dir, ErrorType* out_error) {
base::File::Error ferror;
if (!base::CreateDirectoryAndGetError(dir, &ferror)) {
LOG(ERROR) << "Failed to create directory '" << dir.value()
<< "': " << base::File::ErrorToString(ferror);
*out_error = ERROR_LOCAL_IO;
return false;
}
return true;
}
// Sets file permissions for a given filepath and sets error message on failure.
bool SetFilePermissions(const base::FilePath& fp,
int mode,
ErrorType* out_error) {
if (!base::SetPosixFilePermissions(fp, mode)) {
LOG(ERROR) << "Failed to set permissions on '" << fp.value() << "'";
*out_error = ERROR_LOCAL_IO;
return false;
}
return true;
}
// Similar to |SetFilePermissions|, but sets permissions recursively up the path
// to |base_fp| (not including |base_fp|). Returns false if |base_fp| is not a
// parent of |fp|.
bool SetFilePermissionsRecursive(const base::FilePath& fp,
const base::FilePath& base_fp,
int mode,
ErrorType* out_error) {
if (!base_fp.IsParent(fp)) {
LOG(ERROR) << "Base path '" << base_fp.value() << "' is not a parent of '"
<< fp.value() << "'";
*out_error = ERROR_LOCAL_IO;
return false;
}
for (base::FilePath curr_fp = fp; curr_fp != base_fp;
curr_fp = curr_fp.DirName()) {
if (!SetFilePermissions(curr_fp, mode, out_error))
return false;
}
return true;
}
// Copies the machine keytab file to the state directory. The copy is owned by
// authpolicyd, so that authpolicyd_exec cannot modify it anymore.
bool SecureMachineKeyTab(ErrorType* out_error) {
// At this point, tmp_kt_fp is rw for authpolicyd-exec only, so we, i.e.
// user authpolicyd, cannot read it. Thus, change file permissions as
// authpolicyd-exec user, so that the authpolicyd group can read it.
const base::FilePath tmp_kt_fp(kMachineKtTmpFilePath);
const base::FilePath state_kt_fp(kMachineKtStateFilePath);
// Set group read permissions on keytab as authpolicyd-exec, so we can copy it
// as authpolicyd (and own the copy).
{
ScopedAuthpolicyExecSwitch switch_scope;
if (!SetFilePermissions(tmp_kt_fp, kFileMode_rwr, out_error))
return false;
}
// Create empty file in destination directory. Note that it is created with
// rw_r__r__ permissions.
if (base::WriteFile(state_kt_fp, nullptr, 0) != 0) {
LOG(ERROR) << "Failed to create file '" << state_kt_fp.value() << "'";
*out_error = ERROR_LOCAL_IO;
return false;
}
// Revoke 'read by others' permission. We could also just copy tmp_kt_fp to
// state_kt_fp (see below) and revoke the read permission afterwards, but then
// state_kt_fp would be readable by anyone for a split second, causing a
// potential security risk.
if (!SetFilePermissions(state_kt_fp, kFileMode_rwr, out_error))
return false;
// Now we may copy the file. The copy is owned by authpolicyd:authpolicyd.
if (!base::CopyFile(tmp_kt_fp, state_kt_fp)) {
PLOG(ERROR) << "Failed to copy file '" << tmp_kt_fp.value()
<< "' to '" << state_kt_fp.value() << "'";
*out_error = ERROR_LOCAL_IO;
return false;
}
// Clean up temp file (must be done as authpolicyd-exec).
{
ScopedAuthpolicyExecSwitch switch_scope;
if (!base::DeleteFile(tmp_kt_fp, false)) {
LOG(ERROR) << "Failed to delete file '" << tmp_kt_fp.value() << "'";
*out_error = ERROR_LOCAL_IO;
return false;
}
}
return true;
}
// Writes the samba configuration file.
bool WriteSmbConf(const ap::SambaConfig* config, const std::string& workgroup,
ErrorType* out_error) {
if (!config) {
LOG(ERROR) << "Missing configuration. Must call JoinMachine first.";
*out_error = ERROR_NOT_JOINED;
return false;
}
std::string data =
base::StringPrintf(kSmbConfData, config->machine_name().c_str(),
workgroup.c_str(), config->realm().c_str());
const base::FilePath fp(kSmbFilePath);
const int data_size = static_cast<int>(data.size());
if (base::WriteFile(fp, data.c_str(), data_size) != data_size) {
LOG(ERROR) << "Failed to write samba conf file '" << fp.value() << "'";
*out_error = ERROR_LOCAL_IO;
return false;
}
return true;
}
// Writes the Samba configuration file. If |workgroup| points to an empty
// string, the workgroup is queried from the server and the string is updated.
// Workgroups are only queried once a session, they are expected to change very
// rarely.
bool UpdateWorkgroupAndWriteSmbConf(const ap::SambaConfig* config,
std::string* workgroup,
ErrorType* out_error) {
DCHECK(workgroup);
if (workgroup->empty()) {
// GetWorkgroup requires an smb.conf file, write one with empty workgroup.
if (!WriteSmbConf(config, *workgroup, out_error) ||
!GetWorkgroup(config, workgroup, out_error)) {
return false;
}
}
// Write smb.conf (potentially again, with valid workgroup).
return WriteSmbConf(config, *workgroup, out_error);
}
// Writes the krb5 configuration file.
bool WriteKrb5Conf(const std::string& realm, ErrorType* out_error) {
std::string data = base::StringPrintf(kKrb5ConfData, realm.c_str());
const base::FilePath fp(kKrb5FilePath);
const int data_size = static_cast<int>(data.size());
if (base::WriteFile(fp, data.c_str(), data_size) != data_size) {
LOG(ERROR) << "Failed to write krb5 conf file '" << fp.value() << "'";
*out_error = ERROR_LOCAL_IO;
return false;
}
return true;
}
// Writes the file with configuration information.
bool WriteConfiguration(const ap::SambaConfig* config, ErrorType* out_error) {
DCHECK(config);
std::string config_blob;
if (!config->SerializeToString(&config_blob)) {
LOG(ERROR) << "Failed to serialize configuration to string";
*out_error = ERROR_LOCAL_IO;
return false;
}
const base::FilePath fp(kConfigFilePath);
const int config_size = static_cast<int>(config_blob.size());
if (base::WriteFile(fp, config_blob.c_str(), config_size) != config_size) {
LOG(ERROR) << "Failed to write configuration file '" << fp.value() << "'";
*out_error = ERROR_LOCAL_IO;
return false;
}
LOG(INFO) << "Wrote configuration file '" << fp.value() << "'";
return true;
}
// Reads the file with configuration information.
bool ReadConfiguration(ap::SambaConfig* config) {
const base::FilePath fp(kConfigFilePath);
if (!base::PathExists(fp)) {
LOG(ERROR) << "Configuration file '" << fp.value() << "' does not exist";
return false;
}
std::string config_blob;
if (!base::ReadFileToStringWithMaxSize(fp, &config_blob, kConfigSizeLimit)) {
LOG(ERROR) << "Failed to read configuration file '" << fp.value() << "'";
return false;
}
if (!config->ParseFromString(config_blob)) {
LOG(ERROR) << "Failed to parse configuration from string";
return false;
}
// Check if the config is valid.
if (config->machine_name().empty() || config->realm().empty()) {
LOG(ERROR) << "Configuration is invalid";
return false;
}
LOG(INFO) << "Read configuration file '" << fp.value() << "'";
return true;
}
// Calls net ads search with given |search_string| to get an objectGUID.
bool GetAccountId(const std::string& search_string, std::string* out_account_id,
ErrorType* out_error) {
// Call net ads search to find the user's object GUID, which is used as
// account id.
ProcessExecutor net_cmd(
{kNetPath, "ads", "search", search_string, "objectGUID", "-s",
kSmbFilePath});
if (!SetupJailAndRun(&net_cmd, kNetAdsSeccompFilter)) {
*out_error = GetNetError(net_cmd, "search");
return false;
}
const std::string& net_out = net_cmd.GetStdout();
// Parse the output to find the account id. Enclose in a sandbox for security
// considerations.
ProcessExecutor parse_cmd({ac::kParserPath, ac::kCmdParseAccountId});
parse_cmd.SetInputString(net_out);
if (!SetupJailAndRun(&parse_cmd, kParserSeccompFilter)) {
LOG(ERROR) << "Failed to get user account id. Net response: " << net_out;
*out_error = ERROR_PARSE_FAILED;
return false;
}
*out_account_id = parse_cmd.GetStdout();
return true;
}
bool GetGpoList(const std::string& user_or_machine_name,
ac::PolicyScope scope,
std::string* out_gpo_list,
ErrorType* out_error) {
DCHECK(out_gpo_list);
LOG(INFO) << "Getting GPO list for " << user_or_machine_name;
// Machine names are names ending with $, anything else is a user name.
authpolicy::ProcessExecutor net_cmd({kNetPath, "ads", "gpo", "list",
user_or_machine_name, "-s",
kSmbFilePath});
if (!SetupJailAndRun(&net_cmd, kNetAdsSeccompFilter)) {
*out_error = GetNetError(net_cmd, "gpo list");
return false;
}
// GPO data is written to stderr, not stdin!
const std::string& net_out = net_cmd.GetStderr();
// Parse the GPO list. Enclose in a sandbox for security considerations.
const char* cmd = scope == ac::PolicyScope::USER ? ac::kCmdParseUserGpoList
: ac::kCmdParseDeviceGpoList;
ProcessExecutor parse_cmd({ac::kParserPath, cmd});
parse_cmd.SetInputString(net_out);
if (!SetupJailAndRun(&parse_cmd, kParserSeccompFilter)) {
LOG(ERROR) << "Failed to parse GPO list";
*out_error = ERROR_PARSE_FAILED;
return false;
}
*out_gpo_list = parse_cmd.GetStdout();
return true;
}
struct GpoPaths {
std::string server_; // GPO file path on server (not a local file path!).
base::FilePath local_; // Local GPO file path.
GpoPaths(const std::string& server, const std::string& local)
: server_(server), local_(local) {}
};
bool DownloadGpos(const std::string& gpo_list_blob,
const std::string& domain_controller_name,
const char* preg_dir,
std::vector<base::FilePath>* out_gpo_file_paths,
ErrorType* out_error) {
// Parse GPO list protobuf.
ap::GpoList gpo_list;
if (!gpo_list.ParseFromString(gpo_list_blob)) {
LOG(ERROR) << "Failed to read GPO list protobuf";
return false;
}
if (gpo_list.entries_size() == 0) {
LOG(INFO) << "No GPOs to download";
return true;
}
// Generate all smb source and linux target directories and create targets.
std::string smb_command = "prompt OFF;";
std::string gpo_basepath;
std::vector<GpoPaths> gpo_paths;
for (int entry_idx = 0; entry_idx < gpo_list.entries_size(); ++entry_idx) {
const ap::GpoEntry& gpo = gpo_list.entries(entry_idx);
// Security check, make sure nobody sneaks in smbclient commands.
if (gpo.basepath().find(';') != std::string::npos ||
gpo.directory().find(';') != std::string::npos) {
LOG(ERROR) << "GPO paths may not contain a ';'";
*out_error = ERROR_BAD_GPOS;
return false;
}
// All GPOs should have the same basepath, i.e. come from the same SysVol.
if (gpo_basepath.empty()) {
gpo_basepath = gpo.basepath();
} else if (!base::EqualsCaseInsensitiveASCII(gpo_basepath,
gpo.basepath())) {
LOG(ERROR) << "Inconsistent base path '" << gpo_basepath << "' != '"
<< gpo.basepath() << "'";
*out_error = ERROR_BAD_GPOS;
return false;
}
// Figure out local (Linux) and remote (smb) directories.
std::string smb_dir =
base::StringPrintf("\\%s\\%s", gpo.directory().c_str(), preg_dir);
std::string linux_dir = kGpoLocalDir + smb_dir;
std::replace(linux_dir.begin(), linux_dir.end(), '\\', '/');
// Make local directory.
const base::FilePath linux_dir_fp(linux_dir);
if (!CreateDirectory(linux_dir_fp, out_error))
return false;
// Set group rwx permissions recursively, so that smbclient can write GPOs
// there and the parser tool can read the GPOs later.
if (!SetFilePermissionsRecursive(linux_dir_fp, base::FilePath(kSambaTmpDir),
kFileMode_rwxrwx, out_error)) {
return false;
}
// Build command for smbclient.
smb_command += base::StringPrintf("cd %s;lcd %s;mget %s;", smb_dir.c_str(),
linux_dir.c_str(), kPRegFileName);
// Record output file paths.
gpo_paths.push_back(GpoPaths(smb_dir + "\\" + kPRegFileName,
linux_dir + "/" + kPRegFileName));
// Delete any preexisting policy file. Otherwise, if downloading the file
// failed, we wouldn't realize it and use a stale version.
if (base::PathExists(gpo_paths.back().local_) &&
!base::DeleteFile(gpo_paths.back().local_, false)) {
LOG(ERROR) << "Failed to delete old GPO file '"
<< gpo_paths.back().local_.value().c_str() << "'";
return false;
}
}
std::string service = base::StringPrintf(
"//%s.%s", domain_controller_name.c_str(), gpo_basepath.c_str());
// Download GPO into local directory.
ProcessExecutor smb_client_cmd(
{kSmbClientPath, service, "-s", kSmbFilePath, "-c", smb_command, "-k"});
if (!SetupJailAndRun(&smb_client_cmd, kSmbClientSeccompFilter)) {
// The exit code of smbclient corresponds to the LAST command issued. Thus,
// Execute() might fail if the last GPO file is missing. However, we handle
// this below (not an error), so only error out here on internal errors.
if (smb_client_cmd.GetExitCode() ==
ProcessExecutor::kExitCodeInternalError) {
*out_error = GetSmbclientError(smb_client_cmd);
return false;
}
}
// Note that the errors are in stdout and the output is in stderr :-/
const std::string& smbclient_out_lower =
base::ToLowerASCII(smb_client_cmd.GetStdout());
// Make sure the GPO files actually downloaded.
DCHECK(out_gpo_file_paths);
for (const GpoPaths& gpo_path : gpo_paths) {
if (base::PathExists(gpo_path.local_)) {
out_gpo_file_paths->push_back(gpo_path.local_);
} else {
// Gracefully handle non-existing GPOs. Testing revealed these cases do
// exist, see crbug.com/680921.
const std::string no_file_error_key(
base::ToLowerASCII(kKeyNoSuchFile + gpo_path.server_));
if (Contains(smbclient_out_lower, no_file_error_key)) {
LOG(WARNING) << "Ignoring missing preg file '"
<< gpo_path.local_.value() << "'";
} else {
LOG(ERROR) << "Failed to download preg file '"
<< gpo_path.local_.value() << "'";
*out_error = ERROR_SMBCLIENT_FAILED;
return false;
}
}
}
return true;
}
// Parse GPOs and store them in user/device policy protobufs.
bool ParseGposIntoProtobuf(const std::vector<base::FilePath>& gpo_file_paths,
const char* parser_cmd_string,
std::string* out_policy_blob,
ErrorType* out_error) {
// Convert file paths to proto blob.
std::string gpo_file_paths_blob;
ap::FilePathList fp_proto;
for (const auto& fp : gpo_file_paths)
*fp_proto.add_entries() = fp.value();
if (!fp_proto.SerializeToString(&gpo_file_paths_blob)) {
LOG(ERROR) << "Failed to serialize policy file paths to protobuf";
*out_error = ERROR_PARSE_PREG_FAILED;
return false;
}
// Load GPOs into protobuf. Enclose in a sandbox for security considerations.
ProcessExecutor parse_cmd({ac::kParserPath, parser_cmd_string});
parse_cmd.SetInputString(gpo_file_paths_blob);
if (!SetupJailAndRun(&parse_cmd, kParserSeccompFilter)) {
LOG(ERROR) << "Failed to parse preg files";
*out_error = ERROR_PARSE_PREG_FAILED;
return false;
}
*out_policy_blob = parse_cmd.GetStdout();
return true;
}
} // namespace
bool SambaInterface::Initialize(bool expect_config) {
// Need to create samba dirs since samba can't create dirs recursively...
ErrorType error = ERROR_NONE;
for (const auto& dir_and_mode : kSambaDirsAndMode) {
const base::FilePath dir(dir_and_mode.first);
const int mode = dir_and_mode.second;
if (!CreateDirectory(dir, &error) ||
!SetFilePermissions(dir, mode, &error)) {
LOG(ERROR) << "Failed to initialize SambaInterface";
return false;
}
}
if (expect_config) {
config_ = base::MakeUnique<ap::SambaConfig>();
if (!ReadConfiguration(config_.get())) {
LOG(ERROR) << "Failed to initialize SambaInterface";
config_.reset();
return false;
}
}
// Load debug flags file if present. Always CHECK() the flags, even in
// release, to catch uninitialized variables.
CHECK(!s_disable_seccomp_filters);
CHECK(!s_log_seccomp_filters);
std::string flags;
if (base::ReadFileToString(base::FilePath(kFlagsFilePath), &flags)) {
if (Contains(flags, kFlagDisableSeccomp)) {
LOG(WARNING) << "Seccomp filters disabled";
s_disable_seccomp_filters = true;
}
if (Contains(flags, kFlagLogSeccomp)) {
LOG(WARNING) << "Logging seccomp filter failures";
s_log_seccomp_filters = true;
}
}
return true;
}
bool SambaInterface::AuthenticateUser(const std::string& user_principal_name,
int password_fd,
std::string* out_account_id,
ErrorType* out_error) {
// Split user_principal_name into parts and normalize.
std::string user_name, realm, workgroup, normalized_upn;
if (!ai::ParseUserPrincipalName(user_principal_name, &user_name, &realm,
&normalized_upn, out_error))
return false;
// Write krb5 configuration file.
if (!WriteKrb5Conf(realm, out_error))
return false;
// Write samba configuration file.
if (!UpdateWorkgroupAndWriteSmbConf(config_.get(), &workgroup_, out_error))
return false;
// Call kinit to get the Kerberos ticket-granting-ticket.
ProcessExecutor kinit_cmd({kKInitPath, normalized_upn});
kinit_cmd.SetInputFile(password_fd);
kinit_cmd.SetEnv(kKrb5ConfEnvKey, kKrb5ConfEnvValue); // Kerberos config.
if (!SetupJailAndRun(&kinit_cmd, kKInitSeccompFilter)) {
*out_error = GetKinitError(kinit_cmd);
return false;
}
// Get a unique id for the user account. Search by sAMAccountName first since
// that's what kinit/Windows prefer and if that fails, search by UPN. The name
// part of the principal name can be different from the sAMAccountName!
if (!GetAccountId(
base::StringPrintf("(sAMAccountName=%s)", user_name.c_str()),
out_account_id, out_error) && *out_error == ERROR_PARSE_FAILED) {
LOG(WARNING) << "Object GUID not found by sAMAccountName. "
<< "Trying userPrincipalName.";
*out_error = ERROR_NONE;
if (!GetAccountId(
base::StringPrintf("(userPrincipalName=%s)", normalized_upn.c_str()),
out_account_id, out_error)) {
return false;
}
}
// Store user name for further reference.
const std::string account_id_key(kActiveDirectoryPrefix + *out_account_id);
account_id_key_user_name_map_[account_id_key] = user_name;
return true;
}
bool SambaInterface::JoinMachine(const std::string& machine_name,
const std::string& user_principal_name,
int password_fd,
ErrorType* out_error) {
// Split user principal name into parts.
std::string user_name, realm, normalized_upn;
if (!ai::ParseUserPrincipalName(user_principal_name, &user_name, &realm,
&normalized_upn, out_error))
return false;
// Create config.
std::unique_ptr<ap::SambaConfig> config = base::MakeUnique<ap::SambaConfig>();
config->set_machine_name(base::ToUpperASCII(machine_name));
config->set_realm(realm);
// Write samba configuration. Will query the workgroup.
workgroup_.clear();
if (!UpdateWorkgroupAndWriteSmbConf(config.get(), &workgroup_, out_error))
return false;
// Call net ads join to join the machine to the Active Directory domain.
ProcessExecutor net_cmd(
{kNetPath, "ads", "join", "-U", normalized_upn, "-s", kSmbFilePath});
net_cmd.SetInputFile(password_fd);
net_cmd.SetEnv(kMachineKTEnvKey, kMachineKTEnvValueTmp); // Keytab file path.
if (!SetupJailAndRun(&net_cmd, kNetAdsSeccompFilter)) {
*out_error = GetNetError(net_cmd, "join");
return false;
}
// Prevent that authpolicyd-exec can make changes to the keytab file.
if (!SecureMachineKeyTab(out_error))
return false;
// Store configuration for subsequent runs of the daemon.
if (!WriteConfiguration(config.get(), out_error))
return false;
// Only if everything worked out, keep the config.
config_ = std::move(config);
return true;
}
bool SambaInterface::FetchUserGpos(const std::string& account_id_key,
std::string* out_policy_blob,
ErrorType* out_error) {
// Get user name from account id key (must be logged in to fetch user policy).
std::unordered_map<std::string, std::string>::const_iterator iter =
account_id_key_user_name_map_.find(account_id_key);
if (iter == account_id_key_user_name_map_.end()) {
LOG(ERROR) << "User not logged in. Please call AuthenticateUser first.";
*out_error = ERROR_NOT_LOGGED_IN;
return false;
}
const std::string& user_name = iter->second;
// Write samba configuration file.
if (!UpdateWorkgroupAndWriteSmbConf(config_.get(), &workgroup_, out_error))
return false;
// Make sure we have the domain controller name.
if (!UpdateDomainControllerName(&domain_controller_name_, out_error))
return false;
// FetchDeviceGpos writes a krb5.conf here. For user policy, there's no need
// to do that here since we're reusing the TGT generated in AuthenticateUser.
// Get the list of GPOs for the given user name.
std::string gpo_list_blob;
if (!GetGpoList(user_name, ac::PolicyScope::USER, &gpo_list_blob, out_error))
return false;
// Download GPOs from Active Directory server.
std::vector<base::FilePath> gpo_file_paths;
if (!DownloadGpos(gpo_list_blob, domain_controller_name_, kPRegUserDir,
&gpo_file_paths, out_error))
return false;
// Parse GPOs and store them in a user policy protobuf.
if (!ParseGposIntoProtobuf(gpo_file_paths, ac::kCmdParseUserPreg,
out_policy_blob, out_error))
return false;
return true;
}
bool SambaInterface::FetchDeviceGpos(std::string* out_policy_blob,
ErrorType* out_error) {
// Write samba configuration file.
if (!UpdateWorkgroupAndWriteSmbConf(config_.get(), &workgroup_, out_error))
return false;
// Make sure we have the domain controller name.
if (!UpdateDomainControllerName(&domain_controller_name_, out_error))
return false;
// Write krb5 configuration file.
DCHECK(config_.get());
if (!WriteKrb5Conf(config_->realm(), out_error))
return false;
// Call kinit to get the Kerberos ticket-granting-ticket.
ProcessExecutor kinit_cmd(
{kKInitPath, config_->machine_name() + "$@" + config_->realm(), "-k"});
kinit_cmd.SetEnv(kKrb5ConfEnvKey, kKrb5ConfEnvValue); // Kerberos config.
kinit_cmd.SetEnv(kMachineKTEnvKey, kMachineKTEnvValueState); // Keytab file.
if (!SetupJailAndRun(&kinit_cmd, kKInitSeccompFilter)) {
*out_error = GetKinitError(kinit_cmd);
return false;
}
// Get the list of GPOs for the machine.
std::string gpo_list_blob;
if (!GetGpoList(config_->machine_name() + "$", ac::PolicyScope::MACHINE,
&gpo_list_blob, out_error))
return false;
// Download GPOs from Active Directory server.
std::vector<base::FilePath> gpo_file_paths;
if (!DownloadGpos(gpo_list_blob, domain_controller_name_, kPRegDeviceDir,
&gpo_file_paths, out_error))
return false;
// Parse GPOs and store them in a device policy protobuf.
if (!ParseGposIntoProtobuf(gpo_file_paths, ac::kCmdParseDevicePreg,
out_policy_blob, out_error))
return false;
return true;
}
} // namespace authpolicy