changeset 12adfca888d8 in tryton:default
details: https://hg.tryton.org/tryton?cmd=changeset&node=12adfca888d8
description:
        Add authentication services

        issue11582
        review417471003
diffstat:

 CHANGELOG                    |   2 +
 tryton/common/__init__.py    |   6 +-
 tryton/common/common.py      |  68 ++++++++++++++++++++++++++++++++++++++++---
 tryton/config.py             |   2 +
 tryton/gui/main.py           |   9 +++-
 tryton/gui/window/dblogin.py |  53 ++++++++++++++++++++++++++++++---
 tryton/jsonrpc.py            |  13 ++++---
 tryton/rpc.py                |  39 +++++++++++++++++++++++++
 8 files changed, 169 insertions(+), 23 deletions(-)

diffs (364 lines):

diff -r 8fdd045ddd51 -r 12adfca888d8 CHANGELOG
--- a/CHANGELOG Mon Oct 03 00:22:51 2022 +0200
+++ b/CHANGELOG Sun Oct 09 18:33:16 2022 +0200
@@ -1,3 +1,5 @@
+* Support authentication services
+
 Version 6.4.0 - 2022-05-02
 * Bug fixes (see mercurial logs for details)
 * Use unittest discover
diff -r 8fdd045ddd51 -r 12adfca888d8 tryton/common/__init__.py
--- a/tryton/common/__init__.py Mon Oct 03 00:22:51 2022 +0200
+++ b/tryton/common/__init__.py Sun Oct 09 18:33:16 2022 +0200
@@ -3,11 +3,11 @@
 from . import timedelta
 from .common import (
     COLOR_SCHEMES, MODELACCESS, MODELHISTORY, MODELNAME, MODELNOTIFICATION,
-    TRYTON_ICON, VIEW_SEARCH, IconFactory, Login, Logout, RPCContextReload,
+    TRYTON_ICON, VIEW_SEARCH, IconFactory, Logout, RPCContextReload,
     RPCException, RPCExecute, RPCProgress, Tooltips, apply_label_attributes,
     ask, check_version, concurrency, data2pixbuf, date_format, ellipsize,
     error, file_open, file_selection, file_write, filter_domain,
-    generateColorscheme, get_align, get_hostname, get_port,
+    generateColorscheme, get_align, get_credentials, get_hostname, get_port,
     get_sensible_widget, get_toplevel_window, hex2rgb, highlight_rgb, humanize,
     idle_add, mailto, message, node_attributes, open_documentation,
     process_exception, resize_pixbuf, selection, setup_window, slugify, sur,
@@ -23,7 +23,6 @@
     COLOR_SCHEMES,
     EvalEnvironment,
     IconFactory,
-    Login,
     Logout,
     MODELACCESS,
     MODELHISTORY,
@@ -55,6 +54,7 @@
     filter_leaf,
     generateColorscheme,
     get_align,
+    get_credentials,
     get_hostname,
     get_port,
     get_sensible_widget,
diff -r 8fdd045ddd51 -r 12adfca888d8 tryton/common/common.py
--- a/tryton/common/common.py   Mon Oct 03 00:22:51 2022 +0200
+++ b/tryton/common/common.py   Sun Oct 09 18:33:16 2022 +0200
@@ -14,7 +14,9 @@
 import xml.etree.ElementTree as ET
 from collections import defaultdict
 from decimal import Decimal
+from http.server import BaseHTTPRequestHandler, HTTPServer
 from pathlib import PurePath
+from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
 
 try:
     from http import HTTPStatus
@@ -1003,12 +1005,14 @@
             from tryton.gui.main import Main
             if PLOCK.acquire(False):
                 try:
-                    Login()
+                    get_credentials(rpc._USER)
                 except TrytonError as exception:
-                    if exception.faultCode == 'QueryCanceled':
-                        Main().on_quit()
-                        sys.exit()
-                    raise
+                    if exception.faultCode != 'QueryCanceled':
+                        message(
+                            _("Could not get a session."),
+                            msg_type=Gtk.MessageType.ERROR)
+                    Main().on_quit()
+                    sys.exit()
                 finally:
                     PLOCK.release()
                 if args:
@@ -1024,6 +1028,60 @@
     raise RPCException(exception)
 
 
+def get_credentials(user_id=None):
+    if not CONFIG['login.service']:
+        Login()
+    else:
+        url = CONFIG['login.service']
+        port = CONFIG['login.service.port']
+        next_ = 'http://localhost:%s/' % port
+        url_parts = list(urlparse(url))
+        query = dict(parse_qs(url_parts[4]))
+        query['next'] = next_
+        if user_id is not None:
+            query['renew'] = user_id
+        url_parts[4] = urlencode(query)
+        url = urlunparse(url_parts)
+        webbrowser.open(url)
+
+        class RequestHandler(BaseHTTPRequestHandler):
+            def do_GET(self):
+                self.send_response(200)
+                self.send_header('Content-Type', 'text/html')
+                self.end_headers()
+                try:
+                    rpc.set_service_session(
+                        parse_qs(urlparse(self.path).query))
+                except ValueError:
+                    pass
+                else:
+                    self.server.session_set = True
+                self.wfile.write(b"""\
+<html>
+    <head>
+        <title>Authentication Status</title>
+        <script>
+        window.onload = function() {
+            window.close();
+        }
+        </script>
+    </head>
+    <body>
+        <p>The authentication flow has completed.</p>
+    </body>
+</html>""")
+
+            def log_message(self, *args):
+                pass
+
+        server = HTTPServer(('localhost', port), RequestHandler)
+        server.timeout = 5 * 60
+        server.session_set = False
+        server.handle_request()
+        if not server.session_set:
+            raise TrytonError('SessionFailed')
+
+
 class Login(object):
     def __init__(self, func=rpc.login):
         parameters = {}
diff -r 8fdd045ddd51 -r 12adfca888d8 tryton/config.py
--- a/tryton/config.py  Mon Oct 03 00:22:51 2022 +0200
+++ b/tryton/config.py  Sun Oct 09 18:33:16 2022 +0200
@@ -41,6 +41,8 @@
         self.defaults = {
             'login.profile': demo_server,
             'login.login': 'demo',
+            'login.service': '',
+            'login.service.port': 8001,
             'login.host': demo_server,
             'login.db': demo_database,
             'login.expanded': False,
diff -r 8fdd045ddd51 -r 12adfca888d8 tryton/gui/main.py
--- a/tryton/gui/main.py        Mon Oct 03 00:22:51 2022 +0200
+++ b/tryton/gui/main.py        Sun Oct 09 18:33:16 2022 +0200
@@ -250,11 +250,14 @@
 
         self.set_title()  # Adds username/profile while password is asked
         try:
-            common.Login()
+            common.get_credentials()
         except Exception as exception:
-            if (not isinstance(exception, TrytonError)
-                    or exception.faultCode != 'QueryCanceled'):
+            if not isinstance(exception, TrytonError):
                 common.error(exception, traceback.format_exc())
+            elif exception.faultCode == 'SessionFailed':
+                common.message(
+                    _("Could not get a session."),
+                    msg_type=Gtk.MessageType.ERROR)
             return self.quit()
         self.get_preferences()
 
diff -r 8fdd045ddd51 -r 12adfca888d8 tryton/gui/window/dblogin.py
--- a/tryton/gui/window/dblogin.py      Mon Oct 03 00:22:51 2022 +0200
+++ b/tryton/gui/window/dblogin.py      Sun Oct 09 18:33:16 2022 +0200
@@ -457,6 +457,7 @@
         self.entry_host = Gtk.Entry(hexpand=True)
         self.entry_host.connect_after('focus-out-event',
             self.clear_profile_combo)
+        self.entry_host.connect('focus-out-event', self.update_services)
         self.entry_host.set_activates_default(True)
         self.label_host.set_mnemonic_widget(self.entry_host)
         grid.attach(self.label_host, 0, 3, 1, 1)
@@ -500,6 +501,10 @@
                 for option in ('host', 'database'))
             self.profile_store.append([section, active])
 
+        self.services = []
+        self.service_base = ''
+        self._services = {}
+
     def profile_manage(self, widget):
         def callback(profile_name):
             with open(self.profile_cfg, 'w') as configfile:
@@ -535,6 +540,7 @@
             self.entry_login.set_text(username)
         else:
             self.entry_login.set_text('')
+        self.update_services()
 
     def clear_profile_combo(self, *args):
         netloc = self.entry_host.get_text()
@@ -569,6 +575,33 @@
         self.entry_database.props.visible = visibility
         self.label_database.props.visible = visibility
 
+    def update_services(self, *args):
+        host = self.entry_host.get_text()
+        hostname = common.get_hostname(host)
+        port = common.get_port(host)
+        key = (hostname, port)
+        for response_id, service in enumerate(self.services, 1):
+            button = self.dialog.get_widget_for_response(response_id)
+            button.get_parent().remove(button)
+        self.services = []
+        if key in self._services:
+            self.services = self._services[key]
+        elif hostname and port:
+            self.service_base, self.services = rpc.authentication_services(
+                host, port)
+            self._services[key] = self.services
+        for response_id, (name, url) in enumerate(self.services, 1):
+            button = Gtk.Button(label=name)
+            self.dialog.add_action_widget(button, response_id)
+            button.show()
+
+        # Re-add connect button to be last
+        button_connect = self.dialog.get_widget_for_response(
+            Gtk.ResponseType.OK)
+        button_connect.get_parent().remove(button_connect)
+        self.dialog.add_action_widget(button_connect, Gtk.ResponseType.OK)
+        self.dialog.set_default_response(Gtk.ResponseType.OK)
+
     def run(self):
         profile_name = CONFIG['login.profile']
         can_use_profile = self.profiles.has_section(profile_name)
@@ -598,6 +631,7 @@
             self.entry_database.set_text(db)
             self.entry_login.set_text(CONFIG['login.login'])
             self.clear_profile_combo()
+        self.update_services()
         self.dialog.show_all()
 
         self.entry_login.grab_focus()
@@ -609,7 +643,7 @@
         response, result = None, ('', '', '', '')
         while not all(result):
             response = self.dialog.run()
-            if response != Gtk.ResponseType.OK:
+            if response != Gtk.ResponseType.OK and response <= 0:
                 break
             self.clear_profile_combo()
             active_profile = self.combo_profile.get_active()
@@ -632,15 +666,22 @@
                         parent=self.dialog)
                 continue
             database = self.entry_database.get_text()
