]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/API2/AccessControl.pm
5774fab5fdaa46738f7282abc66668435e26ab63
[pmg-api.git] / src / PMG / API2 / AccessControl.pm
1 package PMG::API2::AccessControl;
2
3 use strict;
4 use warnings;
5
6 use PVE::Exception qw(raise raise_perm_exc);
7 use PVE::SafeSyslog;
8 use PMG::RESTEnvironment;
9 use PVE::RESTHandler;
10 use PVE::JSONSchema qw(get_standard_option);
11
12 use PMG::Utils;
13 use PMG::UserConfig;
14 use PMG::AccessControl;
15 use PMG::API2::Users;
16 use PMG::API2::TFA;
17
18 use Data::Dumper;
19
20 use base qw(PVE::RESTHandler);
21
22 __PACKAGE__->register_method ({
23 subclass => "PMG::API2::Users",
24 path => 'users',
25 });
26
27 __PACKAGE__->register_method ({
28 subclass => "PMG::API2::TFA",
29 path => 'tfa',
30 });
31
32 __PACKAGE__->register_method ({
33 name => 'index',
34 path => '',
35 method => 'GET',
36 description => "Directory index.",
37 permissions => {
38 user => 'all',
39 },
40 parameters => {
41 additionalProperties => 0,
42 properties => {},
43 },
44 returns => {
45 type => 'array',
46 items => {
47 type => "object",
48 properties => {
49 subdir => { type => 'string' },
50 },
51 },
52 links => [ { rel => 'child', href => "{subdir}" } ],
53 },
54 code => sub {
55 my ($param) = @_;
56
57 my $res = [
58 { subdir => 'ticket' },
59 { subdir => 'password' },
60 { subdir => 'users' },
61 ];
62
63 return $res;
64 }});
65
66 my sub create_or_verify_ticket : prototype($$$$$$) {
67 my ($rpcenv, $username, $pw_or_ticket, $path, $otp, $tfa_challenge) = @_;
68
69 my $ticketuser;
70 my $aad;
71
72 if ($pw_or_ticket =~ m/^PMGQUAR:/) {
73 my $ticketuser = PMG::Ticket::verify_quarantine_ticket($pw_or_ticket);
74 if ($ticketuser eq $username) {
75 my $csrftoken = PMG::Ticket::assemble_csrf_prevention_token($username);
76
77 return {
78 role => 'quser',
79 ticket => $pw_or_ticket,
80 username => $username,
81 CSRFPreventionToken => $csrftoken,
82 };
83 }
84 }
85
86 my $role = PMG::AccessControl::check_user_enabled($rpcenv->{usercfg}, $username);
87
88 my $tfa_challenge_is_ticket = 1;
89
90 if (!$tfa_challenge) {
91 $tfa_challenge_is_ticket = 0;
92 ($ticketuser, undef, $tfa_challenge) = PMG::Ticket::verify_ticket($pw_or_ticket, undef, 1);
93 die "No ticket\n" if $tfa_challenge;
94
95 if ($ticketuser && ($ticketuser eq 'root@pam' || $ticketuser eq $username)) {
96 # valid ticket. Note: root@pam can create tickets for other users
97 } elsif ($path && PMG::Ticket::verify_vnc_ticket($pw_or_ticket, $username, $path, 1)) {
98 # valid vnc ticket for $path
99 } else {
100 ($username, $tfa_challenge) =
101 PMG::AccessControl::authenticate_user($username, $pw_or_ticket, 0);
102 $pw_or_ticket = $otp;
103 }
104 }
105
106 if (defined($path)) {
107 # verify only
108 return { username => $username };
109 }
110
111 if ($tfa_challenge && $pw_or_ticket) {
112 if ($tfa_challenge_is_ticket) {
113 (undef, undef, $tfa_challenge) = PMG::Ticket::verify_ticket($tfa_challenge, $username, 0);
114 }
115 PMG::TFAConfig::lock_config(sub {
116 my $tfa_cfg = PMG::TFAConfig->new();
117
118 my $origin = undef;
119 if (!$tfa_cfg->has_webauthn_origin()) {
120 my $rpcenv = PMG::RESTEnvironment->get();
121 $origin = 'https://'.$rpcenv->get_request_host(1);
122 }
123 my $must_save = $tfa_cfg->authentication_verify(
124 $username,
125 $tfa_challenge,
126 $pw_or_ticket,
127 $origin,
128 );
129
130 $tfa_cfg->write() if $must_save;
131 });
132
133 $tfa_challenge = undef;
134 }
135
136 my $ticket_data;
137 my %extra;
138 if ($tfa_challenge) {
139 $ticket_data = '!tfa!' . $tfa_challenge;
140 $aad = $username;
141 $extra{NeedTFA} = 1;
142 } else {
143 $ticket_data = $username;
144 }
145
146 my $ticket = PMG::Ticket::assemble_ticket($ticket_data, $aad);
147 my $csrftoken = PMG::Ticket::assemble_csrf_prevention_token($username);
148
149 return {
150 role => $role,
151 ticket => $ticket,
152 username => $username,
153 CSRFPreventionToken => $csrftoken,
154 %extra,
155 };
156 };
157
158
159 __PACKAGE__->register_method ({
160 name => 'get_ticket',
161 path => 'ticket',
162 method => 'GET',
163 permissions => { user => 'world' },
164 description => "Dummy. Useful for formaters which want to priovde a login page.",
165 parameters => {
166 additionalProperties => 0,
167 },
168 returns => { type => "null" },
169 code => sub { return undef; }});
170
171 __PACKAGE__->register_method ({
172 name => 'create_ticket',
173 path => 'ticket',
174 method => 'POST',
175 permissions => {
176 description => "You need to pass valid credientials.",
177 user => 'world'
178 },
179 protected => 1, # else we can't access shadow files
180 description => "Create or verify authentication ticket.",
181 parameters => {
182 additionalProperties => 0,
183 properties => {
184 username => {
185 description => "User name",
186 type => 'string',
187 maxLength => 64,
188 },
189 realm => get_standard_option('realm', {
190 description => "You can optionally pass the realm using this parameter. Normally the realm is simply added to the username <username>\@<relam>.",
191 optional => 1,
192 }),
193 password => {
194 description => "The secret password. This can also be a valid ticket.",
195 type => 'string',
196 },
197 otp => {
198 description => "One-time password for Two-factor authentication.",
199 type => 'string',
200 optional => 1,
201 },
202 path => {
203 description => "Verify ticket, and check if user have access on 'path'",
204 type => 'string',
205 optional => 1,
206 maxLength => 64,
207 },
208 'tfa-challenge' => {
209 type => 'string',
210 description => "The signed TFA challenge string the user wants to respond to.",
211 optional => 1,
212 },
213 }
214 },
215 returns => {
216 type => "object",
217 properties => {
218 username => { type => 'string' },
219 ticket => { type => 'string', optional => 1},
220 CSRFPreventionToken => { type => 'string', optional => 1 },
221 role => { type => 'string', optional => 1},
222 }
223 },
224 code => sub {
225 my ($param) = @_;
226
227 my $username = $param->{username};
228
229 if ($username !~ m/\@(pam|pmg|quarantine)$/) {
230 my $realm = $param->{realm} // 'quarantine';
231 $username .= "\@$realm";
232 }
233
234 $username = 'root@pam' if $username eq 'root@pmg';
235
236 my $rpcenv = PMG::RESTEnvironment->get();
237
238 my $res;
239 eval {
240 $res = create_or_verify_ticket($rpcenv, $username,
241 $param->{password}, $param->{path}, $param->{otp}, $param->{'tfa-challenge'});
242 };
243 if (my $err = $@) {
244 my $clientip = $rpcenv->get_client_ip() || '';
245 syslog('err', "authentication failure; rhost=$clientip user=$username msg=$err");
246 # do not return any info to prevent user enumeration attacks
247 die PVE::Exception->new("authentication failure\n", code => 401);
248 }
249
250 syslog('info', "successful auth for user '$username'");
251
252 return $res;
253 }});
254
255 __PACKAGE__->register_method ({
256 name => 'change_passsword',
257 path => 'password',
258 method => 'PUT',
259 protected => 1, # else we can't access shadow files
260 permissions => {
261 description => "Each user is allowed to change his own password. Only root can change the password of another user.",
262 user => 'all',
263 },
264 description => "Change user password.",
265 parameters => {
266 additionalProperties => 0,
267 properties => {
268 userid => get_standard_option('userid'),
269 password => {
270 description => "The new password.",
271 type => 'string',
272 minLength => 5,
273 maxLength => 64,
274 },
275 }
276 },
277 returns => { type => "null" },
278 code => sub {
279 my ($param) = @_;
280
281 my $rpcenv = PMG::RESTEnvironment->get();
282 my $authuser = $rpcenv->get_user();
283
284 my ($userid, $ruid, $realm) = PMG::Utils::verify_username($param->{userid});
285
286 if ($authuser eq 'root@pam') {
287 # OK - root can change anything
288 } else {
289 if ($realm eq 'pmg' && $authuser eq $userid) {
290 # OK - each enable user can change its own password
291 PMG::AccessControl::check_user_enabled($rpcenv->{usercfg}, $userid);
292 } else {
293 raise_perm_exc();
294 }
295 }
296
297 PMG::AccessControl::set_user_password($userid, $param->{password});
298
299 syslog('info', "changed password for user '$userid'");
300
301 return undef;
302 }});
303
304 1;