first commit

This commit is contained in:
root
2026-03-14 09:42:12 +00:00
commit 0adbd20c2c
10991 changed files with 1646955 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr ""

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr "No podeu canviar la contrasenya del usuari de LDAP \"%(user)s\"."

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr ""

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr "Das Passwort des LDAP Benutzers \"%(user)s\" kann nicht geändert werden."

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr "No se puede cambiar la contraseña del usuario de LDAP \"%(user)s\"."

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr ""

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr "LDAP kasutaja \"%(user)s\" parooli ei saa muuta."

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr "شما نمی توانید رمز عبور LDAP کاربر:\"%(user)s\" را تغییر دهید."

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr ""

View File

@@ -0,0 +1,10 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr ""
"Vous ne pouvez pas changer le mot de passe de l'utilisateur LDAP "
"« %(user)s »."

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr ""

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr "Anda tidak dapat mengubah kata sandi pengguna LDAP \"%(user)s\"."

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr "Non puoi cambiare la password dell'utente LDAP \"%(user)s\"."

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr ""

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr ""

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr "U kunt het wachtwoord van LDAP-gebruiker \"%(user)s\" niet wijzigen."

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr "Nie możesz zmienić hasła użytkownika LDAP \"%(user)s\"."

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr "Não é possível alterar a senha do usuário LDAP \"%(user)s\"."

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr "Nu puteți modifica parola utilizatorului LDAP \"%(user)s\"."

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr ""

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr "Gesla LDAP uporabnika \"%(user)s\" ni mogoče spremeniti."

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr ""

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr "Ви не можете змінити пароль LDAP-користувача \"%(user)s\"."

View File

@@ -0,0 +1,8 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
#, python-format
msgctxt "model:ir.message,text:msg_ldap_user_change_password"
msgid "You cannot change the password of LDAP user \"%(user)s\"."
msgstr "你不能更改LDAP用户 \"%(user)s\"的密码."

View File

@@ -0,0 +1,10 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data grouped="1">
<record model="ir.message" id="msg_ldap_user_change_password">
<field name="text">You cannot change the password of LDAP user "%(user)s".</field>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,164 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import logging
import ssl
import urllib.parse
import ldap3
from ldap3.core.exceptions import LDAPException
import trytond.config as config
from trytond.exceptions import LoginException
from trytond.i18n import gettext
from trytond.model.exceptions import AccessError
from trytond.pool import PoolMeta
logger = logging.getLogger(__name__)
section = 'ldap_authentication'
# Old version of urlparse doesn't parse query for ldap
# see http://bugs.python.org/issue9374
if 'ldap' not in urllib.parse.uses_query:
urllib.parse.uses_query.append('ldap')
def parse_ldap_url(uri):
unquote = urllib.parse.unquote
uri = config.parse_uri(uri)
dn = unquote(uri.path)[1:]
attributes, scope, filter_, extensions = (
uri.query.split('?') + [''] * 4)[:4]
if not scope:
scope = 'base'
extensions = urllib.parse.parse_qs(extensions)
return (uri, dn, unquote(attributes), unquote(scope), unquote(filter_),
extensions)
def ldap_server():
uri = config.get(section, 'uri')
if not uri:
return
uri, _, _, _, _, extensions = parse_ldap_url(uri)
if uri.scheme.startswith('ldaps'):
scheme, port = 'ldaps', 636
tls = ldap3.Tls(validate=ssl.CERT_REQUIRED)
else:
scheme, port = 'ldap', 389
tls = None
if 'tls' in uri.scheme:
tls = ldap3.Tls(validate=ssl.CERT_REQUIRED)
return ldap3.Server('%s://%s:%s' % (
scheme, uri.hostname, uri.port or port), tls=tls)
class User(metaclass=PoolMeta):
__name__ = 'res.user'
@staticmethod
def ldap_search_user(login, server, attrs=None):
'''
Return the result of a ldap search for the login using the ldap
server.
The attributes values defined in attrs will be return.
'''
_, dn, _, scope, filter_, extensions = parse_ldap_url(
config.get(section, 'uri'))
scope = {
'base': ldap3.BASE,
'onelevel': ldap3.LEVEL,
'one': ldap3.LEVEL,
'subtree': ldap3.SUBTREE,
'sub': ldap3.SUBTREE,
}[scope]
uid = config.get(section, 'uid', default='uid')
if filter_:
filter_ = '(&(%s=%s)%s)' % (uid, login, filter_)
else:
filter_ = '(%s=%s)' % (uid, login)
bindpass = None
bindname, = extensions.get('bindname', [None])
if not bindname:
bindname, = extensions.get('!bindname', [None])
if bindname:
# XXX find better way to get the password
bindpass = config.get(section, 'bind_pass')
bind_method = ldap3.AUTO_BIND_DEFAULT
if server.ssl is False and server.tls is not None:
bind_method = ldap3.AUTO_BIND_TLS_BEFORE_BIND
with ldap3.Connection(
server, bindname, bindpass, auto_bind=bind_method) as con:
con.search(dn, filter_, search_scope=scope, attributes=attrs)
result = con.entries
if result and len(result) > 1:
logger.info('ldap_search_user found more than 1 user')
return [(e.entry_dn, e.entry_attributes_as_dict)
for e in result]
@classmethod
def _check_passwd_ldap_user(cls, logins):
find = False
try:
server = ldap_server()
if not server:
return
for login in logins:
if cls.ldap_search_user(login, server, attrs=[]):
find = True
break
except LDAPException:
logger.error('LDAPError when checking password', exc_info=True)
if find:
raise AccessError(
gettext('ldap_authentication.msg_ldap_user_change_password',
user=login))
@classmethod
def preprocess_values(cls, mode, values):
values = super().preprocess_values(mode, values)
if mode == 'create' and values.get('password') and 'login' in values:
cls._check_passwd_ldap_user([values['login']])
return values
@classmethod
def check_modification(cls, mode, users, values=None, external=False):
super().check_modification(
mode, users, values=values, external=external)
if mode == 'write' and values.get('password'):
cls._check_passwd_ldap_user([u.login for u in users])
@classmethod
def _login_ldap(cls, login, parameters):
if 'password' not in parameters:
msg = gettext('res.msg_user_password', login=login)
raise LoginException('password', msg, type='password')
password = parameters['password']
try:
server = ldap_server()
if server:
uid = config.get(section, 'uid', default='uid')
users = cls.ldap_search_user(login, server, attrs=[uid])
if users and len(users) == 1:
[(dn, attrs)] = users
with ldap3.Connection(
server, dn, password,
auto_bind=ldap3.AUTO_BIND_NONE) as con:
if server.ssl is False and server.tls is not None:
con.start_tls()
if (password and con.bind()):
# Use ldap uid so we always get the right case
login = attrs.get(uid, [login])[0]
user_id = cls._get_login(login)[0]
if user_id:
return user_id
elif config.getboolean(section, 'create_user'):
user, = cls.create([{
'name': login,
'login': login,
}])
return user.id
except LDAPException:
logger.error('LDAPError when login', exc_info=True)

