Schrift
Wiki:Tipp zum Debugging: use Data::Dumper; local $Data::Dumper::Useqq = 1; print Dumper \@var;
[thread]10491[/thread]

Active Directory Abfrage (LDAP?)

Leser: 7


<< |< 1 2 >| >> 11 Einträge, 2 Seiten
Faldaar
 2007-09-28 17:39
#100136 #100136
User since
2003-11-05
14 Artikel
BenutzerIn
[default_avatar]
Hallo,

ich habe das Problem, dass ich Benutzer aus einem AD (genauer aus 2 OUs in diesem AD) auslesen muss und prüfen ob diese Mitglied von bestimmten Gruppen sind (ca. 40 verschiedene Gruppen).

Ich habe bereits ein Perl Programm, das genau diese Funktion in einer NT4 Domäne erfüllt hat, mittels
Code: (dl )
1
2
use Win32::NetAdmin qw(GetUsers GroupIsMember FILTER_NORMAL_ACCOUNT);
use Win32API::Net;

Dazu wurden mit GetUsers alle Benutzer der Domäne abgefragt, per Iteration durchgegangen und für jede Gruppe mit der Funktion GroupIsMember geprüft ob der Benutzer in der Gruppe ist.

Das Programm an sich läuft auch in einer W2k3 Umgebung, ABER es fragt grundsätzlich alle Benutzer der Domäne ab (über 400) anstatt nur die aus den 2 OUs (ca. 190). Dadurch ist es natürlich entsprechend langsam (Laufzeit 180 Sekunden+).

Nun habe ich es schon etwas angepasst und lese die Benutzer per Net::LDAP aus den 2 OUs aus (unsere Anmeldenamen enthalten zum Glück keine Umlaute/Sonderzeichen), Rest ist wie gehabt. Dadurch hat sich die Laufzeit immerhin auf unter 110 Sekunden verkürzt, was ich immer noch extrem lang finde. Mein Ziel sind hier eher 30 Sekunden oder weniger, was aber wohl utopisch ist :)

Nun dachte ich, dass ich eventuell alles per Net::LDAP abfragen kann und dadurch Zeit gewinne, da stellen sich aber schon die ersten Probleme:
1.) Umlaute in den Gruppennamen
2.) get_value('memberOf') gibt leider den kompletten DN der Gruppe zurück. Ich habe zum vergleichen nur eine Liste der samAccountname der Gruppen.

Nun habe ich beim durchforsten des Forums bereits herausfinden können, dass ich das AD wohl auch (besser?) per Win32::OLE abfragen kann.
Kennt sich damit jemand aus und kann mir ein Codeschnipsel geben wie ich dort eine Liste alle Mitglieder einer OU und die Gruppenzugehörigkeit eines Benutzers Abfrage?

Danke
Martin

Edit: Alternative Lösungsvorschläge werden selbstverständlich auch gerne genommen. Glaube so langsam drehe ich mich hier eh grade im Kreis und seh den Wald vor lauter Bäumen nicht mehr.

Edit2: Die o.g. Laufzeiten sind bei Ausführung auf einem Client in der Domäne. Wird das Programm auf dem ADC ausgeführt verringern sich die Zeiten deutlich (ca. 50%).
Es ist schwieriger eine vorgefasste Meinung zu zertrümmern als ein Atom. (Albert Einstein)
Faldaar
 2007-10-01 12:29
#100173 #100173
User since
2003-11-05
14 Artikel
BenutzerIn
[default_avatar]
Nach viel probieren und googeln habe ich das ganze nun mit Net::LDAP hin bekommen. Ist zwar insgesamt nicht die eleganteste Lösung, aber es läuft und noch dazu ziemlich schnell. Bin nun auf knapp 20 Sekunden wenn auf einem Client ausgeführt, unter 10 Sekunden auf dem DC selber. Damit kann ich sehr gut leben.

Gehe nun so vor:
Hole per LDAP den Benutzer und seine Gruppen.
Code (perl): (dl )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
my $noldapserver = 0;
my $ldap = Net::LDAP -> new($dc1) or $noldapserver=1;
if ( $noldapserver == 1 )  {
   $ldap = Net::LDAP -> new($dc2) or die "Error connecting to specified domain controllers $@ \n";
}
my $mesg = $ldap -> bind($user,
                                        password => $passwd
                                        );
