Skip to content

Instantly share code, notes, and snippets.

@nxadm
Created May 15, 2019 17:45
Show Gist options
  • Save nxadm/84ff14675e4dbb2b97b3354e627cf5eb to your computer and use it in GitHub Desktop.
Save nxadm/84ff14675e4dbb2b97b3354e627cf5eb to your computer and use it in GitHub Desktop.
#!/usr/bin/env perl
# Update pwdChangedTime to match sambaPwdLastSet which is provided by by IAM.
# Bugs to [email protected].
# CentOS 7 dependencies:
# - perl-LDAP
# - perl-TermReadKey
# Debian/Ubuntu dependencies:
# - libnet-ldap-perl
# - libterm-readkey-perl
our $VERSION = '0.2.0';
use strict;
use warnings;
use feature 'say';
use Getopt::Long;
use POSIX qw/strftime/;
use Net::LDAP;
use Net::LDAP::Control::Relax;
use Term::ReadKey;
### Variables ###
my ($uri, $binddn, $password, $prompt, $dryrun, $help, $ldif_in);
### CLI interface ###
GetOptions( 'H=s' => \$uri, 'D=s' => \$binddn, 'w=s' => \$password,
'W' => \$prompt, 'dryrun' => \$dryrun, 'help' => \$help)
or exit_with_msgs('Error in command line arguments (try "--help").');
if ($help) { help() and exit 0 }
my @missing;
push @missing, 'Missing uri' unless defined $uri;
push @missing, 'Missing binddn' unless defined $binddn;
if (scalar @missing) { exit_with_msgs(@missing) }
if (scalar @ARGV > 1) { exit_with_msgs("Too many parameters") }
if (scalar @ARGV == 0) { exit_with_msgs("No LDIF file supplied") }
$ldif_in = $ARGV[0];
if (!defined $password || $prompt) {
$password = prompt_for_password() unless $dryrun;
}
### Main ###
my $changes = find_changes();
exit 0 if $dryrun;
ldap_modify($changes);
say "Done.";
exit 0;
### Subroutines ###
sub check_ldap_error {
my ($msg, $action) = @_;
if ($msg->code()) {
say STDERR "LDAP ERROR ($action): " . $msg->error();
exit 2;
}
}
sub exit_with_msgs {
say STDERR "ERROR: $_" for @_ ;
exit 1;
}
sub find_changes {
local $/ = "";
open(my $fh_in, '<', $ldif_in) or die($!);
my @changes;
my $counter = 0;
while(my $record = <$fh_in>) {
next if $record =~ /^\s*$/;
if ($record =~ /^dn::/) {
say STDERR 'base64 DN found (to be implemented)';
exit 1;
}
my ($dn, $pwdChangedTime, $sambaPwdLastSet);
$dn = $1 if $record =~ /^dn:\s+(.+?)\n/;
next unless defined $dn;
$pwdChangedTime = $1 if $record =~ /\bpwdChangedTime:\s+(.+?)\n/;
$sambaPwdLastSet = $1 if $record =~ /\bsambaPwdLastSet:\s+(.+?)\n/;
if (defined $pwdChangedTime && defined $sambaPwdLastSet) {
my $sambachangedtime = strftime "%Y%m%d%H%M%SZ", gmtime($sambaPwdLastSet);
if ($sambachangedtime ne $pwdChangedTime) {
$counter++;
say "Mismatch: $dn, converted sambaPwdLastSet $sambachangedtime, pwdChangedTime $pwdChangedTime";
push @changes, [ $dn, $sambachangedtime ]
}
}
}
say "Number of entries to be updated: $counter" if $dryrun;
return \@changes;
}
sub help {
say 'sync-pwdChangedTime-sambaPwdLastSet: ' .
'Update pwdChangedTime to match sambaPwdLastSet (changed by IAM),' .
"version $VERSION.";
say 'Usage:';
say "sync-pwdChangedTime-sambaPwdLastSet <ldif backup file> [options]";
say "sync-pwdChangedTime-sambaPwdLastSet -h";
say 'Options:';
say "\t-H URI LDAP Uniform Resource Identifier";
say "\t-D bindnd bind DN";
say "\t-w passwd bind password";
say "\t-W passwd prompt for bind password";
say "\t--help this help info";
}
sub ldap_modify {
my $changes = shift;
my $ldap = Net::LDAP->new($uri);
say "LDAP ERROR (URI): $!" and exit 2 unless defined $ldap;
my $msg = $ldap->bind($binddn, password => $password);
check_ldap_error($msg, 'BIND');
my $relax = Net::LDAP::Control::Relax->new();
for my $data ( @{ $changes } ) {
my $dn = $data->[0];
my $time = $data->[1];
say "LDAP: working on $dn...";
$msg = $ldap->modify(
$dn,
replace => { pwdChangedTime => $time },
control => $relax,
);
check_ldap_error($msg, 'MODIFY');
}
$ldap->unbind() or warn($!);
}
sub prompt_for_password {
Term::ReadKey::ReadMode('noecho');
print "Password: ";
my $input = Term::ReadKey::ReadLine(0);
Term::ReadKey::ReadMode('restore');
print "\n";
$input =~ s/\R\z//; # remove EOL
return $input;
}
@Altai-man
Copy link

I think you can write it with cro-ldap using:

use Cro::LDAP::Control;
use Cro::LDAP::Client;

sub ldap-modify(Hash @changes) { # array of hashes
    my $ldap = Cro::LDAP::Client.connect($uri);
    my $bind = await $ldap.bind($binddn, :$password);
    $*ERR.say("LDAP ERROR (bind): $bind.msg()") if $bind.status;
    for @changes -> $change {
        say "LDAP: working on $change<dn>";
        my $modify = await $ldap.modify(
            $change<dn>,
            replace => { :type<pwdChangedTime>, :vals([$change<time>.Str]) },
            controls => Cro::LDAP::Control::Relax.new);
        $*ERR.say("LDAP ERROR (modify): $modify.msg()") if $modify.status;
    }
    await $ldap.unbind;
}

I have nothing to run it against, but such syntax should work, at the very least. I don't recommend to run it against production server ( ;) ), but hope it helps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment