From b932781ac8bbcfb4abe3cfc6edd654330b96dba0 Mon Sep 17 00:00:00 2001 From: vincent Date: Thu, 6 Oct 2022 20:28:43 +0200 Subject: [PATCH 01/29] init ldap implementation --- setup.cfg | 1 + supysonic/config.py | 12 ++++++++++++ supysonic/managers/ldap.py | 12 ++++++++++++ supysonic/managers/user.py | 4 ++++ 4 files changed, 29 insertions(+) create mode 100644 supysonic/managers/ldap.py diff --git a/setup.cfg b/setup.cfg index 661c2d4..ca06214 100644 --- a/setup.cfg +++ b/setup.cfg @@ -60,6 +60,7 @@ install_requires = mediafile watchdog >=0.8.0 zipstream-ng >=1.1.0, <2.0.0 + ldap3 packages = find_namespace: include_package_data = true diff --git a/supysonic/config.py b/supysonic/config.py index 039ae4a..4b859f2 100644 --- a/supysonic/config.py +++ b/supysonic/config.py @@ -55,6 +55,18 @@ class DefaultConfig: } LASTFM = {"api_key": None, "secret": None} LISTENBRAINZ = {"api_url": "https://api.listenbrainz.org"} + LDAP = { + "ldap_server": None, + "base_dn": None, + "user_filter": None, + "admin_filter": None, + "bind_user": None, + "bind_password": None, + "username": "uid", + "email": "mail", + } + + TRANSCODING = {} MIMETYPES = {} diff --git a/supysonic/managers/ldap.py b/supysonic/managers/ldap.py new file mode 100644 index 0000000..83c6147 --- /dev/null +++ b/supysonic/managers/ldap.py @@ -0,0 +1,12 @@ +from ..db import User +import ldap3 +from ..config import get_current_config + +LDAPConfig = get_current_config().LDAP + + +class LdapManager: + + @staticmethod + def tryauth(user, password): + pass diff --git a/supysonic/managers/user.py b/supysonic/managers/user.py index 23215c0..3196284 100644 --- a/supysonic/managers/user.py +++ b/supysonic/managers/user.py @@ -11,6 +11,7 @@ import string import uuid +from ..config import get_current_config from ..db import User @@ -46,6 +47,9 @@ def delete_by_name(name): @staticmethod def try_auth(name, password): + if (get_current_config().LDAP["ldap_server"] is not None): + if(user := ldapManager.try_auth(name, password) is not None): + return user user = User.get_or_none(name=name) if user is None: return None From 916c5da67c6c5bdd321814b1270594ad919f9ace Mon Sep 17 00:00:00 2001 From: vincent Date: Fri, 7 Oct 2022 09:50:26 +0200 Subject: [PATCH 02/29] fix old python compatibility --- supysonic/managers/ldap.py | 2 +- supysonic/managers/user.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/supysonic/managers/ldap.py b/supysonic/managers/ldap.py index 83c6147..1272985 100644 --- a/supysonic/managers/ldap.py +++ b/supysonic/managers/ldap.py @@ -8,5 +8,5 @@ class LdapManager: @staticmethod - def tryauth(user, password): + def try_auth(user, password): pass diff --git a/supysonic/managers/user.py b/supysonic/managers/user.py index 3196284..713cddc 100644 --- a/supysonic/managers/user.py +++ b/supysonic/managers/user.py @@ -48,7 +48,8 @@ def delete_by_name(name): @staticmethod def try_auth(name, password): if (get_current_config().LDAP["ldap_server"] is not None): - if(user := ldapManager.try_auth(name, password) is not None): + user = LdapManager.try_auth(name, password) + if user is not None: return user user = User.get_or_none(name=name) if user is None: From ccc0d2d8f85a357a33e2f1532e30f19cb6bedf55 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 14 Mar 2023 21:43:17 +0100 Subject: [PATCH 03/29] implement LDAP object --- supysonic/config.py | 6 +-- supysonic/managers/ldap.py | 77 +++++++++++++++++++++++++++++++++++--- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/supysonic/config.py b/supysonic/config.py index 4b859f2..b638924 100644 --- a/supysonic/config.py +++ b/supysonic/config.py @@ -58,12 +58,12 @@ class DefaultConfig: LDAP = { "ldap_server": None, "base_dn": None, - "user_filter": None, + "user_filter": "(&(objectClass=inetOrgPerson))", "admin_filter": None, "bind_user": None, "bind_password": None, - "username": "uid", - "email": "mail", + "username_attr": "uid", + "email_attr": "mail", } diff --git a/supysonic/managers/ldap.py b/supysonic/managers/ldap.py index 1272985..8845a32 100644 --- a/supysonic/managers/ldap.py +++ b/supysonic/managers/ldap.py @@ -1,12 +1,77 @@ -from ..db import User -import ldap3 -from ..config import get_current_config +import logging -LDAPConfig = get_current_config().LDAP +try: + import ldap3 +except ModuleNotFoundError: + ldap3 = None +from flask import current_app + +logger = logging.getLogger(__name__) -class LdapManager: +class LdapManager: @staticmethod def try_auth(user, password): - pass + config = current_app.config["LDAP"] + entrie = LdapManager.is_admin(user) + if entrie: + logger.debug("{0} is admin".format(user)) + admin = True + else: + entrie = LdapManager.is_user(user) + if entrie: + admin = False + else: + return False + server = ldap3.Server(config["ldap_server"], get_info="ALL") + try: + with ldap3.Connection( + server, entrie.entry_dn, password, read_only=True + ) as conn: + return { + "uid": entrie[config["username_attr"]], + "mail": entrie[config["email_attr"]], + "admin": admin, + } + except ldap3.core.exceptions.LDAPBindError: + logger.warning("wrong password for user {0}".format(user)) + return False + + @staticmethod + def is_admin(user): + config = current_app.config["LDAP"] + return LdapManager.search_user(user, config["admin_filter"]) + + @staticmethod + def is_user(user): + config = current_app.config["LDAP"] + return LdapManager.search_user(user, config["user_filter"]) + + @staticmethod + def search_user(user, filter): + if not ldap3: + logger.warning("module 'ldap2' is not installed") + return False + config = current_app.config["LDAP"] + if not config["ldap_server"]: + logger.info("No LDAP configured") + return False + server = ldap3.Server(config["ldap_server"], get_info="ALL") + try: + with ldap3.Connection( + server, config["bind_dn"], config["bind_password"], read_only=True + ) as conn: + conn.search( + config["base_dn"], + filter, + attributes=[config["email_attr"], config["username_attr"]], + ) + entries = conn.entries + except ldap3.core.exceptions.LDAPBindError: + logger.warning("wrong can't bind LDAP with {-1}".format(config["bind_dn"])) + + for entrie in entries: + if entrie[config["username_attr"]] == user: + return entrie + return False From 224f937d41cc2875f1ec052b2d8a31c27f8fae87 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 14 Mar 2023 23:07:16 +0100 Subject: [PATCH 04/29] update user manager for ldap --- supysonic/managers/user.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/supysonic/managers/user.py b/supysonic/managers/user.py index 713cddc..ad4cd6e 100644 --- a/supysonic/managers/user.py +++ b/supysonic/managers/user.py @@ -11,8 +11,8 @@ import string import uuid -from ..config import get_current_config from ..db import User +from .ldap import LdapManager class UserManager: @@ -47,17 +47,24 @@ def delete_by_name(name): @staticmethod def try_auth(name, password): - if (get_current_config().LDAP["ldap_server"] is not None): - user = LdapManager.try_auth(name, password) - if user is not None: - return user + ldap_user = LdapManager.try_auth(name, password) user = User.get_or_none(name=name) - if user is None: - return None - elif UserManager.__encrypt_password(password, user.salt)[0] != user.password: - return None - else: + if ldap_user: + if user is None: + user = User.create(name=name,mail=ldap_user["mail"],admin=ldap_user["admin"]) + else: + if user.admin != ldap_user['admin']: + user.admin=ldap_user['admin'] + if user.mail != ldap_user['mail']: + user.mail=ldap_user['mail'] return user + else: + if user is None: + return None + elif UserManager.__encrypt_password(password, user.salt)[0] != user.password: + return None + else: + return user @staticmethod def change_password(uid, old_pass, new_pass): From 9ed0a0108d98c4426555462a10d651f6ca9ca619 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 14 Mar 2023 23:07:40 +0100 Subject: [PATCH 05/29] DB modification for LDAP --- supysonic/db.py | 4 +-- supysonic/schema/migration/mysql/20230314.sql | 2 ++ .../schema/migration/postgres/20230314.sql | 3 ++ .../schema/migration/sqlite/20230314.sql | 28 +++++++++++++++++++ supysonic/schema/mysql.sql | 4 +-- supysonic/schema/postgres.sql | 4 +-- supysonic/schema/sqlite.sql | 4 +-- 7 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 supysonic/schema/migration/mysql/20230314.sql create mode 100644 supysonic/schema/migration/postgres/20230314.sql create mode 100644 supysonic/schema/migration/sqlite/20230314.sql diff --git a/supysonic/db.py b/supysonic/db.py index d273b95..f9ac8e5 100644 --- a/supysonic/db.py +++ b/supysonic/db.py @@ -430,8 +430,8 @@ class User(_Model): id = PrimaryKeyField() name = CharField(64, unique=True) mail = CharField(null=True) - password = FixedCharField(40) - salt = FixedCharField(6) + password = FixedCharField(40,null=True) + salt = FixedCharField(6,null=True) admin = BooleanField(default=False) jukebox = BooleanField(default=False) diff --git a/supysonic/schema/migration/mysql/20230314.sql b/supysonic/schema/migration/mysql/20230314.sql new file mode 100644 index 0000000..1eabf86 --- /dev/null +++ b/supysonic/schema/migration/mysql/20230314.sql @@ -0,0 +1,2 @@ +ALTER TABLE user MODIFY password CHAR(40); +ALTER TABLE user MODIFY salt CHAR(6); diff --git a/supysonic/schema/migration/postgres/20230314.sql b/supysonic/schema/migration/postgres/20230314.sql new file mode 100644 index 0000000..eecbebf --- /dev/null +++ b/supysonic/schema/migration/postgres/20230314.sql @@ -0,0 +1,3 @@ +ALTER TABLE user + ALTER COLUMN password CHAR(40), + ALTER COLUMN salt CHAR(6); diff --git a/supysonic/schema/migration/sqlite/20230314.sql b/supysonic/schema/migration/sqlite/20230314.sql new file mode 100644 index 0000000..24172fc --- /dev/null +++ b/supysonic/schema/migration/sqlite/20230314.sql @@ -0,0 +1,28 @@ +COMMIT; +PRAGMA foreign_keys = OFF; +BEGIN TRANSACTION; +DROP INDEX index_user_last_play_id_fk; +create TABLE user_new ( + id CHAR(36) PRIMARY KEY, + name VARCHAR(64) NOT NULL, + mail VARCHAR(256), + password CHAR(40), + salt CHAR(6), + admin BOOLEAN NOT NULL, + jukebox BOOLEAN NOT NULL, + lastfm_session CHAR(32), + lastfm_status BOOLEAN NOT NULL, + last_play_id CHAR(36) REFERENCES track, + last_play_date DATETIME +); +CREATE INDEX IF NOT EXISTS index_user_last_play_id_fk ON user_new(last_play_id); +INSERT INTO user_new(id, name, mail, password, salt, admin, jukebox, lastfm_session, lastfm_status, last_play_id, last_play_date) +SELECT id, name, mail, password, salt, admin, jukebox, lastfm_session, lastfm_status, last_play_id, last_play_date +FROM user; + +DROP TABLE user; +ALTER TABLE user_new RENAME TO user; +COMMIT; +VACUUM; +PRAGMA foreign_keys = ON; +BEGIN TRANSACTION; diff --git a/supysonic/schema/mysql.sql b/supysonic/schema/mysql.sql index ae7bb69..f6d1017 100644 --- a/supysonic/schema/mysql.sql +++ b/supysonic/schema/mysql.sql @@ -53,8 +53,8 @@ CREATE TABLE IF NOT EXISTS user ( id CHAR(32) PRIMARY KEY, name VARCHAR(64) NOT NULL, mail VARCHAR(256), - password CHAR(40) NOT NULL, - salt CHAR(6) NOT NULL, + password CHAR(40), + salt CHAR(6), admin BOOLEAN NOT NULL, jukebox BOOLEAN NOT NULL, listenbrainz_session CHAR(36), diff --git a/supysonic/schema/postgres.sql b/supysonic/schema/postgres.sql index b870492..c390172 100644 --- a/supysonic/schema/postgres.sql +++ b/supysonic/schema/postgres.sql @@ -53,8 +53,8 @@ CREATE TABLE IF NOT EXISTS "user" ( id UUID PRIMARY KEY, name VARCHAR(64) NOT NULL, mail VARCHAR(256), - password CHAR(40) NOT NULL, - salt CHAR(6) NOT NULL, + password CHAR(40), + salt CHAR(6), admin BOOLEAN NOT NULL, jukebox BOOLEAN NOT NULL, listenbrainz_session CHAR(36), diff --git a/supysonic/schema/sqlite.sql b/supysonic/schema/sqlite.sql index 996d566..2fc3a36 100644 --- a/supysonic/schema/sqlite.sql +++ b/supysonic/schema/sqlite.sql @@ -55,8 +55,8 @@ CREATE TABLE IF NOT EXISTS user ( id CHAR(36) PRIMARY KEY, name VARCHAR(64) NOT NULL, mail VARCHAR(256), - password CHAR(40) NOT NULL, - salt CHAR(6) NOT NULL, + password CHAR(40), + salt CHAR(6), admin BOOLEAN NOT NULL, jukebox BOOLEAN NOT NULL, listenbrainz_session CHAR(36), From b7ceed7c5e31ce4d57fbc2861907e6cb45361c5e Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 14 Mar 2023 23:12:57 +0100 Subject: [PATCH 06/29] add ldap to sample config --- config.sample | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config.sample b/config.sample index e0c2b63..937516c 100644 --- a/config.sample +++ b/config.sample @@ -98,4 +98,11 @@ default_transcode_target = mp3 ; Default: none ;mp3 = audio/mpeg ;ogg = audio/vorbis +[LDAP] +;ldap_server= ldap://ldap.example.com +;base_dn= dc=example,dc=com +;bind_dn= uid=root,cn=users,dc=example,dc=com +;bind_password= +;user_filter= (&(memberOf=CN=SupysonicUsers,cn=groups,dc=example,dc=com)) +;admin_filter= (&(memberOf=CN=SupysonicAdmins,cn=groups,dc=example,dc=com)) From d07853f6585cc12b66bfe4f77c8bb1d2f5788731 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 14 Mar 2023 23:27:25 +0100 Subject: [PATCH 07/29] change config call to avoid flask dependancy --- supysonic/managers/ldap.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/supysonic/managers/ldap.py b/supysonic/managers/ldap.py index 8845a32..9137776 100644 --- a/supysonic/managers/ldap.py +++ b/supysonic/managers/ldap.py @@ -5,7 +5,7 @@ except ModuleNotFoundError: ldap3 = None -from flask import current_app +from ..config import get_current_config logger = logging.getLogger(__name__) @@ -13,13 +13,13 @@ class LdapManager: @staticmethod def try_auth(user, password): - config = current_app.config["LDAP"] - entrie = LdapManager.is_admin(user) + config = get_current_config().LDAP + entrie = LdapManager.search_user(user, config["admin_filter"]) if entrie: logger.debug("{0} is admin".format(user)) admin = True else: - entrie = LdapManager.is_user(user) + entrie = LdapManager.search_user(user, config["user_filter"]) if entrie: admin = False else: @@ -38,22 +38,12 @@ def try_auth(user, password): logger.warning("wrong password for user {0}".format(user)) return False - @staticmethod - def is_admin(user): - config = current_app.config["LDAP"] - return LdapManager.search_user(user, config["admin_filter"]) - - @staticmethod - def is_user(user): - config = current_app.config["LDAP"] - return LdapManager.search_user(user, config["user_filter"]) - @staticmethod def search_user(user, filter): if not ldap3: logger.warning("module 'ldap2' is not installed") return False - config = current_app.config["LDAP"] + config = get_current_config().LDAP if not config["ldap_server"]: logger.info("No LDAP configured") return False From 0f76a8e5b6d3ff26e1448989ada159e917aeb257 Mon Sep 17 00:00:00 2001 From: vincent Date: Thu, 16 Mar 2023 21:34:12 +0100 Subject: [PATCH 08/29] remove dependance between ldapmanager and config --- supysonic/config.py | 2 +- supysonic/managers/ldap.py | 71 +++++++++++++++++++------------------- supysonic/managers/user.py | 12 +++++-- 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/supysonic/config.py b/supysonic/config.py index b638924..e391a81 100644 --- a/supysonic/config.py +++ b/supysonic/config.py @@ -60,7 +60,7 @@ class DefaultConfig: "base_dn": None, "user_filter": "(&(objectClass=inetOrgPerson))", "admin_filter": None, - "bind_user": None, + "bind_dn": None, "bind_password": None, "username_attr": "uid", "email_attr": "mail", diff --git a/supysonic/managers/ldap.py b/supysonic/managers/ldap.py index 9137776..0f9e16a 100644 --- a/supysonic/managers/ldap.py +++ b/supysonic/managers/ldap.py @@ -1,67 +1,68 @@ import logging -try: - import ldap3 -except ModuleNotFoundError: - ldap3 = None -from ..config import get_current_config +import ldap3 + logger = logging.getLogger(__name__) class LdapManager: - @staticmethod - def try_auth(user, password): - config = get_current_config().LDAP - entrie = LdapManager.search_user(user, config["admin_filter"]) - if entrie: - logger.debug("{0} is admin".format(user)) - admin = True - else: - entrie = LdapManager.search_user(user, config["user_filter"]) + + def __init__(self, ldap_server, base_dn, user_filter, admin_filter, bind_dn, bind_password, username_attr, email_attr): + self.ldap_server=ldap_server + self.base_dn=base_dn + self.user_filter=user_filter + self.admin_filter=admin_filter + self.bind_dn=bind_dn + self.bind_password=bind_password + self.username_attr=username_attr + self.email_attr=email_attr + if not self.ldap_server: + raise ValueError("No LDAP configured") + self.server = ldap3.Server(self.ldap_server, get_info="ALL") + + def try_auth(self,user, password): + admin= False + if self.admin_filter: + entrie = self.search_user(user, self.admin_filter) if entrie: - admin = False - else: + logger.debug("{0} is admin".format(user)) + admin = True + if not admin: + entrie = self.search_user(user, self.user_filter) + if not entrie: return False - server = ldap3.Server(config["ldap_server"], get_info="ALL") try: with ldap3.Connection( - server, entrie.entry_dn, password, read_only=True + self.server, entrie.entry_dn, password, read_only=True ) as conn: return { - "uid": entrie[config["username_attr"]], - "mail": entrie[config["email_attr"]], + "uid": entrie[self.username_attr], + "mail": entrie[self.email_attr], "admin": admin, } except ldap3.core.exceptions.LDAPBindError: logger.warning("wrong password for user {0}".format(user)) return False - @staticmethod - def search_user(user, filter): - if not ldap3: - logger.warning("module 'ldap2' is not installed") - return False - config = get_current_config().LDAP - if not config["ldap_server"]: - logger.info("No LDAP configured") - return False - server = ldap3.Server(config["ldap_server"], get_info="ALL") + def search_user(self,user, filter): + try: with ldap3.Connection( - server, config["bind_dn"], config["bind_password"], read_only=True + self.server, self.bind_dn, self.bind_password, read_only=True ) as conn: conn.search( - config["base_dn"], + self.base_dn, filter, - attributes=[config["email_attr"], config["username_attr"]], + attributes=[self.email_attr, self.username_attr], ) entries = conn.entries except ldap3.core.exceptions.LDAPBindError: - logger.warning("wrong can't bind LDAP with {-1}".format(config["bind_dn"])) + logger.warning( + "wrong can't bind LDAP with {0}".format(self.bind_dn)) for entrie in entries: - if entrie[config["username_attr"]] == user: + if entrie[self.username_attr] == user: return entrie return False diff --git a/supysonic/managers/user.py b/supysonic/managers/user.py index ad4cd6e..b0b15b6 100644 --- a/supysonic/managers/user.py +++ b/supysonic/managers/user.py @@ -12,8 +12,13 @@ import uuid from ..db import User -from .ldap import LdapManager +from ..config import get_current_config +try: + from .ldap import LdapManager + ldap=LdapManager(**get_current_config().LDAP) +except: + ldap=None class UserManager: @staticmethod @@ -47,7 +52,10 @@ def delete_by_name(name): @staticmethod def try_auth(name, password): - ldap_user = LdapManager.try_auth(name, password) + if ldap: + ldap_user = ldap.try_auth(name, password) + else: + ldap_user= False user = User.get_or_none(name=name) if ldap_user: if user is None: From 8d2beb98faca343e7385c0571f0bf0f64a324ab6 Mon Sep 17 00:00:00 2001 From: vincent Date: Thu, 16 Mar 2023 22:23:02 +0100 Subject: [PATCH 09/29] update docs --- README.md | 1 + config.sample | 1 + docs/setup/configuration.rst | 25 +++++++++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/README.md b/README.md index 8ebf6fa..8f32e95 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Current supported features are: * [Last.fm][lastfm] scrobbling * [ListenBrainz][listenbrainz] scrobbling * Jukebox mode +* LDAP authentification Supysonic currently targets the version 1.12.0 of the Subsonic API. For more details, go check the [API implementation status][docs-api]. diff --git a/config.sample b/config.sample index 937516c..e5dcda0 100644 --- a/config.sample +++ b/config.sample @@ -98,6 +98,7 @@ default_transcode_target = mp3 ; Default: none ;mp3 = audio/mpeg ;ogg = audio/vorbis + [LDAP] ;ldap_server= ldap://ldap.example.com ;base_dn= dc=example,dc=com diff --git a/docs/setup/configuration.rst b/docs/setup/configuration.rst index 2dd7572..ddf4ef8 100644 --- a/docs/setup/configuration.rst +++ b/docs/setup/configuration.rst @@ -363,3 +363,28 @@ See the following links for a list of examples: ; Default: none ;mp3 = audio/mpeg ;ogg = audio/vorbis + +``[LDAP]`` section +----------------------- + +This section define LDAP connection parameters +when user is find on LDAP server and not exist in supysonic database, +user is add to database + +``ldap_server`` + ldap URL + +``base_dn`` + ldap dn where search is perform + +``bind_dn`` + user dn use to perform query + +`bind_password`` + password for bind_dn + +``user_filter`` + ldap filter use for lookup standart user + +``admin_filter`` + ldap filter use for lookup admin user From 41a336b14337c33945667b0a29c123347f919a48 Mon Sep 17 00:00:00 2001 From: vincent Date: Sun, 19 Mar 2023 22:03:58 +0100 Subject: [PATCH 10/29] add test --- tests/managers/test_manager_ldap.py | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/managers/test_manager_ldap.py diff --git a/tests/managers/test_manager_ldap.py b/tests/managers/test_manager_ldap.py new file mode 100644 index 0000000..8f44e4e --- /dev/null +++ b/tests/managers/test_manager_ldap.py @@ -0,0 +1,48 @@ +import ldap3 +from supysonic import db +from supysonic.managers.ldap import LdapManager +import unittest +from unittest.mock import patch + +LDAP = { + "ldap_server": "fakeServer", + "base_dn": "ou=test,o=lab", + "user_filter": "(&(objectClass=inetOrgPerson))", + "admin_filter": None, + "bind_dn": "cn=my_user,ou=test,o=lab", + "bind_password": 'my_password', + "username_attr": "uid", + "email_attr": "mail", +} + + +class LdapManagerTestCase(unittest.TestCase): + + def setUp(self): + # Create an empty sqlite database in memory + pass + + def tearDown(self): + pass + + @patch('supysonic.managers.ldap.ldap3.Connection') + def test_ldapManager_searchUser(self, mock_object): + mock_object.return_value.__enter__.return_value.entries = [ + {LDAP["username_attr"]:"toto"}] + ldap = LdapManager(**LDAP) + ldap_user = ldap.search_user("toto", LDAP["user_filter"]) + self.assertEqual(ldap_user[LDAP["username_attr"]], "toto") + ldap_user = ldap.search_user("tata", LDAP["user_filter"]) + self.assertFalse(ldap_user) + + @patch('supysonic.managers.ldap.ldap3.Connection') + def test_ldapManager_try_auth(self, mock_object): + mock_object.return_value.__enter__.return_value.entries = [ + {LDAP["username_attr"]:"toto", "entry_dn":"cn=toto", "mail":"toto@example.com"}] + ldap = LdapManager(**LDAP) + ldap_user = ldap.try_auth("toto", "toto") + self.assertFalse(ldap_user["admin"]) + self.assertEqual(ldap_user[LDAP["username_attr"]], "toto") + self.assertEqual(ldap_user[LDAP["email_attr"]], "toto@example.com") + ldap_user = ldap.try_auth("tata", "tata") + self.assertFalse(ldap_user) From b6a8e30e7e7610af145c968c428d25790c052d97 Mon Sep 17 00:00:00 2001 From: vincent Date: Sun, 19 Mar 2023 22:05:13 +0100 Subject: [PATCH 11/29] format --- supysonic/managers/ldap.py | 42 ++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/supysonic/managers/ldap.py b/supysonic/managers/ldap.py index 0f9e16a..59db5c9 100644 --- a/supysonic/managers/ldap.py +++ b/supysonic/managers/ldap.py @@ -8,22 +8,31 @@ class LdapManager: - - def __init__(self, ldap_server, base_dn, user_filter, admin_filter, bind_dn, bind_password, username_attr, email_attr): - self.ldap_server=ldap_server - self.base_dn=base_dn - self.user_filter=user_filter - self.admin_filter=admin_filter - self.bind_dn=bind_dn - self.bind_password=bind_password - self.username_attr=username_attr - self.email_attr=email_attr + def __init__( + self, + ldap_server, + base_dn, + user_filter, + admin_filter, + bind_dn, + bind_password, + username_attr, + email_attr, + ): + self.ldap_server = ldap_server + self.base_dn = base_dn + self.user_filter = user_filter + self.admin_filter = admin_filter + self.bind_dn = bind_dn + self.bind_password = bind_password + self.username_attr = username_attr + self.email_attr = email_attr if not self.ldap_server: raise ValueError("No LDAP configured") self.server = ldap3.Server(self.ldap_server, get_info="ALL") - def try_auth(self,user, password): - admin= False + def try_auth(self, user, password): + admin = False if self.admin_filter: entrie = self.search_user(user, self.admin_filter) if entrie: @@ -35,7 +44,7 @@ def try_auth(self,user, password): return False try: with ldap3.Connection( - self.server, entrie.entry_dn, password, read_only=True + self.server, entrie["entry_dn"], password, read_only=True ) as conn: return { "uid": entrie[self.username_attr], @@ -46,8 +55,7 @@ def try_auth(self,user, password): logger.warning("wrong password for user {0}".format(user)) return False - def search_user(self,user, filter): - + def search_user(self, user, filter): try: with ldap3.Connection( self.server, self.bind_dn, self.bind_password, read_only=True @@ -59,9 +67,7 @@ def search_user(self,user, filter): ) entries = conn.entries except ldap3.core.exceptions.LDAPBindError: - logger.warning( - "wrong can't bind LDAP with {0}".format(self.bind_dn)) - + logger.warning("wrong can't bind LDAP with {0}".format(self.bind_dn)) for entrie in entries: if entrie[self.username_attr] == user: return entrie From 289eddafb596dfce3f26eb9bef51208445d3ac30 Mon Sep 17 00:00:00 2001 From: vincent Date: Sun, 19 Mar 2023 22:11:04 +0100 Subject: [PATCH 12/29] add ldap3 to dependance --- tests/managers/test_manager_ldap.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/managers/test_manager_ldap.py b/tests/managers/test_manager_ldap.py index 8f44e4e..f2d39c1 100644 --- a/tests/managers/test_manager_ldap.py +++ b/tests/managers/test_manager_ldap.py @@ -1,4 +1,3 @@ -import ldap3 from supysonic import db from supysonic.managers.ldap import LdapManager import unittest From bba7ae60af8033feb50b3a6ded58ff98bdcafbf3 Mon Sep 17 00:00:00 2001 From: vincent Date: Mon, 20 Mar 2023 19:13:16 +0100 Subject: [PATCH 13/29] add usermanager test --- supysonic/managers/user.py | 5 ++++- tests/managers/test_manager_user.py | 20 +++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/supysonic/managers/user.py b/supysonic/managers/user.py index b0b15b6..00d8f77 100644 --- a/supysonic/managers/user.py +++ b/supysonic/managers/user.py @@ -16,7 +16,6 @@ try: from .ldap import LdapManager - ldap=LdapManager(**get_current_config().LDAP) except: ldap=None @@ -52,6 +51,10 @@ def delete_by_name(name): @staticmethod def try_auth(name, password): + try: + ldap=LdapManager(**get_current_config().LDAP) + except: + ldap= None if ldap: ldap_user = ldap.try_auth(name, password) else: diff --git a/tests/managers/test_manager_user.py b/tests/managers/test_manager_user.py index 6fc9d7c..9bf57ef 100644 --- a/tests/managers/test_manager_user.py +++ b/tests/managers/test_manager_user.py @@ -8,8 +8,9 @@ from supysonic import db from supysonic.managers.user import UserManager - +from supysonic.config import get_current_config import unittest +from unittest.mock import patch import uuid @@ -142,6 +143,23 @@ def test_try_auth(self): # Non-existent user self.assertIsNone(UserManager.try_auth("null", "null")) + + @patch('supysonic.managers.ldap.ldap3.Connection') + def test_try_auth_ldap(self,mock_object): + config=get_current_config() + config.LDAP["ldap_server"]="fakeserver" + mock_object.return_value.__enter__.return_value.entries = [ + {"uid":"toto", "entry_dn":"cn=toto", "mail":"toto@example.com"}] + authed= UserManager.try_auth('toto','toto') + user = db.User.get(name="toto") + self.assertEqual(authed, user) + + # Non-existent user + + self.assertIsNone(UserManager.try_auth('tata','toto')) + + + def test_change_password(self): self.create_data() From 69e64aaf9fc627e4c9b4e3bdb1884320dff71827 Mon Sep 17 00:00:00 2001 From: vincent Date: Mon, 20 Mar 2023 19:26:48 +0100 Subject: [PATCH 14/29] test admin an mail change --- tests/managers/test_manager_user.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/managers/test_manager_user.py b/tests/managers/test_manager_user.py index 9bf57ef..018b311 100644 --- a/tests/managers/test_manager_user.py +++ b/tests/managers/test_manager_user.py @@ -154,6 +154,14 @@ def test_try_auth_ldap(self,mock_object): user = db.User.get(name="toto") self.assertEqual(authed, user) + # test admin and mail change + config.LDAP["admin_filter"]="fake_admin_filer" + mock_object.return_value.__enter__.return_value.entries = [ + {"uid":"toto", "entry_dn":"cn=toto", "mail":"toto2@example.com"}] + authed= UserManager.try_auth('toto','toto') + self.assertEqual(authed.mail,"toto2@example.com") + self.assertEqual(authed.admin,True) + # Non-existent user self.assertIsNone(UserManager.try_auth('tata','toto')) From ce5213015189871575fd79d1ac6701f3c2b8799b Mon Sep 17 00:00:00 2001 From: vincent Date: Mon, 20 Mar 2023 22:52:06 +0100 Subject: [PATCH 15/29] fix postgres migration --- supysonic/schema/migration/postgres/20230314.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/supysonic/schema/migration/postgres/20230314.sql b/supysonic/schema/migration/postgres/20230314.sql index eecbebf..5f73a9e 100644 --- a/supysonic/schema/migration/postgres/20230314.sql +++ b/supysonic/schema/migration/postgres/20230314.sql @@ -1,3 +1,3 @@ -ALTER TABLE user - ALTER COLUMN password CHAR(40), - ALTER COLUMN salt CHAR(6); +ALTER TABLE "user" + ALTER COLUMN password DROP NOT NULL, + ALTER COLUMN salt DROP NOT NULL; From 448302f5721bb1b8e38cf62b610700f4409fc043 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 21 Mar 2023 19:32:56 +0100 Subject: [PATCH 16/29] return false if can't bind ldap --- supysonic/managers/ldap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/supysonic/managers/ldap.py b/supysonic/managers/ldap.py index 59db5c9..33f7278 100644 --- a/supysonic/managers/ldap.py +++ b/supysonic/managers/ldap.py @@ -68,6 +68,7 @@ def search_user(self, user, filter): entries = conn.entries except ldap3.core.exceptions.LDAPBindError: logger.warning("wrong can't bind LDAP with {0}".format(self.bind_dn)) + return False for entrie in entries: if entrie[self.username_attr] == user: return entrie From f16d29e30149f2ff9b52834f7f926ee565ff8ca7 Mon Sep 17 00:00:00 2001 From: vincent Date: Tue, 21 Mar 2023 20:33:19 +0100 Subject: [PATCH 17/29] fix issue with ldap entry_dn call --- supysonic/managers/ldap.py | 2 +- tests/managers/test_manager_ldap.py | 8 +++++++- tests/managers/test_manager_user.py | 5 +++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/supysonic/managers/ldap.py b/supysonic/managers/ldap.py index 33f7278..11edbda 100644 --- a/supysonic/managers/ldap.py +++ b/supysonic/managers/ldap.py @@ -44,7 +44,7 @@ def try_auth(self, user, password): return False try: with ldap3.Connection( - self.server, entrie["entry_dn"], password, read_only=True + self.server, entrie.entry_dn, password, read_only=True ) as conn: return { "uid": entrie[self.username_attr], diff --git a/tests/managers/test_manager_ldap.py b/tests/managers/test_manager_ldap.py index f2d39c1..9a4fe11 100644 --- a/tests/managers/test_manager_ldap.py +++ b/tests/managers/test_manager_ldap.py @@ -14,6 +14,12 @@ "email_attr": "mail", } +class MockEntrie (): + def __init__(self,dn,attr): + self.entry_dn=dn + self.attribute=attr + def __getitem__(self, item): + return self.attribute[item] class LdapManagerTestCase(unittest.TestCase): @@ -37,7 +43,7 @@ def test_ldapManager_searchUser(self, mock_object): @patch('supysonic.managers.ldap.ldap3.Connection') def test_ldapManager_try_auth(self, mock_object): mock_object.return_value.__enter__.return_value.entries = [ - {LDAP["username_attr"]:"toto", "entry_dn":"cn=toto", "mail":"toto@example.com"}] + MockEntrie ("cn=toto",{LDAP["username_attr"]:"toto", "mail":"toto@example.com"})] ldap = LdapManager(**LDAP) ldap_user = ldap.try_auth("toto", "toto") self.assertFalse(ldap_user["admin"]) diff --git a/tests/managers/test_manager_user.py b/tests/managers/test_manager_user.py index 018b311..41fa186 100644 --- a/tests/managers/test_manager_user.py +++ b/tests/managers/test_manager_user.py @@ -13,6 +13,7 @@ from unittest.mock import patch import uuid +from .test_manager_ldap import MockEntrie class UserManagerTestCase(unittest.TestCase): def setUp(self): @@ -149,7 +150,7 @@ def test_try_auth_ldap(self,mock_object): config=get_current_config() config.LDAP["ldap_server"]="fakeserver" mock_object.return_value.__enter__.return_value.entries = [ - {"uid":"toto", "entry_dn":"cn=toto", "mail":"toto@example.com"}] + MockEntrie ("cn=toto",{config.LDAP["username_attr"]:"toto", "mail":"toto@example.com"})] authed= UserManager.try_auth('toto','toto') user = db.User.get(name="toto") self.assertEqual(authed, user) @@ -157,7 +158,7 @@ def test_try_auth_ldap(self,mock_object): # test admin and mail change config.LDAP["admin_filter"]="fake_admin_filer" mock_object.return_value.__enter__.return_value.entries = [ - {"uid":"toto", "entry_dn":"cn=toto", "mail":"toto2@example.com"}] + MockEntrie ("cn=toto",{config.LDAP["username_attr"]:"toto", "mail":"toto2@example.com"})] authed= UserManager.try_auth('toto','toto') self.assertEqual(authed.mail,"toto2@example.com") self.assertEqual(authed.admin,True) From c6527d09a6cd7a25ca6671a95ca1d8d739c6ea3f Mon Sep 17 00:00:00 2001 From: vithyze <127023076+vithyze@users.noreply.github.com> Date: Mon, 27 Jan 2025 23:45:40 +0000 Subject: [PATCH 18/29] Update config.py --- supysonic/config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/supysonic/config.py b/supysonic/config.py index e391a81..59c487a 100644 --- a/supysonic/config.py +++ b/supysonic/config.py @@ -56,12 +56,12 @@ class DefaultConfig: LASTFM = {"api_key": None, "secret": None} LISTENBRAINZ = {"api_url": "https://api.listenbrainz.org"} LDAP = { - "ldap_server": None, - "base_dn": None, - "user_filter": "(&(objectClass=inetOrgPerson))", - "admin_filter": None, + "server_url": False, "bind_dn": None, - "bind_password": None, + "bind_pw": None, + "base_dn": None, + "user_filter": "(&(objectClass=inetOrgperson)(uid={username}))", + "admin_filter": False, "username_attr": "uid", "email_attr": "mail", } From 1fe581062eb5daf52d3217b750785cd4cf7ee7e0 Mon Sep 17 00:00:00 2001 From: vithyze <127023076+vithyze@users.noreply.github.com> Date: Mon, 27 Jan 2025 23:46:10 +0000 Subject: [PATCH 19/29] Update config.sample --- config.sample | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/config.sample b/config.sample index e5dcda0..ff16220 100644 --- a/config.sample +++ b/config.sample @@ -99,11 +99,14 @@ default_transcode_target = mp3 ;mp3 = audio/mpeg ;ogg = audio/vorbis -[LDAP] -;ldap_server= ldap://ldap.example.com -;base_dn= dc=example,dc=com -;bind_dn= uid=root,cn=users,dc=example,dc=com -;bind_password= -;user_filter= (&(memberOf=CN=SupysonicUsers,cn=groups,dc=example,dc=com)) -;admin_filter= (&(memberOf=CN=SupysonicAdmins,cn=groups,dc=example,dc=com)) - +[ldap] +;server_url = ldapi://%2Frun%2Fslapd%2Fldapi ;server_url = ldap://127.0.0.1:389 +;bind_dn = cn=username,dc=example,dc=org +;bind_pw = password +;base_dn = ou=Users,dc=example,dc=org + +; Optional parameters with default values +;user_filter = (&(objectClass=inetOrgperson)(uid={username})) +;admin_filter = +;username_attr = uid +;email_attr = mail From afc6fa7647bb0289bae65f41923e0eb617bc5b15 Mon Sep 17 00:00:00 2001 From: vithyze <127023076+vithyze@users.noreply.github.com> Date: Mon, 27 Jan 2025 23:46:39 +0000 Subject: [PATCH 20/29] Update configuration.rst --- docs/setup/configuration.rst | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/docs/setup/configuration.rst b/docs/setup/configuration.rst index ddf4ef8..057d630 100644 --- a/docs/setup/configuration.rst +++ b/docs/setup/configuration.rst @@ -364,27 +364,34 @@ See the following links for a list of examples: ;mp3 = audio/mpeg ;ogg = audio/vorbis -``[LDAP]`` section +``[ldap]`` section ----------------------- -This section define LDAP connection parameters -when user is find on LDAP server and not exist in supysonic database, -user is add to database +This section defines the LDAP connection parameters. +when an LDAP user is found on a server and doesn't exist in the Supysonic database, +a new user is created. -``ldap_server`` - ldap URL - -``base_dn`` - ldap dn where search is perform +``server_url`` + URL of the LDAP server ``bind_dn`` - user dn use to perform query +``bind_pw`` + Bind credentials used for the search query -`bind_password`` - password for bind_dn +``base_dn`` + Base DN where the search is performed ``user_filter`` - ldap filter use for lookup standart user + Filter for finding users + A special variable ``{username}`` can be used for filtering ``admin_filter`` - ldap filter use for lookup admin user + Same as ``user_filter`` but for finding admins + +``username_attr`` + Attribute containing the username + Default is ``uid`` + +``email_attr`` + Attribute containing the e-mail address + Default is ``mail`` From ea98ac910b3833e4955d23a10a2a433a3994c4b1 Mon Sep 17 00:00:00 2001 From: vithyze <127023076+vithyze@users.noreply.github.com> Date: Mon, 27 Jan 2025 23:47:37 +0000 Subject: [PATCH 21/29] Update ldap.py --- supysonic/managers/ldap.py | 109 +++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 60 deletions(-) diff --git a/supysonic/managers/ldap.py b/supysonic/managers/ldap.py index 11edbda..b68f60a 100644 --- a/supysonic/managers/ldap.py +++ b/supysonic/managers/ldap.py @@ -1,75 +1,64 @@ import logging - - -import ldap3 - +try: + import ldap3 +except ModuleNotFoundError: + ldap3 = None logger = logging.getLogger(__name__) - class LdapManager: - def __init__( - self, - ldap_server, - base_dn, - user_filter, - admin_filter, - bind_dn, - bind_password, - username_attr, - email_attr, - ): - self.ldap_server = ldap_server - self.base_dn = base_dn - self.user_filter = user_filter - self.admin_filter = admin_filter - self.bind_dn = bind_dn - self.bind_password = bind_password - self.username_attr = username_attr - self.email_attr = email_attr - if not self.ldap_server: - raise ValueError("No LDAP configured") - self.server = ldap3.Server(self.ldap_server, get_info="ALL") + def __init__(self, **config): + if not config["server_url"]: + raise ValueError('No LDAP Server_url configure ') + elif not ldap3: + logger.error("Module 'ldap3' is not installed.") + raise ValueError('Module ldap3 is not installed') + elif None in config.values(): + logger.error("Some required LDAP parameters are missing.") + raise ValueError('some required LDAP paramters are missing') + + self.server = ldap3.Server(config["server_url"], get_info=None) + self.config = config - def try_auth(self, user, password): + def try_auth(self, username, password): admin = False - if self.admin_filter: - entrie = self.search_user(user, self.admin_filter) - if entrie: - logger.debug("{0} is admin".format(user)) + + if self.config["admin_filter"]: + entry = self.search_user(username, self.config["admin_filter"]) + if entry: + logger.info(f"User '{username}' is admin.") admin = True + if not admin: - entrie = self.search_user(user, self.user_filter) - if not entrie: - return False - try: - with ldap3.Connection( - self.server, entrie.entry_dn, password, read_only=True - ) as conn: - return { - "uid": entrie[self.username_attr], - "mail": entrie[self.email_attr], - "admin": admin, - } - except ldap3.core.exceptions.LDAPBindError: - logger.warning("wrong password for user {0}".format(user)) - return False + entry = self.search_user(username, self.config["user_filter"]) + + if entry: + try: + with ldap3.Connection(self.server, entry.entry_dn, password, read_only=True): + return { + "mail": entry[self.config["email_attr"]], + "admin": admin + } + except ldap3.core.exceptions.LDAPBindError: + logger.error(f"Bind failed for '{entry.entry_dn}'.") + except Exception as e: + logger.error(f"LDAP error: {e}") - def search_user(self, user, filter): + def search_user(self, username, _filter): try: - with ldap3.Connection( - self.server, self.bind_dn, self.bind_password, read_only=True - ) as conn: + with ldap3.Connection(self.server, self.config["bind_dn"], self.config["bind_pw"], read_only=True) as conn: conn.search( - self.base_dn, - filter, - attributes=[self.email_attr, self.username_attr], + self.config["base_dn"], + _filter.format(username=username), + attributes=[self.config["username_attr"], self.config["email_attr"]], + size_limit=1 ) entries = conn.entries + if entries and entries[0][self.config["username_attr"]] == username: + return entries[0] + else: + logger.info(f"User '{username}' not found in LDAP database.") except ldap3.core.exceptions.LDAPBindError: - logger.warning("wrong can't bind LDAP with {0}".format(self.bind_dn)) - return False - for entrie in entries: - if entrie[self.username_attr] == user: - return entrie - return False + logger.error(f"Bind failed for '{self.config['bind_dn']}'.") + except Exception as e: + logger.error(f"LDAP error: {e}") From 7a7442e4102d7926d7f652f1388e96dbaf671253 Mon Sep 17 00:00:00 2001 From: vithyze <127023076+vithyze@users.noreply.github.com> Date: Mon, 27 Jan 2025 23:48:16 +0000 Subject: [PATCH 22/29] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8f32e95..6276494 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Current supported features are: * [Last.fm][lastfm] scrobbling * [ListenBrainz][listenbrainz] scrobbling * Jukebox mode -* LDAP authentification +* LDAP authentication Supysonic currently targets the version 1.12.0 of the Subsonic API. For more details, go check the [API implementation status][docs-api]. From f52d559dcbf7b869d6544587580e726bd38921d2 Mon Sep 17 00:00:00 2001 From: vithyze <127023076+vithyze@users.noreply.github.com> Date: Mon, 27 Jan 2025 23:48:45 +0000 Subject: [PATCH 23/29] Update setup.cfg --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ca06214..661c2d4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -60,7 +60,6 @@ install_requires = mediafile watchdog >=0.8.0 zipstream-ng >=1.1.0, <2.0.0 - ldap3 packages = find_namespace: include_package_data = true From 614d8a1f01b0672b06c01b3d295fd68b455f6d9c Mon Sep 17 00:00:00 2001 From: vithyze <127023076+vithyze@users.noreply.github.com> Date: Mon, 27 Jan 2025 23:49:16 +0000 Subject: [PATCH 24/29] Update test_manager_ldap.py --- tests/managers/test_manager_ldap.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/managers/test_manager_ldap.py b/tests/managers/test_manager_ldap.py index 9a4fe11..e9c3787 100644 --- a/tests/managers/test_manager_ldap.py +++ b/tests/managers/test_manager_ldap.py @@ -4,12 +4,12 @@ from unittest.mock import patch LDAP = { - "ldap_server": "fakeServer", + "server_url": "fakeServer", + "bind_dn": "cn=my_user,ou=test,o=lab", + "bind_pw": "my_password", "base_dn": "ou=test,o=lab", "user_filter": "(&(objectClass=inetOrgPerson))", - "admin_filter": None, - "bind_dn": "cn=my_user,ou=test,o=lab", - "bind_password": 'my_password', + "admin_filter": False, "username_attr": "uid", "email_attr": "mail", } @@ -30,7 +30,7 @@ def setUp(self): def tearDown(self): pass - @patch('supysonic.managers.ldap.ldap3.Connection') + @patch("supysonic.managers.ldap.ldap3.Connection") def test_ldapManager_searchUser(self, mock_object): mock_object.return_value.__enter__.return_value.entries = [ {LDAP["username_attr"]:"toto"}] @@ -40,14 +40,13 @@ def test_ldapManager_searchUser(self, mock_object): ldap_user = ldap.search_user("tata", LDAP["user_filter"]) self.assertFalse(ldap_user) - @patch('supysonic.managers.ldap.ldap3.Connection') + @patch("supysonic.managers.ldap.ldap3.Connection") def test_ldapManager_try_auth(self, mock_object): mock_object.return_value.__enter__.return_value.entries = [ - MockEntrie ("cn=toto",{LDAP["username_attr"]:"toto", "mail":"toto@example.com"})] + MockEntrie ("cn=toto",{"mail":"toto@example.com"})] ldap = LdapManager(**LDAP) ldap_user = ldap.try_auth("toto", "toto") self.assertFalse(ldap_user["admin"]) - self.assertEqual(ldap_user[LDAP["username_attr"]], "toto") self.assertEqual(ldap_user[LDAP["email_attr"]], "toto@example.com") ldap_user = ldap.try_auth("tata", "tata") self.assertFalse(ldap_user) From f7fd0d7d1822f42813491ece21881ac8333f6f94 Mon Sep 17 00:00:00 2001 From: vithyze <127023076+vithyze@users.noreply.github.com> Date: Mon, 27 Jan 2025 23:49:25 +0000 Subject: [PATCH 25/29] Update test_manager_user.py --- tests/managers/test_manager_user.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/managers/test_manager_user.py b/tests/managers/test_manager_user.py index 41fa186..83b0cf7 100644 --- a/tests/managers/test_manager_user.py +++ b/tests/managers/test_manager_user.py @@ -145,27 +145,27 @@ def test_try_auth(self): self.assertIsNone(UserManager.try_auth("null", "null")) - @patch('supysonic.managers.ldap.ldap3.Connection') + @patch("supysonic.managers.ldap.ldap3.Connection") def test_try_auth_ldap(self,mock_object): - config=get_current_config() - config.LDAP["ldap_server"]="fakeserver" + config = get_current_config() + config.LDAP["ldap_server"] = "fakeserver" mock_object.return_value.__enter__.return_value.entries = [ - MockEntrie ("cn=toto",{config.LDAP["username_attr"]:"toto", "mail":"toto@example.com"})] - authed= UserManager.try_auth('toto','toto') + MockEntrie ("cn=toto",{"mail":"toto@example.com"})] + authed = UserManager.try_auth("toto","toto") user = db.User.get(name="toto") self.assertEqual(authed, user) # test admin and mail change - config.LDAP["admin_filter"]="fake_admin_filer" + config.LDAP["admin_filter"] = "fake_admin_filter" mock_object.return_value.__enter__.return_value.entries = [ - MockEntrie ("cn=toto",{config.LDAP["username_attr"]:"toto", "mail":"toto2@example.com"})] - authed= UserManager.try_auth('toto','toto') + MockEntrie ("cn=toto",{"mail":"toto2@example.com", "admin":True})] + authed= UserManager.try_auth("toto","toto") self.assertEqual(authed.mail,"toto2@example.com") self.assertEqual(authed.admin,True) # Non-existent user - self.assertIsNone(UserManager.try_auth('tata','toto')) + self.assertIsNone(UserManager.try_auth("tata","toto")) From 5ff8f60fe1ff737d4b7a9dbc58afeeade080145f Mon Sep 17 00:00:00 2001 From: vithyze <127023076+vithyze@users.noreply.github.com> Date: Mon, 27 Jan 2025 23:49:57 +0000 Subject: [PATCH 26/29] Update user.py --- supysonic/managers/user.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/supysonic/managers/user.py b/supysonic/managers/user.py index 00d8f77..240aeee 100644 --- a/supysonic/managers/user.py +++ b/supysonic/managers/user.py @@ -13,11 +13,7 @@ from ..db import User from ..config import get_current_config - -try: - from .ldap import LdapManager -except: - ldap=None +from .ldap import LdapManager class UserManager: @staticmethod @@ -52,22 +48,19 @@ def delete_by_name(name): @staticmethod def try_auth(name, password): try: - ldap=LdapManager(**get_current_config().LDAP) - except: - ldap= None - if ldap: - ldap_user = ldap.try_auth(name, password) - else: - ldap_user= False + ldap = LdapManager(**get_current_config().LDAP) + except ValueError: + ldap = None + ldap_user = ldap.try_auth(name, password) if ldap else None user = User.get_or_none(name=name) if ldap_user: if user is None: - user = User.create(name=name,mail=ldap_user["mail"],admin=ldap_user["admin"]) + user = User.create(name=name, mail=ldap_user["mail"], admin=ldap_user["admin"]) else: - if user.admin != ldap_user['admin']: - user.admin=ldap_user['admin'] - if user.mail != ldap_user['mail']: - user.mail=ldap_user['mail'] + if user.admin != ldap_user["admin"]: + user.admin = ldap_user["admin"] + if user.mail != ldap_user["mail"]: + user.mail = ldap_user["mail"] return user else: if user is None: From 421f594bd3cd2e2a9597b1bbe163d20444580074 Mon Sep 17 00:00:00 2001 From: vincent Date: Thu, 30 Jan 2025 21:52:50 +0100 Subject: [PATCH 27/29] update ldap tests --- tests/managers/test_manager_ldap.py | 12 +++++++----- tests/managers/test_manager_user.py | 9 ++++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/managers/test_manager_ldap.py b/tests/managers/test_manager_ldap.py index e9c3787..7f35811 100644 --- a/tests/managers/test_manager_ldap.py +++ b/tests/managers/test_manager_ldap.py @@ -33,20 +33,22 @@ def tearDown(self): @patch("supysonic.managers.ldap.ldap3.Connection") def test_ldapManager_searchUser(self, mock_object): mock_object.return_value.__enter__.return_value.entries = [ - {LDAP["username_attr"]:"toto"}] + {LDAP["email_attr"]:"toto@example.com", + LDAP["username_attr"]:"toto" + }] ldap = LdapManager(**LDAP) ldap_user = ldap.search_user("toto", LDAP["user_filter"]) - self.assertEqual(ldap_user[LDAP["username_attr"]], "toto") + self.assertEqual(ldap_user[LDAP["email_attr"]], "toto@example.com") ldap_user = ldap.search_user("tata", LDAP["user_filter"]) - self.assertFalse(ldap_user) + self.assertIsNone(ldap_user) @patch("supysonic.managers.ldap.ldap3.Connection") def test_ldapManager_try_auth(self, mock_object): mock_object.return_value.__enter__.return_value.entries = [ - MockEntrie ("cn=toto",{"mail":"toto@example.com"})] + MockEntrie ("cn=toto",{LDAP["email_attr"]:"toto@example.com", LDAP["username_attr"]:"toto"})] ldap = LdapManager(**LDAP) ldap_user = ldap.try_auth("toto", "toto") self.assertFalse(ldap_user["admin"]) self.assertEqual(ldap_user[LDAP["email_attr"]], "toto@example.com") ldap_user = ldap.try_auth("tata", "tata") - self.assertFalse(ldap_user) + self.assertIsNone(ldap_user) diff --git a/tests/managers/test_manager_user.py b/tests/managers/test_manager_user.py index 83b0cf7..a932cf9 100644 --- a/tests/managers/test_manager_user.py +++ b/tests/managers/test_manager_user.py @@ -148,9 +148,12 @@ def test_try_auth(self): @patch("supysonic.managers.ldap.ldap3.Connection") def test_try_auth_ldap(self,mock_object): config = get_current_config() - config.LDAP["ldap_server"] = "fakeserver" + config.LDAP["server_url"] = "fakeserver" + config.LDAP["bind_dn"]="cn=my_user,ou=test,o=lab" + config.LDAP["bind_pw"]= "my_password", + config.LDAP["base_dn"]= "ou=test,o=lab", mock_object.return_value.__enter__.return_value.entries = [ - MockEntrie ("cn=toto",{"mail":"toto@example.com"})] + MockEntrie ("cn=toto",{config.LDAP["email_attr"]:"toto@example.com",config.LDAP["username_attr"]:"toto"})] authed = UserManager.try_auth("toto","toto") user = db.User.get(name="toto") self.assertEqual(authed, user) @@ -158,7 +161,7 @@ def test_try_auth_ldap(self,mock_object): # test admin and mail change config.LDAP["admin_filter"] = "fake_admin_filter" mock_object.return_value.__enter__.return_value.entries = [ - MockEntrie ("cn=toto",{"mail":"toto2@example.com", "admin":True})] + MockEntrie ("cn=toto",{config.LDAP["email_attr"]:"toto2@example.com",config.LDAP["username_attr"]:"toto"})] authed= UserManager.try_auth("toto","toto") self.assertEqual(authed.mail,"toto2@example.com") self.assertEqual(authed.admin,True) From f6334d55493b24cb3fd4f129cffc57bd9fec579a Mon Sep 17 00:00:00 2001 From: vincent Date: Thu, 30 Jan 2025 23:05:27 +0100 Subject: [PATCH 28/29] add ldap3 to requirement --- ci-requirements.txt | 1 + setup.cfg | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ci-requirements.txt b/ci-requirements.txt index 161c59d..df6693f 100644 --- a/ci-requirements.txt +++ b/ci-requirements.txt @@ -2,3 +2,4 @@ lxml coverage +ldap3 diff --git a/setup.cfg b/setup.cfg index 661c2d4..fd8549a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,6 +54,7 @@ python_requires = >=3.7 install_requires = click flask >=0.11 + ldap3 peewee Pillow >=9.1.0 requests >=1.0.0 @@ -73,6 +74,8 @@ console_scripts = supysonic-cli = supysonic.cli:main supysonic-daemon = supysonic.daemon:main supysonic-server = supysonic.server:main - +[options.extras_require] + ldap= + ldap3 [options.data_files] share/man/man1 = man/*.1 From b58984a2119612f2336d6593512d3227ffd9e715 Mon Sep 17 00:00:00 2001 From: vithyze <127023076+vithyze@users.noreply.github.com> Date: Thu, 30 Jan 2025 23:30:52 +0000 Subject: [PATCH 29/29] chore: clean LdapManager exception --- supysonic/managers/ldap.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/supysonic/managers/ldap.py b/supysonic/managers/ldap.py index b68f60a..e2d2136 100644 --- a/supysonic/managers/ldap.py +++ b/supysonic/managers/ldap.py @@ -9,13 +9,14 @@ class LdapManager: def __init__(self, **config): if not config["server_url"]: - raise ValueError('No LDAP Server_url configure ') + logger.debug("LDAP 'server_url' is not configured.") + raise ValueError elif not ldap3: logger.error("Module 'ldap3' is not installed.") - raise ValueError('Module ldap3 is not installed') + raise ValueError elif None in config.values(): logger.error("Some required LDAP parameters are missing.") - raise ValueError('some required LDAP paramters are missing') + raise ValueError self.server = ldap3.Server(config["server_url"], get_info=None) self.config = config