#=============================================================================
# Doloforge::Command::User : user/developer creation/update
#
# Front-end to the system's user add/mod/remove commands. This need to run on
# a single host only, since userid replication is easily done using standard
# tools.
# Also, the more developers are constrained to the development platform
# only, the better the testing and production platforms work...
#
# Full help :
#   doloforge user --man
#
# Testing this package :
#   fakeroot perl ./src/doloforge --config t/doloforge.yaml user --add --name toto
#=============================================================================


package Doloforge::Command::User;

use strict;

# This base class implements the constructor and help methods in a generic way.
use base qw(Doloforge::Command);
use File::Path;
use DBI;
use DBD::Pg;


#=============================================================================
=encoding utf8

=head1 NAME

Doloforge::User - User/developer creation/update

=head1 SYNOPSIS

    # Add a new developer to the system
    doloforge user --add --name johndoe
    # Give him right to work on another client (project)
    doloforge user --group --name johndoe --client myclient

=head1 DESCRIPTION

Todo

=cut


#=============================================================================
# Method getOptions
#
# Accepted arguments from the command-line, in the form accepted by Getopt::Long.
# Options with no value that match method names of that package will be called as
# methods, with the decoded options as parameters.

sub getOptions {
    return (
        # actions :
        'add',                  # add a new user
        'group',                # add client-groups to the user, after user-creation
        # parameters :
        'name=s',               # userid of the user to work on
        'client=s',             # clients this user is allowed to work for ([.,:/] separated list)
        'description|desc=s',   # full-name of the user (GECOS string, first part)
    );
}

#=============================================================================
# Method add
#
# Adds a new developer on the platform, using system commands.
# Also add the user to the various databases systems :
#   nhuillard=# CREATE USER "blongjumeau";
#   nhuillard=# CREATE DATABASE "blongjumeau" WITH OWNER "blongjumeau" ENCODING 'UNICODE';
# Cosmetic addition :
#   nhuillard=# \c blongjumeau
#   blongjumeau=# COMMENT ON DATABASE "blongjumeau" IS 'Personnal database for Bertrand Longjumeau';

sub add {
    my $self = shift;

    # User account creation can only be done on user_role hosts
    return unless ($self->has_role('user_role'));

    # On authorized hosts, only root can do this...
    $self->error("add: must be run as root (try su, sudo, etc.)")
        unless ($self->check_uid('root'));

    my ($option) = @_;

    # Abort if either the UID or the GID (with the same name) exist on the current host, or if it is not provided...
    $self->error("Missing mandatory user name.") unless ($option->{name});
    $self->error("The userid '$option->{name}' already exist on that host, please select another name.") if (defined(getpwnam($option->{name})));
    $self->error("The groupid '$option->{name}' already exist on that host, please select another name.") if (defined(getgrnam($option->{name})));

    # Get command-line parameters. They will be augmented by the adduser config file
    # GECOS is either passed on the command-line, or set with the username.
    # The date of user creation is added as "other" field (see chfn(1))
    my $gecos = $self->gecos_string($option->{description} || $option->{name}, 'user', undef, undef, $self->now());
    my $homedir = "$self->{global}->{dir}->{users}/$option->{name}";
    
    # Create the user, using the Doloforge specific adduser.conf which:
    #   - sets the FIRST_[GU]ID to higher values than standard adduser.conf,
    #     allowing uid/gid re-creation on other hosts, being confident that
    #     new users will have the same id than on the first one (if created
    #     in the same order)
    #   - sets USERGROUPS, to automatically create a personnal group for
    #     each new user. The personnal group should have the gid = uid...
    $self->message("You may choose a password from the proposed ones below :");
    system($self->{global}->{commands}->{passwdgen});
    system('/usr/sbin/adduser',
        '--conf', "$self->{global}->{dir}->{conf}/adduser.conf",
        '--home', $homedir,
        '--gecos', $gecos,
        $option->{name});
    # chmod g+s ... (perfunc(1) : "...the mode contains both the file type and its permissions, you should mask off the file type portion...")
    chmod((stat($homedir))[2] & 07777 | 02000, $homedir);

    # Unconditionnally create the user in the Postgres7 service. This way, we will be ready to add him
    # in new projects, even if he is not add at creation-time...
    # Handles to various DBS are returned as a list, each one must be treated equally
    # FIXME : move this in D::S::Postgres7 (cf. _connect_dbs)
    #my ($dbh_pg7) = $self->_connect_dbs();
    #$self->message("Creating Postgres7 user '$option->{name}' and personnal DB.");
    #$dbh_pg7->do('CREATE USER ' . $dbh_pg7->quote_identifier($option->{name}));
    #$dbh_pg7->do('CREATE DATABASE ' . $dbh_pg7->quote_identifier($option->{name}) . ' WITH OWNER ' . $dbh_pg7->quote_identifier($option->{name}) . ' ENCODING \'UNICODE\'');

    # Add the user to each listed client (ie. group)
    $self->group($option);
}

