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