$mesg = $ldap -> search( 
                                        base => $base,
                                        filter => "(&(objectclass=user)(samAccountname=*))"
                                        );
# Check for Error Code
if ( $mesg -> code ) {
        &LDAPerror( "Search", $mesg );
}
# Ergebnisanzahl auslesen
my $max = $mesg -> count;

# Ergebnisse durchgehen
for ( my $i = 0; $i < $max; $i++ ) {
        my $entry = $mesg -> entry ( $i );
        my $user = lc( $entry -> get_value( 'samAccountname' ) );
        my @groups = $entry -> get_value('memberOf');
        # weiterer code, u.a. Gruppenprüfung s.u.
}

Dann werden die Gruppen des Benutzers durchlaufen, und verglichen mit einem Hash, der bestimmte Gruppen enthält, die mich interessieren:
Code (perl): (dl )
1
2
3
4
5
6
7
8
9
10
11
foreach ( @groups ) {
        my $gruppe = $_;
        my @gnamen = split( /,/, $gruppe );
        if ( $gnamen[1] ne "OU=Gruppen"  ) { # es interessieren nur Gruppen unterhalb der OU Gruppen
                next();
        }
        my $grp = ( split( /=/, $gnamen[0] ) )[1];
        if ( $Freigaben{$grp} ) {
                # etwas tun
        }
}


Die Fehlerbehandlung habe ich Beispielhaft nur einmal drin gelassen, alle anderen sehen ähnlich aus.
Wie gesagt sicher nicht die schönste Lösung, aber es erfüllt seinen Zweck und das (wohl langsame) Modul Win32::NetAdmin hat ausgedient.

Martin

Edit: Durch weitere Optimierungen, vor allem in der Programmlogik, auf eine Laufzeit von 1,5 bis 5 Sekunden runter. Gleich mal schauen was man aus dem AD noch so alles raus quetschen kann :)
Es ist schwieriger eine vorgefasste Meinung zu zertrümmern als ein Atom. (Albert Einstein)
Strat
 2007-10-03 17:22
#100237 #100237
User since
2003-08-04
5246 Artikel
ModeratorIn
[Homepage] [default_avatar]
deine suche verwendet kein paging; falls da mal mehr als 1000 Objekte gefunden werden, werden nur die ersten 1000 angezeigt. Siehe CPAN:Net::LDAP::Control::Paged.

Der Zugriff via Net::LDAP aufs ActiveDirectory funktioniert nur dann gut, solange keine Umlaute ins Spiel kommen; LDAP V3 besteht auf utf8, waehrend das AD (genauer gesagt: die AD-Admin-Tools) so eine Art Windows-ANSI verlangt. Beim Lesen spielt das dank CPAN:Encode keine Rolle, aber wenn man neue Objekte anlegt oder bestehende aendert, kann es das Problem geben, dass in den Admin-Tools anstelle des Umlautes unleserliche Zeichen angezeigt werden.

Man kann dies umgehen, indem man LDAP V2 verwendet; allerdings faellt dann Paging flach.

Ich bevorzuge den Weg mit ADSI ueber Win32::OLE. Infos zu ADSI via Win32::OLE findest du auf http://techtasks.com/code/viewbook/2?lang=Perl

<Werbung>
Ich will fuer eine der naechsten Ausgaben des Foo-Magazin einen Artikel ueber ADSI via Win32::OLE schreiben. In der aktuellen Ausgabe (3) steht uebrigens ein Artikel ueber Net::LDAP, in dem auch Paged Resultsets behandelt werden ...
</Werbung>
perl -le "s::*erlco'unaty.'.dk':e,y;*kn:ai;penmic;;print"
http://www.fabiani.net/
Faldaar
 2007-10-04 17:02
#100271 #100271
User since
2003-11-05
14 Artikel
BenutzerIn
[default_avatar]
Danke für die Hinweise, vor allem den Link zu Win32::OLE Beispielen. Genau so etwas suchte ich. Werde mich bei Gelegenheit mal daran versuchen.

Das mit den Umlauten habe ich gemerkt, aber da zur Zeit nur aus dem AD gelesen wird und daraus Berichte/Listen generiert werden sollte das nicht stören.

Wegen dem Paging: Ich hatte zuerst Net::LDAP::Control::Paged genutzt, es aber wieder verworfen, da wir die Grenze von 1000 Objekten wohl nicht knacken werden. Zumindest nicht innerhalb einer OU und da die Listen Standort/OU abhängig generiert werden, sollte ich da kein Problem haben.

