File Coverage

blib/lib/Apache2/AuthZSympa.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Apache2::AuthZSympa;
2              
3 1     1   4539 use warnings;
  1         2  
  1         31  
4 1     1   5 use strict;
  1         1  
  1         31  
5 1     1   409 use mod_perl2;
  0            
  0            
6              
7             BEGIN {
8             require Apache2::Const;
9             require Apache2::Access;
10             require Apache2::SubRequest;
11             require Apache2::RequestRec;
12             require Apache2::RequestUtil;
13             require Apache2::Response;
14             require APR::Table;
15             Apache2::Const->import(-compile => 'HTTP_UNAUTHORIZED','OK', 'HTTP_INTERNAL_SERVER_ERROR');
16             require SOAP::Lite;
17             require Apache2::Log;
18             require Apache2::Directive;
19             require Cache::Memcached;
20             use Digest::MD5 qw(md5_hex);
21             }
22              
23             =head1 NAME
24              
25             Apache2::AuthZSympa - Authorization module based on Sympa mailing list server group definition
26              
27             =head1 HOMEPAGE
28              
29             L
30              
31             =head1 VERSION
32              
33             Version 0.5.2
34              
35             =cut
36              
37             our $VERSION = '0.5.2';
38             =head1 SYNOPSIS
39              
40             This module is an authorization handler for Apache 2. Its authorization method relies on mailing lists membership ; it is designed for Sympa mailing list software (http://sympa.org). This authorization handler has been initially designed to work with its peer authentication handler Apache2::AuthNSympa that performs authentication against a Sympa SOAP server. The handler has later been extended to work with third party authentication Apache modules :
41              
42             =over
43              
44             =item *
45             Apache2::AuthNSympa (default)
46              
47             =item *
48             SSL authentication (mod_ssl)
49              
50             =item *
51             CAS authentication (mod_cas)
52              
53             =item *
54             Shibboleth authentication (mod_shib)
55              
56             =back
57              
58             This module needs the associated authentication handler to provide a trusted user email address ; the user email address is later used to query list membership. Because some authentication modules (CAS) don't provide the user email address, the authorization module may be configured to query an LDAP directory. The environment variable name may also be configured (when used with Shibboleth).
59              
60              
61              
62             =head1 GENERAL CONFIGURATION TIPS
63              
64             Regardless what authentication module is used, the following rules are needed in your Apache configuration file :
65              
66             =over
67              
68             =item *
69             URL of your Sympa SOAP server
70              
71             =item *
72             list of mailing lists for which the user has to be a member
73              
74             =item *
75             handler calling rule
76              
77             =item *
78             optionaly, because SOAP can be slow, you can configure a cache server based on memcached (http://www.danga.com/memcached/).
79              
80             =back
81              
82             Of course, your mod_perl2 Apache module has to be correctly configured.
83              
84             For example, in a location section of your Apache configuration file, you have to put the following rules :
85              
86             PerlSetVar SympaSoapServer http://mysympa.server/soap # URL of the sympa SOAP server
87             PerlAuthzHandler Apache2::AuthZSympa
88             require SympaLists sympa-users@demo.sympa.org,sympa-test@demo.sympa.org # lists for which the member has to be a member (he needs to be at least a member for one of them)
89             PerlSetVar MemcachedServer 10.219.213.24:11211 # URL for cache server (option)
90             PerlSetVar CacheExptime 3600 # Cache expiration time in seconds for the cache server (default 1800)
91              
92             We provide a working example of a web page that has a restricted access for members of test@cru.fr mailing list only. You should subscribe to the test mailing list if you wish to try it : http://listes.cru.fr/sympa/info/test
93              
94             The following page will request your email address and Sympa password : http://www.cru.fr/demo_authsympa/
95              
96              
97              
98             =head1 SYMPA AUTHENTICATION MODULE
99              
100             It is based on a basic HTTP authentication authentication (popup on client side). Once the user has authenticated, the REMOTE_USER environnement var contains the user email address. The authentication module implements a SOAP client that validates user credentials against the Sympa SOAP server.
101             Example:
102              
103            
104             AuthName SympaAuth
105             AuthType Basic
106             PerlSetVar SympaSoapServer http://mysympa.server/soap
107             PerlAuthenHandler Apache2::AuthNSympa
108             PerlAuthzHandler Apache2::AuthZSympa
109             require valid-user
110             require SympaLists sympa-users@demo.sympa.org,sympa-test@demo.sympa.org
111            
112              
113              
114              
115             =head1 SSL AUTHENTICATION
116              
117             Mod_ssl can be used to do the user authentication, based on user client certificates. Your mod_ssl configuration should look like this :
118              
119             =over
120              
121             =item *
122             SSLCACertificateFile # or SSLCACertificatePath
123              
124             =item *
125             SSLRequireSSL # to prevent from disabling SSL
126              
127             =item *
128             SSLVerifyClient require
129              
130             =item *
131             AuthType SSL
132              
133              
134             =back
135              
136             Because Apache does not consider mod_ssl as an authentication handler, an authentication handler must be added. So we recommend to call Apache2::AuthNSympa because it is bypassed if "AuthType" is different from "Sympa"
137             The authentication handler will get the expected user email address extracted from the certificate.
138              
139             Example :
140              
141            
142             SSLVerifyClient require
143             SSLRequireSSL
144             SSLOptions +StdEnvVars
145             AuthType SSL
146             PerlSetVar SympaSoapServer http://mysympa.server/soap
147             PerlAuthenHandler Apache2::AuthNSympa
148             PerlAuthzHandler Apache2::AuthZSympa
149             require SympaLists sympa-users@demo.sympa.org,sympa-test@demo.sympa.org
150            
151              
152              
153              
154             =head1 CAS AUTHENTICATION
155              
156             CAS is a web single sign-on software, developped by the university of Yale : http://www.ja-sig.org/products/cas/
157              
158             CAS does not provide any email address . Therefore the authorization module will first query an LDAP directory to get the user email address, given his UID.
159              
160              
161             Example:
162              
163            
164             AuthName SympaAuth
165             AuthType CAS
166             PerlSetVar SympaSoapServer http://mysympa.server/soap
167             PerlSetVar MemcachedServer 10.219.213.24:11211
168             PerlSetVar CacheExptime 3600 # in seconds, default 1800
169              
170             ## here is ldap filters to retrieve user email address
171             ## if CAS uid is an email address, no need these directives
172             PerlSetVar LDAPHost ldap.localdomain
173             PerlSetVar LDAPSuffix ou=people
174             PerlSetVar LDAPEmailFilter (uid=[uid])
175             PerlSetVar LDAPEmailAttribute mail
176             PerlSetVar LDAPScope sub
177              
178             PerlAuthzHandler Apache2::AuthZSympa
179             require valid-user
180             require SympaLists sympa-users@demo.sympa.org,sympa-test@demo.sympa.org
181            
182              
183              
184              
185             =head1 SHIBBOLETH AUTHENTICATION
186              
187             Shibboleth is an open source software developped by Internet2 : http://shibboleth.internet2.edu
188              
189             The default behavior of mod_shib authentication module is to provide the user email address in the HTTP_SHIB_INETORGPERSON_MAIL HTTP header. The AuthZSympa module still provides a ShibbolethMailVar parameter to declare which HTTP header contains the user email address, if not the default one.
190              
191             The following rules are required:
192              
193             =over
194              
195             =item *
196             AuthType shibboleth
197              
198             =item *
199             require valid-user
200              
201             =item *
202             ShibbolethMailVar (if not HTTP_SHIB_INETORGPERSON_MAIL)
203              
204             =back
205              
206             Example:
207            
208            
209              
210             AuthType shibboleth
211             PerlSetVar SympaSoapServer http://mysympa.server/soap
212             PerlSetVar MemcachedServer 10.219.213.24:11211
213             PerlSetVar CacheExptime 3600 # in seconds, default 1800
214              
215             PerlSetVar ShibbolethMailVar HTTP_SHIB_INETORGPERSON_MAIL
216             PerlAuthzHandler Apache2::AuthZSympa
217             require valid-user
218             require SympaLists sympa-users@demo.sympa.org,sympa-test@demo.sympa.org
219            
220              
221              
222              
223             =head1 COMPLETE MODULE RULES LIST
224              
225             # required to identify the good authentication type
226             AuthType CAS # can be SSL, Sympa or shibboleth
227            
228             # URL to query Sympa server SOAP interface, required
229             PerlSetEnv SympaSoapServer
230            
231             # lists to verify membership of user, required
232             require SympaLists list1@mydomain,list2@mydomain
233            
234             # IP address and port of memcached server if necessary
235             PerlSetEnv MemcachedServer 192.168.0.1:11211
236              
237             # Cache expiration time in seconds if memcached server used, default 1800
238             PerlSetEnv CacheExptime 3600
239            
240             # LDAP Host for CAS backend
241             PerlSetEnv LDAPHost ldap.mydomain
242            
243             # LDAP suffix to query LDAP backend
244             PerlSetenv LDAPSuffix o=people
245            
246             # Filter to query LDAP backend. It has to match uid provided by CAS server
247             PerlSetenv LDAPEmailFilter myIdAttribute=([uid])
248            
249             # LDAP backend attribute containing email address
250             PerlSetenv LDAPEmailAttribute mail
251            
252             # LDAP scope, default sub
253             PerlSetenv LDAPScope sub
254            
255             # Shibboleth env var to match email address. optional, default HTTP_SHIB_INETORGPERSON_MAIL
256             PerlSetenv ShibbolethMailVar HTTP_SHIB_INETORGPERSON_MAIL
257            
258              
259             =cut
260              
261             sub handler{
262             my $r= shift;
263             return Apache2::Const::OK unless $r->is_initial_req;
264             ## Location Variables to connect to the good server
265             my $SympaSoapServer = $r->dir_config('SympaSoapServer') || "localhost"; ## url of sympa soap server
266             my $cacheserver = $r->dir_config('MemcachedServer') || "127.0.0.1:11211"; ## cache server
267             my $exptime = $r->dir_config('CacheExptime') || 1800; ## 30 minutes of cache
268             my $ShibMailVar = $r->dir_config('ShibbolethMailVar') || 'HTTP_SHIB_INETORGPERSON_MAIL';
269             my $SympaList = ""; ## list for which the mail will be checked
270             my $mail_user;
271             my $response;
272             my $result;
273             my $auth_type = lc($r->auth_type);
274            
275             my $requires = $r->requires;
276             my $location = $r->location;
277              
278            
279              
280             # verify if require SympaLists is present
281             for my $entry (@$requires){
282             my $requirement;
283             if ($entry->{requirement} =~ /SympaLists/){
284             ($requirement,$SympaList) = split(/\s+/,$entry->{requirement});
285             $r->log->debug("Apache2::AuthZSympa : require type '$requirement' for $location with lists $SympaList");
286             last;
287             }
288             }
289            
290             my @SympaLists = split(/\,/,$SympaList);
291              
292            
293             ## instanciation of a new Soap::Lite object
294             my $soap;
295             my $soap_error=0;
296             my $soap_session;
297             my $soap_res;
298             unless($soap = new SOAP::Lite()){
299             $r->log_error("Apache2::AuthZSympa : Unable to create SOAP::Lite object while accessing $location");
300             return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
301             }
302             ## if there is an error during soap request. $soap_error will be instanciated
303             $soap->on_fault(sub {
304             ($soap_session, $soap_res) = @_;
305             $soap_error=1;
306             });
307             $soap->uri('urn:sympasoap');
308             $soap->proxy($SympaSoapServer);
309              
310            
311             unless(defined $soap){
312             return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
313             }
314              
315             ## instanciation of cache
316             ## preventing from errors, verification of its naming
317             unless( $cacheserver =~ /[^\:]+\:\d{1,6}/){
318             $r->log_error("Apache2::AuthZSympa configuration ($location) : memcached server ($cacheserver) naming format is incorrect, a port number is required");
319             return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
320             }
321             my $cache = new Cache::Memcached {
322             'servers' => [ $cacheserver ],
323             'namespace' => 'AuthZSympa',
324             };
325              
326              
327              
328             ## if an email from SSL request is got, then authentication was made via SSL
329             $r->subprocess_env;
330             my $subr = $r->lookup_uri($r->uri);
331             my $ssl_proto = $subr->subprocess_env('SSL_CLIENT_S_DN_Email');
332             if ($subr->subprocess_env('SSL_CLIENT_S_DN_Email') && ($auth_type eq "ssl")){
333             $mail_user=$subr->subprocess_env('SSL_CLIENT_S_DN_Email');
334             $r->user($mail_user);
335             }elsif($auth_type eq 'basic' && $r->user){
336             ## if basic_auth, get remote_user
337             $mail_user= $r->user;
338             }elsif($auth_type eq 'cas'){
339             ## if CAS
340             my $user = $r->user;
341             $mail_user = "";
342              
343             ## verification of ldap directives
344             my $ldap_host = $r->dir_config('LDAPHost') || "";
345             if ($ldap_host eq ""){
346             $r->log->debug("Apache2::AuthZSympa : no LDAPHost, email adress in uid ?");
347             if ($user =~ /@/){
348             ## if user is emailAddress, don't need ldap to retrieve emailadddress
349             $r->log->debug("Apache2::AuthZSympa : no need with LDAP, email adress in uid");
350             $mail_user = $user;
351             }else{
352             $r->log_error("Apache2::AuthZSympa : no ldap_host defined for $location, can't verify registrations");
353             return Apache2::Const::HTTP_UNAUTHORIZED;
354             }
355             }
356             ## key for cache (key for email)
357             my $user_key = md5_hex($r->user.$ldap_host);
358            
359            
360             ## verification first in cache
361             if (defined $cache->get($user_key)){
362             $r->log->debug("Apache2::AuthZSympa : retrieve mail from cache for $user_key");
363             $mail_user = ${$cache->get($user_key)};
364             $r->log->debug("Apache2::AuthZSympa : retrieved mail ($mail_user) from cache") if $mail_user ne "";
365             }
366             ## then retrieve mail from ldap
367             if ($mail_user eq ""){
368             $r->log->debug("Apache2::AuthZSympa : retrieve mail from LDAP");
369             $mail_user = &casGetMail($r);
370             }
371             if ($mail_user ne ""){
372             $r->log->debug("Apache2::AuthZSympa : retrieved mail $mail_user");
373             $cache->set($user_key,\$mail_user,$exptime);
374             }else{
375             $r->log_error("Apache2::AuthZSympa : no mail for $user in $ldap_host");
376             return Apache2::Const::HTTP_UNAUTHORIZED;
377             }
378            
379             }elsif($auth_type eq 'shibboleth'){
380              
381             $mail_user=$ENV{$ShibMailVar};
382             if($mail_user eq ""){
383             $r->log_error("Apache2::AuthZSympa : no mail in var $ShibMailVar");
384             $r->log->debug("Apache2::AuthZSympa : $ShibMailVar value : $mail_user");
385             return Apache2::Const::HTTP_UNAUTHORIZED;
386             }else{
387             $r->log->debug("Apache2::AuthZSympa : $ShibMailVar value : $mail_user");
388             }
389            
390             }else{
391             $r->log_error("Apache2::AuthZSympa : no user authenticated for $location, can't verify registrations");
392             return Apache2::Const::HTTP_UNAUTHORIZED;
393             }
394            
395             ## key generation for cache : md5($mail_user + server name) -> prevents from errors when updating
396             my $user_key = md5_hex($mail_user.$SympaSoapServer);
397              
398             ## verify subscription first in cache
399             ## if its in the cache as OK for the list, go,
400             ## if its in all the list as not OK, refuse
401             ## else, next step
402             my %cache_lists;
403             if (defined $cache){
404             if (defined $cache->get($user_key)){
405             %cache_lists = %{$cache->get($user_key)};
406             }
407             my $ok=1;
408             foreach my $list (@SympaLists){
409             if (defined $cache_lists{$list}){
410             if ($cache_lists{$list} == 1){
411             return Apache2::Const::OK;
412             }elsif($cache_lists{$list} == 0){
413             $ok = 0;
414             }
415             }
416             }
417             if ($ok == 0){
418             my $lists_string = join(", nor in ",@SympaLists);
419             $r->log->notice("Apache2::AuthZSympa : $location. $mail_user is not registred on server $SympaSoapServer in ",$lists_string);
420             return Apache2::Const::HTTP_UNAUTHORIZED;
421             }
422             }
423             ## if not in cache, verify soap server
424             foreach my $list (@SympaLists){
425             $r->log->debug("Apache2::AuthZSympa liste $list");
426             $soap_error=0;
427             $list =~ s/\s//g;
428             $response = $soap->amI($list,'subscriber',$mail_user);
429             ## verify if error during soap service request
430             if ($soap_error==1){
431             my ($type_error,$detail) = &traite_soap_error($soap, $soap_res);
432             if ($type_error eq 'ERROR'){
433             $r->log_error("Apache2::AuthZSympa : $location, SOAP error $detail (server $SympaSoapServer)");
434             }else{
435             $r->log->notice("Apache2::AuthZSympa : $location, $detail (server $SympaSoapServer)");
436             };
437             $cache_lists{$list} = 0;
438             next;
439             }else{
440             $result = $response->result;
441             if ($result == 1){
442             if (defined $cache){
443             $cache_lists{$list} = 1;
444             $cache->set($user_key, \%cache_lists,$exptime);
445             }
446             return Apache2::Const::OK;
447             }else{
448             $cache_lists{$list} = 0;
449             }
450             }
451             }
452             $cache->set($user_key, \%cache_lists,$exptime);
453             my $lists_string = join(", nor in ",@SympaLists);
454             $r->log->notice("Apache2::AuthZSympa : $location. $mail_user is not registred on server $SympaSoapServer in ",$lists_string);
455             return Apache2::Const::HTTP_UNAUTHORIZED;
456              
457              
458             }
459              
460             sub traite_soap_error {
461             my ($soap, $res) = @_;
462             my $detail = "";
463             my $type = "";
464              
465             if(ref(\$res) eq 'REF'){
466             $detail = $res->faultdetail;
467             $type = "NOTICE";
468             }else{
469             $detail = $soap->transport->status;
470             $type = "ERROR";
471             };
472             return ($type, $detail);
473             }
474              
475             sub casGetMail(){
476             my ($r) = @_;
477             my $error="";
478             use Net::LDAP;
479             my $user = $r->user;
480             my $ldap_host = $r->dir_config('LDAPHost');
481             my $ldap_suffix = $r->dir_config('LDAPSuffix');
482             my $uid_filter = $r->dir_config('LDAPEmailFilter');
483             my $attribute = $r->dir_config('LDAPEmailAttribute');
484             my $scope = $r->dir_config('LDAPScope') || "sub";
485             my $location = $r->location;
486             my $ldap;
487             unless($ldap = Net::LDAP->new($ldap_host)){
488             $r->log_error("Apache2::AuthZSympa : $location, unable to create Net::LDAP object with $ldap_host");
489             return "";
490             }
491             my $mesg;
492             unless($mesg = $ldap->bind){
493             $r->log_error("Apache2::AuthZSympa : $location, unable to bind $ldap_host");
494             return "";
495             }
496             my $filter = $uid_filter;
497             $filter =~ s/\[uid\]/$user/;
498             $mesg = $ldap->search( # perform a search
499             base => $ldap_suffix,
500             scope => $scope,
501             attrs => [$attribute],
502             filter => $filter,
503             );
504             my $nb_entries = $mesg->count;
505             if(($nb_entries == 0) | ($nb_entries>1)){
506             $r->log->notice("Apache2::AuthZSympa : $location, $nb_entries entries returned while querying $ldap_host, maybe wrong parameter ?");
507             $ldap->unbind;
508             return "";
509             }
510             my $entry = $mesg->entry(0);
511             my $mail_user = $entry->get_value($attribute);
512             $ldap->unbind;
513             return $mail_user;
514            
515             }
516             =head1 AUTHOR
517              
518             Dominique Launay,Comite Reseau des Universites, C<< >>
519              
520              
521             =head1 COPYRIGHT & LICENSE
522              
523             Copyright 2005 Comite Reseau des Universites L All Rights Reserved.
524              
525             This program is free software; you can redistribute it and/or modify it
526             under the same terms as Perl itself.
527              
528             =cut
529              
530             1; # End of Apache2::AuthZSympa