| // 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 <algorithm> |
| #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/single_thread_task_runner.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 <base/threading/platform_thread.h> |
| #include <base/time/time.h> |
| |
| #include "authpolicy/log_level.h" |
| #include "authpolicy/platform_helper.h" |
| #include "authpolicy/process_executor.h" |
| #include "bindings/authpolicy_containers.pb.h" |
| |
| namespace authpolicy { |
| namespace { |
| |
| // Samba configuration file data. |
| const char kSmbConfData[] = |
| "[global]\n" |
| "\tnetbios name = %s\n" |
| "\tsecurity = ADS\n" |
| "\tworkgroup = %s\n" |
| "\trealm = %s\n" |
| "\tlock directory = %s\n" |
| "\tcache directory = %s\n" |
| "\tstate directory = %s\n" |
| "\tprivate directory = %s\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"; |
| |
| 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 with permissions to be created. AUTHPOLICY_TMP_DIR needs group rx |
| // access to read smb.conf and krb5.conf and to access SAMBA_DIR, but no write |
| // access. The Samba directories need full group rwx access since Samba reads |
| // and writes files there. |
| constexpr std::pair<Path, int> kDirsAndMode[] = { |
| {Path::TEMP_DIR, kFileMode_rwxrx}, |
| {Path::SAMBA_DIR, kFileMode_rwxrwx}, |
| {Path::SAMBA_LOCK_DIR, kFileMode_rwxrwx}, |
| {Path::SAMBA_CACHE_DIR, kFileMode_rwxrwx}, |
| {Path::SAMBA_STATE_DIR, kFileMode_rwxrwx}, |
| {Path::SAMBA_PRIVATE_DIR, kFileMode_rwxrwx}}; |
| |
| // Directory / filenames for user and device policy. |
| const char kPRegUserDir[] = "User"; |
| const char kPRegDeviceDir[] = "Machine"; |
| const char kPRegFileName[] = "registry.pol"; |
| |
| // Flags. Write kFlag* strings to the Path::DEBUG_FLAGS file to toggle flags. |
| const char kFlagDisableSeccomp[] = "disable_seccomp"; |
| const char kFlagLogSeccomp[] = "log_seccomp"; |
| const char kFlagTraceKinit[] = "trace_kinit"; |
| |
| // Size limit when loading the config file (256 kb). |
| const size_t kConfigSizeLimit = 256 * 1024; |
| |
| // Maximum smbclient tries. |
| const int kSmbClientMaxTries = 5; |
| // Wait interval between two smbclient tries. |
| const int kSmbClientRetryWaitSeconds = 1; |
| |
| // Keys for interpreting net output. |
| const char kKeyJoinAccessDenied[] = "NT_STATUS_ACCESS_DENIED"; |
| const char kKeyInvalidMachineName[] = "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 kKeyConnectionReset[] = "NT_STATUS_CONNECTION_RESET"; |
| const char kKeyNetworkTimeout[] = "NT_STATUS_IO_TIMEOUT"; |
| const char kKeyObjectNameNotFound[] = |
| "NT_STATUS_OBJECT_NAME_NOT_FOUND opening remote file "; |
| |
| ErrorType GetNetError(const ProcessExecutor& executor, |
| const std::string& net_command) { |
| 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 problem"; |
| 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, kKeyInvalidMachineName)) { |
| LOG(ERROR) << error_msg << "invalid machine name"; |
| return ERROR_INVALID_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 << "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) || |
| Contains(smb_client_out, kKeyConnectionReset)) { |
| LOG(ERROR) << "smbclient failed - network problem"; |
| return ERROR_NETWORK_PROBLEM; |
| } |
| LOG(ERROR) << "smbclient failed with exit code " |
| << smb_client_cmd.GetExitCode(); |
| return ERROR_SMBCLIENT_FAILED; |
| } |
| |
| // Creates the given directory recursively and sets error message on failure. |
| ErrorType CreateDirectory(const base::FilePath& dir) { |
| base::File::Error ferror; |
| if (!base::CreateDirectoryAndGetError(dir, &ferror)) { |
| LOG(ERROR) << "Failed to create directory '" << dir.value() |
| << "': " << base::File::ErrorToString(ferror); |
| return ERROR_LOCAL_IO; |
| } |
| return ERROR_NONE; |
| } |
| |
| // Sets file permissions for a given filepath and sets error message on failure. |
| ErrorType SetFilePermissions(const base::FilePath& fp, int mode) { |
| if (!base::SetPosixFilePermissions(fp, mode)) { |
| LOG(ERROR) << "Failed to set permissions on '" << fp.value() << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| return ERROR_NONE; |
| } |
| |
| // 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|. |
| ErrorType SetFilePermissionsRecursive(const base::FilePath& fp, |
| const base::FilePath& base_fp, |
| int mode) { |
| if (!base_fp.IsParent(fp)) { |
| LOG(ERROR) << "Base path '" << base_fp.value() << "' is not a parent of '" |
| << fp.value() << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| ErrorType error = ERROR_NONE; |
| for (base::FilePath curr_fp = fp; curr_fp != base_fp && error == ERROR_NONE; |
| curr_fp = curr_fp.DirName()) { |
| error = SetFilePermissions(curr_fp, mode); |
| } |
| return error; |
| } |
| |
| } // namespace |
| |
| SambaInterface::SambaInterface( |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner, |
| AuthPolicyMetrics* metrics, |
| const PathService* path_service) |
| : metrics_(metrics), |
| paths_(path_service), |
| jail_helper_(paths_), |
| user_tgt_manager_(task_runner, |
| paths_, |
| metrics_, |
| &jail_helper_, |
| Path::USER_KRB5_CONF, |
| Path::USER_CREDENTIAL_CACHE), |
| device_tgt_manager_(task_runner, |
| paths_, |
| metrics_, |
| &jail_helper_, |
| Path::DEVICE_KRB5_CONF, |
| Path::DEVICE_CREDENTIAL_CACHE) { |
| DCHECK(paths_); |
| } |
| |
| ErrorType SambaInterface::Initialize(bool expect_config) { |
| ErrorType error = ERROR_NONE; |
| for (const auto& dir_and_mode : kDirsAndMode) { |
| const base::FilePath dir(paths_->Get(dir_and_mode.first)); |
| const int mode = dir_and_mode.second; |
| error = ::authpolicy::CreateDirectory(dir); |
| if (error != ERROR_NONE) |
| return error; |
| error = SetFilePermissions(dir, mode); |
| if (error != ERROR_NONE) |
| return error; |
| } |
| |
| if (expect_config) { |
| error = ReadConfiguration(); |
| if (error != ERROR_NONE) |
| return error; |
| } |
| |
| // Load debug flags from file. |
| bool disable_seccomp_filters = false; |
| bool log_seccomp_filters = false; |
| bool trace_kinit = false; |
| std::string flags; |
| const std::string& flags_path = paths_->Get(Path::DEBUG_FLAGS); |
| if (base::ReadFileToString(base::FilePath(flags_path), &flags)) { |
| if (Contains(flags, kFlagDisableSeccomp)) { |
| LOG(WARNING) << "Seccomp filters disabled"; |
| disable_seccomp_filters = true; |
| } |
| if (Contains(flags, kFlagLogSeccomp)) { |
| LOG(WARNING) << "Logging seccomp filter failures"; |
| log_seccomp_filters = true; |
| } |
| if (Contains(flags, kFlagTraceKinit)) { |
| LOG(WARNING) << "Trace kinit"; |
| trace_kinit = true; |
| } |
| } |
| |
| // Set debug flags. |
| jail_helper_.SetDebugFlags(disable_seccomp_filters, log_seccomp_filters); |
| user_tgt_manager_.SetKinitTraceEnabled(trace_kinit); |
| device_tgt_manager_.SetKinitTraceEnabled(trace_kinit); |
| |
| return ERROR_NONE; |
| } |
| |
| // static |
| bool SambaInterface::CleanState(const PathService* path_service) { |
| // Note: We're not permitted to delete the folder and DeleteFile apparently |
| // doesn't support wildcards, so DeleteFile returns false. |
| DCHECK(path_service); |
| base::FilePath state_dir(path_service->Get(Path::STATE_DIR)); |
| base::DeleteFile(state_dir, true /* recursive */); |
| if (!base::IsDirectoryEmpty(state_dir)) { |
| LOG(ERROR) << "Failed to clean state dir '" << state_dir.value() << "'"; |
| return false; |
| } |
| return true; |
| } |
| |
| ErrorType SambaInterface::AuthenticateUser( |
| const std::string& user_principal_name, |
| const std::string& account_id, |
| int password_fd, |
| ActiveDirectoryAccountInfo* account_info) { |
| // Split user_principal_name into parts and normalize. |
| std::string user_name, realm, workgroup, normalized_upn; |
| if (!ParseUserPrincipalName( |
| user_principal_name, &user_name, &realm, &normalized_upn)) { |
| return ERROR_PARSE_UPN_FAILED; |
| } |
| |
| // Write Samba configuration file. |
| ErrorType error = EnsureWorkgroupAndWriteSmbConf(); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Make sure we have realm info. |
| protos::RealmInfo realm_info; |
| error = GetRealmInfo(&realm_info); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Get account info for the user. |
| error = GetAccountInfo( |
| user_name, normalized_upn, account_id, realm_info, account_info); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Update normalized_upn. This handles the situation when the user name |
| // changes on the server and the user logs in with his old user name (e.g. |
| // from the pods screen in Chrome). |
| normalized_upn = account_info->sam_account_name() + "@" + realm; |
| |
| // Call kinit to get the Kerberos ticket-granting-ticket. |
| error = user_tgt_manager_.AcquireTgtWithPassword( |
| normalized_upn, password_fd, realm, realm_info.kdc_ip()); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Renew TGT periodically. The usual validity lifetime is about 10 hours, so |
| // this won't happen too often. |
| user_tgt_manager_.EnableTgtAutoRenewal(true); |
| |
| // Store sAMAccountName for policy fetch. Note that net ads gpo list always |
| // wants the sAMAccountName. |
| const std::string account_id_key(kActiveDirectoryPrefix + |
| account_info->account_id()); |
| user_id_name_map_[account_id_key] = account_info->sam_account_name(); |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::GetUserStatus( |
| const std::string& account_id, ActiveDirectoryUserStatus* user_status) { |
| // Write Samba configuration file. |
| ErrorType error = EnsureWorkgroupAndWriteSmbConf(); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Make sure we have realm info. |
| protos::RealmInfo realm_info; |
| error = GetRealmInfo(&realm_info); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Get account info for the user. |
| ActiveDirectoryAccountInfo account_info; |
| error = GetAccountInfo("" /* user_name unused */, |
| "" /* normalized_upn unused */, |
| account_id, |
| realm_info, |
| &account_info); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Determine the status of the TGT. |
| ActiveDirectoryUserStatus::TgtStatus tgt_status = |
| ActiveDirectoryUserStatus::TGT_VALID; |
| error = GetUserTgtStatus(&tgt_status); |
| if (error != ERROR_NONE) |
| return error; |
| |
| *user_status->mutable_account_info() = account_info; |
| user_status->set_tgt_status(tgt_status); |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::JoinMachine(const std::string& machine_name, |
| const std::string& user_principal_name, |
| int password_fd) { |
| // Split user principal name into parts. |
| std::string user_name, realm, normalized_upn; |
| if (!ParseUserPrincipalName( |
| user_principal_name, &user_name, &realm, &normalized_upn)) { |
| return ERROR_PARSE_UPN_FAILED; |
| } |
| |
| // Wipe and (re-)create config. Note that all session data is wiped to make |
| // testing easier. |
| Reset(); |
| config_ = base::MakeUnique<protos::ActiveDirectoryConfig>(); |
| config_->set_machine_name(base::ToUpperASCII(machine_name)); |
| config_->set_realm(realm); |
| |
| // Write Samba configuration. Will query the workgroup. |
| ErrorType error = EnsureWorkgroupAndWriteSmbConf(); |
| if (error != ERROR_NONE) { |
| Reset(); |
| return error; |
| } |
| |
| // Call net ads join to join the machine to the Active Directory domain. |
| ProcessExecutor net_cmd({paths_->Get(Path::NET), |
| "ads", |
| "join", |
| "-U", |
| normalized_upn, |
| "-s", |
| paths_->Get(Path::SMB_CONF), |
| "-d", |
| kNetLogLevel}); |
| net_cmd.SetInputFile(password_fd); |
| net_cmd.SetEnv(kKrb5KTEnvKey, // Machine keytab file path. |
| kFilePrefix + paths_->Get(Path::MACHINE_KT_TEMP)); |
| if (!jail_helper_.SetupJailAndRun( |
| &net_cmd, Path::NET_ADS_SECCOMP, TIMER_NET_ADS_JOIN)) { |
| Reset(); |
| return GetNetError(net_cmd, "join"); |
| } |
| |
| // Prevent that authpolicyd-exec can make changes to the keytab file. |
| error = SecureMachineKeyTab(); |
| if (error != ERROR_NONE) { |
| Reset(); |
| return error; |
| } |
| |
| // Store configuration for subsequent runs of the daemon. |
| error = WriteConfiguration(); |
| if (error != ERROR_NONE) { |
| Reset(); |
| return error; |
| } |
| |
| // Only if everything worked out, keep the config. |
| retry_machine_kinit_ = true; |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::FetchUserGpos(const std::string& account_id_key, |
| std::string* policy_blob) { |
| // Get sAMAccountName from account id key (must be logged in to fetch user |
| // policy). |
| auto iter = user_id_name_map_.find(account_id_key); |
| if (iter == user_id_name_map_.end()) { |
| LOG(ERROR) << "User not logged in. Please call AuthenticateUser first."; |
| return ERROR_NOT_LOGGED_IN; |
| } |
| const std::string& sam_account_name = iter->second; |
| |
| // Write Samba configuration file. |
| ErrorType error = EnsureWorkgroupAndWriteSmbConf(); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Make sure we have the domain controller name. |
| protos::RealmInfo realm_info; |
| error = GetRealmInfo(&realm_info); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // 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. |
| protos::GpoList gpo_list; |
| error = GetGpoList(sam_account_name, PolicyScope::USER, &gpo_list); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Download GPOs from Active Directory server. |
| std::vector<base::FilePath> gpo_file_paths; |
| error = DownloadGpos( |
| gpo_list, realm_info.dc_name(), PolicyScope::USER, &gpo_file_paths); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Parse GPOs and store them in a user policy protobuf. |
| error = ParseGposIntoProtobuf(gpo_file_paths, kCmdParseUserPreg, policy_blob); |
| if (error != ERROR_NONE) |
| return error; |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::FetchDeviceGpos(std::string* policy_blob) { |
| // Write Samba configuration file. |
| ErrorType error = EnsureWorkgroupAndWriteSmbConf(); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Get realm info. |
| protos::RealmInfo realm_info; |
| error = GetRealmInfo(&realm_info); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Call kinit to get the Kerberos ticket-granting-ticket. retry_machine_kinit_ |
| // is true for the first device policy fetch after joining Active Directory, |
| // which can be very slow because machine credentials need to propagate |
| // through the AD deployment. |
| std::string machine_principal = |
| config_->machine_name() + "$@" + config_->realm(); |
| error = device_tgt_manager_.AcquireTgtWithKeytab(machine_principal, |
| Path::MACHINE_KT_STATE, |
| retry_machine_kinit_, |
| config_->realm(), |
| realm_info.kdc_ip()); |
| retry_machine_kinit_ = false; |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Get the list of GPOs for the machine. |
| protos::GpoList gpo_list; |
| error = GetGpoList( |
| config_->machine_name() + "$", PolicyScope::MACHINE, &gpo_list); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Download GPOs from Active Directory server. |
| std::vector<base::FilePath> gpo_file_paths; |
| error = DownloadGpos( |
| gpo_list, realm_info.dc_name(), PolicyScope::MACHINE, &gpo_file_paths); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Parse GPOs and store them in a device policy protobuf. |
| error = |
| ParseGposIntoProtobuf(gpo_file_paths, kCmdParseDevicePreg, policy_blob); |
| if (error != ERROR_NONE) |
| return error; |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::GetRealmInfo(protos::RealmInfo* realm_info) const { |
| authpolicy::ProcessExecutor net_cmd({paths_->Get(Path::NET), |
| "ads", |
| "info", |
| "-s", |
| paths_->Get(Path::SMB_CONF), |
| "-d", |
| kNetLogLevel}); |
| if (!jail_helper_.SetupJailAndRun( |
| &net_cmd, Path::NET_ADS_SECCOMP, TIMER_NET_ADS_INFO)) { |
| return GetNetError(net_cmd, "info"); |
| } |
| 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({paths_->Get(Path::PARSER), kCmdParseRealmInfo}); |
| parse_cmd.SetInputString(net_out); |
| if (!jail_helper_.SetupJailAndRun( |
| &parse_cmd, Path::PARSER_SECCOMP, TIMER_NONE)) { |
| LOG(ERROR) << "authpolicy_parser parse_realm_info failed with exit code " |
| << parse_cmd.GetExitCode(); |
| return ERROR_PARSE_FAILED; |
| } |
| if (!realm_info->ParseFromString(parse_cmd.GetStdout())) { |
| LOG(ERROR) << "Failed to parse realm info from string"; |
| return ERROR_PARSE_FAILED; |
| } |
| |
| LOG(INFO) << "Found DC name = '" << realm_info->dc_name() << "'"; |
| LOG(INFO) << "Found KDC IP = '" << realm_info->kdc_ip() << "'"; |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::GetUserTgtStatus( |
| ActiveDirectoryUserStatus::TgtStatus* tgt_status) { |
| protos::TgtLifetime lifetime; |
| ErrorType error = user_tgt_manager_.GetTgtLifetime(&lifetime); |
| if (error == ERROR_NONE) { |
| *tgt_status = lifetime.validity_seconds() > 0 |
| ? ActiveDirectoryUserStatus::TGT_VALID |
| : ActiveDirectoryUserStatus::TGT_EXPIRED; |
| return ERROR_NONE; |
| } |
| |
| // Eat two errors and convert them to TgtStatus values instead. |
| if (error == ERROR_NO_CREDENTIALS_CACHE_FOUND) { |
| *tgt_status = ActiveDirectoryUserStatus::TGT_NOT_FOUND; |
| return ERROR_NONE; |
| } |
| if (error == ERROR_KERBEROS_TICKET_EXPIRED) { |
| *tgt_status = ActiveDirectoryUserStatus::TGT_EXPIRED; |
| return ERROR_NONE; |
| } |
| |
| return error; |
| } |
| |
| ErrorType SambaInterface::EnsureWorkgroup() { |
| if (!workgroup_.empty()) |
| return ERROR_NONE; |
| |
| ProcessExecutor net_cmd({paths_->Get(Path::NET), |
| "ads", |
| "workgroup", |
| "-s", |
| paths_->Get(Path::SMB_CONF), |
| "-d", |
| kNetLogLevel}); |
| if (!jail_helper_.SetupJailAndRun( |
| &net_cmd, Path::NET_ADS_SECCOMP, TIMER_NET_ADS_WORKGROUP)) { |
| return GetNetError(net_cmd, "workgroup"); |
| } |
| 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({paths_->Get(Path::PARSER), kCmdParseWorkgroup}); |
| parse_cmd.SetInputString(net_out); |
| if (!jail_helper_.SetupJailAndRun( |
| &parse_cmd, Path::PARSER_SECCOMP, TIMER_NONE)) { |
| LOG(ERROR) << "authpolicy_parser parse_workgroup failed with exit code " |
| << parse_cmd.GetExitCode(); |
| return ERROR_PARSE_FAILED; |
| } |
| workgroup_ = parse_cmd.GetStdout(); |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::WriteSmbConf() const { |
| if (!config_) { |
| LOG(ERROR) << "Missing configuration. Must call JoinMachine first."; |
| return ERROR_NOT_JOINED; |
| } |
| |
| std::string data = |
| base::StringPrintf(kSmbConfData, |
| config_->machine_name().c_str(), |
| workgroup_.c_str(), |
| config_->realm().c_str(), |
| paths_->Get(Path::SAMBA_LOCK_DIR).c_str(), |
| paths_->Get(Path::SAMBA_CACHE_DIR).c_str(), |
| paths_->Get(Path::SAMBA_STATE_DIR).c_str(), |
| paths_->Get(Path::SAMBA_PRIVATE_DIR).c_str()); |
| |
| const base::FilePath smbconf_path(paths_->Get(Path::SMB_CONF)); |
| const int data_size = static_cast<int>(data.size()); |
| if (base::WriteFile(smbconf_path, data.c_str(), data_size) != data_size) { |
| LOG(ERROR) << "Failed to write Samba conf file '" << smbconf_path.value() |
| << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::EnsureWorkgroupAndWriteSmbConf() { |
| if (workgroup_.empty()) { |
| // EnsureWorkgroup requires an smb.conf file, write one with empty |
| // workgroup. |
| ErrorType error = WriteSmbConf(); |
| if (error != ERROR_NONE) |
| return error; |
| |
| error = EnsureWorkgroup(); |
| if (error != ERROR_NONE) |
| return error; |
| } |
| |
| // Write smb.conf (potentially again, with valid workgroup). |
| return WriteSmbConf(); |
| } |
| |
| ErrorType SambaInterface::WriteConfiguration() const { |
| DCHECK(config_); |
| std::string config_blob; |
| if (!config_->SerializeToString(&config_blob)) { |
| LOG(ERROR) << "Failed to serialize configuration to string"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| const base::FilePath config_path(paths_->Get(Path::CONFIG_DAT)); |
| const int config_size = static_cast<int>(config_blob.size()); |
| if (base::WriteFile(config_path, config_blob.c_str(), config_size) != |
| config_size) { |
| LOG(ERROR) << "Failed to write configuration file '" << config_path.value() |
| << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| // This file is only authpolicyd's business. |
| ErrorType error = |
| SetFilePermissions(config_path, base::FILE_PERMISSION_READ_BY_USER); |
| if (error != ERROR_NONE) |
| return error; |
| |
| LOG(INFO) << "Wrote configuration file '" << config_path.value() << "'"; |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::ReadConfiguration() { |
| const base::FilePath config_path(paths_->Get(Path::CONFIG_DAT)); |
| if (!base::PathExists(config_path)) { |
| LOG(ERROR) << "Configuration file '" << config_path.value() |
| << "' does not exist"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| std::string config_blob; |
| if (!base::ReadFileToStringWithMaxSize( |
| config_path, &config_blob, kConfigSizeLimit)) { |
| LOG(ERROR) << "Failed to read configuration file '" << config_path.value() |
| << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| auto config = base::MakeUnique<protos::ActiveDirectoryConfig>(); |
| if (!config->ParseFromString(config_blob)) { |
| LOG(ERROR) << "Failed to parse configuration from string"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| // Check if the config is valid. |
| if (config->machine_name().empty() || config->realm().empty()) { |
| LOG(ERROR) << "Configuration is invalid"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| config_ = std::move(config); |
| LOG(INFO) << "Read configuration file '" << config_path.value() << "'"; |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::SecureMachineKeyTab() const { |
| // 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 temp_kt_fp(paths_->Get(Path::MACHINE_KT_TEMP)); |
| const base::FilePath state_kt_fp(paths_->Get(Path::MACHINE_KT_STATE)); |
| ErrorType error; |
| |
| // Set group read permissions on keytab as authpolicyd-exec, so we can copy it |
| // as authpolicyd (and own the copy). |
| { |
| ScopedSwitchToSavedUid switch_scope; |
| error = SetFilePermissions(temp_kt_fp, kFileMode_rwr); |
| if (error != ERROR_NONE) |
| return error; |
| } |
| |
| // 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() << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| // Revoke 'read by others' permission. We could also just copy temp_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. |
| error = SetFilePermissions(state_kt_fp, kFileMode_rwr); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Now we may copy the file. The copy is owned by authpolicyd:authpolicyd. |
| if (!base::CopyFile(temp_kt_fp, state_kt_fp)) { |
| PLOG(ERROR) << "Failed to copy file '" << temp_kt_fp.value() << "' to '" |
| << state_kt_fp.value() << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| // Clean up temp file (must be done as authpolicyd-exec). |
| { |
| ScopedSwitchToSavedUid switch_scope; |
| if (!base::DeleteFile(temp_kt_fp, false)) { |
| LOG(ERROR) << "Failed to delete file '" << temp_kt_fp.value() << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| } |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::GetAccountInfo( |
| const std::string& user_name, |
| const std::string& normalized_upn, |
| const std::string& account_id, |
| const protos::RealmInfo& realm_info, |
| ActiveDirectoryAccountInfo* account_info) { |
| // Refresh the device TGT. Note that the user TGT might not be accessible at |
| // this point (we need the sAMAccountName returned in |account_info| to fetch |
| // the user TGT). |
| std::string machine_principal = |
| config_->machine_name() + "$@" + config_->realm(); |
| ErrorType error = |
| device_tgt_manager_.AcquireTgtWithKeytab(machine_principal, |
| Path::MACHINE_KT_STATE, |
| false, |
| config_->realm(), |
| realm_info.kdc_ip()); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // If |account_id| is provided, search by objectGUID only. |
| if (!account_id.empty()) { |
| // Searching by objectGUID has to use the octet string representation! |
| // Note: If |account_id| is malformed, the search yields no results. |
| const std::string account_id_octet = GuidToOctetString(account_id); |
| std::string search_string = |
| base::StringPrintf("(objectGUID=%s)", account_id_octet.c_str()); |
| return SearchAccountInfo(search_string, account_info); |
| } |
| |
| // Otherwise, search by sAMAccountName, then by userPrincipalName. |
| std::string search_string = |
| base::StringPrintf("(sAMAccountName=%s)", user_name.c_str()); |
| error = SearchAccountInfo(search_string, account_info); |
| if (error != ERROR_BAD_USER_NAME) // ERROR_BAD_USER_NAME means there were |
| return error; // no search results. |
| |
| LOG(WARNING) << "Account info not found by sAMAccountName. " |
| << "Trying userPrincipalName."; |
| search_string = |
| base::StringPrintf("(userPrincipalName=%s)", normalized_upn.c_str()); |
| return SearchAccountInfo(search_string, account_info); |
| } |
| |
| ErrorType SambaInterface::SearchAccountInfo( |
| const std::string& search_string, |
| ActiveDirectoryAccountInfo* account_info) { |
| // Call net ads search to find the user's account info. |
| ProcessExecutor net_cmd({paths_->Get(Path::NET), |
| "ads", |
| "search", |
| search_string, |
| kSearchObjectGUID, |
| kSearchSAMAccountName, |
| kSearchDisplayName, |
| kSearchGivenName, |
| "-s", |
| paths_->Get(Path::SMB_CONF), |
| "-d", |
| kNetLogLevel}); |
| // Use the machine TGT to query the account info. |
| net_cmd.SetEnv(kKrb5CCEnvKey, |
| paths_->Get(device_tgt_manager_.GetCredentialCachePath())); |
| if (!jail_helper_.SetupJailAndRun( |
| &net_cmd, Path::NET_ADS_SECCOMP, TIMER_NET_ADS_SEARCH)) { |
| return GetNetError(net_cmd, "search"); |
| } |
| const std::string& net_out = net_cmd.GetStdout(); |
| |
| // Parse the output to find the account info proto blob. Enclose in a sandbox |
| // for security considerations. |
| ProcessExecutor parse_cmd({paths_->Get(Path::PARSER), kCmdParseAccountInfo}); |
| parse_cmd.SetInputString(net_out); |
| if (!jail_helper_.SetupJailAndRun( |
| &parse_cmd, Path::PARSER_SECCOMP, TIMER_NONE)) { |
| LOG(ERROR) << "Failed to get user account id. Net response: " << net_out; |
| return ERROR_PARSE_FAILED; |
| } |
| const std::string& account_info_blob = parse_cmd.GetStdout(); |
| |
| // Parse account info protobuf. |
| if (account_info_blob.empty()) { |
| // No search results. Return ERROR_BAD_USER_NAME since it usually means that |
| // the user mistyped his user name. |
| LOG(WARNING) << "Search yielded no results"; |
| return ERROR_BAD_USER_NAME; |
| } else if (!account_info->ParseFromString(account_info_blob)) { |
| LOG(ERROR) << "Failed to parse account info protobuf"; |
| return ERROR_PARSE_FAILED; |
| } |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::GetGpoList(const std::string& user_or_machine_name, |
| PolicyScope scope, |
| protos::GpoList* gpo_list) const { |
| DCHECK(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({paths_->Get(Path::NET), |
| "ads", |
| "gpo", |
| "list", |
| user_or_machine_name, |
| "-s", |
| paths_->Get(Path::SMB_CONF), |
| "-d", |
| kNetLogLevel}); |
| const TgtManager& tgt_manager = |
| scope == PolicyScope::USER ? user_tgt_manager_ : device_tgt_manager_; |
| net_cmd.SetEnv(kKrb5CCEnvKey, |
| paths_->Get(tgt_manager.GetCredentialCachePath())); |
| if (!jail_helper_.SetupJailAndRun( |
| &net_cmd, Path::NET_ADS_SECCOMP, TIMER_NET_ADS_GPO_LIST)) { |
| return GetNetError(net_cmd, "gpo list"); |
| } |
| |
| // 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 == PolicyScope::USER ? kCmdParseUserGpoList |
| : kCmdParseDeviceGpoList; |
| ProcessExecutor parse_cmd({paths_->Get(Path::PARSER), cmd}); |
| parse_cmd.SetInputString(net_out); |
| if (!jail_helper_.SetupJailAndRun( |
| &parse_cmd, Path::PARSER_SECCOMP, TIMER_NONE)) { |
| LOG(ERROR) << "Failed to parse GPO list"; |
| return ERROR_PARSE_FAILED; |
| } |
| std::string gpo_list_blob = parse_cmd.GetStdout(); |
| |
| // Parse GPO list protobuf. |
| if (!gpo_list->ParseFromString(gpo_list_blob)) { |
| LOG(ERROR) << "Failed to read GPO list protobuf"; |
| return ERROR_PARSE_FAILED; |
| } |
| |
| return ERROR_NONE; |
| } |
| |
| 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) {} |
| }; |
| |
| ErrorType SambaInterface::DownloadGpos( |
| const protos::GpoList& gpo_list, |
| const std::string& domain_controller_name, |
| PolicyScope scope, |
| std::vector<base::FilePath>* gpo_file_paths) const { |
| metrics_->Report(METRIC_DOWNLOAD_GPO_COUNT, gpo_list.entries_size()); |
| if (gpo_list.entries_size() == 0) { |
| LOG(INFO) << "No GPOs to download"; |
| return ERROR_NONE; |
| } |
| |
| // Generate all smb source and linux target directories and create targets. |
| ErrorType error; |
| std::string smb_command = "prompt OFF;lowercase ON;"; |
| std::string gpo_basepath; |
| std::vector<GpoPaths> gpo_paths; |
| for (int entry_idx = 0; entry_idx < gpo_list.entries_size(); ++entry_idx) { |
| const protos::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 ';'"; |
| return ERROR_BAD_GPOS; |
| } |
| |
| // 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() << "'"; |
| return ERROR_BAD_GPOS; |
| } |
| |
| // Figure out local (Linux) and remote (smb) directories. |
| const char* preg_dir = |
| scope == PolicyScope::USER ? kPRegUserDir : kPRegDeviceDir; |
| std::string smb_dir = |
| base::StringPrintf("\\%s\\%s", gpo.directory().c_str(), preg_dir); |
| std::string linux_dir = paths_->Get(Path::GPO_LOCAL_DIR) + smb_dir; |
| std::replace(linux_dir.begin(), linux_dir.end(), '\\', '/'); |
| |
| // Make local directory. |
| const base::FilePath linux_dir_fp(linux_dir); |
| error = ::authpolicy::CreateDirectory(linux_dir_fp); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Set group rwx permissions recursively, so that smbclient can write GPOs |
| // there and the parser tool can read the GPOs later. |
| error = SetFilePermissionsRecursive( |
| linux_dir_fp, |
| base::FilePath(paths_->Get(Path::SAMBA_DIR)), |
| kFileMode_rwxrwx); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Build command for smbclient. |
| smb_command += base::StringPrintf("cd %s;lcd %s;get %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 ERROR_LOCAL_IO; |
| } |
| } |
| |
| const std::string service = base::StringPrintf( |
| "//%s.%s", domain_controller_name.c_str(), gpo_basepath.c_str()); |
| |
| // Download GPO into local directory. Retry a couple of times in case of |
| // network errors, Kerberos authentication may be flaky in some deployments, |
| // see crbug.com/684733. |
| ProcessExecutor smb_client_cmd({paths_->Get(Path::SMBCLIENT), |
| service, |
| "-s", |
| paths_->Get(Path::SMB_CONF), |
| "-k", |
| "-c", |
| smb_command}); |
| const TgtManager& tgt_manager = |
| scope == PolicyScope::USER ? user_tgt_manager_ : device_tgt_manager_; |
| smb_client_cmd.SetEnv(kKrb5CCEnvKey, |
| paths_->Get(tgt_manager.GetCredentialCachePath())); |
| smb_client_cmd.SetEnv(kKrb5ConfEnvKey, // Kerberos configuration file path. |
| kFilePrefix + paths_->Get(tgt_manager.GetConfigPath())); |
| bool success = false; |
| int tries, failed_tries = 0; |
| for (tries = 1; tries <= kSmbClientMaxTries; ++tries) { |
| if (tries > 1 && smbclient_retry_sleep_enabled_) { |
| base::PlatformThread::Sleep( |
| base::TimeDelta::FromSeconds(kSmbClientRetryWaitSeconds)); |
| } |
| success = jail_helper_.SetupJailAndRun( |
| &smb_client_cmd, Path::SMBCLIENT_SECCOMP, TIMER_SMBCLIENT); |
| if (success) { |
| error = ERROR_NONE; |
| break; |
| } |
| failed_tries++; |
| error = GetSmbclientError(smb_client_cmd); |
| if (error != ERROR_NETWORK_PROBLEM) |
| break; |
| } |
| metrics_->Report(METRIC_SMBCLIENT_FAILED_TRY_COUNT, failed_tries); |
| |
| if (!success) { |
| // The exit code of smbclient corresponds to the LAST command issued. Thus, |
| // Execute() might fail if the last GPO file is missing, which creates an |
| // ERROR_SMBCLIENT_FAILED error code. However, we handle this below (not an |
| // error), so only error out here on other error codes. |
| if (error != ERROR_SMBCLIENT_FAILED) |
| return error; |
| error = ERROR_NONE; |
| } |
| |
| // 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(gpo_file_paths); |
| for (const GpoPaths& gpo_path : gpo_paths) { |
| if (base::PathExists(gpo_path.local_)) { |
| 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(kKeyObjectNameNotFound + 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() << "'"; |
| return ERROR_SMBCLIENT_FAILED; |
| } |
| } |
| } |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::ParseGposIntoProtobuf( |
| const std::vector<base::FilePath>& gpo_file_paths, |
| const char* parser_cmd_string, |
| std::string* policy_blob) const { |
| // Convert file paths to proto blob. |
| std::string gpo_file_paths_blob; |
| protos::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"; |
| return ERROR_PARSE_PREG_FAILED; |
| } |
| |
| // Load GPOs into protobuf. Enclose in a sandbox for security considerations. |
| ProcessExecutor parse_cmd({paths_->Get(Path::PARSER), parser_cmd_string}); |
| parse_cmd.SetInputString(gpo_file_paths_blob); |
| if (!jail_helper_.SetupJailAndRun( |
| &parse_cmd, Path::PARSER_SECCOMP, TIMER_NONE)) { |
| LOG(ERROR) << "Failed to parse preg files"; |
| return ERROR_PARSE_PREG_FAILED; |
| } |
| *policy_blob = parse_cmd.GetStdout(); |
| return ERROR_NONE; |
| } |
| |
| void SambaInterface::Reset() { |
| user_id_name_map_.clear(); |
| config_.reset(); |
| workgroup_.clear(); |
| retry_machine_kinit_ = false; |
| } |
| |
| } // namespace authpolicy |