Martin
Es ist schwieriger eine vorgefasste Meinung zu zertrümmern als ein Atom. (Albert Einstein)
Strat
 2007-10-05 00:51
#100293 #100293
User since
2003-08-04
5246 Artikel
ModeratorIn
[Homepage] [default_avatar]
Faldaar+2007-10-04 15:02:15--
Wegen dem Paging: Ich hatte zuerst Net::LDAP::Control::Paged genutzt, es aber wieder verworfen, da wir die Grenze von 1000 Objekten wohl nicht knacken werden. Zumindest nicht innerhalb einer OU und da die Listen Standort/OU abhängig generiert werden, sollte ich da kein Problem haben.


Man soll niemals nie sagen. Ich habe schon Pferde kotzen sehen (zumindest, wenn man sie schwarz anstreicht, mit Puffmais fuettert, sie dann in die Sonne stellt und dabei einen Film auf einer Grossbildleinwand sieht...)

Da Paging ausser ein paar Zeilen Code kaum was kostet, sollte jede Suche, die mehr als 1-2 Eintraege zurueckliefern kann, mit Paging gemacht werden. Das dauert zwar ein paar Minuten laenger, aber dafuer hat man wesendlich robusteren Code, und wenn mal eine Fehlersuche noetig ist, hat man die paar Minuten schnell hundertfach wieder herinnen. Bei einem meiner Kunden laufen noch einige Scripte, die ich 1998 geschrieben habe. Und in fast zehn Jahren ist eine Menge passiert, und sie laufen nur noch deshalb, weil ich sie damals sehr robust geschrieben habe.
perl -le "s::*erlco'unaty.'.dk':e,y;*kn:ai;penmic;;print"
http://www.fabiani.net/
Faldaar
 2007-10-05 16:32
#100364 #100364
User since
2003-11-05
14 Artikel
BenutzerIn
[default_avatar]
Stimme da eigentlich zu, dass es robust sein sollte.

Hätte da nochmal ne Frage zu Control::Paged. Wie geht man damit am sinnvollsten um? Hänge mal als Beispiel nen Code (inkl. Control::Paged) rein, der mir alle E-mail Adressen von allen Benutzern (die ein Exchange Postfach haben) ausgibt.

Muss gestehen, hab mich bis vor kurzem noch nicht so tief mit automatisierten AD Abfragen befasst.

Code (perl): (dl )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#!/usr/bin/perl -T -w

use Net::LDAP;
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant ( "LDAP_CONTROL_PAGED" );

$dc1="172.xx.xx.xx";
$dc2="172.xx.xx.xx";

$hqbase="dc=dom,dc=local";

$user="cn=USER,cn=Users,dc=dom,dc=local";
$passwd="PASSWORD";

# Connecting to Active Directory domain controllers
$noldapserver=0;
$ldap = Net::LDAP->new($dc1) or
   $noldapserver=1;
if ($noldapserver == 1)  {
   $ldap = Net::LDAP->new($dc2) or
      die "Error connecting to specified domain controllers $@ \n";
}

$mesg = $ldap->bind ( dn => $user,
                      password =>$passwd);
if ( $mesg->code()) {
    die ("error:", $mesg->error_text((),"\n"));
}

# How many LDAP query results to grab for each paged round
# Set to under 1000 for Active Directory
$page = Net::LDAP::Control::Paged->new( size => 990 );

@args = ( base     => $hqbase,
         filter => "(& (mailnickname=*) (| (&(objectCategory=person)
                    (objectClass=user)(!(homeMDB=*))(!(msExchHomeServerName=*)))
                    (&(objectCategory=person)(objectClass=user)(|(homeMDB=*)
                    (msExchHomeServerName=*)))(&(objectCategory=person))
                     ))",
          control  => [ $page ],
          attrs  => "proxyAddresses",
);

