\documentclass{ictlab}

\RCS $Revision: 1.3 $

\usepackage{alltt,xr,url}
\usepackage[breaklinks,pdfpagemode=None,pdfauthor={Nick Urbanik}]{hyperref}
\externaldocument[gr-]{../../../ossi/lab/grub/grub}

\newcommand*{\labTitle}{Assignment 3: Creating bulk \POSIX accounts in
  an LDAP  Directory}

\begin{Solutions}%
  \gdef\solution{\paragraph{Solution:}}%
\end{Solutions}

\providecommand*{\MD}{\acro{MD}\xspace}

\begin{document}

\paragraph{Submission:}

by 8pm, Sunday, 23 May 2004

\paragraph{Where:}

Online at \url{http://nicku.org/perl2/submit.cgi}.  A
paper submission is not required (save the trees!)

\paragraph{Important:}

All programs you write must start with these lines:
\begin{verbatim}
#! /usr/bin/perl

use warnings;
use strict;
\end{verbatim}
and should compile without errors.


\paragraph{Cheating:}

Your work \emph{must} be original.  Copying will be \emph{severely}
dealt with.

\section{Background}
\label{sec:background}

This assignment requires the use of the \texttt{Net::LDAP} Perl
module.  There is plenty of documentation that comes with the
software.

I have made a unique ``student registration system file'' for each
student in the class.  Please make sure that you use the correct one
as your input data, or you will receive \emph{zero marks} for the
output from your program.

You will create several hundred \LDAP \POSIX accounts from this
student registration system file.

Feel free to reuse the code you wrote for your first Perl account
creation assignment, since the format of the data has not changed
significantly.

You may wish to use the standard \texttt{Getopt::Long} Perl module as
a way to pass options to your software.  See \texttt{perldoc
  Getopt::Long}.


%% \subsection{Reading command line parameters}
%% \label{sec:cammand-line-parameters}

%% There is a builtin array \texttt{@ARGV} in Perl that holds the
%% \emph{command line arguments}, or \emph{command line parameters}.

%% Try this program to see what happens:
%% \begin{verbatim}
%% #! /usr/bin/perl -w
%% use strict;
%% for ( my $i = 0; $i < @ARGV; ++$i )
%% {
%%     print "argument $i is $ARGV[ $i ]\n";
%% }
%% \end{verbatim}
%% If the program was called \texttt{argv.pl}, you could run it like this:
%% \begin{verbatim}
%% argv.pl This is the command line.  "Some more on the command line."
%% \end{verbatim}


\section*{Assignment Requirements}
\label{sec:requirements}

\begin{enumerate}
\item Write a program that can read the data provided at
  \texttt{http://\allowbreak{}ictlab.\allowbreak{}tyict.\allowbreak{}vtc.\allowbreak{}edu.\allowbreak{}hk/\allowbreak{}snm/\allowbreak{}assignments/\allowbreak{}assignment-perl-ldap/\allowbreak{}data/\allowbreak{}artificial-\allowbreak{}student-\allowbreak{}data-\allowbreak{}\meta{Your
  student ID}\meta{Your
.txt}},
  where \meta{Your student ID} is your nine-digit student number.

  Your program will generate \LDIF to create a \POSIX \LDAP account for each user, and create a
  single private user group for each user.  Make each user a primary member of
  their own private user group.

\item As an option, your program should be able to generate accounts
  directly on the directory server.  In this case, your software
  should check whether the account or group entry exists before
  creating it, and should inform the user about all error conditions.

  
  \paragraph{Note:}
  
  I no longer recommend that you use the built in Perl functions
  \texttt{getpwent} and \texttt{getgrent} to determine the next
  available unused \texttt{uidNumber} and \texttt{gidNumber}.  I
  suggest that instead you either:
  \begin{itemize}
  \item simply start the \texttt{uidNumber} and \texttt{gidNumber}
    from 2000 if your directory has no accounts, since these numbers
    are only relevant in your own directory.
  \item[OR:]
  \item use the method that I outlined in an email I sent you earlier,
    where you search for the highest \texttt{uidNumber} and
    \texttt{gidNumber} values in your directory, and start with a
    number 1 greater than that, or with 2000 if there are no accounts
    in your directory.
  \end{itemize}

