Skip to content

[ldap-user-sync] Add LDAP user sync plugin - #207

Open
ModdingFriendly wants to merge 8 commits into
Dispatcharr:mainfrom
ModdingFriendly:add-ldap-user-sync-plugin
Open

[ldap-user-sync] Add LDAP user sync plugin#207
ModdingFriendly wants to merge 8 commits into
Dispatcharr:mainfrom
ModdingFriendly:add-ldap-user-sync-plugin

Conversation

@ModdingFriendly

Copy link
Copy Markdown

Summary

  • New standard (full-source) plugin: syncs Dispatcharr Admin/Streamer accounts from any standard LDAP directory based on group membership (generic - not tied to any vendor; developed and tested against an Authentik LDAP outpost).
  • Vendors ldap3 (LGPL-3.0-only) and pyasn1 (BSD-2-Clause) as unmodified source under vendor/, since neither is installed in the Dispatcharr venv and plugins have no dependency-install mechanism. Licenses preserved verbatim under vendor/licenses/.
  • XC (Xtream Codes) API password is generated once per new user and emailed via plain smtplib/ssl (styled to match the M3U Expiration Notifier plugin's email template); existing users keep their XC password stable across future syncs. A manual per-user reset action is included.
  • Users removed from both LDAP groups are disabled (is_active=False), toggle-able via settings.
  • Optional (off by default) true LDAP pass-through login via a custom Django authentication backend registered at plugin-load time, kept in its own reload-durable module so a plugin disable/reload never breaks an in-flight login attempt - see the plugin README for the full design and documented limitations.
  • Scheduling uses a self-elected background thread with file-based locking (the same pattern the M3U Expiration Notifier plugin settled on), since Celery Beat doesn't work for dynamically-loaded plugin code.

Test plan

  • Unit-style verification against an in-process ldap3 MOCK_SYNC directory (no real network): create with correct role/unusable-password/XC-password generation, XC password stays stable across a repeat sync, disable-on-group-removal leaves XC password untouched, and the reset action rotates the password independently of email delivery outcome - all exercised against a real Postgres via the Django ORM, test users cleaned up after.
  • Real SMTP delivery verified end-to-end through a live mail relay (including the styled HTML template, confirmed visually).
  • Real LDAP bind verified against a live Authentik LDAP outpost: Test LDAP Connection correctly resolved group membership, and a real Sync Now run correctly created/updated the expected accounts, including correctly applying "admin takes precedence" when a user is a member of both groups.
  • Tested against a running Dispatcharr instance (v0.28.2) end-to-end per CONTRIBUTING.md's requirement.

Syncs Admin/Streamer accounts from any standard LDAP directory into
Dispatcharr on a schedule or on demand, with XC password provisioning
and email delivery.
@dispatcharr-plugins-bot dispatcharr-plugins-bot Bot added the New Plugin A new plugin being contributed to the repository. label Aug 2, 2026
@dispatcharr-plugins-bot

This comment has been minimized.

Stop passing an explicit TLS version into ldap3's Tls() so it takes
the hardened ssl.create_default_context() path (TLS 1.2+ floor)
instead of a manual SSLContext with no such floor - this plugin's own
choice was what triggered ldap3's py/insecure-protocol-flagged branch.

Remove the lazily-imported, never-used ntlm.py (only reached via
explicit NTLM bind, which this plugin never performs), and suppress
the one unavoidable weak-hashing finding in digestMd5.py with an
inline codeql[] comment plus an explanation: MD5 there implements the
DIGEST-MD5 SASL mechanism's RFC 2831 challenge-response computation,
not password storage, and the function is kept only because
core/connection.py imports it unconditionally.
@dispatcharr-plugins-bot

This comment has been minimized.

@sethwv

sethwv commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

I'll take a quick look as to why the inline suppression comment isn't working on

return hashlib.md5(value).digest() # codeql[py/weak-sensitive-data-hashing]

@sethwv

sethwv commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

I'll take a quick look as to why the inline suppression comment isn't working on

return hashlib.md5(value).digest() # codeql[py/weak-sensitive-data-hashing]

The fix is to move the suppression comment to its own line above the line it's intended to suppress:

    # codeql[py/weak-sensitive-data-hashing]
    return hashlib.md5(value).digest()

Regarding the py/insecure-protocol that stuck around, you'll probably want to either add a suppression to, or otherwise modify your vendored tls.py file around lines 202 and 204.

The previous fix didn't hold: CodeQL's py/insecure-protocol query
flags tls.py's own permissiveness (its SSLContext(self.version) branch
enforces no minimum version by default) regardless of what any specific
caller passes in, and the inline `codeql[]` suppression comment on
digestMd5.py's md5_h() was not honored by this repo's SARIF-parsing
report step.

Fix both properly instead of relying on caller behavior or suppression:
- tls.py: explicitly set minimum_version = TLSv1_2 on that branch, so
  the library itself can no longer negotiate an old protocol version.
- digestMd5.py: stub md5_h/md5_kd/md5_hex/md5_hmac to raise
  NotImplementedError, removing every hashlib.md5 call from the file
  instead of just commenting around it. core/connection.py and the
  strategy modules still import these names fine; DIGEST-MD5 SASL auth
  (which is all that ever calls them) is simply unsupported by this
  vendored copy, matching the ntlm.py removal in the previous commit.
@dispatcharr-plugins-bot

This comment has been minimized.

