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