On Wednesday 11 June 2003 18:10, Kristian Koehntopp wrote:
> Has anybody written a ConfSourceLDAP.pm analogue to ConfSourceSQL.pm? I am
> looking for a solution that stores SpamAssassin Preferences within an LDAP
> store.

This is an untested draft, which I am going to debug and test now. Why post 
untested code? Because the test setup is at a distance, and I will be on a 
slow debug cycle. I would like to hear opinions, though, and will submit 
finished code, if there is interest.

Kristian

-- 
Kristian Köhntopp, NetUSE AG, Dr.-Hell-Straße, D-24107 Kiel
Tel: +49 431 386 435 00, Fax: +49 431 386 435 99
diff -uNbr Mail-SpamAssassin-2.55/MANIFEST Mail-SpamAssassin-2.55-ldap/MANIFEST
--- Mail-SpamAssassin-2.55/MANIFEST	2003-03-30 03:55:30.000000000 +0200
+++ Mail-SpamAssassin-2.55-ldap/MANIFEST	2003-06-17 15:51:44.000000000 +0200
@@ -37,6 +37,7 @@
 lib/Mail/SpamAssassin/CmdLearn.pm
 lib/Mail/SpamAssassin/Conf.pm
 lib/Mail/SpamAssassin/ConfSourceSQL.pm
+lib/Mail/SpamAssassin/ConfSourceLDAP.pm
 lib/Mail/SpamAssassin/DBBasedAddrList.pm
 lib/Mail/SpamAssassin/Dns.pm
 lib/Mail/SpamAssassin/EncappedMIME.pm
