xref: /aosp_15_r20/external/cronet/base/enterprise_util_mac.mm (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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