]> git.proxmox.com Git - pmg-api.git/blob - PMG/UserConfig.pm
PMG/API2/Users.pm: implement create API
[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('username'),
50 email => {
51 description => "Users E-Mail address.",
52 type => 'string', format => 'email',
53 optional => 1,
54 },
55 expire => {
56 description => "Account expiration date (seconds since epoch). '0' means no expiration date.",
57 type => 'integer',
58 minimum => 0,
59 default => 0,
60 optional => 1,
61 },
62 enable => {
63 description => "Flag to enable or disable the account.",
64 type => 'boolean',
65 default => 0,
66 optional => 1,
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 => {
92 description => "Keys for two factor auth (yubico).",
93 type => 'string',
94 maxLength => 128,
95 optional => 1,
96 },
97 comment => {
98 description => "Comment.",
99 type => 'string',
100 optional => 1,
101 },
102 },
103 };
104
105 our $update_schema = clone($schema);
106 $update_schema->{properties}->{role}->{optional} = 1;
107
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
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 }
133
134 if ($line =~ m/^
135 (?<userid>(?:[^\s:]+)) :
136 (?<enable>[01]?) :
137 (?<expire>\d*) :
138 (?<crypt_pass>(?:[^\s:]*)) :
139 (?<role>[a-z]+) :
140 (?<email>(?:[^\s:]*)) :
141 (?<first>(?:[^:]*)) :
142 (?<last>(?:[^:]*)) :
143 (?<keys>(?:[^:]*)) :
144 $/x
145 ) {
146 my $d = {
147 userid => $+{userid},
148 enable => $+{enable} || 0,
149 expire => $+{expire} || 0,
150 role => $+{role},
151 };
152 $d->{comment} = $comment if $comment;
153 $comment = '';
154 foreach my $k (qw(crypt_pass email first last keys)) {
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 }
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
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 }
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
231 my $ctime = time();
232 my $expire = $data->{expire};
233
234 die "account expired\n" if $expire && ($expire < $ctime);
235
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;