diff --git a/Makefile b/Makefile index 65360b4..b8bdae1 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,6 @@ build: install: mkdir -p $(DESTDIR)$(PREFIX)/bin $(DESTDIR)/etc $(DESTDIR)/lib/systemd/system - install -m755 mlmmj-ldap-sync $(DESTDIR)$(PREFIX)/bin/mlmmj-ldap-sync - install -m644 conf.example.toml $(DESTDIR)/etc/mlmmj-ldap-sync.toml - install -m644 mlmmj-ldap-sync.service mlmmj-ldap-sync.timer $(DESTDIR)/lib/systemd/system - install -m644 mlmmj-ldap-sync@.service mlmmj-ldap-sync@.timer $(DESTDIR)/lib/systemd/system + install -m755 mailman3-ldap-sync $(DESTDIR)$(PREFIX)/bin/mailman3-ldap-sync + install -m644 conf.example.toml $(DESTDIR)/etc/mailman3-ldap-sync.toml + install -m644 mailman3-ldap-sync.service mailman3-ldap-sync.timer $(DESTDIR)/lib/systemd/system diff --git a/README.md b/README.md index 1249f9e..5b256ea 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ -# mlmmj-ldap-sync +# mailman3-ldap-sync -Allows to sync LDAP groups with mlmmj lists. +Simple script to sync mailman3 lists with LDAP groups, forked from [GNU Generation's +mlmmj-ldap-sync](https://gitlab.gnugen.ch/gnugen/mlmmj-ldap-sync). diff --git a/conf.example.toml b/conf.example.toml index 7d59ee0..a2f772b 100644 --- a/conf.example.toml +++ b/conf.example.toml @@ -1,24 +1,16 @@ -domain = "unipoly.ch" -lists_add = ["membres"] -lists_remove = ["membres"] +domain = "lists.recycled.cloud" +lists = [ 'demo' ] [ldap] -host = "ldap.gnugen.ch" -port = 389 +host = "ldap1.recycled.cloud" +port = 636 [ldap.auth] -username = "cn=unipoly-mlmmj,ou=Services,dc=unipoly,dc=epfl,dc=ch" +username = "uid=mailman3-ldap-sync,ou=services,dc=recycled,dc=cloud" password = "secret" [ldap.lists] -basetree = "ou=Lists,dc=unipoly,dc=epfl,dc=ch" +basetree = "ou=groups,dc=recycled,dc=cloud" [ldap.users] -basetree = "ou=Users,dc=unipoly,dc=epfl,dc=ch" - -[mlmmj] -basepath = "/var/spool/mlmmj" -list_binary = "/usr/bin/mlmmj-list" -sub_binary = "/usr/bin/mlmmj-sub" -unsub_binary = "/usr/bin/mlmmj-unsub" - +basetree = "ou=users,dc=recycled,dc=cloud" diff --git a/debian/changelog b/debian/changelog index 3a5cb3d..983757e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,38 +1,5 @@ -mlmmj-ldap-sync (0.6) unstable; urgency=medium - - * Adapt code for GNU Generation LDAP structure. - * Fix systemd unit files installation. - - -- Timothée Floure Thu, 31 Oct 2019 17:49:44 +0100 - -mlmmj-ldap-sync (0.5) unstable; urgency=medium - - * Rename package from unipoly-mlmmj-ldap-sync to mlmmj-ldap-sync. - * Support custom path for the configuration file. - - -- Timothée Floure Wed, 30 Oct 2019 12:20:01 +0100 - -unipoly-mlmmj-ldap-sync (0.4) unstable; urgency=medium - - * Fix typo in remove_subscriber_from - - -- Timothée Floure Mon, 25 Feb 2019 17:47:40 +0100 - -unipoly-mlmmj-ldap-sync (0.3) unstable; urgency=medium - - * Dissociate addition and removal of users. - - -- Timothée Floure Sun, 23 Dec 2018 12:45:58 +0100 - -unipoly-mlmmj-ldap-sync (0.2) unstable; urgency=low - - * Add systemd units. - * Add default configuration file. - - -- Timothée Floure Wed, 22 Aug 2018 16:25:00 +0200 - -unipoly-mlmmj-ldap-sync (0.1) unstable; urgency=low +mailman3-ldap-sync (0.1) unstable; urgency=medium * Let there be package. - -- Timothée Floure Fri, 29 Jun 2018 12:02:05 +0200 + -- Timothée Floure Thu, 24 Nov 2020 09:17:00 +0100 diff --git a/debian/control b/debian/control index 03bb17f..5e8f21e 100644 --- a/debian/control +++ b/debian/control @@ -1,15 +1,15 @@ -Source: mlmmj-ldap-sync +Source: mailman3-ldap-sync Section: admin Priority: optional Maintainer: Timothée Floure Build-Depends: debhelper (>= 9) Standards-Version: 4.1.1 -Homepage: https://gitlab.gnugen.ch/gnugen/mlmmj-ldap-sync -Vcs-Git: git://gitlab.gnugen.ch/gnugen/mlmmj-ldap-sync.git +Homepage: https://code.recycled.cloud/e-Durable/mailman3-ldap-sync +Vcs-Git: https://code.recycled.cloud/e-Durable/mailman3-ldap-sync -Package: mlmmj-ldap-sync +Package: mailman3-ldap-sync Architecture: all Depends: ruby, ruby-net-ldap, ruby-toml, ${misc:Depends} -Description: Cronjobs to maintain the synchronization of mlmmj mailing lists. +Description: Cronjobs to maintain the synchronization of mailman3 mailing lists. This package installs cronjobs to maintain the synchronization between LDAP groups and mailing lists. diff --git a/debian/copyright b/debian/copyright index f612ee2..b105e0e 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,11 +1,11 @@ Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Files: * -Copyright: 2018 Timothée Floure +Copyright: 2018 Timothée Floure License: Apache-2.0 Files: debian/* -Copyright: 2018 Timothée Floure +Copyright: 2018 Timothée Floure License: Apache-2.0 License: Apache-2.0 diff --git a/mailman3-ldap-sync b/mailman3-ldap-sync new file mode 100755 index 0000000..c75fce6 --- /dev/null +++ b/mailman3-ldap-sync @@ -0,0 +1,146 @@ +#!/usr/bin/env ruby + +require 'toml' +require 'net/ldap' +require 'English' +require 'open3' + +@mailman3_exec = '/usr/bin/mailman' +@configuration_file = '/etc/mailman3-ldap-sync.toml' +@configuration = nil + +def read_configuration(path) + begin + TOML.load_file(path) + rescue Errno::ENOENT => e + puts "Could not open configuration file: #{e}" + exit(1) + end +end + +def connect_ldap + conn = Net::LDAP.new( + host: @configuration['ldap']['host'], + port: @configuration['ldap']['port'], + encryption: :simple_tls, + auth: { + method: :simple, + username: @configuration['ldap']['auth']['username'], + password: @configuration['ldap']['auth']['password'] + } + ) + + ldap_addr = "#{@configuration['ldap']['host']}:#{@configuration['ldap']['port']}" + begin + if conn.bind + conn + else + puts "Failed to authenticate against LDAP server: #{ldap_addr}" + exit(1) + end + rescue Net::LDAP::LdapError => e + puts "Failed to contact LDAP server: #{ldap_addr}" + puts e + exit(1) + end +end + +def get_mailman3_subscribers(list) + unless File.executable?(@mailman3_exec) + puts "could not execute #{@mailman3_exec}" + exit(1) + end + + # Execute mailman and check whether it exited successfuly + raw = `#{@mailman3_exec} members #{list}` + unless $CHILD_STATUS.exitstatus.zero? + puts 'FAIL' + exit(1) + end + + # Return a list of mail addresses. + raw.split("\n") +end + +def add_subscribers_to(list, addresses) + # The address is not subscribed to the list yet. + print "Adding #{addresses.size} addresses... " + + # Subscribe the address to the list. + Open3.popen3(@mailman3_exec, 'members', '-a', '-', list) do |stdin, stdout, _stderr, wait_thr| + stdin.write(addresses.join("\n")) + stdin.close_write + + process = wait_thr.value + if process.exitstatus.zero? + puts 'OK' + else + puts 'FAILED' + end + + puts '> Mailman output:' + puts stdout.read + end +end + +def sync_list(list_name, ldap_group_entry, ldap_conn) + user_basetree = @configuration['ldap']['users']['basetree'] + list = "#{list_name}@#{@configuration['domain']}" + + ldap_members = ldap_group_entry.memberuid.map do |uid| + filter = Net::LDAP::Filter.eq('uid', uid) + matched_ldap_users = ldap_conn.search(base: user_basetree, filter: filter) + if matched_ldap_users.nil? || matched_ldap_users.empty? + '' + else + user = matched_ldap_users.first + address = user.mail.first # 'mail' is an array for some reason. + + address + end + end + + ldap_members.select! {|e| not e.empty? } + puts "Found #{ldap_group_entry.dn} with #{ldap_members.size} members" + + # Extract mail addresses from Mailman + print 'Searchin mailman... ' + mailman3_subscribers = get_mailman3_subscribers(list) + puts 'OK' + puts "Found #{list} with #{mailman3_subscribers.size} subscribers" + + # Add found addresses to the list. + add_subscribers_to(list, ldap_members) +end + +def main + # Parse configuration, bind to LDAP server + configuration_file = ARGV[0] ? ARGV[0] : @configuration_file + @configuration = read_configuration(configuration_file) + ldap_conn = connect_ldap + + domain = @configuration['domain'] + list_basetree = @configuration['ldap']['lists']['basetree'] + lists = @configuration['lists'] + + # Sync mailman3 lists with LDAP groups + lists.each do |list_name| + puts ">> Processing list #{list_name}@#{domain}" + print 'Searching LDAP... ' + + filter = Net::LDAP::Filter.eq('cn', list_name) + matched_ldap_groups = ldap_conn.search(base: list_basetree, filter: filter) + if matched_ldap_groups.nil? || matched_ldap_groups.empty? + # Could not find matching LDAP group + puts 'NOT FOUND' + else + puts 'OK' + ldap_group_entry = matched_ldap_groups.first + sync_list(list_name, ldap_group_entry, ldap_conn) + end + end +end + +### +# Execute script: +main diff --git a/mailman3-ldap-sync.service b/mailman3-ldap-sync.service new file mode 100644 index 0000000..b7e6244 --- /dev/null +++ b/mailman3-ldap-sync.service @@ -0,0 +1,7 @@ +[Unit] +Description=Synchronize mailman3 mailing lists with LDAP groups +After=network.target + +[Service] +Type=simple +ExecStart=/usr/bin/mailman3-ldap-sync diff --git a/mlmmj-ldap-sync@.timer b/mailman3-ldap-sync.timer similarity index 54% rename from mlmmj-ldap-sync@.timer rename to mailman3-ldap-sync.timer index 3b99c39..806545d 100644 --- a/mlmmj-ldap-sync@.timer +++ b/mailman3-ldap-sync.timer @@ -1,5 +1,5 @@ [Unit] -Description=Synchronize mlmmj mailing lists with LDAP groups once a day +Description=Synchronize mailman3 mailing lists with LDAP groups once a day [Timer] OnCalendar=*-*-* 3:00:00 diff --git a/mlmmj-ldap-sync b/mlmmj-ldap-sync deleted file mode 100755 index 6f375b7..0000000 --- a/mlmmj-ldap-sync +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env ruby - -require 'toml' -require 'net/ldap' -require 'English' - -@configuration_file = '/etc/mlmmj-ldap-sync.toml' -@configuration = nil - -def read_configuration(path) - begin - TOML.load_file(path) - rescue Errno::ENOENT => raised_error - puts "Could not open configuration file: #{raised_error}" - exit(1) - end -end - -def connect_ldap - conn = Net::LDAP.new( - host: @configuration['ldap']['host'], - port: @configuration['ldap']['port'], - auth: { - method: :simple, - username: @configuration['ldap']['auth']['username'], - password: @configuration['ldap']['auth']['password'] - } - ) - - ldap_addr = @configuration['ldap']['host'] + ':' + - @configuration['ldap']['port'].to_s - begin - if conn.bind - conn - else - puts "Failed to authenticate against LDAP server: #{ldap_addr}" - exit(1) - end - rescue LdapError => raised_error - puts "Failed to contact LDAP server: #{ldap_addr}" - puts raised_error - exit(1) - end -end - -def get_mlmmj_subscribers(list) - mlmmj_list_binary = @configuration['mlmmj']['list_binary'] - - unless File.executable?(mlmmj_list_binary) - puts "could not execute #{mlmmj_list_binary}" - exit(1) - end - - # Execute mlmmj-list and check whether it exited successfuly - raw = `#{mlmmj_list_binary} -L #{list} -s` - unless $CHILD_STATUS.exitstatus.zero? - puts 'FAIL' - exit(1) - end - - # Return a list of mail addresses. - raw.split("\n") -end - -def add_subscriber_to(list_name, subscribers, list, addr) - if subscribers.include?(addr) - # The address is already subscribed to the list: we're done with it. - subscribers.delete(addr) - else - # The address is not subscribed to the list yet. - print "Adding #{addr}... " - - # Since the configuration file doesn't say we have to add new subscribers - # to this list, we ignore this action. - unless @configuration['lists_add'].include?(list_name) - puts 'IGNORED' - return - end - - # Subscribe the address to the list. - mlmmj_sub_binary = @configuration['mlmmj']['sub_binary'] - `#{mlmmj_sub_binary} -L #{list} -a #{addr} -q -f -s` - if $CHILD_STATUS.exitstatus.zero? - puts 'OK' - else - puts 'FAILED' - end - end -end - -def remove_subscriber_from(list_name, list, addr) - print "Removing #{addr}... " - - # We ignore this action if the configuration file doesn't say we are allowed - # to remove addresses from this list. - unless @configuration['lists_remove'].include?(list_name) - puts 'IGNORED' - return - end - - # Unsubscribe the address from the list. - mlmmj_unsub_binary = @configuration['mlmmj']['unsub_binary'] - `#{mlmmj_unsub_binary} -L #{list} -a #{addr} -q -s` - if $CHILD_STATUS.exitstatus.zero? - puts 'OK' - else - puts 'FAILED' - end -end - -def sync_list(list_name, ldap_group_entry, ldap_conn) - mlmmj_basepath = @configuration['mlmmj']['basepath'] - user_basetree = @configuration['ldap']['users']['basetree'] - list = "#{mlmmj_basepath}/#{list_name}@#{@configuration['domain']}" - - ldap_members = ldap_group_entry.memberuid.map do |uid| - filter = Net::LDAP::Filter.eq('uid', uid) - matched_ldap_users = ldap_conn.search(base: user_basetree, filter: filter) - if matched_ldap_users.nil? || matched_ldap_users.empty? - "" - else - user = matched_ldap_users.first - address = mail.first # 'mail' is an array for some reason. - - address - end - end - - ldap_members.select! {|e| not e.empty? } - puts "Found #{ldap_group_entry.dn} with #{ldap_members.size} members" - - # Extract mail addresses from Mlmmj - print 'Searchin mlmmj... ' - mlmmj_subscribers = get_mlmmj_subscribers(list) - puts 'OK' - puts "Found #{list_name}@#{@configuration['domain']} with\ - #{mlmmj_subscribers.size} subscribers" - - # Add to the subscribers any missing entry - # Remove successful matches - ldap_members.each do |addr| - add_subscriber_to(list_name, mlmmj_subscribers, list, addr) - end - - # Remove remaining "subcribers" (= they did not match with LDAP members) - mlmmj_subscribers.each do |addr| - remove_subscriber_from(list_name, list, addr) - end - print "\n" -end - -def main - # Parse configuration, bind to LDAP server - configuration_file = ARGV[0] ? ARGV[0] : @configuration_file - @configuration = read_configuration(configuration_file) - ldap_conn = connect_ldap - - domain = @configuration['domain'] - list_basetree = @configuration['ldap']['lists']['basetree'] - lists = (@configuration['lists_add'] + @configuration['lists_remove']).uniq - - # Sync Mlmmj lists with LDAP groups - lists.each do |list_name| - puts ">> Processing list #{list_name}@#{domain}" - print 'Searching LDAP... ' - - filter = Net::LDAP::Filter.eq('cn', list_name) - matched_ldap_groups = ldap_conn.search(base: list_basetree, filter: filter) - if matched_ldap_groups.nil? || matched_ldap_groups.empty? - # Could not find matching LDAP group - puts 'NOT FOUND' - else - puts 'OK' - ldap_group_entry = matched_ldap_groups.first - sync_list(list_name, ldap_group_entry, ldap_conn) - end - end -end - -### -# Execute script: -main diff --git a/mlmmj-ldap-sync.service b/mlmmj-ldap-sync.service deleted file mode 100644 index ac0ecaf..0000000 --- a/mlmmj-ldap-sync.service +++ /dev/null @@ -1,7 +0,0 @@ -[Unit] -Description=Synchronize mlmmj mailing lists with LDAP groups -After=network.target - -[Service] -Type=simple -ExecStart=/usr/bin/mlmmj-ldap-sync diff --git a/mlmmj-ldap-sync.timer b/mlmmj-ldap-sync.timer deleted file mode 100644 index 3b99c39..0000000 --- a/mlmmj-ldap-sync.timer +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -Description=Synchronize mlmmj mailing lists with LDAP groups once a day - -[Timer] -OnCalendar=*-*-* 3:00:00 -Persistent=true - -[Install] -WantedBy=timers.target diff --git a/mlmmj-ldap-sync@.service b/mlmmj-ldap-sync@.service deleted file mode 100644 index f1bf483..0000000 --- a/mlmmj-ldap-sync@.service +++ /dev/null @@ -1,7 +0,0 @@ -[Unit] -Description=Synchronize mlmmj mailing lists with LDAP groups -After=network.target - -[Service] -Type=simple -ExecStart=/usr/bin/mlmmj-ldap-sync /etc/mlmmj-ldap-sync-%i.toml