-            login = self.entry_login.get_text()
             CONFIG['login.profile'] = profile
             CONFIG['login.host'] = host
             CONFIG['login.db'] = database
             CONFIG['login.expanded'] = self.expander.props.expanded
-            CONFIG['login.login'] = login
-            result = (
-                hostname, port, database, self.entry_login.get_text())
+            if response == Gtk.ResponseType.OK:
+                authentication = self.entry_login.get_text()
+                CONFIG['login.login'] = authentication
+                CONFIG['login.service'] = ''
+            else:
+                authentication = self.services[response - 1][1]
+                authentication = (
+                    self.service_base + database + authentication)
+                CONFIG['login.login'] = ''
+                CONFIG['login.service'] = authentication
+            result = (hostname, port, database, authentication)
 
         self.dialog.destroy()
         self._window.destroy()
-        return response == Gtk.ResponseType.OK
+        return response == Gtk.ResponseType.OK or response > 0
diff -r 8fdd045ddd51 -r 12adfca888d8 tryton/jsonrpc.py
--- a/tryton/jsonrpc.py Mon Oct 03 00:22:51 2022 +0200
+++ b/tryton/jsonrpc.py Sun Oct 09 18:33:16 2022 +0200
@@ -316,6 +316,11 @@
         return isinstance(self.__transport.make_connection(self.__host),
             http.client.HTTPSConnection)
 
