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.

Edit: Coming back around this week (Aug 10) - thanks for your patience.

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