#!/usr/bin/perl # $Id: smbldap_tools.pl 140 2012-08-07 11:11:49Z fumiyas $ # This code was developped by Jerome Tournier (jtournier@gmail.com) and # contributors (their names can be found in the CONTRIBUTORS file). # This was first contributed by IDEALX (http://www.opentrust.com/) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . use strict; use warnings; package smbldap_tools; use Encode; use POSIX qw(:termios_h); use IO::File; use Net::LDAP; use Net::LDAP::Extension::SetPassword; use Crypt::SmbHash; use Digest::MD5 qw(md5); use Digest::SHA qw(sha1); use MIME::Base64 qw(encode_base64); use constant true => 1; use constant false => 0; my %conf_renamed_by = ( password_hash => 'hash_encrypt', password_crypt_salt_format => 'crypt_salt_format', ); my $smbldap_conf = $ENV{'SMBLDAP_CONF'} || '/etc/smbldap-tools/smbldap.conf'; my $smbldap_bind_conf = $ENV{'SMBLDAP_BIND_CONF'} || '/etc/smbldap-tools/smbldap_bind.conf'; my $samba_conf = $ENV{'SMBLDAP_SMB_CONF'} || $ENV{'SMB_CONF_PATH'} || '/etc/samba/smb.conf'; my $samba_bindir = $ENV{'SMBLDAP_SAMBA_BINDIR'} || '/usr/bin'; use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); use Exporter; $VERSION = 1.00; @ISA = qw(Exporter); use vars qw(%config $ldap); @EXPORT = qw( get_user_dn get_group_dn is_group_member is_samba_user is_unix_user is_nonldap_unix_user is_user_valid does_sid_exist get_dn_from_line add_posix_machine add_samba_machine add_samba_machine_smbpasswd group_add_user add_grouplist_user disable_user delete_user group_add group_del get_homedir read_user read_user_human_readable read_user_entry read_group read_group_entry read_group_entry_gid find_groups_of parse_group group_remove_member group_get_members get_user_dn2 connect_ldap_master connect_ldap_slave group_name_by_type group_type_by_name subst_configvar read_conf read_parameter subst_user split_arg_comma list_union list_minus account_by_sid user_by_uid user_next_uid user_next_rid group_next_uid group_next_rid print_banner getDomainName getLocalSID utf8Encode utf8Decode password_read password_set shadow_update nsc_invalidate %config ); sub print_banner { print STDERR "(c) Jerome Tournier - (jtournier\@gmail.com)- Licensed under the GPL\n" unless $config{no_banner}; } sub read_parameter { my $line = shift; ## check for a param = value if ( $line =~ /=/ ) { my ( $param, $val ); if ( $line =~ /\s*(.*?)\s*=\s*"(.*)"/ ) { ( $param, $val ) = ($1, $2); } elsif ( $line =~ /\s*(.*?)\s*=\s*'(.*)'/ ) { ( $param, $val ) = ($1, $2); } else { ( $param, $val ) = $line =~ /\s*(.*?)\s*=\s*(.*)/; } return ( $param, $val ); } } sub subst_configvar { my $value = shift; my $vars = shift; $value =~ s/\$\{([^}]+)\}/$vars->{$1} ? $vars->{$1} : $1/eg; return $value; } sub read_conf { my %conf; open( CONFIGFILE, "$smbldap_conf" ) || die "Unable to open $smbldap_conf for reading !\n"; while () { chomp($_); ## throw away comments next if ( /^\s*#/ || /^\s*$/ || /^\s*\;/ ); ## check for a param = value my ( $parameter, $value ) = read_parameter($_); $value = &subst_configvar( $value, \%conf ); $conf{$parameter} = $value; } close(CONFIGFILE); if ( $< == 0 ) { open( CONFIGFILE, "$smbldap_bind_conf" ) || die "Unable to open $smbldap_bind_conf for reading !\n"; while () { chomp($_); ## throw away comments next if ( /^\s*#/ || /^\s*$/ || /^\s*\;/ ); ## check for a param = value my ( $parameter, $value ) = read_parameter($_); $value = &subst_configvar( $value, \%conf ); $conf{$parameter} = $value; } close(CONFIGFILE); } else { $conf{slaveDN} = $conf{slavePw} = $conf{masterDN} = $conf{masterPw} = ""; } while (my ($new, $old) = each(%conf_renamed_by)) { if (exists($conf{$old})) { $conf{$new} = delete($conf{$old}); } } # automatically find SID if ( not $conf{SID} ) { $conf{SID} = getLocalSID() || die "Unable to determine domain SID: please edit your smbldap.conf, or start your samba server for a few minutes to allow for SID generation to proceed\n"; } return (%conf); } sub read_smbconf { my %conf; my $smbconf = "$samba_conf"; open( CONFIGFILE, "$smbconf" ) || die "Unable to open $smbconf for reading !\n"; my $global = 0; my $prevline = ""; while () { chomp; if (/^(.*)\\$/) { $prevline .= $1; next; } $_ = $prevline . $_; $prevline = ""; if (/^\[global\]/) { $global = 1; } if ( $global == 1 ) { if ( /^\[/ and !/\[global\]/ ) { $global = 0; } else { ## throw away comments #next if ( ! /workgroup/i ); next if ( /^\s*#/ || /^\s*$/ || /^\s*\;/ || /\[/ ); ## check for a param = value my ( $parameter, $value ) = read_parameter($_); $value = &subst_configvar( $value, \%conf ); $conf{$parameter} = $value; } } } close(CONFIGFILE); return (%conf); } my %smbconf = read_smbconf(); sub getLocalSID { open my $fh, "-|" or exec("$samba_bindir/net", "getlocalsid") || exit(1); my $line = <$fh>; if (!defined($line)) { die "Failed to get SID from Samba net command"; } my ($sid) = ($line =~ m/^SID for domain \S+ is: (\S+)$/); if (!defined($sid)) { die "Samba net command returns invalid output: $line"; } return $sid; } # let's read the configurations file... %config = ( masterLDAP => 'ldap://127.0.0.1/', slaveLDAP => 'ldap://127.0.0.1/', ldapTLS => false, ldapSSL => false, password_hash => 'SSHA', password_crypt_salt_format=>'%s', shadowAccount => true, nscd => "/usr/sbin/nscd", userHomeDirectoryMode => "0700", read_conf(), ); ## Backward compatibility with 0.8.4 and older if (!exists($config{userHome}) && exists($config{userHomePrefix})) { $config{userHome} = "$config{userHomePrefix}/%U"; } sub get_parameter { # this function return the value for a parameter. The name of the parameter can be either this # defined in smb.conf or smbldap.conf my $parameter_smb = shift; my $parameter_smbldap = shift; if ( defined $config{$parameter_smbldap} and $config{$parameter_smbldap} ne "" ) { return $config{$parameter_smbldap}; } elsif ( defined $smbconf{$parameter_smb} and $smbconf{$parameter_smb} ne "" ) { return $smbconf{$parameter_smb}; } else { #print "could not find parameter's value (parameter given: $parameter_smbldap or $parameter_smb) !!\n"; undef $smbconf{$parameter_smb}; } } $config{sambaDomain} = get_parameter( "workgroup", "sambaDomain" ); $config{suffix} = get_parameter( "ldap suffix", "suffix" ); $config{usersdn} = get_parameter( "ldap user suffix", "usersdn" ); if ( $config{usersdn} !~ m/,/ ) { $config{usersdn} = $config{usersdn} . "," . $config{suffix}; } $config{groupsdn} = get_parameter( "ldap group suffix", "groupsdn" ); if ( $config{groupsdn} !~ m/,/ ) { $config{groupsdn} = $config{groupsdn} . "," . $config{suffix}; } $config{computersdn} = get_parameter( "ldap machine suffix", "computersdn" ); if ( $config{computersdn} !~ m/,/ ) { $config{computersdn} = $config{computersdn} . "," . $config{suffix}; } $config{idmapdn} = get_parameter( "ldap idmap suffix", "idmapdn" ); if ( defined $config{idmapdn} ) { if ( $config{idmapdn} !~ m/,/ ) { $config{idmapdn} = $config{idmapdn} . "," . $config{suffix}; } } $config{sambaDomaindn} = "sambaDomainName=$config{sambaDomain},$config{suffix}"; $config{sambaUnixIdPooldn} ||= $config{sambaDomaindn}; if ( $config{ldapSSL} == 1 and $config{ldapTLS} == 1 ) { die "Both options ldapSSL and ldapTLS could not be activated\n"; } sub connect_ldap { my ($server, $port, $tls) = @_; my @params = ( version => 3, timeout => 60, ); my $uri; if ($server =~ m#^\w+://#) { $uri = $server; } else { if ($config{ldapSSL}) { $uri = "ldaps://$server"; push(@params, verify => $config{verify}, cafile => $config{cafile}, ); } else { $uri = "ldap://$server"; } $uri .= ":$port" if ($port); } my $ldap = Net::LDAP->new($uri, @params); unless ($ldap) { die "Cannot connect to LDAP server: $uri: $@\n"; } if ($tls) { my $mesg = $ldap->start_tls( verify => $config{verify}, clientcert => $config{clientcert}, clientkey => $config{clientkey}, cafile => $config{cafile}, ); if ($mesg->code) { $ldap->disconnect; die( "Cannot start TLS on LDAP connection: $uri: " . $mesg->error . "\n"); } } return $ldap; } sub connect_ldap_master { my $bind_dn = defined($_[0]) ? shift : $config{masterDN}; my $bind_pw = defined($_[0]) ? shift : $config{masterPw}; my $ldap_master = connect_ldap( $config{masterLDAP}, $config{masterPort}, $config{ldapTLS}, ); $ldap_master->bind($bind_dn, password => $bind_pw); $ldap = $ldap_master; return $ldap_master; } sub connect_ldap_slave { my $bind_dn = defined($_[0]) ? shift : $config{slaveDN}; my $bind_pw = defined($_[0]) ? shift : $config{slavePw}; my $ldap_slave; eval { $ldap_slave = connect_ldap( $config{slaveLDAP}, $config{slavePort}, $config{ldapTLS}, ); $ldap_slave->bind($bind_dn, password => $bind_pw); }; if ($@) { if ($config{masterLDAP} eq $config{slaveLDAP}) { die "$@"; } warn "$@"; warn "Trying to contact the LDAP master server...\n"; $ldap_slave = connect_ldap_master($bind_dn, $bind_pw); } $ldap = $ldap_slave; return $ldap_slave; } sub get_user_dn { my $user = shift; my $dn = ''; my $mesg = $ldap->search( base => $config{suffix}, scope => $config{scope}, filter => "(&(objectclass=posixAccount)(uid=$user))" ); $mesg->code && die $mesg->error; foreach my $entry ( $mesg->all_entries ) { $dn = $entry->dn; } chomp($dn); if ( $dn eq '' ) { return undef; } $dn = "dn: " . $dn; return $dn; } sub get_user_dn2 { my $user = shift; my $dn = ''; my $mesg = $ldap->search( base => $config{suffix}, scope => $config{scope}, filter => "(&(objectclass=posixAccount)(uid=$user))" ); $mesg->code && warn "failed to perform search; ", $mesg->error; foreach my $entry ( $mesg->all_entries ) { $dn = $entry->dn; } chomp($dn); if ( $dn eq '' ) { return ( 1, undef ); } $dn = "dn: " . $dn; return ( 1, $dn ); } sub get_group_dn { my $group = shift; my $dn = ''; my $filter; if ( $group =~ /^\d+$/ ) { $filter = "(&(objectclass=posixGroup)(|(cn=$group)(gidNumber=$group)))"; } else { $filter = "(&(objectclass=posixGroup)(cn=$group))"; } my $mesg = $ldap->search( base => $config{groupsdn}, scope => $config{scope}, filter => $filter ); $mesg->code && die $mesg->error; foreach my $entry ( $mesg->all_entries ) { $dn = $entry->dn; } chomp($dn); if ( $dn eq '' ) { return undef; } $dn = "dn: " . $dn; return $dn; } # return (success, dn) # bool = is_samba_user($username) sub is_samba_user { my $user = shift; my $mesg = $ldap->search( base => $config{suffix}, scope => $config{scope}, filter => "(&(objectClass=sambaSamAccount)(uid=$user))" ); $mesg->code && die $mesg->error; return ( $mesg->count ne 0 ); } sub is_unix_user { my $user = shift; my $mesg = $ldap->search( base => $config{suffix}, scope => $config{scope}, filter => "(&(objectClass=posixAccount)(uid=$user))" ); $mesg->code && die $mesg->error; return ( $mesg->count ne 0 ); } sub is_nonldap_unix_user { my $user = shift; my $uid = getpwnam($user); if ($uid) { return 1; } else { return 0; } } sub is_group_member { my $dn_group = shift; my $user = shift; my $mesg = $ldap->search( base => $dn_group, scope => 'base', filter => "(&(memberUid=$user))" ); $mesg->code && die $mesg->error; return ( $mesg->count ne 0 ); } # all entries = does_sid_exist($sid,$config{scope}) sub does_sid_exist { my $sid = shift; my $dn_group = shift; my $mesg = $ldap->search( base => $dn_group, scope => $config{scope}, filter => "(sambaSID=$sid)" ); $mesg->code && die $mesg->error; return ($mesg); } # try to bind with user dn and password to validate current password sub is_user_valid { my ($user, $dn, $pass) = @_; my $ldap = connect_ldap( $config{slaveLDAP}, $config{slavePort}, $config{ldapTLS}, ); my $mesg = $ldap->bind(dn => $dn, password => $pass); $ldap->disconnect; if ($mesg->code) { return 0; } return 1; } # dn = get_dn_from_line ($dn_line) # helper to get "a=b,c=d" from "dn: a=b,c=d" sub get_dn_from_line { my $dn = shift; $dn =~ s/^dn: //; return $dn; } # success = add_posix_machine($user, $uid, $gid) sub add_posix_machine { my ( $user, $uid, $gid, $wait ) = @_; if ( !defined $wait ) { $wait = 0; } # bind to a directory with dn and password my $add = $ldap->add( "uid=$user,$config{computersdn}", attr => [ #'objectclass' => ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'], 'objectclass' => [ 'top', 'account', 'posixAccount' ], 'cn' => "$user", #'sn' => "$user", 'uid' => "$user", 'uidNumber' => "$uid", 'gidNumber' => "$gid", 'homeDirectory' => '/nonexistent', 'loginShell' => '/bin/false', 'description' => 'Computer', 'gecos' => 'Computer', ] ); $add->code && warn "failed to add entry: ", $add->error; sleep($wait); return 1; } # success = add_samba_machine_smbpasswd($computername) sub add_samba_machine_smbpasswd { my $user = shift; system($config{smbpasswd}, "-a", "-m", $user); return 1; } sub add_samba_machine { my ( $user, $uid ) = @_; my $sambaSID = 2 * $uid + 1000; my $name = $user; $name =~ s/.$//s; my ( $lmpassword, $ntpassword ) = ntlmgen $name; my $modify = $ldap->modify( "uid=$user,$config{computersdn}", changes => [ #replace => [objectClass => ['inetOrgPerson', 'posixAccount', 'sambaSAMAccount']], replace => [ objectClass => [ 'posixAccount', 'sambaSAMAccount' ] ], add => [ sambaPwdLastSet => '0' ], add => [ sambaLogonTime => '0' ], add => [ sambaLogoffTime => '2147483647' ], add => [ sambaKickoffTime => '2147483647' ], add => [ sambaPwdCanChange => '0' ], add => [ sambaPwdMustChange => '0' ], add => [ sambaAcctFlags => '[W ]' ], add => [ sambaLMPassword => "$lmpassword" ], add => [ sambaNTPassword => "$ntpassword" ], add => [ sambaSID => "$config{SID}-$sambaSID" ], add => [ sambaPrimaryGroupSID => "$config{SID}-0" ] ] ); $modify->code && die "failed to add entry: ", $modify->error; return 1; } sub group_add_user { my ( $group, $userid ) = @_; my $members = ''; my $dn_line = get_group_dn($group); if ( !defined( get_group_dn($group) ) ) { print "$0: group \"$group\" doesn't exist\n"; exit(6); } if ( !defined($dn_line) ) { return 1; } my $dn = get_dn_from_line("$dn_line"); # on look if the user is already present in the group my $is_member = is_group_member( $dn, $userid ); if ( $is_member == 1 ) { print "User \"$userid\" already member of the group \"$group\".\n"; } else { # bind to a directory with dn and password # It does not matter if the user already exist, Net::LDAP will add the user # if he does not exist, and ignore him if his already in the directory. my $modify = $ldap->modify( "$dn", changes => [ add => [ memberUid => $userid ] ] ); $modify->code && die "failed to modify entry: ", $modify->error; } } sub group_del { my $group_dn = shift; # bind to a directory with dn and password my $modify = $ldap->delete($group_dn); $modify->code && die "failed to delete group : ", $modify->error; } sub add_grouplist_user { my ( $grouplist, $user ) = @_; my @array = split( /,/, $grouplist ); foreach my $group (@array) { group_add_user( $group, $user ); } } sub disable_user { my $user = shift; my $dn_line; my $dn = get_dn_from_line($dn_line); if ( !defined( $dn_line = get_user_dn($user) ) ) { print "$0: user $user doesn't exist\n"; exit(10); } my $modify = $ldap->modify( "$dn", changes => [ replace => [ userPassword => '{crypt}!x' ] ] ); $modify->code && die "failed to modify entry: ", $modify->error; if ( is_samba_user($user) ) { my $modify = $ldap->modify( "$dn", changes => [ replace => [ sambaAcctFlags => '[D ]' ] ] ); $modify->code && die "failed to modify entry: ", $modify->error; } } # delete_user($user) sub delete_user { my $user = shift; my $dn_line; if ( !defined( $dn_line = get_user_dn($user) ) ) { print "$0: user $user doesn't exist\n"; exit(10); } my $dn = get_dn_from_line($dn_line); my $modify = $ldap->delete($dn); $modify->code && die "failed to delete entry: ", $modify->error; } # $gid = group_add($groupname, $group_gid, $force_using_existing_gid) sub group_add { my ( $gname, $gid, $force ) = @_; nsc_invalidate("group"); if ( !defined($gid) ) { $gid = group_next_gid(); } else { if ( !defined($force) ) { if ( defined( getgrgid($gid) ) ) { return undef; } } } my $modify = $ldap->add( "cn=$gname,$config{groupsdn}", attrs => [ objectClass => [ 'top', 'posixGroup' ], cn => "$gname", gidNumber => "$gid" ] ); $modify->code && die "failed to add entry: ", $modify->error; return $gid; } # $homedir = get_homedir ($user) sub get_homedir { my $user = shift; my $homeDir = ''; my $entry; my $mesg = $ldap->search( base => $config{usersdn}, scope => $config{scope}, filter => "(&(objectclass=posixAccount)(uid=$user))" ); $mesg->code && die $mesg->error; my $nb = $mesg->count; if ( $nb > 1 ) { print "Aborting: there are $nb existing user named $user\n"; foreach $entry ( $mesg->all_entries ) { my $dn = $entry->dn; print " $dn\n"; } exit(4); } else { $entry = $mesg->shift_entry(); $homeDir = $entry->get_value("homeDirectory"); } chomp $homeDir; if ( $homeDir eq '' ) { return undef; } return $homeDir; } # search for an user sub read_user { my $user = shift; my $lines = ''; my $mesg = $ldap->search( # perform a search base => $config{suffix}, scope => $config{scope}, filter => "(&(objectclass=posixAccount)(uid=$user))" ); $mesg->code && die $mesg->error; foreach my $entry ( $mesg->all_entries ) { $lines .= "dn: " . $entry->dn . "\n"; foreach my $attr ( $entry->attributes ) { my @vals = $entry->get_value($attr); # my $val_utf8 = eval { # Encode::decode_utf8($val, Encode::FB_CROAK); # }; # $val = "**UNPRINTABLE**" if ($@ || $val_utf8 =~ /\P{IsPrint}/); $lines .= $attr . ": " . join( ',', @vals ) . "\n"; } } chomp $lines; if ( $lines eq '' ) { return undef; } return $lines; } # search for an user and print in a human readable format sub read_user_human_readable { my $user = shift; my $lines = ''; my $mesg = $ldap->search( # perform a search base => $config{suffix}, scope => $config{scope}, filter => "(&(objectclass=posixAccount)(uid=$user))" ); $mesg->code && die $mesg->error; foreach my $entry ( $mesg->all_entries ) { $lines .= "dn: " . $entry->dn . "\n"; foreach my $attr ( $entry->attributes ) { my @vals = $entry->get_value($attr); foreach my $val (@vals) { my $val_utf8 = eval { Encode::decode_utf8($val, Encode::FB_CROAK); }; $val = "**UNPRINTABLE**" if ($@ || $val_utf8 =~ /\P{IsPrint}/); } if ( $attr eq "sambaPwdLastSet" or $attr eq "sambaPwdCanChange" or $attr eq "sambaPwdMustChange" or $attr eq "sambaLogoffTime" or $attr eq "sambaKickoffTime" ) { my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday ) = gmtime( $entry->get_value($attr) ); $year += 1900; $mon += 1; $lines .= $attr . ": $year/$mon/$mday\n"; } elsif ( $attr eq "shadowLastChange" or $attr eq "shadowExpire" ) { my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday ) = gmtime( $entry->get_value($attr) * 24 * 60 * 60 ); $year += 1900; $mon += 1; $lines .= $attr . ": $year/$mon/$mday\n"; } else { $lines .= $attr . ": " . join( ',', @vals ) . "\n"; } } } chomp $lines; if ( $lines eq '' ) { return undef; } return $lines; } # search for a user # return the attributes in an array sub read_user_entry { my $user = shift; my $mesg = $ldap->search( # perform a search base => $config{suffix}, scope => $config{scope}, filter => "(&(objectclass=posixAccount)(uid=$user))" ); $mesg->code && die $mesg->error; my $entry = $mesg->entry(); return $entry; } # search for a group sub read_group { my $user = shift; my $lines = ''; my $mesg = $ldap->search( # perform a search base => $config{groupsdn}, scope => $config{scope}, filter => "(&(objectclass=posixGroup)(cn=$user))" ); $mesg->code && die $mesg->error; foreach my $entry ( $mesg->all_entries ) { $lines .= "dn: " . $entry->dn . "\n"; foreach my $attr ( $entry->attributes ) { { $lines .= $attr . ": " . join( ',', $entry->get_value($attr) ) . "\n"; } } } chomp $lines; if ( $lines eq '' ) { return undef; } return $lines; } # find groups of a given user ##### MODIFIE ######## sub find_groups_of { my $user = shift; my @groups = (); my $mesg = $ldap->search( # perform a search base => $config{groupsdn}, scope => $config{scope}, filter => "(&(objectclass=posixGroup)(memberuid=$user))" ); $mesg->code && die $mesg->error; my $entry; while ( $entry = $mesg->shift_entry() ) { push( @groups, scalar( $entry->get_value('cn') ) ); } return (@groups); } sub read_group_entry { my $group = shift; my $entry; my %res; my $mesg = $ldap->search( # perform a search base => $config{groupsdn}, scope => $config{scope}, filter => "(&(objectclass=posixGroup)(cn=$group))" ); $mesg->code && die $mesg->error; my $nb = $mesg->count; if ( $nb > 1 ) { print "Error: $nb groups exist \"cn=$group\"\n"; foreach $entry ( $mesg->all_entries ) { my $dn = $entry->dn; print " $dn\n"; } exit 11; } else { $entry = $mesg->shift_entry(); } return $entry; } sub read_group_entry_gid { my $group = shift; my %res; my $mesg = $ldap->search( # perform a search base => $config{groupsdn}, scope => $config{scope}, filter => "(&(objectclass=posixGroup)(gidNumber=$group))" ); $mesg->code && die $mesg->error; my $entry = $mesg->shift_entry(); return $entry; } # return the gidnumber for a group given as name or gid # -1 : bad group name # -2 : bad gidnumber sub parse_group { my $userGidNumber = shift; if ( $userGidNumber =~ /[^\d]/ ) { # make a search based on the group name my $gname = $userGidNumber; my $mesg = $ldap->search( # perform a search base => $config{groupsdn}, scope => $config{scope}, filter => "(&(objectclass=posixGroup)(cn=$gname))" ); $mesg->code && die $mesg->error; my $entry = $mesg->shift_entry(); my $gidnum; if ($entry) { $gidnum = $entry->get_value('gidNumber'); #my $gidnum = getgrnam($gname); } else { $gidnum = ""; } if ( $gidnum !~ /\d+/ ) { return -1; } else { $userGidNumber = $gidnum; } } else { # make a search based on the group gidNumber # we check that the gidNumber is attributed to a real group my $mesg = $ldap->search( # perform a search base => $config{groupsdn}, scope => $config{scope}, filter => "(&(objectclass=posixGroup)(gidNumber=$userGidNumber))" ); $mesg->code && die $mesg->error; my $entry = $mesg->shift_entry(); if ( !$entry ) { return -2; } } return $userGidNumber; } # remove $user from $group sub group_remove_member { my ( $group, $user ) = @_; my $members = ''; my $grp_line = get_group_dn($group); if ( !defined($grp_line) ) { return 0; } my $dn = get_dn_from_line($grp_line); # we test if the user exist in the group my $is_member = is_group_member( $dn, $user ); if ( $is_member == 1 ) { # delete only the user from the group my $modify = $ldap->modify( "$dn", changes => [ delete => [ memberUid => ["$user"] ] ] ); $modify->code && die "failed to delete entry: ", $modify->error; } return 1; } sub group_get_members { my ($group) = @_; my $members; my @resultat; my $grp_line = get_group_dn($group); if ( !defined($grp_line) ) { return 0; } my $mesg = $ldap->search( base => $config{groupsdn}, scope => $config{scope}, filter => "(&(objectclass=posixgroup)(cn=$group))" ); $mesg->code && die $mesg->error; foreach my $entry ( $mesg->all_entries ) { foreach my $attr ( $entry->attributes ) { if ( $attr =~ /\bmemberUid\b/ ) { foreach my $ent ( $entry->get_value($attr) ) { push( @resultat, $ent ); } } } } return @resultat; } sub group_name_by_type { my $groupmap = shift; my %type_name = ( 2 => 'domain', 4 => 'local', 5 => 'builtin' ); return $type_name{$groupmap}; } sub group_type_by_name { my $type_name = shift; my %groupmap = ( 'domain' => 2, 'local' => 4, 'builtin' => 5 ); return $groupmap{$type_name}; } sub subst_user { my ( $str, $username ) = @_; $str =~ s/%U/$username/ if ($str); return ($str); } # all given mails are stored in a table (remove the comma separated) sub split_arg_comma { my $arg = shift; my @args; if ( defined($arg) ) { if ( $arg eq '-' ) { @args = (); } else { @args = split( /\s*,\s*/, $arg ); } } return (@args); } sub list_union { my ( $list1, $list2 ) = @_; my @res = @$list1; foreach my $e (@$list2) { if ( !grep( $_ eq $e, @$list1 ) ) { push( @res, $e ); } } return @res; } sub list_minus { my ( $list1, $list2 ) = @_; my @res = (); foreach my $e (@$list1) { if ( !grep( $_ eq $e, @$list2 ) ) { push( @res, $e ); } } return @res; } sub account_next_id { my $attr = shift; my $domain = shift || $config{sambaDomain}; my $checker = shift; my $base = $config{sambaUnixIdPooldn}; my $oc = "sambaUnixIdPool"; my $filter = "(objectClass=sambaUnixIdPool)"; my $scope = "base"; my $id_bias = 0; if ($attr =~ /rid$/i) { $base = $config{suffix}; $oc = "sambaDomain"; $filter = "(&(objectClass=sambaDomain)(sambaDomainName=$domain))", $scope = "sub"; ## NOTE: sambaNextRid has "latest RID", not "next RID"! $id_bias = 1; } for (;;) { my $search = $ldap->search( base => $base, filter => $filter, scope => $scope, attrs => [$attr], ); if ($search->code) { die "Failed to search $oc to get next $attr: " . $search->error; } if ($search->count != 1) { die "Failed to find $oc to get next $attr"; } my $entry = $search->entry(0); my $id = $entry->get_value($attr); my $modify = $ldap->modify($entry->dn, changes => [ replace => [ $attr=> $id + 1 ] ] ); if ($modify->code) { die "Failed to update $attr in $oc: " . $modify->error; } $id += $id_bias; unless ($checker && !$checker->($id)) { return $id; } } } sub account_next_rid { my $domain = shift || $config{sambaDomain}; my $checker = shift || \&rid_is_free; return account_next_id("sambaNextRid", $domain, $checker); } sub account_base_rid { my $domain = shift || $config{sambaDomain}; my $search = $ldap->search( base => $config{suffix}, filter => "(&(objectClass=sambaDomain)(sambaDomainName=$domain))", scope => "sub", attrs => ["sambaAlgorithmicRidBase", "sambaNextRid"], ); if ($search->code) { die "Failed to search sambaDomain object to get sambaAlgorithmicRidBase: " . $search->error; } if ($search->count != 1) { die "Failed to find sambaDomain object to get sambaAlgorithmicRidBase"; } my $entry = $search->entry(0); my $rid_base = $entry->get_value("sambaAlgorithmicRidBase"); if (!defined($rid_base) && !defined($entry->get_value("sambaNextRid"))) { return 1000; } return $rid_base; } sub account_by_sid { my $sid = shift; my $search = $ldap->search( base => $config{suffix}, filter => "(sambaSID=$sid)", scope => "sub", ); if ($search->code) { die "Failed to search entries by SID: $sid: " . $search->error; } return ($search->entries)[0]; } sub account_by_rid { my $rid = shift; my $domain_sid = shift || $config{SID}; return account_by_sid("$domain_sid-$rid"); } sub rid_is_free { my $rid = shift; my $domain_sid = shift || $config{SID}; return !defined(account_by_rid($rid, $domain_sid)); } sub user_by_uid { my $uid = shift; my $search = $ldap->search( base => $config{suffix}, filter => "(&(objectClass=posixAccount)(uidNumber=$uid))", scope => "sub", ); if ($search->code) { die "Failed to search entries by UID: $uid: " . $search->error; } return ($search->entries)[0]; } sub uid_is_free { my ($uid) = @_; return !defined(user_by_uid($uid)); } sub user_next_uid { my $domain = shift || $config{sambaDomain}; my $checker = shift || \&uid_is_free; return account_next_id("uidNumber", $domain, $checker); } sub user_next_rid { my $uid = shift; my $domain = shift || $config{sambaDomain}; my $checker = shift || \&rid_is_free; if (defined(my $rid_base = account_base_rid($domain))) { ## Use legacy algorithmic RID generator return $uid * 2 + $rid_base; } return account_next_rid($domain, $checker); } sub group_by_gid { my $gid = shift; my $search = $ldap->search( base => $config{suffix}, filter => "(&(objectClass=posixGroup)(gidNumber=$gid))", scope => "sub", ); if ($search->code) { die "Failed to search entries by GID: $gid: " . $search->error; } return ($search->entries)[0]; } sub gid_is_free { my ($gid) = @_; return !defined(group_by_gid($gid)); } sub group_next_gid { my $domain = shift || $config{sambaDomain}; my $checker = shift || \&gid_is_free; return account_next_id("gidNumber", $domain, $checker); } sub group_next_rid { my $gid = shift; my $domain = shift || $config{sambaDomain}; my $checker = shift || \&rid_is_free; if (defined(my $rid_base = account_base_rid($domain))) { ## Use legacy algorithmic RID generator return $gid * 2 + $rid_base + 1; } return account_next_rid($domain, $checker); } sub utf8Encode { my $encoding = shift; my $string = shift; if ($encoding eq "UTF-8") { return $string; } Encode::from_to($string, $encoding, "UTF-8"); return $string; } sub utf8Decode { my $encoding = shift; my $string = shift; if ($encoding eq "UTF-8") { return $string; } Encode::from_to($string, "UTF-8", $encoding); return $string; } sub password_read { my ($prompt, $timeout) = @_; my $termios = POSIX::Termios->new; my $term_flag = defined($termios->getattr(STDIN->fileno)) ? $termios->getlflag : undef; my $pass; for (;;) { my $sig_handlers_orig = {}; my $sig_sent = {}; my $sig_hander = sub { $sig_sent->{shift(@_)} = 1; die; }; for my $sig_name (qw(ALRM INT HUP QUIT TERM TSTP TTIN TTOU)) { $sig_handlers_orig->{$sig_name} = $SIG{$sig_name}; $SIG{$sig_name} = $sig_hander; } $sig_handlers_orig->{'PIPE'} = $SIG{'PIPE'}; $SIG{'PIPE'} = 'IGNORE'; print $prompt if (defined($prompt)); $pass = eval { if ($term_flag && $term_flag & ECHO) { $termios->setlflag($term_flag & ~ECHO); $termios->setattr(STDIN->fileno, TCSANOW); } alarm($timeout) if ($timeout); STDIN->getline; }; alarm(0) if ($timeout); if ($term_flag && $term_flag & ECHO) { print "\n"; $termios->setlflag($term_flag); $termios->setattr(STDIN->fileno, TCSANOW); } while (my ($sig_name, $sig_handler_orig) = each(%$sig_handlers_orig)) { $SIG{$sig_name} = $sig_handler_orig || 'DEFAULT'; } my $restart = false; for my $sig_name (keys %$sig_sent) { kill($sig_name, $$) unless ($sig_name eq 'ALRM' && $timeout); $restart = true if ($sig_name =~ /^T(STP|TIN|TOU)$/); } last unless ($restart); } chomp($pass) if (defined($pass)); return $pass; } sub password_set { my ($dn, $pass, $pass_old, $hash, $salt_format) = @_; $hash ||= $config{password_hash}; if ($hash eq "exop") { password_exop($dn, $pass, $pass_old); } else { password_modify($dn, $pass, $pass_old, $hash, $salt_format); } shadow_update($dn); } sub password_exop { my ($dn, $pass, $pass_old) = @_; my %values = ( user => $dn, newpasswd => $pass, ); $values{oldpasswd} = $pass_old if (defined($pass_old)); my $set = $ldap->set_password(%values); $set->code && die "Failed to modify UNIX password: ", $set->error; } sub password_modify { my ($dn, $pass, $pass_old, $hash, $salt_format) = @_; my $pass_hashed = password_hash($pass, $hash, $salt_format); my $modify = $ldap->modify ($dn, changes => [ replace => [userPassword => $pass_hashed], ] ); $modify->code && die "Failed to modify UNIX password: ", $modify->error; } sub password_hash { my ($pass, $hash, $salt_format) = @_; return ($config{with_slappasswd}) ? password_hash_by_slappasswd($pass, $hash, $salt_format) : password_hash_internal($pass, $hash, $salt_format); } # Generates hash to be one of the following RFC 2307 schemes: # CRYPT, MD5, SMD5, SHA, SSHA and CLEARTEXT sub password_hash_internal { my $pass = shift; my $hash = shift || $config{password_hash}; my $crypt_salt_format = shift || $config{password_crypt_salt_format}; my $pass_hashed; if ($hash eq "CLEARTEXT") { return $pass; } elsif ($hash eq "CRYPT") { my $salt = sprintf($crypt_salt_format, password_salt()); $pass_hashed = crypt($pass, $salt); } elsif ($hash eq "MD5") { $pass_hashed = encode_base64( md5($pass),'' ); } elsif ($hash eq "SMD5") { my $salt = password_salt(4); $pass_hashed = encode_base64(md5($pass . $salt) . $salt, ''); } elsif ($hash eq "SHA") { $pass_hashed = encode_base64(sha1($pass), ''); } elsif ($hash eq "SSHA") { my $salt = password_salt(4); $pass_hashed = encode_base64(sha1($pass . $salt) . $salt, ''); } else { die "Unknown password hash scheme: $hash\n"; } return "{$hash}$pass_hashed"; } sub password_hash_by_slappasswd { my $pass = shift; my $hash = shift || $config{password_hash}; my $crypt_salt_format = shift || $config{password_crypt_salt_format}; # checking if password is tainted: nothing is changed!!!! # essential for perl 5.8 ($pass =~ /^(.*)$/ and $pass=$1) or die "$0: user password is tainted\n"; my $pass_hashed; if ($hash eq "CLEARTEXT") { return $pass; } elsif ($hash eq "CRYPT") { open BUF, "-|" or exec "$config{slappasswd}", "-h","{$hash}", "-c",$crypt_salt_format, "-s","$pass"; $pass_hashed = ; close BUF; } else { open(BUF, "-|") or exec "$config{slappasswd}", "-h","{$hash}", "-s","$pass"; $pass_hashed = ; close BUF; } defined($pass_hashed) or die "Failed to generate password hash!\n"; chomp($pass_hashed); length($pass_hashed) or die "Failed to generate password hash!"; return $pass_hashed; } # Generates salt # Similar to Crypt::Salt module from CPAN sub password_salt { my $length= shift || 32; my @seeds = ('.', '/', 0..9, 'A'..'Z', 'a'..'z'); return join "", @seeds[map {rand scalar(@seeds)} (1..$length)]; } sub shadow_update { if (!$config{shadowAccount}) { return; } shadow_update_internal(@_); } sub shadow_update_internal { my $dn = shift; my $time = shift || time; my $pass_maxage = shift || $config{defaultMaxPasswordAge}; my $shadowLastChange = int($time / 86400); my $modify = $ldap->modify ($dn, changes => [ replace => [shadowLastChange => $shadowLastChange], ] ); $modify->code && die "Failed to modify shadowLastChange: ", $modify->error; if (($< == 0) && ($pass_maxage)) { my $modify = $ldap->modify ($dn, changes => [ replace => [shadowMax => $pass_maxage] ] ); $modify->code && die "Failed to modify shadowMax: ", $modify->error; } } sub nsc_invalidate { my ($dbname) = @_; return unless (defined($config{nscd}) && length($config{nscd})); system("\Q$config{nscd}\E -i \Q$dbname\E 2>/dev/null"); } 1;