View File

@@ -0,0 +1,2 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

View File

@@ -0,0 +1,227 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import sys
import unittest
from unittest.mock import ANY, MagicMock, Mock, patch
import ldap3
import trytond.config as config
from trytond.modules.ldap_authentication.res import ldap_server, parse_ldap_url
from trytond.pool import Pool
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
section = 'ldap_authentication'
class LDAPAuthenticationTestCase(ModuleTestCase):
'Test LDAPAuthentication module'
module = 'ldap_authentication'
def setUp(self):
super().setUp()
methods = config.get('session', 'authentications', default='')
config.set('session', 'authentications', 'ldap')
self.addCleanup(config.set, 'session', 'authentications', methods)
config.add_section(section)
self.addCleanup(config.remove_section, section)
def _get_login(
self, uri='ldap://localhost/dc=tryton,dc=org', start_tls=False):
pool = Pool()
User = pool.get('res.user')
config.set(section, 'uri', uri)
@patch.object(ldap3, 'Connection')
@patch.object(User, 'ldap_search_user')
def get_login(login, password, find, ldap_search_user, Connection):
con = Connection.return_value = MagicMock()
con.__enter__.return_value = con
con.bind.return_value = bool(find)
if find:
ldap_search_user.return_value = [('dn', {'uid': [find]})]
else:
ldap_search_user.return_value = None
user_id = User.get_login(login, {
'password': password,
})
if find:
Connection.assert_called_with(
ANY, ANY, password, auto_bind=ldap3.AUTO_BIND_NONE)
if start_tls:
con.start_tls.assert_called()
else:
con.start_tls.assert_not_called()
con.bind.assert_called()
return user_id
return get_login
@with_transaction()
def test_user_get_login_existing_user(self):
"Test User.get_login with existing user"
pool = Pool()
User = pool.get('res.user')
user, = User.search([('login', '=', 'admin')])
get_login = self._get_login()
self.assertEqual(get_login('admin', 'admin', 'admin'), user.id)
self.assertEqual(get_login('AdMiN', 'admin', 'admin'), user.id)
@with_transaction()
def test_user_get_login_unknown_user(self):
"test User.get_login with unknown user"
get_login = self._get_login()
self.assertFalse(get_login('foo', 'bar', None))
self.assertFalse(get_login('foo', 'bar', 'foo'))
@with_transaction()
def test_user_get_login_create_user(self):
"Test User.get_login with user to create"
pool = Pool()
User = pool.get('res.user')
config.set(section, 'create_user', 'True')
get_login = self._get_login()
user_id = get_login('foo', 'bar', 'foo')
foo, = User.search([('login', '=', 'foo')])
self.assertEqual(user_id, foo.id)
self.assertEqual(foo.name, 'foo')
@with_transaction()
def test_user_get_login_create_user_case(self):
"Test User.get_login with user to create with different case"
pool = Pool()
User = pool.get('res.user')
config.set(section, 'create_user', 'True')
get_login = self._get_login()
user_id = get_login('BaR', 'foo', 'bar')
bar, = User.search([('login', '=', 'bar')])
self.assertEqual(user_id, bar.id)
self.assertEqual(bar.name, 'bar')
@with_transaction()
def test_user_get_login_with_tls(self):
"Test User.get_login with TLS"
pool = Pool()
User = pool.get('res.user')
user, = User.search([('login', '=', 'admin')])
get_login = self._get_login(
'ldap+tls://localhost/dc=tryton,dc=org', start_tls=True)
self.assertEqual(get_login('admin', 'admin', 'admin'), user.id)
@with_transaction()
def test_user_get_login_with_ssl(self):
"Test User.get_login with SSL"
pool = Pool()
User = pool.get('res.user')
user, = User.search([('login', '=', 'admin')])
get_login = self._get_login('ldaps://localhost/dc=tryton,dc=org')
self.assertEqual(get_login('admin', 'admin', 'admin'), user.id)
def test_parse_ldap_url(self):
'Test parse_ldap_url'
self.assertEqual(
parse_ldap_url('ldap:///o=University%20of%20Michigan,c=US')[1],
'o=University of Michigan,c=US')
self.assertEqual(
parse_ldap_url(
'ldap://ldap.itd.umich.edu/o=University%20of%20Michigan,c=US'
)[1],
'o=University of Michigan,c=US')
self.assertEqual(
parse_ldap_url(
'ldap://ldap.itd.umich.edu/o=University%20of%20Michigan,'
'c=US?postalAddress')[2],
'postalAddress')
self.assertEqual(
parse_ldap_url(
'ldap://host.com:6666/o=University%20of%20Michigan,'
'c=US??sub?(cn=Babs%20Jensen)')[3:5],
('sub', '(cn=Babs Jensen)'))
self.assertEqual(
parse_ldap_url(
'ldap:///??sub??bindname=cn=Manager%2co=Foo')[5],
{'bindname': ['cn=Manager,o=Foo']})
self.assertEqual(
parse_ldap_url(
'ldap:///??sub??!bindname=cn=Manager%2co=Foo')[5],
{'!bindname': ['cn=Manager,o=Foo']})
@unittest.skipIf(
sys.version_info < (3, 8), "call_args does not have args nor kwargs")
def test_ldap_server(self):
"Test ldap_server"
for uri, (host, tls) in [
('ldap://localhost/dc=tryton,dc=org',
('ldap://localhost:389', None)),
('ldaps://localhost/dc=tryton,dc=org',
('ldaps://localhost:636', True)),
('ldap+tls://localhost/dc=tryton,dc=org',
('ldap://localhost:389', True)),
]:
config.set(section, 'uri', uri)
with patch('ldap3.Server') as Server:
ldap_server()
self.assertEqual(Server.call_args.args, (host,))
if tls:
self.assertTrue(Server.call_args.kwargs.get('tls'))
else:
self.assertFalse(Server.call_args.kwargs.get('tls'))
def _ldap_search_user(
self, uri='ldap://localhost/dc=tryton,dc=org',
auto_bind=ldap3.AUTO_BIND_DEFAULT):
pool = Pool()
User = pool.get('res.user')
config.set(section, 'uri', uri)
@patch.object(ldap3, 'Connection')
def ldap_search_user(login, attrs, Connection):
con = Connection.return_value = MagicMock()
con.__enter__.return_value = con
con.entries = [Mock()]
server = ldap_server()
User.ldap_search_user(login, server, attrs=attrs)
Connection.assert_called_with(
ANY, ANY, ANY, auto_bind=auto_bind)
con.search.assert_called()
return ldap_search_user
@with_transaction()
def test_ldap_search_user(self):
"Test User.ldap_search_user"
ldap_search_user = self._ldap_search_user()
ldap_search_user('admin', None)
@with_transaction()
def test_ldap_search_user_with_tls(self):
"Test User.ldap_search_user with TSL"
ldap_search_user = self._ldap_search_user(
'ldap+tls://localhost/dc=tryton,dc=org',
ldap3.AUTO_BIND_TLS_BEFORE_BIND)
ldap_search_user('admin', None)
@with_transaction()
def test_ldap_search_user_with_ssl(self):
"Test User.ldap_search_user with SSL"
ldap_search_user = self._ldap_search_user(
'ldaps://localhost/dc=tryton,dc=org')
ldap_search_user('admin', None)
del ModuleTestCase

View File

@@ -0,0 +1,11 @@
[tryton]
version=7.8.0
depends:
ir
res
xml:
message.xml
[register]
model:
res.User