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