my $cookie;
while(1) {
  # Perform search
  my $mesg = $ldap->search( @args );

# Filtering results for proxyAddresses attributes  
  foreach my $entry ( $mesg->entries ) {
    my $name = $entry->get_value( "cn" );
    # LDAP Attributes are multi-valued, so we have to print each one.
    foreach my $mail ( $entry->get_value( "proxyAddresses" ) ) {
      if ( $mail =~ s/^(smtp|SMTP)://gs ) {
        if ( ! ( $mail =~ /SystemMailbox/i ) ) {
          print $mail . "\n"; 
        }
      }
    }
  }

  # Only continue on LDAP_SUCCESS
  $mesg->code and last;

  # Get cookie from paged control
  my($resp)  = $mesg->control( LDAP_CONTROL_PAGED ) or last;
  $cookie    = $resp->cookie or last;

  # Set cookie in paged control
  $page->cookie($cookie);
}

if ($cookie) {
  # We had an abnormal exit, so let the server know we do not want any more
  $page->cookie($cookie);
  $page->size(0);
  $ldap->search( @args );
  # Also would be a good idea to die unhappily and inform OP at this point
     die("LDAP query unsuccessful");
}

exit(0);


Lässt sich das vielleicht irgendwie vereinfachen? Wenn ich da geschachtelte Abfragen habe sehe ich die Abfragen vor lauter Cookies nicht mehr :)

Martin
Es ist schwieriger eine vorgefasste Meinung zu zertrümmern als ein Atom. (Albert Einstein)
Strat
 2007-10-06 14:25
#100384 #100384
User since
2003-08-04
5246 Artikel
ModeratorIn
[Homepage] [default_avatar]
vielleicht ein bisschen was in subroutinen auslagern? Ich baue mir dafuer meist sogar module, z.B. MyApp::LdapConnect, MyApp::LdapSearch und MyApp::LdapEntry, die mir eine Abstraktionsschicht geben.

PageSize wuerde ich irgendwo zwischen 200 und 500 ansetzen. Warum? Keine Ahnung ;-) Naja, manche Verzeichnisdienste haben auch ein SizeLimit von 500...

nebenbei: ich will dir use strict; und (anstelle von -w) use warnings; ans Herz legen. Ohne ist Perl meiner Meinung nach kaum kontrollierbar, und wenn wo ein Fehler auftritt, sucht man sich haeufig einen Wolf.

Zu Deinem Code:
Code: (dl )
1
2
my @domainControllers = ( $dc1, $dc2 );
my $ldap = Net::LDAP->new( \@domainControllers, version => 3 );

spart den ganzen Quargel mit $noldapserver. Und die Option onerror koennte auch interessant sein, siehe CPAN:Net::LDAP.

Code: (dl )
search: attrs  => "proxyAddresses",

Attrs erwartet eine arrayreferenz, keinen String; und hier musst du entweder alle Attribute angeben, auf die du spaeter zugreifen willst, oder einfach weglassen, dann kannst du auf alle nicht-operationalen attribute zugreifen (braucht halt ein wenig mehr RAM).
Code: (dl )
attrs => [ 'cn', 'proxyAddresses', ... ]


