#!/usr/bin/perl # rt-updatedb.pl # Copyright Worcester College 2006 # This script written by Michael Howe (michael.howe@worc.ox.ac.uk) for Worcester # College. # Released under the GNU GPL v2. # Version: $Id: rt-updatedb.pl 61 2006-02-06 22:56:21Z michael $ # # Usage # Try 'perldoc rt-updatedb.pl', or read the bottom of the script. # use strict; use warnings; use Getopt::Long; use DBI; use File::Basename; # Import RT lib: use lib "/usr/share/request-tracker3.4/lib"; use RT::Interface::CLI qw(CleanEnv); use RT::User; use RT::Group; # # These variables should be updated appropriately, and can also be set via the # command line. # # Database type: my $emaildb_type = 'mysql'; # should be a dbi driver name, eg mysql or Pg # Database host: my $emaildb_host = 'localhost'; # Database name: my $emaildb_name = 'emaildb'; # Database table that contains user data: my $emaildb_table = 'email'; # User with permission to access the database: my $emaildb_user = ''; # Password for above user: my $emaildb_pass = ''; my $rtdb_type = 'mysql'; my $rtdb_host = 'localhost'; my $rtdb_name = 'rtdb'; my $rtdb_user = ''; my $rtdb_pass = ''; # Array of users to ignore (not add to RT): my @ignore_users = qw( itss0044 ); # Array of email addresses to ignore: my @ignore_emails = qw( stuart.sharp laurence.kinsella ); # Example: # my @ignore_emails = qw( stuart.sharp@worc.ox.ac.uk it-support@worc.ox.ac.uk); # File with usernames/emails to ignore, one per line my $ignore_file = ''; # Hash of groups. Each key is the 'status' from the email database table. # The value of $groupmap{$key} is the RT group name to add the users to. my %groupmap = ( undergrad => 'JCR', postgrad => 'MCR', college => 'STAFF', senmem => 'SCR', ); # Print output? my $verbose = 0; # You don't need to change this. my $help = 0; # This should be set to make it execute. my $execute = 0; # # End variables that should need changing # # Other variables: my $procname = basename($0); my $SystemUser; my $CurrentUser; my $CurrentGroup; # # resetrt # This sets the RT objects up to be suitable to work with. # sub resetrt { CleanEnv(); RT::LoadConfig(); RT::Init; $SystemUser = RT::CurrentUser->new(); $SystemUser->LoadByName('RT_System'); $CurrentUser = RT::CurrentUser->new(); $CurrentGroup = RT::Group->new($SystemUser); } # # checkgroups # This checks that the RT groups in the %groupmap hash actually exist, and # complains (and deletes the groupmap item). # sub checkgroups { # Loop through all of the statuses foreach ( keys( %groupmap ) ) { # Attempt to load the group that goes with the status: my $group = $CurrentGroup->LoadUserDefinedGroup( $groupmap{$_}); # If it can't get the ID of the group, we assume that group # doesn't exist and delete it. unless ( $CurrentGroup->id =~ m{(\d+)} ) { print "Group $groupmap{$_} doesn't exist.\nNot adding people with the status '$_'\n"; delete( $groupmap{$_} ); } } } # # verbose # If the verbose flag is set this prints out more info. # sub verbose(@) { print ("$procname: ", @_, "\n") if ($verbose); } # # usage # Prints a usage statement. # sub usage { print < \$help, 'verbose|v' => \$verbose, 'execute|e' => \$execute, 'emaildb_type=s' => \$emaildb_type, 'emaildb_host=s' => \$emaildb_host, 'emaildb_name=s' => \$emaildb_name, 'emaildb_table=s' => \$emaildb_table, 'emaildb_user=s' => \$emaildb_user, 'emaildb_pass=s' => \$emaildb_pass, 'ignore_user=s' => \@ignore_users, 'ignore_email=s' => \@ignore_emails, 'ignore_file=s' => \$ignore_file, 'group=s' => \%groupmap, ); # Check we don't just want help: if ($help or not $execute) { usage(); exit(0); } # Set up RT. resetrt(); # Check the groups we've been given. # Afterwards reset our RT objects, just in case. checkgroups(); #resetrt(); # Grab the ignore list from a file if it exists: if ($ignore_file) { # We _should_ die if the file can't be found, because otherwise we may # add users that really _shouldn't_ exist. ( -f $ignore_file ) or die("Can't read the ignore file '$ignore_file': $!\n"); # Again, die if we can't open it. open( IF, $ignore_file ) or die("Can't open the ignore file '$ignore_file': $!\n"); # Loop through each line of the file. while () { # Remove newlines chomp; # Don't want blank lines next if ( m{^$} ); # If we've got an @ followed by a period, it's an email address. # Even if it's not supposed to be. if ( $_ =~ m{@.*\..*} ) { verbose("Adding '$_' to email addresses to ignore"); push @ignore_emails, $_; } else { # If it's not an email address it's a username verbose("Adding '$_' to usernames to ignore"); push @ignore_users, $_; } } # Close the handle. But if we can't, don't die. close(IF) or print "Can't close '$ignore_file' ($!) - continuing anyway\n"; } # Make it allow comma-separated ignores too, just in case. # This allows the format --ignore_user foo,bar,baz as well as --ignore-user foo # --ignore_user bar --ignore-user baz # Not widely publicised, but just in case, as other apps do it. @ignore_users = split(/,/, join(',',@ignore_users)); @ignore_emails = split(/,/, join(',',@ignore_emails)); # # Email database query. # # This works for $email_type=mysql and Pg. my $edbh = DBI->connect("dbi:$emaildb_type:database=$emaildb_name;host=$emaildb_host", "$emaildb_user", "$emaildb_pass", { RaiseError => 1, }) or die "Can't connect to db '$emaildb_name': $!\n"; # Create the sql to select statuses (surround each status with single # quotes, and separate with commas: my $statuses = "'" . join( "', '", keys( %groupmap ) ) . "'"; # Hash to say what emails/users we _don't_ want to select my %ignore = ( emails => '', users => '', ); # Create the SQL to ignore emails if ( @ignore_emails ) { $ignore{emails} = "AND e.email NOT IN ( '"; foreach ( @ignore_emails ) { # Auto-complete to @worc.ox.ac.uk if there's no @ # Note that this works on command-line set emails only - # because emails set from file must be fully-qualified. $_ .= "\@worc.ox.ac.uk" if ( $_ !~ m{@} ); } # Same trick as with $statuses above. $ignore{emails} .= join( "', '", @ignore_emails) . "' )"; verbose( "Ignoring email addresses '" . join( " '", @ignore_emails ) . "'\n" ); } # Create the sql to ignore usernames if ( @ignore_users ) { $ignore{users} = "AND e.username NOT IN ( '" . join( "', '", @ignore_users) . "' )"; verbose( "Ignoring usernames '" . join( " '", @ignore_users ) . "'\n" ); } # Build the query my $esql = "SELECT e.username, e.email, LEFT(e.bod_id, 7) AS bod_id, e.surname, e.firstname, e.status FROM $emaildb_table AS e WHERE e.status IN ($statuses) $ignore{users} $ignore{emails} ORDER BY e.username ;"; # Prepare and execute the query my $equery = $edbh->prepare( $esql ); $equery->execute(); # Get a reference to an array of the results of the query my $array_ref = $equery->fetchall_arrayref( { username => 1, email => 1, bod_id => 1, surname => 1, firstname => 1, status => 1 } ); # # End of email database query. # my ($uid, $uname, $rtn, $msg); # Loop through each row returned from the database, and check their user details # against the RT database. my $rdbh = DBI->connect("dbi:$rtdb_type:database=$rtdb_name;host=$rtdb_host", "$rtdb_user", "$rtdb_pass", { RaiseError => 1, AutoCommit => 1, }) or die "Can't connect to db '$rtdb_name': $!\n"; my $sth = $rdbh->prepare("UPDATE Users SET Name = ? WHERE EmailAddress = ?"); foreach my $row (@$array_ref) { unless ( ( $row->{username} ) && ( $row->{email} ) && ($row->{bod_id})){ print "User '$row->{username}' (email '$row->{email}', bod_id '$row->{bod_id}') is being ignored"; next; } print "Updating " . $row->{username} . " from " . $row->{email} . "\n"; $sth->execute( $row->{username}, $row->{email} ); } #$rdbh->commit; # Loop through each row returned from the database and try to add/update RT. foreach my $row (@$array_ref) { # Sanity check - if username, email, or bod_id isn't set for a user, # we'll skip them and print a warning. unless ( ( $row->{username} ) && ( $row->{email} ) && ($row->{bod_id})){ print "User '$row->{username}' (email '$row->{email}', bod_id '$row->{bod_id}') is being ignored"; next; } $uname = ''; # Check if the user already exists. While this will slow down the first # run, as they won't exist, it should make successive runs faster. ( $rtn, $msg ) = $CurrentUser->LoadByEmail( $row->{email} ); if ( 'Found Object' eq $msg ) { $uid = $CurrentUser->PrincipalId; $uname = $CurrentUser->Name; if ( $uname ne $row->{username} ) { print "Error: user $row->{username} has a RT username of'$uname' when it should be $row->{username}. Ignoring them.\n"; next; } verbose( "Setting $row->{username}'s password to $row->{bod_id}"); $CurrentUser->UserObj->SetDisabled(0); my ( $prtn, $pmsg ) = $CurrentUser->UserObj->SetPassword( $row->{bod_id} ); print "prtn: $prtn\tpmsg: $pmsg\n"; $CurrentUser->UserObj->SetPrivileged(0); } else { # User doesn't exist - let's go create them! verbose("User " . $row->{'username'} . " does not exist, attempting to create"); my $new_entry = new RT::User($SystemUser); verbose("\t$row->{username}\t$row->{bod_id}\n" ); ( $uid, $msg ) = $new_entry->Create( Name => $row->{username}, Password => $row->{bod_id}, EmailAddress => $row->{email}, RealName => "$row->{firstname} $row->{surname}", ); # If it went right, $uid will be non-zero. if ( $uid ) { verbose("User " . $row->{'username'} . " created successfully (uid $uid)."); } else { warn "(Error: $msg)\n"; } } my $grouploaded = $CurrentGroup->LoadUserDefinedGroup( $groupmap{$row->{'status'}} ); if ( ( ( 'Found Object' eq $grouploaded ) || ( 'Fetched from cache' eq $grouploaded ) ) && ( $uid ) ) { # Add user to group # If they are already a member, remove them from the group and # re-add them. my ( $return, $gmsg ) = $CurrentGroup->AddMember( $uid ); if ( 'Group already has member' eq $gmsg ) { # return = 0 (ie unsuccessful), but we don't mind verbose( "User " . $row->{'username'} . " is already a member of group " . $groupmap{$row->{'status'}} ); # Attempt to delete user from group: my ( $delrtn, $delmsg ) = $CurrentGroup->DeleteMember( $uid ); if ( $delrtn and 'Member deleted' eq $delmsg ) { # Deletion was successful. verbose( "Removed user from group." ); # Attempt to add user to group again my ( $addrtn, $addmsg ) = $CurrentGroup->AddMember( $uid ); if ( $addrtn and 'Member added' eq $addmsg ) { # All was successful - let the user know verbose( "User added to group " . "successfully" ); } else { # We couldn't readd them. Let the user # know about this. verbose( "Couldn't add user '" . $row->{'username'} . " to group '" . $groupmap{$row->{'status'}} . "' successfully - returned code " . "'$addrtn', message '$addmsg'" ); } } else { # We couldn't delete the user. verbose( "Couldn't remove user from group " . "successfully - returned code " . "'$delrtn', message '$delmsg'" ); } } elsif ( 0 == $return ) { # no success warn "(Error: $gmsg)" } } elsif ( $uid ) { # User's all good, but group isn't. # This _shouldn't_ happen, thanks to the checkgroups() function, # but you never know. warn "(Error: Group ". $groupmap{$row->{'status'}}. " could not be loaded for user " . $row->{'username'} . ")\n"; verbose( "Reason for error: '$grouploaded'" ); } } # Cleanup. my $rc = $edbh->disconnect or print $edbh->errstr; # # And here is the end of the script. Thank you for flying with perl, we hope # you enjoyed it. # Below is documentation in the POD format (man perldoc; perldoc rt-updatedb.pl) # =head1 Name rt-updatedb.pl - script to add RT users from an email database =head1 SYNOPSIS B [B<--help>] [B<--verbose>] [B<--emaildb_host >] [B<--emaildb_type >] [B<--emaildb_name >] [B<--emaildb_table >] [B<--emaildb_user >] [B<--emaildb_pass >] [B<--ignore_user >] [B<--ignore_email >] [B<--ignore_file >] [B<--group [database_status=RT_group] [database_status=RT_group]>] =head1 DESCRIPTION I will create users for Request Tracker given the data from a MYSQL database with email addresses, usernames, statuses, names and university card numbers. =head1 OPTIONS =over 5 =item B<--help> Prints out a statement on how to use the script. =item B<--verbose> Describes what the script is actually doing =item B<--emaildb_host> I The host that the database is running on. Note that this file must be run on the host that RT is running on. =item B<--emaildb_type> I The database type, specified as a valid perl driver name. Defaults to mysql. Driver names: Database Perl Driver --------------------------- MYSQL mysql Postgres Pg =item B<--emaildb_name> I The name of the MYSQL database that contains the email data. =item B<--emaildb_table> I The table in the database that contains the email data. =item B<--emaildb_user> I The MYSQL user that has permissions to read the L<--emaildb_table|/--emaildb_table table_name> table. =item B<--emaildb_pass> I Password for the MYSQL user from L<--emaildb_user|/--emaildb_user user_name>. Note that this is I if you do not trust all the users on your machine. If in doubt, specify it in the script instead. =item B<--ignore_user> I A username to ignore when fetching the email lists, so the user is not readded to request tracker. Probably best used to exclude IT support staff. Example: rt-updatedb.pl --ignore_user stuarts Would ignore the person with the username stuarts. Can be used multiple times to specify multiple people to ignore. =item B<--ignore_email> I Like L, this is a username to ignore when fetching the email lists, so the user is not readded to request tracker. It will qualify any addresses given without domain (and @ symbol) to @worc.ox.ac.uk Probably best used to exclude it support staff. Like L<--ignore_user|/--ignore_user username>, can be used multiple times to specify multiple people to ignore. =item B<--ignore_file> I Will read a list of usernames and/or fully-qualified email addresses that should be ignored from I. Will die if file cannot be read. Example file: jcr-support@worc.ox.ac.uk stuarts itss0044 Would ignore the address jcr-support@worc.ox.ac.uk and the users stuarts and itss0044. =item B<--group> I=I Allows you to set the Request Tracker group for all people with a particular status. rt-updatedb.pl will not process people who aren't listed as being in one of these groups. Example: rt-updatedb.pl --group undergrad=JCR --group postgrad=MCR --group senmem=STAFF --group college=STAFF =back =head1 EMAIL DATABASE STRUCTURE The email database should have a table with the following fields: username (eg itss0044), email (eg it-support@worc.ox.ac.uk), bod_id (will take the FIRST SEVEN digits, surname, firstname, status. Each user with a status listed in the %groupmap hash (taken from L<--group|/--group status=RT_Group>) will be selected. =head1 RT SETUP Request Tracker should be configured to give all groups listed as RT_Groups (L<--group|/--group status=RT_Group>) the following permissions on the appropriate queue: S ShowTicketComments could optionally be set; without it requestors cannot see the comments made on their tickets. The users are added as 'non-privileged' users, so they see the RT Self-Service interface, rather than the full-blown interface that 'privileged' users get. For example, using B<--group undergrad=JCRUsers> the group JCRUsers would need to exist with the above permissions for the jcr support queue. When the script is run, everyone with the 'undergrad' status will be added to the JCRUsers group, with the ability to see their own tickets, and create new ones. =head1 BUGS Users are never removed. This means that if they move status groups (eg undergrad -> postgrad), they will be able to access both queues when added. B<--input_file> does not accept usernames with an '@' symbol; it instead parses them as email addresses. There should probably be some way to 'trial' this - ie to do everything _bar_ create the user. =head1 SECURITY This is a perl script. There are almost certainly bugs. Don't run in an environment where you don't trust the system and the users, and beware of the L<--emaildb_pass|/--emaildb_pass password> option, which will show up your password in process lists. =head1 LICENSE Released under the GNU GPL v2. Enjoy. =head1 AUTHOR Original creator: Michael Howe Created on request for Worcester College L. =head1 COPYRIGHT Copyright Worcester College 2006 =cut # vim: tw=80