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