Net::LDAP::Class - object-relational mapper for Net::LDAP


Net-LDAP-Class documentation Contained in the Net-LDAP-Class distribution.

Index


Code Index:

NAME

Top

Net::LDAP::Class - object-relational mapper for Net::LDAP

SYNOPSIS

Top

 # define your class

 package MyLDAPClass;
 use base qw( Net::LDAP::Class );

 __PACKAGE__->metadata->setup(
    attributes          => [qw( name address phone email )],
    unique_attributes   => [qw( email )],
    base_dn             => 'dc=mycompany,dc=com',
 );

 1;

 # then use your class

 use MyLDAPClass;
 use Net::LDAP;

 my $ldap = create_Net_LDAP_object_and_bind();  # you write this

 # create an instance of your class
 my $person = MyLDAPClass->new( ldap => $ldap, email => 'foo@bar.com' );

 # load from LDAP or write if not yet existing
 $person->read or $person->create;

 # set the 'name' attribute
 $person->name( 'Joe Foo' );

 # write your changes 
 $person->update;

 # change your mind?
 $person->delete;




DESCRIPTION

Top

Net::LDAP::Class (NLC) is an object-relational mapping for LDAP.

I know, it's all wrong to confuse the ORM model with LDAP since LDAP is not relational in the same way that a RDBMS is. But the ORM APIs of projects like DBIx::Class and Rose::DB::Object are so fun and easy to use, it seemed like LDAP management should be just as fun and easy.

The architecture of this package is based on Rose::DB::Object, which the author uses to great effect for RDBMS management.

METHODS

Top

NLC uses the Rose::Object package to create methods and handle the mundane get/set features. In addition, Net::LDAP::Class::MethodMaker implements a new method type called related_objects which handles the get/set/fetch of NLC objects related to a given NLC object. Typically these are Users and Groups. A User is typically related to one or more Groups, and a Group is typically related to one or more Users. See Net::LDAP::Class::User and Net::LDAP::Class::Group for examples.

There are some methods which every NLC subclass must implement. See SUBCLASSING for details.

init

Override this in a subclass. Be sure to call SUPER::init in your subclass.

metadata_class

Returns 'Net::LDAP::Class::Metadata' by default.

metadata

Returns an instance of the metadata_class() containing all the metadata for the NLC class. May be called as a class or object method.

init_ldap

If you do not pass a Net::LDAP object to new(), you may instead set the ldap_uri() class method to a URI string and init_ldap() will create a Net::LDAP object and bind() it for you.

Returns a Net::LDAP object.

init_debug

Sets the default debug flag to whatever the PERL_DEBUG or LDAP_DEBUG env variable is set to.

init_ldap_entry

Returns undef by default.

get_ldap_error( ldap_msg )

Stringify the error message for the ldap_msg object.

stringify

Returns the first unique attribute value that is not undef. If no such value is found, returns the object.

By default all NLC-derived objects are overloaded with this method.

find( opts )

Returns array (or array ref if called in scalar context) of objects matching opts.

opts may include:

ldap

If not present, the ldap() method is called instead.

base_dn

If not present, the base_dn() method is called instead.

Any other opts are passed directly to the Net::LDAP search() method.

Returns undef if no results matching opts are found.

create

Write a new object to the database. Calls the action_for_create() method -- see SUBCLASSING.

read

Read an object's attribute values from the database. You must have previously set at least one unique attribute in the object in order for the read() to work.

Returns the object on success, undef if the object was not found.

update

Write changes to the database. Calls action_for_update() -- see SUBCLASSING.

If no changes are detected, aborts and returns undef.

On successful write, returns the value of read().

delete

Remove the object from the database. You must call read() first.

Returns the value of do_batch().

read_or_create

Convenience method. If read() returns undef, create() is called. Returns the object in any case.

save

Convenience method. If ldap_entry() is set, update() is called. Otherwise, read_or_create() is called. The NLC object is returned in any case.

validate( attr_name, attr_value )

Called by MethodMaker every time an attribute is set with a MethodMaker-created method.

