]>
git.proxmox.com Git - pmg-api.git/blob - src/PMG/UserConfig.pm
1 package PMG
::UserConfig
;
7 use Scalar
::Util
'weaken';
11 use PVE
::JSONSchema
qw(get_standard_option);
12 use PVE
::Exception
qw(raise);
16 my $inotify_file_id = 'pmg-user.conf';
17 my $config_filename = '/etc/pmg/user.conf';
19 my $tfa_inotify_file_id = 'pmg-tfa.json';
20 my $tfa_config_filename = '/etc/pmg/tfa.json';
25 my $class = ref($type) || $type;
27 my $cfg = PVE
::INotify
::read_file
($inotify_file_id);
29 return bless $cfg, $class;
35 PVE
::INotify
::write_file
($inotify_file_id, $self);
38 my $lockfile = "/var/lock/pmguser.lck";
39 my $tfa_lockfile = "/var/lock/pmgtfa.lck";
41 # Locking both config files together is only ever allowed in one order:
44 # If we permit the other way round, too, we might end up deadlocking!
45 my $user_config_locked;
47 my ($code, $errmsg) = @_;
50 $user_config_locked = \
$locked;
51 weaken
$user_config_locked; # make this scope guard signal safe...
53 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
54 $user_config_locked = undef;
56 $errmsg ?
die "$errmsg: $err" : die $err;
60 # This lives here in order to enforce lock order.
62 my ($code, $errmsg) = @_;
64 die "tfa config lock cannot be acquired while holding user config lock\n"
65 if ($user_config_locked && $$user_config_locked);
67 my $res = PVE
::Tools
::lock_file
($tfa_lockfile, undef, $code);
69 $errmsg ?
die "$errmsg: $err" : die $err;
76 additionalProperties
=> 0,
78 userid
=> get_standard_option
('userid'),
79 username
=> get_standard_option
('username', { optional
=> 1 }),
81 description
=> "Authentication realm.",
83 enum
=> ['pam', 'pmg'],
88 description
=> "Users E-Mail address.",
89 type
=> 'string', format
=> 'email',
93 description
=> "Account expiration date (seconds since epoch). '0' means no expiration date.",
100 description
=> "Flag to enable or disable the account.",
106 description
=> "Encrypted password (see `man crypt`)",
108 pattern
=> '\$\d\$[a-zA-Z0-9\.\/]+\$[a-zA-Z0-9\.\/]+',
112 description
=> "User role. Role 'root' is reserved for the Unix Superuser.",
114 enum
=> ['root', 'admin', 'helpdesk', 'qmanager', 'audit'],
117 description
=> "First name.",
123 description
=> "Last name.",
129 description
=> "Keys for two factor auth (yubico).",
135 description
=> "Comment.",
142 our $create_schema = clone
($schema);
143 delete $create_schema->{properties
}->{username
};
144 delete $create_schema->{properties
}->{realm
};
145 $create_schema->{properties
}->{password
} = {
146 description
=> "Password",
153 our $update_schema = clone
($create_schema);
154 $update_schema->{properties
}->{role}->{optional
} = 1;
155 $update_schema->{properties
}->{delete} = {
156 type
=> 'string', format
=> 'pve-configid-list',
157 description
=> "A list of settings you want to delete.",
162 my $verify_entry = sub {
166 my $userid = $entry->{userid
};
167 if (defined(my $username = $entry->{username
})) {
168 if ($userid !~ /^\Q$username\E\@/) {
169 $errors->{'username'} = 'invalid username for userid';
172 # make sure the username's length is checked
173 $entry->{username
} = ($userid =~ s/\@.*$//r);
175 PVE
::JSONSchema
::check_prop
($entry, $schema, '', $errors);
176 if (scalar(%$errors)) {
177 raise
"verify entry failed\n", errors
=> $errors;
181 my $fixup_root_properties = sub {
184 $cfg->{'root@pam'}->{userid
} = 'root@pam';
185 $cfg->{'root@pam'}->{username
} = 'root';
186 $cfg->{'root@pam'}->{realm
} = 'pam';
187 $cfg->{'root@pam'}->{enable
} = 1;
188 $cfg->{'root@pam'}->{expire
} = 0;
189 $cfg->{'root@pam'}->{comment
} = 'Unix Superuser';
190 $cfg->{'root@pam'}->{role} = 'root';
191 delete $cfg->{'root@pam'}->{crypt_pass
};
195 my ($filename, $fh) = @_;
203 while (defined(my $line = <$fh>)) {
204 next if $line =~ m/^\s*$/;
205 if ($line =~ m/^#(.*)$/) {
211 (?
<userid
>(?
:[^\s
:]+)) :
214 (?
<crypt_pass
>(?
:[^\s
:]*)) :
216 (?
<email
>(?
:[^\s
:]*)) :
217 (?
<firstname
>(?
:[^:]*)) :
218 (?
<lastname
>(?
:[^:]*)) :
223 username
=> $+{userid
},
224 userid
=> $+{userid
} . '@pmg',
226 enable
=> $+{enable
} || 0,
227 expire
=> $+{expire
} || 0,
230 $d->{comment
} = $comment if $comment;
232 foreach my $k (qw(crypt_pass email firstname lastname keys)) {
233 $d->{$k} = $+{$k} if $+{$k};
237 $cfg->{$d->{userid
}} = $d;
238 die "role 'root' is reserved\n"
239 if $d->{role} eq 'root' && $d->{userid
} ne 'root@pmg';
242 warn "$filename: $err";
245 warn "$filename: ignore invalid line $.\n";
251 # hack: we list root@pam here (root@pmg is an alias for root@pam)
252 $cfg->{'root@pam'} = $cfg->{'root@pmg'} // {};
253 delete $cfg->{'root@pmg'};
254 $cfg->{'root@pam'} //= {};
256 $fixup_root_properties->($cfg);
261 sub write_user_conf
{
262 my ($filename, $fh, $cfg) = @_;
266 $fixup_root_properties->($cfg);
268 foreach my $userid (keys %$cfg) {
269 my $d = $cfg->{$userid};
271 $d->{userid
} = $userid;
273 die "invalid userid '$userid'\n" if $userid eq 'root@pmg';
275 $cfg->{$d->{userid
}} = $d;
277 if ($d->{userid
} ne 'root@pam') {
278 die "role 'root' is reserved\n" if $d->{role} eq 'root';
279 die "unable to add users for realm '$d->{realm}'\n"
280 if $d->{realm
} && $d->{realm
} ne 'pmg';
285 if ($userid eq 'root@pam') {
287 $d->{crypt_pass
} = '',
291 next if $userid !~ m/^(?<username>.+)\@pmg$/;
292 $line = "$+{username}:";
295 for my $k (qw(enable expire crypt_pass role email firstname lastname keys)) {
296 $line .= ($d->{$k} // '') . ':';
298 if (my $comment = $d->{comment
}) {
299 my $firstline = (split /\n/, $comment)[0]; # only allow one line
300 $raw .= "#$firstline\n";
302 $raw .= $line . "\n";
305 my $gid = getgrnam('www-data');
309 PVE
::Tools
::safe_print
($filename, $fh, $raw);
312 PVE
::INotify
::register_file
($inotify_file_id, $config_filename,
316 always_call_parser
=> 1);
318 sub lookup_user_data
{
319 my ($self, $username, $noerr) = @_;
321 return $self->{$username} if $self->{$username};
323 die "no such user ('$username')\n" if !$noerr;
328 sub authenticate_user
{
329 my ($self, $username, $password) = @_;
331 die "no password\n" if !$password;
333 my $data = $self->lookup_user_data($username);
336 my $expire = $data->{expire
};
338 die "account expired\n" if $expire && ($expire < $ctime);
340 if ($data->{crypt_pass
}) {
341 my $encpw = crypt($password, $data->{crypt_pass
});
342 die "invalid credentials\n" if ($encpw ne $data->{crypt_pass
});
344 die "no password set\n";
350 sub set_user_password
{
351 my ($class, $username, $password) = @_;
354 my $cfg = $class->new();
355 my $data = $cfg->lookup_user_data($username); # user exists
356 my $epw = PVE
::Tools
::encrypt_pw
($password);
357 $data->{crypt_pass
} = $epw;