acl.pm
branchfoerste
changeset 56 722cdb1321c7
parent 54 1f74755c407e
child 58 dd04534fe595
equal deleted inserted replaced
55:ef65e9adf0f6 56:722cdb1321c7
     4 # $Id$
     4 # $Id$
     5 # $URL$
     5 # $URL$
     6 
     6 
     7 use strict;
     7 use strict;
     8 use warnings;
     8 use warnings;
       
     9 require 5.10.0;
     9 use File::Path qw(remove_tree);
    10 use File::Path qw(remove_tree);
    10 use Net::LDAP;
    11 use Net::LDAP;
    11 use Net::LDAP::Constant
    12 use Net::LDAP::Constant
    12   qw(LDAP_ALREADY_EXISTS LDAP_NO_SUCH_OBJECT LDAP_TYPE_OR_VALUE_EXISTS);
    13   qw(LDAP_ALREADY_EXISTS LDAP_NO_SUCH_OBJECT LDAP_TYPE_OR_VALUE_EXISTS);
    13 use Net::LDAP::Entry;
    14 use Net::LDAP::Entry;
    14 use Mail::IMAPTalk;
    15 use Mail::IMAPTalk;
    15 use Text::Wrap;
    16 use Text::Wrap;
    16 use password;
    17 use password;
       
    18 use Term::ReadKey;
    17 
    19 
    18 my $Cf;
    20 my $Cf;
    19 my ( $ldap, $ubase, $abase );
    21 my ( $ldap, $ubase, $abase );
    20 my ( $imap, $acl_password );
    22 my ( $imap, $acl_password, $nspat );
    21 END { $imap and $imap = undef; }
    23 END { $imap and $imap = undef; }
    22 
    24 
    23 sub _list();
    25 sub _list();
    24 sub _mkpw($);
    26 sub _mkpw($);
    25 
    27 
    26 sub list_by_user($@);
    28 sub list_by_user($@);
    27 sub list_by_folder($$$);
    29 sub list_by_folder($);
    28 sub uniq(@);
    30 sub uniq(@);
    29 sub verbose(@);
    31 sub verbose(@);
       
    32 sub prompt($$);
       
    33 sub imap_list($$);
       
    34 sub imap_rlist($$$);
       
    35 sub acl_folders($);
    30 
    36 
    31 sub OU_ACCOUNTS();
    37 sub OU_ACCOUNTS();
    32 sub OU_ALIASES();
    38 sub OU_ALIASES();
    33 sub AT_PRIMARYADDRESS();
    39 sub AT_PRIMARYADDRESS();
    34 sub OC_RECIPIENT();
    40 sub OC_RECIPIENT();
    66     $acl_password =
    72     $acl_password =
    67          $Cf->acl_password
    73          $Cf->acl_password
    68       || $ENV{IMAP_PASS}
    74       || $ENV{IMAP_PASS}
    69       || password::ask( "IMAP (" . $Cf->acl_admin . ") password: " );
    75       || password::ask( "IMAP (" . $Cf->acl_admin . ") password: " );
    70 
    76 
    71     if    ( $Cf->list )   { _list() }
       
    72     elsif ( $Cf->modify ) { _modify() }
       
    73     else { die "Need action (--modify|--list)\n" }
       
    74 
       
    75 }
       
    76 
       
    77 sub _modify() {
       
    78 
       
    79     die 'Not yet implemented';
       
    80 
       
    81     # Auch hier gehen wir davon aus, daß die dn direkt aus dem User-Namen folgt:
       
    82     # dn: uid=USER,...
       
    83     my (@user) = @ARGV or die "Need user(s)\n";
       
    84     $Cf->user or die "Need user(s)\n";
       
    85     $Cf->acl or die "Need acl\n";
       
    86     my @dns;
       
    87 
       
    88     my $r = $ldap->search(
       
    89         base   => $ubase,
       
    90         filter => "(|" . join( "", map { "(uid=$_)" } @ARGV ) . ")"
       
    91     );
       
    92     die $r->error if $r->code;
       
    93     die "No entries found.\n" if $r->count == 0;
       
    94 
       
    95     while ( my $e = $r->shift_entry ) {
       
    96         my $r;
       
    97 
       
    98         my $user = $e->get_value("uid");
       
    99         my $dn   = $e->dn;
       
   100 
       
   101         my $modified = 0;
       
   102         verbose "$user:";
       
   103 
       
   104         verbose "\n\t$dn...";
       
   105 
       
   106         # Fix: iusMailOptions wurde erst später eingeführt, bei Bedarf also hinzufügen
       
   107         #if (!grep /^iusMailOptions$/, @{$e->get("objectClass")}) {
       
   108         #$e->add(objectClass => "iusMailOptions");
       
   109         #}
       
   110 
       
   111         if ( my $cn = $Cf->fullname ) {
       
   112 
       
   113             # Aus dem Fullnamen leiten wir cn und sn ab.
       
   114             my $sn = ( reverse split " ", $cn )[0];
       
   115 
       
   116             if ( $cn =~ s/^\+// ) {
       
   117                 $e->replace(
       
   118                     cn => [ uniq $e->get("cn"), $cn ],
       
   119                     sn => [ uniq $e->get("sn"), $sn ]
       
   120                 );
       
   121             } elsif ( $cn =~ s/^-// ) {
       
   122                 $e->delete( cn => [$cn], sn => [$sn] );
       
   123             } else {
       
   124                 $e->replace( cn => $cn, sn => $sn );
       
   125             }
       
   126             $modified++;
       
   127         }
       
   128 
       
   129         if ( defined $Cf->other ) {
       
   130             my @o = split /,/, $Cf->other;
       
   131             grep { /^[+-]/ } @o or $e->delete(AT_ADDRESS);
       
   132 
       
   133             foreach my $a ( split /,/, $Cf->other ) {
       
   134                 if ( $a =~ s/^-// ) {
       
   135                     $e->delete( (AT_ADDRESS) => [$a] );
       
   136                 } else {
       
   137                     $a =~ s/^\+//;
       
   138 
       
   139                     # Darf noch nicht woanders sein
       
   140                     $r = $ldap->search( base => $ubase, filter => "(mail=$a)" );
       
   141                     die $r->error if $r->code;
       
   142                     die "$a ist schon vergeben\n" if $r->count;
       
   143 
       
   144                     $e->add( (AT_ADDRESS) => [$a] );
       
   145                 }
       
   146             }
       
   147             $modified++;
       
   148         }
       
   149 
       
   150         if ( defined $Cf->group ) {
       
   151             my @g = split /,/, $Cf->group;
       
   152             grep { /^[+-]/ } @g
       
   153               or $e->delete(AT_GROUP)
       
   154               if $e->get_value(AT_GROUP);
       
   155 
       
   156             foreach my $g (@g) {
       
   157                 if ( $g =~ s/^-// ) {
       
   158                     $e->delete( (AT_GROUP) => [$g] );
       
   159                 } else {
       
   160                     $g =~ s/^\+//;
       
   161                     $e->add( (AT_GROUP) => [$g] );
       
   162                 }
       
   163             }
       
   164             $modified++;
       
   165         }
       
   166 
       
   167         if ( defined $Cf->forward ) {
       
   168             my @f = split /,/, $Cf->forward;
       
   169             grep { /^[+-]/ } @f
       
   170               or $e->delete(AT_FORWARDINGADDRESS)
       
   171               if $e->get_value(AT_FORWARDINGADDRESS);
       
   172 
       
   173             foreach my $f (@f) {
       
   174                 if ( $f =~ s/^-// ) {
       
   175                     $e->delete( (AT_FORWARDINGADDRESS) => [$f] );
       
   176                 } else {
       
   177                     $f =~ s/^\+//;
       
   178                     $e->add( (AT_FORWARDINGADDRESS) => [$f] );
       
   179                 }
       
   180             }
       
   181             $modified++;
       
   182         }
       
   183 
       
   184         if ( my $a = $Cf->primary ) {
       
   185             $r = $ldap->search(
       
   186                 base => $ubase,
       
   187 
       
   188                 # filter => "(|(mailPrimaryAddress=$a)(mail=$a))");
       
   189                 filter => "(mail=$a)"
       
   190             );
       
   191             die $r->error if $r->code;
       
   192             die "$a ist schon vergeben\n" if $r->count;
       
   193 
       
   194             $e->replace( (AT_PRIMARYADDRESS) => $Cf->primary );
       
   195             $modified++;
       
   196         }
       
   197 
       
   198         if ( my $pw = _mkpw( $Cf->password ) ) {
       
   199             $e->replace( userPassword => $pw );
       
   200             $modified++;
       
   201         }
       
   202 
       
   203         #if ($Cf->internal ne ":") {
       
   204         #$e->replace(iusRestrictedMail => $Cf->internal ? "TRUE" : "FALSE");
       
   205         #$modified++;
       
   206         #}
       
   207 
       
   208         $e->dump if $Cf->debug;
       
   209 
       
   210         if ($modified) {
       
   211             $r = $e->update($ldap);
       
   212             die $r->error . $r->code if $r->code;
       
   213         }
       
   214 
       
   215         verbose "ok\n";
       
   216 
       
   217         print "\n";
       
   218     }
       
   219 
       
   220 }
       
   221 
       
   222 sub _list() {
       
   223 
       
   224     #@ARGV = ("*") unless @ARGV;
       
   225 
       
   226     die "option acl_admin required\n" unless $Cf->acl_admin;
       
   227 
       
   228     $imap = Mail::IMAPTalk->new(
    77     $imap = Mail::IMAPTalk->new(
   229         Server => $Cf->imap_server,
    78         Server => $Cf->imap_server,
   230         Port   => $Cf->imap_port
    79         Port   => $Cf->imap_port
   231     )
    80     )
   232         or die "Can't connect to IMAP Server '", $Cf->imap_server,
    81         or die "Can't connect to IMAP Server '", $Cf->imap_server,
   233     "', Port '", $Cf->imap_port, "': ", $@;
    82     "', Port '", $Cf->imap_port, "': ", $@;
   234     $imap->login( $Cf->acl_admin, $acl_password ) or die $@;
    83     $imap->login( $Cf->acl_admin, $acl_password ) or die $@;
   235     die "IMAP Server does not advertise acl support" unless $imap->capability->{acl};
    84     die "IMAP Server does not advertise acl support" unless $imap->capability->{acl};
   236 
    85 
       
    86     # requires an imap connection
       
    87     my $ns = $imap->namespace() or die "No public namespaces available: $@";
       
    88     $nspat = [];
       
    89     for (@{$ns->[2]}) {
       
    90         (my $n = $_->[0]) =~ s/$_->[1]$//;
       
    91         push @{$nspat}, [qr/\Q$n\E($_->[1]|$)/, $_->[1]];
       
    92     }
       
    93 
       
    94     if    ( $Cf->list )   { _list() }
       
    95     elsif ( $Cf->modify ) { _modify() }
       
    96     elsif ( $Cf->delete ) { $Cf->acl('delete') ; _modify() }
       
    97     else { die "Need action (--modify|--list)\n" }
       
    98 
       
    99 }
       
   100 
       
   101 sub _modify() {
       
   102 
       
   103     # Auch hier gehen wir davon aus, daß die dn direkt aus dem User-Namen folgt:
       
   104     # dn: uid=USER,...
       
   105     my @users;
       
   106     @ARGV or die "Need user(s)\n";
       
   107     $Cf->folder ~~ [] and die "Need folders(s)\n";
       
   108     $Cf->acl or die "Need acl\n";
       
   109     $Cf->recursive and $Cf->create and die "Use either --recursive or --create but not both\n";
       
   110 
       
   111     my $r = $ldap->search(
       
   112         base   => $ubase,
       
   113         filter => "(|" . join( "", map { "(uid=$_)" } @ARGV ) . ")"
       
   114     );
       
   115     die $r->error if $r->code;
       
   116     unless ($r->count) {
       
   117         prompt('No matching user found in ldap. Continue? (y/N)', "n\n") =~ /y/i or exit 0;
       
   118         @users = @ARGV;
       
   119     }
       
   120 
       
   121     while (my $e = ($r->shift_entry or shift @users)) {
       
   122 
       
   123         my ($user, $dn);
       
   124 
       
   125         if (ref $e eq 'Net::LDAP::Entry') {
       
   126             $user = $e->get_value("uid");
       
   127             $dn   = $e->dn;
       
   128         } else {
       
   129             $user = $e;
       
   130             $dn   = '[dn not available]';
       
   131         }
       
   132 
       
   133         my $modified = 0;
       
   134         verbose "$user:\n";
       
   135         verbose "\t$dn...\n";
       
   136 
       
   137         for my $folder (@{$Cf->folder}) {
       
   138 
       
   139             $imap->create($folder) or die "Can't create folder '$folder': $@" if $Cf->create;
       
   140 
       
   141             for my $f (@{acl_folders($folder)}) {
       
   142 
       
   143                 if ($Cf->acl eq 'delete') {
       
   144                     $imap->deleteacl($f, $user) or die "Can't delete acl: $@";
       
   145                     verbose "\t$f: none\n";
       
   146                 } else {
       
   147                     $imap->setacl($f, $user, $Cf->acl) or die "Can't set acl: $@";
       
   148                     verbose "\t$f: " . $Cf->acl . "\n";
       
   149                 }
       
   150 
       
   151             }
       
   152 
       
   153         }
       
   154         # Fix: iusMailOptions wurde erst später eingeführt, bei Bedarf also hinzufügen
       
   155         #if (!grep /^iusMailOptions$/, @{$e->get("objectClass")}) {
       
   156         #$e->add(objectClass => "iusMailOptions");
       
   157         #}
       
   158 
       
   159         #if ($Cf->internal ne ":") {
       
   160         #$e->replace(iusRestrictedMail => $Cf->internal ? "TRUE" : "FALSE");
       
   161         #$modified++;
       
   162         #}
       
   163 
       
   164         verbose "ok\n";
       
   165         print "\n";
       
   166 
       
   167     }
       
   168 
       
   169 }
       
   170 
       
   171 sub _list() {
       
   172 
       
   173     #@ARGV = ("*") unless @ARGV;
       
   174 
       
   175     die "option acl_admin required\n" unless $Cf->acl_admin;
       
   176 
   237     if (@ARGV) {
   177     if (@ARGV) {
   238 
   178 
   239 #        my $uid = $ARGV[0];
   179 #        my $uid = $ARGV[0];
   240 #        # searching by more than use user may be too expensive
   180 #        # searching by more than use user may be too expensive
   241 #        die "Searching by more than one user not supported" unless @ARGV == 1 or $uid =~ /\*/;
   181 #        die "Searching by more than one user not supported" unless @ARGV == 1 or $uid =~ /\*/;
   242         #list_by_user($_) for @ARGV;
   182         #list_by_user($_) for @ARGV;
       
   183 
       
   184         warn "--folder option ignored when listing by user" unless $Cf->folder ~~ [];
   243         list_by_user($imap, @ARGV);
   185         list_by_user($imap, @ARGV);
   244 
   186 
   245     } elsif ($Cf->folder) {
   187     } elsif (not $Cf->folder ~~ []) {
   246 
   188 
   247         list_by_folder($imap, $_, $Cf->recursive) for @{$Cf->folder};
   189         list_by_folder($_) for @{$Cf->folder};
   248 
   190 
   249     } else {
   191     } else {
   250 
   192 
   251         die "Need either user or --folder. If you really want to search all users then supply the pattern '*'.";
   193         die "Need either user or --folder. If you really want to search all users then supply the pattern '*'.";
   252 
   194 
   262     my $r = $ldap->search(
   204     my $r = $ldap->search(
   263         filter => $filter,
   205         filter => $filter,
   264         base   => $ubase,
   206         base   => $ubase,
   265     );
   207     );
   266     die $r->error if $r->code;
   208     die $r->error if $r->code;
   267     verbose("No matching users found\n") unless $r->count;
   209     my @users;
   268 
   210     unless ($r->count) {
   269     while (my $e = $r->shift_entry) {
   211         verbose("No matching users found in ldap.\n");
   270 
   212         @users = @_;
   271         my $uid  = $e->get_value("uid");
   213     }
   272         my $cn   = join( ", ", $e->get_value("cn") );
   214 
   273         my $mr   = $e->get_value(AT_PRIMARYADDRESS) || "";                  # ??
   215     while (my $e = ($r->shift_entry or shift @users)) {
       
   216 
       
   217         my ($uid, $cn, $mr);
       
   218         if (ref $e eq 'Net::LDAP::Entry') {
       
   219             $uid  = $e->get_value("uid");
       
   220             $cn   = join( ", ", $e->get_value("cn") );
       
   221             $mr   = $e->get_value(AT_PRIMARYADDRESS) || "";                  # ??
       
   222         } else {
       
   223             $uid = $e;
       
   224             $cn  = '[cn not available]';
       
   225             $mr  = '[address not available]';
       
   226         }
   274 
   227 
   275         print "$uid: $cn <$mr>\n";
   228         print "$uid: $cn <$mr>\n";
   276 
   229 
   277         #if (($e->get_value("iusRestrictedMail")||"") eq "TRUE") {
   230         #if (($e->get_value("iusRestrictedMail")||"") eq "TRUE") {
   278         #print " INTERNAL";
   231         #print " INTERNAL";
   304         my $hasacl;
   257         my $hasacl;
   305         my $ns = $imap->namespace() or die "No public namespaces available: $@";
   258         my $ns = $imap->namespace() or die "No public namespaces available: $@";
   306         # uns interessieren nur 'public' namespaces
   259         # uns interessieren nur 'public' namespaces
   307         for my $n (@{$ns->[2]}) {
   260         for my $n (@{$ns->[2]}) {
   308 
   261 
   309             my $folders = $imap->list( '', "$n->[0]*" ) or die $@;
   262             my $folders = imap_rlist( '', $n->[0], $n->[1] );
   310             ref $folders or die "Got empty folder list. Does '$n->[0]' actually exist? Is it readable?";
       
   311 
       
   312             for my $f ( @{$folders} ) {
   263             for my $f ( @{$folders} ) {
   313 
   264 
   314                 # single folder sieht wie folgt aus: [[flag1, flag2, ...], separator, foldername]
       
   315                 #next if '\\Noselect' ~~ $f->[0];
   265                 #next if '\\Noselect' ~~ $f->[0];
   316                 my $perms = $imap->getacl( $f->[2] ) or die $@;
   266                 my $perms = $imap->getacl( $f ) or die $@;
   317                 my ($u, $p);
   267                 my ($u, $p);
   318                 while ($u = shift @{$perms} and $p = shift @{$perms} and $u eq $uid) { 
   268                 while ($u = shift @{$perms} and $p = shift @{$perms}) { 
       
   269                     next unless $u eq $uid;
   319                     $hasacl = 1;
   270                     $hasacl = 1;
   320                     print "\t$f->[2]: $u [$p]\n";
   271                     print "\t$f: $u [$p]\n";
   321                 }
   272                 }
   322 
   273 
   323             }
   274             }
   324 
   275 
   325         }
   276         }
   329 
   280 
   330     }
   281     }
   331 
   282 
   332 }
   283 }
   333 
   284 
   334 sub list_by_folder($$$) {
   285 sub list_by_folder($) {
   335 
   286 
   336     my ($imap, $folder, $recursive) = @_;
   287     my ($folder) = @_;
   337 
   288 
   338     $folder .= '/' unless $folder =~ m,/$,;
   289     for my $f ( @{acl_folders($folder)} ) {
   339     my $folders = $recursive
       
   340         ? ($imap->list('', "$folder*") or die $@)
       
   341         : [[ undef, undef, $folder ]];
       
   342 
       
   343     ref $folders or die "Got empty folder list. Does '$folder' actually exist? Is it readable?";
       
   344 
       
   345     for my $f ( @{$folders} ) {
       
   346 
   290 
   347         my $hasacl;
   291         my $hasacl;
   348         print "$f->[2]\n";
   292         print "$f\n";
   349 
   293 
   350         # single folder sieht wie folgt aus: [[flag1, flag2, ...], separator, foldername]
   294         my $perms = $imap->getacl( $f ) or die $@;
   351         #next if '\\Noselect' ~~ $f->[0];
       
   352         my $perms = $imap->getacl( $f->[2] ) or die $@;
       
   353         my ($u, $p);
   295         my ($u, $p);
   354         while ($u = shift @{$perms}
   296         while ($u = shift @{$perms} and $p = shift @{$perms}) {
   355                 and $p = shift @{$perms}) {
   297 
   356             next if $u eq $Cf->acl_admin or $u eq $Cf->imap_admin;
   298 #        use Data::Dumper;
       
   299 #        warn Dumper([ $Cf->acl_admin, $Cf->imap_admin, '#' . $Cf->acl_admin, '#' . $Cf->imap_admin ]);
       
   300 
       
   301             # '#user' will be listed when we have a global acl for 'user'
       
   302             next if $u ~~ [ $Cf->acl_admin, $Cf->imap_admin, '#' . $Cf->acl_admin, '#' . $Cf->imap_admin ];
   357             $hasacl = 1;
   303             $hasacl = 1;
   358             print "\t$u [$p]\n";
   304             print "\t$u [$p]\n";
   359         }
   305         }
   360 
   306 
   361         print "\tno acl found\n" unless $hasacl;
   307         print "\tno acl found\n" unless $hasacl;
   390         return shift @pw;
   336         return shift @pw;
   391 
   337 
   392     }
   338     }
   393 }
   339 }
   394 
   340 
       
   341 sub imap_list($$) {
       
   342 
       
   343     my ($ref, $folder) = @_;
       
   344 
       
   345     my $list = $imap->list($ref, $folder) or die "Can't list('$ref', '$folder'): $@";
       
   346     ref $list or die "Got empty folder list. Does '$folder' actually exist? Is it readable?";
       
   347     # single folder sieht wie folgt aus: [[flag1, flag2, ...], separator, foldername]
       
   348     return [ map $_->[2], @{$list} ];
       
   349 
       
   350 }
       
   351 
       
   352 sub imap_rlist($$$) {
       
   353 
       
   354     my ($ref, $folder, $sep) = @_;
       
   355     $folder =~ s/$sep+$//;
       
   356 
       
   357     my $list = imap_list($ref, $folder);
       
   358     push @{$list}, @{imap_list($ref, "$folder$sep*")} if $Cf->recursive;
       
   359     return $list;
       
   360 }
       
   361 
       
   362 sub acl_folders($) {
       
   363 
       
   364     my ($f) = @_;
       
   365     my $folders;
       
   366 
       
   367     for my $np (@{$nspat}) {
       
   368         # don't modify $f!
       
   369         (my $ft = $f) =~ s/$np->[1]$//;
       
   370         return imap_rlist('', $f, $np->[1]) if ($ft =~ /$np->[0]/);
       
   371     }
       
   372 
       
   373     die "Foldername '$f' must begin with the name of a shared namespace\n";
       
   374 
       
   375 }
       
   376 
       
   377 sub prompt($$) {
       
   378     my ($prompt, $default) = @_;
       
   379     print $prompt, substr($default, 0, 1), "\b";
       
   380     ReadMode 4; my $r = ReadKey(0); ReadMode 0;
       
   381     if ($r eq "\n") { $r = $default }
       
   382     else { $r .= substr($default, 1) }
       
   383     print $r;
       
   384     return $r;
       
   385 }
       
   386 
   395 1;
   387 1;
   396 
   388 
   397 # vim:sts=4 sw=4 aw ai sm nohlsearch:
   389 # vim:sts=4 sw=4 aw ai sm nohlsearch: