Skolelinux user administration

 

(Due to unforeseen changes towards the end, there is a bug in the docbook document.)


Table of Contents
1. Architecture
2. The Code
3. Main solution
List of Tables
3-1. "; print <tb> $text{'class'} $text{'name'} $text{'uid'} $text{'type'} $text{'changep'} $text{'editu'} $text{'deleteuser'} EOF sort(@users);
3-2. EOF print "cb>"; print "cb>"; print ""; print "$class"; foreach $entry (@groups){ print "" . $entry->get_value('cn') . ""; } print ""; print ""; $navn= $in{"cn$i"}; print "latin1 . "\">" ; print "$in{\"uid$i\"}"; print ""; foreach $entry (@agroups){ print $entry->get_value('cn') . ""; } print ""; print ""; $b=1; foreach $entry (@groups){ print "" . $entry->get_value('cn'); $b++; } print ""; print ""; $a=1; foreach $entry (@agroups){ print "" . $entry->get_value('cn'); $a++; } print ""; print "cb>$text{'rootpw'}"; print ""; print ""; print " "; print "

Chapter 1. Architecture

Users and groups are seperate entries in the LDAP database. Users are linked to a group by the field gidNumber. Various PERL scripts are called and executed to carry out maintainence and so forth. A user has it's own privategroup, an can be member of other secondarygroups such as schoolcouncil. The users homedirectory is put under one of these secondarygroups, thought of as mainly being the users class in a schoolsense.


Chapter 2. The Code

The required PERL-functions for LDAP communication are all collected in ldap-users.pl, as shown below. This function binds to the LDAP database. It is set to run whenever a cgi script is called.
sub ldap_connect($$$){
    ($server) = @_;
    
    $ldap = Net::LDAP->new($server) || ("Server=$server");
    $ldap->bind();
}
	
These functions are used to retrieve information about spesific user(s). The first returns all users (redundant), the second returns true if user exists and the third returns all info about a given uid.
sub ldap_get_users($){
    my ($basedn) = @_;

    my $mesg = $ldap->search (base   => "ou=People,$basedn",
			      filter => "objectClass=posixAccount"
                              );
    return $mesg->all_entries();
}

sub ldap_get_user($$){
    my ($basedn, $uid) = @_;

    my $mesg = $ldap->search (base   => "ou=People,$basedn",
			      filter => "uid=$uid"
                              );
    return $mesg->entry(0) if ( 1 == $mesg->count() );

    return undef;
}

sub ldap_get_userinfo($$){
    my ($basedn, $uid) = @_;

    my $mesg = $ldap->search (base   => "ou=People,$basedn",
			      filter => "uid=$uid"
                              );
    return $mesg->all_entries();
}
This function returns the uidNumber for a given uid.
sub ldap_get_uidNumber($$){
    ($gid, $basedn) = @_;

    $mesg = $ldap->search (base   => "ou=People,$basedn",
			      filter => "uid=$uid",
			      attrs  => "uidNumber"
			      );
    return "(not in ldap)" if (! $mesg->count());
    $entry = $mesg->entry(0);
    return $entry->get_value('uidNumber');
}
Function that checks for existing groups.
sub ldap_group_exists($$){
    my ($gidNumber, $basedn) = @_;

    my $mesg = $ldap->search (base   => "ou=Group,$basedn",
			      filter => "gidNumber=$gidNumber"
                              );
    return $mesg->count();
}
Function requires a gidNumber as input and returns the assosiated cn (commonname) for this.
sub ldap_get_groupname($$){
    my ($gidNumber, $basedn) = @_;

    my $mesg = $ldap->search (base   => "ou=Group,$basedn",
			      filter => "gidNumber=$gidNumber",
                              attrs  => "cn",
			      );
    return "(not in ldap)" if (! $mesg->count());
    my $entry = $mesg->entry(0);
    return $entry->get_value('cn');
}
Function finds a valid uidNumber for the next user to be created.

sub ldap_get_max_uid($){
    ($basedn) = @_;
    $mesg = $ldap->search (base   => "ou=People,$basedn",
			      filter => "objectClass=posixAccount"
                              );
    $maxval = 10000;
    foreach $entry ($mesg->all_entries()){
	my $val = $entry->get_value('uidNumber');
	$maxval = $val if ($val > $maxval);
    }
    return $maxval++;
}
The function adds one user to the LDAP server. In the cgi script this function is looped for batch creation.
       

sub ldap_add_user($$$$$$$$$$$){
    ($cn, $uid, $userpw, $uidNumber, $gidNumber, 
     $rootpw, $rootdn, $basedn, $rolle, $homedir, $maildir) = @_;

    $ldap->bind($rootdn, password => $rootpw);
    my $entry = Net::LDAP::Entry->new();
    $entry->dn("uid=$uid,ou=People,$basedn");
    $entry->add(
		objectclass => ['posixAccount',
				'imapUser'],
		cn => $cn,
		uid => $uid,
		uidNumber => $uidNumber,
		gidNumber => $gidNumber,
		homeDirectory => $homedir,
		description => $rolle,
		mailMessageStore => $maildir,
		userPassword => "{crypt}". _crypt($userpw),
		loginShell => "/bin/bash",
		);
    
    return $entry->update($ldap);
}
Same as above, but with groups.

sub ldap_add_group($$$$$){
    ($uid, $gidNumber, $rootpw, $rootdn, $basedn) = @_;

    $ldap->bind($rootdn, password => $rootpw);
    my $entry = Net::LDAP::Entry->new();
    $entry->dn("cn=$uid,ou=Group,$basedn");
    $entry->add(
                objectclass => 'posixGroup',
                cn => $uid,
                gidNumber => $gidNumber,
                );

    return $entry->update($ldap);
}
Does what it says. Binds to the ldap server as root with password, then deletes either user or group. Again this is looped in the cgi script.

sub ldap_delete_user($){
    my ($uid, $rootdn, $rootpw, $basedn) = @_;

    $ldap->bind($rootdn, password => $rootpw);
    return $ldap->delete("uid=$uid,ou=People,$basedn");
}

sub ldap_delete_group($){
    my ($cn, $rootdn, $rootpw, $basedn) = @_;

    $ldap->bind($rootdn, password => $rootpw);
    return $ldap->delete("cn=$cn,ou=Group,$basedn");
}
Closes the ldap connection.
sub ldap_close(){
    $ldap->unbind();
}
Function generates encrypted passwords that goes in to the password field in a user entry.

WarningNote that this is not a standard linux encryption, but a one way encryption. This means that the password can not be viewed, only checked against the pwd file for authencity.
sub gen_crypt($){
    my ($plain) = @_;

    srand(time);
    my $salt = chr(int(rand(97)) + 25).chr(int(rand(97)) + 25);
    return crypt($plain, $salt);

}
Function returns all groups on the LDAP server. Used for the dropdown menus.

sub ldap_get_groups($){
    ($basedn) = @_;
    $mesg = $ldap->search (base   => "ou=Group,$basedn",
			      filter => "objectClass=posixGroup"
			     );
    return $mesg->all_entries();
}
Function for calling linux scripts like create/delete homedir. Mainly called from other functions.
sub run_script {
    my ($scriptname, @args) = @_;

    my $done = 0;
    my $retval = 1;

    print "\n";
    for my $scriptdir (@scriptdirs) {
	my $path = "$scriptdir/$scriptname";
	if ( -x $path) {
	    # from system(), 0 means success.  invert before returning it.
	    $retval = ! system($path, @args);
	    $done = 1;
	    last;
	}
    }
    if ( ! $done ) {
	print($text{'error'}.": Unable to find any executable ".
	      "script $scriptname in ", join(" ", @scriptdirs), ".\n");
	$retval = undef undef;
    }
    print "\n";
    return $retval;
}
Does what it says.

sub create_dir($$$){
    my ($homedir, $uidNumber, $gidNumber) = @_;

    return undef unless $uidNumber;

    return run_script("createhomedir", $homedir, $uidNumber, $gidNumber);
}

sub delete_dir($){
    my ($homedir) = @_;

    return undef unless $homedir;

    return run_script("removehomedir", $homedir);
}
Checks if an uid only contains valid characters.

sub valid_uid($){
    my ($uid) = @_;

    return 0 unless $uid;
    return $uid =~ m/^[a-z][a-z0-9]*$/;
}
Function that prints errors reported by the ldap database.
sub print_ldap_error($$) {
    my ($errormsg, $result) = @_;
    print "". $errormsg ."\n";
    print $text{'error'}.": \n";
    print "";
    print Dumper($result->error());
    print "";
}
Translates gid to gidNumber
sub ldap_get_gidNumber($$){
    ($gid, $basedn) = @_;

    $mesg = $ldap->search (base   => "ou=Group,$basedn",
			      filter => "cn=$gid",
			      attrs  => "gidNumber"
			      );
    return "(not in ldap)" if (! $mesg->count());
    $entry = $mesg->entry(0);
    return $entry->get_value('gidNumber');
}
Searches the ldap server for people with a given gidNumber.
sub ldap_search_gidNum($){
($gidNumber) = @_;
    $mesg = $ldap->search (base   => "ou=People,$basedn",
			      filter => "gidNumber=$gidNumber"
                              );
    return $mesg->all_entries();

}
Main function for admin.cgi. Does searches on the ldap server for firstname, surname and group. Searching in spesific groups is not implemented.
sub ldap_searcher($$){
($gidNumber, $cn) = @_;
	            
		    if(!$cn){
                    $mesg = $ldap->search (base   => "ou=People,$basedn",
			      filter => "gidNumber=$gidNumber"
                              );
                    return $mesg->all_entries();
		    }
		    if($gidNumber eq '(not in ldap)'){
                     $mesg = $ldap->search (base   => "ou=People,$basedn",
			      filter => "cn=$cn"
                              );
                     return $mesg->all_entries();
		    }
		    else{
                    $mesg = $ldap->search (base   => "cn=$cn,ou=People,$basedn",
                              filter => "gidNumber=$gidNumber"
			      );
                    return $mesg->all_entries();
		    }
}
This function makes the uid. Uids consist of up to ten letters of the firstname + some letters of the surname if another user has an equal firstname. $f = substr($f, 0, 10); Change this to set how many letters of the firstname you want to use. $surname_part = substr($s, 0, $i); Change i to a constant number like 3 for $f + $s = $uid. The loop checks if the uid is unique and adds letters until it is.

sub make_uid($$){
($f, $s) = @_;
$f = ($f);
$s = ($s);
$i = 1;
$nr = 0;
$f =~ tr/ //d;
$f =~ tr/-//d;
$f = substr($f, 0, 10);
$uid = $f;

$entry = _get_user($config{'basedn'} , $uid);

while ( defined $entry ){	

$surname_part = substr($s, 0, $i);
$uid = lc("$f"."$surname_part");
	
	if("$surname_part" eq "$s"){
		$nr++;
		if($nr > 0){
			chop($uid);
		}
	
		$uid = lc("$f"."$surname_part"."$nr");
	}
$entry = _get_user($config{'basedn'} , $uid);
$i++;
}	
		
if("$in{'fpass'}" eq "no" || "$in{'fepass'}" eq "no"){

$userpw = $uid;

}

if("$in{'fpass'}" eq "yes" || "$in{'fepass'}" eq "yes"){
$userpw = $in{'kpwd'};
}
Function that removes æøåÆØÅ from first\surnames. æÆåÅ is replaced by a and øØ by o.
sub lettertrans($){
    ($l) = @_;  
$line = lc("$l");
    $line =~ s/\346/\141/g;
    $line =~ s/\370/\o/g;
    $line =~ s/\345/\141/g;
    $line =~ s/\306/\141/g;
    $line =~ s/\330/\o/g;
    $line =~ s/\305/\141/g;

   return $line;
}


}
Creates homedir.
sub min_create_dir($$$){
    my ($homedir, $uidNumber, $gidNumber) = @_;

    return undef unless $gidNumber;

    return run_script("createhomedir", $homedir, $uidNumber, $gidNumber);
}
Does what it says. Helps generating unique gidNumbers
sub ldap_get_max_gid($){
    ($basedn) = @_;
    $mesg = $ldap->search (base   => "ou=Group,$basedn",
			      filter => "objectClass=posixGroup"
                              );
    $maxval = 10000;
    foreach $entry ($mesg->all_entries()){
	my $val = $entry->get_value('gidNumber');
	$maxval = $val if ($val > $maxval);
    }
    return $maxval++;
}
Translates from gid to gidNumber.
sub ldap_get_gidNumber($$){
    my ($gid, $basedn) = @_;

    my $mesg = $ldap->search (base   => "ou=Group,$basedn",
			      filter => "cn=$gid",
			      attrs  => "gidNumber"
			      );
    return "(not in ldap)" if (! $mesg->count());
    my $entry = $mesg->entry(0);
    return $entry->get_value('gidNumber');
}
Adds group to ldap database.
sub min_add_group($$$$$){
    ($gid, $gidNumber,  $rootpw, $rootdn, $basedn) = @_;

    $ldap->bind($rootdn, password => $rootpw);
    $entry = Net::LDAP::Entry->new();
    $entry->dn("cn=$gid,ou=Group,$basedn");
    $entry->add(
                objectclass => 'posixGroup',
                cn => $gid,
                gidNumber => $gidNumber,
              
		);
}
Adds members of a groups uids to the multifield member uids. Experimental, and the LDAPdeletegrpmemberid does not work.

sub LDAPmodify($)
      {
      ($dn) = @_ ;
      
      $mesg = $ldap->bind($config{'rootdn'}, password => "root");
      
      $result = $ldap->modify($dn,
                             replace => { cn => $in{'cn'}, gidNumber => $gidNumber }
			     		  
                               );
      return ($result );
      }
      
sub LDAPupdate($)
      {
      ($dn) = @_ ;
      
      $mesg = $ldap->bind($config{'rootdn'}, password => "root");
      
      $result = $ldap->modify($dn,
                              add => {memberUid => $uid,}
			     		  
                               );
      return ($result );
      }
      
sub LDAPdeletegrpmemberid($)
      {
      ($dn) = @_ ;
      
      $mesg = $ldap->bind($config{'rootdn'}, password => "root");
      
      $result = $ldap->modify($dn,
                              remove => {memberUid => $uid}
			     		  
                               );
      return ($result );
      }
To change your of password you bind to the ldap server as yourself. Your password is the only field you have write access too.

sub change_pass{

    ($user, $cur, $new1, $new2) = (
                    $in{'user'},
                    $in{'cur'},
                    $in{'new1'},
                    $in{'new2'},
                    );
    $user =~ s@^\s+|\s+$@@g;
    $dn = "uid=$user,ou=People,$config{'basedn'}";
    my(@err);
    if ($user eq "") {
        push @err, "You must enter your username";
       
    } elsif ($user =~ /\@/) {
        push @err, "You must enter your username.
It looks like you entered your email address (ie $user).";
       
    } elsif ($cur eq "") {
        push @err, "You must enter your current password.",
       
    } elsif ($new1 eq "" || $new2 eq "") {
        push @err, "You must enter your new password twice.",
       
    } elsif ($new2 ne $new1) {
        push @err, "The two new passwords you entered do not match.",
       
    }
   
    if (@err) {
        die("The following errors occurred:", @err);
       
    }
    $mesg = $ldap->bind($dn, password => $cur) or die "$@";

    die("Unable to bind. Your current password was probably incorrect",
$mesg->code, $mesg->error) if $mesg->code;
   
    my $ctoa =
"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    my @ctoa = split(//, $ctoa);
    my $t = srand( time() ^ ($$ + ($$ << 15)) );
    my $salt = $ctoa[$q % 64] . $ctoa[($t/64)%64];
    my $passwd = crypt($new1, $salt);
   
    $mesg = $ldap->modify( $dn,
replace=>{'userPassword' => "{crypt}$passwd"} );
   
    die("Unable to modify ",$mesg->code, $mesg->error) if $mesg->code;
   
print "Du kan nå logge inn med ditt nye passord.";
   
}