\item The root of your directory shall be \texttt{ou=\meta{Your
      Student ID},o=ICT}.  The user accounts shall be under
  \texttt{ou=People,ou=\meta{Your Student ID},o=ICT}.  The groups
  shall be under \texttt{ou=Group,ou=\meta{Your Student ID},o=ICT}.

\item Each user account entry shall be a member of the following
  object classes: \texttt{posixAccount}, \texttt{shadowAccount} and
  any suitable \texttt{structural} class such as
  \texttt{inetOrgPerson}.

\item Each group entry should be a member of the \texttt{posixGroup}
  object class, and should contain an attribute \texttt{cn} which is
  equal to the user \ID for whom this is a private group.  It should
  also contain an attribute \texttt{gidNumber} with a unique integer
  number greater than 1999 that has the same value as the
  \texttt{gidNumber} attribute that is in the corresponding user entry.

\item Each user account should have an attribute \texttt{loginShell}
  with the default value \texttt{/bin/bash}, and an attribute
  \texttt{homeDirectory} with the value \texttt{/home/\meta{user
  name}}.  Each user entry should also have a \texttt{gidNumber}
  attribute (as mentioned above), and a \texttt{uidNumber} attribute.
  Both the \texttt{uidNumber} and \texttt{gidNumber} attributes should
  have unique values over 1999.
  
\item The program should also provide an option to generate \LDIF to delete the
  accounts read from the file(s) in the input.
  
\item The program does not need to create a home directory for each user.

\item The username should be the student number of the student and
  shall be stored in the \texttt{uid} attribute.
  \begin{explanation}
    For example, for a student with the name ``YEUNG, Hoi Man,'' and the
    student number 915367894, the user name that the person logs in
    with is \texttt{915367894}.
  \end{explanation}
\item The password shall be their Hong Kong \ID, with letters in
  \emph{lower} case, but \texttt{not} including the parentheses or the
  check digit.  So for a student with the Kong Kong \ID A456789(A),
  their password should be \texttt{a456789}.  This means that there
  will be seven characters in each password.

  You are to create a \texttt{userPassword} attribute in each account,
  putting into it the text which is the \MD{}5 hash of the password.
  You can create this using either the \texttt{unix\_md5\_crypt()}
  method of the \texttt{Crypt::PasswdMD5} module you can install from
  \CPAN, or use the \texttt{hash\_md5\_password()} function you used in
  your previous assignment.

  The \texttt{userPassword} attribute should begin with the string
  \texttt{\{crypt\}}.  So for example, if a student number is
  A123456(3), then the output of:
\begin{verbatim}
use Crypt::PasswdMD5;
our $clear_passwd = shift || 'a123456';
our $hashed_password = unix_md5_crypt $clear_passwd;
print "{crypt}$hashed_password\n";
\end{verbatim}
may look like this:
\begin{alltt}
 \textbf{$ ./make-password.pl}
\{crypt\}$1$xM8WAHwg$qwA3/nAhJRSSQ75qM6IdT0
\end{alltt}
and be suitable as a value for the \texttt{userPassword} attribute.

\item You should include the full name of each student in their \texttt{cn}
  attribute, their family name will be stored in the \texttt{sn} attribute,
  their given name will be stored in the \texttt{givenName} attribute.

\item More marks will be awarded for solutions that are more general,
  more flexible and of course, better structured.
  
\item Also, with the soft copy of your program, provide a soft copy of
  the following:
  \begin{itemize}
  \item an \LDIF file output from your program that creates all the
    accounts and creates the private user groups for all the students
    in the artificial student data that was allocated to you.

  \item an \LDIF file output from your program that deletes all the
    accounts and private user groups listed in the artificial
    student data file that was allocated to you.
  \end{itemize}
  
