]>
Commit | Line | Data |
---|---|---|
62ebb4bc DM |
1 | package PMG::UserConfig; |
2 | ||
3 | ||
4 | use strict; | |
5 | use warnings; | |
fff8e89c DM |
6 | use Data::Dumper; |
7 | use Clone 'clone'; | |
62ebb4bc DM |
8 | |
9 | use PVE::Tools; | |
10 | use PVE::INotify; | |
c861cb3a DM |
11 | use PVE::JSONSchema qw(get_standard_option); |
12 | use PVE::Exception qw(raise); | |
62ebb4bc DM |
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 | ||
fff8e89c | 46 | our $schema = { |
c861cb3a DM |
47 | additionalProperties => 0, |
48 | properties => { | |
49 | userid => get_standard_option('username'), | |
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 | }, | |
79 | first => { | |
80 | description => "First name.", | |
81 | type => 'string', | |
82 | maxLength => 64, | |
83 | optional => 1, | |
84 | }, | |
85 | 'last' => { | |
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 |
105 | our $update_schema = clone($schema); |
106 | $update_schema->{properties}->{role}->{optional} = 1; | |
107 | ||
c861cb3a DM |
108 | my $verity_entry = sub { |
109 | my ($entry) = @_; | |
110 | ||
111 | my $errors = {}; | |
112 | PVE::JSONSchema::check_prop($entry, $schema, '', $errors); | |
113 | if (scalar(%$errors)) { | |
114 | raise "verify entry failed\n", errors => $errors; | |
115 | } | |
116 | }; | |
117 | ||
62ebb4bc DM |
118 | sub read_user_conf { |
119 | my ($filename, $fh) = @_; | |
120 | ||
121 | my $cfg = {}; | |
122 | ||
123 | if ($fh) { | |
124 | ||
125 | my $comment = ''; | |
126 | ||
127 | while (defined(my $line = <$fh>)) { | |
128 | next if $line =~ m/^\s*$/; | |
129 | if ($line =~ m/^#(.*)$/) { | |
130 | $comment = $1; | |
131 | next; | |
132 | } | |
c861cb3a DM |
133 | |
134 | if ($line =~ m/^ | |
135 | (?<userid>(?:[^\s:]+)) : | |
fff8e89c DM |
136 | (?<enable>[01]?) : |
137 | (?<expire>\d*) : | |
138 | (?<crypt_pass>(?:[^\s:]*)) : | |
c861cb3a DM |
139 | (?<role>[a-z]+) : |
140 | (?<email>(?:[^\s:]*)) : | |
141 | (?<first>(?:[^:]*)) : | |
142 | (?<last>(?:[^:]*)) : | |
143 | (?<keys>(?:[^:]*)) : | |
144 | $/x | |
145 | ) { | |
62ebb4bc | 146 | my $d = { |
c861cb3a | 147 | userid => $+{userid}, |
fff8e89c DM |
148 | enable => $+{enable} || 0, |
149 | expire => $+{expire} || 0, | |
c861cb3a | 150 | role => $+{role}, |
62ebb4bc DM |
151 | }; |
152 | $d->{comment} = $comment if $comment; | |
153 | $comment = ''; | |
fff8e89c | 154 | foreach my $k (qw(crypt_pass email first last keys)) { |
c861cb3a DM |
155 | $d->{$k} = $+{$k} if $+{$k}; |
156 | } | |
157 | eval { | |
158 | $verity_entry->($d); | |
159 | $cfg->{$d->{userid}} = $d; | |
160 | }; | |
161 | if (my $err = $@) { | |
162 | warn "$filename: $err"; | |
163 | } | |
62ebb4bc DM |
164 | } else { |
165 | warn "$filename: ignore invalid line $.\n"; | |
166 | $comment = ''; | |
167 | } | |
168 | } | |
169 | } | |
170 | ||
171 | $cfg->{root} //= {}; | |
172 | $cfg->{root}->{userid} = 'root'; | |
173 | $cfg->{root}->{enable} = 1; | |
174 | $cfg->{root}->{comment} = 'Unix Superuser'; | |
175 | $cfg->{root}->{role} = 'root'; | |
176 | delete $cfg->{root}->{crypt_pass}; | |
177 | ||
178 | return $cfg; | |
179 | } | |
180 | ||
181 | sub write_user_conf { | |
182 | my ($filename, $fh, $cfg) = @_; | |
183 | ||
184 | my $raw = ''; | |
185 | ||
fff8e89c DM |
186 | delete $cfg->{root}->{crypt_pass}; |
187 | ||
188 | foreach my $userid (keys %$cfg) { | |
189 | my $d = $cfg->{$userid}; | |
190 | $d->{userid} = $userid; | |
191 | eval { | |
192 | $verity_entry->($d); | |
193 | $cfg->{$d->{userid}} = $d; | |
194 | }; | |
195 | if (my $err = $@) { | |
196 | die $err; | |
197 | } | |
198 | my $line = "$userid:"; | |
199 | for my $k (qw(enable expire crypt_pass role email first last keys)) { | |
200 | $line .= ($d->{$k} // '') . ':'; | |
201 | } | |
202 | $raw .= $line . "\n"; | |
203 | } | |
62ebb4bc DM |
204 | |
205 | PVE::Tools::safe_print($filename, $fh, $raw); | |
206 | } | |
207 | ||
208 | PVE::INotify::register_file($inotify_file_id, $config_filename, | |
209 | \&read_user_conf, | |
210 | \&write_user_conf, | |
211 | undef, | |
212 | always_call_parser => 1); | |
213 | ||
214 | sub lookup_user_data { | |
215 | my ($self, $username, $noerr) = @_; | |
216 | ||
217 | return $self->{$username} if $self->{$username}; | |
218 | ||
219 | die "no such user ('$username')\n" if !$noerr; | |
220 | ||
221 | return undef; | |
222 | } | |
223 | ||
224 | sub authenticate_user { | |
225 | my ($self, $username, $password) = @_; | |
226 | ||
227 | die "no password\n" if !$password; | |
228 | ||
229 | my $data = $self->lookup_user_data($username); | |
230 | ||
c861cb3a DM |
231 | my $ctime = time(); |
232 | my $expire = $data->{expire}; | |
233 | ||
234 | die "account expired\n" if $expire && ($expire < $ctime); | |
235 | ||
62ebb4bc DM |
236 | if ($data->{crypt_pass}) { |
237 | my $encpw = crypt($password, $data->{crypt_pass}); | |
238 | die "invalid credentials\n" if ($encpw ne $data->{crypt_pass}); | |
239 | } else { | |
240 | die "no password set\n"; | |
241 | } | |
242 | ||
243 | return 1; | |
244 | } | |
245 | ||
246 | sub set_password { | |
247 | my ($class, $username, $password) = @_; | |
248 | ||
249 | lock_config(sub { | |
250 | my $cfg = $class->new(); | |
251 | my $data = $cfg->lookup_user_data($username); # user exists | |
252 | my $epw = PMG::Utils::encrypt_pw($password); | |
253 | $data->{crypt_pass} = $epw; | |
254 | $cfg->write(); | |
255 | }); | |
256 | } | |
257 | ||
258 | 1; |