#---------------------------------------------------------------------- # Copyright 1999-2003 Mitel Networks Corporation # This program is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. #---------------------------------------------------------------------- package esmith::AccountsDB; use strict; use warnings; use esmith::db; use vars qw( $AUTOLOAD @ISA ); use esmith::DB::db; @ISA = qw(esmith::DB::db); =head1 NAME esmith::AccountsDB - interface to esmith configuration database =head1 SYNOPSIS use esmith::AccountsDB; my $a = esmith::AccountsDB->open; my @users = $a->users(); my @groups = $a->groups(); my @ibays = $a->ibays(); my @printers = $a->printers(); my @pseudonyms = $a->pseudonyms(); $a->is_user_in_group($user, $group); my @groups = $a->user_group_list($user); $a->add_user_to_groups($user, @groups); $a->remove_user_from_groups($user, @groups); $a->create_user_auto_pseudonyms($user); $a->remove_user_auto_pseudonyms($user); $a->remove_all_user_pseudonyms($user); my $dp = $a->dot_pseudonym($user); my $up = $a->underbar_pseudonym($user); my $uid = $a->get_next_uid(); =head1 DESCRIPTION This module provides an abstracted interface to the esmith accounts database. =cut our $VERSION = sprintf '%d.%03d', q$Revision: 1.18 $ =~ /: (\d+).(\d+)/; =head2 open() Loads an existing account database and returns an esmith::AccountsDB object representing it. =begin testing use esmith::TestUtils qw(scratch_copy); use_ok("esmith::AccountsDB"); use esmith::db; use vars qw($a); my $conf = scratch_copy('10e-smith-lib/accounts.conf'); $a = esmith::AccountsDB->open($conf); isa_ok($a, 'esmith::AccountsDB'); is( $a->get("global")->prop('type'), "system", "We can get stuff from the db"); =end testing =cut sub open { my($class, $file) = @_; $file = $file || $ENV{ESMITH_ACCOUNT_DB} || "accounts"; return $class->SUPER::open($file); } =head2 open_ro() Like esmith::DB->open_ro, but if given no $file it will try to open the file in the ESMITH_ACCOUNT_DB environment variable or accounts. =begin testing =end testing =cut sub open_ro { my($class, $file) = @_; $file = $file || $ENV{ESMITH_ACCOUNT_DB} || "accounts"; return $class->SUPER::open_ro($file); } =head2 users(), groups(), ibays(), printers(), pseudonyms() Returns a list of records (esmith::DB::db::Record objects) of the given type. =for testing foreach my $t (qw(users groups pseudonyms)) { my @list = $a->$t(); ok(@list, "Got a list of $t"); isa_ok($list[0], 'esmith::DB::db::Record'); } =cut sub AUTOLOAD { my $self = shift; my ($called_sub_name) = ($AUTOLOAD =~ m/([^:]*)$/); my @types = qw( users groups ibays printers pseudonyms); if (grep /^$called_sub_name$/, @types) { $called_sub_name =~ s/s$//g; # de-pluralize return $self->get_all_by_prop(type => $called_sub_name); } } =head1 GROUP MANAGEMENT =head2 $a->is_user_in_group($user, $group) Returns true if the user is a member of the group, false otherwise. The arguments are a user name and a group name. This routine will return undef if there is no such group, false (but defined) if the user is not in the group, and true if the user is in the group. =for testing ok($a->is_user_in_group('bart', 'simpsons'), "Bart is in group Simpsons"); ok(not($a->is_user_in_group('moe', 'simpsons')), "Moe is not in group Simpsons"); ok(not(defined $a->is_user_in_group('moe', 'flanders')), "No such group as Flanders"); =cut sub is_user_in_group { my ($self, $user, $group) = @_; $group = $self->get($group) || return undef; my $members = $group->prop('Members'); return grep(/^$user$/, split /,/, $members) ? 1 : 0; } =head2 $a->user_group_list($user) Get a list of groups (by name) of which a user is a member. The $user argument is simply the username. =for testing my @groups = $a->user_group_list('bart'); is_deeply(\@groups, ['simpsons'], "Bart's group list is 'simpsons'"); =cut sub user_group_list { my ($self, $user) = @_; my @groups = $self->groups(); my @user_groups; foreach my $g (@groups) { push(@user_groups, $g->key()) if $self->is_user_in_group($user, $g->key()); } return @user_groups; } =head2 $a->add_user_to_groups($user, @groups) Given a list of groups (by name), adds the user to all of them. Doesn't signal the group-modify event, just does the DB work. Note: the method used here is a bit kludgy. It could result in a user being in the same group twice. =for testing my @groups = $a->groups(); $a->remove_user_from_groups('maggie', map { $_->key() } @groups); my @mg = $a->user_group_list('maggie'); is(scalar @mg, 0, "Maggie has been removed from all groups"); $a->add_user_to_groups('maggie', 'simpsons'); @mg = $a->user_group_list('maggie'); is_deeply(\@mg, ['simpsons'], "Maggie has been added to group 'simpsons'"); $a->remove_user_from_groups('maggie', 'simpsons'); @mg = $a->user_group_list('maggie'); is_deeply(\@mg, [], "Maggie's been removed from all groups again"); $a->set_user_groups('maggie', 'simpsons'); @mg = $a->user_group_list('maggie'); is_deeply(\@mg, ['simpsons'], "Maggie's groups have been set to: 'simpsons'"); =cut sub add_user_to_groups { my ($self, $user, @groups) = @_; GROUP: foreach my $group (@groups) { unless (($group) = ($group =~ /(^[\w.-]+$)/)) { warn "Group name doesn't look like a group!\n"; next GROUP; } my $group_rec = $self->get($group) || next GROUP; my @members = split(/,/, $group_rec->prop('Members')); push @members, $user; # Remove duplicates my %members = map { $_ => 1 } @members; $group_rec->set_prop('Members', join(',', sort keys %members)); } } =head2 $a->remove_user_from_groups($user, @groups) Given a list of groups, removes a user from all of them. Doesn't signal the group-modify event, just does the DB work. =cut sub remove_user_from_groups { my ($self, $user, @groups) = @_; GROUP: foreach my $g (@groups) { my $group_rec = $self->get($g) || next GROUP; my $members = $group_rec->prop('Members'); my @members = split (/,/, $members); @members = grep (!/^$user$/, @members); @members = qw(admin) unless @members; # admin *must* be in every group $group_rec->set_prop('Members', join(',', @members)); } } =head2 $a->set_user_groups($user, @groups) Sets the user's groups in one fell swoop. Under the hood, it's removing the user from every group they're in then adding them to the set you give. =cut sub set_user_groups { my ($self, $user, @groups) = @_; my @old_groups = $self->user_groups_list($user); $self->remove_user_from_groups($user, @old_groups); $self->add_user_to_groups($user, @groups); } =head1 PSEUDONYM MANAGEMENT =head2 $a->create_user_auto_pseudonyms($user) Given a user name, creates standard pseudonyms ("dot" and "underbar" style) for that user. =for testing my $user = 'bart'; ok($a->pseudonyms(), "There are pseudonyms in the accounts db"); $a->remove_user_auto_pseudonyms($user); ok(! $a->get('bart.simpson'), "Removed dot-pseudonym"); ok(! $a->get('bart_simpson'), "Removed underbar-pseudonym"); $a->create_user_auto_pseudonyms($user); ok($a->get('bart.simpson'), "Created dot-pseudonym"); ok($a->get('bart_simpson'), "Created underbar-pseudonym"); =cut sub create_user_auto_pseudonyms { my ($self, $user) = @_; my $user_rec = $self->get($user); my $firstName = $user_rec->prop("FirstName"); my $lastName = $user_rec->prop("LastName"); my $dot_pseudonym = dot_pseudonym($self, $user); my $underbar_pseudonym = underbar_pseudonym($self, $user); my $dot_acct = $self->get($dot_pseudonym) || $self->new_record($dot_pseudonym, { type => 'pseudonym', Account => $user} ); my $underbar_acct = $self->get($underbar_pseudonym) || $self->new_record($underbar_pseudonym, { type => 'pseudonym', Account => $user} ); } =head2 $a->remove_all_user_pseudonyms($user) Given a username, remove any pseudonyms related to that user from the accounts database. Also removes any pseudonyms related to a pseudonym being removed. Returns the number of pseudonym records deleted. =cut sub remove_all_user_pseudonyms { my ($self, $user) = @_; my $count = 0; foreach my $p_rec (grep { $_->prop("Account") eq $user } $self->pseudonyms()) { foreach my $p_p_rec (grep { $_->prop("Account") eq $p_rec->key } $self->pseudonyms()) { $p_p_rec->delete; $count++; } $p_rec->delete; $count++; } return $count; } =head2 $a->remove_user_auto_pseudonyms($user) Given a username, remove the dot_pseudonym and underbar_pseudonym related to that user from the accounts database. Returns the number of pseudonym records deleted. =cut sub remove_user_auto_pseudonyms { my ($self, $user) = @_; my $dot_pseudonym = dot_pseudonym($self, $user); my $underbar_pseudonym = underbar_pseudonym($self, $user); my $count = 0; foreach my $p_rec ($self->get($dot_pseudonym), $self->get($underbar_pseudonym)) { if (defined $p_rec && $p_rec->prop("type") eq "pseudonym" && $p_rec->prop("Account") eq $user) { $p_rec->delete; $count++; } } return $count; } =head2 $a->dot_pseudonym($user) Returns the "dot"-style pseudonym for a user as a string. For instance, dot_pseudonym("bart") might return "bart.simpson". =cut sub dot_pseudonym { my ($self, $user) = @_; my $user_rec = $self->get($user); my $firstName = $user_rec->prop("FirstName"); my $lastName = $user_rec->prop("LastName"); my $dot_pseudonym = lc("$firstName $lastName"); $dot_pseudonym =~ s/^\s+//; # Strip leading whitespace $dot_pseudonym =~ s/\s+$//; # Strip trailing whitespace $dot_pseudonym =~ s/\s+/ /g; # Multiple spaces become single spaces $dot_pseudonym =~ s/\s/./g; # Change all spaces to dots return $dot_pseudonym; } =head2 $a->underbar_pseudonym($user) Returns the "underbar"-style pseudonym for a user as a string. For instance, underbar_pseudonym("bart") might return "bart_simpson". =begin testing my @users = $a->users(); my $user = 'bart'; my $rec = $a->get($user); my $firstName = $rec->prop("FirstName"); my $lastName = $rec->prop("LastName"); my $up = $a->underbar_pseudonym($user); is($up, "bart_simpson", "Underbar pseudonym created correctly"); my $dp = $a->dot_pseudonym($user); is($dp, "bart.simpson", "Underbar pseudonym created correctly"); =end testing =cut sub underbar_pseudonym { my ($self, $user) = @_; my $user_rec = $self->get($user); my $firstName = $user_rec->prop("FirstName"); my $lastName = $user_rec->prop("LastName"); my $underbar_pseudonym = lc("$firstName $lastName"); $underbar_pseudonym =~ s/^\s+//; # Strip leading whitespace $underbar_pseudonym =~ s/\s+$//; # Strip trailing whitespace $underbar_pseudonym =~ s/\s+/ /g; # Multiple spaces become single spaces $underbar_pseudonym =~ s/\s/_/g; # Change all spaces to underbars return $underbar_pseudonym; } =head2 $a->activeUsers() Returns the number of active users, ie, accounts which have passwords set and are of type 'user'. =begin testing my $numActiveUsers = scalar $a->activeUsers(); like($numActiveUsers, qr/[0-9]+/, "active users returns a number"); =end testing =cut sub activeUsers() { my $self = shift; my @users = $self->users(); return unless @users; return grep { $_->prop("PasswordSet") eq 'yes' } @users; } =head2 get_next_uid Returns the next available UID from /etc/passwd. All UIDs are above the range reserved for 'system' accounts (currently 5000). =for testing SKIP: { skip "Must be root to run get_next_uid" if $<; my $u = $a->get_next_uid(); ok($u > 5000, "UID should be greater than 5000"); ok(! getpwuid($u), "UID should not yet exist"); } =cut sub get_next_uid { use esmith::ConfigDB; my $id; my $db = esmith::ConfigDB->open || die "Couldn't open config db"; if ($id = $db->get('MinUid')) { $id = $id->value(); } else { $db->new_record('MinUid'); $id = 5000; } my $maxid = 1 << 31; setpwent(); setgrent(); while (getpwuid $id || getgrgid $id) { die "All userids in use" if ($id == $maxid); $id++; } endpwent(); endgrent(); $db->set_value('MinUid', $id + 1); return $id; } =pod =head2 new_record ($key, \%props) This method is overridden from esmith::DB::db. We do an additional check for implicit accounts here - accounts that exist in /etc/passwd but not in the db. Otherwise it behaves just like the superclass method. =begin testing isnt($a->new_record("root", {type=>'system'}), "OK", "can't create existing account"); is($a->get("nobody"), undef, "nobody doesn't exist in db"); isnt($a->new_record("nobody", {type=>'system'}), "OK", "can't create account in /etc/passwd"); isnt($a->new_record("screwy", {type=>'user'}), undef, "created a regular user"); =end testing =cut sub new_record { my ($self, $key, $props) = @_; if(getpwnam($key) || getgrnam($key)) { warn "Attempt to create account '$key' which already exists ", "in passwd"; return undef; } return $self->SUPER::new_record($key, $props); } =pod =head2 validate_account_name ($name) Check $name to see if it is a valid account name. Valid account names start with a letter or number and contain only letters, numbers, underscores, dots and dashes. =begin testing is($a->validate_account_name("root"), "OK", "root is a valid name"); is($a->validate_account_name("fred.frog"), "OK", "fred.frog is a valid name"); is($a->validate_account_name("jane_doe"), "OK", "jane_doe is a valid name"); isnt($a->validate_account_name("^root"), "OK", "^root is not a valid name"); is(esmith::AccountsDB::validate_account_name("root"), "OK", "called as a function"); =end testing =cut sub validate_account_name { my $self; # Were we called as a method or a function? if($#_ > 0) { $self = shift; } my $name = shift; return ($name =~ /[^0-9a-z\-_\.]/ or $name !~ /^[a-z]/) ? undef : 'OK'; } =head1 AUTHOR SME Server Developers See http://www.e-smith.org/ for more information