If validate() returns true, attr_value is set. If validate() returns false, a fatal error is thrown and error() set.

This method should be overriden in your subclass to provide schema-specific validation. The default behaviour is a no-op (always returns true).

do_batch( array_of_actions )

Creates (if batch() is not already set) and runs a Net::LDAP::Batch object, passing it the array_of_actions to run. Will croak on any error.

Returns the Net::LDAP::Batch object on success.

add_to_batch( array_of_actions )

Initializes (if necessary) and adds array_of_actions to the Net::LDAP::Batch object in batch().

rollback

Will call the rollback() method on the Net::LDAP::Batch object returned by batch(). If there is not batch() set, will croak.

action_for_create

See SUBCLASSING.

action_for_update

See SUBCLASSING.

action_for_delete

See SUBCLASSING.

check_unique_attributes_set

Returns true (1) if any unique attribute is set with a defined value.

Returns false (0) if no unique attributes are set.

AUTOLOAD

Will croak() with a helpful message if you call a method that does not exist. Mostly useful for catching cases where you forget to predefine an attribute.

dump

Returns Data::Dump::dump output for the NLC object. Useful for debugging. See also the Net::LDAP::Entry dump() method which can be called on the ldap_entry value.

 $nlc->dump;                # same as Data::Dump::dump( $nlc )
 $nlc->ldap_entry->dump;    # see Net::LDAP::Entry dump() method

has_local_changes

Convenience method. Returns true if the object has had any values set since the last time it was written to the server.

batch

Get/set the Net::LDAP::Batch object for the current transaction. Typically you don't want to mess with this but documented for completeness.

prev_batch

Get/set the Net::LDAP::Batch object for the just-completed transaction. Typically you don't want to mess with this but documented for completeness.

act_on_all( code_ref [, opts] )

Performs coderef sub reference on all records in LDAP. The coderef should expect one argument: a Net::LDAP::Class-derived object.

act_on_all() operates using Net::LDAP::Control::Paged, performing a search() using a filter based on unique_attributes() and iterating over all matches in groups of (by default) 500. You may set the pager size in opts. opts should be a hash ref. The following key/value pairs are supported:

page_size

Default: 500. Sets the Net::LDAP::Control::Paged size.

filter

Default: unique_atttributes->[0] = '*'

Set the filter for the search.

ldap

A Net::LDAP object. Required if you call act_on_all() as a class method.

Returns the number of Net::LDAP::Class results acted upon.

isa_user

Convenience method. Just returns shift->isa('Net::LDAP::Class::User').

isa_group

Just like isa_user() but checks the Net::LDAP::Class::Group.

SUBCLASSING

Top

NLC is designed as a base class with basic default behaviours for most common usage. However, every subclass must implement some methods, usually because such methods are specific to the particular LDAP schema you are using with the subclass.

The following methods are required by every NLC subclass. These action_for_* methods should return either a Net::LDAP::Batch::Action-based object or an array of values that can be passed to the add_actions() method of the Net::LDAP::Batch class.

See Net::LDAP::Class::User::POSIX and Net::LDAP::Class::Group::POSIX for examples.

action_for_create

action_for_update

action_for_delete

In addition, if you use the related_objects MethodMaker feature, then your subclass must implement a fetch_method_name method for each related_objects method name. Again, see Net::LDAP::Class::User::POSIX and Net::LDAP::Class::Group::POSIX for examples.

AUTHOR

Top

Peter Karman, <karman at cpan.org>

BUGS

Top

Please report any bugs or feature requests to bug-net-ldap-class at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Net-LDAP-Class. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

Top

You can find documentation for this module with the perldoc command.

    perldoc Net::LDAP::Class

You can also look for information at:

* AnnoCPAN: Annotated CPAN documentation

http://annocpan.org/dist/Net-LDAP-Class

* CPAN Ratings

http://cpanratings.perl.org/d/Net-LDAP-Class

* RT: CPAN's request tracker

http://rt.cpan.org/NoAuth/Bugs.html?Dist=Net-LDAP-Class

* Search CPAN

