]> git.proxmox.com Git - pmg-api.git/blame - PMG/UserConfig.pm
PMG/UserConfig.pm: use better property names
[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
fff8e89c 46our $schema = {
c861cb3a
DM
47 additionalProperties => 0,
48 properties => {
4d813470 49 userid => get_standard_option('userid'),
c861cb3a
DM
50 email => {
51 description => "Users E-Mail address.",
fff8e89c 52 type => 'string', format => 'email',
c861cb3a
DM
53 optional => 1,
54 },
55 expire => {
fff8e89c 56 description => "Account expiration date (seconds since epoch). '0' means no expiration date.",
c861cb3a
DM
57 type => 'integer',
58 minimum => 0,
fff8e89c
DM
59 default => 0,
60 optional => 1,
c861cb3a
DM
61 },
62 enable => {
63 description => "Flag to enable or disable the account.",
64 type => 'boolean',
fff8e89c
DM
65 default => 0,
66 optional => 1,
c861cb3a
DM
67 },
68 crypt_pass => {
69 description => "Encrypted password (see `man crypt`)",
70 type => 'string',
71 pattern => '\$\d\$[a-zA-Z0-9\.\/]+\$[a-zA-Z0-9\.\/]+',
72 optional => 1,
73 },
74 role => {
75 description => "User role.",
76 type => 'string',
77 enum => ['root', 'admin', 'qmanager', 'quser', 'audit'],
78 },
aec44b24 79 firstname => {
c861cb3a
DM
80 description => "First name.",
81 type => 'string',
82 maxLength => 64,
83 optional => 1,
84 },
aec44b24 85 lastname => {
c861cb3a
DM
86 description => "Last name.",
87 type => 'string',
88 maxLength => 64,
89 optional => 1,
90 },
91 keys => {
fff8e89c 92 description => "Keys for two factor auth (yubico).",
c861cb3a
DM
93 type => 'string',
94 maxLength => 128,
95 optional => 1,
96 },
fff8e89c
DM
97 comment => {
98 description => "Comment.",
99 type => 'string',
100 optional => 1,
101 },
c861cb3a
DM
102 },
103};
104
fff8e89c
DM
105our $update_schema = clone($schema);
106$update_schema->{properties}->{role}->{optional} = 1;
0ecf02bc
DM
107$update_schema->{properties}->{delete} = {
108 type => 'string', format => 'pve-configid-list',
109 description => "A list of settings you want to delete.",
110 maxLength => 4096,
111 optional => 1,
112};
fff8e89c 113
c861cb3a
DM
114my $verity_entry = sub {
115 my ($entry) = @_;
116
117 my $errors = {};
118 PVE::JSONSchema::check_prop($entry, $schema, '', $errors);
119 if (scalar(%$errors)) {
120 raise "verify entry failed\n", errors => $errors;
121 }
122};
123
62ebb4bc
DM
124sub read_user_conf {
125 my ($filename, $fh) = @_;
126
127 my $cfg = {};
128
129 if ($fh) {
130
131 my $comment = '';
132
133 while (defined(my $line = <$fh>)) {
134 next if $line =~ m/^\s*$/;
135 if ($line =~ m/^#(.*)$/) {
136 $comment = $1;
137 next;
138 }
c861cb3a
DM
139
140 if ($line =~ m/^
141 (?<userid>(?:[^\s:]+)) :
fff8e89c
DM
142 (?<enable>[01]?) :
143 (?<expire>\d*) :
144 (?<crypt_pass>(?:[^\s:]*)) :
c861cb3a
DM
145 (?<role>[a-z]+) :
146 (?<email>(?:[^\s:]*)) :
aec44b24
DM
147 (?<firstname>(?:[^:]*)) :
148 (?<lastname>(?:[^:]*)) :
c861cb3a
DM
149 (?<keys>(?:[^:]*)) :
150 $/x
151 ) {
62ebb4bc 152 my $d = {
4d813470 153 userid => $+{userid} . '@pmg',
fff8e89c
DM
154 enable => $+{enable} || 0,
155 expire => $+{expire} || 0,
c861cb3a 156 role => $+{role},
62ebb4bc
DM
157 };
158 $d->{comment} = $comment if $comment;
159 $comment = '';
aec44b24 160 foreach my $k (qw(crypt_pass email firstname lastname keys)) {
c861cb3a
DM
161 $d->{$k} = $+{$k} if $+{$k};
162 }
163 eval {
164 $verity_entry->($d);
165 $cfg->{$d->{userid}} = $d;
166 };
167 if (my $err = $@) {
168 warn "$filename: $err";
169 }
62ebb4bc
DM
170 } else {
171 warn "$filename: ignore invalid line $.\n";
172 $comment = '';
173 }
174 }
175 }
176
177 $cfg->{root} //= {};
4d813470 178 $cfg->{root}->{userid} = 'root@pam'; # hack: we list root@pam here
62ebb4bc
DM
179 $cfg->{root}->{enable} = 1;
180 $cfg->{root}->{comment} = 'Unix Superuser';
181 $cfg->{root}->{role} = 'root';
182 delete $cfg->{root}->{crypt_pass};
183
184 return $cfg;
185}
186
187sub write_user_conf {
188 my ($filename, $fh, $cfg) = @_;
189
190 my $raw = '';
191
fff8e89c
DM
192 delete $cfg->{root}->{crypt_pass};
193
194 foreach my $userid (keys %$cfg) {
195 my $d = $cfg->{$userid};
196 $d->{userid} = $userid;
4d813470 197
fff8e89c
DM
198 eval {
199 $verity_entry->($d);
200 $cfg->{$d->{userid}} = $d;
201 };
202 if (my $err = $@) {
203 die $err;
204 }
4d813470
DM
205 next if $userid !~ m/^(?<username>.+)\@pmg$/;
206
207 my $line = "$+{username}:";
208
aec44b24 209 for my $k (qw(enable expire crypt_pass role email firstname lastname keys)) {
fff8e89c
DM
210 $line .= ($d->{$k} // '') . ':';
211 }
0ecf02bc
DM
212 if (my $comment = $d->{comment}) {
213 my $firstline = (split /\n/, $comment)[0]; # only allow one line
214 $raw .= "#$firstline\n";
215 }
fff8e89c
DM
216 $raw .= $line . "\n";
217 }
62ebb4bc
DM
218
219 PVE::Tools::safe_print($filename, $fh, $raw);
220}
221
222PVE::INotify::register_file($inotify_file_id, $config_filename,
223 \&read_user_conf,
224 \&write_user_conf,
225 undef,
226 always_call_parser => 1);
227
228sub lookup_user_data {
229 my ($self, $username, $noerr) = @_;
230
231 return $self->{$username} if $self->{$username};
232
233 die "no such user ('$username')\n" if !$noerr;
234
235 return undef;
236}
237
238sub authenticate_user {
239 my ($self, $username, $password) = @_;
240
241 die "no password\n" if !$password;
242
243 my $data = $self->lookup_user_data($username);
244
c861cb3a
DM
245 my $ctime = time();
246 my $expire = $data->{expire};
247
248 die "account expired\n" if $expire && ($expire < $ctime);
249
62ebb4bc
DM
250 if ($data->{crypt_pass}) {
251 my $encpw = crypt($password, $data->{crypt_pass});
252 die "invalid credentials\n" if ($encpw ne $data->{crypt_pass});
253 } else {
254 die "no password set\n";
255 }
256
257 return 1;
258}
259
260sub set_password {
261 my ($class, $username, $password) = @_;
262
263 lock_config(sub {
264 my $cfg = $class->new();
265 my $data = $cfg->lookup_user_data($username); # user exists
266 my $epw = PMG::Utils::encrypt_pw($password);
267 $data->{crypt_pass} = $epw;
268 $cfg->write();
269 });
270}
271
2721;