]>
Commit | Line | Data |
---|---|---|
2c3a6c0a DM |
1 | package PVE::API2::Domains; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
673d2bf2 | 5 | use PVE::Exception qw(raise_param_exc); |
5bb4e06a | 6 | use PVE::Tools qw(extract_param); |
2c3a6c0a DM |
7 | use PVE::Cluster qw (cfs_read_file cfs_write_file); |
8 | use PVE::AccessControl; | |
9 | use PVE::JSONSchema qw(get_standard_option); | |
10 | ||
11 | use PVE::SafeSyslog; | |
2c3a6c0a | 12 | use PVE::RESTHandler; |
5bb4e06a | 13 | use PVE::Auth::Plugin; |
2c3a6c0a DM |
14 | |
15 | my $domainconfigfile = "domains.cfg"; | |
16 | ||
17 | use base qw(PVE::RESTHandler); | |
18 | ||
19 | __PACKAGE__->register_method ({ | |
32449f35 DC |
20 | name => 'index', |
21 | path => '', | |
2c3a6c0a DM |
22 | method => 'GET', |
23 | description => "Authentication domain index.", | |
32449f35 | 24 | permissions => { |
82b63965 | 25 | description => "Anyone can access that, because we need that list for the login box (before the user is authenticated).", |
32449f35 | 26 | user => 'world', |
82b63965 | 27 | }, |
2c3a6c0a DM |
28 | parameters => { |
29 | additionalProperties => 0, | |
30 | properties => {}, | |
31 | }, | |
32 | returns => { | |
33 | type => 'array', | |
34 | items => { | |
35 | type => "object", | |
36 | properties => { | |
37 | realm => { type => 'string' }, | |
f3c87f9b | 38 | type => { type => 'string' }, |
96f8ebd6 DM |
39 | tfa => { |
40 | description => "Two-factor authentication provider.", | |
41 | type => 'string', | |
1abc2c0a | 42 | enum => [ 'yubico', 'oath' ], |
96f8ebd6 DM |
43 | optional => 1, |
44 | }, | |
52b2eff3 DM |
45 | comment => { |
46 | description => "A comment. The GUI use this text when you select a domain (Realm) on the login window.", | |
47 | type => 'string', | |
48 | optional => 1, | |
49 | }, | |
2c3a6c0a DM |
50 | }, |
51 | }, | |
52 | links => [ { rel => 'child', href => "{realm}" } ], | |
53 | }, | |
54 | code => sub { | |
55 | my ($param) = @_; | |
32449f35 | 56 | |
2c3a6c0a DM |
57 | my $res = []; |
58 | ||
59 | my $cfg = cfs_read_file($domainconfigfile); | |
5bb4e06a DM |
60 | my $ids = $cfg->{ids}; |
61 | ||
62 | foreach my $realm (keys %$ids) { | |
63 | my $d = $ids->{$realm}; | |
2c3a6c0a DM |
64 | my $entry = { realm => $realm, type => $d->{type} }; |
65 | $entry->{comment} = $d->{comment} if $d->{comment}; | |
66 | $entry->{default} = 1 if $d->{default}; | |
96f8ebd6 DM |
67 | if ($d->{tfa} && (my $tfa_cfg = PVE::Auth::Plugin::parse_tfa_config($d->{tfa}))) { |
68 | $entry->{tfa} = $tfa_cfg->{type}; | |
69 | } | |
2c3a6c0a DM |
70 | push @$res, $entry; |
71 | } | |
72 | ||
73 | return $res; | |
74 | }}); | |
75 | ||
76 | __PACKAGE__->register_method ({ | |
32449f35 | 77 | name => 'create', |
2c3a6c0a | 78 | protected => 1, |
32449f35 | 79 | path => '', |
2c3a6c0a | 80 | method => 'POST', |
32449f35 | 81 | permissions => { |
82b63965 | 82 | check => ['perm', '/access/realm', ['Realm.Allocate']], |
96919234 | 83 | }, |
2c3a6c0a | 84 | description => "Add an authentication server.", |
5bb4e06a | 85 | parameters => PVE::Auth::Plugin->createSchema(), |
2c3a6c0a DM |
86 | returns => { type => 'null' }, |
87 | code => sub { | |
88 | my ($param) = @_; | |
89 | ||
5bb4e06a | 90 | PVE::Auth::Plugin::lock_domain_config( |
2c3a6c0a | 91 | sub { |
32449f35 | 92 | |
2c3a6c0a | 93 | my $cfg = cfs_read_file($domainconfigfile); |
5bb4e06a | 94 | my $ids = $cfg->{ids}; |
2c3a6c0a | 95 | |
5bb4e06a DM |
96 | my $realm = extract_param($param, 'realm'); |
97 | my $type = $param->{type}; | |
32449f35 DC |
98 | |
99 | die "domain '$realm' already exists\n" | |
5bb4e06a | 100 | if $ids->{$realm}; |
2c3a6c0a DM |
101 | |
102 | die "unable to use reserved name '$realm'\n" | |
103 | if ($realm eq 'pam' || $realm eq 'pve'); | |
104 | ||
5bb4e06a DM |
105 | die "unable to create builtin type '$type'\n" |
106 | if ($type eq 'pam' || $type eq 'pve'); | |
af4a8a85 | 107 | |
5bb4e06a DM |
108 | my $plugin = PVE::Auth::Plugin->lookup($type); |
109 | my $config = $plugin->check_config($realm, $param, 1, 1); | |
2c3a6c0a | 110 | |
5bb4e06a DM |
111 | if ($config->{default}) { |
112 | foreach my $r (keys %$ids) { | |
113 | delete $ids->{$r}->{default}; | |
0c156363 | 114 | } |
af4a8a85 DM |
115 | } |
116 | ||
5bb4e06a DM |
117 | $ids->{$realm} = $config; |
118 | ||
2c3a6c0a DM |
119 | cfs_write_file($domainconfigfile, $cfg); |
120 | }, "add auth server failed"); | |
121 | ||
122 | return undef; | |
123 | }}); | |
124 | ||
125 | __PACKAGE__->register_method ({ | |
32449f35 DC |
126 | name => 'update', |
127 | path => '{realm}', | |
2c3a6c0a | 128 | method => 'PUT', |
32449f35 | 129 | permissions => { |
82b63965 | 130 | check => ['perm', '/access/realm', ['Realm.Allocate']], |
96919234 | 131 | }, |
2c3a6c0a DM |
132 | description => "Update authentication server settings.", |
133 | protected => 1, | |
5bb4e06a | 134 | parameters => PVE::Auth::Plugin->updateSchema(), |
2c3a6c0a DM |
135 | returns => { type => 'null' }, |
136 | code => sub { | |
137 | my ($param) = @_; | |
138 | ||
5bb4e06a | 139 | PVE::Auth::Plugin::lock_domain_config( |
2c3a6c0a | 140 | sub { |
32449f35 | 141 | |
2c3a6c0a | 142 | my $cfg = cfs_read_file($domainconfigfile); |
5bb4e06a | 143 | my $ids = $cfg->{ids}; |
2c3a6c0a | 144 | |
5bb4e06a DM |
145 | my $digest = extract_param($param, 'digest'); |
146 | PVE::SectionConfig::assert_if_modified($cfg, $digest); | |
147 | ||
148 | my $realm = extract_param($param, 'realm'); | |
2c3a6c0a | 149 | |
32449f35 | 150 | die "domain '$realm' does not exist\n" |
5bb4e06a DM |
151 | if !$ids->{$realm}; |
152 | ||
153 | my $delete_str = extract_param($param, 'delete'); | |
154 | die "no options specified\n" if !$delete_str && !scalar(keys %$param); | |
2c3a6c0a | 155 | |
5bb4e06a DM |
156 | foreach my $opt (PVE::Tools::split_list($delete_str)) { |
157 | delete $ids->{$realm}->{$opt}; | |
2c3a6c0a | 158 | } |
32449f35 | 159 | |
5bb4e06a DM |
160 | my $plugin = PVE::Auth::Plugin->lookup($ids->{$realm}->{type}); |
161 | my $config = $plugin->check_config($realm, $param, 0, 1); | |
2c3a6c0a | 162 | |
5bb4e06a DM |
163 | if ($config->{default}) { |
164 | foreach my $r (keys %$ids) { | |
165 | delete $ids->{$r}->{default}; | |
2c3a6c0a DM |
166 | } |
167 | } | |
168 | ||
5bb4e06a DM |
169 | foreach my $p (keys %$config) { |
170 | $ids->{$realm}->{$p} = $config->{$p}; | |
af4a8a85 DM |
171 | } |
172 | ||
2c3a6c0a DM |
173 | cfs_write_file($domainconfigfile, $cfg); |
174 | }, "update auth server failed"); | |
175 | ||
176 | return undef; | |
177 | }}); | |
178 | ||
179 | # fixme: return format! | |
180 | __PACKAGE__->register_method ({ | |
32449f35 DC |
181 | name => 'read', |
182 | path => '{realm}', | |
2c3a6c0a DM |
183 | method => 'GET', |
184 | description => "Get auth server configuration.", | |
32449f35 | 185 | permissions => { |
82b63965 | 186 | check => ['perm', '/access/realm', ['Realm.Allocate', 'Sys.Audit'], any => 1], |
96919234 | 187 | }, |
2c3a6c0a DM |
188 | parameters => { |
189 | additionalProperties => 0, | |
190 | properties => { | |
191 | realm => get_standard_option('realm'), | |
192 | }, | |
193 | }, | |
194 | returns => {}, | |
195 | code => sub { | |
196 | my ($param) = @_; | |
197 | ||
198 | my $cfg = cfs_read_file($domainconfigfile); | |
199 | ||
200 | my $realm = $param->{realm}; | |
32449f35 | 201 | |
5bb4e06a | 202 | my $data = $cfg->{ids}->{$realm}; |
2c3a6c0a DM |
203 | die "domain '$realm' does not exist\n" if !$data; |
204 | ||
5bb4e06a DM |
205 | $data->{digest} = $cfg->{digest}; |
206 | ||
2c3a6c0a DM |
207 | return $data; |
208 | }}); | |
209 | ||
210 | ||
211 | __PACKAGE__->register_method ({ | |
32449f35 DC |
212 | name => 'delete', |
213 | path => '{realm}', | |
2c3a6c0a | 214 | method => 'DELETE', |
32449f35 | 215 | permissions => { |
82b63965 | 216 | check => ['perm', '/access/realm', ['Realm.Allocate']], |
96919234 | 217 | }, |
2c3a6c0a DM |
218 | description => "Delete an authentication server.", |
219 | protected => 1, | |
220 | parameters => { | |
221 | additionalProperties => 0, | |
222 | properties => { | |
223 | realm => get_standard_option('realm'), | |
224 | } | |
225 | }, | |
226 | returns => { type => 'null' }, | |
227 | code => sub { | |
228 | my ($param) = @_; | |
229 | ||
5bb4e06a | 230 | PVE::Auth::Plugin::lock_domain_config( |
2c3a6c0a DM |
231 | sub { |
232 | ||
233 | my $cfg = cfs_read_file($domainconfigfile); | |
5bb4e06a | 234 | my $ids = $cfg->{ids}; |
2c3a6c0a DM |
235 | |
236 | my $realm = $param->{realm}; | |
32449f35 | 237 | |
5bb4e06a | 238 | die "domain '$realm' does not exist\n" if !$ids->{$realm}; |
2c3a6c0a | 239 | |
5bb4e06a | 240 | delete $ids->{$realm}; |
2c3a6c0a DM |
241 | |
242 | cfs_write_file($domainconfigfile, $cfg); | |
243 | }, "delete auth server failed"); | |
32449f35 | 244 | |
2c3a6c0a DM |
245 | return undef; |
246 | }}); | |
247 | ||
673d2bf2 DC |
248 | __PACKAGE__->register_method ({ |
249 | name => 'sync', | |
250 | path => '{realm}/sync', | |
251 | method => 'POST', | |
252 | permissions => { | |
cf109814 TL |
253 | description => "'Realm.AllocateUser' on '/access/realm/<realm>' and " |
254 | ." 'User.Modify' permissions to '/access/groups/'.", | |
673d2bf2 | 255 | check => [ 'and', |
cf109814 TL |
256 | [ 'userid-param', 'Realm.AllocateUser' ], |
257 | [ 'userid-group', ['User.Modify'] ], | |
258 | ], | |
673d2bf2 | 259 | }, |
cf109814 TL |
260 | description => "Syncs users and/or groups from the configured LDAP to user.cfg." |
261 | ." NOTE: Synced groups will have the name 'name-\$realm', so make sure" | |
262 | ." those groups do not exist to prevent overwriting.", | |
673d2bf2 DC |
263 | protected => 1, |
264 | parameters => { | |
265 | additionalProperties => 0, | |
266 | properties => { | |
267 | realm => get_standard_option('realm'), | |
268 | scope => { | |
269 | description => "Select what to sync.", | |
270 | type => 'string', | |
271 | enum => [qw(users groups both)], | |
272 | }, | |
273 | full => { | |
274 | description => "If set, uses the LDAP Directory as source of truth, ". | |
275 | "deleting all information not contained there. ". | |
276 | "Otherwise only syncs information set explicitly.", | |
277 | type => 'boolean', | |
278 | }, | |
279 | enable => { | |
280 | description => "Enable newly synced users.", | |
281 | type => 'boolean', | |
282 | }, | |
283 | purge => { | |
284 | description => "Remove ACLs for users/groups that were removed from the config.", | |
285 | type => 'boolean', | |
286 | }, | |
287 | } | |
288 | }, | |
cf109814 TL |
289 | returns => { |
290 | description => 'Worker Task-UPID', | |
291 | type => 'string' | |
292 | }, | |
673d2bf2 DC |
293 | code => sub { |
294 | my ($param) = @_; | |
295 | ||
296 | my $rpcenv = PVE::RPCEnvironment::get(); | |
297 | my $authuser = $rpcenv->get_user(); | |
298 | ||
673d2bf2 DC |
299 | my $realm = $param->{realm}; |
300 | my $cfg = cfs_read_file($domainconfigfile); | |
cf109814 | 301 | my $realmconfig = $cfg->{ids}->{$realm}; |
673d2bf2 | 302 | |
cf109814 TL |
303 | raise_param_exc({ 'realm' => 'Realm does not exist.' }) if !defined($realmconfig); |
304 | my $type = $realmconfig->{type}; | |
673d2bf2 DC |
305 | |
306 | if ($type ne 'ldap' && $type ne 'ad') { | |
cf109814 | 307 | die "Cannot sync realm type '$type'! Only LDAP/AD realms can be synced.\n"; |
673d2bf2 | 308 | } |
673d2bf2 | 309 | |
673d2bf2 | 310 | |
cf109814 TL |
311 | my $scope = $param->{scope}; |
312 | my $whatstring = $scope eq 'both' ? "users and groups" : $scope; | |
673d2bf2 | 313 | |
cf109814 | 314 | my $plugin = PVE::Auth::Plugin->lookup($type); |
673d2bf2 DC |
315 | |
316 | my $worker = sub { | |
cf109814 TL |
317 | print "starting sync for realm $realm\n"; |
318 | ||
319 | my ($synced_users, $dnmap) = $plugin->get_users($realmconfig, $realm); | |
320 | my $synced_groups = {}; | |
321 | if ($scope eq 'groups' || $scope eq 'both') { | |
322 | $synced_groups = $plugin->get_groups($realmconfig, $realm, $dnmap); | |
673d2bf2 DC |
323 | } |
324 | ||
cf109814 | 325 | PVE::AccessControl::lock_user_config(sub { |
673d2bf2 | 326 | my $usercfg = cfs_read_file("user.cfg"); |
cf109814 | 327 | print "got data from server, updating $whatstring\n"; |
673d2bf2 DC |
328 | |
329 | if ($sync_users) { | |
330 | print "syncing users\n"; | |
331 | my $oldusers = $usercfg->{users}; | |
332 | ||
333 | my $oldtokens = {}; | |
334 | my $oldenabled = {}; | |
335 | ||
336 | if ($param->{full}) { | |
337 | print "full sync, deleting existing users first\n"; | |
338 | foreach my $userid (keys %$oldusers) { | |
339 | next if $userid !~ m/\@$realm$/; | |
340 | # we save the old tokens | |
341 | $oldtokens->{$userid} = $oldusers->{$userid}->{tokens}; | |
342 | $oldenabled->{$userid} = $oldusers->{$userid}->{enable} // 0; | |
343 | delete $oldusers->{$userid}; | |
344 | PVE::AccessControl::delete_user_acl($userid, $usercfg) | |
345 | if $param->{purge} && !$users->{$userid}; | |
346 | print "removed user '$userid'\n"; | |
347 | } | |
348 | } | |
349 | ||
350 | foreach my $userid (keys %$users) { | |
351 | my $user = $users->{$userid}; | |
352 | if (!defined($oldusers->{$userid})) { | |
353 | $oldusers->{$userid} = $user; | |
354 | ||
355 | if (defined($oldenabled->{$userid})) { | |
356 | $oldusers->{$userid}->{enable} = $oldenabled->{$userid}; | |
357 | } elsif ($param->{enable}) { | |
358 | $oldusers->{$userid}->{enable} = 1; | |
359 | } | |
360 | ||
361 | if (defined($oldtokens->{$userid})) { | |
362 | $oldusers->{$userid}->{tokens} = $oldtokens->{$userid}; | |
363 | } | |
364 | ||
365 | print "added user '$userid'\n"; | |
366 | } else { | |
367 | my $olduser = $oldusers->{$userid}; | |
368 | foreach my $attr (keys %$user) { | |
369 | $olduser->{$attr} = $user->{$attr}; | |
370 | } | |
371 | print "updated user '$userid'\n"; | |
372 | } | |
373 | } | |
374 | } | |
375 | ||
376 | if ($sync_groups) { | |
377 | print "syncing groups\n"; | |
378 | my $oldgroups = $usercfg->{groups}; | |
379 | ||
380 | if ($param->{full}) { | |
381 | print "full sync, deleting existing groups first\n"; | |
382 | foreach my $groupid (keys %$oldgroups) { | |
383 | next if $groupid !~ m/\-$realm$/; | |
384 | delete $oldgroups->{$groupid}; | |
385 | PVE::AccessControl::delete_group_acl($groupid, $usercfg) | |
386 | if $param->{purge} && !$groups->{$groupid}; | |
387 | print "removed group '$groupid'\n"; | |
388 | } | |
389 | } | |
390 | ||
391 | foreach my $groupid (keys %$groups) { | |
392 | my $group = $groups->{$groupid}; | |
393 | if (!defined($oldgroups->{$groupid})) { | |
394 | $oldgroups->{$groupid} = $group; | |
395 | print "added group '$groupid'\n"; | |
396 | } else { | |
397 | my $oldgroup = $oldgroups->{$groupid}; | |
398 | foreach my $attr (keys %$group) { | |
399 | $oldgroup->{$attr} = $group->{$attr}; | |
400 | } | |
401 | print "updated group '$groupid'\n"; | |
402 | } | |
403 | } | |
404 | } | |
cf109814 | 405 | |
673d2bf2 | 406 | cfs_write_file("user.cfg", $usercfg); |
cf109814 TL |
407 | print "successfully updated $whatstring configuration\n"; |
408 | }, "syncing $whatstring failed"); | |
673d2bf2 DC |
409 | }; |
410 | ||
6a2138e4 | 411 | return $rpcenv->fork_worker('auth-realm-sync', $realm, $authuser, $worker); |
673d2bf2 DC |
412 | }}); |
413 | ||
2c3a6c0a | 414 | 1; |