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