]>
Commit | Line | Data |
---|---|---|
ac344d7d DM |
1 | package PVE::API2::OpenId; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use PVE::Tools qw(extract_param); | |
7 | use PVE::RS::OpenId; | |
8 | ||
9 | use PVE::Exception qw(raise raise_perm_exc raise_param_exc); | |
10 | use PVE::SafeSyslog; | |
11 | use PVE::RPCEnvironment; | |
fbf4594a | 12 | use PVE::Cluster qw(cfs_read_file cfs_write_file); |
ac344d7d DM |
13 | use PVE::AccessControl; |
14 | use PVE::JSONSchema qw(get_standard_option); | |
fbf4594a | 15 | use PVE::Auth::Plugin; |
ac344d7d DM |
16 | |
17 | use PVE::RESTHandler; | |
18 | ||
19 | use base qw(PVE::RESTHandler); | |
20 | ||
21 | my $openid_state_path = "/var/lib/pve-manager"; | |
22 | ||
23 | my $lookup_openid_auth = sub { | |
24 | my ($realm, $redirect_url) = @_; | |
25 | ||
26 | my $cfg = cfs_read_file('domains.cfg'); | |
27 | my $ids = $cfg->{ids}; | |
28 | ||
29 | die "authentication domain '$realm' does not exist\n" if !$ids->{$realm}; | |
30 | ||
31 | my $config = $ids->{$realm}; | |
32 | die "wrong realm type ($config->{type} != openid)\n" if $config->{type} ne "openid"; | |
33 | ||
34 | my $openid_config = { | |
35 | issuer_url => $config->{'issuer-url'}, | |
36 | client_id => $config->{'client-id'}, | |
37 | client_key => $config->{'client-key'}, | |
38 | }; | |
348c7038 | 39 | $openid_config->{prompt} = $config->{'prompt'} if defined($config->{'prompt'}); |
ac344d7d | 40 | |
48e51c33 TL |
41 | my $scopes = $config->{'scopes'} // 'email profile'; |
42 | $openid_config->{scopes} = [ PVE::Tools::split_list($scopes) ]; | |
43 | ||
bc9d1159 TL |
44 | if (defined(my $acr = $config->{'acr-values'})) { |
45 | $openid_config->{acr_values} = [ PVE::Tools::split_list($acr) ]; | |
46 | } | |
47 | ||
ac344d7d DM |
48 | my $openid = PVE::RS::OpenId->discover($openid_config, $redirect_url); |
49 | return ($config, $openid); | |
50 | }; | |
51 | ||
52 | __PACKAGE__->register_method ({ | |
53 | name => 'index', | |
54 | path => '', | |
55 | method => 'GET', | |
56 | description => "Directory index.", | |
57 | permissions => { | |
58 | user => 'all', | |
59 | }, | |
60 | parameters => { | |
61 | additionalProperties => 0, | |
62 | properties => {}, | |
63 | }, | |
64 | returns => { | |
65 | type => 'array', | |
66 | items => { | |
67 | type => "object", | |
68 | properties => { | |
69 | subdir => { type => 'string' }, | |
70 | }, | |
71 | }, | |
72 | links => [ { rel => 'child', href => "{subdir}" } ], | |
73 | }, | |
74 | code => sub { | |
75 | my ($param) = @_; | |
76 | ||
77 | return [ | |
78 | { subdir => 'auth-url' }, | |
79 | { subdir => 'login' }, | |
80 | ]; | |
81 | }}); | |
82 | ||
83 | __PACKAGE__->register_method ({ | |
84 | name => 'auth_url', | |
85 | path => 'auth-url', | |
86 | method => 'POST', | |
87 | protected => 1, | |
88 | description => "Get the OpenId Authorization Url for the specified realm.", | |
89 | parameters => { | |
90 | additionalProperties => 0, | |
91 | properties => { | |
92 | realm => get_standard_option('realm'), | |
93 | 'redirect-url' => { | |
94 | description => "Redirection Url. The client should set this to the used server url (location.origin).", | |
95 | type => 'string', | |
96 | maxLength => 255, | |
97 | }, | |
98 | }, | |
99 | }, | |
100 | returns => { | |
101 | type => "string", | |
102 | description => "Redirection URL.", | |
103 | }, | |
104 | permissions => { user => 'world' }, | |
105 | code => sub { | |
106 | my ($param) = @_; | |
107 | ||
bb0cfca4 | 108 | my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg'); |
afda4f1a | 109 | local $ENV{all_proxy} = $dcconf->{http_proxy} if exists $dcconf->{http_proxy}; |
bb0cfca4 | 110 | |
ac344d7d DM |
111 | my $realm = extract_param($param, 'realm'); |
112 | my $redirect_url = extract_param($param, 'redirect-url'); | |
113 | ||
114 | my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url); | |
115 | my $url = $openid->authorize_url($openid_state_path , $realm); | |
116 | ||
117 | return $url; | |
118 | }}); | |
119 | ||
120 | __PACKAGE__->register_method ({ | |
121 | name => 'login', | |
122 | path => 'login', | |
123 | method => 'POST', | |
124 | protected => 1, | |
125 | description => " Verify OpenID authorization code and create a ticket.", | |
126 | parameters => { | |
127 | additionalProperties => 0, | |
128 | properties => { | |
129 | 'state' => { | |
130 | description => "OpenId state.", | |
131 | type => 'string', | |
132 | maxLength => 1024, | |
133 | }, | |
134 | code => { | |
135 | description => "OpenId authorization code.", | |
136 | type => 'string', | |
137 | maxLength => 1024, | |
138 | }, | |
139 | 'redirect-url' => { | |
140 | description => "Redirection Url. The client should set this to the used server url (location.origin).", | |
141 | type => 'string', | |
142 | maxLength => 255, | |
143 | }, | |
144 | }, | |
145 | }, | |
146 | returns => { | |
147 | properties => { | |
148 | username => { type => 'string' }, | |
149 | ticket => { type => 'string' }, | |
150 | CSRFPreventionToken => { type => 'string' }, | |
151 | cap => { type => 'object' }, # computed api permissions | |
152 | clustername => { type => 'string', optional => 1 }, | |
153 | }, | |
154 | }, | |
155 | permissions => { user => 'world' }, | |
156 | code => sub { | |
157 | my ($param) = @_; | |
158 | ||
159 | my $rpcenv = PVE::RPCEnvironment::get(); | |
160 | ||
161 | my $res; | |
162 | eval { | |
bb0cfca4 | 163 | my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg'); |
afda4f1a | 164 | local $ENV{all_proxy} = $dcconf->{http_proxy} if exists $dcconf->{http_proxy}; |
bb0cfca4 | 165 | |
ac344d7d DM |
166 | my ($realm, $private_auth_state) = PVE::RS::OpenId::verify_public_auth_state( |
167 | $openid_state_path, $param->{'state'}); | |
168 | ||
169 | my $redirect_url = extract_param($param, 'redirect-url'); | |
170 | ||
171 | my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url); | |
172 | ||
173 | my $info = $openid->verify_authorization_code($param->{code}, $private_auth_state); | |
174 | my $subject = $info->{'sub'}; | |
175 | ||
271bbc10 | 176 | my $unique_name; |
aa71c0f0 TL |
177 | |
178 | my $user_attr = $config->{'username-claim'} // 'sub'; | |
179 | if (defined($info->{$user_attr})) { | |
180 | $unique_name = $info->{$user_attr}; | |
181 | } elsif ($user_attr eq 'subject') { # stay compat with old versions | |
182 | $unique_name = $subject; | |
183 | } elsif ($user_attr eq 'username') { # stay compat with old versions | |
184 | my $username = $info->{'preferred_username'}; | |
185 | die "missing claim 'preferred_username'\n" if !defined($username); | |
186 | $unique_name = $username; | |
187 | } else { | |
188 | # neither the attr nor fallback are defined in info.. | |
189 | die "missing configured claim '$user_attr' in returned info object\n"; | |
ac344d7d DM |
190 | } |
191 | ||
192 | my $username = "${unique_name}\@${realm}"; | |
193 | ||
fbf4594a DM |
194 | # first, check if $username respects our naming conventions |
195 | PVE::Auth::Plugin::verify_username($username); | |
196 | ||
197 | if ($config->{'autocreate'} && !$rpcenv->check_user_exist($username, 1)) { | |
198 | PVE::AccessControl::lock_user_config(sub { | |
199 | my $usercfg = cfs_read_file("user.cfg"); | |
200 | ||
201 | die "user '$username' already exists\n" if $usercfg->{users}->{$username}; | |
202 | ||
203 | my $entry = { enable => 1 }; | |
204 | if (defined(my $email = $info->{'email'})) { | |
205 | $entry->{email} = $email; | |
206 | } | |
207 | if (defined(my $given_name = $info->{'given_name'})) { | |
208 | $entry->{firstname} = $given_name; | |
209 | } | |
210 | if (defined(my $family_name = $info->{'family_name'})) { | |
211 | $entry->{lastname} = $family_name; | |
212 | } | |
213 | ||
214 | $usercfg->{users}->{$username} = $entry; | |
215 | ||
216 | cfs_write_file("user.cfg", $usercfg); | |
217 | }, "autocreate openid user failed"); | |
218 | } else { | |
219 | # test if user exists and is enabled | |
220 | $rpcenv->check_user_enabled($username); | |
221 | } | |
ac344d7d DM |
222 | |
223 | my $ticket = PVE::AccessControl::assemble_ticket($username); | |
224 | my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username); | |
225 | my $cap = $rpcenv->compute_api_permission($username); | |
226 | ||
227 | $res = { | |
228 | ticket => $ticket, | |
229 | username => $username, | |
230 | CSRFPreventionToken => $csrftoken, | |
231 | cap => $cap, | |
232 | }; | |
233 | ||
234 | my $clinfo = PVE::Cluster::get_clinfo(); | |
235 | if ($clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)) { | |
236 | $res->{clustername} = $clinfo->{cluster}->{name}; | |
237 | } | |
238 | }; | |
239 | if (my $err = $@) { | |
240 | my $clientip = $rpcenv->get_client_ip() || ''; | |
241 | syslog('err', "openid authentication failure; rhost=$clientip msg=$err"); | |
242 | # do not return any info to prevent user enumeration attacks | |
243 | die PVE::Exception->new("authentication failure\n", code => 401); | |
244 | } | |
245 | ||
246 | PVE::Cluster::log_msg('info', 'root@pam', "successful openid auth for user '$res->{username}'"); | |
247 | ||
248 | return $res; | |
249 | }}); |