package LATMOS::Accounts::Bases::Ldap::User;

use 5.010000;
use strict;
use warnings;

use base qw(LATMOS::Accounts::Bases::Ldap::objects);
use Net::LDAP;
use Net::LDAPS;
use Net::LDAP::Entry;
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant qw( LDAP_CONTROL_PAGED ); 
use Net::LDAP::Util     qw( escape_filter_value );
use LATMOS::Accounts::Log;
use LATMOS::Accounts::Bases::Ldap::Group;
use LATMOS::Accounts::Utils;
use Crypt::SmbHash;

our $VERSION = (q$Rev: 649 $ =~ /^Rev: (\d+) /)[0];

=head1 NAME

LATMOS::Ldap - Perl extension for blah blah blah

=head1 SYNOPSIS

  use LATMOS::Ldap;
  blah blah blah

=head1 DESCRIPTION

Stub documentation for LATMOS::Ldap, created by h2xs. It looks like the
author of the extension was negligent enough to leave the stub
unedited.

Blah blah blah.

=head1 FUNCTIONS

=cut

sub _class_filter { '(&(ObjectClass=posixAccount))' }

sub _key_attr { 'cn' } 

sub _my_ldap_classes { qw(
    top
    inetOrgPerson
    organizationalPerson
    posixAccount
    shadowAccount
    sambaSamAccount
) }

sub _computeSSID {
    my ($value) = @_;
    $value * 2 + 1000
}

sub _get_attr_schema {
    my ($class, $base) = @_;
    $class->SUPER::_get_attr_schema($base,
    {
        displayName => { },
        givenName => { },
        homePhone => { },
        homePostalAddress => { },
        initials => { },
        mail => { },
        sn => { },
        mobile => { },
        o => { },
        uid => { mandatory => 1 },
        manager => {
            delayed => 1,
            can_values => sub { $base->list_objects('user') },
        },
        facsimileTelephoneNumber => { },
        uidNumber => {
            uniq => 1,
            post => sub {
                my ($self, $value) = @_;
                $self->object->set_fields(sambaSID => $self->object->base->sambaSID(_computeSSID($value)));
            },
        },
        gidNumber => {
            reference => 'group',
            mandatory => 1,
            can_values => sub {
                map { $base->get_object('group',
                        $_)->get_attributes('gidNumber') }
                $base->list_objects('group')
            },
            display => sub {
                my ($self, $val) = @_;
                my ($gr) = $self->base->search_objects('group', "gidNumber=$val")
                    or return;
                return $gr;
            },

        },
        homeDirectory => { },
        userPassword => { },
        loginShell => { },
        gecos => { },
        description => { },
        shadowLastChange => { },
        shadowMin => { },
        shadowMax => { },
        shadowWarning => { },
        shadowInactive => { },
        shadowExpire => { },
        shadowFlag => { },
        street => { },
        postOfficeBox => { },
        postalCode => { },
        postalAddress => { },
        streetAddress => { },
        physicalDeliveryOfficeName => { },
        ou => { },
        st => { },
        l => { },
        telephoneNumber => { },
        memberOf => {
            delayed => 1,
            multiple => 1,
            reference => 'group',
        },
        cn => { ro => 1, },
        dn => { ro => 1, },
        objectClass => { ro => 1, },
        pwdAccountLockedTime => {},
        pwdPolicySubentry => {},
        pwdChangedTime => { ro => 1 },
        labeledURI => {},
        userPassword    => { readable => 0, },
        sambaLMPassword => { readable => 0, },
        sambaNTPassword => { readable => 0, },
        sambaSID => { ro => 1 },
    }
    );
}

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

    $self->SUPER::_update_class(
        sambaSID => $self->base->sambaSID(
            _computeSSID($self->_get_attributes('uidNumber'))
        )
    );
}

sub _create {
    my ($class, $base, $id, %data) = @_;

    my $entry = Net::LDAP::Entry->new();

    $entry->dn(join(',',
        sprintf('%s=%s',
            $class->_dn_attribute($base),
            escape_filter_value($id)),
            $base->object_base_dn($class->type),
        )
    );
    $entry->replace(objectClass =>
        [ $class->_my_ldap_classes ],);
    $data{cn} ||= $data{sn} || $id; # cn is mandatory
    $data{sn} ||= $id; # sn is mandatory
    $data{uid} ||= $id; # uid is mandatory
    $data{sambaSID} = $base->sambaSID(_computeSSID($data{uidNumber}));
    $data{homeDirectory} ||= '/dev/null'; # homeDirectory is mandatory
    $data{$class->_key_attribute($base)} = $id;
    foreach (keys %data) {
        $class->_populate_entry($entry,
            $_, $data{$_}, $base);
    }

    my $msg = $base->ldap->add($entry);
    if ($msg->code) {
    	$base->log(LA_ERR, "Cannot create user: %s", $msg->error);
    	return 0;
    }
    return 1;
}