http://search.cpan.org/dist/Net-LDAP-Class

ACKNOWLEDGEMENTS

Top

The Minnesota Supercomputing Institute http://www.msi.umn.edu/ sponsored the development of this software.

COPYRIGHT

Top

SEE ALSO

Top

Net::LDAP, Net::LDAP::Batch


Net-LDAP-Class documentation Contained in the Net-LDAP-Class distribution.
package Net::LDAP::Class;

use strict;
use warnings;
use base qw( Rose::Object );
use Carp;
use Data::Dump ();
use Net::LDAP;
use Net::LDAP::Entry;
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant qw(LDAP_CONTROL_PAGED);
use Net::LDAP::Batch;
use Net::LDAP::Class::Metadata;
use Net::LDAP::Class::Iterator;
use Net::LDAP::Class::MultiIterator;
use Net::LDAP::Class::SimpleIterator;

use Net::LDAP::Class::MethodMaker (
    'scalar --get_set_init' => [qw( ldap ldap_entry debug error )],
    'scalar'                => [qw( batch prev_batch )],
    'object_or_class_meta'  => [qw( attributes unique_attributes base_dn )],
);

use overload '""' => 'stringify', 'bool' => sub {1}, 'fallback' => 1;

our $VERSION = '0.26';

sub init {
    my $self = shift;
    $self->SUPER::init(@_);

    my $meta = $self->metadata;
    if ( !$meta or !$meta->is_initialized ) {
        croak
            "must initialize Metadata class before instantiating a new object";
    }

    $self->{ldap} ||= $self->init_ldap;

    if ( !$self->ldap->isa('Net::LDAP') ) {
        croak "ldap value is not a Net::LDAP-derived object";
    }

    return $self;
}

sub metadata_class {'Net::LDAP::Class::Metadata'}

sub metadata {
    my ($self) = shift;

    # object method
    if ( ref $self ) {
        return $self->{_meta}
            ||= $self->metadata_class->for_class( ref $self );
    }

    # class method
    return $Net::LDAP::Class::Metadata::Objects{$self}
        || $self->metadata_class->for_class($self);
}

sub init_ldap {
    my $self = shift;
    if ( !$self->ldap_uri ) {
        croak "must set ldap_uri() or override init_ldap()";
    }

    my $ldap = Net::LDAP->new( $self->ldap_uri )
        or croak "can't create new Net::LDAP: $!";
    my $msg = $ldap->bind() or croak "can't do anonymous LDAP bind: $!";
    if ( $msg->code ) {
        croak "LDAP bind failed: " . $self->get_ldap_error($msg);
    }
    return $ldap;
}

sub init_debug { $ENV{PERL_DEBUG} || $ENV{LDAP_DEBUG} }

sub init_ldap_entry { return undef }

sub get_ldap_error {
    my $self = shift;
    my $msg  = shift or croak "ldap_msg required";
    my $str  = "\n"
        . join( "\n",
        "Return code: " . $msg->code,
        "Message: " . $msg->error_name,
        " :" . $msg->error_text,
        "MessageID: " . $msg->mesg_id,
        "DN: " . $msg->dn,
        ) . "\n";
    return $str;
}

sub stringify {
    my $self = shift;
    for my $key ( @{ $self->unique_attributes } ) {
        my $val = $self->$key;
        return $val if defined $val;
    }
    return $self;
}

sub find {
    my $self  = shift;
    my $class = ref($self) || $self;
    my %opts  = @_;
    my $ldap  = delete $opts{ldap} || $self->ldap;

    if ( !$ldap ) {
        croak "Net::LDAP object required";
    }

    my $base = delete $opts{base_dn} || $self->base_dn;

    if ( !$base ) {
        croak "must indicate base_dn in opts or call as object method";
    }

    my $attr = delete $opts{attrs} || $self->attributes;

    my $msg = $ldap->search(
        base  => $base,
        attrs => $attr,
        %opts,
    );

    if ( $msg->code ) {
        croak $self->get_ldap_error($msg);
    }

    my @results;

    for my $entry ( $msg->entries() ) {
        push(
            @results,
            $class->new(
                ldap       => $ldap,
                ldap_entry => $entry,
            )
        );
    }

    return unless @results;
    return wantarray ? @results : \@results;
}

