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