| // 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/files/important_file_writer.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 <base/threading/platform_thread.h> |
| #include <base/time/default_clock.h> |
| #include <base/time/time.h> |
| #include <policy/device_policy_impl.h> |
| |
| #include "authpolicy/anonymizer.h" |
| #include "authpolicy/cryptohome_client.h" |
| #include "authpolicy/log_colors.h" |
| #include "authpolicy/platform_helper.h" |
| #include "authpolicy/process_executor.h" |
| |
| namespace em = enterprise_management; |
| |
| namespace authpolicy { |
| namespace { |
| |
| // Samba configuration file data. |
| constexpr char kSmbConfData[] = |
| "[global]\n" |
| "\tnetbios name = %s\n" |
| "\tsecurity = ADS\n" |
| "\tworkgroup = %s\n" |
| "\trealm = %s\n" |
| "\tlock directory = %s\n" |
| "\tinclude system krb5 conf = false\n" |
| "\tcache directory = %s\n" |
| "\tstate directory = %s\n" |
| "\tprivate directory = %s\n" |
| "\tkerberos encryption types = %s\n" |
| "\tclient signing = mandatory\n" |
| "\tclient min protocol = SMB2\n" |
| // TODO(crbug.com/662440): Remove this line if it is no longer necessary. |
| // (Make sure that Samba doesn't default to some older version) |
| "\tclient max protocol = SMB3\n" |
| "\tclient ipc min protocol = SMB2\n" |
| "\tclient ldap sasl wrapping = sign\n"; |
| |
| // Fake domain SID to work around issue in Samba-4.8.6+, see |
| // `MaybeSetFakeDomainSid()`. |
| constexpr char kFakeDomainSid[] = "S-1-5-21-0000000000-0000000000-00000000"; |
| |
| constexpr int kFileMode_rwr = base::FILE_PERMISSION_READ_BY_USER | |
| base::FILE_PERMISSION_WRITE_BY_USER | |
| base::FILE_PERMISSION_READ_BY_GROUP; |
| |
| constexpr int kFileMode_rwxrx = kFileMode_rwr | |
| base::FILE_PERMISSION_EXECUTE_BY_USER | |
| base::FILE_PERMISSION_EXECUTE_BY_GROUP; |
| |
| constexpr 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 struct CreateDirectories { |
| Path path; |
| int mode; |
| bool owned_by_authpolicyd_exec; |
| } kDirsToCreate[] = {{Path::TEMP_DIR, kFileMode_rwxrx, false}, |
| {Path::SAMBA_DIR, kFileMode_rwxrwx, false}, |
| {Path::SAMBA_LOCK_DIR, kFileMode_rwxrwx, true}, |
| {Path::SAMBA_CACHE_DIR, kFileMode_rwxrwx, true}, |
| {Path::SAMBA_STATE_DIR, kFileMode_rwxrwx, true}, |
| {Path::SAMBA_PRIVATE_DIR, kFileMode_rwxrwx, true}}; |
| |
| // Directory / filenames for user and device policy. |
| constexpr char kPRegUserDir[] = "User"; |
| constexpr char kPRegDeviceDir[] = "Machine"; |
| constexpr char kPRegFileName[] = "registry.pol"; |
| |
| // Size limit when loading the config file (256 kb). |
| constexpr size_t kConfigSizeLimit = 256 * 1024; |
| |
| // SessionStateChanged signal payload we care about. |
| constexpr char kSessionStarted[] = "started"; |
| |
| // Maximum smbclient tries. |
| constexpr int kSmbClientMaxTries = 5; |
| // Wait interval between two smbclient tries. |
| constexpr base::TimeDelta kSmbClientRetryDelay = |
| base::TimeDelta::FromSeconds(1); |
| |
| // Check every 120 minutes whether the machine password has to be changed. |
| constexpr base::TimeDelta kPasswordChangeCheckRate = |
| base::TimeDelta::FromMinutes(120); |
| |
| // Default GPO version cache TTL. Can be overridden with the |
| // DeviceGpoCacheLifetime policy. Make sure the value matches the policy |
| // description in policy_templates.json! |
| constexpr base::TimeDelta kDefaultGpoVersionCacheTTL = |
| base::TimeDelta::FromHours(25); |
| |
| // Default auth data cache TTL. Can be overridden with the |
| // DeviceAuthDataCacheLifetime policy. Make sure the value matches the policy |
| // description in policy_templates.json! |
| constexpr base::TimeDelta kDefaultAuthDataCacheTTL = |
| base::TimeDelta::FromHours(73); |
| |
| constexpr base::TimeDelta kZeroDelta = base::TimeDelta::FromHours(0); |
| |
| // Keys for interpreting net output. |
| constexpr char kKeyJoinAccessDenied[] = "NT_STATUS_ACCESS_DENIED"; |
| constexpr char kKeyJoinAccessDenied2[] = |
| "Failed to join domain: failed to set machine os attributes: Insufficient " |
| "access"; |
| constexpr char kKeyInvalidMachineName[] = "Improperly formed account name"; |
| constexpr char kKeyInvalidMachineName2[] = |
| "The name provided is not a properly formed account name"; |
| constexpr char kKeyMachineNameTooLong[] = "Our netbios name can be at most"; |
| constexpr char kKeyUserHitJoinQuota[] = |
| "Insufficient quota exists to complete the operation"; |
| constexpr char kKeyJoinFailedToFindDC[] = "failed to find DC"; |
| constexpr char kKeyNoLogonServers[] = "No logon servers"; |
| constexpr char kKeyJoinLogonFailure[] = "Logon failure"; |
| constexpr char kKeyJoinLogonFailure2[] = "The attempted logon is invalid"; |
| constexpr char kKeyJoinMustChangePassword[] = "Must change password"; |
| constexpr char kKeyJoinMustChangePassword2[] = "password must be changed"; |
| // Setting OU during domain join failed. More specific errors below. |
| constexpr char kKeyBadOuCommon[] = "failed to precreate account in ou"; |
| // The domain join createcomputer argument specified a non-existent OU. |
| constexpr char kKeyBadOuNoSuchObject[] = "No such object"; |
| // The domain join createcomputer argument syntax was invalid. Caused by some |
| // special characters in OU names, e.g. 'ou=123!' or 'a"b'. Seems like a Samba |
| // issue since OUs allow all characters and we do escape names properly. |
| constexpr char kKeyBadOuInvalidDnSyntax[] = "Invalid DN syntax"; |
| // Domain join operation would have violated an attribute constraint. |
| constexpr char kKeyBadOuConstrainViolation[] = "Constraint violation"; |
| // Domain join required access permissions that the user does not possess. |
| constexpr char kKeyBadOuInsufficientAccess[] = "Insufficient access"; |
| // All other OU errors result in a generic ERROR_SETTING_OU_FAILED, e.g. |
| // - "Referral": dc=... specification resulted in a referral to another server. |
| // - "Operations error": Unspecific error. |
| // Keys for interpreting smbclient output. |
| constexpr char kKeyConnectionReset[] = "NT_STATUS_CONNECTION_RESET"; |
| constexpr char kKeyNetworkTimeout[] = "NT_STATUS_IO_TIMEOUT"; |
| constexpr char kKeyObjectNameNotFound[] = |
| "NT_STATUS_OBJECT_NAME_NOT_FOUND opening remote file "; |
| constexpr char kKeyEncTypeNotSupported[] = |
| "KDC has no support for encryption type"; |
| constexpr char kKeyEncTypeNotSupported2[] = |
| "The encryption type requested is not supported by the KDC"; |
| |
| // Replacement strings for anonymization. |
| constexpr char kMachineNamePlaceholder[] = "<MACHINE_NAME>"; |
| constexpr char kLogonNamePlaceholder[] = "<USER_LOGON_NAME>"; |
| constexpr char kGivenNamePlaceholder[] = "<USER_GIVEN_NAME>"; |
| constexpr char kDisplayNamePlaceholder[] = "<USER_DISPLAY_NAME>"; |
| constexpr char kSAMAccountNamePlaceholder[] = "<USER_SAM_ACCOUNT_NAME>"; |
| constexpr char kCommonNamePlaceholder[] = "<USER_COMMON_NAME>"; |
| constexpr char kAccountIdPlaceholder[] = "<USER_ACCOUNT_ID>"; |
| constexpr char kWorkgroupPlaceholder[] = "<WORKGROUP>"; |
| constexpr char kDeviceRealmPlaceholder[] = "<DEVICE_REALM>"; |
| constexpr char kUserRealmPlaceholder[] = "<USER_REALM>"; |
| constexpr char kForestPlaceholder[] = "<FOREST>"; |
| constexpr char kDomainPlaceholder[] = "<DOMAIN>"; |
| constexpr char kServerNamePlaceholder[] = "<SERVER_NAME>"; |
| constexpr char kSiteNamePlaceholder[] = "<SITE_NAME>"; |
| constexpr char kIpAddressPlaceholder[] = "<IP_ADDRESS>"; |
| constexpr char kPasswordPlaceholder[] = "<PASSWORD>"; |
| |
| // Keys for net ads searches. |
| constexpr char kKeyWorkgroup[] = "Workgroup"; |
| constexpr char kKeyAdsDnsParseRrSrv[] = "ads_dns_parse_rr_srv"; |
| constexpr char kKeyPdcDnsName[] = "pdc_dns_name"; |
| constexpr char kKeyAdsDcName[] = "ads_dc_name"; |
| constexpr char kKeyPdcName[] = "pdc_name"; |
| constexpr char kKeyServerSite[] = "server_site"; |
| constexpr char kKeyClientSite[] = "client_site"; |
| constexpr char kKeyForest[] = "Forest"; |
| constexpr char kKeyDomain[] = "Domain"; |
| constexpr char kKeyDomainController[] = "Domain Controller"; |
| constexpr char kKeyPreWin2kDomain[] = "Pre-Win2k Domain"; |
| constexpr char kKeyPreWin2kHostname[] = "Pre-Win2k Hostname"; |
| constexpr char kKeyServerSiteName[] = "Server Site Name"; |
| constexpr char kKeyClientSiteName[] = "Client Site Name"; |
| constexpr char kKeyKdcServer[] = "KDC server"; |
| constexpr char kKeyLdapServer[] = "LDAP server"; |
| constexpr char kKeyLdapServerName[] = "LDAP server name"; |
| |
| // Kerberos encryption types strings for smb.conf. |
| constexpr char kEncTypesAll[] = "all"; |
| constexpr char kEncTypesStrong[] = "strong"; |
| constexpr char kEncTypesLegacy[] = "legacy"; |
| |
| // Maximum time that logging through SetDefaultLogLevel() should stay enabled. |
| // The method is called through the authpolicy_debug crosh command. The time is |
| // limited so users don't have to remember to turn logging off. |
| // Keep in sync with description in crosh! |
| constexpr int kMaxDefaultLogLevelUptimeMinutes = 30; |
| |
| // Auth state backup filename in user daemon store. |
| constexpr char kBackupFileName[] = "user_backup_data"; |
| constexpr int kMaxBackupSizeBytes = 4 * 1024 * 1024; |
| |
| WARN_UNUSED_RESULT 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) || |
| Contains(net_out, kKeyJoinLogonFailure2)) { |
| LOG(ERROR) << error_msg << "logon failure"; |
| return ERROR_BAD_PASSWORD; |
| } |
| if (Contains(net_out, kKeyJoinMustChangePassword) || |
| Contains(net_out, kKeyJoinMustChangePassword2)) { |
| LOG(ERROR) << error_msg << "must change password"; |
| return ERROR_PASSWORD_EXPIRED; |
| } |
| if (Contains(net_out, kKeyJoinAccessDenied) || |
| Contains(net_out, kKeyJoinAccessDenied2)) { |
| LOG(ERROR) << error_msg << "user is not permitted to join the domain"; |
| return ERROR_JOIN_ACCESS_DENIED; |
| } |
| if (Contains(net_out, kKeyInvalidMachineName) || |
| Contains(net_out, kKeyInvalidMachineName2)) { |
| 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; |
| } |
| if (Contains(net_out, kKeyBadOuCommon)) { |
| if (Contains(net_out, kKeyBadOuNoSuchObject)) { |
| LOG(ERROR) << error_msg << "computer OU does not exist"; |
| return ERROR_OU_DOES_NOT_EXIST; |
| } |
| if (Contains(net_out, kKeyBadOuInvalidDnSyntax)) { |
| LOG(ERROR) << error_msg << "computer OU invalid"; |
| return ERROR_INVALID_OU; |
| } |
| if (Contains(net_out, kKeyBadOuConstrainViolation)) { |
| LOG(ERROR) << error_msg << "constraint violation setting computer OU"; |
| return ERROR_OU_CONSTRAINT_VIOLATION; |
| } |
| if (Contains(net_out, kKeyBadOuInsufficientAccess)) { |
| LOG(ERROR) << error_msg << "access denied setting computer OU"; |
| return ERROR_OU_ACCESS_DENIED; |
| } |
| // Fall back to generic OU error. |
| LOG(ERROR) << error_msg << "setting computer OU failed, unspecified error"; |
| return ERROR_SETTING_OU_FAILED; |
| } |
| if (Contains(net_out, kKeyEncTypeNotSupported) || |
| Contains(net_out, kKeyEncTypeNotSupported2)) { |
| LOG(ERROR) << error_msg << "KDC does not support encryption type"; |
| return ERROR_KDC_DOES_NOT_SUPPORT_ENCRYPTION_TYPE; |
| } |
| LOG(ERROR) << error_msg << "exit code " << executor.GetExitCode(); |
| return ERROR_NET_FAILED; |
| } |
| |
| WARN_UNUSED_RESULT 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. |
| WARN_UNUSED_RESULT 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. |
| WARN_UNUSED_RESULT 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|. |
| WARN_UNUSED_RESULT 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; |
| } |
| |
| // Checks whether the file at |default_level_path| exists and was last modified |
| // in a certain time range. If not, it is deleted to prevent that a user forgets |
| // to disable logging. |
| bool CheckFlagsDefaultLevelValid(const base::FilePath& default_level_path) { |
| // Having no file is the out-of-box state with no level set, so exit quietly. |
| if (!base::PathExists(default_level_path)) |
| return false; |
| |
| base::File::Info info; |
| if (!GetFileInfo(default_level_path, &info)) { |
| PLOG(ERROR) << "Failed to get file info from '" |
| << default_level_path.value() << "'"; |
| return false; |
| } |
| |
| // Check < -1 to prevent issues with clocks running backwards for a bit. |
| int uptime_min = (base::Time::Now() - info.last_modified).InMinutes(); |
| if (uptime_min < -1 || uptime_min > kMaxDefaultLogLevelUptimeMinutes) { |
| LOG(INFO) << "Removing flags default level file and resetting (uptime: " |
| << uptime_min << " minutes)."; |
| PCHECK(base::DeleteFile(default_level_path, false /* recursive */)) |
| << "Failed to delete flags default level file '" |
| << default_level_path.value() << "'"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Parses |gpo_policy_data| from |gpo_policy_data_blob|. Returns ERROR_NONE on |
| // success. Returns ERROR_PARSE_FAILED and prints an error on failure. |
| WARN_UNUSED_RESULT ErrorType |
| ParsePolicyData(const std::string& gpo_policy_data_blob, |
| protos::GpoPolicyData* gpo_policy_data) { |
| if (!gpo_policy_data->ParseFromString(gpo_policy_data_blob)) { |
| LOG(ERROR) << "Failed to parse policy data from string"; |
| return ERROR_PARSE_FAILED; |
| } |
| return ERROR_NONE; |
| } |
| |
| // Returns the string representation of |encryption_types| for smb.conf. |
| const char* GetEncryptionTypesString(KerberosEncryptionTypes encryption_types) { |
| switch (encryption_types) { |
| case ENC_TYPES_ALL: |
| return kEncTypesAll; |
| case ENC_TYPES_STRONG: |
| return kEncTypesStrong; |
| case ENC_TYPES_LEGACY: |
| return kEncTypesLegacy; |
| case ENC_TYPES_COUNT: |
| NOTREACHED() << "Not a valid encryption type and will default to strong."; |
| return kEncTypesStrong; |
| } |
| CHECK(false); |
| } |
| |
| // Returns the value of the DeviceKerberosEncryptionTypes policy or |
| // ENC_TYPES_STRONG if the policy is not set or invalid. |
| KerberosEncryptionTypes GetEncryptionTypes( |
| const em::ChromeDeviceSettingsProto& device_policy) { |
| if (!device_policy.has_device_kerberos_encryption_types() || |
| !device_policy.device_kerberos_encryption_types().has_types()) { |
| return ENC_TYPES_STRONG; |
| } |
| |
| em::DeviceKerberosEncryptionTypesProto::Types policy_encryption_types = |
| device_policy.device_kerberos_encryption_types().types(); |
| |
| switch (policy_encryption_types) { |
| case em::DeviceKerberosEncryptionTypesProto::ENC_TYPES_ALL: |
| return ENC_TYPES_ALL; |
| case em::DeviceKerberosEncryptionTypesProto::ENC_TYPES_STRONG: |
| return ENC_TYPES_STRONG; |
| case em::DeviceKerberosEncryptionTypesProto::ENC_TYPES_LEGACY: |
| return ENC_TYPES_LEGACY; |
| } |
| |
| CHECK(false); |
| } |
| |
| // Returns the value of the DeviceUserPolicyLoopbackProcessingMode policy or |
| // |USER_POLICY_MODE_DEFAULT| if the policy is not. |
| em::DeviceUserPolicyLoopbackProcessingModeProto::Mode GetUserPolicyMode( |
| const em::ChromeDeviceSettingsProto& device_policy) { |
| if (!device_policy.has_device_user_policy_loopback_processing_mode() || |
| !device_policy.device_user_policy_loopback_processing_mode().has_mode()) { |
| return em::DeviceUserPolicyLoopbackProcessingModeProto:: |
| USER_POLICY_MODE_DEFAULT; |
| } |
| return device_policy.device_user_policy_loopback_processing_mode().mode(); |
| } |
| |
| // Returns the value of the DeviceMachinePasswordChangeRate policy or |
| // |kDefaultMachinePasswordChange| if the policy is not set. |
| base::TimeDelta GetMachinePasswordChangeRate( |
| const em::ChromeDeviceSettingsProto& device_policy) { |
| if (!device_policy.has_device_machine_password_change_rate() || |
| !device_policy.device_machine_password_change_rate().has_rate_days()) { |
| return kDefaultMachinePasswordChangeRate; |
| } |
| return base::TimeDelta::FromDays( |
| device_policy.device_machine_password_change_rate().rate_days()); |
| } |
| |
| // Returns the value of the DeviceGpoCacheLifetime policy or |
| // |kDefaultGpoVersionCacheTTL| if the policy is not set. |
| base::TimeDelta GetGpoVersionCacheTTL( |
| const em::ChromeDeviceSettingsProto& device_policy) { |
| if (!device_policy.has_device_gpo_cache_lifetime() || |
| !device_policy.device_gpo_cache_lifetime().has_lifetime_hours()) { |
| return kDefaultGpoVersionCacheTTL; |
| } |
| return base::TimeDelta::FromHours( |
| device_policy.device_gpo_cache_lifetime().lifetime_hours()); |
| } |
| |
| // Returns the value of the DeviceAuthDataCacheLifetime policy or |
| // |kDefaultAuthDataCacheTTL| if the policy is not set. |
| base::TimeDelta GetAuthDataCacheTTL( |
| const em::ChromeDeviceSettingsProto& device_policy) { |
| if (!device_policy.has_device_auth_data_cache_lifetime() || |
| !device_policy.device_auth_data_cache_lifetime().has_lifetime_hours()) { |
| return kDefaultAuthDataCacheTTL; |
| } |
| return base::TimeDelta::FromHours( |
| device_policy.device_auth_data_cache_lifetime().lifetime_hours()); |
| } |
| |
| std::ostream& operator<<(std::ostream& os, |
| const ActiveDirectoryUserStatus::TgtStatus& status) { |
| switch (status) { |
| case ActiveDirectoryUserStatus::TGT_VALID: |
| return os << "valid"; |
| case ActiveDirectoryUserStatus::TGT_EXPIRED: |
| return os << "expired"; |
| case ActiveDirectoryUserStatus::TGT_NOT_FOUND: |
| return os << "not found"; |
| } |
| NOTREACHED(); |
| return os << "unknown"; |
| } |
| |
| std::ostream& operator<<( |
| std::ostream& os, const ActiveDirectoryUserStatus::PasswordStatus& status) { |
| switch (status) { |
| case ActiveDirectoryUserStatus::PASSWORD_VALID: |
| return os << "valid"; |
| case ActiveDirectoryUserStatus::PASSWORD_EXPIRED: |
| return os << "expired"; |
| case ActiveDirectoryUserStatus::PASSWORD_CHANGED: |
| return os << "changed"; |
| } |
| NOTREACHED(); |
| return os << "unknown"; |
| } |
| |
| // Helper to log |status| if the |log_status| debug flag is enabled. |
| void LogUserStatus(const ActiveDirectoryUserStatus& status, |
| const protos::DebugFlags& flags) { |
| if (!flags.log_status()) |
| return; |
| |
| LOG(INFO) << kColorStatus << "User Status:" << kColorReset; |
| LOG(INFO) << kColorStatus << " TGT: " << status.tgt_status() |
| << kColorReset; |
| LOG(INFO) << kColorStatus |
| << " Password: " << status.password_status() |
| << kColorReset; |
| LOG(INFO) << kColorStatus << " Password Last Set: " |
| << status.account_info().pwd_last_set() << kColorReset; |
| LOG(INFO) << kColorStatus << " User Account Control: " |
| << status.account_info().user_account_control() << kColorReset; |
| // Note: Don't log the other account info data, it's all PII. |
| } |
| |
| // Logs an error in case of failure. Returns true on success. |
| bool ReadMachinePasswordToString(const base::FilePath& password_path, |
| std::string* password) { |
| if (!base::ReadFileToString(password_path, password)) { |
| PLOG(ERROR) << "Could not read machine password file '" |
| << password_path.value() << "'"; |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| SambaInterface::SambaInterface(AuthPolicyMetrics* metrics, |
| const PathService* path_service, |
| const base::Closure& user_kerberos_files_changed) |
| : user_account_(Path::USER_SMB_CONF), |
| device_account_(Path::DEVICE_SMB_CONF), |
| metrics_(metrics), |
| paths_(path_service), |
| anonymizer_(std::make_unique<Anonymizer>()), |
| jail_helper_(paths_, &flags_, anonymizer_.get()), |
| user_tgt_manager_(paths_, |
| metrics_, |
| &flags_, |
| &jail_helper_, |
| anonymizer_.get(), |
| this /* TgtManager::Delegate */, |
| Path::USER_KRB5_CONF, |
| Path::USER_CREDENTIAL_CACHE), |
| device_tgt_manager_(paths_, |
| metrics_, |
| &flags_, |
| &jail_helper_, |
| anonymizer_.get(), |
| this /* TgtManager::Delegate */, |
| Path::DEVICE_KRB5_CONF, |
| Path::DEVICE_CREDENTIAL_CACHE), |
| gpo_version_cache_(&flags_), |
| gpo_version_cache_ttl_(kDefaultGpoVersionCacheTTL), |
| auth_data_cache_(&flags_), |
| auth_data_cache_ttl_(kDefaultAuthDataCacheTTL) { |
| DCHECK(paths_); |
| LoadFlagsDefaultLevel(); |
| user_tgt_manager_.SetKerberosFilesChangedCallback( |
| user_kerberos_files_changed); |
| } |
| |
| SambaInterface::~SambaInterface() = default; |
| |
| ErrorType SambaInterface::Initialize(bool expect_config) { |
| ReloadDebugFlags(); |
| |
| ErrorType error = ERROR_NONE; |
| { |
| // Note: From 4.8.0 on Samba performs a strict ownership check on some |
| // directories, so they have to be owned by authpolicyd-exec. |
| for (const auto& dir : kDirsToCreate) { |
| std::unique_ptr<ScopedSwitchToSavedUid> switch_scope; |
| if (dir.owned_by_authpolicyd_exec) |
| switch_scope = std::make_unique<ScopedSwitchToSavedUid>(); |
| const base::FilePath path(paths_->Get(dir.path)); |
| error = ::authpolicy::CreateDirectory(path); |
| if (error != ERROR_NONE) |
| return error; |
| error = SetFilePermissions(path, dir.mode); |
| if (error != ERROR_NONE) |
| return error; |
| } |
| } |
| |
| if (expect_config) { |
| error = ReadConfiguration(); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Load cached auth data. It's OK if that fails, just start with an empty |
| // cache. |
| // NOTE: Load cache before UpdateDevicePolicyDependencies() as that may |
| // modify the cache! |
| base::FilePath cache_path(paths_->Get(Path::AUTH_DATA_CACHE)); |
| if (base::PathExists(cache_path)) |
| auth_data_cache_.Load(cache_path); |
| |
| // Load device policy and update stuff that depends on device policy. If |
| // there's a config, it means the device is locked and there should also be |
| // device policy at this point. |
| std::unique_ptr<policy::DevicePolicyImpl> policy_impl = |
| std::move(device_policy_impl_for_testing); |
| if (!policy_impl) |
| policy_impl = std::make_unique<policy::DevicePolicyImpl>(); |
| if (!policy_impl->LoadPolicy()) { |
| LOG(ERROR) << "Failed to load device policy. Authentication and policy " |
| "fetch might behave unexpectedly until the next device " |
| "policy fetch."; |
| } |
| |
| // Call this even when loading failed to get the defaults right (e.g. |
| // turn on machine password auto renewal). |
| UpdateDevicePolicyDependencies(policy_impl->get_device_policy()); |
| } |
| |
| return ERROR_NONE; |
| } |
| |
| void SambaInterface::SetCryptohomeClient( |
| std::unique_ptr<CryptohomeClient> cryptohome_client) { |
| cryptohome_client_ = std::move(cryptohome_client); |
| } |
| |
| // 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); |
| const 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) { |
| ReloadDebugFlags(); |
| |
| ErrorType error = AuthenticateUserInternal(user_principal_name, account_id, |
| password_fd, account_info); |
| |
| last_auth_error_ = error; |
| return error; |
| } |
| |
| ErrorType SambaInterface::AuthenticateUserInternal( |
| const std::string& user_principal_name, |
| const std::string& account_id, |
| int password_fd, |
| ActiveDirectoryAccountInfo* account_info) { |
| if (!account_id.empty()) |
| SetUserAccountId(account_id); |
| |
| // We technically don't have to be in joined state, but check it anyway, |
| // because the device should always be joined during auth. |
| if (!IsDeviceJoined()) |
| return ERROR_NOT_JOINED; |
| |
| // Split user_principal_name into parts and normalize. |
| std::string user_name, user_realm, normalized_upn; |
| if (!ParseUserPrincipalName(user_principal_name, &user_name, &user_realm, |
| &normalized_upn)) { |
| return ERROR_PARSE_UPN_FAILED; |
| } |
| SetUserRealm(user_realm); |
| user_tgt_manager_.SetPrincipal(normalized_upn); |
| |
| // Clean up auth data cache. |
| auth_data_cache_.RemoveEntriesOlderThan(auth_data_cache_ttl_); |
| |
| // Acquire Kerberos ticket-granting-ticket for the user account. |
| ErrorType error = AcquireUserTgt(password_fd); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Get account info for the user. |
| error = GetAccountInfo(user_name, normalized_upn, account_id, account_info); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Renew TGT periodically. The usual validity lifetime is 1 day, so this won't |
| // happen too often. There's a corner-case if pwdLastSet or userAccountControl |
| // are missing, see crbug.com/795758. In that case, GetUserStatus cannot |
| // determine the password validity and just *assumes* it's valid. However, the |
| // AD admin might have requested the user to change their password. To limit |
| // the impact, don't renew the TGT automatically, so that the user will be |
| // prompted to relog after 1 day instead of the renewal lifetime of usually 1 |
| // week. |
| bool should_auto_renew = account_info->has_pwd_last_set() && |
| account_info->has_user_account_control(); |
| LOG_IF(WARNING, !should_auto_renew) |
| << "pwdLastSet or userAccountControl fields missing. Will not " |
| "be able to determine password validity. Turning off TGT " |
| "renewal to limit lifetime."; |
| user_tgt_manager_.EnableTgtAutoRenewal(should_auto_renew); |
| |
| if (account_id.empty()) |
| SetUserAccountId(account_info->account_id()); |
| |
| // Store sAMAccountName for policy fetch. Note that net ads gpo list always |
| // wants the sAMAccountName. Also note that pwd_last_set is zero and stale |
| // at this point if AcquireTgtWithPassword() set a new password, but that's |
| // fine, the timestamp is updated in the next GetUserStatus() call. |
| user_account_.user_name = account_info->sam_account_name(); |
| if (account_info->has_pwd_last_set()) |
| user_pwd_last_set_ = account_info->pwd_last_set(); |
| user_logged_in_ = true; |
| |
| // Backup state on user's Cryptohome. |
| MaybeBackupUserAuthState(); |
| |
| // Collecting metrics with the encryption types used during this successful |
| // login. This value has been set through the DeviceKerberosEncryptionTypes |
| // policy. |
| metrics_->ReportEncryptionType(ENC_TYPES_OF_AUTHENTICATE_USER, |
| encryption_types_); |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::GetUserStatus( |
| const std::string& user_principal_name, |
| const std::string& account_id, |
| ActiveDirectoryUserStatus* user_status) { |
| ReloadDebugFlags(); |
| SetUserAccountId(account_id); |
| user_status->Clear(); |
| |
| // Try to restore TGT if it doesn't exist. The TGT is required for reading the |
| // account info below. |
| MaybeRestoreUserAuthState(); |
| |
| // We technically don't have to be in joined state, but check it anyway, |
| // because the device should always be joined during getting status. |
| if (!IsDeviceJoined()) |
| return ERROR_NOT_JOINED; |
| |
| // Split user_principal_name into parts and normalize. |
| std::string user_name, user_realm, normalized_upn; |
| if (!ParseUserPrincipalName(user_principal_name, &user_name, &user_realm, |
| &normalized_upn)) { |
| return ERROR_PARSE_UPN_FAILED; |
| } |
| SetUserRealm(user_realm); |
| |
| // Tell Chrome that the password expired in case the TGT is not valid and the |
| // GetUserPasswordStatus() call below doesn't happen. See crbug.com/849318. |
| if (last_auth_error_ == ERROR_PASSWORD_EXPIRED) { |
| user_status->set_password_status( |
| ActiveDirectoryUserStatus::PASSWORD_EXPIRED); |
| } |
| |
| // If authentication failed with bad password, but the session was still |
| // started and Cryptohome could be unmounted, it means that the logon password |
| // must have been a valid, old password and the password must have changed on |
| // the server. |
| if (last_auth_error_ == ERROR_BAD_PASSWORD) { |
| user_status->set_password_status( |
| ActiveDirectoryUserStatus::PASSWORD_CHANGED); |
| } |
| |
| // Determine the status of the TGT. |
| ActiveDirectoryUserStatus::TgtStatus tgt_status = |
| ActiveDirectoryUserStatus::TGT_VALID; |
| ErrorType error = GetUserTgtStatus(&tgt_status); |
| if (error != ERROR_NONE) { |
| LogUserStatus(*user_status, flags_); |
| return error; |
| } |
| user_status->set_tgt_status(tgt_status); |
| |
| // If we don't have a valid TGT, we can't GetAccountInfo() because that uses |
| // the TGT to authenticate. Thus, just return the TGT status and the last auth |
| // error. |
| if (tgt_status != ActiveDirectoryUserStatus::TGT_VALID) { |
| // Just try to ping the server here. Otherwise, Chrome shows a popup that |
| // the user has to relog in order to get a new TGT, but AuthenticateUser() |
| // fails if the server is unavailable and the popup is shown again. |
| // See crbug.com/844662. |
| LogUserStatus(*user_status, flags_); |
| return PingServer(&user_account_); |
| } |
| |
| // Update smb.conf, IPs, server names etc. for the user account. |
| error = UpdateAccountData(&user_account_); |
| if (error != ERROR_NONE) { |
| LogUserStatus(*user_status, flags_); |
| return error; |
| } |
| |
| // Get account info for the user. |
| ActiveDirectoryAccountInfo account_info; |
| error = |
| GetAccountInfo("" /* user_name unused */, "" /* normalized_upn unused */, |
| account_id, &account_info); |
| if (error != ERROR_NONE) { |
| LogUserStatus(*user_status, flags_); |
| return error; |
| } |
| *user_status->mutable_account_info() = account_info; |
| |
| // Determine the status of the password. |
| ActiveDirectoryUserStatus::PasswordStatus password_status = |
| GetUserPasswordStatus(account_info); |
| user_status->set_password_status(password_status); |
| |
| LogUserStatus(*user_status, flags_); |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::GetUserKerberosFiles(const std::string& account_id, |
| KerberosFiles* files) { |
| ReloadDebugFlags(); |
| SetUserAccountId(account_id); |
| |
| // Try to restore TGT, user_account_id_ etc. if it doesn't exist. |
| MaybeRestoreUserAuthState(); |
| |
| return user_tgt_manager_.GetKerberosFiles(files); |
| } |
| |
| ErrorType SambaInterface::JoinMachine( |
| const std::string& machine_name, |
| const std::string& machine_domain, |
| const std::vector<std::string>& machine_ou, |
| const std::string& user_principal_name, |
| KerberosEncryptionTypes encryption_types, |
| int password_fd, |
| std::string* joined_domain) { |
| ReloadDebugFlags(); |
| |
| // Prevent joining a second time for security reasons (a hacked Chrome might |
| // call this). |
| if (IsDeviceJoined()) |
| return ERROR_ALREADY_JOINED; |
| |
| // Split user_principal_name into parts and normalize. |
| std::string user_name, user_realm, normalized_upn; |
| if (!ParseUserPrincipalName(user_principal_name, &user_name, &user_realm, |
| &normalized_upn)) { |
| return ERROR_PARSE_UPN_FAILED; |
| } |
| AnonymizeRealm(user_realm, kUserRealmPlaceholder); |
| anonymizer_->SetReplacement(user_name, kSAMAccountNamePlaceholder); |
| |
| std::string join_realm; |
| if (!machine_domain.empty()) { |
| // Join machine to the given domain (note: realm and domain is the same). |
| join_realm = base::ToUpperASCII(machine_domain); |
| AnonymizeRealm(join_realm, kDeviceRealmPlaceholder); |
| } else { |
| // By default, join machine to the user's realm. |
| join_realm = user_realm; |
| } |
| |
| // The netbios name in smb.conf needs to be upper-case, but there is also |
| // Samba code that logs the machine name lower-case, so add both here. |
| anonymizer_->SetReplacementAllCases(machine_name, kMachineNamePlaceholder); |
| |
| // Wipe and (re-)create config. Note that all session data is wiped to make |
| // testing easier. |
| Reset(); |
| InitDeviceAccount(base::ToUpperASCII(machine_name), join_realm); |
| |
| // Note: Encryption types stay valid through the initial device policy fetch, |
| // which, if it succeeds, resets or updates the value. |
| SetKerberosEncryptionTypes(encryption_types); |
| |
| // Update smb.conf, IPs, server names etc. for the device account. |
| ErrorType error = UpdateAccountData(&device_account_); |
| 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", kUserParam, |
| normalized_upn, kKerberosParam, kConfigParam, |
| paths_->Get(Path::DEVICE_SMB_CONF), kDebugParam, |
| flags_.net_log_level(), kMachinepassStdinParam}); |
| if (!machine_ou.empty()) { |
| net_cmd.PushArg(kCreatecomputerParam + |
| BuildDistinguishedName(machine_ou, join_realm)); |
| } |
| const std::string os_name = GetOsName(); |
| const std::string os_version = GetOsVersion(); |
| if (!os_name.empty() && !os_version.empty()) { |
| // Both must be specified for the params to take effect. |
| net_cmd.PushArg(kOsNameParam + os_name); |
| net_cmd.PushArg(kOsVersionParam + os_version); |
| // Prevent Samba from setting "Samba x.x.x" here. |
| net_cmd.PushArg(kOsServicePackParam); |
| } |
| |
| // The machine password and the user password are read from stdin. |
| const std::string machine_pass = GenerateRandomMachinePassword(); |
| anonymizer_->SetReplacement(machine_pass, kPasswordPlaceholder); |
| base::ScopedFD passwords_pipe = |
| WriteStringAndPipeToPipe(machine_pass + "\n", password_fd); |
| net_cmd.SetInputFile(passwords_pipe.get()); |
| if (!jail_helper_.SetupJailAndRun(&net_cmd, Path::NET_ADS_SECCOMP, |
| TIMER_NET_ADS_JOIN)) { |
| Reset(); |
| return GetNetError(net_cmd, "join"); |
| } |
| |
| // Store the machine password. |
| error = WriteMachinePassword(Path::MACHINE_PASS, machine_pass); |
| if (error != ERROR_NONE) { |
| Reset(); |
| return error; |
| } |
| |
| // Store configuration for subsequent runs of the daemon. |
| error = WriteConfiguration(); |
| if (error != ERROR_NONE) { |
| Reset(); |
| return error; |
| } |
| |
| // Cache auth data. Note that users in the device realm are always affiliated. |
| UpdateAuthDataCache(device_account_, true /* is_affiliated */); |
| |
| // Since we just created the account, set propagation retry to give the |
| // password time to propagate through Active Directory. |
| device_tgt_manager_.SetPropagationRetry(true); |
| |
| // Only if everything worked out, keep the config. |
| if (joined_domain) |
| *joined_domain = join_realm; |
| |
| // Collecting metrics with the encryption types used during this successful |
| // enrollment. This value has been set through the advanced settings of the |
| // domain join screen. |
| metrics_->ReportEncryptionType(ENC_TYPES_OF_JOIN_AD_DOMAIN, |
| encryption_types_); |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::FetchUserGpos( |
| const std::string& account_id, protos::GpoPolicyData* gpo_policy_data) { |
| ReloadDebugFlags(); |
| SetUserAccountId(account_id); |
| |
| // Try to restore TGT, user_account_id_ etc. if it doesn't exist. |
| MaybeRestoreUserAuthState(); |
| |
| if (!user_logged_in_) { |
| LOG(ERROR) << "User not logged in. Did AuthenticateUser() fail?"; |
| return ERROR_NOT_LOGGED_IN; |
| } |
| DCHECK(!user_account_.user_name.empty()); |
| DCHECK(!user_account_.realm.empty()); |
| |
| // We need user_policy_mode_ to properly fetch user policy, which is read from |
| // device policy. |
| if (!has_device_policy_) { |
| LOG(ERROR) << "Unknown user policy mode. Did FetchDeviceGpos() fail?"; |
| return ERROR_NO_DEVICE_POLICY; |
| } |
| |
| // Download GPOs for the given user, taking the loopback processing |mode| |
| // into account: |
| // USER_POLICY_MODE_DEFAULT: Process user GPOs as usual. |
| // USER_POLICY_MODE_MERGE: Apply user policy from device GPOs on top of |
| // user policy from user GPOs. |
| // USER_POLICY_MODE_REPLACE: Only apply user policy from device GPOs. |
| ErrorType error; |
| std::vector<base::FilePath> gpo_file_paths; |
| if (user_policy_mode_ != em::DeviceUserPolicyLoopbackProcessingModeProto:: |
| USER_POLICY_MODE_REPLACE) { |
| // Update smb.conf, IPs, server names etc for the user account. |
| error = UpdateAccountData(&user_account_); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Download user GPOs with user policy data. |
| error = GetGpos(GpoSource::USER, PolicyScope::USER, &gpo_file_paths); |
| if (error != ERROR_NONE) |
| return error; |
| } |
| if (user_policy_mode_ != em::DeviceUserPolicyLoopbackProcessingModeProto:: |
| USER_POLICY_MODE_DEFAULT) { |
| // Acquire Kerberos ticket-granting-ticket for the device account. |
| error = AcquireDeviceTgt(); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Download device GPOs with user policy data. |
| // Note that device account data was updated by calling `AcquireDeviceTgt()` |
| // above. |
| error = GetGpos(GpoSource::MACHINE, PolicyScope::USER, &gpo_file_paths); |
| if (error != ERROR_NONE) |
| return error; |
| } |
| |
| // Parse GPOs and store them in a user+extension policy protobuf. |
| std::string gpo_policy_data_blob; |
| error = ParseGposIntoProtobuf(gpo_file_paths, kCmdParseUserPreg, |
| &gpo_policy_data_blob); |
| if (error != ERROR_NONE) |
| return error; |
| |
| error = ParsePolicyData(gpo_policy_data_blob, gpo_policy_data); |
| if (error != ERROR_NONE) |
| return error; |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::FetchDeviceGpos( |
| protos::GpoPolicyData* gpo_policy_data) { |
| ReloadDebugFlags(); |
| |
| // Check if the device is domain joined. |
| if (!IsDeviceJoined()) |
| return ERROR_NOT_JOINED; |
| |
| // Acquire Kerberos ticket-granting-ticket for the device account. |
| ErrorType error = AcquireDeviceTgt(); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Download device GPOs with device policy data. |
| // Note that account data was updated by calling `AcquireDeviceTgt()` above. |
| std::vector<base::FilePath> gpo_file_paths; |
| error = GetGpos(GpoSource::MACHINE, PolicyScope::MACHINE, &gpo_file_paths); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Parse GPOs and store them in a device+extension policy protobuf. |
| std::string gpo_policy_data_blob; |
| error = ParseGposIntoProtobuf(gpo_file_paths, kCmdParseDevicePreg, |
| &gpo_policy_data_blob); |
| if (error != ERROR_NONE) |
| return error; |
| |
| error = ParsePolicyData(gpo_policy_data_blob, gpo_policy_data); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Update stuff that depends on device policy. |
| em::ChromeDeviceSettingsProto device_policy; |
| if (!device_policy.ParseFromString( |
| gpo_policy_data->user_or_device_policy())) { |
| LOG(ERROR) << "Failed to parse device policy"; |
| return ERROR_PARSE_FAILED; |
| } |
| UpdateDevicePolicyDependencies(device_policy); |
| |
| return ERROR_NONE; |
| } |
| |
| void SambaInterface::OnSessionStateChanged(const std::string& state) { |
| LOG(INFO) << "Session state changed to '" << state << "'"; |
| in_user_session_ = state == kSessionStarted; |
| MaybeBackupUserAuthState(); |
| } |
| |
| void SambaInterface::SetDefaultLogLevel(AuthPolicyFlags::DefaultLevel level) { |
| flags_default_level_ = level; |
| LOG(INFO) << "Flags default level = " << flags_default_level_; |
| SaveFlagsDefaultLevel(); |
| } |
| |
| std::string SambaInterface::GetUserPrincipal() const { |
| return user_account_.GetPrincipal(); |
| } |
| |
| void SambaInterface::OnTgtRenewed() { |
| MaybeBackupUserAuthState(); |
| } |
| |
| void SambaInterface::DisableRetrySleepForTesting() { |
| retry_sleep_disabled_for_testing_ = true; |
| device_tgt_manager_.DisableRetrySleepForTesting(); |
| } |
| |
| ErrorType SambaInterface::RenewUserTgtForTesting() { |
| return user_tgt_manager_.RenewTgt(); |
| } |
| |
| void SambaInterface::SetDevicePolicyImplForTesting( |
| std::unique_ptr<policy::DevicePolicyImpl> policy_impl) { |
| device_policy_impl_for_testing = std::move(policy_impl); |
| } |
| |
| void SambaInterface::ResetForTesting() { |
| Reset(); |
| } |
| |
| ErrorType SambaInterface::ChangeMachinePasswordForTesting() { |
| const base::FilePath password_path(paths_->Get(Path::MACHINE_PASS)); |
| std::string old_password; |
| if (!ReadMachinePasswordToString(password_path, &old_password)) |
| return ERROR_LOCAL_IO; |
| |
| auto stored_password_change_rate_ = password_change_rate_; |
| password_change_rate_ = base::TimeDelta::FromMilliseconds(1); |
| ErrorType error = CheckMachinePasswordChange(); |
| password_change_rate_ = stored_password_change_rate_; |
| if (error != ERROR_NONE) |
| return error; |
| |
| std::string new_password; |
| if (!ReadMachinePasswordToString(password_path, &new_password)) |
| return ERROR_LOCAL_IO; |
| |
| if (new_password == old_password) |
| return ERROR_KPASSWD_FAILED; |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::UpdateKdcIpAndServerTime(AccountData* account) const { |
| // Look up KDC IP from cache. |
| if (account->kdc_ip.empty()) { |
| base::Optional<std::string> kdc_ip = |
| auth_data_cache_.GetKdcIp(account->realm); |
| if (kdc_ip) { |
| account->kdc_ip = std::move(*kdc_ip); |
| anonymizer_->SetReplacementAllCases(account->kdc_ip, |
| kIpAddressPlaceholder); |
| } |
| } |
| |
| // Use cached KDC IP and server time. Caching server time seems weird since it |
| // changes constantly, but most code doesn't need server time. If an |
| // up-to-date server time is needed, just reset it to base::Time() before |
| // calling UpdateAccountData(); |
| if (!account->kdc_ip.empty() && !account->server_time.is_null()) |
| return ERROR_NONE; |
| |
| // Call net ads info to get the KDC IP. |
| const std::string& smb_conf_path = paths_->Get(account->smb_conf_path); |
| authpolicy::ProcessExecutor net_cmd({paths_->Get(Path::NET), "ads", "info", |
| kConfigParam, smb_conf_path, kDebugParam, |
| flags_.net_log_level()}); |
| // Replace a few values immediately in the net_cmd output, see |
| // SearchAccountInfo for an explanation. |
| anonymizer_->ReplaceSearchArg(kKeyKdcServer, kIpAddressPlaceholder); |
| anonymizer_->ReplaceSearchArg(kKeyLdapServer, kIpAddressPlaceholder); |
| anonymizer_->ReplaceSearchArg(kKeyLdapServerName, kServerNamePlaceholder); |
| const bool net_result = jail_helper_.SetupJailAndRun( |
| &net_cmd, Path::NET_ADS_SECCOMP, TIMER_NET_ADS_INFO); |
| anonymizer_->ResetSearchArgReplacements(); |
| if (!net_result) |
| return GetNetError(net_cmd, "info"); |
| const std::string& net_out = net_cmd.GetStdout(); |
| |
| // Parse the output to find the KDC IP. Enclose in a sandbox for security |
| // considerations. |
| ProcessExecutor parse_cmd( |
| {paths_->Get(Path::PARSER), kCmdParseServerInfo, SerializeFlags(flags_)}); |
| parse_cmd.SetInputString(net_out); |
| if (!jail_helper_.SetupJailAndRun(&parse_cmd, Path::PARSER_SECCOMP, |
| TIMER_NONE)) { |
| // Log net output if it hasn't been done yet. |
| net_cmd.LogOutputOnce(); |
| LOG(ERROR) << "authpolicy_parser parse_server_info failed with exit code " |
| << parse_cmd.GetExitCode(); |
| return ERROR_PARSE_FAILED; |
| } |
| |
| protos::ServerInfo server_info; |
| if (!server_info.ParseFromString(parse_cmd.GetStdout())) { |
| // Log net output if it hasn't been done yet. |
| net_cmd.LogOutputOnce(); |
| LOG(ERROR) << "Failed to parse server info protobuf"; |
| return ERROR_PARSE_FAILED; |
| } |
| |
| account->kdc_ip = server_info.kdc_ip(); |
| account->server_time = |
| base::Time::FromInternalValue(server_info.server_time()); |
| |
| // Explicitly set replacements again, see SearchAccountInfo for an |
| // explanation. |
| anonymizer_->SetReplacementAllCases(account->kdc_ip, kIpAddressPlaceholder); |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::UpdateDcName(AccountData* account) const { |
| // Look up DC name from cache. |
| if (account->dc_name.empty()) { |
| base::Optional<std::string> dc_name = |
| auth_data_cache_.GetDcName(account->realm); |
| if (dc_name) { |
| account->dc_name = std::move(*dc_name); |
| anonymizer_->SetReplacementAllCases(account->dc_name, |
| kServerNamePlaceholder); |
| } |
| } |
| |
| // Use cached DC name. |
| if (!account->dc_name.empty()) |
| return ERROR_NONE; |
| |
| // Call net ads lookup to get the domain controller name. |
| const std::string& smb_conf_path = paths_->Get(account->smb_conf_path); |
| authpolicy::ProcessExecutor net_cmd({paths_->Get(Path::NET), "ads", "lookup", |
| kConfigParam, smb_conf_path, kDebugParam, |
| flags_.net_log_level()}); |
| // Replace a few values immediately in the net_cmd output, see |
| // SearchAccountInfo for an explanation. |
| anonymizer_->ReplaceSearchArg(kKeyForest, kForestPlaceholder); |
| anonymizer_->ReplaceSearchArg(kKeyDomain, kDomainPlaceholder); |
| anonymizer_->ReplaceSearchArg(kKeyDomainController, kServerNamePlaceholder); |
| anonymizer_->ReplaceSearchArg(kKeyPreWin2kDomain, kDomainPlaceholder); |
| anonymizer_->ReplaceSearchArg(kKeyPreWin2kHostname, kServerNamePlaceholder); |
| anonymizer_->ReplaceSearchArg(kKeyServerSiteName, kSiteNamePlaceholder); |
| anonymizer_->ReplaceSearchArg(kKeyClientSiteName, kSiteNamePlaceholder); |
| const bool net_result = jail_helper_.SetupJailAndRun( |
| &net_cmd, Path::NET_ADS_SECCOMP, TIMER_NET_ADS_INFO); |
| anonymizer_->ResetSearchArgReplacements(); |
| if (!net_result) |
| return GetNetError(net_cmd, "lookup"); |
| 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), kCmdParseDcName, SerializeFlags(flags_)}); |
| parse_cmd.SetInputString(net_out); |
| if (!jail_helper_.SetupJailAndRun(&parse_cmd, Path::PARSER_SECCOMP, |
| TIMER_NONE)) { |
| // Log net output if it hasn't been done yet. |
| net_cmd.LogOutputOnce(); |
| LOG(ERROR) << "authpolicy_parser parse_dc_name failed with exit code " |
| << parse_cmd.GetExitCode(); |
| return ERROR_PARSE_FAILED; |
| } |
| account->dc_name = parse_cmd.GetStdout(); |
| |
| // Explicitly set replacements again, see SearchAccountInfo for an |
| // explanation. |
| anonymizer_->SetReplacementAllCases(account->dc_name, kServerNamePlaceholder); |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::GetUserTgtStatus( |
| ActiveDirectoryUserStatus::TgtStatus* tgt_status) { |
| protos::TgtLifetime lifetime; |
| ErrorType error = user_tgt_manager_.GetTgtLifetime(&lifetime); |
| switch (error) { |
| case 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. |
| case ERROR_NO_CREDENTIALS_CACHE_FOUND: |
| *tgt_status = ActiveDirectoryUserStatus::TGT_NOT_FOUND; |
| return ERROR_NONE; |
| case ERROR_KERBEROS_TICKET_EXPIRED: |
| *tgt_status = ActiveDirectoryUserStatus::TGT_EXPIRED; |
| return ERROR_NONE; |
| default: |
| return error; |
| } |
| } |
| |
| ActiveDirectoryUserStatus::PasswordStatus SambaInterface::GetUserPasswordStatus( |
| const ActiveDirectoryAccountInfo& account_info) { |
| // See https://msdn.microsoft.com/en-us/library/ms679430(v=vs.85).aspx. |
| |
| // Gracefully handle missing fields, see crbug.com/795758. |
| if (!account_info.has_pwd_last_set() || |
| !account_info.has_user_account_control()) { |
| return ActiveDirectoryUserStatus::PASSWORD_VALID; |
| } |
| |
| // Password is always valid if it never expires. |
| if ((account_info.user_account_control() & UF_DONT_EXPIRE_PASSWD) != 0) |
| return ActiveDirectoryUserStatus::PASSWORD_VALID; |
| |
| // Password expired, user will have to enter a new password. |
| if (account_info.pwd_last_set() == 0) |
| return ActiveDirectoryUserStatus::PASSWORD_EXPIRED; |
| |
| // Memorize pwd_last_set if it wasn't set yet. This happens after the password |
| // expired and was reset by AuthenticateUser(). |
| if (user_pwd_last_set_ == 0) { |
| user_pwd_last_set_ = account_info.pwd_last_set(); |
| return ActiveDirectoryUserStatus::PASSWORD_VALID; |
| } |
| |
| // Password changed on the server. Note: Don't update pwd_last_set_ here, |
| // update it in AuthenticateUser() when we know that Chrome sent the right |
| // password. |
| if (user_pwd_last_set_ != account_info.pwd_last_set()) |
| return ActiveDirectoryUserStatus::PASSWORD_CHANGED; |
| |
| // pwd_last_set did not change, password is still valid. |
| return ActiveDirectoryUserStatus::PASSWORD_VALID; |
| } |
| |
| ErrorType SambaInterface::UpdateWorkgroup(AccountData* account) const { |
| // Look up workgroup from cache. |
| if (account->workgroup.empty()) { |
| base::Optional<std::string> workgroup = |
| auth_data_cache_.GetWorkgroup(account->realm); |
| if (workgroup) { |
| account->workgroup = std::move(*workgroup); |
| anonymizer_->SetReplacement(account->workgroup, kWorkgroupPlaceholder); |
| } |
| } |
| |
| // Use cached workgroup. |
| if (!account->workgroup.empty()) |
| return ERROR_NONE; |
| |
| const std::string& smb_conf_path = paths_->Get(account->smb_conf_path); |
| ProcessExecutor net_cmd({paths_->Get(Path::NET), "ads", "workgroup", |
| kConfigParam, smb_conf_path, kDebugParam, |
| flags_.net_log_level()}); |
| // Parse workgroup from the net_cmd output immediately, see SearchAccountInfo |
| // for an explanation. Also replace a bunch of other server names. |
| anonymizer_->ReplaceSearchArg(kKeyWorkgroup, kWorkgroupPlaceholder); |
| anonymizer_->ReplaceSearchArg(kKeyAdsDnsParseRrSrv, kServerNamePlaceholder, |
| "Parsed (.+?)\\."); |
| anonymizer_->ReplaceSearchArg(kKeyPdcDnsName, kServerNamePlaceholder, |
| "'(.+)'"); |
| anonymizer_->ReplaceSearchArg(kKeyAdsDcName, kServerNamePlaceholder, |
| "using server='(.+?)\\."); |
| anonymizer_->ReplaceSearchArg(kKeyPdcName, kServerNamePlaceholder, "'(.+)'"); |
| anonymizer_->ReplaceSearchArg(kKeyServerSite, kSiteNamePlaceholder, "'(.+)'"); |
| anonymizer_->ReplaceSearchArg(kKeyClientSite, kSiteNamePlaceholder, "'(.+)'"); |
| const bool net_result = jail_helper_.SetupJailAndRun( |
| &net_cmd, Path::NET_ADS_SECCOMP, TIMER_NET_ADS_WORKGROUP); |
| anonymizer_->ResetSearchArgReplacements(); |
| if (!net_result) |
| 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, SerializeFlags(flags_)}); |
| 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; |
| } |
| account->workgroup = parse_cmd.GetStdout(); |
| |
| // Explicitly set replacements again, see SearchAccountInfo for an |
| // explanation. |
| anonymizer_->SetReplacement(account->workgroup, kWorkgroupPlaceholder); |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::WriteSmbConf(const AccountData& account) const { |
| // account.netbios_name and account.workgroup may be empty at this point. |
| DCHECK(!account.realm.empty()); |
| |
| std::string data = base::StringPrintf( |
| kSmbConfData, account.netbios_name.c_str(), account.workgroup.c_str(), |
| account.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(), |
| GetEncryptionTypesString(encryption_types_)); |
| |
| const base::FilePath smbconf_path(paths_->Get(account.smb_conf_path)); |
| const int data_size = static_cast<int>(data.size()); |
| if (base::WriteFile(smbconf_path, data.data(), data_size) != data_size) { |
| LOG(ERROR) << "Failed to write Samba conf file '" << smbconf_path.value() |
| << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::UpdateAccountData(AccountData* account) { |
| // Write smb.conf for UpdateWorkgroup(). |
| ErrorType error = WriteSmbConf(*account); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Update |account|->workgroup. |
| const std::string prev_workgroup = account->workgroup; |
| error = UpdateWorkgroup(account); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Write smb.conf again for the rest in case the workgroup changed. |
| if (account->workgroup != prev_workgroup) { |
| error = WriteSmbConf(*account); |
| if (error != ERROR_NONE) |
| return error; |
| } |
| |
| // Set fake domain SID if it was not set for this account workgroup yet. |
| // This is workaround for Samba 4.8.6+, see `MaybeSetFakeDomainSid()` |
| // description. |
| error = MaybeSetFakeDomainSid(*account); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Query the key distribution center IP and server time and store them in |
| // |account|->kdc_ip and |account|->server_time, respectively. |
| error = UpdateKdcIpAndServerTime(account); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Query the domain controller name and store it in |account|->dc_name. |
| error = UpdateDcName(account); |
| if (error != ERROR_NONE) |
| return error; |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::PingServer(AccountData* account) { |
| // Write smb.conf for UpdateWorkgroup(). |
| ErrorType error = WriteSmbConf(*account); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Update |account|->workgroup. Make sure to invalidate the workgroup and |
| // disable the cache, so that the server is actually hit. |
| std::string prev_workgroup; |
| prev_workgroup.swap(account->workgroup); |
| bool prev_enabled = auth_data_cache_.IsEnabled(); |
| auth_data_cache_.SetEnabled(false); |
| |
| error = UpdateWorkgroup(account); |
| |
| auth_data_cache_.SetEnabled(prev_enabled); |
| prev_workgroup.swap(account->workgroup); |
| return error; |
| } |
| |
| bool SambaInterface::IsUserAffiliated() { |
| // Check cache first. |
| base::Optional<bool> cached_is_affiliated = |
| auth_data_cache_.GetIsAffiliated(user_account_.realm); |
| if (cached_is_affiliated) { |
| // Right now, only affiliated realms should be cached (but we'll keep it |
| // generic, anyway, in case that changes in the future). |
| CHECK(*cached_is_affiliated) |
| << "Caching for unaffiliated realms not supported"; |
| return *cached_is_affiliated; |
| } |
| |
| // Users on device realm are always affiliated. |
| if (user_account_.realm == device_account_.realm) |
| return true; |
| |
| // Call net ads search using |
| // - the device smb.conf, but |
| // - the user's credentials! |
| // This enforces a trust check, which tells us about affiliation. |
| const std::string& smb_conf_path = paths_->Get(device_account_.smb_conf_path); |
| std::string search_string = base::StringPrintf( |
| "(sAMAccountName=%s)", device_account_.user_name.c_str()); |
| ProcessExecutor net_cmd({paths_->Get(Path::NET), "ads", "search", |
| search_string, kSearchSAMAccountName, kConfigParam, |
| smb_conf_path, kDebugParam, flags_.net_log_level(), |
| kKerberosParam}); |
| net_cmd.SetEnv(kKrb5CCEnvKey, |
| paths_->Get(user_tgt_manager_.GetCredentialCachePath())); |
| const bool net_result = jail_helper_.SetupJailAndRun( |
| &net_cmd, Path::NET_ADS_SECCOMP, TIMER_NET_ADS_SEARCH); |
| |
| // net is expected to fail if the user is not affiliated. In my test setup |
| // with different KDCs, net failed with exit code 255 and no error message, |
| // resulting in ERROR_NET_FAILED (there was a "Cannot read password" error in |
| // debug logs). It's unclear, though, if that's always the case, so just print |
| // out the error otherwise and assume the user is not affiliated. By no means |
| // bail on error here. |
| if (!net_result) { |
| ErrorType error = GetNetError(net_cmd, "search"); |
| if (error != ERROR_NET_FAILED) |
| LOG(ERROR) << "Affiliation check failed with error " << error; |
| return false; |
| } |
| |
| // Expected output in case of success: |
| // Got 1 replies |
| // |
| // sAMAccountName: <MACHINE_NAME> |
| return Contains(net_cmd.GetStdout(), kSearchSAMAccountName); |
| } |
| |
| ErrorType SambaInterface::AcquireUserTgt(int password_fd) { |
| // Update smb.conf, IPs, server names etc. for the user account. |
| ErrorType error = UpdateAccountData(&user_account_); |
| if (error != ERROR_NONE) |
| return error; |
| user_tgt_manager_.SetKdcIp(user_account_.kdc_ip); |
| |
| // Call kinit to get the Kerberos ticket-granting-ticket. |
| return user_tgt_manager_.AcquireTgtWithPassword(password_fd); |
| } |
| |
| ErrorType SambaInterface::AcquireDeviceTgt() { |
| // Update smb.conf, IPs, server names etc for the device account. |
| ErrorType error = UpdateAccountData(&device_account_); |
| if (error != ERROR_NONE) |
| return error; |
| device_tgt_manager_.SetKdcIp(device_account_.kdc_ip); |
| |
| // Acquire the Kerberos ticket-granting-ticket. |
| const base::FilePath password_path(paths_->Get(Path::MACHINE_PASS)); |
| if (!base::PathExists(password_path)) { |
| // This is expected to happen on devices that had been domain joined before |
| // authpolicyd managed the machine password. They stored the machine keytab |
| // instead of the password, so use that for authentication. |
| return device_tgt_manager_.AcquireTgtWithKeytab(Path::MACHINE_KEYTAB); |
| } |
| |
| // Authenticate using password. Note: There is no keytab file here. |
| base::ScopedFD password_fd = ReadFileToPipe(password_path); |
| if (!password_fd.is_valid()) { |
| LOG(ERROR) << "Failed to open machine password file '" |
| << password_path.value() << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| const base::FilePath prev_password_path(paths_->Get(Path::PREV_MACHINE_PASS)); |
| error = device_tgt_manager_.AcquireTgtWithPassword(password_fd.get()); |
| if (error != ERROR_BAD_PASSWORD || !base::PathExists(prev_password_path)) |
| return error; |
| |
| // Try again with the previous password. After a password change the password |
| // might not have propagated through a large AD deployment yet. |
| password_fd = ReadFileToPipe(prev_password_path); |
| if (!password_fd.is_valid()) { |
| LOG(ERROR) << "Failed to open machine password file '" |
| << prev_password_path.value() << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| return device_tgt_manager_.AcquireTgtWithPassword(password_fd.get()); |
| } |
| |
| ErrorType SambaInterface::WriteMachinePassword( |
| Path path, const std::string& machine_pass) const { |
| const base::FilePath password_path(paths_->Get(path)); |
| if (!base::ImportantFileWriter::WriteFileAtomically(password_path, |
| machine_pass)) { |
| LOG(ERROR) << "Failed to write machine password file '" |
| << password_path.value() << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| // This file is only authpolicyd's business. |
| int mode = |
| base::FILE_PERMISSION_READ_BY_USER | base::FILE_PERMISSION_WRITE_BY_USER; |
| ErrorType error = SetFilePermissions(password_path, mode); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Set file time to match server time, so that we can determine the password |
| // age and renew the machine password without relying on local time. |
| if (!base::TouchFile(password_path, device_account_.server_time, |
| device_account_.server_time)) { |
| LOG(ERROR) << "Failed to set file time on machine password file '" |
| << password_path.value() << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| LOG(INFO) << "Wrote machine password file '" << password_path.value() << "'"; |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::RollMachinePassword() { |
| const base::FilePath password_path(paths_->Get(Path::MACHINE_PASS)); |
| const base::FilePath prev_password_path(paths_->Get(Path::PREV_MACHINE_PASS)); |
| const base::FilePath new_password_path(paths_->Get(Path::NEW_MACHINE_PASS)); |
| |
| base::File::Error file_error; |
| if (!base::ReplaceFile(password_path, prev_password_path, &file_error) || |
| !base::ReplaceFile(new_password_path, password_path, &file_error)) { |
| LOG(ERROR) << "Machine password roll failed: " |
| << base::File::ErrorToString(file_error); |
| return ERROR_LOCAL_IO; |
| } |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::WriteConfiguration() const { |
| DCHECK(!device_account_.realm.empty()); |
| DCHECK(!device_account_.netbios_name.empty()); |
| |
| protos::ActiveDirectoryConfig config; |
| config.set_realm(device_account_.realm); |
| config.set_machine_name(device_account_.netbios_name); |
| |
| 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)); |
| if (!base::ImportantFileWriter::WriteFileAtomically(config_path, |
| config_blob)) { |
| 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)) { |
| PLOG(ERROR) << "Failed to read configuration file '" << config_path.value() |
| << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| auto config = std::make_unique<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; |
| } |
| |
| InitDeviceAccount(config->machine_name(), config->realm()); |
| |
| LOG(INFO) << "Read configuration file '" << config_path.value() << "'"; |
| |
| AnonymizeRealm(device_account_.realm, kDeviceRealmPlaceholder); |
| anonymizer_->SetReplacementAllCases(device_account_.netbios_name, |
| kMachineNamePlaceholder); |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::GetAccountInfo( |
| const std::string& user_name, |
| const std::string& normalized_upn, |
| const std::string& account_id, |
| ActiveDirectoryAccountInfo* account_info) { |
| // 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); |
| anonymizer_->SetReplacement(account_id_octet, kAccountIdPlaceholder); |
| 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. |
| anonymizer_->SetReplacement(user_name, kSAMAccountNamePlaceholder); |
| std::string search_string = |
| base::StringPrintf("(sAMAccountName=%s)", user_name.c_str()); |
| ErrorType 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."; |
| anonymizer_->SetReplacement(user_name, kLogonNamePlaceholder); |
| 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) { |
| // Set up net ads search to find the user's account info. |
| const std::string& smb_conf_path = paths_->Get(user_account_.smb_conf_path); |
| ProcessExecutor net_cmd( |
| {paths_->Get(Path::NET), "ads", "search", search_string, |
| kSearchObjectGUID, kSearchSAMAccountName, kSearchCommonName, |
| kSearchDisplayName, kSearchGivenName, kSearchPwdLastSet, |
| kSearchUserAccountControl, kConfigParam, smb_conf_path, kDebugParam, |
| flags_.net_log_level(), kKerberosParam}); |
| |
| // Parse the search args from the net_cmd output immediately. This resolves |
| // the chicken-egg-problem that replacement strings cannot be set before the |
| // strings-to-replace are known, so the output of net_cmd would still contain |
| // sensitive strings. |
| anonymizer_->ReplaceSearchArg(kSearchObjectGUID, kAccountIdPlaceholder); |
| anonymizer_->ReplaceSearchArg(kSearchDisplayName, kDisplayNamePlaceholder); |
| anonymizer_->ReplaceSearchArg(kSearchGivenName, kGivenNamePlaceholder); |
| anonymizer_->ReplaceSearchArg(kSearchSAMAccountName, |
| kSAMAccountNamePlaceholder); |
| anonymizer_->ReplaceSearchArg(kSearchCommonName, kCommonNamePlaceholder); |
| |
| // Use the user's TGT to query the account info. |
| net_cmd.SetEnv(kKrb5CCEnvKey, |
| paths_->Get(user_tgt_manager_.GetCredentialCachePath())); |
| const bool net_result = jail_helper_.SetupJailAndRun( |
| &net_cmd, Path::NET_ADS_SECCOMP, TIMER_NET_ADS_SEARCH); |
| anonymizer_->ResetSearchArgReplacements(); |
| if (!net_result) { |
| 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, |
| SerializeFlags(flags_)}); |
| parse_cmd.SetInputString(net_out); |
| if (!jail_helper_.SetupJailAndRun(&parse_cmd, Path::PARSER_SECCOMP, |
| TIMER_NONE)) { |
| // Log net output if it hasn't been done yet. |
| net_cmd.LogOutputOnce(); |
| LOG(ERROR) << "Failed to parse account info. 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 their user name. |
| LOG(WARNING) << "Search yielded no results"; |
| return ERROR_BAD_USER_NAME; |
| } else if (!account_info->ParseFromString(account_info_blob)) { |
| // Log net output if it hasn't been done yet. |
| net_cmd.LogOutputOnce(); |
| LOG(ERROR) << "Failed to parse account info protobuf"; |
| return ERROR_PARSE_FAILED; |
| } |
| |
| // Explicitly set replacements again in case logging is currently disabled |
| // and the anonymizer has not parsed the search values above. If we didn't do |
| // it here and logging would be enabled later, logs would contain sensitive |
| // data. |
| anonymizer_->SetReplacement(account_info->account_id(), |
| kAccountIdPlaceholder); |
| anonymizer_->SetReplacement(account_info->display_name(), |
| kDisplayNamePlaceholder); |
| anonymizer_->SetReplacement(account_info->given_name(), |
| kGivenNamePlaceholder); |
| anonymizer_->SetReplacement(account_info->sam_account_name(), |
| kSAMAccountNamePlaceholder); |
| anonymizer_->SetReplacement(account_info->common_name(), |
| kCommonNamePlaceholder); |
| |
| return ERROR_NONE; |
| } |
| |
| ErrorType SambaInterface::GetGpos(GpoSource source, |
| PolicyScope scope, |
| std::vector<base::FilePath>* gpo_file_paths) { |
| // There's no use case for machine policy from user GPOs right now. |
| DCHECK(!(source == GpoSource::USER && scope == PolicyScope::MACHINE)); |
| |
| // Query list of GPOs from Active Directory server. |
| protos::GpoList gpo_list; |
| ErrorType error = GetGpoList(source, scope, &gpo_list); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Download GPOs from Active Directory server. |
| return DownloadGpos(gpo_list, source, scope, gpo_file_paths); |
| } |
| |
| ErrorType SambaInterface::GetGpoList(GpoSource source, |
| PolicyScope scope, |
| protos::GpoList* gpo_list) { |
| DCHECK(gpo_list); |
| LOG(INFO) << "Getting " << (scope == PolicyScope::USER ? "user" : "device") |
| << " GPO list for " |
| << (source == GpoSource::USER ? "user" : "device") << " account"; |
| |
| const AccountData& account = GetAccount(source); |
| const TgtManager& tgt_manager = GetTgtManager(source); |
| authpolicy::ProcessExecutor net_cmd( |
| {paths_->Get(Path::NET), "ads", "gpo", "list", account.user_name, |
| kConfigParam, paths_->Get(account.smb_conf_path), kDebugParam, |
| flags_.net_log_level(), kKerberosParam}); |
| 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. Note |
| // that |cmd| depends on |scope| since the parse command is concerned with the |
| // type of policy, not which account a GPO came from. |
| const char* cmd = scope == PolicyScope::USER ? kCmdParseUserGpoList |
| : kCmdParseDeviceGpoList; |
| ProcessExecutor parse_cmd( |
| {paths_->Get(Path::PARSER), cmd, SerializeFlags(flags_)}); |
| parse_cmd.SetInputString(net_out); |
| if (!jail_helper_.SetupJailAndRun(&parse_cmd, Path::PARSER_SECCOMP, |
| TIMER_NONE)) { |
| // Log net output if it hasn't been done yet. |
| net_cmd.LogOutputOnce(); |
| 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. |
| std::string cache_key_; // Key into the gpo version cache; gpo giud + "-U/M". |
| uint32_t version_; // User or machine version of the GPO. |
| bool use_cache_; // Whether to use cached version. False to redownload. |
| GpoPaths(const std::string& server, |
| const base::FilePath& local, |
| const std::string& cache_key, |
| uint32_t version, |
| bool use_cache) |
| : server_(server), |
| local_(local), |
| cache_key_(cache_key), |
| version_(version), |
| use_cache_(use_cache) {} |
| }; |
| |
| ErrorType SambaInterface::DownloadGpos( |
| const protos::GpoList& gpo_list, |
| GpoSource source, |
| PolicyScope scope, |
| std::vector<base::FilePath>* gpo_file_paths) { |
| 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; |
| } |
| |
| // Clean up GPO cache. |
| gpo_version_cache_.RemoveEntriesOlderThan(gpo_version_cache_ttl_); |
| |
| // Generate all smb source and linux target directories and create targets. |
| ErrorType error; |
| std::string smb_command = "prompt OFF;lowercase ON;"; |
| std::string gpo_share; |
| std::vector<GpoPaths> gpo_paths; |
| bool anything_to_download = false; |
| 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.share().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 share, i.e. come from the same SysVol. |
| if (gpo_share.empty()) { |
| gpo_share = gpo.share(); |
| } else if (!base::EqualsCaseInsensitiveASCII(gpo_share, gpo.share())) { |
| LOG(ERROR) << "Inconsistent share '" << gpo_share << "' != '" |
| << gpo.share() << "'"; |
| 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_CACHE_DIR)), |
| kFileMode_rwxrwx); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Figure out whether we can use cached GPO to skip download. As cache key |
| // use {GPO-GUID}-U for user policy and {GPO-GUID}-M for machine policy. |
| // (User and machine policy are two separate files, even though it's the |
| // same GPO). Note that the GPO file may not exist, but that's fine. |
| const char* scope_extension = (scope == PolicyScope::USER ? "-U" : "-M"); |
| const std::string cache_key = gpo.name() + scope_extension; |
| const bool use_cache = |
| gpo_version_cache_.MayUseCachedGpo(cache_key, gpo.version()); |
| |
| // Record output file paths. |
| const std::string server_path = smb_dir + "\\" + kPRegFileName; |
| const auto local_path = base::FilePath(linux_dir).Append(kPRegFileName); |
| gpo_paths.push_back( |
| GpoPaths(server_path, local_path, cache_key, gpo.version(), use_cache)); |
| |
| if (!use_cache) { |
| // Delete the stale GPO file if it exists. |
| if (!base::DeleteFile(local_path, false /* recursive */)) { |
| LOG(ERROR) << "Failed to delete old GPO file '" |
| << anonymizer_->Process(local_path.value()) << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| // Build command to download the GPO file via smbclient. |
| smb_command += base::StringPrintf("cd %s;lcd %s;get %s;", smb_dir.c_str(), |
| linux_dir.c_str(), kPRegFileName); |
| anything_to_download = true; |
| } |
| } |
| |
| // Skip smbclient call if there's nothing to download. |
| if (anything_to_download) { |
| const AccountData& account = GetAccount(source); |
| DCHECK(!account.dc_name.empty()); |
| const std::string service = base::StringPrintf( |
| "//%s/%s", account.dc_name.c_str(), gpo_share.c_str()); |
| |
| // The exit code of smbclient corresponds to the LAST command issued. Some |
| // files might be missing and fail to download, which is fine and handled |
| // below. Appending 'exit' makes sure the exit code is not 1 if the last |
| // file happens to be missing. |
| smb_command += "exit;"; |
| |
| // 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, kConfigParam, |
| paths_->Get(account.smb_conf_path), kKerberosParam, kDebugParam, |
| flags_.net_log_level(), kCommandParam, smb_command}); |
| const TgtManager& tgt_manager = GetTgtManager(source); |
| 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())); |
| int tries, failed_tries = 0; |
| for (tries = 1; tries <= kSmbClientMaxTries; ++tries) { |
| if (tries > 1 && !retry_sleep_disabled_for_testing_) |
| base::PlatformThread::Sleep(kSmbClientRetryDelay); |
| if (jail_helper_.SetupJailAndRun(&smb_client_cmd, Path::SMBCLIENT_SECCOMP, |
| TIMER_SMBCLIENT)) { |
| 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 (error != ERROR_NONE) |
| return error; |
| |
| // 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()); |
| |
| // Gracefully handle non-existing GPOs. Testing revealed these cases do |
| // exist, see crbug.com/680921. |
| for (const GpoPaths& gpo_path : gpo_paths) { |
| if (gpo_path.use_cache_) |
| continue; |
| if (base::PathExists(gpo_path.local_)) |
| continue; |
| |
| const std::string no_file_error_key( |
| base::ToLowerASCII(kKeyObjectNameNotFound + gpo_path.server_)); |
| if (Contains(smbclient_out_lower, no_file_error_key)) { |
| LOG_IF(WARNING, flags_.log_gpo()) |
| << "Ignoring missing preg file '" |
| << anonymizer_->Process(gpo_path.local_.value()) << "'"; |
| } else { |
| // Log smbclient output if it hasn't been done yet. |
| smb_client_cmd.LogOutputOnce(); |
| LOG(ERROR) << "Failed to download preg file '" |
| << anonymizer_->Process(gpo_path.local_.value()) << "'"; |
| gpo_version_cache_.Remove(gpo_path.cache_key_); |
| return ERROR_SMBCLIENT_FAILED; |
| } |
| } |
| } |
| |
| // Gather a list of existing GPO files and update cache. |
| 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_); |
| |
| // Add GPO to the cache even if the file didn't actually download. |
| if (!gpo_path.use_cache_) |
| gpo_version_cache_.Add(gpo_path.cache_key_, gpo_path.version_); |
| } |
| |
| 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, SerializeFlags(flags_)}); |
| 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::UpdateDevicePolicyDependencies( |
| const em::ChromeDeviceSettingsProto& device_policy) { |
| has_device_policy_ = true; |
| |
| // Get Kerberos encryption types policy. Note that we fall back to strong |
| // encryption if the policy is not set. |
| KerberosEncryptionTypes enc_types = GetEncryptionTypes(device_policy); |
| SetKerberosEncryptionTypes(enc_types); |
| |
| // Get loopback processing mode. |
| user_policy_mode_ = GetUserPolicyMode(device_policy); |
| |
| // Update machine password change rate. Use the default 30 days for now until |
| // the DeviceMachinePasswordChangeRate arrives in Chrome OS. |
| base::TimeDelta password_change_rate = |
| GetMachinePasswordChangeRate(device_policy); |
| UpdateMachinePasswordAutoChange(password_change_rate); |
| |
| // Update GPO version cache. The cache is disabled if the TTL is 0. |
| gpo_version_cache_ttl_ = GetGpoVersionCacheTTL(device_policy); |
| gpo_version_cache_.SetEnabled(gpo_version_cache_ttl_ > kZeroDelta); |
| if (!gpo_version_cache_.IsEnabled()) |
| gpo_version_cache_.Clear(); |
| |
| // Update auth data cache. The cache is disabled if the TTL is 0. |
| auth_data_cache_ttl_ = GetAuthDataCacheTTL(device_policy); |
| auth_data_cache_.SetEnabled(auth_data_cache_ttl_ > kZeroDelta); |
| if (!auth_data_cache_.IsEnabled()) { |
| auth_data_cache_.Clear(); |
| const base::FilePath cache_path(paths_->Get(Path::AUTH_DATA_CACHE)); |
| if (!base::DeleteFile(cache_path, false /* recursive */)) { |
| LOG(ERROR) << "Failed delete auth data cache file '" << cache_path.value() |
| << "'"; |
| } |
| } |
| } |
| |
| void SambaInterface::UpdateUserAffiliation() { |
| // Must be called after successful login. |
| DCHECK(user_logged_in_); |
| |
| // Figure out if the user is affiliated. |
| is_user_affiliated_ = IsUserAffiliated(); |
| LOG(INFO) << "User is " << (is_user_affiliated_ ? "" : "not ") |
| << "affiliated"; |
| |
| // Cache auth data, but ONLY if the user is affiliated (for privacy reasons). |
| if (is_user_affiliated_) |
| UpdateAuthDataCache(user_account_, is_user_affiliated_); |
| } |
| |
| void SambaInterface::UpdateAuthDataCache(const AccountData& account, |
| bool is_affiliated) { |
| // Update cache. |
| auth_data_cache_.SetWorkgroup(account.realm, account.workgroup); |
| auth_data_cache_.SetKdcIp(account.realm, account.kdc_ip); |
| auth_data_cache_.SetDcName(account.realm, account.dc_name); |
| auth_data_cache_.SetIsAffiliated(account.realm, is_affiliated); |
| |
| // Flush cache to file. Do a best effort, don't bother if it fails. |
| const base::FilePath cache_path(paths_->Get(Path::AUTH_DATA_CACHE)); |
| auth_data_cache_.Save(cache_path); |
| } |
| |
| void SambaInterface::UpdateMachinePasswordAutoChange( |
| const base::TimeDelta& rate) { |
| password_change_rate_ = rate; |
| |
| // Disable password auto change if the rate is non-positive. |
| if (password_change_rate_ <= base::TimeDelta::FromDays(0)) { |
| password_change_timer_.Stop(); |
| return; |
| } |
| |
| // Are we using a machine password at all? Devices joined before the switch |
| // from keytab to password still use keytabs, so changing the machine password |
| // isn't possible. |
| if (!base::PathExists(base::FilePath(paths_->Get(Path::MACHINE_PASS)))) { |
| LOG(WARNING) |
| << "Cannot change the machine password since this devices still uses " |
| "the keytab file. Re-enrolling the device will fix this."; |
| return; |
| } |
| |
| // Start timer for the password change checker. |
| if (!password_change_timer_.IsRunning()) { |
| password_change_timer_.Start( |
| FROM_HERE, kPasswordChangeCheckRate, this, |
| &SambaInterface::AutoCheckMachinePasswordChange); |
| |
| // Perform a check immediately. This usually happens on startup and makes |
| // sure we do at least one check during a session. |
| AutoCheckMachinePasswordChange(); |
| } |
| } |
| |
| void SambaInterface::AutoCheckMachinePasswordChange() { |
| LOG(INFO) << "Running scheduled machine password age check"; |
| ErrorType error = CheckMachinePasswordChange(); |
| if (error != ERROR_NONE) |
| LOG(ERROR) << "Machine password check failed with error " << error; |
| did_password_change_check_run_for_testing_ = true; |
| metrics_->ReportError(ERROR_OF_AUTO_MACHINE_PASSWORD_CHANGE, error); |
| } |
| |
| ErrorType SambaInterface::CheckMachinePasswordChange() { |
| // Get the latest server time and KDC IP. Reset |server_time| to enforce an |
| // update (otherwise, the cached values are kept). |
| device_account_.server_time = base::Time(); |
| ErrorType error = UpdateAccountData(&device_account_); |
| if (error != ERROR_NONE) |
| return error; |
| device_tgt_manager_.SetKdcIp(device_account_.kdc_ip); |
| |
| const base::FilePath password_path(paths_->Get(Path::MACHINE_PASS)); |
| base::File::Info file_info; |
| if (!GetFileInfo(password_path, &file_info)) { |
| LOG(ERROR) |
| << "Machine password check failed. Could not get info for machine " |
| << "password file '" << password_path.value() << "'"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| // Check if the password is older than the change rate (=max age). |
| base::TimeDelta password_age = |
| device_account_.server_time - file_info.last_modified; |
| if (password_age < password_change_rate_) { |
| int total_hours_left = (password_change_rate_ - password_age).InHours(); |
| int days_left = total_hours_left / base::Time::kHoursPerDay; |
| int hours_left = total_hours_left % base::Time::kHoursPerDay; |
| |
| LOG(INFO) << "No need to change machine password (" << days_left << "d " |
| << hours_left << "h left)"; |
| return ERROR_NONE; |
| } |
| |
| LOG(INFO) << "Machine password is older than " |
| << password_change_rate_.InDays() << " days. Changing."; |
| |
| // Read the old password. |
| std::string old_password; |
| if (!ReadMachinePasswordToString(password_path, &old_password)) |
| return ERROR_LOCAL_IO; |
| |
| // Generate and write a new password. |
| const std::string new_password = GenerateRandomMachinePassword(); |
| error = WriteMachinePassword(Path::NEW_MACHINE_PASS, new_password); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Change the machine password on the server. |
| error = device_tgt_manager_.ChangePassword(old_password, new_password); |
| if (error != ERROR_NONE) |
| return error; |
| |
| // Roll password files. |
| error = RollMachinePassword(); |
| |
| if (error != ERROR_NONE) { |
| // Try writing the new password directly, ignoring the previous one. |
| error = WriteMachinePassword(Path::MACHINE_PASS, new_password); |
| } |
| |
| if (error != ERROR_NONE) { |
| // Do a best effort recovering the old password. If that doesn't work, we |
| // won't be able to access the machine account anymore! |
| ErrorType change_back_error = |
| device_tgt_manager_.ChangePassword(new_password, old_password); |
| ErrorType write_error = |
| WriteMachinePassword(Path::MACHINE_PASS, old_password); |
| if (change_back_error != ERROR_NONE || write_error != ERROR_NONE) { |
| LOG(ERROR) << "Recovering the old machine password failed. Your device " |
| "is in an invalid state and needs to be re-enrolled."; |
| } |
| return error; |
| } |
| |
| LOG(INFO) << "Successfully changed machine password"; |
| return ERROR_NONE; |
| } |
| |
| void SambaInterface::SetUserAccountId(const std::string& account_id) { |
| // Don't allow authenticating multiple users. Chrome should prevent that. |
| CHECK(!account_id.empty()); |
| if (user_account_id_ == account_id) |
| return; |
| CHECK(user_account_id_.empty()) << "Multi-user not supported"; |
| user_account_id_ = account_id; |
| |
| // Get the user daemon store path to back up auth data. |
| DCHECK(cryptohome_client_); |
| std::string sanitized_username = |
| cryptohome_client_->GetSanitizedUsername(GetAccountIdKey(account_id)); |
| if (sanitized_username.empty()) { |
| LOG(ERROR) << "Failed to get sanitized username. " |
| "Auth state backups won't work."; |
| return; |
| } |
| user_daemon_store_path_ = base::FilePath(paths_->Get(Path::DAEMON_STORE_DIR)) |
| .Append(sanitized_username); |
| } |
| |
| void SambaInterface::SetUserRealm(const std::string& user_realm) { |
| // Allow setting the realm only once. This makes sure that nobody calls |
| // AuthenticateUser() with a different realm, the call fails and we're stuck |
| // with a wrong realm. |
| CHECK(!user_realm.empty()); |
| CHECK(user_account_.realm.empty() || user_account_.realm == user_realm) |
| << "Multi-user not supported"; |
| user_account_.realm = user_realm; |
| user_tgt_manager_.SetRealm(user_account_.realm); |
| AnonymizeRealm(user_realm, kUserRealmPlaceholder); |
| } |
| |
| ErrorType SambaInterface::MaybeSetFakeDomainSid(const AccountData& account) { |
| // Don't set twice for the same workgroup. |
| if (fake_domain_sid_was_set_for_workgroup_.find(account.workgroup) != |
| fake_domain_sid_was_set_for_workgroup_.end()) |
| return ERROR_NONE; |
| |
| // Reuse the NET_ADS_SECCOMP filter for simplicity, even though it's not a net |
| // ads command. |
| ProcessExecutor net_cmd({paths_->Get(Path::NET), "setdomainsid", |
| kFakeDomainSid, kConfigParam, |
| paths_->Get(account.smb_conf_path), kDebugParam, |
| flags_.net_log_level()}); |
| if (!jail_helper_.SetupJailAndRun(&net_cmd, Path::NET_ADS_SECCOMP, |
| TIMER_NONE)) { |
| // This is actually a local operation. |
| LOG(ERROR) << "Failed to set fake domain SID"; |
| return ERROR_LOCAL_IO; |
| } |
| |
| // Mark as set for this workgroup. |
| fake_domain_sid_was_set_for_workgroup_.insert(account.workgroup); |
| return ERROR_NONE; |
| } |
| |
| void SambaInterface::InitDeviceAccount(const std::string& netbios_name, |
| const std::string& realm) { |
| device_account_.netbios_name = netbios_name; |
| device_account_.user_name = device_account_.netbios_name + "$"; |
| device_account_.realm = realm; |
| device_tgt_manager_.SetRealm(device_account_.realm); |
| device_tgt_manager_.SetPrincipal(device_account_.GetPrincipal()); |
| } |
| |
| void SambaInterface::SetKerberosEncryptionTypes( |
| KerberosEncryptionTypes encryption_types) { |
| if (encryption_types_ != encryption_types) { |
| LOG(INFO) << "Kerberos encryption types changed to " |
| << GetEncryptionTypesString(encryption_types); |
| } |
| encryption_types_ = encryption_types; |
| user_tgt_manager_.SetKerberosEncryptionTypes(encryption_types_); |
| device_tgt_manager_.SetKerberosEncryptionTypes(encryption_types_); |
| } |
| |
| void SambaInterface::MaybeBackupUserAuthState() { |
| if (!user_logged_in_ || !in_user_session_ || user_daemon_store_path_.empty()) |
| return; |
| DCHECK(!user_account_id_.empty()); |
| |
| // Since we're in the session, Cryptohome should be mounted. |
| DCHECK(base::PathExists(user_daemon_store_path_)); |
| |
| // Back up TGT state. |
| protos::UserBackupData data; |
| if (!user_tgt_manager_.Backup(data.mutable_tgt_state())) |
| return; |
| |
| // Put all other data we want to serialize into the proto. |
| data.set_pwd_last_set(user_pwd_last_set_); |
| data.set_user_name(user_account_.user_name); |
| data.set_is_user_affiliated(is_user_affiliated_); |
| data.set_user_realm(user_account_.realm); |
| |
| // Convert proto to string. |
| std::string data_blob; |
| if (!data.SerializeToString(&data_blob)) { |
| LOG(WARNING) << "Backup failed to serialize backup data to string"; |
| return; |
| } |
| |
| // Save string to disk. |
| const int size = static_cast<int>(data_blob.size()); |
| const base::FilePath backup_path = |
| user_daemon_store_path_.Append(kBackupFileName); |
| if (base::WriteFile(backup_path, data_blob.data(), size) != size) { |
| LOG(WARNING) << "Backup failed to write data to " << backup_path.value(); |
| return; |
| } |
| |
| LOG(INFO) << "Backup successfully written to " << backup_path.value(); |
| } |
| |
| void SambaInterface::MaybeRestoreUserAuthState() { |
| if (user_logged_in_ || !in_user_session_ || user_daemon_store_path_.empty()) |
| return; |
| DCHECK(!user_account_id_.empty()); |
| |
| // Exit quietly if the backup path doesn't exist (yet). |
| const base::FilePath backup_path = |
| user_daemon_store_path_.Append(kBackupFileName); |
| if (!base::PathExists(backup_path)) |
| return; |
| |
| // Read string from disk. |
| std::string data_blob; |
| if (!base::ReadFileToStringWithMaxSize(backup_path, &data_blob, |
| kMaxBackupSizeBytes)) { |
| PLOG(ERROR) << "Backup failed to read data from " << backup_path.value(); |
| return; |
| } |
| |
| // Convert string to proto. |
| protos::UserBackupData data; |
| if (!data.ParseFromString(data_blob)) { |
| LOG(WARNING) << "Backup failed to parse backup data from string"; |
| return; |
| } |
| |
| // Check proto. |
| if (!data.has_tgt_state() || !data.has_pwd_last_set() || |
| !data.has_user_name() || data.user_name().empty() || |
| !data.has_is_user_affiliated()) { |
| LOG(WARNING) << "Backup data is bad"; |
| return; |
| } |
| |
| // Restore TGT state. |
| if (!user_tgt_manager_.Restore(data.tgt_state())) |
| return; |
| |
| // Restore all other data from the proto. |
| user_pwd_last_set_ = data.pwd_last_set(); |
| user_account_.user_name = data.user_name(); |
| is_user_affiliated_ = data.is_user_affiliated(); |
| // User realm might be missing in old backup data. New data should have it. |
| if (data.has_user_realm()) |
| SetUserRealm(data.user_realm()); |
| user_logged_in_ = true; |
| |
| LOG(INFO) << "Backup successfully restored from " << backup_path.value(); |
| } |
| |
| void SambaInterface::AnonymizeRealm(const std::string& realm, |
| const char* placeholder) { |
| anonymizer_->SetReplacementAllCases(realm, placeholder); |
| |
| std::vector<std::string> parts = base::SplitString( |
| realm, ".", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| for (const auto& part : parts) |
| anonymizer_->SetReplacementAllCases(part, placeholder); |
| } |
| |
| bool SambaInterface::IsDeviceJoined() const { |
| DCHECK(device_account_.realm.empty() ^ !device_account_.netbios_name.empty()); |
| return !device_account_.realm.empty() && |
| !device_account_.netbios_name.empty(); |
| } |
| |
| void SambaInterface::Reset() { |
| user_account_id_.clear(); |
| user_pwd_last_set_ = 0; |
| user_logged_in_ = false; |
| is_user_affiliated_ = false; |
| user_account_ = AccountData(Path::USER_SMB_CONF); |
| device_account_ = AccountData(Path::DEVICE_SMB_CONF); |
| user_tgt_manager_.Reset(); |
| device_tgt_manager_.Reset(); |
| gpo_version_cache_.Clear(); |
| gpo_version_cache_.SetEnabled(true); |
| gpo_version_cache_ttl_ = kDefaultGpoVersionCacheTTL; |
| auth_data_cache_.Clear(); |
| auth_data_cache_.SetEnabled(true); |
| auth_data_cache_ttl_ = kDefaultAuthDataCacheTTL; |
| SetKerberosEncryptionTypes(ENC_TYPES_STRONG); |
| user_policy_mode_ = |
| em::DeviceUserPolicyLoopbackProcessingModeProto::USER_POLICY_MODE_DEFAULT; |
| password_change_timer_.Stop(); |
| password_change_rate_ = base::TimeDelta(); |
| has_device_policy_ = false; |
| device_policy_impl_for_testing.reset(); |
| did_password_change_check_run_for_testing_ = false; |
| } |
| |
| void SambaInterface::LoadFlagsDefaultLevel() { |
| const base::FilePath default_level_path( |
| paths_->Get(Path::FLAGS_DEFAULT_LEVEL)); |
| if (!CheckFlagsDefaultLevelValid(default_level_path)) |
| return; |
| std::string level_str; |
| if (!base::ReadFileToStringWithMaxSize(default_level_path, &level_str, 16)) { |
| PLOG(ERROR) << "Failed to read flags default level from '" |
| << default_level_path.value() << "'"; |
| return; |
| } |
| int level_int; |
| if (!base::StringToInt(level_str, &level_int) || |
| level_int < AuthPolicyFlags::kMinLevel || |
| level_int > AuthPolicyFlags::kMaxLevel) { |
| LOG(ERROR) << "Bad flags default level '" << level_str << "'"; |
| return; |
| } |
| flags_default_level_ = static_cast<AuthPolicyFlags::DefaultLevel>(level_int); |
| LOG(INFO) << "Flags default level = " << flags_default_level_; |
| } |
| |
| void SambaInterface::SaveFlagsDefaultLevel() { |
| const base::FilePath default_level_path( |
| paths_->Get(Path::FLAGS_DEFAULT_LEVEL)); |
| const std::string level_str = std::to_string(flags_default_level_); |
| const int size = static_cast<int>(level_str.size()); |
| if (flags_default_level_ == AuthPolicyFlags::kQuiet) { |
| // Remove the file, kQuiet is the default, anyway. |
| if (!base::DeleteFile(default_level_path, false /* recursive */)) { |
| PLOG(ERROR) << "Failed to delete flags default level file '" |
| << default_level_path.value() << "'"; |
| } |
| } else { |
| // Write the file. |
| if (base::WriteFile(default_level_path, level_str.data(), size) != size) { |
| PLOG(ERROR) << "Failed to write flags default level to '" |
| << default_level_path.value() << "'"; |
| } |
| } |
| } |
| |
| void SambaInterface::ReloadDebugFlags() { |
| const base::FilePath default_level_path( |
| paths_->Get(Path::FLAGS_DEFAULT_LEVEL)); |
| if (flags_default_level_ != AuthPolicyFlags::kQuiet && |
| !CheckFlagsDefaultLevelValid(default_level_path)) { |
| // Default flags file expired, reset default level. |
| flags_default_level_ = AuthPolicyFlags::kQuiet; |
| } |
| |
| // First set defaults, then load file on top. |
| AuthPolicyFlags flags_container; |
| flags_container.SetDefaults(flags_default_level_); |
| const base::FilePath path(paths_->Get(Path::DEBUG_FLAGS)); |
| if (flags_container.LoadFromJsonFile(path) || |
| flags_default_level_ != AuthPolicyFlags::kQuiet) { |
| flags_container.Dump(); |
| } |
| flags_ = flags_container.Get(); |
| if (disable_seccomp_for_testing_) |
| flags_.set_disable_seccomp(true); |
| |
| // Toggle anonymizer. |
| anonymizer_->set_disabled(flags_.disable_anonymizer()); |
| } |
| |
| } // namespace authpolicy |