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.
5 #include "base/enterprise_util.h"
7 #import <OpenDirectory/OpenDirectory.h>
12 #include "base/logging.h"
13 #include "base/mac/foundation_util.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"
21 bool IsManagedDevice() {
22 // MDM enrollment indicates the device is actively being managed. Simply being
23 // joined to a domain, however, does not.
25 // IsDeviceRegisteredWithManagementNew is only available after 10.13.4.
26 // Eventually switch to it when that is the minimum OS required by Chromium.
27 if (@available(macOS 10.13.4, *)) {
28 base::MacDeviceManagementStateNew mdm_state =
29 base::IsDeviceRegisteredWithManagementNew();
31 base::MacDeviceManagementStateNew::kLimitedMDMEnrollment ||
32 mdm_state == base::MacDeviceManagementStateNew::kFullMDMEnrollment ||
33 mdm_state == base::MacDeviceManagementStateNew::kDEPMDMEnrollment;
36 base::MacDeviceManagementStateOld mdm_state =
37 base::IsDeviceRegisteredWithManagementOld();
38 return mdm_state == base::MacDeviceManagementStateOld::kMDMEnrollment;
41 bool IsEnterpriseDevice() {
42 // Domain join is a basic indicator of being an enterprise device.
43 DeviceUserDomainJoinState join_state = AreDeviceAndUserJoinedToDomain();
44 return join_state.device_joined || join_state.user_joined;
47 MacDeviceManagementStateOld IsDeviceRegisteredWithManagementOld() {
48 static MacDeviceManagementStateOld state = [] {
50 std::vector<std::string> profiler_argv{"/usr/sbin/system_profiler",
51 "SPConfigurationProfileDataType",
58 std::string profiler_stdout;
59 if (!GetAppOutput(profiler_argv, &profiler_stdout)) {
60 LOG(WARNING) << "Could not get system_profiler output.";
61 return MacDeviceManagementStateOld::kFailureAPIUnavailable;
64 NSArray* root = base::mac::ObjCCast<NSArray>([NSPropertyListSerialization
65 propertyListWithData:[SysUTF8ToNSString(profiler_stdout)
66 dataUsingEncoding:NSUTF8StringEncoding]
67 options:NSPropertyListImmutable
71 LOG(WARNING) << "Could not parse system_profiler output.";
72 return MacDeviceManagementStateOld::kFailureUnableToParseResult;
75 for (NSDictionary* results in root) {
76 for (NSDictionary* dict in results[@"_items"]) {
77 for (NSDictionary* device_config_profiles in dict[@"_items"]) {
78 for (NSDictionary* profile_item in
79 device_config_profiles[@"_items"]) {
80 if (![profile_item[@"_name"] isEqual:@"com.apple.mdm"])
83 NSString* payload_data =
84 profile_item[@"spconfigprofile_payload_data"];
85 NSDictionary* payload_data_dict =
86 base::mac::ObjCCast<NSDictionary>([NSPropertyListSerialization
88 [payload_data dataUsingEncoding:NSUTF8StringEncoding]
89 options:NSPropertyListImmutable
93 if (!payload_data_dict)
96 // Verify that the URL validates.
97 if ([NSURL URLWithString:payload_data_dict[@"CheckInURL"]])
98 return MacDeviceManagementStateOld::kMDMEnrollment;
104 return MacDeviceManagementStateOld::kNoEnrollment;
111 MacDeviceManagementStateNew IsDeviceRegisteredWithManagementNew() {
112 static MacDeviceManagementStateNew state = [] {
113 if (@available(macOS 10.13.4, *)) {
114 std::vector<std::string> profiles_argv{"/usr/bin/profiles", "status",
115 "-type", "enrollment"};
117 std::string profiles_stdout;
118 if (!GetAppOutput(profiles_argv, &profiles_stdout)) {
119 LOG(WARNING) << "Could not get profiles output.";
120 return MacDeviceManagementStateNew::kFailureAPIUnavailable;
123 // Sample output of `profiles` with full MDM enrollment:
124 // Enrolled via DEP: Yes
125 // MDM enrollment: Yes (User Approved)
126 // MDM server: https://applemdm.example.com/some/path?foo=bar
127 StringPairs property_states;
128 if (!SplitStringIntoKeyValuePairs(profiles_stdout, ':', '\n',
130 return MacDeviceManagementStateNew::kFailureUnableToParseResult;
133 bool enrolled_via_dep = false;
134 bool mdm_enrollment_not_approved = false;
135 bool mdm_enrollment_user_approved = false;
137 for (const auto& property_state : property_states) {
138 StringPiece property =
139 TrimString(property_state.first, kWhitespaceASCII, TRIM_ALL);
141 TrimString(property_state.second, kWhitespaceASCII, TRIM_ALL);
143 if (property == "Enrolled via DEP") {
145 enrolled_via_dep = true;
146 else if (state != "No")
147 return MacDeviceManagementStateNew::kFailureUnableToParseResult;
148 } else if (property == "MDM enrollment") {
150 mdm_enrollment_not_approved = true;
151 else if (state == "Yes (User Approved)")
152 mdm_enrollment_user_approved = true;
153 else if (state != "No")
154 return MacDeviceManagementStateNew::kFailureUnableToParseResult;
156 // Ignore any other output lines, for future extensibility.
160 if (!enrolled_via_dep && !mdm_enrollment_not_approved &&
161 !mdm_enrollment_user_approved) {
162 return MacDeviceManagementStateNew::kNoEnrollment;
165 if (!enrolled_via_dep && mdm_enrollment_not_approved &&
166 !mdm_enrollment_user_approved) {
167 return MacDeviceManagementStateNew::kLimitedMDMEnrollment;
170 if (!enrolled_via_dep && !mdm_enrollment_not_approved &&
171 mdm_enrollment_user_approved) {
172 return MacDeviceManagementStateNew::kFullMDMEnrollment;
175 if (enrolled_via_dep && !mdm_enrollment_not_approved &&
176 mdm_enrollment_user_approved) {
177 return MacDeviceManagementStateNew::kDEPMDMEnrollment;
180 return MacDeviceManagementStateNew::kFailureUnableToParseResult;
182 return MacDeviceManagementStateNew::kFailureAPIUnavailable;
189 DeviceUserDomainJoinState AreDeviceAndUserJoinedToDomain() {
190 static DeviceUserDomainJoinState state = [] {
191 DeviceUserDomainJoinState state{false, false};
194 ODSession* session = [ODSession defaultSession];
195 if (session == nil) {
196 DLOG(WARNING) << "ODSession default session is nil.";
200 NSError* error = nil;
202 NSArray<NSString*>* all_node_names =
203 [session nodeNamesAndReturnError:&error];
204 if (!all_node_names) {
205 DLOG(WARNING) << "ODSession failed to give node names: "
206 << error.localizedDescription.UTF8String;
210 NSUInteger num_nodes = all_node_names.count;
212 DLOG(WARNING) << "ODSession returned too few node names: "
213 << all_node_names.description.UTF8String;
218 // Non-enterprise machines have:"/Search", "/Search/Contacts",
219 // "/Local/Default". Everything else would be enterprise management.
220 state.device_joined = true;
223 ODNode* node = [ODNode nodeWithSession:session
224 type:kODNodeTypeAuthentication
227 DLOG(WARNING) << "ODSession cannot obtain the authentication node: "
228 << error.localizedDescription.UTF8String;
232 // Now check the currently logged on user.
233 ODQuery* query = [ODQuery queryWithNode:node
234 forRecordTypes:kODRecordTypeUsers
235 attribute:kODAttributeTypeRecordName
236 matchType:kODMatchEqualTo
237 queryValues:NSUserName()
238 returnAttributes:kODAttributeTypeAllAttributes
242 DLOG(WARNING) << "ODSession cannot create user query: "
243 << mac::NSToCFCast(error);
247 NSArray* results = [query resultsAllowingPartial:NO error:&error];
249 DLOG(WARNING) << "ODSession cannot obtain current user node: "
250 << error.localizedDescription.UTF8String;
254 if (results.count != 1) {
255 DLOG(WARNING) << @"ODSession unexpected number of user nodes: "
259 for (id element in results) {
260 ODRecord* record = mac::ObjCCastStrict<ODRecord>(element);
261 NSArray* attributes =
262 [record valuesForAttribute:kODAttributeTypeMetaRecordName
264 for (id attribute in attributes) {
265 NSString* attribute_value = mac::ObjCCastStrict<NSString>(attribute);
266 // Example: "uid=johnsmith,ou=People,dc=chromium,dc=org
267 NSRange domain_controller =
268 [attribute_value rangeOfString:@"(^|,)\\s*dc="
269 options:NSRegularExpressionSearch];
270 if (domain_controller.length > 0) {
271 state.user_joined = true;
275 // Scan alternative identities.
277 [record valuesForAttribute:kODAttributeTypeAltSecurityIdentities
279 for (id attribute in attributes) {
280 NSString* attribute_value = mac::ObjCCastStrict<NSString>(attribute);
282 [attribute_value rangeOfString:@"CN=com.apple.idms.appleid.prd"
283 options:NSCaseInsensitiveSearch];
284 if (!icloud.length) {
285 // Any alternative identity that is not iCloud is likely enterprise
287 state.user_joined = true;