]>
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; | |
12 | use PVE::Cluster qw(cfs_read_file); | |
13 | use PVE::AccessControl; | |
14 | use PVE::JSONSchema qw(get_standard_option); | |
15 | ||
16 | use PVE::RESTHandler; | |
17 | ||
18 | use base qw(PVE::RESTHandler); | |
19 | ||
20 | my $openid_state_path = "/var/lib/pve-manager"; | |
21 | ||
22 | my $lookup_openid_auth = sub { | |
23 | my ($realm, $redirect_url) = @_; | |
24 | ||
25 | my $cfg = cfs_read_file('domains.cfg'); | |
26 | my $ids = $cfg->{ids}; | |
27 | ||
28 | die "authentication domain '$realm' does not exist\n" if !$ids->{$realm}; | |
29 | ||
30 | my $config = $ids->{$realm}; | |
31 | die "wrong realm type ($config->{type} != openid)\n" if $config->{type} ne "openid"; | |
32 | ||
33 | my $openid_config = { | |
34 | issuer_url => $config->{'issuer-url'}, | |
35 | client_id => $config->{'client-id'}, | |
36 | client_key => $config->{'client-key'}, | |
37 | }; | |
38 | ||
39 | my $openid = PVE::RS::OpenId->discover($openid_config, $redirect_url); | |
40 | return ($config, $openid); | |
41 | }; | |
42 | ||
43 | __PACKAGE__->register_method ({ | |
44 | name => 'index', | |
45 | path => '', | |
46 | method => 'GET', | |
47 | description => "Directory index.", | |
48 | permissions => { | |
49 | user => 'all', | |
50 | }, | |
51 | parameters => { | |
52 | additionalProperties => 0, | |
53 | properties => {}, | |
54 | }, | |
55 | returns => { | |
56 | type => 'array', | |
57 | items => { | |
58 | type => "object", | |
59 | properties => { | |
60 | subdir => { type => 'string' }, | |
61 | }, | |
62 | }, | |
63 | links => [ { rel => 'child', href => "{subdir}" } ], | |
64 | }, | |
65 | code => sub { | |
66 | my ($param) = @_; | |
67 | ||
68 | return [ | |
69 | { subdir => 'auth-url' }, | |
70 | { subdir => 'login' }, | |
71 | ]; | |
72 | }}); | |
73 | ||
74 | __PACKAGE__->register_method ({ | |
75 | name => 'auth_url', | |
76 | path => 'auth-url', | |
77 | method => 'POST', | |
78 | protected => 1, | |
79 | description => "Get the OpenId Authorization Url for the specified realm.", | |
80 | parameters => { | |
81 | additionalProperties => 0, | |
82 | properties => { | |
83 | realm => get_standard_option('realm'), | |
84 | 'redirect-url' => { | |
85 | description => "Redirection Url. The client should set this to the used server url (location.origin).", | |
86 | type => 'string', | |
87 | maxLength => 255, | |
88 | }, | |
89 | }, | |
90 | }, | |
91 | returns => { | |
92 | type => "string", | |
93 | description => "Redirection URL.", | |
94 | }, | |
95 | permissions => { user => 'world' }, | |
96 | code => sub { | |
97 | my ($param) = @_; | |
98 | ||
99 | my $realm = extract_param($param, 'realm'); | |
100 | my $redirect_url = extract_param($param, 'redirect-url'); | |
101 | ||
102 | my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url); | |
103 | my $url = $openid->authorize_url($openid_state_path , $realm); | |
104 | ||
105 | return $url; | |
106 | }}); | |
107 | ||
108 | __PACKAGE__->register_method ({ | |
109 | name => 'login', | |
110 | path => 'login', | |
111 | method => 'POST', | |
112 | protected => 1, | |
113 | description => " Verify OpenID authorization code and create a ticket.", | |
114 | parameters => { | |
115 | additionalProperties => 0, | |
116 | properties => { | |
117 | 'state' => { | |
118 | description => "OpenId state.", | |
119 | type => 'string', | |
120 | maxLength => 1024, | |
121 | }, | |
122 | code => { | |
123 | description => "OpenId authorization code.", | |
124 | type => 'string', | |
125 | maxLength => 1024, | |
126 | }, | |
127 | 'redirect-url' => { | |
128 | description => "Redirection Url. The client should set this to the used server url (location.origin).", | |
129 | type => 'string', | |
130 | maxLength => 255, | |
131 | }, | |
132 | }, | |
133 | }, | |
134 | returns => { | |
135 | properties => { | |
136 | username => { type => 'string' }, | |
137 | ticket => { type => 'string' }, | |
138 | CSRFPreventionToken => { type => 'string' }, | |
139 | cap => { type => 'object' }, # computed api permissions | |
140 | clustername => { type => 'string', optional => 1 }, | |
141 | }, | |
142 | }, | |
143 | permissions => { user => 'world' }, | |
144 | code => sub { | |
145 | my ($param) = @_; | |
146 | ||
147 | my $rpcenv = PVE::RPCEnvironment::get(); | |
148 | ||
149 | my $res; | |
150 | eval { | |
151 | my ($realm, $private_auth_state) = PVE::RS::OpenId::verify_public_auth_state( | |
152 | $openid_state_path, $param->{'state'}); | |
153 | ||
154 | my $redirect_url = extract_param($param, 'redirect-url'); | |
155 | ||
156 | my ($config, $openid) = $lookup_openid_auth->($realm, $redirect_url); | |
157 | ||
158 | my $info = $openid->verify_authorization_code($param->{code}, $private_auth_state); | |
159 | my $subject = $info->{'sub'}; | |
160 | ||
161 | die "missing openid claim 'sub'\n" if !defined($subject); | |
162 | ||
163 | my $unique_name = $subject; # default | |
164 | if (defined(my $user_attr = $config->{'username-claim'})) { | |
165 | if ($user_attr eq 'subject') { | |
166 | $unique_name = $subject; | |
167 | } elsif ($user_attr eq 'username') { | |
168 | my $username = $info->{'preferred_username'}; | |
169 | die "missing claim 'preferred_username'\n" if !defined($username); | |
170 | $unique_name = $username; | |
171 | } elsif ($user_attr eq 'email') { | |
172 | my $email = $info->{'email'}; | |
173 | die "missing claim 'email'\n" if !defined($email); | |
174 | $unique_name = $email; | |
175 | } else { | |
176 | die "got unexpected value for 'username-claim': '${user_attr}'\n"; | |
177 | } | |
178 | } | |
179 | ||
180 | my $username = "${unique_name}\@${realm}"; | |
181 | ||
182 | # test if user exists and is enabled | |
183 | $rpcenv->check_user_enabled($username); | |
184 | ||
185 | my $ticket = PVE::AccessControl::assemble_ticket($username); | |
186 | my $csrftoken = PVE::AccessControl::assemble_csrf_prevention_token($username); | |
187 | my $cap = $rpcenv->compute_api_permission($username); | |
188 | ||
189 | $res = { | |
190 | ticket => $ticket, | |
191 | username => $username, | |
192 | CSRFPreventionToken => $csrftoken, | |
193 | cap => $cap, | |
194 | }; | |
195 | ||
196 | my $clinfo = PVE::Cluster::get_clinfo(); | |
197 | if ($clinfo->{cluster}->{name} && $rpcenv->check($username, '/', ['Sys.Audit'], 1)) { | |
198 | $res->{clustername} = $clinfo->{cluster}->{name}; | |
199 | } | |
200 | }; | |
201 | if (my $err = $@) { | |
202 | my $clientip = $rpcenv->get_client_ip() || ''; | |
203 | syslog('err', "openid authentication failure; rhost=$clientip msg=$err"); | |
204 | # do not return any info to prevent user enumeration attacks | |
205 | die PVE::Exception->new("authentication failure\n", code => 401); | |
206 | } | |
207 | ||
208 | PVE::Cluster::log_msg('info', 'root@pam', "successful openid auth for user '$res->{username}'"); | |
209 | ||
210 | return $res; | |
211 | }}); |