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