From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 1F7837484B for ; Fri, 9 Jul 2021 13:44:06 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 1CF5B2513E for ; Fri, 9 Jul 2021 13:44:06 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id A3CB425001 for ; Fri, 9 Jul 2021 13:44:03 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 7915E40EE0 for ; Fri, 9 Jul 2021 13:44:03 +0200 (CEST) From: Dominik Csapak To: pbs-devel@lists.proxmox.com Date: Fri, 9 Jul 2021 13:43:59 +0200 Message-Id: <20210709114401.277841-5-d.csapak@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20210709114401.277841-1-d.csapak@proxmox.com> References: <20210709114401.277841-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.633 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [domain.rs] Subject: [pbs-devel] [PATCH proxmox-backup 4/6] api: access: domains: add get/create/update/delete domain call X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 09 Jul 2021 11:44:06 -0000 modeled like our other section config api calls two drawbacks of doing it this way: * we have to copy some api properties again for the update call, since not all of them are updateable (username-claim) * we only handle openid for now, which we would have to change when we add ldap/ad Signed-off-by: Dominik Csapak --- src/api2/access/domain.rs | 280 +++++++++++++++++++++++++++++++++++++- 1 file changed, 277 insertions(+), 3 deletions(-) diff --git a/src/api2/access/domain.rs b/src/api2/access/domain.rs index c850603a..6b1a4743 100644 --- a/src/api2/access/domain.rs +++ b/src/api2/access/domain.rs @@ -1,6 +1,6 @@ //! List Authentication domains/realms -use anyhow::{Error}; +use anyhow::{bail, Error}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; @@ -8,7 +8,11 @@ use serde_json::{json, Value}; use proxmox::api::{api, Permission, Router, RpcEnvironment}; use crate::api2::types::*; -use crate::config::domains::{OpenIdRealmConfig, OpenIdUserAttribute}; +use crate::config::{ + self, + acl::{PRIV_REALM_ALLOCATE, PRIV_SYS_AUDIT}, + domains::{OpenIdRealmConfig, OpenIdUserAttribute}, +}; #[api] #[derive(Deserialize, Serialize, PartialEq, Eq)] @@ -158,5 +162,275 @@ fn list_domains(mut rpcenv: &mut dyn RpcEnvironment) -> Result Result { + let entry = match realm.as_str() { + "pam" => json!({ + "realm": "pam", + "type": "pam", + "comment": "Linux PAM standard authentication", + "default": Some(true), + }), + "pbs" => json!({ + "realm": "pbs", + "type": "pbs", + "comment": "Proxmox Backup authentication server", + }), + _ => { + let (config, digest) = config::domains::config()?; + rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into(); + if let Some((section_type, v)) = config.sections.get(&realm) { + let mut entry = v.clone(); + entry["type"] = Value::from(section_type.clone()); + entry + } else { + bail!("domain '{}' does not exist", realm); + } + } + }; + + Ok(serde_json::from_value(entry)?) +} + +#[api( + protected: true, + input: { + properties: { + info: { + type: RealmInfo, + flatten: true, + }, + }, + }, + access: { + permission: &Permission::Privilege(&["access", "domains"], PRIV_REALM_ALLOCATE, false), + }, +)] +/// Create a realm +fn create_domain(param: Value) -> Result<(), Error> { + let basic_info: BasicRealmInfo = serde_json::from_value(param.clone())?; + + // for now we only have to care about openid + if basic_info.ty != RealmType::OpenId { + bail!( + "Cannot create realm of type '{}'", + serde_json::to_string(&basic_info.ty)? + ); + } + + let new_realm: OpenIdRealmConfig = serde_json::from_value(param)?; + let _lock = config::domains::lock_config()?; + + let (mut config, _digest) = config::domains::config()?; + + let existing: Vec = config.convert_to_typed_array("openid")?; + + for realm in existing { + if realm.realm == new_realm.realm { + bail!("Entry '{}' already exists", realm.realm); + } + } + + config.set_data(&new_realm.realm, "openid", &new_realm)?; + + config::domains::save_config(&config)?; + + Ok(()) +} + +#[api] +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[allow(non_camel_case_types)] +pub enum DeletableProperty { + /// Delete the comment property. + comment, + /// Delete the client-key property. + client_key, + /// Delete the autocreate property. + autocreate, +} + +#[api( + protected: true, + input: { + properties: { + realm: { + schema: REALM_ID_SCHEMA, + }, + comment: { + optional: true, + schema: SINGLE_LINE_COMMENT_SCHEMA, + }, + "issuer-url": { + description: "OpenID Issuer Url", + type: String, + optional: true, + }, + "client-id": { + description: "OpenID Client ID", + type: String, + optional: true, + }, + "client-key": { + description: "OpenID Client Key", + type: String, + optional: true, + }, + autocreate: { + description: "Automatically create users if they do not exist.", + optional: true, + type: bool, + }, + delete: { + description: "List of properties to delete.", + type: Array, + optional: true, + items: { + type: DeletableProperty, + } + }, + digest: { + optional: true, + schema: PROXMOX_CONFIG_DIGEST_SCHEMA, + }, + }, + }, + access: { + permission: &Permission::Privilege(&["access", "domains"], PRIV_REALM_ALLOCATE, false), + }, +)] +/// Update a realm +fn update_domain( + realm: String, + comment: Option, + issuer_url: Option, + client_id: Option, + client_key: Option, + autocreate: Option, + delete: Option>, + digest: Option, + _rpcenv: &mut dyn RpcEnvironment, +) -> Result<(), Error> { + let _lock = config::domains::lock_config()?; + + let (mut config, expected_digest) = config::domains::config()?; + + if let Some(ref digest) = digest { + let digest = proxmox::tools::hex_to_digest(digest)?; + crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; + } + + // only have to worry about openid for now + let mut data: OpenIdRealmConfig = config.lookup("openid", realm.as_str())?; + + if let Some(delete) = delete { + for delete_prop in delete { + match delete_prop { + DeletableProperty::comment => data.comment = None, + DeletableProperty::client_key => data.client_key = None, + DeletableProperty::autocreate => data.autocreate = None, + } + } + } + + if let Some(comment) = comment { + let comment = comment.trim().to_string(); + if comment.is_empty() { + data.comment = None; + } else { + data.comment = Some(comment); + } + } + + if let Some(issuer_url) = issuer_url { + data.issuer_url = issuer_url + }; + if let Some(client_id) = client_id { + data.client_id = client_id + }; + if let Some(client_key) = client_key { + data.client_key = if client_key.is_empty() { + None + } else { + Some(client_key) + }; + }; + if let Some(autocreate) = autocreate { + data.autocreate = Some(autocreate) + }; + + config.set_data(&realm, "openid", &data)?; + + config::domains::save_config(&config)?; + + Ok(()) +} + +#[api( + protected: true, + input: { + properties: { + realm: { + schema: REALM_ID_SCHEMA, + }, + digest: { + optional: true, + schema: PROXMOX_CONFIG_DIGEST_SCHEMA, + }, + }, + }, + access: { + permission: &Permission::Privilege(&["access", "domains"], PRIV_REALM_ALLOCATE, false), + }, +)] +/// Delete a realm +fn delete_domain(realm: String, digest: Option) -> Result<(), Error> { + if realm == "pam" || realm == "pbs" { + bail!("cannot remove realm '{}'", realm); + } + let _lock = config::domains::lock_config()?; + + let (mut config, expected_digest) = config::domains::config()?; + + if let Some(ref digest) = digest { + let digest = proxmox::tools::hex_to_digest(digest)?; + crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?; + } + + match config.sections.get(&realm) { + Some(_) => { + config.sections.remove(&realm); + } + None => bail!("realm '{}' does not exist.", realm), + } + + config::domains::save_config(&config)?; + + Ok(()) +} + pub const ROUTER: Router = Router::new() - .get(&API_METHOD_LIST_DOMAINS); + .get(&API_METHOD_LIST_DOMAINS) + .post(&API_METHOD_CREATE_DOMAIN) + .match_all("realm", &DOMAIN_ROUTER); + +const DOMAIN_ROUTER: Router = Router::new() + .get(&API_METHOD_GET_DOMAIN) + .put(&API_METHOD_UPDATE_DOMAIN) + .delete(&API_METHOD_DELETE_DOMAIN); -- 2.30.2