1 package PMG
::API2
::AccessControl
;
6 use PVE
::Exception
qw(raise raise_perm_exc);
8 use PMG
::RESTEnvironment
;
10 use PVE
::JSONSchema
qw(get_standard_option);
14 use PMG
::AccessControl
;
20 use base
qw(PVE::RESTHandler);
22 __PACKAGE__-
>register_method ({
23 subclass
=> "PMG::API2::Users",
27 __PACKAGE__-
>register_method ({
28 subclass
=> "PMG::API2::TFA",
32 __PACKAGE__-
>register_method ({
36 description
=> "Directory index.",
41 additionalProperties
=> 0,
49 subdir
=> { type
=> 'string' },
52 links
=> [ { rel
=> 'child', href
=> "{subdir}" } ],
58 { subdir
=> 'ticket' },
59 { subdir
=> 'password' },
60 { subdir
=> 'users' },
66 my sub create_or_verify_ticket
: prototype($$$$$$) {
67 my ($rpcenv, $username, $pw_or_ticket, $path, $otp, $tfa_challenge) = @_;
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);
79 ticket
=> $pw_or_ticket,
80 username
=> $username,
81 CSRFPreventionToken
=> $csrftoken,
86 my $role = PMG
::AccessControl
::check_user_enabled
($rpcenv->{usercfg
}, $username);
88 my $tfa_challenge_is_ticket = 1;
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;
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
100 ($username, $tfa_challenge) =
101 PMG
::AccessControl
::authenticate_user
($username, $pw_or_ticket, 0);
102 $pw_or_ticket = $otp;
106 if (defined($path)) {
108 return { username
=> $username };
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);
115 PMG
::TFAConfig
::lock_config
(sub {
116 my $tfa_cfg = PMG
::TFAConfig-
>new();
119 if (!$tfa_cfg->has_webauthn_origin()) {
120 my $rpcenv = PMG
::RESTEnvironment-
>get();
121 $origin = 'https://'.$rpcenv->get_request_host(1);
123 my $must_save = $tfa_cfg->authentication_verify(
130 $tfa_cfg->write() if $must_save;
133 $tfa_challenge = undef;
138 if ($tfa_challenge) {
139 $ticket_data = '!tfa!' . $tfa_challenge;
143 $ticket_data = $username;
146 my $ticket = PMG
::Ticket
::assemble_ticket
($ticket_data, $aad);
147 my $csrftoken = PMG
::Ticket
::assemble_csrf_prevention_token
($username);
152 username
=> $username,
153 CSRFPreventionToken
=> $csrftoken,
159 __PACKAGE__-
>register_method ({
160 name
=> 'get_ticket',
163 permissions
=> { user
=> 'world' },
164 description
=> "Dummy. Useful for formaters which want to priovde a login page.",
166 additionalProperties
=> 0,
168 returns
=> { type
=> "null" },
169 code
=> sub { return undef; }});
171 __PACKAGE__-
>register_method ({
172 name
=> 'create_ticket',
176 description
=> "You need to pass valid credientials.",
179 protected
=> 1, # else we can't access shadow files
180 description
=> "Create or verify authentication ticket.",
182 additionalProperties
=> 0,
185 description
=> "User name",
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>.",
194 description
=> "The secret password. This can also be a valid ticket.",
198 description
=> "One-time password for Two-factor authentication.",
203 description
=> "Verify ticket, and check if user have access on 'path'",
210 description
=> "The signed TFA challenge string the user wants to respond to.",
218 username
=> { type
=> 'string' },
219 ticket
=> { type
=> 'string', optional
=> 1},
220 CSRFPreventionToken
=> { type
=> 'string', optional
=> 1 },
221 role => { type
=> 'string', optional
=> 1},
227 my $username = $param->{username
};
229 if ($username !~ m/\@(pam|pmg|quarantine)$/) {
230 my $realm = $param->{realm
} // 'quarantine';
231 $username .= "\@$realm";
234 $username = 'root@pam' if $username eq 'root@pmg';
236 my $rpcenv = PMG
::RESTEnvironment-
>get();
240 $res = create_or_verify_ticket
($rpcenv, $username,
241 $param->{password
}, $param->{path
}, $param->{otp
}, $param->{'tfa-challenge'});
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);
250 syslog
('info', "successful auth for user '$username'");
255 __PACKAGE__-
>register_method ({
256 name
=> 'change_passsword',
259 protected
=> 1, # else we can't access shadow files
261 description
=> "Each user is allowed to change his own password. Only root can change the password of another user.",
264 description
=> "Change user password.",
266 additionalProperties
=> 0,
268 userid
=> get_standard_option
('userid'),
270 description
=> "The new password.",
277 returns
=> { type
=> "null" },
281 my $rpcenv = PMG
::RESTEnvironment-
>get();
282 my $authuser = $rpcenv->get_user();
284 my ($userid, $ruid, $realm) = PMG
::Utils
::verify_username
($param->{userid
});
286 if ($authuser eq 'root@pam') {
287 # OK - root can change anything
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);
297 PMG
::AccessControl
::set_user_password
($userid, $param->{password
});
299 syslog
('info', "changed password for user '$userid'");