1// Copyright 2019 The Chromium Authors 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "base/enterprise_util.h" 6 7#import <OpenDirectory/OpenDirectory.h> 8 9#include <string> 10#include <vector> 11 12#include "base/apple/foundation_util.h" 13#include "base/logging.h" 14#include "base/process/launch.h" 15#include "base/strings/string_split.h" 16#include "base/strings/string_util.h" 17#include "base/strings/sys_string_conversions.h" 18 19namespace base { 20 21bool IsManagedDevice() { 22 // MDM enrollment indicates the device is actively being managed. Simply being 23 // joined to a domain, however, does not. 24 base::MacDeviceManagementState mdm_state = 25 base::IsDeviceRegisteredWithManagement(); 26 return mdm_state == base::MacDeviceManagementState::kLimitedMDMEnrollment || 27 mdm_state == base::MacDeviceManagementState::kFullMDMEnrollment || 28 mdm_state == base::MacDeviceManagementState::kDEPMDMEnrollment; 29} 30 31bool IsEnterpriseDevice() { 32 // Domain join is a basic indicator of being an enterprise device. 33 DeviceUserDomainJoinState join_state = AreDeviceAndUserJoinedToDomain(); 34 return join_state.device_joined || join_state.user_joined; 35} 36 37MacDeviceManagementState IsDeviceRegisteredWithManagement() { 38 static MacDeviceManagementState state = [] { 39 std::vector<std::string> profiles_argv{"/usr/bin/profiles", "status", 40 "-type", "enrollment"}; 41 42 std::string profiles_stdout; 43 if (!GetAppOutput(profiles_argv, &profiles_stdout)) { 44 LOG(WARNING) << "Could not get profiles output."; 45 return MacDeviceManagementState::kFailureAPIUnavailable; 46 } 47 48 // Sample output of `profiles` with full MDM enrollment: 49 // Enrolled via DEP: Yes 50 // MDM enrollment: Yes (User Approved) 51 // MDM server: https://applemdm.example.com/some/path?foo=bar 52 StringPairs property_states; 53 if (!SplitStringIntoKeyValuePairs(profiles_stdout, ':', '\n', 54 &property_states)) { 55 return MacDeviceManagementState::kFailureUnableToParseResult; 56 } 57 58 bool enrolled_via_dep = false; 59 bool mdm_enrollment_not_approved = false; 60 bool mdm_enrollment_user_approved = false; 61 62 for (const auto& property_state : property_states) { 63 StringPiece property = 64 TrimString(property_state.first, kWhitespaceASCII, TRIM_ALL); 65 StringPiece state = 66 TrimString(property_state.second, kWhitespaceASCII, TRIM_ALL); 67 68 if (property == "Enrolled via DEP") { 69 if (state == "Yes") { 70 enrolled_via_dep = true; 71 } else if (state != "No") { 72 return MacDeviceManagementState::kFailureUnableToParseResult; 73 } 74 } else if (property == "MDM enrollment") { 75 if (state == "Yes") { 76 mdm_enrollment_not_approved = true; 77 } else if (state == "Yes (User Approved)") { 78 mdm_enrollment_user_approved = true; 79 } else if (state != "No") { 80 return MacDeviceManagementState::kFailureUnableToParseResult; 81 } 82 } else { 83 // Ignore any other output lines, for future extensibility. 84 } 85 } 86 87 if (!enrolled_via_dep && !mdm_enrollment_not_approved && 88 !mdm_enrollment_user_approved) { 89 return MacDeviceManagementState::kNoEnrollment; 90 } 91 92 if (!enrolled_via_dep && mdm_enrollment_not_approved && 93 !mdm_enrollment_user_approved) { 94 return MacDeviceManagementState::kLimitedMDMEnrollment; 95 } 96 97 if (!enrolled_via_dep && !mdm_enrollment_not_approved && 98 mdm_enrollment_user_approved) { 99 return MacDeviceManagementState::kFullMDMEnrollment; 100 } 101 102 if (enrolled_via_dep && !mdm_enrollment_not_approved && 103 mdm_enrollment_user_approved) { 104 return MacDeviceManagementState::kDEPMDMEnrollment; 105 } 106 107 return MacDeviceManagementState::kFailureUnableToParseResult; 108 }(); 109 110 return state; 111} 112 113DeviceUserDomainJoinState AreDeviceAndUserJoinedToDomain() { 114 static DeviceUserDomainJoinState state = [] { 115 DeviceUserDomainJoinState state{.device_joined = false, 116 .user_joined = false}; 117 118 @autoreleasepool { 119 ODSession* session = [ODSession defaultSession]; 120 if (session == nil) { 121 DLOG(WARNING) << "ODSession default session is nil."; 122 return state; 123 } 124 125 // Machines that are domain-joined have nodes under "/LDAPv3" or "/Active 126 // Directory". See https://stackoverflow.com/questions/32470557/ and 127 // https://stackoverflow.com/questions/69093499/, respectively, for 128 // examples. 129 NSError* error = nil; 130 NSArray<NSString*>* node_names = [session nodeNamesAndReturnError:&error]; 131 if (!node_names) { 132 DLOG(WARNING) << "ODSession failed to give node names: " 133 << error.localizedDescription.UTF8String; 134 return state; 135 } 136 137 for (NSString* node_name in node_names) { 138 if ([node_name hasPrefix:@"/LDAPv3"] || 139 [node_name hasPrefix:@"/Active Directory"]) { 140 state.device_joined = true; 141 } 142 } 143 144 ODNode* node = [ODNode nodeWithSession:session 145 type:kODNodeTypeAuthentication 146 error:&error]; 147 if (node == nil) { 148 DLOG(WARNING) << "ODSession cannot obtain the authentication node: " 149 << error.localizedDescription.UTF8String; 150 return state; 151 } 152 153 // Now check the currently logged on user. 154 ODQuery* query = [ODQuery queryWithNode:node 155 forRecordTypes:kODRecordTypeUsers 156 attribute:kODAttributeTypeRecordName 157 matchType:kODMatchEqualTo 158 queryValues:NSUserName() 159 returnAttributes:kODAttributeTypeAllAttributes 160 maximumResults:0 161 error:&error]; 162 if (query == nil) { 163 DLOG(WARNING) << "ODSession cannot create user query: " 164 << error.localizedDescription.UTF8String; 165 return state; 166 } 167 168 NSArray* results = [query resultsAllowingPartial:NO error:&error]; 169 if (!results) { 170 DLOG(WARNING) << "ODSession cannot obtain current user node: " 171 << error.localizedDescription.UTF8String; 172 return state; 173 } 174 175 if (results.count != 1) { 176 DLOG(WARNING) << @"ODSession unexpected number of user nodes: " 177 << results.count; 178 } 179 180 for (id element in results) { 181 ODRecord* record = base::apple::ObjCCastStrict<ODRecord>(element); 182 NSArray* attributes = 183 [record valuesForAttribute:kODAttributeTypeMetaRecordName 184 error:nil]; 185 for (id attribute in attributes) { 186 NSString* attribute_value = 187 base::apple::ObjCCastStrict<NSString>(attribute); 188 // Example: "uid=johnsmith,ou=People,dc=chromium,dc=org 189 NSRange domain_controller = 190 [attribute_value rangeOfString:@"(^|,)\\s*dc=" 191 options:NSRegularExpressionSearch]; 192 if (domain_controller.length > 0) { 193 state.user_joined = true; 194 } 195 } 196 197 // Scan alternative identities. 198 attributes = 199 [record valuesForAttribute:kODAttributeTypeAltSecurityIdentities 200 error:nil]; 201 for (id attribute in attributes) { 202 NSString* attribute_value = 203 base::apple::ObjCCastStrict<NSString>(attribute); 204 NSRange icloud = 205 [attribute_value rangeOfString:@"CN=com.apple.idms.appleid.prd" 206 options:NSCaseInsensitiveSearch]; 207 if (!icloud.length) { 208 // Any alternative identity that is not iCloud is likely enterprise 209 // management. 210 state.user_joined = true; 211 } 212 } 213 } 214 } 215 216 return state; 217 }(); 218 219 return state; 220} 221 222} // namespace base 223