• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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