| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- #!/usr/bin/env perl
- package tcms::useradd;
- use 5.040;
- use re '/aasx';
- use warnings FATAL => 'all';
- use FindBin::libs;
- use Getopt::Long;
- use Pod::Usage;
- use List::Util qw{first};
- use Trog::Auth;
- use Trog::Log;
- use Trog::Data;
- use Trog::Config;
- # Don't murder our terminal when done
- $ENV{NOHUP} = 1;
- =head1 SYNOPSIS
- Add or edit a tCMS user. In the event of a user edit, every option save for user is optional,
- and the ones you leave out keep whatever value they already had.
- Creating a user who does not exist yet needs --password, --display_name and --contact_email,
- as there is no existing profile post to inherit them from.
- Use this to reset user passwords or fix broken users.
- =head2 USAGE
- tcms-useradd --user foo --password bar --display_name baz --contact_email foo@bar.baz --acl fred --acl wilma
- =head2 OPTIONS
- =over 4
- =item --user
- Specify the user to add, or edit if the user already exists.
- =item --password
- Set a password for the user. Omit the option entirely if you want to keep the
- password an existing user already has.
- Passing --password with no value prompts for one instead, with echo off, which
- keeps it out of your shell history and off your screen:
- tcms-useradd --user foo --password # prompts, twice, to confirm
- It can also be piped in, in which case it is read once and not confirmed:
- echo hunter2 | tcms-useradd --user foo --password
- Note that setting a password kills any session the user currently holds, so
- they will have to log back in.
- =item --display_name
- Set the display name for the user.
- =item --contact_email
- Set the contact email for the user.
- =item --acl
- Set an acl for this user. May be passed multiple times.
- Defaults to 'admin' acl.
- =item --help, --man
- Display this output.
- =back
- =cut
- # The logger is set up per-request by the server; a CLI run has to do it itself,
- # or the first thing Trog::Auth logs blows up on an undefined $log.
- sub _init_logs {
- my ($who) = @_;
- Trog::Log::log_init( 'logs/tcms.log', 'info' );
- $Trog::Log::user = $who // 'nobody';
- return;
- }
- # Read a password off the terminal without echoing it, so it stays out of the
- # shell history and off the screen. Falls back to a plain read when we aren't
- # on a TTY, which is what makes `echo hunter2 | tcms-useradd ...` work.
- sub _read_password {
- my ($prompt) = @_;
- my $password;
- if ( -t STDIN ) {
- print STDERR $prompt;
- require Term::ReadKey;
- Term::ReadKey::ReadMode('noecho');
- $password = <STDIN>;
- Term::ReadKey::ReadMode('restore');
- print STDERR "\n";
- }
- else {
- $password = <STDIN>;
- }
- chomp $password if defined $password;
- return $password;
- }
- # What a brand new user's profile post looks like before we merge anything over
- # the top of it. Mirrors what setup() builds for the very first user.
- sub _default_user_post {
- return {
- data => 'Default user',
- preview => '/img/avatar/humm.gif',
- wallpaper => '/img/sys/testpattern.jpg',
- tags => ['about'],
- visibility => 'public',
- acls => ['admin'],
- callback => 'Trog::Routes::HTML::users',
- method => 'GET',
- form => 'profile.tx',
- aliases => [],
- };
- }
- sub main {
- my %options;
- Getopt::Long::GetOptionsFromArray(
- \@_,
- 'user=s' => \$options{user},
- 'display_name=s' => \$options{display_name},
- 'help|?' => \$options{help},
- 'password:s' => \$options{password},
- 'contact_email=s' => \$options{contact_email},
- 'acl=s@' => \$options{acl},
- );
- pod2usage( -exitval => 0, -verbose => 1 ) if $options{help};
- unless ( $options{user} ) {
- print STDERR "--user is required.\n";
- return 2;
- }
- # --password with no value means "ask me", as opposed to leaving the option
- # out entirely, which means "leave the password alone".
- if ( defined $options{password} && $options{password} eq q{} ) {
- my $interactive = -t STDIN;
- $options{password} = _read_password("New password for '$options{user}': ");
- unless ( ( $options{password} // q{} ) ne q{} ) {
- print STDERR "No password provided.\n";
- return 2;
- }
- # Only worth confirming when a human is typing it. Piped input has no
- # second line to read, and typos in a script aren't fixed by asking twice.
- if ($interactive) {
- my $again = _read_password("Confirm: ");
- unless ( defined $again && $options{password} eq $again ) {
- print STDERR "Passwords did not match.\n";
- return 2;
- }
- }
- }
- $options{acl} //= [];
- $options{acl} = [ $options{acl} ] if $options{acl} && ref $options{acl} ne 'ARRAY';
- $options{acl} = ['admin'] unless @{ $options{acl} };
- _init_logs( $options{user} );
- # Find the user's existing post, if they have one.
- my $conf = Trog::Config::get();
- my $data = Trog::Data->new($conf);
- my @userposts = $data->get( tags => ['about'], acls => [qw{admin}] );
- my $user_obj = first { ( $_->{user} || '' ) eq $options{user} } @userposts;
- # A user we've never seen before has no post to edit, so start from the
- # defaults rather than dereferencing undef.
- my $is_new = !$user_obj;
- $user_obj //= _default_user_post();
- if ($is_new) {
- unless ( $options{password} && $options{display_name} && $options{contact_email} ) {
- print STDERR "User '$options{user}' does not exist yet.\n";
- print STDERR "Creating one needs --password, --display_name and --contact_email.\n";
- return 2;
- }
- }
- # Only the options actually passed should win. Merging %options wholesale
- # writes an undef over every field the caller didn't mention, which is how
- # resetting a password used to blank out the display name and contact email.
- my %provided = map { $_ => $options{$_} } grep { defined $options{$_} } qw{user display_name contact_email};
- my %merged = (
- %$user_obj,
- %provided,
- );
- # The display name is the title of the post, and the URL it lives at.
- $merged{title} = $merged{display_name} if $merged{display_name};
- $merged{local_href} = "/users/$merged{display_name}" if $merged{display_name};
- Trog::Auth::killsession( $options{user} );
- local $@;
- eval { Trog::Auth::useradd( $options{user}, $merged{display_name}, $options{password}, $options{acl}, $merged{contact_email} ); 1 } or do {
- print STDERR "Could not add user '$options{user}': $@";
- return 1;
- };
- eval { $data->add( \%merged ); 1 } or do {
- my @errors = ref $@ eq 'ARRAY' ? @{$@} : ($@);
- print STDERR "Saved the credentials for '$options{user}', but their profile post was rejected:\n";
- print STDERR join( "\n", @errors ), "\n";
- return 1;
- };
- print "@{[ $is_new ? 'Added' : 'Updated' ]} user '$options{user}'.\n";
- return 0;
- }
- exit main(@ARGV) unless caller;
- 1;
|