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