Code: (dl )
foreach my $entry ( $mesg->entries ) {

da bevorzuge ich den speicherschonenderen Weg:
Code: (dl )
while( my $entry = $mesg->shift_entry ) {

Vor allem bietet dies auch die Moeglichkeit, aus der Suche einen Iterator zu machen und eine Menge des Codes in eine Sub auszulagern, z.B. so auf die Art

Code (perl): (dl )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package MyApp::LdapSearch;
use warnings; 
use strict;

sub search {
    my( $class, $ldapConnection, %args ) = @_;

    my $self = bless( {}, $class );

    my $pageSize = 200; # falls kein PageSize mitgegeben, verwende 200
    $pageSize = delete $args{pageSize} if exists $args{pageSize};

    $self->{page} = Net::LDAP::Control::Paged->new( size => $pageSize );
    $args{control}  = [ $page ];
    
    # speichere den kram zur spaeteren verwendung in getNextEntry
    $self->{ldapConnection} = $ldapConnection;
    $self->{searchArgs} = \%args;

    $self->{result} = $ldapConnection->search( %args );

    return $self;
}

sub getNextEntry {
    my( $self ) = @_;

    my $result = $self->{result};

    if( my $entry = $self->{result}->shift_entry ) {
        return $entry;
    }
    
    my( $resp )  = $result->control( LDAP_CONTROL_PAGED ) or return;
    my $cookie    = $resp->cookie or return;
    
    # Set cookie in paged control
    $self->{page}->cookie( $cookie );

    $self->{result} =  $self->{ldapConnection}->search( $self->{searchArgs} );
    if( my $entry = $self->{result}->shift_entry ) {
        return $entry;
    }
    return;
}

# und im Hauptprogramm dann sowas wie:

my $result = MyApp::LdapSearch->search(
    $ldap, 
    base  => $hqbase,
    filter => $filter,
    attrs  => \@attributes,
    scope  => 'sub',
);

while( my $entry = $result->getNextEntry ) {
    my $name = $entry->get_value( 'cn' );
    # ...
} # while

Dies bitte nicht 1:1 uebernehmen, es soll nur die Idee demonstrieren. Es ist weder getestet noch hat es Fehlerbehandlung. Anstelle von $self->{page} = ... auch besser Getter/Setter verwenden: $self->page( ... ), usw.


perl -le "s::*erlco'unaty.'.dk':e,y;*kn:ai;penmic;;print"
http://www.fabiani.net/
Faldaar
 2007-10-09 12:33
#100540 #100540
User since
2003-11-05
14 Artikel
BenutzerIn
[default_avatar]
Vielen Dank für die ausführliche Antwort Strat.

Ich muss gestehen "use strict;" habe ich hier schlichtweg vergessen. normalerweise ist das direkt das erste was ich bei einem Perl Programm eintipper. Dies war mein erstes Programm mit LDAP Abfrage und durch das viele probieren hab ich es wohl irgendwann weg gelassen und in der finalen Version vergessen. Wird sofort wieder eingebaut.

Die Page Size von 990 hab ich gesetzt nachdem ich herausgefunden hatte das bei AD das Maximum 1000 ist und ich hatte wie gesagt keine LDAP Erfahrung. Werde Deinen Tip beherzigen und es irgendwo unter 500 ansetzen. Wer weiß was wir noch für Verzeichnisse bekommen ;)

Code: (dl )
my $ldap = Net::LDAP->new( \@domainControllers, version => 3 );

Wird sofort umgesetzt, sieht auch viel besser aus als meine Variante.

Quote
Attrs erwartet eine Arrayreferenz, keinen String; und hier musst du entweder alle Attribute angeben, auf die du spaeter zugreifen willst, oder einfach weglassen, dann kannst du auf alle nicht-operationalen Attribute zugreifen (braucht halt ein wenig mehr RAM).

Man lernt nie aus. Wieviel mehr ist "ein wenig mehr Ram"? Lohnt es sich darüber nachzudenken bei ca. 1500 Objekten?

Code: (dl )
while( my $entry = $mesg->shift_entry ) {

Wird sofort umgebaut.

Danke nochmal für die Tips, habe wieder viel dazu gelernt und werde nun gleich mal ein wenig mit dem Code "spielen".

Martin
Es ist schwieriger eine vorgefasste Meinung zu zertrümmern als ein Atom. (Albert Einstein)
Strat
 2007-10-09 14:21
#100556 #100556
User since
2003-08-04
5246 Artikel
ModeratorIn
[Homepage] [default_avatar]
Faldaar+2007-10-09 10:33:13--
Man lernt nie aus. Wieviel mehr ist "ein wenig mehr Ram"? Lohnt es sich darüber nachzudenken bei ca. 1500 Objekten?

Das haengt - genauso wie bei SQL mit SELECT * FROM table oder SELECT col1,col2,col3 FROM table - davon ab, wie "gross" die Objekte sind, also wie viele Attribute und Attributwerte sie verwenden und wie gross die Inhalte sind (z.B. Bilder, Zertifikate, ...). In einem Standard-AD ist der Unterschied fuer die paar Objekte wohl nicht so wild. Da die Daten jedoch aus der darunterliegenden Datenbank gelesen werden muessen und diese somit belasten, dann uebers Netz gesendet werden und dann noch ueber Perl zugreifbar gemacht werden muss, wuerde ich da die [s]Spalten[/s] Attribute immer angeben, auch wenn der Unterschied nicht gross ist.
perl -le "s::*erlco'unaty.'.dk':e,y;*kn:ai;penmic;;print"
http://www.fabiani.net/
nepos
 2007-10-09 15:01
#100561 #100561
User since
2005-08-17
1420 Artikel
BenutzerIn
[Homepage] [default_avatar]
Du könntest dich ja mal mit CPAN:Devel::Size spielen, um den Speicherplatz, den so ein Objekt benötigt, zu bestimmen.
<< |< 1 2 >| >> 11 Einträge, 2 Seiten



View all threads created 2007-09-28 17:39.