#=============================================================================
# Method group
#
# Add/remove client groups to a developer.
# Also add the user/group relationship to the various databases systems :
#   nhuillard=# ALTER GROUP "internal" ADD USER "blongjumeau";

sub group {
    my $self = shift;

    # User account creation can only be done on account_role hosts
    return unless ($self->has_role('account_role'));

    # On authorized hosts, only root can do this...
    $self->error("group: must be run as root (try su, sudo, etc.)")
        unless ($self->check_uid('root'));

    my ($option) = @_;

    # Additionnal groups the user belong to (clients he will be working for)...
    my @groups;
    @groups = split(/[\.,:\/]/, $option->{client});

    # Unconditionnally add/drop user from Postgres7 groups.
    # Handles to various DBS are returned as a list, each one must be treated equally
    #my ($dbh_pg7) = $self->_connect_dbs();

    # Add the user to each listed client (ie. group). The system command will check sanity...
    foreach my $group (@groups) {
        # If the group name begins with a dash, remove the user from that client/group
        unless ($group =~ s/^-+//) {
            system('/usr/sbin/adduser', $option->{name}, $group);
            #$self->message("Adding Posgtres7 user '$option->{name}' in Posgtres7 group '$group'");
            #$dbh_pg7->do('ALTER GROUP ' . $dbh_pg7->quote_identifier($group) . ' ADD USER ' . $dbh_pg7->quote_identifier($option->{name}));
        } else {
            system('/usr/sbin/deluser', $option->{name}, $group);
            #$self->message("Removing Posgtres7 user '$option->{name}' from Posgtres7 group '$group'");
            #$dbh_pg7->do('ALTER GROUP ' . $dbh_pg7->quote_identifier($group) . ' DROP USER ' . $dbh_pg7->quote_identifier($option->{name}));
        }
    }
}

#=============================================================================
# Private method to connect to various database servers : add users, put in groups
# Connects to the Postgres7 DB server, and cache the connection handle
# FIXME : move these methods in a new Doloforge::Daemon::Postgres7 hierarchy ? or in Doloforge::Service::Postgres7 ?

sub _connect_dbs {
    my $self = shift;

    # Postgres7 daemon :
#    my $pg7host = $self->{global}->{cluster}->{$self->{global}->{stage}}->{services}->{postgres7}->{hostname};
#    my $pg7port = $self->{global}->{cluster}->{$self->{global}->{stage}}->{services}->{postgres7}->{port};
    # FIXME use this instead of previous
    my $dbhost = $self->{config}->{stages}->{$self->{global}->{stage}}->{hostname};
    my $dbport = $self->{config}->{port};

    # Connect to the "template1" database, the always-available central point for other databases manipulation.
    # Also cache the created connection, for other tasks use in the same instance ($self->group())...
    # The user running this process (root) must have privileged access to the database server : check pg_hba.conf
    # "CREATE DATABASE cannot run inside a transaction block" : no transactions on this connection...
    $self->{dbh_pg7} ||= DBI->connect("DBI:Pg(RaiseError=>1,AutoCommit=>1):dbname=template1;host=$dbhost;port=$dbport")
        or $self->error("Postgres template1 database connection denied on '$dbhost:$dbport'.");

    # Return each database handle in a list, ready to be used. The caller never have to know about cached handles...
    return ($self->{dbh_pg7});
}



1;
__END__

=head1 AUTHOR

Nicolas Huillard <F<nicolas@huillard.net>>, Dolomède

=head1 SEE ALSO

doloforge(1)

=cut
