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