]>
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 | ||
100 | my $realm = extract_param($param, 'realm'); | |
101 | my $redirect_url = extract_param($param, 'redirect-url'); | |
102 | ||
103 | my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url); | |
104 | my $url = $openid->authorize_url($openid_state_path , $realm); | |
105 | ||
106 | return $url; | |
107 | }}); | |
108 | ||
109 | __PACKAGE__->register_method ({ | |
110 | name => 'login', | |
111 | path => 'login', | |
112 | method => 'POST', | |
113 | protected => 1, | |
114 | description => " Verify OpenID authorization code and create a ticket.", | |
115 | parameters => { | |
116 | additionalProperties => 0, | |
117 | properties => { | |
118 | 'state' => { | |
119 | description => "OpenId state.", | |
120 | type => 'string', | |
121 | maxLength => 1024, | |
122 | }, | |
123 | code => { | |
124 | description => "OpenId authorization code.", | |
125 | type => 'string', | |
126 | maxLength => 1024, | |
127 | }, | |
128 | 'redirect-url' => { | |
129 | description => "Redirection Url. The client should set this to the used server url (location.origin).", | |
130 | type => 'string', | |
131 | maxLength => 255, | |
132 | }, | |
133 | }, | |
134 | }, | |
135 | returns => { | |
136 | properties => { | |
137 | username => { type => 'string' }, | |
138 | ticket => { type => 'string' }, | |
139 | CSRFPreventionToken => { type => 'string' }, | |
140 | cap => { type => 'object' }, # computed api permissions | |
141 | clustername => { type => 'string', optional => 1 }, | |
142 | }, | |
143 | }, | |
144 | permissions => { user => 'world' }, | |
145 | code => sub { | |
146 | my ($param) = @_; | |
147 | ||
148 | my $rpcenv = PVE::RPCEnvironment::get(); | |
149 | ||
150 | my $res; | |
151 | eval { | |
152 | my ($realm, $private_auth_state) = PVE::RS::OpenId::verify_public_auth_state( | |
153 | $openid_state_path, $param->{'state'}); | |
154 | ||
155 | my $redirect_url = extract_param($param, 'redirect-url'); | |
156 | ||
157 | my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url); | |
158 | ||
159 | my $info = $openid->verify_authorization_code($param->{code}, $private_auth_state); | |
160 | my $subject = $info->{'sub'}; | |
161 | ||
162 | die "missing openid claim 'sub'\n" if !defined($subject); | |
163 | ||
164 | my $unique_name = $subject; # default | |
165 | if (defined(my $user_attr = $config->{'username-claim'})) { | |
166 | if ($user_attr eq 'subject') { | |
167 | $unique_name = $subject; | |
168 | } elsif ($user_attr eq 'username') { | |
169 | my $username = $info->{'preferred_username'}; | |
170 | die "missing claim 'preferred_username'\n" if !defined($username); | |
171 | $unique_name = $username; | |
172 | } elsif ($user_attr eq 'email') { | |
173 | my $email = $info->{'email'}; | |
174 | die "missing claim 'email'\n" if !defined($email); | |
175 | $unique_name = $email; | |
176 | } else { | |
177 | die "got unexpected value for 'username-claim': '${user_attr}'\n"; | |
178 | } | |
179 | } | |
180 | ||
181 | my $username = "${unique_name}\@${realm}"; | |
182 | ||
fbf4594a DM |
183 | # first, check if $username respects our naming conventions |
184 | PVE::Auth::Plugin::verify_username($username); | |
185 | ||
186 | if ($config->{'autocreate'} && !$rpcenv->check_user_exist($username, 1)) { | |
187 | PVE::AccessControl::lock_user_config(sub { | |
188 | my $usercfg = cfs_read_file("user.cfg"); | |
189 | ||
190 | die "user '$username' already exists\n" if $usercfg->{users}->{$username}; | |
191 | ||
192 | my $entry = { enable => 1 }; | |
193 | if (defined(my $email = $info->{'email'})) { | |
194 | $entry->{email} = $email; | |
195 | } | |
196 | if (defined(my $given_name = $info->{'given_name'})) { | |
197 | $entry->{firstname} = $given_name; | |
198 | } | |
199 | if (defined(my $family_name = $info->{'family_name'})) { | |
200 | $entry->{lastname} = $family_name; | |
201 | } | |
202 | ||
203 | $usercfg->{users}->{$username} = $entry; | |
204 | ||
205 | cfs_write_file("user.cfg", $usercfg); | |
206 | }, "autocreate openid user failed"); | |
207 | } else { | |
208 | # test if user exists and is enabled | |
209 | $rpcenv->check_user_enabled($username); | |
210 | } | |
ac344d7d DM |
211 | |
212 | my $ticket = PVE::AccessControl::assemble_ticket($username); | |
213 | my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username); | |
214 | my $cap = $rpcenv->compute_api_permission($username); | |
215 | ||
216 | $res = { | |
217 | ticket => $ticket, | |
218 | username => $username, | |
219 | CSRFPreventionToken => $csrftoken, | |
220 | cap => $cap, | |
221 | }; | |
222 | ||
223 | my $clinfo = PVE::Cluster::get_clinfo(); | |
224 | if ($clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)) { | |
225 | $res->{clustername} = $clinfo->{cluster}->{name}; | |
226 | } | |
227 | }; | |
228 | if (my $err = $@) { | |
229 | my $clientip = $rpcenv->get_client_ip() || ''; | |
230 | syslog('err', "openid authentication failure; rhost=$clientip msg=$err"); | |
231 | # do not return any info to prevent user enumeration attacks | |
232 | die PVE::Exception->new("authentication failure\n", code => 401); | |
233 | } | |
234 | ||
235 | PVE::Cluster::log_msg('info', 'root@pam', "successful openid auth for user '$res->{username}'"); | |
236 | ||
237 | return $res; | |
238 | }}); |