+    @property
+    def url(self):
+        scheme = 'https' if self.ssl else 'http'
+        return urljoin(scheme + '://' + self.__host, self.__handler)
+
 
 class ServerPool(object):
     keep_max = 4
@@ -370,12 +375,8 @@
 
     @property
     def url(self):
-        if self.ssl is None:
-            return None
-        scheme = 'https' if self.ssl else 'http'
-        return urljoin(
-            scheme + '://' + self._host + ':' + str(self._port),
-            self._database)
+        for conn in self._pool + list(self._used.values()):
+            return conn.url
 
     @contextmanager
     def __call__(self):
diff -r 8fdd045ddd51 -r 12adfca888d8 tryton/rpc.py
--- a/tryton/rpc.py     Mon Oct 03 00:22:51 2022 +0200
+++ b/tryton/rpc.py     Sun Oct 09 18:33:16 2022 +0200
@@ -17,6 +17,7 @@
 from tryton.exceptions import TrytonServerError, TrytonServerUnavailable
 from tryton.jsonrpc import Fault, ServerPool, ServerProxy
 
+logger = logging.getLogger(__name__)
 CONNECTION = None
 _USER = None
 CONTEXT = {}
@@ -69,6 +70,44 @@
         return None
 
 
+def authentication_services(host, port):
+    try:
+        connection = ServerProxy(host, port)
+        logger.info('common.authentication.services()')
+        services = connection.common.authentication.services()
+        logger.debug(repr(services))
+        return connection.url, services
+    except Exception as e:
+        logger.error(e)
+        return '', []
+
+
+def set_service_session(parameters):
+    from tryton import common
+    global CONNECTION, _USER
+    host = CONFIG['login.host']
+    hostname = common.get_hostname(host)
+    port = common.get_port(host)
+    database = CONFIG['login.db']
+    CONFIG['login.login'] = username = parameters.get('login', [''])[0]
+    try:
+        user_id = int(parameters.get('user_id', [None])[0])
+    except TypeError:
+        pass
+    session = parameters.get('session', [''])[0]
+    if 'renew' in parameters:
+        renew_id = int(parameters.get('renew', [-1])[0])
+        if _USER != renew_id:
+            raise ValueError
+    _USER = user_id
+    session = ':'.join(map(str, [username, user_id, session]))
+    if CONNECTION is not None:
+        CONNECTION.close()
+    CONNECTION = ServerPool(
+        hostname, port, database, session=session, cache=not CONFIG['dev'])
+    bus.listen(CONNECTION)
+
+
 def login(parameters):
     from tryton import common
     global CONNECTION, _USER

Reply via email to