The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package Passwd::Samba;

use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);

use warnings;
use strict;

use Crypt::SmbHash;
use File::Spec;
#======================================================================
$VERSION = '0.142';
@ISA = qw(Exporter);
@EXPORT_OK = qw(del uid maxuid passwd rename user users smbpasswd_file);
#======================================================================
use constant PASSWD => '/etc/samba/smbpasswd';
#======================================================================
my $Self = __PACKAGE__->new();
#======================================================================
sub new {
	my ($class, %params) = @_;

	return bless {
		smbpasswd => (defined $params{smbpasswd} ? $params{smbpasswd} : PASSWD),
	}, $class;
}
#======================================================================
sub smbpasswd_file { 
	my $self = scalar @_ && ref $_[0] eq __PACKAGE__ ? shift : $Self;
	my ($val) = @_;
	return $self->{smbpasswd} unless defined $val;
	$self->{smbpasswd} = File::Spec->canonpath($val);
	return $self->{smbpasswd};
}
#======================================================================
sub del {
	my $self = scalar @_ && ref $_[0] eq __PACKAGE__ ? shift : $Self;
	
	open(my $fh, '<', $self->smbpasswd_file);
	my @a;
	while(<$fh>){
		push @a,$_ if /^[^:]+:/o;
	}
	close($fh);
	
	my $re = '^'.join('$|^', @_).'$';
	$re = qr/$re/;
	
	open($fh, '>', $self->smbpasswd_file);
	print $fh grep { (split(/:/,$_))[0] !~ $re } @a;
	close($fh);
	
	return;
}
#======================================================================
sub rename {
	my $self = scalar @_ && ref $_[0] eq __PACKAGE__ ? shift : $Self;
	return unless defined $_[1];
	
	open(my $fh, '<', $self->smbpasswd_file);
	my @a;
	while(<$fh>){
		push @a,$_ if /^[^:]+:/o;
	}
	close($fh);
	# jesli taki uzytkownik juz istnieje
	return if grep { /^$_[1]:/ } @a;
	
	@a = map { s/^$_[0]:/$_[1]:/;$_ } @a;
	
	open($fh, '>', $self->smbpasswd_file);
	print $fh @a;
	close($fh);
	
	return 1;
}
#======================================================================
sub uid {
	my $self = scalar @_ && ref $_[0] eq __PACKAGE__ ? shift : $Self;
	
	open(my $fh, '<', $self->smbpasswd_file);
	if(not defined $_[1]){
		while(<$fh>){
			return (split(/:/,$_))[1] if /^$_[0]:/;
		}
	}else{
		my @a;
		while(<$fh>){
			if(/^[^:]+:/o){
				if(/^$_[0]:/){
					my @tmp = split(/:/,$_);
					$tmp[1] = $_[1];
					push @a, join(':', @tmp);
				}else{ push @a, $_; }
			}
		}
		close($fh);
		open($fh, '>', $self->smbpasswd_file);
		print $fh @a;
	}
	close($fh);

	return 1;
}
#======================================================================
sub maxuid {
	my $self = scalar @_ && ref $_[0] eq __PACKAGE__ ? shift : $Self;

	my $max = 0;
	open(my $fh, '<', $self->smbpasswd_file);
	while(<$fh>){
		my $tmp = (split(/:/,$_))[1];
		$max = $tmp > $max ? $tmp : $max;
	}
	close($fh);
	return $max;
}
#======================================================================
*user = \&passwd;
#======================================================================
sub passwd {
	my $self = scalar @_ && ref $_[0] eq __PACKAGE__ ? shift : $Self;
	
	my ($name, $passwd) = @_;
	return unless defined $passwd;
	my $uid = (getpwnam($name))[2];
	my ($lm, $nt);
	ntlmgen $passwd, $lm, $nt;
	__PACKAGE__->del($name);

	open(my $fh, '>>', $self->smbpasswd_file);
	printf $fh "%s:%d:%s:%s:[%-11s]:LCT-%08X\n", $name, $uid, $lm, $nt, "U", time;
	close($fh);

	return 1;
}
#======================================================================
sub users {
	my $self = scalar @_ && ref $_[0] eq __PACKAGE__ ? shift : $Self;

	my @a;
	open(my $fh, '<', $self->smbpasswd_file);
	push @a, (split(/:/,$_))[0] while <$fh>;
	close($fh);
	return @a;
}
#======================================================================
1;


=head1 NAME

Passwd::Samba


=head1 SYNOPSIS

	use Passwd::Samba;
	
	my $ps = Passwd::Samba->new();
	my $err = $ps->passwd("example", "_plain_text_secret_" );
	foreach my $user ($ps->users) {
		print "Username: $user\nUID: ", $ps->uid($user), "\n\n";
	}
	my $uid = $ps->uid('example');
	$ps->rename('example', 'new_example');
	$pu->del('new_example');

	# or 

	use Passwd::Samba qw(del uid maxuid passwd rename users);

	my $err = passwd("example", "_plain_text_secret_" );
	foreach my $user (users()) {
		print "Username: $user\nUID: ", uid($user), "\n\n";
	}
	my $uid = uid('example');
	rename('example', 'new_example');
	del('new_example');

=head1 DESCRIPTION

The Passwd::Samba module provides an abstract interface to /etc/samba/smbpasswd format files. It is inspired by Unix::PasswdFile module.

=head1 SUBROUTINES/METHODS

=over 4

=item B<new( )>

Constructor. Possible parameters are:

=over 8

=item B<smbpasswd> - path to smbpasswd file; default C</etc/samba/smbpasswd>

=back

=item B<del( USERNAME0, USERNAME1... )>

This method will delete the list of users. It has no effect if the supplied user does not exist.

=item B<maxuid( )>

This method returns the maximum UID in use by all users. 

=item B<passwd( USERNAME, PASSWD )>

Modify a user's password. Returns the result of operation (TRUE or FALSE).

=item B<rename( OLDNAME, NEWNAME )>

This method changes the username for a user. If NEWNAME corresponds to an existing user, that user will be overwritten. It returns FALSE on failure and TRUE on success.

=item B<uid( USERNAME [,UID] )>

Read or modify a user's UID. Returns the result of operation (TRUE or FALSE) if UID was specified otherwhise returns the UID.

=item B<user()>

This alias for passwd. User is created if doesn't exists. 

=item B<users()>

This method returns a list of all existing usernames. 

=item B<smbpasswd_file([PATH])>

This method, if called with an argument, sets path to the I<smbpasswd> file.
Otherwise returns the current PATH.

=back

=head1 DEPENDENCIES

=over 4

=item Crypt::SmbHash

=item Exporter

=back

=head1 INCOMPATIBILITIES

None known.

=head1 BUGS AND LIMITATIONS

None known.

=head1 THANKS

=over 4

=item Thanks to Andy Gorman for suggestions as well as supplying relevant patch! 

=back

=head1 AUTHOR

Strzelecki Ɓukasz <lukasz@strzeleccy.eu>

=head1 LICENCE AND COPYRIGHT

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

See http://www.perl.com/perl/misc/Artistic.html