]> git.proxmox.com Git - pmg-api.git/blame - PMG/UserConfig.pm
PMG/UserConfig.pm: cleanup schma definitions
[pmg-api.git] / PMG / UserConfig.pm
CommitLineData
62ebb4bc
DM
1package PMG::UserConfig;
2
3
4use strict;
5use warnings;
fff8e89c
DM
6use Data::Dumper;
7use Clone 'clone';
62ebb4bc
DM
8
9use PVE::Tools;
10use PVE::INotify;
c861cb3a
DM
11use PVE::JSONSchema qw(get_standard_option);
12use PVE::Exception qw(raise);
62ebb4bc
DM
13
14use PMG::Utils;
15
16my $inotify_file_id = 'pmg-user.conf';
17my $config_filename = '/etc/pmg/user.conf';
18
19sub new {
20 my ($type) = @_;
21
22 my $class = ref($type) || $type;
23
24 my $cfg = PVE::INotify::read_file($inotify_file_id);
25
26 return bless $cfg, $class;
27}
28
29sub write {
30 my ($self) = @_;
31
32 PVE::INotify::write_file($inotify_file_id, $self);
33}
34
35my $lockfile = "/var/lock/pmguser.lck";
36
37sub lock_config {
38 my ($code, $errmsg) = @_;
39
40 my $p = PVE::Tools::lock_file($lockfile, undef, $code);
41 if (my $err = $@) {
42 $errmsg ? die "$errmsg: $err" : die $err;
43 }
44}
45
8333a87c 46my $schema = {
c861cb3a
DM
47 additionalProperties => 0,
48 properties => {
4d813470 49 userid => get_standard_option('userid'),
0c7e2ca4 50 username => get_standard_option('username', { optional => 1 }),
1beed876
DM
51 realm => {
52 description => "Authentication realm.",
53 type => 'string',
54 enum => ['pam', 'pmg'],
55 default => 'pmg',
56 optional => 1,
57 },
c861cb3a
DM
58 email => {
59 description => "Users E-Mail address.",
fff8e89c 60 type => 'string', format => 'email',
c861cb3a
DM
61 optional => 1,
62 },
63 expire => {
fff8e89c 64 description => "Account expiration date (seconds since epoch). '0' means no expiration date.",
c861cb3a
DM
65 type => 'integer',
66 minimum => 0,
fff8e89c
DM
67 default => 0,
68 optional => 1,
c861cb3a
DM
69 },
70 enable => {
71 description => "Flag to enable or disable the account.",
72 type => 'boolean',
fff8e89c
DM
73 default => 0,
74 optional => 1,
c861cb3a
DM
75 },
76 crypt_pass => {
77 description => "Encrypted password (see `man crypt`)",
78 type => 'string',
79 pattern => '\$\d\$[a-zA-Z0-9\.\/]+\$[a-zA-Z0-9\.\/]+',
80 optional => 1,
81 },
82 role => {
6006ffde 83 description => "User role. Role 'root' is reseved for the Unix Superuser.",
c861cb3a 84 type => 'string',
6006ffde 85 enum => ['root', 'admin', 'qmanager', 'audit'],
c861cb3a 86 },
aec44b24 87 firstname => {
c861cb3a
DM
88 description => "First name.",
89 type => 'string',
90 maxLength => 64,
91 optional => 1,
92 },
aec44b24 93 lastname => {
c861cb3a
DM
94 description => "Last name.",
95 type => 'string',
96 maxLength => 64,
97 optional => 1,
98 },
99 keys => {
fff8e89c 100 description => "Keys for two factor auth (yubico).",
c861cb3a
DM
101 type => 'string',
102 maxLength => 128,
103 optional => 1,
104 },
fff8e89c
DM
105 comment => {
106 description => "Comment.",
107 type => 'string',
108 optional => 1,
109 },
c861cb3a
DM
110 },
111};
112
8333a87c
DM
113our $create_schema = clone($schema);
114delete $create_schema->{properties}->{username};
115delete $create_schema->{properties}->{realm};
116
fff8e89c
DM
117our $update_schema = clone($schema);
118$update_schema->{properties}->{role}->{optional} = 1;
8333a87c
DM
119delete $update_schema->{properties}->{username};
120delete $update_schema->{properties}->{realm};
0ecf02bc
DM
121$update_schema->{properties}->{delete} = {
122 type => 'string', format => 'pve-configid-list',
123 description => "A list of settings you want to delete.",
124 maxLength => 4096,
125 optional => 1,
126};
fff8e89c 127
c861cb3a
DM
128my $verity_entry = sub {
129 my ($entry) = @_;
130
131 my $errors = {};
132 PVE::JSONSchema::check_prop($entry, $schema, '', $errors);
133 if (scalar(%$errors)) {
134 raise "verify entry failed\n", errors => $errors;
135 }
136};
137
1461db83
DM
138my $fixup_root_properties = sub {
139 my ($cfg) = @_;
140
141 $cfg->{'root@pam'}->{userid} = 'root@pam';
0c7e2ca4 142 $cfg->{'root@pam'}->{username} = 'root';
1beed876 143 $cfg->{'root@pam'}->{realm} = 'pam';
1461db83
DM
144 $cfg->{'root@pam'}->{enable} = 1;
145 $cfg->{'root@pam'}->{expire} = 0;
146 $cfg->{'root@pam'}->{comment} = 'Unix Superuser';
147 $cfg->{'root@pam'}->{role} = 'root';
148 delete $cfg->{'root@pam'}->{crypt_pass};
149};
150
62ebb4bc
DM
151sub read_user_conf {
152 my ($filename, $fh) = @_;
153
154 my $cfg = {};
155
156 if ($fh) {
157
158 my $comment = '';
159
160 while (defined(my $line = <$fh>)) {
161 next if $line =~ m/^\s*$/;
162 if ($line =~ m/^#(.*)$/) {
163 $comment = $1;
164 next;
165 }
c861cb3a
DM
166
167 if ($line =~ m/^
168 (?<userid>(?:[^\s:]+)) :
fff8e89c
DM
169 (?<enable>[01]?) :
170 (?<expire>\d*) :
171 (?<crypt_pass>(?:[^\s:]*)) :
c861cb3a
DM
172 (?<role>[a-z]+) :
173 (?<email>(?:[^\s:]*)) :
aec44b24
DM
174 (?<firstname>(?:[^:]*)) :
175 (?<lastname>(?:[^:]*)) :
c861cb3a
DM
176 (?<keys>(?:[^:]*)) :
177 $/x
178 ) {
62ebb4bc 179 my $d = {
0c7e2ca4 180 username => $+{userid},
4d813470 181 userid => $+{userid} . '@pmg',
1beed876 182 realm => 'pmg',
fff8e89c
DM
183 enable => $+{enable} || 0,
184 expire => $+{expire} || 0,
c861cb3a 185 role => $+{role},
62ebb4bc
DM
186 };
187 $d->{comment} = $comment if $comment;
188 $comment = '';
aec44b24 189 foreach my $k (qw(crypt_pass email firstname lastname keys)) {
c861cb3a
DM
190 $d->{$k} = $+{$k} if $+{$k};
191 }
192 eval {
193 $verity_entry->($d);
194 $cfg->{$d->{userid}} = $d;
6006ffde
DM
195 die "role 'root' is reserved\n"
196 if $d->{role} eq 'root' && $d->{userid} ne 'root@pmg';
c861cb3a
DM
197 };
198 if (my $err = $@) {
199 warn "$filename: $err";
200 }
62ebb4bc
DM
201 } else {
202 warn "$filename: ignore invalid line $.\n";
203 $comment = '';
204 }
205 }
206 }
207
82613302
DM
208 # hack: we list root@pam here (root@pmg is an alias for root@pam)
209 $cfg->{'root@pam'} = $cfg->{'root@pmg'} // {};
210 delete $cfg->{'root@pmg'};
211 $cfg->{'root@pam'} //= {};
1461db83
DM
212
213 $fixup_root_properties->($cfg);
62ebb4bc
DM
214
215 return $cfg;
216}
217
218sub write_user_conf {
219 my ($filename, $fh, $cfg) = @_;
220
221 my $raw = '';
222
1461db83 223 $fixup_root_properties->($cfg);
fff8e89c
DM
224
225 foreach my $userid (keys %$cfg) {
226 my $d = $cfg->{$userid};
82613302 227
fff8e89c 228 $d->{userid} = $userid;
4d813470 229
cf009a08 230 die "invalid userid '$userid'\n" if $userid eq 'root@pmg';
7e818421 231
fff8e89c
DM
232 eval {
233 $verity_entry->($d);
234 $cfg->{$d->{userid}} = $d;
6006ffde 235
1beed876
DM
236 if ($d->{userid} ne 'root@pam') {
237 die "role 'root' is reserved\n" if $d->{role} eq 'root';
238 die "unable to add users for realm '$d->{realm}'\n"
b5dc7933 239 if $d->{realm} && $d->{realm} ne 'pmg';
1beed876 240 }
fff8e89c
DM
241 };
242 if (my $err = $@) {
243 die $err;
244 }
4d813470 245
82613302
DM
246 my $line;
247
248 if ($userid eq 'root@pam') {
249 $line = 'root:';
250 $d->{crypt_pass} = '',
05e34f94 251 $d->{expire} = '0',
82613302
DM
252 $d->{role} = 'root';
253 } else {
254 next if $userid !~ m/^(?<username>.+)\@pmg$/;
255 $line = "$+{username}:";
256 }
4d813470 257
aec44b24 258 for my $k (qw(enable expire crypt_pass role email firstname lastname keys)) {
fff8e89c
DM
259 $line .= ($d->{$k} // '') . ':';
260 }
0ecf02bc
DM
261 if (my $comment = $d->{comment}) {
262 my $firstline = (split /\n/, $comment)[0]; # only allow one line
263 $raw .= "#$firstline\n";
264 }
fff8e89c
DM
265 $raw .= $line . "\n";
266 }
62ebb4bc
DM
267
268 PVE::Tools::safe_print($filename, $fh, $raw);
269}
270
271PVE::INotify::register_file($inotify_file_id, $config_filename,
272 \&read_user_conf,
273 \&write_user_conf,
274 undef,
275 always_call_parser => 1);
276
277sub lookup_user_data {
278 my ($self, $username, $noerr) = @_;
279
280 return $self->{$username} if $self->{$username};
281
282 die "no such user ('$username')\n" if !$noerr;
283
284 return undef;
285}
286
287sub authenticate_user {
288 my ($self, $username, $password) = @_;
289
290 die "no password\n" if !$password;
291
292 my $data = $self->lookup_user_data($username);
293
c861cb3a
DM
294 my $ctime = time();
295 my $expire = $data->{expire};
296
297 die "account expired\n" if $expire && ($expire < $ctime);
298
62ebb4bc
DM
299 if ($data->{crypt_pass}) {
300 my $encpw = crypt($password, $data->{crypt_pass});
301 die "invalid credentials\n" if ($encpw ne $data->{crypt_pass});
302 } else {
303 die "no password set\n";
304 }
305
306 return 1;
307}
308
be06bc52 309sub set_user_password {
62ebb4bc
DM
310 my ($class, $username, $password) = @_;
311
312 lock_config(sub {
313 my $cfg = $class->new();
314 my $data = $cfg->lookup_user_data($username); # user exists
315 my $epw = PMG::Utils::encrypt_pw($password);
316 $data->{crypt_pass} = $epw;
317 $cfg->write();
318 });
319}
320
3211;