]>
Commit | Line | Data |
---|---|---|
dc547a13 WB |
1 | package PVE::API2::TFA; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use PVE::AccessControl; | |
7 | use PVE::Cluster qw(cfs_read_file); | |
8 | use PVE::JSONSchema qw(get_standard_option); | |
9 | use PVE::Exception qw(raise raise_perm_exc raise_param_exc); | |
10 | use PVE::RPCEnvironment; | |
11 | ||
12 | use PVE::API2::AccessControl; # for old login api get_u2f_instance method | |
13 | ||
14 | use PVE::RESTHandler; | |
15 | ||
16 | use base qw(PVE::RESTHandler); | |
17 | ||
18 | ### OLD API | |
19 | ||
20 | __PACKAGE__->register_method({ | |
21 | name => 'verify_tfa', | |
22 | path => '', | |
23 | method => 'POST', | |
24 | permissions => { user => 'all' }, | |
25 | protected => 1, # else we can't access shadow files | |
26 | allowtoken => 0, # we don't want tokens to access TFA information | |
27 | description => 'Finish a u2f challenge.', | |
28 | parameters => { | |
29 | additionalProperties => 0, | |
30 | properties => { | |
31 | response => { | |
32 | type => 'string', | |
33 | description => 'The response to the current authentication challenge.', | |
34 | }, | |
35 | } | |
36 | }, | |
37 | returns => { | |
38 | type => 'object', | |
39 | properties => { | |
40 | ticket => { type => 'string' }, | |
41 | # cap | |
42 | } | |
43 | }, | |
44 | code => sub { | |
45 | my ($param) = @_; | |
46 | ||
47 | my $rpcenv = PVE::RPCEnvironment::get(); | |
48 | my $authuser = $rpcenv->get_user(); | |
49 | my ($username, undef, $realm) = PVE::AccessControl::verify_username($authuser); | |
50 | ||
51 | my ($tfa_type, $tfa_data) = PVE::AccessControl::user_get_tfa($username, $realm, 0); | |
52 | if (!defined($tfa_type)) { | |
53 | raise('no u2f data available'); | |
54 | } | |
55 | ||
56 | eval { | |
57 | if ($tfa_type eq 'u2f') { | |
58 | my $challenge = $rpcenv->get_u2f_challenge() | |
59 | or raise('no active challenge'); | |
60 | ||
61 | my $keyHandle = $tfa_data->{keyHandle}; | |
62 | my $publicKey = $tfa_data->{publicKey}; | |
63 | raise("incomplete u2f setup") | |
64 | if !defined($keyHandle) || !defined($publicKey); | |
65 | ||
66 | my $u2f = PVE::API2::AccessControl::get_u2f_instance($rpcenv, $publicKey, $keyHandle); | |
67 | $u2f->set_challenge($challenge); | |
68 | ||
69 | my ($counter, $present) = $u2f->auth_verify($param->{response}); | |
70 | # Do we want to do anything with these? | |
71 | } else { | |
72 | # sanity check before handing off to the verification code: | |
73 | my $keys = $tfa_data->{keys} or die "missing tfa keys\n"; | |
74 | my $config = $tfa_data->{config} or die "bad tfa entry\n"; | |
75 | PVE::AccessControl::verify_one_time_pw($tfa_type, $authuser, $keys, $config, $param->{response}); | |
76 | } | |
77 | }; | |
78 | if (my $err = $@) { | |
79 | my $clientip = $rpcenv->get_client_ip() || ''; | |
80 | syslog('err', "authentication verification failure; rhost=$clientip user=$authuser msg=$err"); | |
81 | die PVE::Exception->new("authentication failure\n", code => 401); | |
82 | } | |
83 | ||
84 | return { | |
85 | ticket => PVE::AccessControl::assemble_ticket($authuser), | |
86 | cap => $rpcenv->compute_api_permission($authuser), | |
87 | } | |
88 | }}); | |
89 | ||
90 | ### END OLD API | |
91 | ||
92 | my $TFA_TYPE_SCHEMA = { | |
93 | type => 'string', | |
94 | description => 'TFA Entry Type.', | |
95 | enum => [qw(totp u2f webauthn recovery yubico)], | |
96 | }; | |
97 | ||
98 | my %TFA_INFO_PROPERTIES = ( | |
99 | id => { | |
100 | type => 'string', | |
101 | description => 'The id used to reference this entry.', | |
102 | }, | |
103 | description => { | |
104 | type => 'string', | |
105 | description => 'User chosen description for this entry.', | |
106 | }, | |
107 | created => { | |
108 | type => 'integer', | |
109 | description => 'Creation time of this entry as unix epoch.', | |
110 | }, | |
111 | enable => { | |
112 | type => 'boolean', | |
113 | description => 'Whether this TFA entry is currently enabled.', | |
114 | optional => 1, | |
115 | default => 1, | |
116 | }, | |
117 | ); | |
118 | ||
119 | my $TYPED_TFA_ENTRY_SCHEMA = { | |
120 | type => 'object', | |
121 | description => 'TFA Entry.', | |
122 | properties => { | |
123 | type => $TFA_TYPE_SCHEMA, | |
124 | %TFA_INFO_PROPERTIES, | |
125 | }, | |
126 | }; | |
127 | ||
128 | my $TFA_ID_SCHEMA = { | |
129 | type => 'string', | |
130 | description => 'A TFA entry id.', | |
131 | }; | |
132 | ||
133 | __PACKAGE__->register_method ({ | |
134 | name => 'list_user_tfa', | |
135 | path => '{userid}', | |
136 | method => 'GET', | |
137 | permissions => { | |
138 | check => [ 'or', | |
139 | ['userid-param', 'self'], | |
140 | ['userid-group', ['User.Modify', 'Sys.Audit']], | |
141 | ], | |
142 | }, | |
143 | protected => 1, # else we can't access shadow files | |
144 | allowtoken => 0, # we don't want tokens to change the regular user's TFA settings | |
145 | description => 'List TFA configurations of users.', | |
146 | parameters => { | |
147 | additionalProperties => 0, | |
148 | properties => { | |
149 | userid => get_standard_option('userid', { | |
150 | completion => \&PVE::AccessControl::complete_username, | |
151 | }), | |
152 | } | |
153 | }, | |
154 | returns => { | |
155 | description => "A list of the user's TFA entries.", | |
156 | type => 'array', | |
157 | items => $TYPED_TFA_ENTRY_SCHEMA, | |
158 | }, | |
159 | code => sub { | |
160 | my ($param) = @_; | |
161 | my $tfa_cfg = cfs_read_file('priv/tfa.cfg'); | |
162 | return $tfa_cfg->api_list_user_tfa($param->{userid}); | |
163 | }}); | |
164 | ||
165 | __PACKAGE__->register_method ({ | |
166 | name => 'get_tfa_entry', | |
167 | path => '{userid}/{id}', | |
168 | method => 'GET', | |
169 | permissions => { | |
170 | check => [ 'or', | |
171 | ['userid-param', 'self'], | |
172 | ['userid-group', ['User.Modify', 'Sys.Audit']], | |
173 | ], | |
174 | }, | |
175 | protected => 1, # else we can't access shadow files | |
176 | allowtoken => 0, # we don't want tokens to change the regular user's TFA settings | |
177 | description => 'A requested TFA entry if present.', | |
178 | parameters => { | |
179 | additionalProperties => 0, | |
180 | properties => { | |
181 | userid => get_standard_option('userid', { | |
182 | completion => \&PVE::AccessControl::complete_username, | |
183 | }), | |
184 | id => $TFA_ID_SCHEMA, | |
185 | } | |
186 | }, | |
187 | returns => $TYPED_TFA_ENTRY_SCHEMA, | |
188 | code => sub { | |
189 | my ($param) = @_; | |
190 | my $tfa_cfg = cfs_read_file('priv/tfa.cfg'); | |
191 | return $tfa_cfg->api_get_tfa_entry($param->{userid}, $param->{id}); | |
192 | }}); | |
193 | ||
194 | __PACKAGE__->register_method ({ | |
195 | name => 'list_tfa', | |
196 | path => '', | |
197 | method => 'GET', | |
198 | permissions => { | |
199 | description => "Returns all or just the logged-in user, depending on privileges.", | |
200 | user => 'all', | |
201 | }, | |
202 | protected => 1, # else we can't access shadow files | |
203 | allowtoken => 0, # we don't want tokens to change the regular user's TFA settings | |
204 | description => 'List TFA configurations of users.', | |
205 | parameters => { | |
206 | additionalProperties => 0, | |
207 | properties => {} | |
208 | }, | |
209 | returns => { | |
210 | description => "The list tuples of user and TFA entries.", | |
211 | type => 'array', | |
212 | items => { | |
213 | type => 'object', | |
214 | properties => { | |
215 | userid => { | |
216 | type => 'string', | |
217 | description => 'User this entry belongs to.', | |
218 | }, | |
219 | entries => { | |
220 | type => 'array', | |
221 | items => $TYPED_TFA_ENTRY_SCHEMA, | |
222 | }, | |
223 | }, | |
224 | }, | |
225 | }, | |
226 | code => sub { | |
227 | my ($param) = @_; | |
228 | ||
229 | my $rpcenv = PVE::RPCEnvironment::get(); | |
230 | my $authuser = $rpcenv->get_user(); | |
231 | ||
232 | ||
233 | my $top_level_allowed = ($authuser eq 'root@pam'); | |
234 | ||
235 | my $tfa_cfg = cfs_read_file('priv/tfa.cfg'); | |
236 | return $tfa_cfg->api_list_tfa($authuser, $top_level_allowed); | |
237 | }}); | |
238 | ||
239 | 1; |