xpra icon
Bug tracker and wiki

This bug tracker and wiki are being discontinued
please use https://github.com/Xpra-org/xpra instead.


Ticket #2989: pamexec_auth.py

File pamexec_auth.py, 8.4 KB (added by louis-mulder, 4 months ago)

Python version of pamexec_auth.py

Line 
1# This file is part of Xpra.
2# Copyright (C) 2013-2018 Antoine Martin <antoine@xpra.org>
3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
4# later version. See the file COPYING for details.
5#
6# Merge of exec_auth and pam_auth
7#
8# Louis Mulder 2020
9#
10
11import os
12import sys
13import subprocess
14import glib
15from xpra.os_util import OSX
16from xpra.child_reaper import getChildReaper
17
18from xpra.util import (
19    first_time,
20    csv, merge_dicts, typedict, notypedict, flatten_dict, parse_simple_dict,
21    repr_ellipsized, dump_all_frames, nonl, envint, envbool, envfloat,
22    SERVER_SHUTDOWN, SERVER_UPGRADE, LOGIN_TIMEOUT, DONE, PROTOCOL_ERROR,
23    SERVER_ERROR, VERSION_ERROR, CLIENT_REQUEST, SERVER_EXIT,
24    )
25
26from xpra.server.server_core import get_server_info
27from xpra.net.net_util import get_network_caps, get_info
28
29from xpra.scripts.config import parse_bool
30from xpra.server.auth.sys_auth_base import SysAuthenticator, init, log
31assert init and log #tests will disable logging from here
32
33PAM_AUTH_SERVICE = os.environ.get("XPRA_PAM_AUTH_SERVICE", "login")
34PAM_EXEC_COMMAND= os.environ.get( "XPRA_PAM_EXEC_COMMAND","/srv/bin/start_pod.sh")
35TIMEOUT = envint("XPRA_EXEC_AUTH_TIMEOUT", 600)
36
37PAM_CHECK_ACCOUNT = envbool("XPRA_PAM_CHECK_ACCOUNT", True)
38
39def parse_auth_line(out):
40    ldata = out.split(u"|")
41    assert len(ldata)>=2, "not enough fields: %i" % (len(ldata))
42    #parse fields:
43    username = ldata[0]
44    password = ldata[1]
45    if len(ldata)>=5:
46        uid = ldata[2]
47        gid = ldata[3]
48        displays = ldata[4].split(u",")
49    else:
50        #this will use the default value, usually "nobody":
51        uid = parse_uid(None)
52        gid = parse_gid(None)
53        displays = []
54    env_options = {}
55    session_options = {}
56    if len(ldata)>=6:
57        env_options = parse_simple_dict(ldata[5], u";")
58    if len(ldata)>=7:
59        session_options = parse_simple_dict(ldata[6], u";")
60    return username, password, uid, gid, displays, env_options, session_options
61
62
63def check(username, password, service=PAM_AUTH_SERVICE, check_account=PAM_CHECK_ACCOUNT):
64    log("pamexec check(%s, [..])", username)
65    from xpra.server.pam import pam_session #@UnresolvedImport
66    session = pam_session(username, password, service)
67    if not session.start(password):
68        return False
69    try:
70        success = session.authenticate()
71        if success and check_account:
72            success = session.check_account()
73    finally:
74        try:
75            session.close()
76        except:
77            log("error closing session %s", session, exc_info=True)
78    return success
79
80def perform_command(self):
81
82    try:
83       from xpra.server.server_core import capabilities
84       c = typedict(capabilities)
85    except ImportError as error:
86       return True
87
88    argv = c.strget("argv")
89
90    argv_iter = argv
91    start = argv_iter.find("('")
92
93    if  start >= 0:
94        start = start + 2
95        argv_iter = argv_iter[start:]
96
97    end = argv_iter.rfind("')")
98
99    if  end > 0:
100        argv_iter = argv_iter[0:end]
101
102    argv_iter = argv_iter.replace("', '",",")
103    argv_iter = argv_iter.split(',')
104       
105    for d in argv_iter:
106       my_argv = d
107       if "ssh:" in d:
108           conn_type = "ssh"
109           break
110       elif "https:" in d:
111           conn_type = "http"
112           break
113       elif "http:" in d:
114           conn_type = "http"
115           break
116       elif "ssl:" in d:
117           conn_type = "ssl"
118           break
119       elif "wss:" in d:
120           conn_type = "wss"
121           break
122       elif "ws:" in d:
123           conn_type = "ws"
124           break
125
126    display = my_argv.split(u"/")[-1]
127    start = display.find('-index')
128    if start >= 0:
129        display = display[0:start]
130
131    log("CONN_TYPE = %s DISPLAY %s",conn_type,display)
132
133    cmd = [self.command, self.username, display, argv, str(self.timeout)]
134    proc = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, universal_newlines=True)
135
136    out = proc.communicate()[0]
137    out = out.strip()
138    proc.stdout.close()
139
140    username, password, uid, gid, displays, env_options, session_options = parse_auth_line(out)
141
142    self.username = username
143    self.sessions = int(uid), int(gid) , displays , env_options , session_options
144    self.conn_type = conn_type
145
146    capabilities.update({"display" : displays[0]})
147    self.proc = proc
148
149    log("PAMEXEC authenticate(..) Popen(%s)=%s", cmd, proc)
150    #if required, make sure we kill the command when it times out:
151    if self.timeout>0:
152        self.timer = glib.timeout_add(self.timeout*1000, self.command_timedout)
153        if not OSX:
154           #python on macos may set a 0 returncode when we use poll()
155           #so we cannot use the ChildReaper on macos,
156           #and we can't cancel the timer
157           getChildReaper().add_process(proc, "exec auth", cmd, True, True, self.command_ended)
158    v = proc.wait()
159    log("authenticate(..) returncode(%s)=%s", cmd, v)
160    if self.timeout_event:
161        return False
162    return v==0
163       
164
165class Authenticator(SysAuthenticator):
166
167    def __init__(self, username, **kwargs):
168
169        service = kwargs.pop("service", PAM_AUTH_SERVICE)
170
171        self.check_account = parse_bool("check-account", kwargs.pop("check-account", PAM_CHECK_ACCOUNT), False)
172
173        self.timeout = TIMEOUT
174        self.command = PAM_EXEC_COMMAND
175        self.service = PAM_AUTH_SERVICE
176
177        end = service.find(":",1) 
178
179        if end > 0 :
180           self.service = service[0:end]
181           svc = service[end+1:].split(":")
182
183           for it in svc:
184               s_it = it.split("=")
185
186               if "command" == s_it[0]:
187                   self.command = s_it[1]
188               elif "service" == s_it[0]:
189                   self.service = s_it[1]
190               elif "timeout" == s_it[0]:
191                   self.timeout = int(s_it[1])
192       
193        self.timer = None
194        self.proc = None
195        self.timeout_event = False
196
197        if not self.command:
198            #try to find the default auth_dialog executable:
199            from xpra.platform.paths import get_libexec_dir
200            libexec = get_libexec_dir()
201            xpralibexec = os.path.join(libexec, "xpra")
202            log("libexec=%s, xpralibexec=%s", libexec, xpralibexec)
203            if os.path.exists(xpralibexec) and os.path.isdir(xpralibexec):
204                libexec = xpralibexec
205            auth_dialog = os.path.join(libexec, "auth_dialog")
206            if EXECUTABLE_EXTENSION:
207                #ie: add ".exe" on MS Windows
208                auth_dialog += ".%s" % EXECUTABLE_EXTENSION
209            log("auth_dialog=%s", auth_dialog)
210            if os.path.exists(auth_dialog):
211                self.command = auth_dialog
212        assert self.command, "exec authentication module is not configured correctly: no command specified"
213        connection = kwargs.get("connection")
214        log("exec connection info: %s", connection)
215        assert connection, "connection object is missing"
216        self.connection_str = str(connection)
217        SysAuthenticator.__init__(self, username, **kwargs)
218
219    def check(self, password):
220        log("pam.check(..) pw=%s", self.pw)
221        if self.pw is None:
222            return False
223        if check(self.username, password, self.service, self.check_account):
224            return perform_command(self)
225        return False
226
227    def get_challenge(self, digests):
228        if "xor" not in digests:
229           log.error("Error: pamexec authentication requires the 'xor' digest")
230           return None
231        return SysAuthenticator.get_challenge(self, ["xor"])
232
233    def command_ended(self, *args):
234        t = self.timer
235        log("exec auth.command_ended%s timer=%s", args, t)
236        if t:
237            self.timer = None
238            glib.source_remove(t)
239
240    def command_timedout(self):
241        proc = self.proc
242        log("exec auth.command_timedout() proc=%s", proc)
243        self.timeout_event = True
244        self.timer = None
245        if proc:
246            try:
247                proc.terminate()
248            except:
249                log("error trying to terminate exec auth process %s", proc, exc_info=True)
250
251    def get_sessions(self):
252        return self.sessions
253
254    def __repr__(self):
255        return "PAMEXEC"
256
257
258def main(args):
259    if len(args)!=3:
260        print("invalid number of arguments")
261        print("usage:")
262        print("%s username password" % (args[0],))
263        return 1
264    a = Authenticator(args[1])
265    if a.check(args[2]):
266        print("success")
267        return 0
268    else:
269        print("failed")
270        return -1
271
272if __name__ == "__main__":
273    sys.exit(main(sys.argv))