The previous minimum_version fix ran too late: SSLContext(self.version)
can pin the context to a single legacy protocol before that line ever
executes, so setting minimum_version afterward doesn't undo it - CodeQL
correctly kept flagging this as reachable.

Validate `version` in Tls.__init__ instead and raise
LDAPSSLConfigurationError for any SSLv2/SSLv3/TLSv1/TLSv1.1/SSLv23
constant, checked defensively via hasattr since several no longer exist
on newer Python. This plugin never passes an explicit version at all
(defaults to None, i.e. ssl.create_default_context()), so this only
ever closes off a capability nothing here uses. Keeps the earlier
minimum_version assignment as defense in depth.
@dispatcharr-plugins-bot

This comment has been minimized.

Third attempt at the same finding: CodeQL's py/insecure-protocol
flags the mere existence of ssl.SSLContext(self.version) feeding a
wrap_socket() call, regardless of runtime validation on `version`
(the previous commit's __init__ guard didn't change the result).

Delete that branch outright and always build the context via
create_default_context() (TLS 1.2+ floor, verified certs) - this
plugin never passes an explicit `version` at all, so nothing depends
on the removed path.
@dispatcharr-plugins-bot

This comment has been minimized.

CodeQL's py/insecure-protocol treats create_default_context() itself as
not guaranteed to exclude TLSv1/TLSv1.1 unless the returned context also
has minimum_version set explicitly - the previous commit removed the
custom-version branch but didn't set this on the remaining
create_default_context() path. Add it there too.
@dispatcharr-plugins-bot

This comment has been minimized.

…mments

A maintainer pointed out the inline suppression comment needs to be on
its own line above the flagged statement, not a trailing same-line
comment - that's why it wasn't honored before. Revert all of the
previous commits' vendored-source modifications (ntlm.py deletion,
digestMd5.py stubbing, tls.py SSLContext rewrite) back to byte-identical
upstream, and instead annotate the actual flagged lines in place with
correctly-formatted `codeql[<rule-id>]` comments plus an explanation of
why each is a protocol-mandated construct this plugin never exercises
(SIMPLE bind only, no DIGEST-MD5/NTLM, no explicit TLS version).
@dispatcharr-plugins-bot

Copy link
Copy Markdown

Plugin Validation Results

Modified plugins: 1

Plugin: ldap-user-sync

Syncs Dispatcharr user accounts from any standard LDAP directory (Active Directory, OpenLDAP, 389 Directory Server, Authentik's LDAP outpost, etc.). On a configurable schedule or on demand, binds to your directory, finds members of two configurable group DNs, and creates/updates the matching Dispatcharr users as Admins or Streamers, disabling accounts that fall out of both groups. Generates and emails a random Xtream Codes API password for each newly created user via plain SMTP, with a manual per-user reset action. Optionally enables true LDAP pass-through login so a user's Dispatcharr login password always matches their live LDAP password.

Check Status Details
Required fields All required fields present
Maintainers ModdingFriendly
License MIT - MIT License
Permission New plugin - ModdingFriendly listed in author/maintainers
Version 1.0.0
Version bump New plugin

CodeQL found 6 high or critical issue(s) - these must be fixed before merging.

Rule Location Description
py/weak-sensitive-data-hashing plugins/ldap-user-sync/vendor/ldap3/protocol/sasl/digestMd5.py:49 Sensitive data (password) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function.
py/weak-sensitive-data-hashing plugins/ldap-user-sync/vendor/ldap3/utils/ntlm.py:491 Sensitive data (password) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD5) that is insecure for password hashing, since it is not a computationally expensive hash function.
py/weak-sensitive-data-hashing plugins/ldap-user-sync/vendor/ldap3/utils/ntlm.py:506 Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function.
py/weak-sensitive-data-hashing plugins/ldap-user-sync/vendor/ldap3/utils/ntlm.py:510 Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function. Sensitive data (password) is used in a hashing algorithm (MD4) that is insecure for password hashing, since it is not a computationally expensive hash function.
py/insecure-protocol plugins/ldap-user-sync/vendor/ldap3/core/tls.py:208 Insecure SSL/TLS protocol version TLSv1 allowed by call to create_default_context. Insecure SSL/TLS protocol version TLSv1 allowed by call to ssl.SSLContext. Insecure SSL/TLS protocol version TLSv1_1 allowed by call to create_default_context. Insecure SSL/TLS protocol version TLSv1_1 allowed by call to ssl.SSLContext.
py/insecure-protocol plugins/ldap-user-sync/vendor/ldap3/core/tls.py:211 Insecure SSL/TLS protocol version TLSv1 allowed by call to create_default_context. Insecure SSL/TLS protocol version TLSv1 allowed by call to ssl.SSLContext. Insecure SSL/TLS protocol version TLSv1_1 allowed by call to create_default_context. Insecure SSL/TLS protocol version TLSv1_1 allowed by call to ssl.SSLContext.

CodeQL found 1 medium severity issue(s)
These are not blocking, but are included for visibility.

Rule Location Description
py/bind-socket-all-network-interfaces plugins/ldap-user-sync/vendor/ldap3/core/server.py:337 Binding a socket to all interfaces (using '') is a security risk.

❌ Validation failed

Some checks failed. Please review the errors above and update your PR.

@sethwv

sethwv commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

The workflow should be honouring those comments, apologies for all the troubles on this one.
I will take a look later today and see what is happening on our end and if any mitigation is possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

New Plugin A new plugin being contributed to the repository.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants