#!/usr/bin/env ruby require 'toml' require 'net/ldap' require 'English' @configuration_file = '/etc/unipoly-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'] 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 #{s} -q -s` if $CHILD_STATUS.exitstatus.zero? puts 'OK' else puts 'FAILED' end end def sync_list(list_name, ldap_group_entry) mlmmj_basepath = @configuration['mlmmj']['basepath'] list = "#{mlmmj_basepath}/#{list_name}@#{@configuration['domain']}" # Members are formatted as # 'mail=user@domain.tld,ou=Users,dc=unipoly,dc=epfl,dc=ch': we extract the # mail address. ldap_members = ldap_group_entry.uniquemember.map do |dn| /mail=([^,]+),/.match(dn).values_at(1).first.downcase end 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 = read_configuration(@configuration_file) conn = connect_ldap domain = @configuration['domain'] 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 = conn.search(base: basetree, filter: filter) if 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) end end end ### # Execute script: main