1# Lint as: python2, python3 2# Copyright 2017 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5""" 6Wrapper for D-Bus calls ot the AuthPolicy daemon. 7""" 8 9from __future__ import absolute_import 10from __future__ import division 11from __future__ import print_function 12 13import logging 14import os 15import sys 16 17import common 18import dbus 19 20from autotest_lib.client.common_lib import error 21from autotest_lib.client.common_lib import utils 22from autotest_lib.client.cros import upstart 23 24 25class AuthPolicy(object): 26 """ 27 Wrapper for D-Bus calls ot the AuthPolicy daemon. 28 29 The AuthPolicy daemon handles Active Directory domain join, user 30 authentication and policy fetch. This class is a wrapper around the D-Bus 31 interface to the daemon. 32 33 """ 34 35 # Log file written by authpolicyd. 36 _LOG_FILE = '/var/log/authpolicy.log' 37 38 # Number of log lines to include in error logs. 39 _LOG_LINE_LIMIT = 50 40 41 # The usual system log file (minijail logs there!). 42 _SYSLOG_FILE = '/var/log/messages' 43 44 # Authpolicy daemon D-Bus parameters. 45 _DBUS_SERVICE_NAME = 'org.chromium.AuthPolicy' 46 _DBUS_SERVICE_PATH = '/org/chromium/AuthPolicy' 47 _DBUS_INTERFACE_NAME = 'org.chromium.AuthPolicy' 48 _DBUS_ERROR_SERVICE_UNKNOWN = 'org.freedesktop.DBus.Error.ServiceUnknown' 49 50 # Default timeout in seconds for D-Bus calls. 51 _DEFAULT_TIMEOUT = 120 52 53 def __init__(self, bus_loop, proto_binding_location): 54 """ 55 Constructor 56 57 Creates and returns a D-Bus connection to authpolicyd. The daemon must 58 be running. 59 60 @param bus_loop: glib main loop object. 61 @param proto_binding_location: the location of generated python bindings 62 for authpolicy protobufs. 63 """ 64 65 # Pull in protobuf bindings. 66 sys.path.append(proto_binding_location) 67 68 self._bus_loop = bus_loop 69 self.restart() 70 71 def restart(self): 72 """ 73 Restarts authpolicyd and rebinds to D-Bus interface. 74 """ 75 logging.info('restarting authpolicyd') 76 upstart.restart_job('authpolicyd') 77 bus = dbus.SystemBus(self._bus_loop) 78 proxy = bus.get_object(self._DBUS_SERVICE_NAME, 79 self._DBUS_SERVICE_PATH) 80 self._authpolicyd = dbus.Interface(proxy, self._DBUS_INTERFACE_NAME) 81 82 def stop(self): 83 """ 84 Turns debug logs off. 85 86 Stops authpolicyd. 87 """ 88 logging.info('stopping authpolicyd') 89 90 # Reset log level and stop. Ignore errors that occur when authpolicy is 91 # already down. 92 try: 93 self.set_default_log_level(0) 94 except dbus.exceptions.DBusException as ex: 95 if ex.get_dbus_name() != self._DBUS_ERROR_SERVICE_UNKNOWN: 96 raise 97 try: 98 upstart.stop_job('authpolicyd') 99 except error.CmdError as ex: 100 if (ex.result_obj.exit_status == 0): 101 raise 102 103 self._authpolicyd = None 104 105 def join_ad_domain(self, 106 user_principal_name, 107 password, 108 machine_name, 109 machine_domain=None, 110 machine_ou=None): 111 """ 112 Joins a machine (=device) to an Active Directory domain. 113 114 @param user_principal_name: Logon name of the user (with @realm) who 115 joins the machine to the domain. 116 @param password: Password corresponding to user_principal_name. 117 @param machine_name: Netbios computer (aka machine) name for the joining 118 device. 119 @param machine_domain: Domain (realm) the machine should be joined to. 120 If not specified, the machine is joined to the user's realm. 121 @param machine_ou: Array of organizational units (OUs) from leaf to 122 root. The machine is put into the leaf OU. If not specified, the 123 machine account is created in the default 'Computers' OU. 124 125 @return A tuple with the ErrorType and the joined domain returned by the 126 D-Bus call. 127 128 """ 129 130 from active_directory_info_pb2 import JoinDomainRequest 131 132 request = JoinDomainRequest() 133 request.user_principal_name = user_principal_name 134 request.machine_name = machine_name 135 if machine_ou: 136 request.machine_ou.extend(machine_ou) 137 if machine_domain: 138 request.machine_domain = machine_domain 139 140 with self.PasswordFd(password) as password_fd: 141 return self._authpolicyd.JoinADDomain( 142 dbus.ByteArray(request.SerializeToString()), 143 dbus.types.UnixFd(password_fd), 144 timeout=self._DEFAULT_TIMEOUT, 145 byte_arrays=True) 146 147 def authenticate_user(self, user_principal_name, account_id, password): 148 """ 149 Authenticates a user with an Active Directory domain. 150 151 @param user_principal_name: User logon name ([email protected]) for the 152 Active Directory domain. 153 #param account_id: User account id (aka objectGUID). May be empty. 154 @param password: Password corresponding to user_principal_name. 155 156 @return A tuple with the ErrorType and an ActiveDirectoryAccountInfo 157 blob string returned by the D-Bus call. 158 159 """ 160 161 from active_directory_info_pb2 import ActiveDirectoryAccountInfo 162 from active_directory_info_pb2 import AuthenticateUserRequest 163 from active_directory_info_pb2 import ERROR_NONE 164 165 request = AuthenticateUserRequest() 166 request.user_principal_name = user_principal_name 167 if account_id: 168 request.account_id = account_id 169 170 with self.PasswordFd(password) as password_fd: 171 error_value, account_info_blob = self._authpolicyd.AuthenticateUser( 172 dbus.ByteArray(request.SerializeToString()), 173 dbus.types.UnixFd(password_fd), 174 timeout=self._DEFAULT_TIMEOUT, 175 byte_arrays=True) 176 account_info = ActiveDirectoryAccountInfo() 177 if error_value == ERROR_NONE: 178 account_info.ParseFromString(account_info_blob) 179 return error_value, account_info 180 181 def refresh_user_policy(self, account_id): 182 """ 183 Fetches user policy and sends it to Session Manager. 184 185 @param account_id: User account ID (aka objectGUID). 186 187 @return ErrorType from the D-Bus call. 188 189 """ 190 191 return self._authpolicyd.RefreshUserPolicy( 192 dbus.String(account_id), 193 timeout=self._DEFAULT_TIMEOUT, 194 byte_arrays=True) 195 196 def refresh_device_policy(self): 197 """ 198 Fetches device policy and sends it to Session Manager. 199 200 @return ErrorType from the D-Bus call. 201 202 """ 203 204 return self._authpolicyd.RefreshDevicePolicy( 205 timeout=self._DEFAULT_TIMEOUT, byte_arrays=True) 206 207 def change_machine_password(self): 208 """ 209 Changes machine password. 210 211 @return ErrorType from the D-Bus call. 212 213 """ 214 return self._authpolicyd.ChangeMachinePasswordForTesting( 215 timeout=self._DEFAULT_TIMEOUT, byte_arrays=True) 216 217 def set_default_log_level(self, level): 218 """ 219 Fetches device policy and sends it to Session Manager. 220 221 @param level: Log level, 0=quiet, 1=taciturn, 2=chatty, 3=verbose. 222 223 @return error_message: Error message, empty if no error occurred. 224 225 """ 226 227 return self._authpolicyd.SetDefaultLogLevel(level, byte_arrays=True) 228 229 def print_log_tail(self): 230 """ 231 Prints out authpolicyd log tail. Catches and prints out errors. 232 233 """ 234 235 try: 236 cmd = 'tail -n %s %s' % (self._LOG_LINE_LIMIT, self._LOG_FILE) 237 log_tail = utils.run(cmd).stdout 238 logging.info('Tail of %s:\n%s', self._LOG_FILE, log_tail) 239 except error.CmdError as ex: 240 logging.error('Failed to print authpolicyd log tail: %s', ex) 241 242 def print_seccomp_failure_info(self): 243 """ 244 Detects seccomp failures and prints out the failing syscall. 245 246 """ 247 248 # Exit code 253 is minijail's marker for seccomp failures. 249 cmd = 'grep -q "exit code 253" %s' % self._LOG_FILE 250 if utils.run(cmd, ignore_status=True).exit_status == 0: 251 logging.error('Seccomp failure detected!') 252 cmd = 'grep -oE "blocked syscall: \\w+" %s | tail -1' % \ 253 self._SYSLOG_FILE 254 try: 255 logging.error(utils.run(cmd).stdout) 256 logging.error( 257 'This can happen if you changed a dependency of ' 258 'authpolicyd. Consider allowlisting this syscall in ' 259 'the appropriate -seccomp.policy file in authpolicyd.' 260 '\n') 261 except error.CmdError as ex: 262 logging.error( 263 'Failed to determine reason for seccomp issue: %s', ex) 264 265 def clear_log(self): 266 """ 267 Clears the authpolicy daemon's log file. 268 269 """ 270 271 try: 272 utils.run('echo "" > %s' % self._LOG_FILE) 273 except error.CmdError as ex: 274 logging.error('Failed to clear authpolicyd log file: %s', ex) 275 276 class PasswordFd(object): 277 """ 278 Writes password into a file descriptor. 279 280 Use in a 'with' statement to automatically close the returned file 281 descriptor. 282 283 @param password: Plaintext password string. 284 285 @return A file descriptor (pipe) containing the password. 286 287 """ 288 289 def __init__(self, password): 290 self._password = password 291 self._read_fd = None 292 293 def __enter__(self): 294 """Creates the password file descriptor.""" 295 self._read_fd, write_fd = os.pipe() 296 os.write(write_fd, self._password.encode('utf-8')) 297 os.close(write_fd) 298 return self._read_fd 299 300 def __exit__(self, my_type, value, traceback): 301 """Closes the password file descriptor again.""" 302 if self._read_fd: 303 os.close(self._read_fd) 304