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