// Copyright 2019 The Chromium 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 "base/enterprise_util.h" #import #include #include #include "base/logging.h" #include "base/mac/foundation_util.h" #include "base/process/launch.h" #include "base/strings/string_split.h" #include "base/strings/string_util.h" #include "base/strings/sys_string_conversions.h" namespace base { bool IsManagedDevice() { // MDM enrollment indicates the device is actively being managed. Simply being // joined to a domain, however, does not. // IsDeviceRegisteredWithManagementNew is only available after 10.13.4. // Eventually switch to it when that is the minimum OS required by Chromium. if (@available(macOS 10.13.4, *)) { base::MacDeviceManagementStateNew mdm_state = base::IsDeviceRegisteredWithManagementNew(); return mdm_state == base::MacDeviceManagementStateNew::kLimitedMDMEnrollment || mdm_state == base::MacDeviceManagementStateNew::kFullMDMEnrollment || mdm_state == base::MacDeviceManagementStateNew::kDEPMDMEnrollment; } base::MacDeviceManagementStateOld mdm_state = base::IsDeviceRegisteredWithManagementOld(); return mdm_state == base::MacDeviceManagementStateOld::kMDMEnrollment; } bool IsEnterpriseDevice() { // Domain join is a basic indicator of being an enterprise device. DeviceUserDomainJoinState join_state = AreDeviceAndUserJoinedToDomain(); return join_state.device_joined || join_state.user_joined; } MacDeviceManagementStateOld IsDeviceRegisteredWithManagementOld() { static MacDeviceManagementStateOld state = [] { @autoreleasepool { std::vector profiler_argv{"/usr/sbin/system_profiler", "SPConfigurationProfileDataType", "-detailLevel", "mini", "-timeout", "15", "-xml"}; std::string profiler_stdout; if (!GetAppOutput(profiler_argv, &profiler_stdout)) { LOG(WARNING) << "Could not get system_profiler output."; return MacDeviceManagementStateOld::kFailureAPIUnavailable; }; NSArray* root = base::mac::ObjCCast([NSPropertyListSerialization propertyListWithData:[SysUTF8ToNSString(profiler_stdout) dataUsingEncoding:NSUTF8StringEncoding] options:NSPropertyListImmutable format:nil error:nil]); if (!root) { LOG(WARNING) << "Could not parse system_profiler output."; return MacDeviceManagementStateOld::kFailureUnableToParseResult; }; for (NSDictionary* results in root) { for (NSDictionary* dict in results[@"_items"]) { for (NSDictionary* device_config_profiles in dict[@"_items"]) { for (NSDictionary* profile_item in device_config_profiles[@"_items"]) { if (![profile_item[@"_name"] isEqual:@"com.apple.mdm"]) continue; NSString* payload_data = profile_item[@"spconfigprofile_payload_data"]; NSDictionary* payload_data_dict = base::mac::ObjCCast([NSPropertyListSerialization propertyListWithData: [payload_data dataUsingEncoding:NSUTF8StringEncoding] options:NSPropertyListImmutable format:nil error:nil]); if (!payload_data_dict) continue; // Verify that the URL validates. if ([NSURL URLWithString:payload_data_dict[@"CheckInURL"]]) return MacDeviceManagementStateOld::kMDMEnrollment; } } } } return MacDeviceManagementStateOld::kNoEnrollment; } }(); return state; } MacDeviceManagementStateNew IsDeviceRegisteredWithManagementNew() { static MacDeviceManagementStateNew state = [] { if (@available(macOS 10.13.4, *)) { std::vector profiles_argv{"/usr/bin/profiles", "status", "-type", "enrollment"}; std::string profiles_stdout; if (!GetAppOutput(profiles_argv, &profiles_stdout)) { LOG(WARNING) << "Could not get profiles output."; return MacDeviceManagementStateNew::kFailureAPIUnavailable; } // Sample output of `profiles` with full MDM enrollment: // Enrolled via DEP: Yes // MDM enrollment: Yes (User Approved) // MDM server: https://applemdm.example.com/some/path?foo=bar StringPairs property_states; if (!SplitStringIntoKeyValuePairs(profiles_stdout, ':', '\n', &property_states)) { return MacDeviceManagementStateNew::kFailureUnableToParseResult; } bool enrolled_via_dep = false; bool mdm_enrollment_not_approved = false; bool mdm_enrollment_user_approved = false; for (const auto& property_state : property_states) { StringPiece property = TrimString(property_state.first, kWhitespaceASCII, TRIM_ALL); StringPiece state = TrimString(property_state.second, kWhitespaceASCII, TRIM_ALL); if (property == "Enrolled via DEP") { if (state == "Yes") enrolled_via_dep = true; else if (state != "No") return MacDeviceManagementStateNew::kFailureUnableToParseResult; } else if (property == "MDM enrollment") { if (state == "Yes") mdm_enrollment_not_approved = true; else if (state == "Yes (User Approved)") mdm_enrollment_user_approved = true; else if (state != "No") return MacDeviceManagementStateNew::kFailureUnableToParseResult; } else { // Ignore any other output lines, for future extensibility. } } if (!enrolled_via_dep && !mdm_enrollment_not_approved && !mdm_enrollment_user_approved) { return MacDeviceManagementStateNew::kNoEnrollment; } if (!enrolled_via_dep && mdm_enrollment_not_approved && !mdm_enrollment_user_approved) { return MacDeviceManagementStateNew::kLimitedMDMEnrollment; } if (!enrolled_via_dep && !mdm_enrollment_not_approved && mdm_enrollment_user_approved) { return MacDeviceManagementStateNew::kFullMDMEnrollment; } if (enrolled_via_dep && !mdm_enrollment_not_approved && mdm_enrollment_user_approved) { return MacDeviceManagementStateNew::kDEPMDMEnrollment; } return MacDeviceManagementStateNew::kFailureUnableToParseResult; } else { return MacDeviceManagementStateNew::kFailureAPIUnavailable; } }(); return state; } DeviceUserDomainJoinState AreDeviceAndUserJoinedToDomain() { static DeviceUserDomainJoinState state = [] { DeviceUserDomainJoinState state{false, false}; @autoreleasepool { ODSession* session = [ODSession defaultSession]; if (session == nil) { DLOG(WARNING) << "ODSession default session is nil."; return state; } NSError* error = nil; NSArray* all_node_names = [session nodeNamesAndReturnError:&error]; if (!all_node_names) { DLOG(WARNING) << "ODSession failed to give node names: " << error.localizedDescription.UTF8String; return state; } NSUInteger num_nodes = all_node_names.count; if (num_nodes < 3) { DLOG(WARNING) << "ODSession returned too few node names: " << all_node_names.description.UTF8String; return state; } if (num_nodes > 3) { // Non-enterprise machines have:"/Search", "/Search/Contacts", // "/Local/Default". Everything else would be enterprise management. state.device_joined = true; } ODNode* node = [ODNode nodeWithSession:session type:kODNodeTypeAuthentication error:&error]; if (node == nil) { DLOG(WARNING) << "ODSession cannot obtain the authentication node: " << error.localizedDescription.UTF8String; return state; } // Now check the currently logged on user. ODQuery* query = [ODQuery queryWithNode:node forRecordTypes:kODRecordTypeUsers attribute:kODAttributeTypeRecordName matchType:kODMatchEqualTo queryValues:NSUserName() returnAttributes:kODAttributeTypeAllAttributes maximumResults:0 error:&error]; if (query == nil) { DLOG(WARNING) << "ODSession cannot create user query: " << mac::NSToCFCast(error); return state; } NSArray* results = [query resultsAllowingPartial:NO error:&error]; if (!results) { DLOG(WARNING) << "ODSession cannot obtain current user node: " << error.localizedDescription.UTF8String; return state; } if (results.count != 1) { DLOG(WARNING) << @"ODSession unexpected number of user nodes: " << results.count; } for (id element in results) { ODRecord* record = mac::ObjCCastStrict(element); NSArray* attributes = [record valuesForAttribute:kODAttributeTypeMetaRecordName error:nil]; for (id attribute in attributes) { NSString* attribute_value = mac::ObjCCastStrict(attribute); // Example: "uid=johnsmith,ou=People,dc=chromium,dc=org NSRange domain_controller = [attribute_value rangeOfString:@"(^|,)\\s*dc=" options:NSRegularExpressionSearch]; if (domain_controller.length > 0) { state.user_joined = true; } } // Scan alternative identities. attributes = [record valuesForAttribute:kODAttributeTypeAltSecurityIdentities error:nil]; for (id attribute in attributes) { NSString* attribute_value = mac::ObjCCastStrict(attribute); NSRange icloud = [attribute_value rangeOfString:@"CN=com.apple.idms.appleid.prd" options:NSCaseInsensitiveSearch]; if (!icloud.length) { // Any alternative identity that is not iCloud is likely enterprise // management. state.user_joined = true; } } } } return state; }(); return state; } } // namespace base