diff --git a/Cargo.toml b/Cargo.toml index fb2d6ac..7d44ae9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,6 @@ members = [ "src/daemon", "src/cli", "src/common", - "src/graph", "src/pam", "src/nss", "src/glue", @@ -38,7 +37,6 @@ tracing = "^0.1.37" himmelblau_unix_common = { path = "src/common" } kanidm_unix_common = { path = "src/glue" } msal = { version = "0.2.3" } -graph = { path = "src/graph" } clap = { version = "^4.5", features = ["derive", "env"] } clap_complete = "^4.4.1" reqwest = { version = "^0.12.2", features = ["json"] } diff --git a/src/common/Cargo.toml b/src/common/Cargo.toml index ffe3007..55fbe0a 100644 --- a/src/common/Cargo.toml +++ b/src/common/Cargo.toml @@ -20,7 +20,6 @@ serde_json = { workspace = true } tracing = { workspace = true } configparser = "^3.0.2" msal = { workspace = true } -graph = { workspace = true } reqwest = { workspace = true } pem = { workspace = true } kanidm-hsm-crypto = { workspace = true } diff --git a/src/common/src/config.rs b/src/common/src/config.rs index 1dab439..2cb5229 100644 --- a/src/common/src/config.rs +++ b/src/common/src/config.rs @@ -1,4 +1,3 @@ -use anyhow::{anyhow, Result}; use configparser::ini::Ini; use std::fmt; use std::io::Error; @@ -6,15 +5,13 @@ use std::path::PathBuf; use tracing::{debug, error}; use crate::constants::{ - DEFAULT_AUTHORITY_HOST, DEFAULT_CACHE_TIMEOUT, DEFAULT_CONFIG_PATH, DEFAULT_CONN_TIMEOUT, - DEFAULT_DB_PATH, DEFAULT_GRAPH, DEFAULT_HELLO_ENABLED, DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, + BROKER_APP_ID, DEFAULT_CACHE_TIMEOUT, DEFAULT_CONFIG_PATH, DEFAULT_CONN_TIMEOUT, + DEFAULT_DB_PATH, DEFAULT_HELLO_ENABLED, DEFAULT_HOME_ALIAS, DEFAULT_HOME_ATTR, DEFAULT_HOME_PREFIX, DEFAULT_HSM_PIN_PATH, DEFAULT_ID_ATTR_MAP, DEFAULT_ODC_PROVIDER, DEFAULT_SELINUX, DEFAULT_SHELL, DEFAULT_SOCK_PATH, DEFAULT_TASK_SOCK_PATH, DEFAULT_USE_ETC_SKEL, SERVER_CONFIG_PATH, }; use crate::unix_config::{HomeAttr, HsmType}; -use graph::constants::BROKER_APP_ID; -use graph::misc::request_federation_provider; use idmap::DEFAULT_IDMAP_RANGE; use std::env; @@ -62,65 +59,6 @@ fn match_bool(val: Option, default: bool) -> bool { } } -struct FederationProvider { - odc_provider: String, - domain: String, - tenant_id: Option, - authority_host: Option, - graph: Option, -} - -impl FederationProvider { - fn new(odc_provider: &str, domain: &str) -> Self { - FederationProvider { - odc_provider: odc_provider.to_string(), - domain: domain.to_string(), - tenant_id: None, - authority_host: None, - graph: None, - } - } - - async fn set(&mut self) -> Result<()> { - let (authority_host, tenant_id, graph) = - request_federation_provider(&self.odc_provider, &self.domain).await?; - self.tenant_id = Some(tenant_id); - self.authority_host = Some(authority_host); - self.graph = Some(graph); - Ok(()) - } - - async fn get_tenant_id(&mut self) -> Result { - if self.tenant_id.is_none() { - self.set().await?; - } - match &self.tenant_id { - Some(tenant_id) => Ok(tenant_id.to_string()), - None => Err(anyhow!("Failed fetching tenant_id")), - } - } - - async fn get_authority_host(&mut self) -> Result { - if self.authority_host.is_none() { - self.set().await?; - } - match &self.authority_host { - Some(authority_host) => Ok(authority_host.to_string()), - None => Err(anyhow!("Failed fetching authority_host")), - } - } - - async fn get_graph(&mut self) -> Result { - if self.graph.is_none() { - self.set().await?; - } - match &self.graph { - Some(graph) => Ok(graph.to_string()), - None => Err(anyhow!("Failed fetching graph")), - } - } -} - impl HimmelblauConfig { pub fn new(config_path: Option<&str>) -> Result { let mut sconfig = Ini::new(); @@ -225,7 +163,7 @@ impl HimmelblauConfig { } } - fn get_odc_provider(&self, domain: &str) -> String { + pub fn get_odc_provider(&self, domain: &str) -> String { match self.config.get(domain, "odc_provider") { Some(val) => val, None => match self.config.get("global", "odc_provider") { @@ -235,42 +173,6 @@ impl HimmelblauConfig { } } - pub async fn get_tenant_id_authority_and_graph( - &self, - domain: &str, - ) -> Result<(String, String, String)> { - let mut federation_provider = - FederationProvider::new(&self.get_odc_provider(domain), domain); - let tenant_id = match self.config.get(domain, "tenant_id") { - Some(val) => val, - None => match federation_provider.get_tenant_id().await { - Ok(val) => val, - Err(e) => return Err(anyhow!("Failed fetching tenant_id: {}", e)), - }, - }; - let authority_host = match self.config.get(domain, "authority_host") { - Some(val) => val, - None => match self.config.get("global", "authority_host") { - Some(val) => val, - None => match federation_provider.get_authority_host().await { - Ok(val) => val, - Err(_) => String::from(DEFAULT_AUTHORITY_HOST), - }, - }, - }; - let graph = match self.config.get(domain, "graph") { - Some(val) => val, - None => match self.config.get("global", "graph") { - Some(val) => val, - None => match federation_provider.get_graph().await { - Ok(val) => val, - Err(_) => String::from(DEFAULT_GRAPH), - }, - }, - }; - Ok((authority_host, tenant_id, graph)) - } - pub fn get_app_id(&self, domain: &str) -> String { match self.config.get(domain, "app_id") { Some(val) => val, diff --git a/src/common/src/constants.rs b/src/common/src/constants.rs index 856efbe..6380283 100644 --- a/src/common/src/constants.rs +++ b/src/common/src/constants.rs @@ -22,3 +22,5 @@ pub const DEFAULT_SELINUX: bool = true; pub const DEFAULT_HSM_PIN_PATH: &str = "/var/lib/himmelblaud/hsm-pin"; pub const DEFAULT_HELLO_ENABLED: bool = true; pub const DEFAULT_ID_ATTR_MAP: IdAttr = IdAttr::Name; +pub const BROKER_APP_ID: &str = "29d9ed98-a469-4536-ade2-f981bc1d605e"; +pub const BROKER_CLIENT_IDENT: &str = "38aa3b87-a06d-4817-b275-7a316988d93b"; diff --git a/src/common/src/idprovider/himmelblau.rs b/src/common/src/idprovider/himmelblau.rs index 30b3876..8182f67 100644 --- a/src/common/src/idprovider/himmelblau.rs +++ b/src/common/src/idprovider/himmelblau.rs @@ -10,7 +10,6 @@ use crate::idprovider::interface::tpm; use crate::unix_proto::{DeviceAuthorizationResponse, PamAuthRequest}; use anyhow::{anyhow, Result}; use async_trait::async_trait; -use graph::user::{request_user_groups, DirectoryObject}; use idmap::SssIdmap; use kanidm_hsm_crypto::{LoadableIdentityKey, LoadableMsOapxbcRsaKey, PinValue, SealedData, Tpm}; use msal::auth::{ @@ -20,6 +19,7 @@ use msal::auth::{ }; use msal::discovery::EnrollAttrs; use msal::error::{MsalError, AUTH_PENDING, DEVICE_AUTH_FAIL, REQUIRES_MFA}; +use msal::graph::{DirectoryObject, Graph}; use reqwest; use std::collections::HashMap; use std::sync::Arc; @@ -92,11 +92,11 @@ impl HimmelblauMultiProvider { debug!("Adding provider for domain {}", domain); let range = cfg.get_idmap_range(&domain); let mut idmap_lk = idmap.write().await; - let (authority_host, tenant_id, graph) = - match cfg.get_tenant_id_authority_and_graph(&domain).await { - Ok(res) => res, - Err(e) => return Err(anyhow!("{}", e)), - }; + let graph = Graph::new(&cfg.get_odc_provider(&domain), &domain) + .await + .map_err(|e| anyhow!("{:?}", e))?; + let authority_host = graph.authority_host(); + let tenant_id = graph.tenant_id(); idmap_lk .add_gen_domain(&domain, &tenant_id, range) .map_err(|e| anyhow!("{:?}", e))?; @@ -109,7 +109,7 @@ impl HimmelblauMultiProvider { &tenant_id, &domain, &authority_host, - &graph, + graph, &idmap, ) .map_err(|_| anyhow!("Failed to initialize the provider"))?; @@ -329,7 +329,7 @@ pub struct HimmelblauProvider { tenant_id: String, domain: String, authority_host: String, - graph_url: String, + graph: Graph, refresh_cache: RefreshCache, idmap: Arc>, } @@ -341,7 +341,7 @@ impl HimmelblauProvider { tenant_id: &str, domain: &str, authority_host: &str, - graph_url: &str, + graph: Graph, idmap: &Arc>, ) -> Result { Ok(HimmelblauProvider { @@ -350,7 +350,7 @@ impl HimmelblauProvider { tenant_id: tenant_id.to_string(), domain: domain.to_string(), authority_host: authority_host.to_string(), - graph_url: graph_url.to_string(), + graph, refresh_cache: RefreshCache::new(), idmap: idmap.clone(), }) @@ -1300,7 +1300,7 @@ impl HimmelblauProvider { }; match &value.access_token { Some(access_token) => { - groups = match request_user_groups(&self.graph_url, access_token).await { + groups = match self.graph.request_user_groups(access_token).await { Ok(groups) => { let mut gt_groups = vec![]; for g in groups { @@ -1366,23 +1366,19 @@ impl HimmelblauProvider { value: DirectoryObject, ) -> Result { let config = self.config.read().await; - let name = match value.get("display_name") { + let name = match value.display_name { Some(name) => name, None => return Err(anyhow!("Failed retrieving group display_name")), }; - let id = match value.get("id") { - Some(id) => { - Uuid::parse_str(id).map_err(|e| anyhow!("Failed parsing user uuid: {}", e))? - } - None => return Err(anyhow!("Failed retrieving group uuid")), - }; + let id = + Uuid::parse_str(&value.id).map_err(|e| anyhow!("Failed parsing user uuid: {}", e))?; let idmap = self.idmap.read().await; let gidnumber = match config.get_id_attr_map() { IdAttr::Uuid => idmap .object_id_to_unix_id(&self.tenant_id, &id) .map_err(|e| anyhow!("Failed fetching gid for {}: {:?}", id, e))?, IdAttr::Name => idmap - .gen_to_unix(&self.tenant_id, name) + .gen_to_unix(&self.tenant_id, &name) .map_err(|e| anyhow!("Failed fetching gid for {}: {:?}", name, e))?, }; @@ -1436,11 +1432,6 @@ impl HimmelblauProvider { "Setting domain {} config device_id to {}", self.domain, &device_id ); - config.set(&self.domain, "graph", &self.graph_url); - debug!( - "Setting domain {} config graph to {}", - self.domain, &self.graph_url - ); config.set(&self.domain, "tenant_id", &self.tenant_id); debug!( "Setting domain {} config tenant_id to {}", diff --git a/src/graph/Cargo.toml b/src/graph/Cargo.toml deleted file mode 100644 index b969853..0000000 --- a/src/graph/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "graph" - -version.workspace = true -authors.workspace = true -rust-version.workspace = true -edition.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true - -[lib] -name = "graph" -path = "src/lib.rs" - -[dependencies] -tracing = { workspace = true } -reqwest = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -anyhow = { workspace = true } -uuid = { version = "^1.3.4", features = [ "v4" ] } -pem = { workspace = true } -base64 = { workspace = true } -chrono = { workspace = true } -kanidm-hsm-crypto = { workspace = true } -openssl = { workspace = true } -hostname = "^0.4.0" -os-release = { workspace = true } -compact_jwt = { workspace = true } -jsonwebtoken = { workspace = true } -urlencoding = { workspace = true } -msal = { workspace = true } diff --git a/src/graph/src/application.rs b/src/graph/src/application.rs deleted file mode 100644 index 2673e0b..0000000 --- a/src/graph/src/application.rs +++ /dev/null @@ -1,137 +0,0 @@ -use anyhow::{anyhow, Result}; -use base64::{engine::general_purpose, Engine as _}; -use chrono::Utc; -use reqwest::{header, Url}; -use serde::{Deserialize, Serialize}; -use serde_json::{json, to_string_pretty}; -use tracing::debug; -use uuid::Uuid; - -#[derive(Debug, Deserialize, Clone)] -pub struct Application { - #[serde(rename = "appId")] - pub app_id: Option, - #[serde(rename = "displayName")] - pub display_name: Option, - pub id: Option, - #[serde(rename = "keyCredentials")] - pub key_creds: Option>, -} - -#[derive(Debug, Deserialize)] -struct ApplicationList { - value: Vec, -} - -pub async fn list_applications(graph_url: &str, access_token: &str) -> Result> { - let url = &format!("{}/v1.0/applications", graph_url); - let client = reqwest::Client::new(); - let resp = client - .get(url) - .header(header::AUTHORIZATION, format!("Bearer {}", access_token)) - .send() - .await?; - if resp.status().is_success() { - let json_resp: ApplicationList = resp.json().await?; - Ok(json_resp.value) - } else { - Err(anyhow!(resp.status())) - } -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct KeyCredential { - #[serde(rename = "customKeyIdentifier")] - custom_key_identifier: Option, - #[serde(rename = "displayName")] - display_name: Option, - #[serde(rename = "endDateTime")] - end_date_time: Option, - key: Option, - #[serde(rename = "keyId")] - key_id: String, - #[serde(rename = "startDateTime")] - start_date_time: String, - r#type: String, - usage: String, -} - -pub async fn get_application( - graph_url: &str, - access_token: &str, - app_id: &str, -) -> Result { - let url = Url::parse_with_params( - &format!("{}/v1.0/applications", graph_url,), - &[ - ("$select", "keyCredentials,id"), - ("$filter", format!("appId eq '{}'", app_id).as_str()), - ], - )?; - let client = reqwest::Client::new(); - let resp = client - .get(url) - .header(header::AUTHORIZATION, format!("Bearer {}", access_token)) - .send() - .await?; - if resp.status().is_success() { - let json_resp: ApplicationList = resp.json().await?; - match json_resp.value.first() { - Some(app) => Ok(app.clone()), - None => Err(anyhow!("Application {} not found", app_id)), - } - } else { - Err(anyhow!(resp.status())) - } -} - -pub async fn add_application_certificate( - graph_url: &str, - access_token: &str, - app_id: &str, - cert: &str, - desc: &str, -) -> Result<()> { - let app = get_application(graph_url, access_token, app_id).await?; - let app_id = match &app.id { - Some(id) => id.clone(), - None => return Err(anyhow!("Application {} missing id", app_id)), - }; - let url = &format!("{}/v1.0/applications/{}", graph_url, app_id); - let mut key_creds = match app.key_creds { - Some(key_creds) => key_creds, - None => Vec::::new(), - }; - key_creds.push(KeyCredential { - custom_key_identifier: None, /* Leaving this blank will default to the thumbprint */ - display_name: Some(desc.to_string()), - end_date_time: None, /* Leaving this blank defaults to 1 year from now */ - key: Some(general_purpose::STANDARD.encode(cert)), - key_id: Uuid::new_v4().to_string(), - start_date_time: Utc::now().to_rfc3339(), - r#type: "AsymmetricX509Cert".to_string(), - usage: "Verify".to_string(), - }); - let payload = json!({ - "keyCredentials": key_creds, - }); - match to_string_pretty(&payload) { - Ok(pretty) => { - debug!("POST {}: {}", url, pretty); - } - Err(_e) => {} - }; - let client = reqwest::Client::new(); - let resp = client - .patch(url) - .header(header::AUTHORIZATION, format!("Bearer {}", access_token)) - .header(header::CONTENT_TYPE, "application/json") - .json(&payload) - .send() - .await?; - if resp.status().is_success() { - Ok(()) - } else { - Err(anyhow!(resp.status())) - } -} diff --git a/src/graph/src/constants.rs b/src/graph/src/constants.rs deleted file mode 100644 index e19bbd3..0000000 --- a/src/graph/src/constants.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub const BROKER_APP_ID: &str = "29d9ed98-a469-4536-ade2-f981bc1d605e"; -pub const BROKER_CLIENT_IDENT: &str = "38aa3b87-a06d-4817-b275-7a316988d93b"; diff --git a/src/graph/src/group.rs b/src/graph/src/group.rs deleted file mode 100644 index a692f0d..0000000 --- a/src/graph/src/group.rs +++ /dev/null @@ -1,33 +0,0 @@ -use anyhow::{anyhow, Result}; -use reqwest::{header, Url}; -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -pub struct GroupObject { - #[serde(rename = "displayName")] - pub displayname: String, - pub id: String, -} - -pub async fn request_group( - graph_url: &str, - access_token: &str, - displayname: &str, -) -> Result { - let url = Url::parse_with_params( - &format!("{}/v1.0/groups", graph_url), - &[("$filter", format!("displayName eq '{}'", displayname))], - )?; - let client = reqwest::Client::new(); - let resp = client - .get(url) - .header(header::AUTHORIZATION, format!("Bearer {}", access_token)) - .send() - .await?; - if resp.status().is_success() { - let json_resp: GroupObject = resp.json().await?; - Ok(json_resp) - } else { - Err(anyhow!(resp.status())) - } -} diff --git a/src/graph/src/lib.rs b/src/graph/src/lib.rs deleted file mode 100644 index aad521f..0000000 --- a/src/graph/src/lib.rs +++ /dev/null @@ -1,29 +0,0 @@ -#![deny(warnings)] -#![warn(unused_extern_crates)] -#![deny(clippy::todo)] -#![deny(clippy::unimplemented)] -#![deny(clippy::unwrap_used)] -#![deny(clippy::expect_used)] -#![deny(clippy::panic)] -#![deny(clippy::unreachable)] -#![deny(clippy::await_holding_lock)] -#![deny(clippy::needless_pass_by_value)] -#![deny(clippy::trivially_copy_pass_by_ref)] - -#[cfg(target_family = "unix")] -pub mod misc; - -#[cfg(target_family = "unix")] -pub mod nonce; - -#[cfg(target_family = "unix")] -pub mod constants; - -#[cfg(target_family = "unix")] -pub mod user; - -#[cfg(target_family = "unix")] -pub mod application; - -#[cfg(target_family = "unix")] -pub mod group; diff --git a/src/graph/src/misc.rs b/src/graph/src/misc.rs deleted file mode 100644 index cc0c484..0000000 --- a/src/graph/src/misc.rs +++ /dev/null @@ -1,37 +0,0 @@ -use anyhow::{anyhow, Result}; -use reqwest::Url; -use serde::Deserialize; -use tracing::debug; - -#[derive(Debug, Deserialize)] -struct FederationProvider { - #[serde(rename = "tenantId")] - tenant_id: String, - authority_host: String, - graph: String, -} - -pub async fn request_federation_provider( - odc_provider: &str, - domain: &str, -) -> Result<(String, String, String)> { - let url = Url::parse_with_params( - &format!("https://{}/odc/v2.1/federationProvider", odc_provider), - &[("domain", domain)], - )?; - - let resp = reqwest::get(url).await?; - if resp.status().is_success() { - let json_resp: FederationProvider = resp.json().await?; - debug!("Discovered tenant_id: {}", json_resp.tenant_id); - debug!("Discovered authority_host: {}", json_resp.authority_host); - debug!("Discovered graph: {}", json_resp.graph); - Ok(( - json_resp.authority_host, - json_resp.tenant_id, - json_resp.graph, - )) - } else { - Err(anyhow!(resp.status())) - } -} diff --git a/src/graph/src/nonce.rs b/src/graph/src/nonce.rs deleted file mode 100644 index feffafe..0000000 --- a/src/graph/src/nonce.rs +++ /dev/null @@ -1,47 +0,0 @@ -use anyhow::{anyhow, Result}; -use msal::discovery::{NonceService, DISCOVERY_URL}; -use reqwest::{header, Url}; -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -struct NonceResp { - #[serde(rename = "Value")] - value: String, -} - -pub async fn request_nonce( - nonce_service: Option, - tenant_id: &str, - access_token: &str, -) -> Result { - let url = match nonce_service { - Some(nonce_service) => { - let endpoint = match nonce_service.endpoint { - Some(endpoint) => endpoint, - None => format!("{}/EnrollmentServer/nonce/{}/", DISCOVERY_URL, tenant_id), - }; - let service_version = match nonce_service.service_version { - Some(service_version) => service_version, - None => "1.0".to_string(), - }; - Url::parse_with_params(&endpoint, &[("api-version", &service_version)])? - } - None => Url::parse_with_params( - &format!("{}/EnrollmentServer/nonce/{}/", DISCOVERY_URL, tenant_id), - &[("api-version", "1.0")], - )?, - }; - - let client = reqwest::Client::new(); - let resp = client - .get(url) - .header(header::AUTHORIZATION, format!("Bearer {}", access_token)) - .send() - .await?; - if resp.status().is_success() { - let json_resp: NonceResp = resp.json().await?; - Ok(json_resp.value) - } else { - Err(anyhow!(resp.status())) - } -} diff --git a/src/graph/src/user.rs b/src/graph/src/user.rs deleted file mode 100644 index 97fc992..0000000 --- a/src/graph/src/user.rs +++ /dev/null @@ -1,129 +0,0 @@ -use anyhow::{anyhow, Result}; -use reqwest::header; -use serde::Deserialize; -use serde_json::{json, to_string_pretty}; -use tracing::{debug, error}; - -#[derive(Debug, Deserialize)] -pub struct DirectoryObject { - #[serde(rename = "@odata.type")] - odata_type: String, - id: String, - description: Option, - #[serde(rename = "displayName")] - display_name: Option, - #[serde(rename = "securityIdentifier")] - security_identifier: Option, -} - -impl DirectoryObject { - pub fn get(&self, key: &str) -> Option<&String> { - match key { - "id" => Some(&self.id), - "description" => self.description.as_ref(), - /* Azure only provides an ID if we lack the GroupMember.Read.All - * permission, in which case just use the ID as the displayName. */ - "display_name" => match &self.display_name { - Some(val) => Some(val), - None => Some(&self.id), - }, - "security_identifier" => self.security_identifier.as_ref(), - _ => None, - } - } -} - -#[derive(Debug, Deserialize)] -struct DirectoryObjects { - value: Vec, -} - -pub async fn request_user_groups( - graph_url: &str, - access_token: &str, -) -> Result> { - let url = &format!("{}/v1.0/me/memberOf", graph_url); - let client = reqwest::Client::new(); - let resp = client - .get(url) - .header(header::AUTHORIZATION, format!("Bearer {}", access_token)) - .send() - .await?; - let mut res: Vec = Vec::new(); - if resp.status().is_success() { - let json_resp: DirectoryObjects = resp.json().await?; - for entry in json_resp.value { - if entry.odata_type == "#microsoft.graph.group" { - res.push(entry) - } - } - Ok(res) - } else { - let status = resp.status(); - error!( - "Error encountered while fetching user groups: {}", - resp.text().await.map_err(|_| { anyhow!(status) })? - ); - Err(anyhow!(status)) - } -} - -#[derive(Debug, Deserialize)] -pub struct UserObject { - #[serde(rename = "displayName")] - pub displayname: String, - #[serde(rename = "userPrincipalName")] - pub upn: String, - pub id: String, -} - -pub async fn request_user(graph_url: &str, access_token: &str, upn: &str) -> Result { - let url = &format!("{}/v1.0/users/{}", graph_url, upn); - let client = reqwest::Client::new(); - let resp = client - .get(url) - .header(header::AUTHORIZATION, format!("Bearer {}", access_token)) - .send() - .await?; - if resp.status().is_success() { - let json_resp: UserObject = resp.json().await?; - Ok(json_resp) - } else { - Err(anyhow!(resp.status())) - } -} - -pub async fn assign_device_to_user( - graph_url: &str, - access_token: &str, - device_id: &str, - upn: &str, -) -> Result<()> { - let url = &format!( - "{}/v1.0/devices/{}/registeredOwners/$ref", - graph_url, device_id - ); - let user_obj = request_user(graph_url, access_token, upn).await?; - let payload = json!({ - "@odata.id": format!("{}/v1.0/directoryObjects/{}", graph_url, user_obj.id), - }); - match to_string_pretty(&payload) { - Ok(pretty) => { - debug!("POST {}: {}", url, pretty); - } - Err(_e) => {} - }; - let client = reqwest::Client::new(); - let resp = client - .post(url) - .header(header::AUTHORIZATION, format!("Bearer {}", access_token)) - .header(header::CONTENT_TYPE, "application/json") - .json(&payload) - .send() - .await?; - if resp.status().is_success() { - Ok(()) - } else { - Err(anyhow!(resp.status())) - } -}