tcms-useradd 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. #!/usr/bin/env perl
  2. package tcms::useradd;
  3. use 5.040;
  4. use re '/aasx';
  5. use warnings FATAL => 'all';
  6. use FindBin::libs;
  7. use Getopt::Long;
  8. use Pod::Usage;
  9. use IO::Prompter();
  10. use List::Util qw{first};
  11. use Trog::Auth;
  12. use Trog::Log;
  13. use Trog::Data;
  14. use Trog::Config;
  15. # Don't murder our terminal when done
  16. $ENV{NOHUP} = 1;
  17. =head1 SYNOPSIS
  18. Add or edit a tCMS user. In the event of a user edit, every option save for user is optional,
  19. and the ones you leave out keep whatever value they already had.
  20. Creating a user who does not exist yet needs --password, --display_name and --contact_email,
  21. as there is no existing profile post to inherit them from.
  22. Use this to reset user passwords or fix broken users.
  23. =head2 USAGE
  24. tcms-useradd --user foo --password bar --display_name baz --contact_email foo@bar.baz --acl fred --acl wilma
  25. =head2 OPTIONS
  26. =over 4
  27. =item --user
  28. Specify the user to add, or edit if the user already exists.
  29. =item --password
  30. Set a password for the user. Omit the option entirely if you want to keep the
  31. password an existing user already has.
  32. Passing --password with no value prompts for one instead, with echo off, which
  33. keeps it out of your shell history and off your screen:
  34. tcms-useradd --user foo --password # prompts, twice, to confirm
  35. It can also be piped in, in which case it is read once and not confirmed:
  36. echo hunter2 | tcms-useradd --user foo --password
  37. Note that setting a password kills any session the user currently holds, so
  38. they will have to log back in.
  39. =item --display_name
  40. Set the display name for the user.
  41. =item --contact_email
  42. Set the contact email for the user.
  43. =item --acl
  44. Set an acl for this user. May be passed multiple times.
  45. Defaults to 'admin' acl.
  46. =item --help, --man
  47. Display this output.
  48. =back
  49. =cut
  50. # The logger is set up per-request by the server; a CLI run has to do it itself,
  51. # or the first thing Trog::Auth logs blows up on an undefined $log.
  52. my sub init_logs {
  53. my ($who) = @_;
  54. Trog::Log::log_init( 'logs/tcms.log', 'info' );
  55. $Trog::Log::user = $who // 'nobody';
  56. return;
  57. }
  58. # Read a password off the terminal without echoing it, so it stays out of the
  59. # shell history and off the screen. Falls back to a plain read when we aren't
  60. # on a TTY, which is what makes `echo hunter2 | tcms-useradd ...` work. Returns
  61. # undef only at the end of the input, and q{} for an empty line.
  62. my sub read_password ($prompt) {
  63. my $password = IO::Prompter::prompt( $prompt, -echo => q{}, -in => *STDIN, -out => *STDERR );
  64. return $password ? "$password" : undef;
  65. }
  66. # What a brand new user's profile post looks like before we merge anything over
  67. # the top of it. Mirrors what setup() builds for the very first user.
  68. my sub default_user_post {
  69. return {
  70. data => 'Default user',
  71. preview => '/img/avatar/humm.gif',
  72. wallpaper => '/img/sys/testpattern.jpg',
  73. tags => ['about'],
  74. visibility => 'public',
  75. acls => ['admin'],
  76. callback => 'Trog::Routes::HTML::users',
  77. method => 'GET',
  78. form => 'profile.tx',
  79. aliases => [],
  80. };
  81. }
  82. sub main {
  83. my %options;
  84. Getopt::Long::GetOptionsFromArray(
  85. \@_,
  86. 'user=s' => \$options{user},
  87. 'display_name=s' => \$options{display_name},
  88. 'help|?' => \$options{help},
  89. 'password:s' => \$options{password},
  90. 'contact_email=s' => \$options{contact_email},
  91. 'acl=s@' => \$options{acl},
  92. );
  93. pod2usage( -exitval => 0, -verbose => 1 ) if $options{help};
  94. unless ( $options{user} ) {
  95. print {*STDERR} "--user is required.\n"; ## no critic (ProhibitPrintSTDERR) -- for whoever ran this
  96. return 2;
  97. }
  98. # --password with no value means "ask me", as opposed to leaving the option
  99. # out entirely, which means "leave the password alone".
  100. if ( defined $options{password} && !$options{password} ) {
  101. $options{password} = read_password("New password for '$options{user}': ");
  102. unless ( $options{password} ) {
  103. print {*STDERR} "No password provided.\n"; ## no critic (ProhibitPrintSTDERR) -- for whoever ran this
  104. return 2;
  105. }
  106. # A person at a terminal always enters a line, even an empty one, so
  107. # they always confirm. Piped input with one line has no second line,
  108. # and there is nothing to confirm against.
  109. my $again = read_password("Confirm: ");
  110. if ( defined $again && $options{password} ne $again ) {
  111. print {*STDERR} "Passwords did not match.\n"; ## no critic (ProhibitPrintSTDERR) -- for whoever ran this
  112. return 2;
  113. }
  114. }
  115. $options{acl} //= [];
  116. $options{acl} = [ $options{acl} ] if $options{acl} && ref $options{acl} ne 'ARRAY';
  117. $options{acl} = ['admin'] unless @{ $options{acl} };
  118. init_logs( $options{user} );
  119. # Find the user's existing post, if they have one.
  120. my $conf = Trog::Config::get();
  121. my $data = Trog::Data->new($conf);
  122. my @userposts = $data->get( tags => ['about'], acls => [qw{admin}] );
  123. my $user_obj = first { ( $_->{user} || '' ) eq $options{user} } @userposts;
  124. # A user we've never seen before has no post to edit, so start from the
  125. # defaults rather than dereferencing undef.
  126. my $is_new = !$user_obj;
  127. $user_obj //= default_user_post();
  128. if ($is_new) {
  129. unless ( $options{password} && $options{display_name} && $options{contact_email} ) {
  130. print {*STDERR} "User '$options{user}' does not exist yet.\nCreating one needs --password, --display_name and --contact_email.\n"; ## no critic (ProhibitPrintSTDERR) -- for whoever ran this
  131. return 2;
  132. }
  133. }
  134. # Only the options actually passed should win. Merging %options wholesale
  135. # writes an undef over every field the caller didn't mention, which is how
  136. # resetting a password used to blank out the display name and contact email.
  137. my %provided = map { $_ => $options{$_} } grep { defined $options{$_} } qw{user display_name contact_email};
  138. my %merged = (
  139. %$user_obj,
  140. %provided,
  141. );
  142. # The display name is the title of the post, and the URL it lives at.
  143. $merged{title} = $merged{display_name} if $merged{display_name};
  144. $merged{local_href} = "/users/$merged{display_name}" if $merged{display_name};
  145. Trog::Auth::killsession( $options{user} );
  146. local $@;
  147. eval { Trog::Auth::useradd( $options{user}, $merged{display_name}, $options{password}, $options{acl}, $merged{contact_email} ); 1 } or do {
  148. print {*STDERR} "Could not add user '$options{user}': $@"; ## no critic (ProhibitPrintSTDERR) -- for whoever ran this
  149. return 1;
  150. };
  151. eval { $data->add( \%merged ); 1 } or do {
  152. my @errors = ref $@ eq 'ARRAY' ? @{$@} : ($@);
  153. print {*STDERR} "Saved the credentials for '$options{user}', but their profile post was rejected:\n" . join( "\n", @errors ) . "\n"; ## no critic (ProhibitPrintSTDERR) -- for whoever ran this
  154. return 1;
  155. };
  156. print "@{[ $is_new ? 'Added' : 'Updated' ]} user '$options{user}'.\n";
  157. return 0;
  158. }
  159. exit main(@ARGV) unless caller;
  160. 1;