Chapter 3. Main solution

Here we will take a look at the cgi scripts and their usage of the functions in the .pl files. The header declares what objects to use, perl-ldap is used extensively.
	
#!/usr/bin/perl

use Net::LDAP qw(:all);
use Data::Dumper;
use Unicode::String qw (utf8 latin1);

require '/usr/share/webmin/web-lib.pl';
require "ldap-users.pl";
use vars qw(%text %config %in $tb $cb);

@scriptdirs = qw(/etc/webmin/ldap-users /usr/share/webmin/ldap-users .);
These are webmin spesific functions that are required for the script to run, ldap_connect to start an ldap session and the almighty readparse, which is nearly the heart of the entire system.

_config();
( $text{'title1'} ,"", undef, 1);
();
_connect($config{'server'}, $config{'rootdn'});
Retrieves the groups from the ldapserver for use in the menus +++. These groups are distinguished from the users private group by "description=Class" rather than "..=user". Generic counters are also initialized, primarily for counting lines.
@groups = _get_groups($config{'basedn'});
$c = $in{'counter'};
$i=1;
Typical listing, the "get_value" determines what to get from the @groups hash. The $entry is one "row" from the @groups hash
    --
EOF
foreach $entry (@groups){
print "" . $entry->get_value('cn') .  "";
}
print "";
print "";
For constructing the searchstring that in the end is put into the ldap_searcher function. Various checks to ensure nothing goes wrong..

if("$in{'Søk'}" ne ""){
  if($in{'surname'} ne "" && $in{'firstname'} eq ""){
    $cn =  ("*" . $in{'surname'}  . "*");
    print "Søker etter: " . $cn;
  }
  if($in{'firstname'} ne "" && $in{'surname'} eq ""){
    $cn =  ("*" . $in{'firstname'}  . "*");
    print "Søker etter: " . $cn;
  } 
  if($in{'firstname'} ne "" && $in{'surname'} ne ""){
    $cn = ("*" . $in{'surname'}  . "*" . $in{'firstname'} . "*");
    print "Søker etter: " . $cn;
  }
  if($in{'surname'} eq "" && $in{'firstname'} eq "" && $in{'kull'} eq '--'){
    $cn =  "*";
    print "Søker etter: " . $cn;
  }
 if($in{'kull'} eq '--'){
 $gid = "*" ;
 }
 else{
 $gid = $in{"kull"};
 }
@users = _searcher($gid, latin1($cn)->utf8);
Here we want to print out the search results. Ldapsearcher returns a list of relevant uid's based on the groups memberUid field. So we search for eachone of them.
print "";
print "$text{'users'}";
print "
If you search for a cn, (ie. $gid = *) a hash is returned, so you have to retrieve the uid by doing this:
foreach $field (@users){
    if($gid eq "*"){
    @usr = _get_users($config{'basedn'},$field->get_value('uid'));
    }
    else{
    @usr = _get_users($config{'basedn'},$field);
    }
[1]
With all the data collected, this is how we print! The counters keeping tracks of the entries.
    print "cb>\n";
    print "" . $class. "\n";
    print "" . utf8($user->get_value('cn'))->latin1 . "get_value('cn') . "\">\n" ;
    print "" . $user->get_value('uid') . "\n" ;
    print "" . utf8($user->get_value('description'))->latin1 . "\n" ;
    print "get_value('uid') . ">$text{'changep'}?\n" ;
    print "\n";
    print "\n";
    }
  $i++;
  $c++;
  }
 
    print <cb>
  
    
  
   $text{'rootpw'}
  
   
  
   
  
    EOF

    }