\item Your program should display an informative error message for
  each account that fails to be created or deleted.

\item You should be prepared to demonstrate your program in the
  laboratory.
\end{enumerate}

If you have any questions about the assignment, please ask them by
email.  I will send the reply to all students if that seems helpful,
but I will conceal the identity of the person who asked the question.
I welcome any questions.

  \begin{solution}
    This is not really a solution, but is part of one possible
    solution.  This handles the requirements for accounts on Linux.
\begin{verbatim}
#! /usr/bin/perl -w

use strict;
use Getopt::Long;
our $add = 0;
our $del = 0;
our $remove_dir = 0;
sub usage { die "usage: $0 --add|(--del [--rem])\n"; }
GetOptions( add => \$add, del => \$del, rem => \$remove_dir ) or usage();
usage unless $add or $del;
usage if $add and $del;
usage if $remove_dir and $add;


sub hash_md5_password($)
{
    my $clear_text_password = shift;
    my $salt = join '',
    ('.', '/', 0..9, 'A'..'Z', 'a'..'z')[rand 64, rand 64, rand 64, rand 64,
                                         rand 64, rand 64, rand 64, rand 64];
    $salt = '$1$'.$salt.'$';
    my $hashed_password = crypt( $clear_text_password, $salt );
    return $hashed_password;
}

my $course;
my $year;

while ( <> )
{
    chomp;
    
    if ( m!^(\d+)/(\d)\s! )
    {
        $course = $1;
        $year = $2;
        next;
    }
    if (
        my ( $name, $gender, $student_id, $hk_id )
         #= m!\s\s+([A-Z]+(?: [A-Z][a-z]*)+)\s\s+([MF])\s+(\d{9})\s\s+([a-zA-Z]\d{6}\([\dA-Z]\))! )
         = m{
                 \s+                        # at leaset 1 space
                 (                          # this matches $name
                     [A-Z]+,?               # family name is upper case
                     (?:\s[A-Z][a-z]*)+     # one or more given names
                 )
                 \s\s+                      # at leaset 2 spaces
                 ([MF])                     # gender
                 \s+                        # at least one space
                 (\d{9})                    # student id is 9 digits
                 \s\s+                      # at leaset 2 spaces
                 ([a-zA-Z]\d{6}\([\dA-Z]\)) # HK ID
            }x
       )
    {
        print "sex=$gender, student ID = $student_id, ",
        "hkID = $hk_id, course = $course, name=$name, ",
        defined $year ? "year = $year\n" : "\n";
        my ( $full_name, $userid, $clear_password )
            = ( $name, substr( $name, 0, 1 ) . $student_id, $hk_id );

        our $hashed_password = hash_md5_password( $clear_password ) if $add;
       
        my @cmd = (
               '/usr/sbin/useradd',
               '-c', "\"$full_name\"",
               '-p', "$hashed_password",
               "$userid"
          ) if $add;
        @cmd = (
               '/usr/sbin/userdel',
               "-r",
               "$userid",
          ) if $del;
        splice @cmd, 1, 1 if $del and not $remove_dir;

        # The "and <STDIN>" is just to pause the display when there is
        # a problem:
        system( @cmd ) == 0 or ( warn "Cannot execute @cmd: $!" and <STDIN> );

        next;
    }
    # Just to check there are no lines that we have missed:
    warn "POSSIBLE UNMATCHED STUDENT: $_\n" if m!^\s*\d+\s+!;
}
\end{verbatim}
Here is a simple test program to generate one account, written before
developing the program above:
\begin{verbatim}
#! /usr/bin/perl -w

# Example program to generate MD5 hashed passwords suitable for use in
# /etc/shadow on a Linux system.
# You could pass the output of this function to useradd -p xxxxx,
# where xxxxx is the output of hash_md5_password().
# Based on /usr/share/doc/samba-2.2.1a/examples/LDAP/ldapsync.pl,
# distributed with samba.
# A portable alternative is the module Crypt::PasswdMD5, available
# through the cpan program.

