xref: /aosp_15_r20/external/autotest/client/common_lib/cros/authpolicy.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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