]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/access/openid.rs
cleanup schema function calls
[proxmox-backup.git] / src / api2 / access / openid.rs
CommitLineData
3b7b1dfb
DM
1//! OpenID redirect/login API
2use std::convert::TryFrom;
3
36b7085e 4use anyhow::{bail, format_err, Error};
3b7b1dfb
DM
5use serde_json::{json, Value};
6
e25982f2 7use proxmox_sys::sortable;
6ef1b649
WB
8use proxmox_router::{
9 http_err, list_subdirs_api_method, Router, RpcEnvironment, SubdirMap, Permission,
10};
9fa3026a 11use proxmox_schema::api;
3b7b1dfb 12
10beed11 13use proxmox_openid::{OpenIdAuthenticator, OpenIdConfig};
26a3450f 14
10beed11
DM
15use pbs_api_types::{
16 OpenIdRealmConfig, User, Userid,
17 EMAIL_SCHEMA, FIRST_NAME_SCHEMA, LAST_NAME_SCHEMA, OPENID_DEFAILT_SCOPE_LIST,
18 REALM_ID_SCHEMA,
19};
923f94a4 20use pbs_buildcfg::PROXMOX_BACKUP_RUN_DIR_M;
9eb78407 21use pbs_tools::ticket::Ticket;
3b7b1dfb 22
ba3d7e19 23use pbs_config::CachedUserInfo;
21211748 24use pbs_config::open_backup_lockfile;
7526d864 25
3b7b1dfb 26use crate::auth_helpers::*;
01a08021 27use crate::server::ticket::ApiTicket;
3b7b1dfb 28
26a3450f 29fn openid_authenticator(realm_config: &OpenIdRealmConfig, redirect_url: &str) -> Result<OpenIdAuthenticator, Error> {
10beed11
DM
30
31 let scopes: Vec<String> = realm_config.scopes.as_deref().unwrap_or(OPENID_DEFAILT_SCOPE_LIST)
32 .split(|c: char| c == ',' || c == ';' || char::is_ascii_whitespace(&c))
33 .filter(|s| !s.is_empty())
34 .map(String::from)
35 .collect();
36
37 let mut acr_values = None;
38 if let Some(ref list) = realm_config.acr_values {
39 acr_values = Some(
40 list
41 .split(|c: char| c == ',' || c == ';' || char::is_ascii_whitespace(&c))
42 .filter(|s| !s.is_empty())
43 .map(String::from)
44 .collect()
45 );
46 }
47
26a3450f
FG
48 let config = OpenIdConfig {
49 issuer_url: realm_config.issuer_url.clone(),
50 client_id: realm_config.client_id.clone(),
51 client_key: realm_config.client_key.clone(),
10beed11
DM
52 prompt: realm_config.prompt.clone(),
53 scopes: Some(scopes),
54 acr_values,
26a3450f
FG
55 };
56 OpenIdAuthenticator::discover(&config, redirect_url)
57}
58
3b7b1dfb
DM
59#[api(
60 input: {
61 properties: {
62 state: {
63 description: "OpenId state.",
64 type: String,
65 },
66 code: {
67 description: "OpenId authorization code.",
68 type: String,
69 },
70 "redirect-url": {
71 description: "Redirection Url. The client should set this to used server url.",
72 type: String,
73 },
74 },
75 },
76 returns: {
77 properties: {
78 username: {
79 type: String,
80 description: "User name.",
81 },
82 ticket: {
83 type: String,
84 description: "Auth ticket.",
85 },
86 CSRFPreventionToken: {
87 type: String,
88 description: "Cross Site Request Forgery Prevention Token.",
89 },
90 },
91 },
92 protected: true,
93 access: {
94 permission: &Permission::World,
95 },
96)]
97/// Verify OpenID authorization code and create a ticket
98///
99/// Returns: An authentication ticket with additional infos.
100pub fn openid_login(
101 state: String,
102 code: String,
103 redirect_url: String,
36b7085e 104 rpcenv: &mut dyn RpcEnvironment,
3b7b1dfb 105) -> Result<Value, Error> {
36b7085e
DM
106 use proxmox_rest_server::RestEnvironment;
107
108 let env: &RestEnvironment = rpcenv.as_any().downcast_ref::<RestEnvironment>()
109 .ok_or_else(|| format_err!("detected worng RpcEnvironment type"))?;
110
3b7b1dfb
DM
111 let user_info = CachedUserInfo::new()?;
112
36b7085e
DM
113 let mut tested_username = None;
114
6ef1b649 115 let result = proxmox_lang::try_block!({
3b7b1dfb 116
36b7085e
DM
117 let (realm, private_auth_state) =
118 OpenIdAuthenticator::verify_public_auth_state(PROXMOX_BACKUP_RUN_DIR_M!(), &state)?;
3b7b1dfb 119
36b7085e
DM
120 let (domains, _digest) = pbs_config::domains::config()?;
121 let config: OpenIdRealmConfig = domains.lookup("openid", &realm)?;
3b7b1dfb 122
36b7085e 123 let open_id = openid_authenticator(&config, &redirect_url)?;
3b7b1dfb 124
10beed11 125 let info = open_id.verify_authorization_code_simple(&code, &private_auth_state)?;
3b7b1dfb 126
10beed11 127 // eprintln!("VERIFIED {:?}", info);
36b7085e 128
10beed11
DM
129 let name_attr = config.username_claim.as_deref().unwrap_or("sub");
130
131 // Try to be compatible with previous versions
132 let try_attr = match name_attr {
133 "subject" => Some("sub"),
134 "username" => Some("preferred_username"),
135 _ => None,
136 };
137
138 let unique_name = match info[name_attr].as_str() {
139 Some(name) => name.to_owned(),
140 None => {
141 if let Some(try_attr) = try_attr {
142 match info[try_attr].as_str() {
143 Some(name) => name.to_owned(),
144 None => bail!("missing claim '{}'", name_attr),
145 }
146 } else {
147 bail!("missing claim '{}'", name_attr);
36b7085e 148 }
3b7b1dfb 149 }
36b7085e
DM
150 };
151
36b7085e
DM
152 let user_id = Userid::try_from(format!("{}@{}", unique_name, realm))?;
153 tested_username = Some(unique_name.to_string());
154
155 if !user_info.is_active_user_id(&user_id) {
156 if config.autocreate.unwrap_or(false) {
157 use pbs_config::user;
158 let _lock = open_backup_lockfile(user::USER_CFG_LOCKFILE, None, true)?;
68fd9ca6 159
10beed11 160 let firstname = info["given_name"].as_str().map(|n| n.to_string())
9fa3026a 161 .filter(|n| FIRST_NAME_SCHEMA.parse_simple_value(n).is_ok());
68fd9ca6 162
10beed11 163 let lastname = info["family_name"].as_str().map(|n| n.to_string())
9fa3026a 164 .filter(|n| LAST_NAME_SCHEMA.parse_simple_value(n).is_ok());
68fd9ca6 165
10beed11 166 let email = info["email"].as_str().map(|n| n.to_string())
9fa3026a 167 .filter(|n| EMAIL_SCHEMA.parse_simple_value(n).is_ok());
68fd9ca6 168
36b7085e
DM
169 let user = User {
170 userid: user_id.clone(),
171 comment: None,
172 enable: None,
173 expire: None,
68fd9ca6
DM
174 firstname,
175 lastname,
176 email,
36b7085e
DM
177 };
178 let (mut config, _digest) = user::config()?;
689ed513
DM
179 if let Ok(old_user) = config.lookup::<User>("user", user.userid.as_str()) {
180 if let Some(false) = old_user.enable {
181 bail!("user '{}' is disabled.", user.userid);
182 } else {
183 bail!("autocreate user failed - '{}' already exists.", user.userid);
184 }
36b7085e
DM
185 }
186 config.set_data(user.userid.as_str(), "user", &user)?;
187 user::save_config(&config)?;
188 } else {
189 bail!("user account '{}' missing, disabled or expired.", user_id);
3b7b1dfb 190 }
3b7b1dfb 191 }
3b7b1dfb 192
36b7085e
DM
193 let api_ticket = ApiTicket::full(user_id.clone());
194 let ticket = Ticket::new("PBS", &api_ticket)?.sign(private_auth_key(), None)?;
195 let token = assemble_csrf_prevention_token(csrf_secret(), &user_id);
3b7b1dfb 196
36b7085e
DM
197 env.log_auth(user_id.as_str());
198
199 Ok(json!({
200 "username": user_id,
201 "ticket": ticket,
202 "CSRFPreventionToken": token,
203 }))
204 });
205
206 if let Err(ref err) = result {
207 let msg = err.to_string();
208 env.log_failed_auth(tested_username, &msg);
209 return Err(http_err!(UNAUTHORIZED, "{}", msg))
210 }
3b7b1dfb 211
36b7085e 212 result
3b7b1dfb
DM
213}
214
215#[api(
216 protected: true,
217 input: {
218 properties: {
219 realm: {
220 schema: REALM_ID_SCHEMA,
221 },
222 "redirect-url": {
223 description: "Redirection Url. The client should set this to used server url.",
224 type: String,
225 },
226 },
227 },
228 returns: {
229 description: "Redirection URL.",
230 type: String,
231 },
232 access: {
233 description: "Anyone can access this (before the user is authenticated).",
234 permission: &Permission::World,
235 },
236)]
237/// Create OpenID Redirect Session
238fn openid_auth_url(
239 realm: String,
240 redirect_url: String,
241 _rpcenv: &mut dyn RpcEnvironment,
242) -> Result<String, Error> {
243
21211748 244 let (domains, _digest) = pbs_config::domains::config()?;
3b7b1dfb
DM
245 let config: OpenIdRealmConfig = domains.lookup("openid", &realm)?;
246
26a3450f 247 let open_id = openid_authenticator(&config, &redirect_url)?;
3b7b1dfb
DM
248
249 let url = open_id.authorize_url(PROXMOX_BACKUP_RUN_DIR_M!(), &realm)?
250 .to_string();
251
252 Ok(url.into())
253}
254
255#[sortable]
256const SUBDIRS: SubdirMap = &sorted!([
257 ("login", &Router::new().post(&API_METHOD_OPENID_LOGIN)),
258 ("auth-url", &Router::new().post(&API_METHOD_OPENID_AUTH_URL)),
259]);
260
261pub const ROUTER: Router = Router::new()
262 .get(&list_subdirs_api_method!(SUBDIRS))
263 .subdirs(SUBDIRS);