sub create {
    my $self = shift;
    unless ( $self->check_unique_attributes_set ) {
        croak
            "at least one unique attribute must be set in order to create()";
    }
    my @action = $self->action_for_create(@_) or return;
    $self->do_batch(@action);
    $self->read or croak "cannot read newly created $self";
    return $self;
}

sub read {
    my $self = shift;
    my %opts = @_;

    my ( $filter, $value );

    if ( !$opts{filter} && !$opts{value} ) {

        unless ( $self->check_unique_attributes_set ) {
            croak "cannot read() without unique attribute set. "
                . "Unique attributes include: "
                . join( ', ', @{ $self->unique_attributes } );
        }

        # get first unique key set for filter

        for my $key ( @{ $self->unique_attributes } ) {
            if ( defined $self->$key ) {
                $filter = $key;
                $value  = $self->$key;
                last;
            }
        }

    }
    else {
        $filter = delete $opts{filter};
        $value  = delete $opts{value};
    }

    if ( !defined $filter ) {
        croak "could not find a unique filter to read() on";
    }
    if ( !defined $value ) {
        croak "could not find a unique value to read() on";
    }

    my $base_dn = delete $opts{base_dn} || $self->base_dn;

    $self->debug && warn "read() within $base_dn : $filter=$value\n";

    my $msg = $self->ldap->search(
        base   => $base_dn,
        scope  => "sub",
        filter => "($filter=$value)",
        attrs  => $self->attributes,
    );

    if ( $msg->count() > 0 ) {
        carp "$filter $value exists" if $self->debug;

        my $entry = $msg->entry(0);

        # set any entry attributes we've got cached in $self
        for my $attr ( keys %{ $self->{_not_yet_set} } ) {

            my $new = $self->{_not_yet_set}->{$attr};
            my $old = $entry->get_value($attr) || '';
            if ( $new ne $old ) {
                $entry->replace( $attr,
                    delete $self->{_not_yet_set}->{$attr} );
                $self->{_was_set}->{$attr}->{new} = $new;
                $self->{_was_set}->{$attr}->{old} = $old;
            }
            else {
                delete $self->{_not_yet_set}->{$attr};
            }

        }

        # this will cause any existing entry to be DESTROYed
        $self->ldap_entry($entry);

        return $self;
    }
    else {
        return;
    }
}

sub update {
    my $self = shift;
    $self->check_unique_attributes_set;
    unless ( $self->ldap_entry ) {
        croak "can't update() without first having a Net::LDAP::Entry loaded";
    }
    my @action = $self->action_for_update(@_) or return;

    # clear, since action_for_update() has already used them.
    $self->{_was_set} = {};

    $self->do_batch(@action);

    return $self->read;
}

sub delete {
    my $self = shift;
    $self->check_unique_attributes_set;
    unless ( $self->ldap_entry ) {
        croak "can't delete() without having a Net::LDAP::Entry loaded";
    }
    my @action = $self->action_for_delete or return;
    return $self->do_batch(@action);
}

sub read_or_create {
    my $self = shift;
    if ( !$self->read(@_) ) {
        $self->create(@_);
    }
    return $self;
}

sub save {
    my $self = shift;
    if ( $self->ldap_entry ) {
        $self->update;
    }
    else {
        $self->read_or_create;
    }
    return $self;
}

sub validate {
    my ( $self, $attr, $value ) = @_;
    if ( $self->debug ) {
        warn "validate $attr: $value\n";
    }
    return 1;
}

sub do_batch {
    my $self    = shift;
    my @actions = @_;
    if ( !@actions ) {
        warn "no actions to execute\n";
        return;
    }
    my $batch = $self->batch || Net::LDAP::Batch->new(
        ldap  => $self->ldap,
        debug => $self->debug,
    );
    if ( $self->debug ) {
        warn "Batch: " . Data::Dump::dump($batch);
    }
    $batch->add_actions(@actions);
    $self->prev_batch($batch);
    $batch->do or croak $batch->error;
    return $batch;
}

