]> git.proxmox.com Git - pve-access-control.git/blame - src/PVE/Auth/Plugin.pm
realm: sync: allow explicit 'none' for 'remove-vanished' option
[pve-access-control.git] / src / PVE / Auth / Plugin.pm
CommitLineData
5bb4e06a
DM
1package PVE::Auth::Plugin;
2
3use strict;
4use warnings;
b49abe2d 5
5bb4e06a 6use Digest::SHA;
b49abe2d
TL
7use Encode;
8
5bb4e06a 9use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_lock_file);
b49abe2d
TL
10use PVE::JSONSchema qw(get_standard_option);
11use PVE::SectionConfig;
12use PVE::Tools;
5bb4e06a 13
5bb4e06a
DM
14use base qw(PVE::SectionConfig);
15
16my $domainconfigfile = "domains.cfg";
17
0a6e09fd 18cfs_register_file($domainconfigfile,
5bb4e06a
DM
19 sub { __PACKAGE__->parse_config(@_); },
20 sub { __PACKAGE__->write_config(@_); });
21
22sub lock_domain_config {
23 my ($code, $errmsg) = @_;
24
25 cfs_lock_file($domainconfigfile, undef, $code);
26 my $err = $@;
27 if ($err) {
28 $errmsg ? die "$errmsg: $err" : die $err;
29 }
30}
31
8e23f971
FG
32our $realm_regex = qr/[A-Za-z][A-Za-z0-9\.\-_]+/;
33our $user_regex = qr![^\s:/]+!;
5bb4e06a
DM
34
35PVE::JSONSchema::register_format('pve-realm', \&pve_verify_realm);
36sub pve_verify_realm {
37 my ($realm, $noerr) = @_;
0a6e09fd 38
5bb4e06a
DM
39 if ($realm !~ m/^${realm_regex}$/) {
40 return undef if $noerr;
0a6e09fd 41 die "value does not look like a valid realm\n";
5bb4e06a
DM
42 }
43 return $realm;
44}
45
46PVE::JSONSchema::register_standard_option('realm', {
47 description => "Authentication domain ID",
48 type => 'string', format => 'pve-realm',
49 maxLength => 32,
50});
51
98df2ebc
DC
52my $remove_options = "(?:acl|properties|entry)";
53
d29d2d4a
TL
54my $realm_sync_options_desc = {
55 scope => {
56 description => "Select what to sync.",
57 type => 'string',
58 enum => [qw(users groups both)],
59 optional => '1',
60 },
98df2ebc
DC
61 'remove-vanished' => {
62 description => "A semicolon-seperated list of things to remove when they or the user"
63 ." vanishes during a sync. The following values are possible: 'entry' removes the"
64 ." user/group when not returned from the sync. 'properties' removes the set"
65 ." properties on existing user/group that do not appear in the source (even custom ones)."
982db929
DC
66 ." 'acl' removes acls when the user/group is not returned from the sync."
67 ." Instead of a list it also can be 'none' (the default).",
98df2ebc 68 type => 'string',
982db929
DC
69 default => 'none',
70 typetext => "([acl];[properties];[entry])|none",
71 pattern => "(?:(?:$remove_options\;)*$remove_options)|none",
98df2ebc
DC
72 optional => '1',
73 },
74 # TODO check/rewrite in pve7to8, and remove with 8.0
d29d2d4a 75 full => {
98df2ebc
DC
76 description => "DEPRECATED: use 'remove-vanished' instead. If set, uses the LDAP Directory as source of truth,"
77 ." deleting users or groups not returned from the sync and removing"
78 ." all locally modified properties of synced users. If not set,"
79 ." only syncs information which is present in the synced data, and does not"
80 ." delete or modify anything else.",
d29d2d4a
TL
81 type => 'boolean',
82 optional => '1',
83 },
84 'enable-new' => {
85 description => "Enable newly synced users immediately.",
86 type => 'boolean',
87 default => '1',
88 optional => '1',
89 },
90 purge => {
98df2ebc
DC
91 description => "DEPRECATED: use 'remove-vanished' instead. Remove ACLs for users or"
92 ." groups which were removed from the config during a sync.",
d29d2d4a
TL
93 type => 'boolean',
94 optional => '1',
95 },
96};
97PVE::JSONSchema::register_standard_option('realm-sync-options', $realm_sync_options_desc);
98PVE::JSONSchema::register_format('realm-sync-options', $realm_sync_options_desc);
99
5bb4e06a
DM
100PVE::JSONSchema::register_format('pve-userid', \&verify_username);
101sub verify_username {
102 my ($username, $noerr) = @_;
103
104 $username = '' if !$username;
105 my $len = length($username);
106 if ($len < 3) {
107 die "user name '$username' is too short\n" if !$noerr;
108 return undef;
109 }
110 if ($len > 64) {
111 die "user name '$username' is too long ($len > 64)\n" if !$noerr;
112 return undef;
113 }
114
115 # we only allow a limited set of characters
0a6e09fd 116 # colon is not allowed, because we store usernames in
5bb4e06a
DM
117 # colon separated lists)!
118 # slash is not allowed because it is used as pve API delimiter
0a6e09fd 119 # also see "man useradd"
8e23f971 120 if ($username =~ m!^(${user_regex})\@(${realm_regex})$!) {
5bb4e06a
DM
121 return wantarray ? ($username, $1, $2) : $username;
122 }
123
124 die "value '$username' does not look like a valid user name\n" if !$noerr;
125
126 return undef;
127}
128
129PVE::JSONSchema::register_standard_option('userid', {
af5d7da7 130 description => "User ID",
5bb4e06a
DM
131 type => 'string', format => 'pve-userid',
132 maxLength => 64,
133});
134
9401be39
WB
135my $tfa_format = {
136 type => {
137 description => "The type of 2nd factor authentication.",
138 format_description => 'TFATYPE',
139 type => 'string',
140 enum => [qw(yubico oath)],
141 },
142 id => {
143 description => "Yubico API ID.",
144 format_description => 'ID',
145 type => 'string',
146 optional => 1,
147 },
148 key => {
149 description => "Yubico API Key.",
150 format_description => 'KEY',
151 type => 'string',
152 optional => 1,
153 },
154 url => {
155 description => "Yubico API URL.",
156 format_description => 'URL',
157 type => 'string',
158 optional => 1,
159 },
160 digits => {
161 description => "TOTP digits.",
162 format_description => 'COUNT',
163 type => 'integer',
164 minimum => 6, maximum => 8,
165 default => 6,
166 optional => 1,
167 },
168 step => {
169 description => "TOTP time period.",
170 format_description => 'SECONDS',
171 type => 'integer',
172 minimum => 10,
173 default => 30,
174 optional => 1,
175 },
176};
177
178PVE::JSONSchema::register_format('pve-tfa-config', $tfa_format);
96f8ebd6
DM
179
180PVE::JSONSchema::register_standard_option('tfa', {
181 description => "Use Two-factor authentication.",
182 type => 'string', format => 'pve-tfa-config',
183 optional => 1,
184 maxLength => 128,
185});
186
187sub parse_tfa_config {
188 my ($data) = @_;
189
9401be39 190 return PVE::JSONSchema::parse_property_string($tfa_format, $data);
96f8ebd6
DM
191}
192
5bb4e06a
DM
193my $defaultData = {
194 propertyList => {
195 type => { description => "Realm type." },
196 realm => get_standard_option('realm'),
197 },
198};
199
200sub private {
201 return $defaultData;
202}
203
204sub parse_section_header {
205 my ($class, $line) = @_;
206
207 if ($line =~ m/^(\S+):\s*(\S+)\s*$/) {
208 my ($type, $realm) = (lc($1), $2);
209 my $errmsg = undef; # set if you want to skip whole section
210 eval { pve_verify_realm($realm); };
211 $errmsg = $@ if $@;
212 my $config = {}; # to return additional attributes
213 return ($type, $realm, $errmsg, $config);
214 }
215 return undef;
216}
217
218sub parse_config {
219 my ($class, $filename, $raw) = @_;
220
221 my $cfg = $class->SUPER::parse_config($filename, $raw);
222
223 my $default;
224 foreach my $realm (keys %{$cfg->{ids}}) {
225 my $data = $cfg->{ids}->{$realm};
226 # make sure there is only one default marker
227 if ($data->{default}) {
228 if ($default) {
229 delete $data->{default};
230 } else {
231 $default = $realm;
232 }
233 }
234
235 if ($data->{comment}) {
236 $data->{comment} = PVE::Tools::decode_text($data->{comment});
237 }
238
239 }
240
241 # add default domains
242
96f8ebd6
DM
243 $cfg->{ids}->{pve}->{type} = 'pve'; # force type
244 $cfg->{ids}->{pve}->{comment} = "Proxmox VE authentication server"
245 if !$cfg->{ids}->{pve}->{comment};
5bb4e06a 246
96f8ebd6
DM
247 $cfg->{ids}->{pam}->{type} = 'pam'; # force type
248 $cfg->{ids}->{pam}->{plugin} = 'PVE::Auth::PAM';
249 $cfg->{ids}->{pam}->{comment} = "Linux PAM standard authentication"
250 if !$cfg->{ids}->{pam}->{comment};
5bb4e06a
DM
251
252 return $cfg;
253};
254
255sub write_config {
256 my ($class, $filename, $cfg) = @_;
257
5bb4e06a
DM
258 foreach my $realm (keys %{$cfg->{ids}}) {
259 my $data = $cfg->{ids}->{$realm};
260 if ($data->{comment}) {
261 $data->{comment} = PVE::Tools::encode_text($data->{comment});
262 }
263 }
0a6e09fd 264
5bb4e06a
DM
265 $class->SUPER::write_config($filename, $cfg);
266}
267
268sub authenticate_user {
269 my ($class, $config, $realm, $username, $password) = @_;
270
271 die "overwrite me";
272}
273
274sub store_password {
275 my ($class, $config, $realm, $username, $password) = @_;
276
277 my $type = $class->type();
278
279 die "can't set password on auth type '$type'\n";
280}
281
282sub delete_user {
283 my ($class, $config, $realm, $username) = @_;
284
285 # do nothing by default
286}
287
89338e4d
TL
288# called during addition of realm (before the new domain config got written)
289# `password` is moved to %param to avoid writing it out to the config
7d23b7ca 290# die to abort addition if there are (grave) problems
89338e4d
TL
291# NOTE: runs in a domain config *locked* context
292sub on_add_hook {
293 my ($class, $realm, $config, %param) = @_;
294 # do nothing by default
295}
296
297# called during domain configuration update (before the updated domain config got
298# written). `password` is moved to %param to avoid writing it out to the config
299# die to abort the update if there are (grave) problems
300# NOTE: runs in a domain config *locked* context
301sub on_update_hook {
302 my ($class, $realm, $config, %param) = @_;
303 # do nothing by default
304}
305
306# called during deletion of realms (before the new domain config got written)
307# and if the activate check on addition fails, to cleanup all storage traces
308# which on_add_hook may have created.
309# die to abort deletion if there are (very grave) problems
310# NOTE: runs in a storage config *locked* context
311sub on_delete_hook {
312 my ($class, $realm, $config) = @_;
313 # do nothing by default
314}
315
5bb4e06a 3161;