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/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"
21 bool 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;
31 bool 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;
37 MacDeviceManagementState IsDeviceRegisteredWithManagement() {
38 static MacDeviceManagementState state = [] {
39 std::vector<std::string> profiles_argv{"/usr/bin/profiles", "status",
40 "-type", "enrollment"};
42 std::string profiles_stdout;
43 if (!GetAppOutput(profiles_argv, &profiles_stdout)) {
44 LOG(WARNING) << "Could not get profiles output.";
45 return MacDeviceManagementState::kFailureAPIUnavailable;
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',
55 return MacDeviceManagementState::kFailureUnableToParseResult;
58 bool enrolled_via_dep = false;
59 bool mdm_enrollment_not_approved = false;
60 bool mdm_enrollment_user_approved = false;
62 for (const auto& property_state : property_states) {
63 StringPiece property =
64 TrimString(property_state.first, kWhitespaceASCII, TRIM_ALL);
66 TrimString(property_state.second, kWhitespaceASCII, TRIM_ALL);
68 if (property == "Enrolled via DEP") {
70 enrolled_via_dep = true;
71 } else if (state != "No") {
72 return MacDeviceManagementState::kFailureUnableToParseResult;
74 } else if (property == "MDM enrollment") {
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;
83 // Ignore any other output lines, for future extensibility.
87 if (!enrolled_via_dep && !mdm_enrollment_not_approved &&
88 !mdm_enrollment_user_approved) {
89 return MacDeviceManagementState::kNoEnrollment;
92 if (!enrolled_via_dep && mdm_enrollment_not_approved &&
93 !mdm_enrollment_user_approved) {
94 return MacDeviceManagementState::kLimitedMDMEnrollment;
97 if (!enrolled_via_dep && !mdm_enrollment_not_approved &&
98 mdm_enrollment_user_approved) {
99 return MacDeviceManagementState::kFullMDMEnrollment;
102 if (enrolled_via_dep && !mdm_enrollment_not_approved &&
103 mdm_enrollment_user_approved) {
104 return MacDeviceManagementState::kDEPMDMEnrollment;
107 return MacDeviceManagementState::kFailureUnableToParseResult;
113 DeviceUserDomainJoinState AreDeviceAndUserJoinedToDomain() {
114 static DeviceUserDomainJoinState state = [] {
115 DeviceUserDomainJoinState state{false, false};
118 ODSession* session = [ODSession defaultSession];
119 if (session == nil) {
120 DLOG(WARNING) << "ODSession default session is nil.";
124 NSError* error = nil;
126 NSArray<NSString*>* all_node_names =
127 [session nodeNamesAndReturnError:&error];
128 if (!all_node_names) {
129 DLOG(WARNING) << "ODSession failed to give node names: "
130 << error.localizedDescription.UTF8String;
134 NSUInteger num_nodes = all_node_names.count;
136 DLOG(WARNING) << "ODSession returned too few node names: "
137 << all_node_names.description.UTF8String;
142 // Non-enterprise machines have:"/Search", "/Search/Contacts",
143 // "/Local/Default". Everything else would be enterprise management.
144 state.device_joined = true;
147 ODNode* node = [ODNode nodeWithSession:session
148 type:kODNodeTypeAuthentication
151 DLOG(WARNING) << "ODSession cannot obtain the authentication node: "
152 << error.localizedDescription.UTF8String;
156 // Now check the currently logged on user.
157 ODQuery* query = [ODQuery queryWithNode:node
158 forRecordTypes:kODRecordTypeUsers
159 attribute:kODAttributeTypeRecordName
160 matchType:kODMatchEqualTo
161 queryValues:NSUserName()
162 returnAttributes:kODAttributeTypeAllAttributes
166 DLOG(WARNING) << "ODSession cannot create user query: "
167 << error.localizedDescription.UTF8String;
171 NSArray* results = [query resultsAllowingPartial:NO error:&error];
173 DLOG(WARNING) << "ODSession cannot obtain current user node: "
174 << error.localizedDescription.UTF8String;
178 if (results.count != 1) {
179 DLOG(WARNING) << @"ODSession unexpected number of user nodes: "
183 for (id element in results) {
184 ODRecord* record = base::apple::ObjCCastStrict<ODRecord>(element);
185 NSArray* attributes =
186 [record valuesForAttribute:kODAttributeTypeMetaRecordName
188 for (id attribute in attributes) {
189 NSString* attribute_value =
190 base::apple::ObjCCastStrict<NSString>(attribute);
191 // Example: "uid=johnsmith,ou=People,dc=chromium,dc=org
192 NSRange domain_controller =
193 [attribute_value rangeOfString:@"(^|,)\\s*dc="
194 options:NSRegularExpressionSearch];
195 if (domain_controller.length > 0) {
196 state.user_joined = true;
200 // Scan alternative identities.
202 [record valuesForAttribute:kODAttributeTypeAltSecurityIdentities
204 for (id attribute in attributes) {
205 NSString* attribute_value =
206 base::apple::ObjCCastStrict<NSString>(attribute);
208 [attribute_value rangeOfString:@"CN=com.apple.idms.appleid.prd"
209 options:NSCaseInsensitiveSearch];
210 if (!icloud.length) {
211 // Any alternative identity that is not iCloud is likely enterprise
213 state.user_joined = true;