sub _rename {
    my ($class, $base, $uid, $newuid) = @_;

    $class->SUPER::_rename($base, $uid, $newuid) or return;
}

sub get_field {
    my ($self, $field) = @_;

    $field eq 'streetAddress' and $field = 'street';
    $field eq 'memberOf' and do {
        my @res;
        $self->base->_unlimited_search(
            base => $self->base->object_base_dn('group'),
            filter => sprintf(
                '(&(objectClass=posixGroup)(memberUID=%s))',
                escape_filter_value($self->id),
            ),
            callback => sub {
                my ($mesg, $entry) = @_;
                ref $entry eq 'Net::LDAP::Entry' or return;
                push(@res, $entry->get_value(
                    LATMOS::Accounts::Bases::Ldap::Group->_key_attribute($self->base),
                ));
                1;
            },
        );
        return [ sort(@res) ];
    };
    $field eq 'manager' and do {
        my $dn = $self->SUPER::get_field($field) or return;
        my $manager = $self->base->_get_object_from_dn($dn) or return;
        return $manager->get_value(__PACKAGE__->_key_attribute($self->base));
    };
    $self->SUPER::get_field($field);
}

sub _populate_entry {
    my ($self, $entry, $f, $val, $base) = @_;
    $base ||= $self->base;
    for ($f) {
        /^sn$/ and $val ||= $entry->get_value($self->_key_attribute);
        /^memberOf$/ and do {
            my %users;
            $users{$_}{e} = 1 foreach (ref $self
                ? $self->get_attributes('memberOf')
                : ());
            $val = [ $val ] unless(ref $val);
            $users{$_}{n} = 1 foreach (@{ $val || []});
            foreach (keys %users) {
                $users{$_}{e} && $users{$_}{n} and next;
                my $group = $base->get_object('group', $_) or next;
                if ($users{$_}{e}) {
                    $group->{entry}->delete(memberUID =>
                        $entry->get_value(
                            LATMOS::Accounts::Bases::Ldap::User->_key_attribute($base),
                        )
                    );
                } elsif ($users{$_}{n}) {
                    $group->{entry}->add(memberUID =>
                        $entry->get_value(
                            LATMOS::Accounts::Bases::Ldap::User->_key_attribute($base),
                        )
                    );
                } # else {} # can't happen
                my $mesg = $group->{entry}->update($base->ldap);
                if ($mesg->code) {
                    $base->log(LA_ERR, "Cannot set attributes: %s", $mesg->error);
                    return;
                }
            }
            return 1;
        };
        /^manager$/ && $val and do {
            my $user = $base->get_object('user', $val) or do {
                $val = undef;
                next;
            };
            $val = $user->get_field('dn');
            next;
        };
        /^gecos$/ and do {
            $val = to_ascii($val);
        };
        /^homeDirectory$/ and $val ||= '/dev/null';
    }
    $self->SUPER::_populate_entry($entry, $f, $val, $base);
}

sub _set_password {
    my ($self, $clear_pass) = @_;

    my @salt_char = (('a' .. 'z'), ('A' .. 'Z'), (0 .. 9), '/', '.');
    my $salt = join('', map { $salt_char[rand(scalar(@salt_char))] } (1 .. 8));
    # openldap use prefix to identify encryption passwd
    # {CRYPT} is system dependant, eg use crypt from system
    # As we run openldap on UNIX, this should not be a problem
    # as we use perl crypt() which does the same
    # This code will have to be changed if we use openldap on other UNIX
    my $md5 = '{CRYPT}' . crypt($clear_pass, '$1$' . $salt);

    my ($lm, $nt) = ntlmgen $clear_pass;

    my $res = $self->set_fields(
        userPassword    => $md5,
        sambaLMPassword => $lm,
        sambaNTPassword => $nt,
    );
    $self->base->log(LA_NOTICE, 'Mot de passe changé pour %s', $self->id)
    if($res);
    return $res;
}

1;

__END__

=head1 SEE ALSO

=head1 AUTHOR

Olivier Thauvin, E<lt>olivier.thauvin@aerov.jussieu.frE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2008 CNRS SA/CETP/LATMOS

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.10.0 or,
at your option, any later version of Perl 5 you may have available.


=cut