diff -uNbr Mail-SpamAssassin-2.55/ldap/README Mail-SpamAssassin-2.55-ldap/ldap/README
--- Mail-SpamAssassin-2.55/ldap/README	1970-01-01 01:00:00.000000000 +0100
+++ Mail-SpamAssassin-2.55-ldap/ldap/README	2003-06-17 16:01:14.000000000 +0200
@@ -0,0 +1,82 @@
+
+Using SpamAssassin With An LDAP Server
+--------------------------------------
+
+SpamAssassin can now load users' score files from an LDAP server.  The concept
+here is to have a web application (PHP/perl/ASP/etc.) that will allow users to
+be able to update their local preferences on how SpamAssassin will filter their
+e-mail.  The most common use for a system like this would be for users to be
+able to update the white list of addresses (whitelist_from) without the need
+for them to update their $HOME/.spamassassin/user_prefs file.  It is also quite
+common for users listed in /etc/passwd to not have a home directory,
+therefore, the only way to have their own local settings would be through a
+database or LDAP server.
+
+SpamAssassin will check the global configuration file (ie. any file matching
+/etc/mail/spamassassin/*.cf) for the following settings:
+
+user_scores_dsn			DBI:driver:database:hostname[:port]
+user_scores_ldap_username	bind dn
+user_scores_ldap_password	password
+
+The first option, user_scores_dsn, describes the data source name that will be
+used to create the connection to your LDAP server. You have to write the DSN
+as an LDAP URL, the components being the host and port to connect to, the
+base DN for the seasrch, the scope of the search (base, one or sub), the
+single attribute being the multivalued attribute used to hold the
+configuration data (space separated pairs of key and value, just as
+in a file) and finally the filter being the expression used to filter
+out the wanted username. Note that the filter expression is being used
+in a sprintf statement with the username as the only parameter, thus it
+can hold a single %s expression. This is where the username goes.
+
+Example: ldap://localhost:389/dc=koehntopp,dc=de?spamassassinconfig?uid=%s
+
+
+If the user_scores_dsn option does not exist, SpamAssassin will not attempt
+to use an LDAP server for retrieving users' preferences. Note that this will
+NOT look for test rules, only local scores, whitelist_from(s), required_hits,
+and auto_report_threshold.
+
+Requirements
+------------
+
+In order for SpamAssassin to work with your SQL database, you must have
+the perl Net::LDAP module installed. You'll also need the URI module.
+
+
+Database Schema
+---------------
+
+You can use any schema extension to your user entries with SpamAssassin,
+as long as the attribute is multivalued and correctly named in your LDAP url.
+We are currently using a <customername>spamassassin field that is part of
+our inetOrgPerson subclass.
+
+Testing SpamAssassin/LDAP
+-------------------------
+
+To test your LDAP setup, and debug any possible problems, you should start
+spamd with the -D option, which will keep spamd in the foreground, and will
+output debug message to the terminal. You should then test spamd with a
+message by calling spamc.  You can use the sample-spam.txt file with the
+following command:
+
+cat sample-spam.txt | spamc
+
+Watch the debug output from spamd and look for the following debug line:
+
+retrieving LDAP prefs for <username>: <value>
+
+If you do not see the above text, then the LDAP query was not successful, and
+you should see any error messages reported. <username> should be the user
+that was passed to spamd and is usually the user executing spamc.
+
+******
+NB:  This should be considered BETA, and the interface or overall
+operation of LDAP support may change at any time with future releases of SA.
+******
+
+Please send any comments to kk /at/ netuse.de .
+
+Kristian Köhntopp
diff -uNbr Mail-SpamAssassin-2.55/lib/Mail/SpamAssassin/Conf.pm Mail-SpamAssassin-2.55-ldap/lib/Mail/SpamAssassin/Conf.pm
--- Mail-SpamAssassin-2.55/lib/Mail/SpamAssassin/Conf.pm	2003-05-20 08:06:17.000000000 +0200
+++ Mail-SpamAssassin-2.55-ldap/lib/Mail/SpamAssassin/Conf.pm	2003-06-17 15:48:00.000000000 +0200
@@ -195,6 +195,10 @@
   $self->{user_scores_sql_username} = '';
   $self->{user_scores_sql_password} = '';
   $self->{user_scores_sql_table} = 'userpref'; # Morgan - default to userpref for backwords compatibility
+
+  $self->{user_scores_ldap_username} = 'username';
+  $self->{user_scores_ldap_password} = '';
+
 # Michael 'Moose' Dinn, <[EMAIL PROTECTED]>
 # For integration with Horde's preference storage
 # 20020831
@@ -1864,12 +1868,47 @@
 If you load user scores from an SQL database, this will set the DSN
 used to connect.  Example: C<DBI:mysql:spamassassin:localhost>
 
+If you load user scores from an LDAP directory, this will set the DSN
+used to connect. You have to write the DSN as an LDAP URL, the components
+being the host and port to connect to, the base DN for the seasrch, the
+scope of the search (base, one or sub), the single attribute being the
+multivalued attribute used to hold the configuration data (space separated
+pairs of key and value, just as in a file) and finally the filter being
+the expression used to filter out the wanted username. Note that the filter
+expression is being used in a sprintf statement with the username as the
+only parameter, thus is can hold a single %s expression. This is where the
+username goes.
+
+Example: C<ldap://localhost:389/dc=koehntopp,dc=de?spamassassinconfig?uid=%s>
+
 =cut
 
     if (/^user_scores_dsn\s+(\S+)$/) {
       $self->{user_scores_dsn} = $1; next;
     }
 
+=item user_scores_ldap_username
+
+This is the Bind DN used to connect to the LDAP server.
+
+Example: C<cn=master,dc=koehntopp,dc=de>
+
+=cut
+
+    if (/^user_scores_ldap_username\s+(\S+)$/) {
+      $self->{user_scores_ldap_username} = $1; next;
+    }
+
+=item user_scores_ldap_password
+
+This is the password used to connect to the LDAP server.
+
+=cut
+
+    if (/^user_scores_ldap_password\s+(\S+)$/) {
+      $self->{user_scores_ldap_password} = $1; next;
+    }
+
 =item user_scores_sql_username username
 
 The authorized username to connect to the above DSN.
diff -uNbr Mail-SpamAssassin-2.55/lib/Mail/SpamAssassin/ConfSourceLDAP.pm Mail-SpamAssassin-2.55-ldap/lib/Mail/SpamAssassin/ConfSourceLDAP.pm
--- Mail-SpamAssassin-2.55/lib/Mail/SpamAssassin/ConfSourceLDAP.pm	1970-01-01 01:00:00.000000000 +0100
+++ Mail-SpamAssassin-2.55-ldap/lib/Mail/SpamAssassin/ConfSourceLDAP.pm	2003-06-17 15:42:44.000000000 +0200
@@ -0,0 +1,162 @@
+=head1 NAME
+
+Mail::SpamAssassin::ConfSourceLDAP - load SpamAssassin scores from LDAP database
+
+=head1 SYNOPSIS
+
+  (see Mail::SpamAssassin)
+
+
+=head1 DESCRIPTION
+
+Mail::SpamAssassin is a module to identify spam using text analysis and
+several internet-based realtime blacklists.
+
+This class is used internally by SpamAssassin to load scores from an LDAP
+database.  Please refer to the C<Mail::SpamAssassin> documentation for public
+interfaces.
+
+=head1 METHODS
+
+=over 4
+
+=cut
+
+package Mail::SpamAssassin::ConfSourceLDAP;
+
+use strict;
+use bytes;
+use Carp;
+
+use vars qw{
+  @ISA
+};
+
[EMAIL PROTECTED] = qw();
+
+###########################################################################
+
+sub new {
+  my $class = shift;
+  $class = ref($class) || $class;
+  my ($main) = @_;
+
+  my $self = {
+    'main'              => $main
+  };
+
+  bless ($self, $class);
+  $self;
+}
+
+###########################################################################
+
+sub load_modules {		# static
+  eval {
+    require Net::LDAP; # actual server connection
+    require URI;       # parse server connection dsn
+  };
+
+  # do any other preloading that will speed up operation
+}
+
+###########################################################################
+
+=item $f->load ($username)
+
+Read configuration paramaters from LDAP server and parse scores from it.
+
+=cut
+
+sub load {
+   my ($self, $username) = @_;
+
+   my $url = $self->{main}->{conf}->{user_scores_dsn}; # an ldap URI
+   if(!defined($url) || $url eq '') {
+     dbg ("No LDAP URL defined; skipping LDAP");
+     return;
+   }
+
+   eval {
+     # make sure we can see croak messages from DBI
+     local $SIG{'__DIE__'} = sub { warn "$_[0]"; };
+     require Net::LDAP;
+     load_with_ldap($self, $username, $url);
+   };
+
+   if ($@) {
+     warn "failed to load user scores from LDAP server, ignored\n";
+   }
+}
+
+sub load_with_ldap {
+  my ($self, $username, $url) = @_;
+
+#       ldapurl    = scheme "://" [hostport] ["/"
+#                    [dn ["?" [attributes] ["?" [scope]
+#                    ["?" [filter] ["?" extensions]]]]]]
+
+  $uri = URI->new("$url");
+
+  my $host   = $uri->host;
+  if (!defined($host) || $host eq '') {
+    dbg("No LDAP server host specified, assuming localhost");
+    $host = "localhost";
+  }
+  my $port   = $uri->port;
+  my $base   = $uri->dn;
+  my @attr   = $uri->attributes;
+  my $scope  = $uri->scope;
+  my $filter = $uri->filter;
+  my %extn   = $uri->extensions; # unused
+
+  my $main = $self->{main};
+  my $ldapuser = $main->{conf}->{user_scores_ldap_username};
+  my $ldappass = $main->{conf}->{user_scores_ldap_password};
+
+  my $f_attribute = $attr[0];
+
+  my $ldap = Net::LDAP->new ("$host:$port");
+  if (!$ldap) {
+    warn("LDAP Error: cannot connect $host:$port");
+    return;
+  }
+
+  if (!defined($ldapuser) || !defined($ldappass)) {
+    $ldap->bind;
+  } else {
+    $ldap->bind($ldapuser, password => $ldappass);
+  }
+  if ($ldap->code) {
+    warn("LDAP Error: cannot bind - " . $ldap->error);
+    return;
+  }
+
+  $filter = sprintf($filter, $username);
+  my $result = $ldap->search( base => $base,
+			      filter => $filter,
+			      scope => $scope,
+			      attrs => [EMAIL PROTECTED]
+                            );
+  if ($ldap->code != 0) {
+    warn("LDAP Error: $query\n" . $ldap->error);
+  };
+
+  foreach my $entry ($ldap->all_entries) {
+    @v = $entry->get_value($f_attribute);
+    foreach $v (@v) {
+      $main->{conf}->parse_scores_only("$v\n");
+      dbg("retrieving LDAP prefs for $username: $v");
+    }
+  }
+  return;
+}
+
+###########################################################################
+
+sub dbg { Mail::SpamAssassin::dbg (@_); }
+sub sa_die { Mail::SpamAssassin::sa_die (@_); }
+
+###########################################################################
+
+1;
diff -uNbr Mail-SpamAssassin-2.55/lib/Mail/SpamAssassin.pm Mail-SpamAssassin-2.55-ldap/lib/Mail/SpamAssassin.pm
--- Mail-SpamAssassin-2.55/lib/Mail/SpamAssassin.pm	2003-05-20 08:06:17.000000000 +0200
+++ Mail-SpamAssassin-2.55-ldap/lib/Mail/SpamAssassin.pm	2003-06-17 15:50:38.000000000 +0200
@@ -59,6 +59,7 @@
 
 use Mail::SpamAssassin::Conf;
 use Mail::SpamAssassin::ConfSourceSQL;
+use Mail::SpamAssassin::ConfSourceLDAP;
 use Mail::SpamAssassin::PerMsgStatus;
 use Mail::SpamAssassin::NoMailAudit;
 use Mail::SpamAssassin::Bayes;
@@ -954,6 +955,27 @@
   $src->load($username);
 }
 
+###########################################################################
+
+=item $f->load_scoreonly_sql ($username)
+
+Read configuration paramaters from an LDAP server and parse scores from it.  This
+will only take effect if the perl C<Net::LDAP> and C<URI> modules are installed,
+and the configuration parameters C<user_scores_dsn>, C<user_scores_ldap_username>,
+and C<user_scores_ldap_password> are set correctly.
+
+The username in C<$username> will also be used for the C<username> attribute of
+the Mail::SpamAssassin object.
+
+=cut
+
+sub load_scoreonly_ldap {
+  my ($self, $username) = @_;
+
+  my $src = Mail::SpamAssassin::ConfSourceLDAP->new ($self);
+  $self->{username} = $username;
+  $src->load($username);
+}
 
 ###########################################################################
 
@@ -1017,6 +1039,7 @@
   my $dsn = $self->{conf}->{user_scores_dsn};
   if ($dsn ne '') {
     Mail::SpamAssassin::ConfSourceSQL::load_modules();
+    Mail::SpamAssassin::ConfSourceLDAP::load_modules();
   }
 
   $self->{bayes_scanner}->sanity_check_is_untied();
diff -uNbr Mail-SpamAssassin-2.55/spamd/README.spamd Mail-SpamAssassin-2.55-ldap/spamd/README.spamd
--- Mail-SpamAssassin-2.55/spamd/README.spamd	2003-05-12 21:15:36.000000000 +0200
+++ Mail-SpamAssassin-2.55-ldap/spamd/README.spamd	2003-06-17 16:09:39.000000000 +0200
@@ -65,7 +65,7 @@
 When run as root, spamd will change uid's to the user invoking spamc in 
 order to read and write to their configurations. This functionality
 is not possible if spamd does not run as root and is a disadvantage if
-you rely on this. If you use mysql for per-user configuration there
+you rely on this. If you use mysql or LDAP  for per-user configuration there
 is no reason in the world to run as root, and this remains fully
 functional.
 
@@ -117,8 +117,8 @@
 If users do not have the opportunity to invoke spamc themselves, and
 the network is secure, running spamd as root is the preferred option,
 Be clear that the issues above dont affect you. Note: if you use mysql
-for per-user configuration on systems, you will remain vulnerable to
-(1.) and (2.). 
+or LDAP for per-user configuration on systems, you will remain vulnerable
+to (1.) and (2.).
 
 configuration:              Mysql           .spamassassin/user_prefs
                              / \                  / \
diff -uNbr Mail-SpamAssassin-2.55/spamd/spamd.raw Mail-SpamAssassin-2.55-ldap/spamd/spamd.raw
--- Mail-SpamAssassin-2.55/spamd/spamd.raw	2003-05-20 08:06:18.000000000 +0200
+++ Mail-SpamAssassin-2.55-ldap/spamd/spamd.raw	2003-06-17 16:08:55.000000000 +0200
@@ -91,8 +91,10 @@
   'max-children|m=i'            => \$opt{'max-children'},
   'port|p=i'                    => \$opt{'port'},
   'sql-config!'                 => \$opt{'sql-config'},
+  'ldap-config!'                => \$opt{'ldap-config'},
     'q'                         => \$opt{'sql-config'},
   'setuid-with-sql'             => \$opt{'setuid-with-sql'},
+  'setuid-with-ldap'            => \$opt{'setuid-with-ldap'},
     'Q'                         => \$opt{'setuid-with-sql'},
   'virtual-config|V=s'          => \$opt{'virtual-config'},
   'virtual-config-dir=s'        => \$opt{'virtual-config-dir'},
@@ -472,11 +474,16 @@
 		{
 		  if ($opt{'sql-config'}) {
 		    handle_user_sql($current_user);
+		  } elsif ($opt{'ldap-config'}) {
+		    handle_user_ldap($current_user);
 		  } elsif ($opt{'virtual-config'} || $opt{'virtual-config-dir'}) {
 		    handle_virtual_user($current_user);
 		  } elsif ($opt{'setuid-with-sql'}) {
 		    handle_user_setuid_with_sql($current_user);
 		    $setuid_to_user = 1; #to benefit from any paranoia.
+		  } elsif ($opt{'setuid-with-ldap'}) {
+		    handle_user_setuid_with_ldap($current_user);
+		    $setuid_to_user = 1; # as above
 		  }
 		} else {
 		  handle_user($current_user);
@@ -509,6 +516,10 @@
         handle_user_sql('nobody');
     }
 
+    if ($opt{'ldap-config'} && !defined($current_user)) {
+        handle_user-ldap('nobody');
+    }
+
     my $resp = "EX_OK";
 
     # Now read in message
@@ -776,7 +787,7 @@
 }
 
 # Handle user configs without the necessity of having individual users or a
-# SQL database.
+# SQL/LDAP database.
 sub handle_virtual_user
 {
     my $username = shift;
@@ -843,6 +854,13 @@
     return 1;
 }
 
+sub handle_user_ldap
+{
+    my $username = shift;
+    $spamtest->load_scoreonly_ldap ($username);
+    return 1;
+}
+
 sub handle_user_setuid_with_sql
 {
     my $username = shift;
@@ -888,6 +906,51 @@
     return 1;
 }
 
+sub handle_user_setuid_with_ldap
+{
+    my $username = shift;
+    my ($name,$pwd,$uid,$gid,$quota,$comment,$gcos,$dir,$etc) = getpwnam($username);
+
+    if ( !$spamtest->{'paranoid'} && !defined($uid) ) {
+       #if we are given a username, but can't look it up,
+       #Maybe NIS is down? lets break out here to allow
+       #them to get 'defaults' when we are not running paranoid.
+       logmsg "handle_user() -> unable to find user [$username]!\n";
+       return 0;
+    }
+
+    if ($setuid_to_user) {
+       $) = "$gid $gid"; # change eGID
+       $> = $uid; # change eUID
+       if ( !defined($uid) || ($> != $uid and $> != ($uid-2**32))) {
+           logmsg "fatal: setuid to $username failed";
+          die;         # make it fatal to avoid security breaches
+        }
+       else
+       {
+          logmsg "info: setuid to $username succeeded, reading scores from LDAP.";
+        }
+    }
+
+    my $spam_conf_dir = $dir . '/.spamassassin';  #needed for AWL, Bayes, etc.
+    if ( ! -d $spam_conf_dir )
+    {
+       if ( mkdir $spam_conf_dir, 0700 )
+       {
+          logmsg "info: created $spam_conf_dir for $username.";
+       }
+       else
+       {
+          logmsg "info: failed to create $spam_conf_dir for $username.";
+       }
+    }
+
+    $spamtest->load_scoreonly_ldap ($username);
+
+    $spamtest->signal_user_changed ({ username => $username });
+    return 1;
+}
+
 sub create_default_cf_if_needed {
     my ($cf_file, $username, $userdir) = @_;
 
@@ -1123,6 +1186,9 @@
  -q, --sql-config                   Enable SQL config (only useful with -x)
  -Q, --setuid-with-sql              Enable SQL config (only useful with -x,
                                     enables use of -a and -H)
+     --ldap-config                  Enable LDAP config (only useful with -x)
+     --setuid-with-ldap             Enable LDAP config (only useful with -x,
+                                    enables use of -a and -H)
  --virtual-config-dir=dir           Enable Virtual configs (needs -x)
  -V, --virtual-config=dir           Enable Virtual configs (needs -x)
  -r pidfile, --pidfile              Write the process id to pidfile
@@ -1211,6 +1277,11 @@
 If your spamc client does not support sending the C<User:> header,
 like C<exiscan>, then the SQL username used will always be B<nobody>.
 
+=item B<--ldap-config>
+
+Turn on LDAP lookups. This is completely analog to C<--sql-config>,
+only it is using an LDAP server.
+
 =item B<-Q>, B<--setuid-with-sql>
 
 Turn on SQL lookups even when per-user config files have been disabled
@@ -1218,6 +1289,12 @@
 which want to load user preferences from an SQL database but also wish to
 support the use of B<-a> (AWL) and B<-H> (Helper home directories.)
 
+=item B<--setuid-with-ldap>
+
+Turn on LDAP lookups even when per-user config files have been disabled
+with B<-x> and also setuid to the user.  This is again completely analog
+to C<--setuid-with-sql>, only it is using an LDAP server.
+
 =item B<--virtual-config-dir>=I<pattern>
 
 This option specifies where per-user preferences can be found for virtual
@@ -1258,7 +1335,7 @@
 databases for that user will be stored in this directory.
 
 Note that this B<requires> that B<-x> is used, and cannot be combined with
-SQL-based configuration.
+SQL- or LDAP-based configuration.
 
 =item B<-V>=I<directory>, B<--virtual-config>=I<directory>
 
@@ -1276,7 +1353,7 @@
 All others will be replaced by underscores (C<_>).
 
 Note that this B<requires> that B<-x> is used, and cannot be combined with
-SQL-based configuration.
+SQL- or LDAP-based configuration.
 
 If a subdirectory is found in that directory, called B<I<username>>, and it is
 writable, it will be used to store auto-whitelist and/or Bayes databases for

Reply via email to