Files
webmin/useradmin/edit_user.cgi
Ilia Ross 762e400156 Add editable SSH public keys for existing Unix users
This PR adds SSH public key management to the Users and Groups edit flow for existing Unix users.

Webmin stores its managed key with a readable marker in `authorized_keys`, validates submitted public keys, preserves unrelated keys, supports rename/update/remove flows, and performs user `.ssh` file operations as the target Unix user.

https://github.com/webmin/webmin/issues/1827
2026-06-22 01:34:40 +02:00

641 lines
19 KiB
Perl
Executable File

#!/usr/local/bin/perl
# edit_user.cgi
# Display a form for editing a user, or creating a new user
require './user-lib.pl';
use Time::Local;
&ReadParse();
# Show header and get the user
@ulist = &list_users();
$n = $in{'user'};
if ($n eq '') {
# Creating a new user
$access{'ucreate'} || &error($text{'uedit_ecreate'});
&ui_print_header(undef, $text{'uedit_title2'}, "", "create_user");
if ($in{'clone'} ne '') {
($clone_hash) = grep { $_->{'user'} eq $in{'clone'} } @ulist;
$clone_hash || &error($text{'uedit_egone'});
%uinfo = %$clone_hash;
&can_edit_user(\%access, \%uinfo) || &error($text{'uedit_eedit'});
$uinfo{'user'} = '';
}
}
else {
# Editing an existing one
($uinfo_hash) = grep { $_->{'user'} eq $n } @ulist;
$uinfo_hash || &error($text{'uedit_egone'});
%uinfo = %$uinfo_hash;
&can_edit_user(\%access, \%uinfo) || &error($text{'uedit_eedit'});
&ui_print_header(undef, $text{'uedit_title'}, "", "edit_user");
}
@tds = ( "width=30%" );
# build list of used shells
%shells = map { $_, 1 } split(/,/, $config{'shells'});
@shlist = ($config{'default_shell'} ? ( $config{'default_shell'} ) : ( ));
push(@shlist, "/bin/sh", "/bin/csh", "/bin/false") if ($shells{'fixed'});
&build_user_used(\%used, $shells{'passwd'} ? \@shlist : undef);
if ($shells{'shells'}) {
open(SHELLS, "</etc/shells");
while(<SHELLS>) {
s/\r|\n//g;
s/#.*$//;
push(@shlist, $_) if (/\S/);
}
close(SHELLS);
}
# Start of the form
print &ui_form_start("save_user.cgi", "post");
print &ui_hidden("old", $n) if ($n ne "");
print &ui_table_start($text{'uedit_details'}, "width=100%", 2, \@tds);
# Username
if ($n eq "" && $config{'new_user_group'} && $access{'gcreate'}) {
$onch = "newgid.value = user.value";
}
if ($access{'urename'} || $n eq "") {
print &ui_table_row(&hlink($text{'user'}, "user"),
&ui_textbox("user", $uinfo{'user'}, 40, 0, undef,
"onChange='$onch'"));
}
else {
print &ui_table_row(&hlink($text{'user'}, "user"),
"<tt>".&html_escape($uinfo{'user'})."</tt>");
print &ui_hidden("user", $uinfo{'user'}),"\n";
}
# User ID
if ($n ne "") {
# Existing user, just show field to edit
$uidfield = &ui_textbox("uid", $uinfo{'uid'}, 10);
}
else {
# Work out which UID modes are available
@uidmodes = ( );
$defuid = &allocate_uid(\%used);
if ($access{'autouid'}) {
push(@uidmodes, [ 1, $text{'uedit_uid_def'} ]);
}
if ($access{'calcuid'}) {
push(@uidmodes, [ 2, $text{'uedit_uid_calc'} ]);
}
if ($access{'useruid'}) {
push(@uidmodes, [ 0, &ui_textbox("uid", $defuid, 10) ]);
}
if (@uidmodes == 1) {
$uidfield = &ui_hidden("uid_def", $uidmodes[0]->[0]).
$uidmodes[0]->[1];
}
else {
$uidfield = &ui_radio("uid_def", $config{'uid_mode'},
\@uidmodes);
}
}
print &ui_table_row(&hlink($text{'uid'}, "uid"), $uidfield);
# Real name
if ($config{'extra_real'}) {
# Has separate name, office, work and home phone parts
local @real = split(/,/, $uinfo{'real'}, 5);
print &ui_table_row(&hlink($text{'real'}, "real"),
&ui_textbox("real", $real[0], 40));
print &ui_table_row(&hlink($text{'office'}, "office"),
&ui_textbox("office", $real[1], 20));
print &ui_table_row(&hlink($text{'workph'}, "workph"),
&ui_textbox("workph", $real[2], 20));
print &ui_table_row(&hlink($text{'homeph'}, "homeph"),
&ui_textbox("homeph", $real[3], 20));
print &ui_table_row(&hlink($text{'extra'}, "extra"),
&ui_textbox("extra", $real[4], 20));
}
else {
# Just a name
$uinfo{'real'} =~ s/,*$//; # Strip empty extra fields
print &ui_table_row(&hlink($text{'real'}, "real"),
&ui_textbox("real", $uinfo{'real'}, 40));
}
# Show input for home directory
if ($access{'autohome'}) {
# Automatic, cannot be changed
$homefield = $text{'uedit_auto'}.
($n eq "" ? "" : " ( <tt>$uinfo{'home'}</tt> )" );
}
else {
if ($config{'home_base'}) {
# Can be automatic
local $grp = &my_getgrgid($uinfo{'gid'});
local $hb = $n eq "" ||
&auto_home_dir($config{'home_base'},
$uinfo{'user'}, $grp) eq $uinfo{'home'};
$homefield = &ui_radio("home_base", $hb ? 1 : 0,
[ [ 1, $text{'uedit_auto'}."<br>" ],
[ 0, $text{'uedit_manual'}." ".
&ui_filebox("home", $hb ? "" : $uinfo{'home'},
40, 0, undef, undef, 1) ] ]);
}
else {
# Allow any directory
$homefield = &ui_filebox("home", $uinfo{'home'}, 25, 0,
undef, undef, 1);
}
}
print &ui_table_row(&hlink($text{'home'}, "home"),
$homefield);
# Show shell drop-down
push(@shlist, $uinfo{'shell'}) if ($n ne "" && $uinfo{'shell'});
if ($access{'shells'} ne "*") {
# Limit to shells from ACL
@shlist = $n ne "" ? ($uinfo{'shell'}) : ();
push(@shlist, split(/\s+/, $access{'shells'}));
$shells = 1;
}
$shells = 1 if ($access{'noother'});
@shlist = &unique(@shlist);
if ($n ne "" && !$uinfo{'shell'}) {
# No shell!
push(@shlist, [ "", "&lt;None&gt;" ]);
}
push(@shlist, [ "*", $text{'uedit_other'} ]) if (!$shells);
$firstshell = ref($shlist[0]) ? $shlist[0]->[0] : $shlist[0];
print &ui_table_row(&hlink($text{'shell'}, "shell"),
&ui_select("shell", $n eq "" ? $config{'default_shell'} || $firstshell
: $uinfo{'shell'},
\@shlist, 1, 0, 0, 0,
"onChange='form.othersh.disabled = form.shell.value != \"*\"'").
($shells ? "" : &ui_filebox("othersh", undef, 40, 1)));
# Get the password, generate random if needed
$pass = $in{'clone'} ne "" ? $uinfo{'pass'} :
$n ne "" ? $uinfo{'pass'} : $config{'lock_string'};
if ($n eq "" && $config{'random_password'}) {
$random_password = &generate_random_password();
}
# Check if temporary locking is supported
if (&supports_temporary_disable()) {
if ($n ne "" && $pass ne $config{'lock_string'} && $pass ne "") {
# Can disable if not already locked, or if a new account
$can_disable = 1;
if ($pass =~ /^\Q$disable_string\E/) {
$disabled = 1;
$pass =~ s/^\Q$disable_string\E//;
}
}
elsif ($n eq "") {
$can_disable = 1;
}
}
# Show password field
$passmode = $random_password ne "" ? 3 :
$pass eq "" ? 0 :
$pass eq $config{'lock_string'} ? 1 :
$pass && $pass ne $config{'lock_string'} ? 2 : -1;
$pffunc = $config{'passwd_stars'} ? \&ui_password : \&ui_textbox;
my @modes;
if ($passmode eq '0' || $config{'empty_mode'}) {
push(@modes, [ 0, $config{'empty_mode'} ? $text{'none1'} : $text{'none2'} ]);
}
push(@modes,[ 1, $text{'nologin'} ],
[ 3, $text{'clear'},
&$pffunc("pass", $config{'random_password'} && $n eq "" ?
$random_password : "", 15, undef, undef,
'autocomplete="off" autocorrect="off" '.
'autocapitalize="none"') ],
$access{'nocrypt'} ?
( [ 2, $text{'nochange'},
&ui_hidden("encpass", $pass) ] ) :
( [ 2, $text{'encrypted'},
&ui_textbox("encpass", $passmode == 2 ? $pass : "", 60) ] ));
print &ui_table_row(&hlink($text{'pass'}, "pass"),
&ui_radio_table("passmode", $passmode, \@modes).
($can_disable ? "&nbsp;&nbsp;".&ui_checkbox("disable", 1,
$text{'uedit_disabled'}, $disabled) : "")
);
# Show SSH public key field. Existing users only display the Webmin-managed
# key, identified by our marker in authorized_keys; unrelated keys stay hidden.
my $sshkey = $n ne '' ? &get_user_ssh_pubkey(\%uinfo) : undef;
print &ui_table_row(&hlink($text{'sshkey'}, "sshkey"),
&ui_textarea("sshkey", $sshkey, 4, 60), 3);
print &ui_table_end();
$pft = &passfiles_type();
if (($pft == 1 || $pft == 6) && $access{'peopt'}) {
# Additional user fields for BSD users
print &ui_table_start($text{'uedit_passopts'}, "width=100%", 4, \@tds);
# Last change date
if ($uinfo{'change'}) {
@tm = localtime($uinfo{'change'});
$cday = $tm[3];
$cmon = $tm[4]+1;
$cyear = $tm[5]+1900;
$chour = sprintf "%2.2d", $tm[2];
$cmin = sprintf "%2.2d", $tm[1];
}
print "<td>";
&date_input($cday, $cmon, $cyear, 'change');
print &ui_table_row(&hlink($text{'change2'}, "change2"),
&date_input($cday, $cmon, $cyear, 'change').
" ".&ui_textbox("changeh", $chour, 3).
":".&ui_textbox("changemi", $cmin, 3), 3);
# Expiry date
if ($n eq "") {
if ($config{'default_expire'} =~
/^(\d+)\/(\d+)\/(\d+)$/) {
$eday = $1;
$emon = $2;
$eyear = $3;
$ehour = "00";
$emin = "00";
}
}
elsif ($uinfo{'expire'}) {
@tm = localtime($uinfo{'expire'});
$eday = $tm[3];
$emon = $tm[4]+1;
$eyear = $tm[5]+1900;
$ehour = sprintf "%2.2d", $tm[2];
$emin = sprintf "%2.2d", $tm[1];
}
print &ui_table_row(&hlink($text{'expire2'}, "expire2"),
&date_input($eday, $emon, $eyear, 'expire').
" ".&ui_textbox("expireh", $ehour, 3).
":".&ui_textbox("expiremi", $emin, 3), 3);
# BSD login class
print &ui_table_row(&hlink($text{'class'}, "class"),
&ui_textbox("class", $uinfo{'class'}, 10));
print &ui_table_end();
}
elsif (($pft == 2 || $pft == 5) && $access{'peopt'}) {
# System has a shadow password file as well.. which means it supports
# password expiry and so on
print &ui_table_start($text{'uedit_passopts'}, "width=100%", 4, \@tds);
# Last change date
local $max = $n eq "" ? $config{'default_max'} : $uinfo{'max'};
print &ui_table_row(&hlink($text{'change'}, "change"),
($uinfo{'change'} ? &make_date(timelocal(
gmtime($uinfo{'change'} * 60*60*24)),1) :
$n eq "" ? $text{'uedit_never'} :
$text{'uedit_unknown'}));
if ($pft == 2) {
# Expiry date
if ($n eq "") {
if ($config{'default_expire'} =~
/^(\d+)\/(\d+)\/(\d+)$/) {
$eday = $1;
$emon = $2;
$eyear = $3;
}
}
elsif ($uinfo{'expire'}) {
@tm = localtime(timelocal(gmtime($uinfo{'expire'} *
60*60*24)));
$eday = $tm[3];
$emon = $tm[4]+1;
$eyear = $tm[5]+1900;
}
print &ui_table_row(&hlink($text{'expire'}, "expire"),
&date_input($eday, $emon, $eyear, 'expire'));
}
else {
# Ask at first login?
print &ui_table_row(&hlink($text{'ask'}, "ask"),
&ui_yesno_radio("ask", $uinfo{'change'} eq '0'));
}
# Minimum and maximum days for changing
print &ui_table_row(&hlink($text{'min'}, "min"),
&ui_textbox("min", $n eq "" ? $config{'default_min'} :
$uinfo{'min'}, 5));
print &ui_table_row(&hlink($text{'max'}, "max"),
&ui_textbox("max", $n eq "" ? $config{'default_max'} :
$uinfo{'max'}, 5));
if ($pft == 2) {
# Warning and inactive days. Only available when full shadow
# files are used
print &ui_table_row(&hlink($text{'warn'}, "warn"),
&ui_textbox("warn", $n eq "" ? $config{'default_warn'}
: $uinfo{'warn'}, 5));
print &ui_table_row(&hlink($text{'inactive'}, "inactive"),
&ui_textbox("inactive", $n eq "" ?
$config{'default_inactive'} :
$uinfo{'inactive'}, 5));
}
# Force change at next login
if (($max || $gconfig{'os_type'} =~ /-linux$/) && $pft == 2) {
print &ui_table_row(
&hlink($text{'uedit_forcechange'}, 'forcechange'),
&ui_yesno_radio("forcechange", 0));
}
print &ui_table_end();
}
elsif ($pft == 4 && $access{'peopt'}) {
# System has extra AIX password information
print &ui_table_start($text{'uedit_passopts'}, "width=100%", 4, \@tds);
# Last change date and time
print &ui_table_row(&hlink($text{'change'}, "change"),
($uinfo{'change'} ? &make_date($uinfo{'change'}) :
$n eq "" ? $text{'uedit_never'} :
$text{'uedit_unknown'}));
if ($uinfo{'expire'} =~ /^(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/) {
$emon = $1;
$eday = $2;
$ehour = $3;
$emin = $4;
$eyear = $5;
if ($eyear > 38) {
$eyear += 1900;
}
else {
$eyear += 2000;
}
}
$emon =~ s/0(\d)/$1/; # strip leading 0
print &ui_table_row(&hlink($text{'expire'}, "expire"),
&ui_radio("expire_def", $uinfo{'expire'} eq '' ? 1 :
$uinfo{'expire'} eq '0' ? 2 : 0,
[ [ 1, $text{'uedit_sys'} ],
[ 2, $text{'uedit_never'} ],
[ 0, &date_input($eday, $emon, $eyear, 'expire').
" ".&ui_textbox("expireh", $ehour, 3).
"/".&ui_textbox("expiremi", $emin, 3) ] ]), 3);
# Minimum and maximum ages in weeks
print &ui_table_row(&hlink($text{'min_weeks'}, "min_weeks"),
&ui_opt_textbox("min", $uinfo{'min'}, 5, $text{'uedit_sys'}), 3);
print &ui_table_row(&hlink($text{'max_weeks'}, "max_weeks"),
&ui_opt_textbox("max", $uinfo{'max'}, 5, $text{'uedit_sys'}), 3);
# Warning days
print &ui_table_row(&hlink($text{'warn'}, "warn"),
&ui_opt_textbox("warn", $uinfo{'warn'}, 5, $text{'uedit_sys'}), 3);
# AIX-specific flags
print &ui_table_row(&hlink($text{'flags'}, "flags"),
&ui_checkbox("flags", "admin", $text{'uedit_admin'},
$uinfo{'admin'})."<br>".
&ui_checkbox("flags", "admchg", $text{'uedit_admchg'},
$uinfo{'admchg'})."<br>".
&ui_checkbox("flags", "nocheck", $text{'uedit_nocheck'},
$uinfo{'nocheck'}), 3);
print &ui_table_end();
}
# Group memberships section
print &ui_table_start($text{'uedit_gmem'}, "width=100%", 4, \@tds);
# Primary group
@groupopts = ( );
$gidmode = 0;
if ($n eq "" && $access{'gcreate'}) {
# Has option to create a group
push(@groupopts, [ 2, $text{'uedit_samg'} ]);
push(@groupopts, [ 1, $text{'uedit_newg'},
&ui_textbox("newgid", undef, 20) ]);
$gidmode = $config{'new_user_group'} ? 2 : 0;
}
if ($access{'ugroups'} eq "*" || $access{'uedit_gmode'} >= 3) {
# Group can be chosen with popup window
local $w = 300;
local $h = 200;
if ($gconfig{'db_sizeuser'}) {
($w, $h) = split(/x/, $gconfig{'db_sizeuser'});
}
push(@groupopts, [ 0, $text{'uedit_oldg'},
&ui_textbox("gid", $n eq "" ? $config{'default_group'} :
scalar(&my_getgrgid($uinfo{'gid'})), 13)." ".
&popup_window_button("my_group_chooser.cgi?multi=0", $w, $h,
1, [ [ "ifield", "gid", "group" ] ]) ]);
}
else {
# From fixed menu of groups
$cg = $uinfo{'gid'} ? &my_getgrgid($uinfo{'gid'}) : undef;
@gl = &unique($cg ? ($cg) : (),
&split_quoted_string($access{'ugroups'}));
push(@groupopts, [ 0, $text{'uedit_oldg'},
&ui_select("gid", $cg, \@gl) ]);
}
if (@groupopts == 1) {
$groupfield = $groupopts[0]->[2];
}
else {
$groupfield = &ui_radio_table("gidmode", $gidmode, \@groupopts);
}
print &ui_table_row(&hlink($text{'group'}, "group"), $groupfield, 3);
# Work out which secondary groups the user is in
if ($config{'secmode'} != 1) {
@defsecs = &split_quoted_string($config{'default_secs'});
@glist = &list_groups();
@glist = &sort_groups(\@glist, $config{'sort_mode'});
%ingroups = ( );
foreach $g (@glist) {
@mems = split(/,/ , $g->{'members'});
$ismem = &indexof($uinfo{'user'}, @mems) >= 0;
if ($in{'clone'} ne '') {
$ismem ||= &indexof($in{'clone'}, @mems) >= 0;
}
if ($n eq "") {
$ismem = 1 if (&indexof($g->{'group'}, @defsecs) >= 0);
}
$ingroups{$g->{'group'}} = $ismem;
}
}
if ($config{'secmode'} == 0) {
# Show secondary groups with select menu
@canglist = ( );
foreach $g (@glist) {
next if (!&can_use_group(\%access, $g->{'group'}) &&
!$ingroups{$g->{'group'}});
push(@canglist, [ $g->{'group'}, &html_escape($g->{'group'}) ]);
}
@ingroups = map { [ $_, $_ ] } sort { $a cmp $b }
grep { $ingroups{$_} } (keys %ingroups);
$secfield = &ui_multi_select("sgid", \@ingroups, \@canglist, 5, 1, 0,
$text{'uedit_allg'}, $text{'uedit_ing'});
}
elsif ($config{'secmode'} == 2) {
# Show a text box
@insecs = ( );
foreach $g (@glist) {
if ($ingroups{$g->{'group'}}) {
push(@insecs, $g->{'group'});
}
}
$secfield = &ui_textarea("sgid", join("\n", @insecs), 5, 20);
}
else {
# Don't show
$secfield = undef;
}
if ($secfield) {
print &ui_table_row(&hlink($text{'uedit_2nd'}, "2nd"), $secfield, 3);
}
print &ui_table_end();
if ($n ne "") {
# Editing a user - show options for moving home directory, changing IDs
# and updating in other modules
if ($access{'movehome'} == 1 || $access{'chuid'} == 1 ||
$access{'chgid'} == 1 || $access{'mothers'} == 1) {
print &ui_table_start($text{'onsave'}, "width=100%", 2, \@tds);
# Move home directory
if ($access{'movehome'} == 1) {
print &ui_table_row(
&hlink($text{'uedit_movehome'}, "movehome"),
&ui_yesno_radio("movehome", 1));
}
# Change UID on files
if ($access{'chuid'} == 1) {
print &ui_table_row(
&hlink($text{'uedit_chuid'},"chuid"),
&ui_radio("chuid", 1,
[ [ 0, $text{'no'} ],
[ 1, $text{'home'} ],
[ 2, $text{'uedit_allfiles'} ] ]));
}
# Change GID on files
if ($access{'chgid'} == 1) {
print &ui_table_row(
&hlink($text{'uedit_chgid'},"chgid"),
&ui_radio("chgid", 1,
[ [ 0, $text{'no'} ],
[ 1, $text{'home'} ],
[ 2, $text{'uedit_allfiles'} ] ]));
}
# Modify in other modules
if ($access{'mothers'} == 1) {
print &ui_table_row(
&hlink($text{'uedit_mothers'},"others"),
&ui_yesno_radio("others",
$config{'default_other'} ? 1 : 0));
}
# Rename group, if the same and if editable
@ginfo = &my_getgrgid($uinfo{'gid'});
if ($ginfo[0] eq $uinfo{'user'}) {
($group) = grep { $_->{'gid'} == $uinfo{'gid'} }
&list_groups();
if (&can_edit_group(\%access, $group)) {
print &ui_table_row(
&hlink($text{'uedit_grename'},"grename"),
&ui_yesno_radio("grename", 1));
}
}
print &ui_table_end(),"\n";
}
}
else {
# Creating a user - show options for creating home directory, copying
# skel files and creating in other modules
if ($access{'makehome'} == 1 || $access{'copy'} == 1 ||
$access{'cothers'} == 1) {
print &ui_table_start($text{'uedit_oncreate'}, "width=100%",
2, \@tds);
# Create home dir
if ($access{'makehome'} == 1) {
print &ui_table_row(
&hlink($text{'uedit_makehome'}, "makehome"),
&ui_yesno_radio("makehome", 1));
}
# Copy skel files
if ($config{'user_files'} =~ /\S/ && $access{'copy'} == 1) {
print &ui_table_row(
&hlink($text{'uedit_copy'}, "copy_files"),
&ui_yesno_radio("copy_files", 1));
}
# Create in other modules
if ($access{'cothers'} == 1) {
print &ui_table_row(
&hlink($text{'uedit_cothers'},"others"),
&ui_yesno_radio("others",
$config{'default_other'}));
}
print &ui_table_end();
}
}
if ($n ne "") {
# Buttons for saving and other actions
@buts = ( [ undef, $text{'save'} ] );
# List logins by user
push(@buts, [ "list", $text{'uedit_logins'} ]);
# Link to the mailboxes module, if installed
if (&foreign_available("mailboxes") &&
&foreign_installed("mailboxes", 1)) {
push(@buts, [ "mailboxes", $text{'uedit_mail'} ]);
}
# Link to Usermin for switching user
if (&foreign_available("usermin") &&
&foreign_installed("usermin", 1) &&
(%uacl = &get_module_acl("usermin") &&
$uacl{'sessions'})) {
# Link to Usermin module for switching to some user
&foreign_require("usermin", "usermin-lib.pl");
local %uminiserv;
&usermin::get_usermin_miniserv_config(\%uminiserv);
if ($uminiserv{'session'}) {
push(@buts, [ "switch", $text{'uedit_swit'}, undef, 0,
"onClick='form.target=\"_blank\"'" ]);
}
}
# Clone user
if ($access{'ucreate'}) {
push(@buts, [ "clone", $text{'uedit_clone'} ]);
}
# Delete user
if ($access{'udelete'}) {
push(@buts, [ "delete", $text{'delete'} ]);
}
print &ui_form_end(\@buts);
}
else {
# Create button
print &ui_form_end([ [ undef, $text{'create'} ] ]);
}
&ui_print_footer("index.cgi?mode=users", $text{'index_return'});