sub add_to_batch {
    my $self = shift;
    my $batch = $self->batch || Net::LDAP::Batch->new(
        ldap  => $self->ldap,
        debug => $self->debug
    );
    $batch->add_actions(@_);
    $self->batch($batch);
}

sub rollback {
    my $self = shift;
    if ( $self->prev_batch ) {
        $self->prev_batch->rollback or croak $self->prev_batch->error;
    }
    else {
        croak "no prev_batch to rollback";
    }
    return 1;
}

sub action_for_create {
    croak "must override action_for_create()";
}

sub action_for_update {
    croak "must override action_for_update()";
}

sub action_for_delete {
    croak "must override action_for_delete()";
}

sub check_unique_attributes_set {
    my $self = shift;
    my $uk   = $self->unique_attributes;
    if ( !ref($uk) eq 'ARRAY' ) {
        croak "unique_attributes must be an ARRAY ref";
    }
    for my $key (@$uk) {
        if ( defined $self->$key ) {
            return 1;
        }
    }
    return 0;
}

sub AUTOLOAD {
    my ( $self, @args ) = @_;

    my ($attribute) = ( our $AUTOLOAD =~ /([^:]+)$/ );

    #    carp "AUTOLOAD called for "
    #        . ref($self)
    #        . " -> $attribute "
    #        . Data::Dump::dump( \@args );

    if ( $attribute eq 'DESTROY' ) {

        #Data::Dump::dump($self);
        return;
    }

    croak qq[no such attribute or method "$attribute" defined for package "]
        . ref($self)
        . qq[ -- do you need to add '$attribute' to your setup() call?"];
}

sub dump {
    my $self = shift;
    return Data::Dump::dump($self);
}

sub has_local_changes {
    my $self = shift;
    return scalar keys %{ $self->{_was_set} };
}

sub act_on_all {
    my $self    = shift;
    my $coderef = shift or croak "coderef required";
    my $opts    = shift || {};
    my $class   = ref($self) || $self;

    if ( ref $coderef ne 'CODE' ) {
        croak "coderef is not a CODE reference";
    }

    my $filter = $opts->{filter}
        || $self->metadata->unique_attributes->[0] . '=*';
    my $page_size = $opts->{page_size} || 500;

    my $ldap = $opts->{ldap} || $self->ldap;
    my $page = Net::LDAP::Control::Paged->new( size => $page_size );
    my $cookie;
    my @args = (
        'base'    => $self->metadata->base_dn,
        'filter'  => "($filter)",
        'attrs'   => $self->metadata->attributes,
        'control' => [$page],
    );

    my $count = 0;

PAGE: while ( my $ldap_search = $ldap->search(@args) ) {

        # fatal on search error
        croak "error searching ldap: ", $self->get_ldap_error($ldap_search)
            if ( $ldap_search->code );

    ENTRY: while ( my $ldap_entry = $ldap_search->shift_entry ) {

            $count++;

            my $nlc = $class->new(
                ldap       => $ldap,
                ldap_entry => $ldap_entry
            );

            $self->debug and warn sprintf( "%6d %s\n", $count, $nlc );

            $coderef->($nlc);
        }

        # handle next search page
        my ($resp) = $ldap_search->control(LDAP_CONTROL_PAGED) or last PAGE;
        $cookie = $resp->cookie;
        if ( !$cookie ) {
            last PAGE;
        }
        $page->cookie($cookie);

    }

    # be nice to the server and stop the search if we still have a cookie
    if ($cookie) {
        $page->cookie($cookie);
        $page->size(0);
        $ldap->ldap->search(@args);
        croak "LDAP seach ended prematurely.";
    }

    return $count;

}

sub isa_user {
    return shift->isa('Net::LDAP::Class::User');
}

sub isa_group {
    return shift->isa('Net::LDAP::Class::Group');
}

1;

__END__