use strict;

sub hash_md5_password($)
{
    my $clear_text_password = shift;
    my $salt = join '',
    ('.', '/', 0..9, 'A'..'Z', 'a'..'z')[rand 64, rand 64, rand 64, rand 64,
                                         rand 64, rand 64, rand 64, rand 64];
    $salt = '$1$'.$salt.'$';
    my $hashed_password = crypt( $clear_text_password, $salt );
    return $hashed_password;
}

# This code is a stub just to test the function:
our $clear_password = "test";
our $hashed_password = hash_md5_password( $clear_password );

print "MD5 Hash of '$clear_password' is '$hashed_password'\n";

my ( $full_name, $userid ) = ( 'Test One', 'tst1' );

my @cmd = (
               '/usr/sbin/useradd',
               '-c', "\"$full_name\"",
               '-p', "$hashed_password",
               "$userid"
          );

system( @cmd ) == 0 or die "Cannot execute @cmd: $!";
\end{verbatim}%$
  \end{solution}

\end{document}

$ ldapsearch -x '(&(year=3)(course=41300)(registrationDate=*03))' uid
-LLL | grep '^uid: ' | cut -d' ' -f2 | wc -l
     80
[nicku@nickpc] 4y 6m 9d, 10:1:54 ~/teaching/ict/snm/lab/namelist 13w4d
10:58:6
$ ldapsearch -x '(&(year=3)(course=41300)(registrationDate=*03))' uid
-LLL | grep '^uid: ' | cut -d' ' -f2 | while read uid;do
./generate-fake-enrolment-data
--format-file=format-for-assignment-2003-2004.txt
/home/nicku/admin/ict/student-data/ft.txt; mv
/home/nicku/admin/ict/student-data/ft.txt.fictionalised
/home/nicku/admin/ict/student-data/artificial-student-data-${uid}.txt;done
$this_max_class=150
$this_max_class=157
$this_max_class=141
$this_max_class=150
$this_max_class=141
$this_max_class=141
$this_max_class=144
$this_max_class=146
$this_max_class=146
$this_max_class=142
$this_max_class=142
$this_max_class=152
$this_max_class=147
$this_max_class=152
$this_max_class=146
$this_max_class=144
$this_max_class=156
$this_max_class=147
$this_max_class=145
$this_max_class=158
$this_max_class=158
$this_max_class=152
$this_max_class=155
$this_max_class=144
$this_max_class=141
$this_max_class=153
$this_max_class=145
$this_max_class=150
$this_max_class=154
$this_max_class=152
$this_max_class=150
Use of uninitialized value in string ne at
./generate-fake-enrolment-data line 607, <IN> line 3867.
$this_max_class=156
$this_max_class=145
$this_max_class=143
$this_max_class=141
$this_max_class=145
$this_max_class=148
$this_max_class=145
$this_max_class=156
$this_max_class=142
$this_max_class=148
$this_max_class=154
$this_max_class=146
$this_max_class=158
$this_max_class=155
$this_max_class=154
$this_max_class=156
$this_max_class=151
[nicku@nickpc] 4y 6m 9d, 10:9:39 ~/teaching/ict/snm/lab/namelist 13w4d
10:50:21
$
$ egrep -c '[A-Z][0-9]{6}\([0-9A]\)' artificial-student-data-*
artificial-student-data-000713210.txt:923
artificial-student-data-010042338.txt:927
artificial-student-data-010074127.txt:929
artificial-student-data-010077977.txt:921
artificial-student-data-010145653.txt:930
artificial-student-data-010145917.txt:940
artificial-student-data-010184168.txt:922

[nicku@nickpc] 4y 6m 9d, 10:15:37 ~/admin/ict/student-data
~/teaching/ict/snm/lab/namelist 13w4d 10:44:22
$