Then over to the various maintenance funcitons. When pushing the edit button, that user is "transerred" to another screen. The @agroups is all available "class groups", the @group is the groups a user is already a member of. The user can be added to as many groups that you like.
$i=1;
    for$i(1..$c){
    if(defined $in{"edituser$i"}){
    $entry = ldap_get_user($config{'basedn'}, $in{"uid$i"});
    @groups = _get_groups($config{'basedn'});
    @agroups = _get_class($config{'basedn'},$in{"uid$i"});
    $class = $in{"kull$i"};

    print <";
    print "";
    }
Here is mechanics for deleting users. Mainly a loop around the various delete functions. The is there to clean up any groupmemberships the user might have had.

if(defined $in{"deleteUser$i"}){
    $entry = ldap_get_user($config{'basedn'}, $in{"uid$i"});
    	if (! defined $entry) {
    	    # error, user have no home directory
    	    print "Error: Unable to find user in LDAP database\n";
    	    return;
    	}
    	$homedir = $entry->get_value('homeDirectory');

    	
    	$result = _delete_user($in{"uid$i"}, $config{'rootdn'},
    				       $in{'rootpw'}, $config{'basedn'});
    	if ($result->code){
    	    print_ldap_error(text("delfailed", $uid), $result);
    	}
    	else {
    	    print "". text("deleted", $in{"uid$i"}) ."\n";

    		$result = _dir($homedir);
    		if ($result) {
    		    print "".text("delHomedirSuccess",$homedir)."\n";
    		}
    		else {
    		    print "". text("delHomedirFailed",$homedir) ."\n";
    		}
    	_delete_group($in{"uid$i"}, $config{'rootdn'},
    				       $in{'rootpw'}, $config{'basedn'});
    	}
    @agroups = _get_class($config{'basedn'},$in{"uid$i"});
    $uid = $in{"uid$i"};
    foreach $entry(@agroups){
    	$gid=$entry->get_value('cn');
    	$bn = "cn=$gid,ou=Group,$config{'basedn'}";
    	$result = ($bn);
    }

    $i++;
    }
    }
This is the mechanics behind the edituser dialog. First the users home folder is moved to a new location if desired, then any new group memberships are adde, then follows the deletion of any group memberships and finally entries edited, like cn and homeDirectory is put into ldap

if($in{"button"} eq "$text{'update'}"){
    $rootpw = $in{'rootpw'};
    _connect($config{'server'}, $config{'rootdn'});
    $c = $in{'number'};
    $gidNumber = _get_gidNumber($in{"kull"}, $config{'basedn'});
    $uid = $in{'uid'};
    $cn = $navn;
    $gruppeNr = _get_gidNumber($in{"grupper"}, $config{'basedn'});
    $homedir = $config{'homeprefix'}. "/". $in{"kull"} . "/" . $uid;
    $maildir = $config{'mailprefix'}. "/". $in{"kull"} . "/" . $uid;
    $prev = $config{'homeprefix'}. "/". $in{'old'} . "/". $uid;
    $result = _dir($prev, $homedir);
    $i=1;
    	for $i(1..$c){
    	$gid = $in{"gruppe$i"};
    	$gn = "cn=$gid, ou=Group, $config{'basedn'}" ;
    	$result = LDAPupdate($gn);
    	$i++;
    	}
    $i=1;
    for $i(1..$c){
    	$gid=$in{"delgruppe$i"};
    	$gn = "cn=$gid, ou=Group, $config{'basedn'}" ;
    	$result = LDAPdeletegrpmemberid($gn);
    	$i++;
    }
    $gid = $in{"kull"} ;
    $gn = "cn=$gid, ou=Group, $config{'basedn'}" ;
    LDAPupdate($gn);
    $dn = "uid=$uid, ou=People, $config{'basedn'}";
    $result = LDAPmodify($dn);
    print "Bruker informasjon oppdatert.";
    }
    print <EOF

Notes

[1]
To determine which of the groups that also contain your homedir, a search checks the users homeDirectory attribute.
        _check_mainsec reads "check which secondary group that the user has their homedirectory in." Phew...
foreach $user(@usr){ foreach $entry (@groups){ $homedir = $config{'homeprefix'}. "/". $entry->get_value('cn') . "/" . $user->get_value('uid'); $result = _check_mainsec($config{'basedn'}, $homedir); if($result){ $class = $entry->get_value('cn'); } }