#microsoft #sdk #oauth #one-drive #blocking-client #api-client #api-request

graph-rs-sdk

Microsoft Graph 和 Microsoft Identity Platform 的 Rust SDK 客户端

20 个版本 (9 个稳定版)

2.0.1 2024 年 8 月 11 日
2.0.0 2024 年 5 月 23 日
2.0.0-beta.02024 年 1 月 6 日
1.1.4 2024 年 2 月 1 日
0.0.1 2020 年 12 月 13 日

#1 in #microsoft

Download history 180/week @ 2024-04-27 245/week @ 2024-05-04 173/week @ 2024-05-11 381/week @ 2024-05-18 273/week @ 2024-05-25 314/week @ 2024-06-01 337/week @ 2024-06-08 344/week @ 2024-06-15 249/week @ 2024-06-22 127/week @ 2024-06-29 215/week @ 2024-07-06 203/week @ 2024-07-13 360/week @ 2024-07-20 296/week @ 2024-07-27 361/week @ 2024-08-03 256/week @ 2024-08-10

1,314 每月下载量

MIT 许可证

2MB
43K SLoC

graph-rs-sdk

Build Static Badge

Microsoft Graph 和 Microsoft Identity Platform 的 Rust SDK 客户端

crates.io 上可用 - v2.0.1 - 最新稳定版本

功能概述

Microsoft Graph V1 和 Beta API 客户端

  • 广泛支持 Graph API
  • 使用 Streaming、Channels 或 Iterators 进行分页
  • 上传会话、OData 查询和文件下载

Microsoft Identity Platform(获取访问令牌)

  • 授权码、客户端凭证、设备码、OpenId
  • 内存令牌缓存
  • 自动刷新令牌
  • 交互式 WebView 身份验证(功能 = interactive-auth
  • X509 证书(功能 = openssl)和证明密钥代码交换(PKCE)支持

还有更多。有关功能列表的更全面信息,请参阅 功能。有关可以在 crate 中启用的功能的详细信息,请参阅 Cargo 功能标志

graph-rs-sdk = "2.0.1"
tokio = { version = "1.25.0", features = ["full"] }

用于将实现 serde Serialize 的类型用作请求体或将 serde 的 json 宏传递

serde = { version = "1", features = ["derive"] }
serde_json = "1"

要使用 stream 功能,请添加 futures crate

futures = "0.3"

并导入 futures::StreamExt

use futures::StreamExt;
use graph_rs_sdk::*;

功能

图客户端

OAuth 和 Openid

身份平台身份验证示例

可用的API有哪些

可用的API是从存储在Microsoft的msgraph-metadata存储库中的OpenApi配置生成的,用于Graph API。可能有一些请求和/或API尚未包含在此项目中,但通常大多数已实现。

Cargo 功能标志

  • interactive-auth:在支持WebView的平台(如桌面)上使用WebView进行交互式身份验证。使用wrytao crate进行WebView支持。支持Linux和Windows平台。目前,不支持MacOS - 此工作的正在进行中。
  • openssl:启用在客户端凭据和授权码身份验证流程中使用证书的功能。此外,还启用相关类型,如X509Certificate,用于构建/运行基于证书的身份验证流程。
  • test-util:启用仅测试功能。目前,这仅启用在http客户端中关闭https的功能,以便使用crate的mocking框架。未来可能会添加其他与测试相关的功能。
  • native-tls:在reqwest http-client中启用功能native-tls。有关更多详细信息,请参阅reqwest crate
  • rustls-tls:在reqwest http-client中启用功能rustls-tls。有关更多详细信息,请参阅reqwest crate
  • brotli:在reqwest http-client中启用功能brotli。有关更多详细信息,请参阅reqwest crate
  • deflate:在reqwest http-client中启用功能deflate。有关更多详细信息,请参阅reqwest crate
  • trust-dns:在reqwest http-client中启用功能trust-dns。有关更多详细信息,请参阅reqwest crate
  • socks:在reqwest http-client中启用功能socks(socks代理支持)。有关更多详细信息,请参阅reqwest crate

用法

有关大量示例,请参阅GitHub上的examples目录

异步和阻塞客户端

该crate可以同时进行异步和阻塞请求。

异步客户端(默认)

graph-rs-sdk = "2.0.1"
tokio = { version = "1.25.0", features = ["full"] }

示例

use graph_rs_sdk::*;

#[tokio::main]
async fn main() -> GraphResult<()> {
  let client = GraphClient::new("ACCESS_TOKEN");

  let response = client
      .users()
      .list_user()
      .send()
      .await?;

  println!("{:#?}", response);

  let body: serde_json::Value = response.json().await?;
  println!("{:#?}", body);
  
  Ok(())
}

阻塞客户端

要使用阻塞客户端,请使用into_blocking()方法。使用阻塞客户端时,不应使用tokio

graph-rs-sdk = "2.0.1"

示例

use graph_rs_sdk::*;

fn main() -> GraphResult<()> {
    let client = GraphClient::new("ACCESS_TOKEN");

    let response = client
        .users()
        .list_user()
        .into_blocking()    
        .send()?;

    println!("{:#?}", response);

    let body: serde_json::Value = response.json()?;
    println!("{:#?}", body);

    Ok(())
}

发送方法

send()方法是将请求发送到主方法,并返回一个Result<rewest::Response, GraphFailure>。有关Response类型的详细信息,请参阅reqwest crate。

use graph_rs_sdk::*;

pub async fn get_drive_item() -> GraphResult<()> {
  let client = GraphClient::new("ACCESS_TOKEN");

  let response = client
      .me()
      .drive()
      .get_drive()
      .send()
      .await?;

  println!("{:#?}", response);

  let body: serde_json::Value = response.json().await?;
  println!("{:#?}", body);
  
  Ok(())
}

响应错误/错误类型

虽然该库有自己的错误类型和结果,但由于可能出现的错误数量很多,你可能会想使用诸如anyhow之类的库。

如果Microsoft Graph API返回了错误,这几乎总是出现在响应体中。异步请求的ResponseExt和阻塞请求的BlockingResponseExt提供了方便的方法,可以用来从响应体中获取错误信息。

use graph_rs_sdk::{http::ResponseExt, Graph};
use std::error::Error;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let client = GraphClient::new("token");
    let response = client.users().list_user().send().await?;

    if !response.status().is_success() {
        if let Ok(error) = response.into_graph_error_message().await {
            println!("{error:#?}");
        }
    }

    Ok(())
}
自定义类型

您可以通过实现serde::Serialize将您自己的类型传递给需要请求体的API请求。

您可以通过利用reqwest::Response的方法来实现自己的类型。这些类型必须实现serde::Deserialize。有关更多信息,请参阅reqwest库。

#[macro_use]
extern crate serde;

use graph_rs_sdk::*;

#[derive(Debug, Serialize, Deserialize)]
pub struct DriveItem {
    #[serde(skip_serializing_if = "Option::is_none")]
    id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    name: Option<String>,
    // ... Any other fields
}

static ACCESS_TOKEN: &str = "ACCESS_TOKEN";

static ITEM_ID: &str = "ITEM_ID";

pub async fn get_drive_item() -> GraphResult<()> {
  let client = GraphClient::new(ACCESS_TOKEN);
  
  let drive_item = DriveItem {
        id: None,
        name: Some("new name".into())
  };

  let response = client
      .me()
      .drive()
      .item(ITEM_ID)
      .update_items(&drive_item)
      .send()
      .await?;

  println!("{:#?}", response);

  let drive_item: DriveItem = response.json().await?;
  println!("{:#?}", drive_item);

  let response = client
      .me()
      .drive()
      .item(ITEM_ID)
      .get_items()
      .send()
      .await?;

  println!("{:#?}", response);
  
  let drive_item: DriveItem = response.json().await?;
  println!("{:#?}", drive_item);
  
  Ok(())
}

分页

分页处理了响应体是数据截断版本且提供了一个URL以继续调用并获取剩余数据的场景。分页可以包含调用链中的多个链接。

SDK提供了方便的方法来获取分页场景中的所有数据,例如使用下一个链接或使用delta链接跟踪Graph数据的更改

如果您只需要快速轻松地获取所有下一个链接响应或JSON体,可以使用paging().json()方法,该方法将耗尽所有下一个链接调用并返回所有响应的VecDeque<Response<Result<T>>>。请注意,需要调用的下一个链接调用量越大,调用此方法的返回延迟就越长。

所有分页方法都会读取响应体以获取调用下一个链接请求的@odata.nextLink URL。因此,原始的reqwest::Response丢失了。然而,分页响应被重新包装在类似于reqwest::Response的Response对象(http::Response)中。

有关在SDK中使用分页调用的更多信息,请参阅分页详细指南

对于分页Microsoft Graph API,支持级别不同。有关受支持的API和可用性的更多信息,请参阅文档在您的应用程序中分页Microsoft Graph数据

#[macro_use]
extern crate serde;

use graph_rs_sdk::*;

static ACCESS_TOKEN: &str = "ACCESS_TOKEN";

#[derive(Debug, Serialize, Deserialize)]
pub struct User {
    pub(crate) id: Option<String>,
    #[serde(rename = "userPrincipalName")]
    user_principal_name: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Users {
  pub value: Vec<User>,
}

async fn paging() -> GraphResult<()> {
  let client = GraphClient::new(ACCESS_TOKEN);

  let deque = client
      .users()
      .list_user()
      .select(&["id", "userPrincipalName"])
      .paging()
      .json::<Users>()
      .await?;
  
  println!("{:#?}", deque);
  
  Ok(())
}

分页示例显示了列出用户并调用所有下一个链接的简单方法。您还可以流式传输下一个链接响应或使用通道接收器来获取响应。

流式传输

仅通过异步客户端可用。

use futures::StreamExt;
use graph_rs_sdk::*;

static ACCESS_TOKEN: &str = "ACCESS_TOKEN";

pub async fn stream_next_links() -> GraphResult<()> {
    let client = GraphClient::new(ACCESS_TOKEN);

    let mut stream = client
        .users()
        .list_user()
        .select(&["id", "userPrincipalName"])
        .paging()
        .stream::<serde_json::Value>()?;

    while let Some(result) = stream.next().await {
        let response = result?;
        println!("{:#?}", response);

        let body = response.into_body();
        println!("{:#?}", body);
    }

    Ok(())
}

pub async fn stream_delta() -> GraphResult<()> {
  let client = GraphClient::new(ACCESS_TOKEN);
  let mut stream = client
          .users()
          .delta()
          .paging()
          .stream::<serde_json::Value>()?;

  while let Some(result) = stream.next().await {
      let response = result?;
      println!("{:#?}", response);
  
      let body = response.into_body();
      println!("{:#?}", body);
  }

  Ok(())
}

通道

use graph_rs_sdk::*;

static ACCESS_TOKEN: &str = "ACCESS_TOKEN";

async fn channel_next_links() -> GraphResult<()> {
  let client = GraphClient::new(ACCESS_TOKEN);
  let mut receiver = client
      .users()
      .list_user()
      .paging()
      .channel::<serde_json::Value>()
      .await?;

  while let Some(result) = receiver.recv().await {
    match result {
      Ok(response) => {
        println!("{:#?}", response);

        let body = response.into_body();
        println!("{:#?}", body);
      }
      Err(err) => panic!("{:#?}", err),
    }
  }
  
  Ok(())
}

使用实现std::io::Read或tokio::io::AsyncReadExt的类型

文件类型

如果您熟悉使用reqwest库,可以使用reqwest::Bodyreqwest::blocking::Body来处理文件。

  1. 您可以直接传递std::fs::Filetokio::fs::File
use graph_rs_sdk::GraphClient;

fn use_file_directly(file: File) -> anyhow::Result<()> {
  let client = GraphClient::new("token");
  let _ = client
          .drive("drive-id")
          .item_by_path(":/drive/path:")
          .update_items_content(file)
          .into_blocking()
          .send()?;

  Ok(())
}

async fn use_async_file_directly(file: tokio::fs::File) -> anyhow::Result<()> {
  let client = GraphClient::new("token");
  let _ = client
          .drive("drive-id")
          .item_by_path(":/drive/path:")
          .update_items_content(file)
          .send()
          .await?;

  Ok(())
}
  1. reqwest::Body 类型被重导出为 graph_rs_sdk::http::Body,而 reqwest::blocking::Body 类型被重导出为 graph_rs_sdk::http::blocking::Body
use graph_rs_sdk::GraphClient;
use graph_rs_sdk::http::{Body, blocking};

fn use_reqwest_for_files(file: std::fs::File) -> anyhow::Result<()> {
    let client = GraphClient::new("token");
    let _ = client.drive("drive-id")
        .item_by_path(":/drive/path:")
        .update_items_content(blocking::Body::from(file))
        .into_blocking()
        .send()?;
  
  Ok(())
}

async fn use_reqwest_for_async_files(file: tokio::fs::File) -> anyhow::Result<()> {
    let client = GraphClient::new("token");
    let _ = client.drive("drive-id")
        .item_by_path(":/drive/path:")
        .update_items_content(Body::from(file))
        .send()
        .await?;
  
  Ok(())
}
  1. 如果您想直接使用 reqwest 包
fn use_reqwest_for_files(file: File) -> anyhow::Result<()> {
  let client = GraphClient::new("token");
  let _ = client
          .drive("drive-id")
          .item_by_path(":/drive/path:")
          .update_items_content(reqwest::blocking::Body::from(file))
          .into_blocking()
          .send()?;

  Ok(())
}

async fn use_reqwest_for_tokio_files(file: tokio::fs::File) -> anyhow::Result<()> {
  let client = GraphClient::new("token");
  let _ = client
          .drive("drive-id")
          .item_by_path(":/drive/path:")
          .update_items_content(reqwest::Body::from(file))
          .send()
          .await?;

  Ok(())
}

其他实现了 std::io::Read 或 tokio::io::AsyncReadExt 的类型

您可以使用辅助类型 BodyRead

use graph_rs_sdk::http::BodyRead;
use graph_rs_sdk::GraphClient;

fn use_read(reader: impl std::io::Read) -> anyhow::Result<()> {
  let client = GraphClient::new("token");

  let body_read = BodyRead::from_read(reader)?;

  let _ = client
      .drive("drive-id")
      .item_by_path(":/drive/path:")
      .update_items_content(body_read)
      .into_blocking()
      .send()?;

  Ok(())
}

async fn use_async_read(
    async_reader: impl tokio::io::AsyncReadExt + Unpin,
) -> anyhow::Result<()> {
  let client = GraphClient::new("token");
  
  let async_body_read = BodyRead::from_async_read(async_reader).await?;
  
  let _ = client
      .drive("drive-id")
      .item_by_path(":/drive/path:")
      .update_items_content(async_body_read)
      .send()
      .await?;

  Ok(())
}

API 使用

以下展示了如何使用客户端和一些 API 的几个示例。

OneDrive

使用 drive id 或通过为我、网站、用户和组指定的特定驱动来向驱动发送请求。

use graph_rs_sdk::*;

async fn drives() -> GraphResult<()> {
  let client = GraphClient::new("ACCESS_TOKEN");

  let response = client
      .drives()
      .list_drive()
      .send()
      .await
      .unwrap();

  println!("{:#?}", response);

  let body: serde_json::Value = response.json().await?;
  println!("{:#?}", body);

  let response = client
      .drive("DRIVE-ID")
      .item("ITEM_ID")
      .get_items()
      .send()
      .await?;

  println!("{:#?}", response);

  let body: serde_json::Value = response.json().await?;
  println!("{:#?}", body);
  
  Ok(())
}

Me API

async fn drive_me() -> GraphResult<()> {
  let client = GraphClient::new("ACCESS_TOKEN");

  let response = client
      .me()
      .drive()
      .item("ITEM_ID")
      .get_items()
      .send()
      .await?;

  println!("{:#?}", response);

  let body: serde_json::Value = response.json().await?;
  println!("{:#?}", body);

  Ok(())
}

用户 API

async fn drive_users() -> GraphResult<()> {
  let client = GraphClient::new("ACCESS_TOKEN");

  let response = client
      .user("USER_ID")
      .drive()
      .item("ITEM_ID")
      .get_items()
      .send()
      .await?;

  println!("{:#?}", response);

  let body: serde_json::Value = response.json().await?;
  println!("{:#?}", body);

  Ok(())
}

网站 API

async fn drive_users() -> GraphResult<()> {
  let client = GraphClient::new("ACCESS_TOKEN");

  let response = client
      .site("SITE_ID")
      .drive()
      .item("ITEM_ID")
      .get_items()
      .send()
      .await?;

  println!("{:#?}", response);

  let body: serde_json::Value = response.json().await?;
  println!("{:#?}", body);

  Ok(())
}

创建文件夹。

use graph_rs_sdk::*;
use std::collections::HashMap;

static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
static FOLDER_NAME: &str = "NEW_FOLDER_NAME";
static PARENT_ID: &str = "PARENT_ID";

// For more info on creating a folder see:
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_post_children?view=odsp-graph-online

pub async fn create_new_folder() -> GraphResult<()> {
  let client = GraphClient::new(ACCESS_TOKEN);
  let folder: HashMap<String, serde_json::Value> = HashMap::new();

  let response = client
      .me()
      .drive()
      .item(PARENT_ID)
      .create_children(&serde_json::json!({
          "name": FOLDER_NAME,
          "folder": folder,
          "@microsoft.graph.conflictBehavior": "fail"
      }))
      .send()
      .await?;
  
  println!("{:#?}", response);
  
  Ok(())
}

针对驱动器的路径地址。

// Pass the path location of the item staring from the OneDrive root folder.
// Start the path with :/ and end with :

async fn get_item_by_path() -> GraphResult<()> {
  let client = GraphClient::new("ACCESS_TOKEN");

  let response = client
          .me()
          .drive()
          .item_by_path(":/documents/document.docx:")
          .get_items()
          .send()
          .await?;

  println!("{:#?}", response);

  let body: serde_json::Value = response.json().await?;
  println!("{:#?}", body);

  Ok(())
}

邮件

use graph_rs_sdk::*;

static ACCESS_TOKEN: &str = "ACCESS_TOKEN";

async fn get_mail_folder() -> GraphResult<()> {
  let client = GraphClient::new(ACCESS_TOKEN);

  let response = client.me()
      .mail_folder(MAIL_FOLDER_ID)
      .get_mail_folders()
      .send()
      .await?;

  println!("{:#?}", response);

  let body: serde_json::Value = response.json().await.unwrap();
  println!("{:#?}", body);

  Ok(())
}

创建消息

use graph_rs_sdk::*;

static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
static MAIL_FOLDER_ID: &str = "MAIL_FOLDER_ID";

async fn create_message() -> GraphResult<()> {
  let client = GraphClient::new(ACCESS_TOKEN);
  
  let response = client
      .me()
      .messages()
      .create_messages(&serde_json::json!({
          "subject":"Did you see last night's game?",
          "importance":"Low",
          "body":{
              "contentType":"HTML",
              "content":"They were <b>awesome</b>!"
          },
          "toRecipients":[
              {
                  "emailAddress":{
                      "address":"[email protected]"
                  }
              }
          ]
      }))
      .send()
      .await?;

  println!("{:#?}", response);
  
  Ok(())
}

发送邮件

use graph_rs_sdk::*;

static ACCESS_TOKEN: &str = "ACCESS_TOKEN";

async fn send_mail() -> GraphResult<()> {
    let client = GraphClient::new(ACCESS_TOKEN);

    let response = client
        .me()
        .send_mail(&serde_json::json!({
            "message": {
              "subject": "Meet for lunch?",
              "body": {
                  "contentType": "Text",
                  "content": "The new cafeteria is open."
              },
              "toRecipients": [
                  {
                      "emailAddress": {
                      "address": "[email protected]"
                      }
                  }
              ],
              "ccRecipients": [
                  {
                      "emailAddress": {
                      "address": "[email protected]"
                      }
                  }
              ]
            },
          "saveToSentItems": "false"
        }))
        .send()
        .await?;

    println!("{:#?}", response);
    
    Ok(())
}

邮件文件夹

use graph_rs_sdk::*;

static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
static MAIL_FOLDER_ID: &str = "MAIL_FOLDER_ID";

async fn create_mail_folder_message() -> GraphResult<()> {
    let client = GraphClient::new(ACCESS_TOKEN);
  
    let response = client
        .me()
        .mail_folder(MAIL_FOLDER_ID)
        .messages()
        .create_messages(&serde_json::json!({
            "subject":"Did you see last night's game?",
            "importance":"Low",
            "body": {
                "contentType":"HTML",
                "content":"They were <b>awesome</b>!"
            },
            "toRecipients":[{
                    "emailAddress":{
                        "address":"[email protected]"
                    }
                }
            ]
        }))
        .send()
        .await?;

    println!("{:#?}", response);
    
    Ok(())
}

获取收件箱消息

use graph_rs_sdk::*;

static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
static USER_ID: &str = "USER_ID";

async fn get_user_inbox_messages() -> GraphResult<()> {
  let client = GraphClient::new(ACCESS_TOKEN);
  
  let response = client
      .user(USER_ID)
      .mail_folder("Inbox")
      .messages()
      .list_messages()
      .top("2")
      .send()
      .await?;

  println!("{:#?}", response);

  let body: serde_json::Value = response.json().await?;
  println!("{:#?}", body);

  Ok(())
}

使用您自己的结构体。任何实现了 serde::Serialize 的内容都可以用于创建邮件消息或为 OneDrive 创建文件夹。

#[macro_use]
extern crate serde;

use graph_rs_sdk::*;

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
struct Message {
    subject: String,
    importance: String,
    body: HashMap<String, String>,
    #[serde(rename = "toRecipients")]
    to_recipients: Vec<ToRecipient>,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
struct ToRecipient {
    #[serde(rename = "emailAddress")]
    email_address: EmailAddress,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
struct EmailAddress {
    address: String,
}

async fn create_message() -> GraphResult<()> {
  let client = GraphClient::new("ACCESS_TOKEN");

  let mut body: HashMap<String, String> = HashMap::new();
  body.insert("contentType".to_string(), "HTML".to_string());
  body.insert("content".to_string(), "They were <b>awesome</b>!".to_string());

  let message = Message {
      subject: "Did you see last night's game?".into(),
      importance: "Low".into(),
      body,
      to_recipients: vec![
        ToRecipient {
            email_address: EmailAddress {
              address : "[email protected]".into()
            }
        }
      ]
  };

  let response = client
      .me()
      .messages()
      .create_messages(&message)
      .send()
      .await?;

  println!("{:#?}", response);

  let body: serde_json::Value = response.json().await?;
  println!("{:#?}", body);

  Ok(())
}

OData 查询

use graph_rs_sdk::*;

async fn create_message() -> GraphResult<()> {
  let client = GraphClient::new("ACCESS_TOKEN");

// Get all files in the root of the drive
// and select only specific properties.
  let response = client
          .me()
          .drive()
          .get_drive()
          .select(&["id", "name"])
          .send()
          .await?;

  println!("{:#?}", response);

  let body: serde_json::Value = response.json().await?;
  println!("{:#?}", body);

  Ok(())
}

批量请求

在单个请求中调用多个 Graph API。

use graph_rs_sdk::*;

static USER_ID: &str = "USER_ID";
static ACCESS_TOKEN: &str = "ACCESS_TOKEN";

async fn batch() -> GraphResult<()> {
  let client = GraphClient::new(ACCESS_TOKEN);

  let json = serde_json::json!({
        "requests": [
            {
                "id": "1",
                "method": "GET",
                "url": format!("/users/{USER_ID}/drive")
            },
            {
                "id": "2",
                "method": "GET",
                "url": format!("/users/{USER_ID}/drive/root")
            },
            {
                "id": "3",
                "method": "GET",
                "url": format!("/users/{USER_ID}/drive/recent")
            },
            {
                "id": "4",
                "method": "GET",
                "url": format!("/users/{USER_ID}/drive/root/children")
            },
            {
                "id": "5",
                "method": "GET",
                "url": format!("/users/{USER_ID}/drive/special/documents")
            },
        ]
    });

  let response = client.batch(&json).send().await?;

  let body: serde_json::Value = response.json().await?;
  println!("{:#?}", body);

  Ok(())
}

Id 对比非 Id 方法(例如 user("user-id")users()

许多可用的 API 都有不需要资源 id 的方法,同时许多 API 也有需要 id 的方法。对于大多数这些资源,该 SDK 使用两种不同的命名方案实现了这些方法,允许用户直接访问他们想要的方法。

例如,用户 API 可以列出没有 id 的用户,您可以通过调用 users() 方法找到 list_users(),而获取特定用户则需要用户 id,您可以通过调用 user<ID: AsRef<str>>(id: ID) 方法找到 get_users()

使用 users() 方法

use graph_rs_sdk::*;

// For more info on users see: https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0
// For more examples see the examples directory on GitHub.

static ACCESS_TOKEN: &str = "ACCESS_TOKEN";

async fn list_users() -> GraphResult<()> {
  let client = GraphClient::new(ACCESS_TOKEN);

  let response = client
      .users()
      .list_user()
      .send()
      .await
      .unwrap();

  println!("{:#?}", response);

  let body: serde_json::Value = response.json().await.unwrap();
  println!("{:#?}", body);
  
  Ok(())
}

使用用户 id user<ID: AsRef<str>>(id: ID) 方法

use graph_rs_sdk::*;

// For more info on users see: https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0
// For more examples see the examples directory on GitHub

static ACCESS_TOKEN: &str = "ACCESS_TOKEN";
static USER_ID: &str = "USER_ID";

async fn get_user() -> GraphResult<()> {
    let client = GraphClient::new(ACCESS_TOKEN);

    let response = client
        .user(USER_ID)
        .get_user()
        .send()
        .await?;

    println!("{:#?}", response);
    
    let body: serde_json::Value = response.json().await?;
    println!("{:#?}", body);
    
    Ok(())
}

OAuth - 获取访问令牌

使用应用程序构建器存储您的认证配置,并由客户端处理访问令牌请求。

支持

  • OpenId、授权码授权、客户端凭证、设备代码、证书认证
  • 自动刷新令牌
  • 交互式认证 | features = interactive-auth
  • 设备代码轮询
  • 使用证书进行授权 | features = openssl

详细示例

构建您选择的OAuth或OpenId Connect流主要有两种类型。

  • PublicClientApplication
  • ConfidentialClientApplication

一旦构建了ConfidentialClientApplicationPublicClientApplication,可以将它们传递给图形客户端。

自动令牌刷新也是通过将ConfidentialClientApplicationPublicClientApplication传递给GraphClient客户端来完成的。

更多示例,请参阅GitHub上examples/oauth目录中的OAuth示例,地址为GitHub

fn build_client(confidential_client: ConfidentialClientApplication<ClientSecretCredential>) {
  let graph_client = GraphClient::from(&confidential_client);
}

身份平台支持

以下是从Microsoft Identity Platform支持的流

对于需要使用登录后重定向的授权码的流,您可以使用

凭证

授权码授予

授权码授予被认为是机密客户端(除了混合流之外),我们可以通过用户执行登录后重定向的URL查询中返回的授权码来获取访问令牌。

一旦您有了授权码,就可以将其传递给客户端,客户端将在您发出的第一个图形API调用时执行获取访问令牌的请求。

授权码密钥

use graph_rs_sdk::{
  GraphClient,
  oauth::ConfidentialClientApplication,
};

async fn build_client(
  authorization_code: &str,
  client_id: &str,
  client_secret: &str,
  redirect_uri: url::Url,
  scope: Vec<&str>
) -> anyhow::Result<GraphClient> {
  let mut confidential_client = ConfidentialClientApplication::builder(client_id)
          .with_authorization_code(authorization_code) // returns builder type for AuthorizationCodeCredential
          .with_client_secret(client_secret)
          .with_scope(scope)
          .with_redirect_uri(redirect_uri)
          .build();

  let graph_client = GraphClient::from(confidential_client);
  
  Ok(graph_client)
}

带有证明密钥代码交换的授权码密钥

use graph_rs_sdk::identity::{
  AuthorizationCodeCredential, ConfidentialClientApplication, GenPkce,
  ProofKeyCodeExchange, TokenCredentialExecutor,
};
use lazy_static::lazy_static;
use url::Url;

// You can also pass your own values for PKCE instead of automatic generation by
// calling ProofKeyCodeExchange::new(code_verifier, code_challenge, code_challenge_method)
lazy_static! {
    static ref PKCE: ProofKeyCodeExchange = ProofKeyCodeExchange::oneshot().unwrap();
}

fn authorization_sign_in_url(client_id: &str, redirect_uri: url::Url, scope: Vec<String>) -> anyhow::Result<Url> {
  Ok(AuthorizationCodeCredential::authorization_url_builder(client_id)
          .with_scope(scope)
          .with_redirect_uri(redirect_uri)
          .with_pkce(&PKCE)
          .url()?)
}

fn build_confidential_client(
  authorization_code: &str,
  client_id: &str,
  client_secret: &str,
  redirect_uri: url::Url,
  scope: Vec<String>,
) -> anyhow::Result<ConfidentialClientApplication<AuthorizationCodeCredential>> {
  Ok(ConfidentialClientApplication::builder(client_id)
          .with_auth_code(authorization_code)
          .with_client_secret(client_secret)
          .with_scope(scope)
          .with_redirect_uri(redirect_uri)
          .with_pkce(&PKCE)
          .build())
}

客户端凭证

OAuth 2.0客户端凭据授予流程允许一个网络服务(机密客户端)使用其自己的凭据,而不是模拟用户,在调用另一个网络服务时进行身份验证。在RFC 6749中指定的授予,有时称为双腿OAuth,可以使用应用程序的身份来访问托管在Web上的资源。此类型通常用于在后台运行、无需立即与用户交互的服务器到服务器的交互,通常称为守护进程或服务帐户。

客户端凭据流程需要一次性管理员接受应用程序作用域的权限。要查看构建登录URL并作为管理员接受权限的示例,请参阅管理员同意示例

客户端密钥凭证

use graph_rs_sdk::{
  oauth::ConfidentialClientApplication, GraphClient
};

pub async fn get_graph_client(tenant: &str, client_id: &str, client_secret: &str) -> GraphClient {
  let mut confidential_client_application = ConfidentialClientApplication::builder(client_id)
          .with_client_secret(client_secret)
          .with_tenant(tenant)
          .build();

  GraphClient::from(&confidential_client_application)
}

环境凭证

客户端密钥环境凭证

环境变量

  • AZURE_TENANT_ID(可选/推荐 - 在授权URL中放置租户ID)
  • AZURE_CLIENT_ID(必需)
  • AZURE_CLIENT_SECRET(必需)
pub fn client_secret_credential() -> anyhow::Result<GraphClient> {
    let confidential_client = EnvironmentCredential::client_secret_credential()?;
    Ok(GraphClient::from(&confidential_client))
}

资源所有者密码凭证

环境变量

  • AZURE_TENANT_ID(可选 - 在授权URL中放置租户ID)
  • AZURE_CLIENT_ID(必需)
  • AZURE_USERNAME(必需)
  • AZURE_PASSWORD(必需)
pub fn username_password() -> anyhow::Result<GraphClient> {
    let public_client = EnvironmentCredential::resource_owner_password_credential()?;
    Ok(GraphClient::from(&public_client))
}

自动刷新令牌

客户端使用内存缓存存储令牌。有关其他持久化机制,请参阅令牌持久化机制开发

使用自动令牌刷新需要从令牌响应中获取刷新令牌。要获取刷新令牌,您必须包含offline_access作用域。

自动令牌刷新是通过将ConfidentialClientApplicationPublicClientApplication传递给GraphClient客户端来完成的。

如果您使用的是client_credentials授予,则不需要offline_access作用域。令牌仍会自动刷新,因为此流程不需要使用刷新令牌来获取新的访问令牌。

以下示例使用授权码授予。

首先创建用户登录的URL。用户登录后,将被重定向回您的应用程序,并且认证代码将包含在uri的查询中。

pub fn authorization_sign_in_url(client_id: &str, tenant: &str, redirect_uri: url::Url) -> Url {
  let scope = vec!["offline_access"];

  AuthorizationCodeCredential::authorization_url_builder(client_id)
          .with_redirect_uri(redirect_uri)
          .with_scope(scope)
          .url()
          .unwrap()
}

一旦您获得授权代码,就可以构建一个机密客户端并将其传递给图形客户端。

async fn build_client(
  authorization_code: &str,
  client_id: &str,
  client_secret: &str,
  scope: Vec<String>, // with offline_access
  redirect_uri: url::Url,
) -> anyhow::Result<GraphClient> {
  let mut confidential_client = ConfidentialClientApplication::builder(client_id)
          .with_auth_code(authorization_code) // returns builder type for AuthorizationCodeCredential
          .with_client_secret(client_secret)
          .with_scope(scope)
          .with_redirect_uri(redirect_uri)
          .build();

  let graph_client = GraphClient::from(&confidential_client);

  Ok(graph_client)
}

令牌持久化机制开发

当前仅提供内存中的令牌缓存用于令牌持久化。正在积极开发其他持久化机制,例如Azure Key Vault和桌面机制(如MacOS KeyChain),这些将在2.0.0版本之后发布。您可以在https://github.com/sreeise/graph-rs-sdk/issues/432跟踪此进度。

交互式认证

需要功能 interactive-auth

警告:在某些场景下,在异步上下文中运行交互式认证可能会导致崩溃。我们建议进行彻底测试,以确保您能够为您的用例使用交互式认证。此外,设备代码交互式认证目前不支持在异步代码中。我们正在努力在2.0.0版本后解决这个问题。

[dependencies]
graph-rs-sdk = { version = "...", features = ["interactive-auth"] }

交互式认证使用wry crate在支持的平台(如桌面)上运行网页视图。

use graph_rs_sdk::{identity::{AuthorizationCodeCredential, Secret}, GraphClient};

async fn authenticate(
  tenant_id: &str,
  client_id: &str,
  client_secret: &str,
  redirect_uri: url::Url,
  scope: Vec<&str>,
) -> anyhow::Result<GraphClient> {
  std::env::set_var("RUST_LOG", "debug");
  pretty_env_logger::init();

  let (authorization_query_response, mut credential_builder) =
          AuthorizationCodeCredential::authorization_url_builder(client_id)
                  .with_tenant(tenant_id)
                  .with_scope(scope) // Adds offline_access as a scope which is needed to get a refresh token.
                  .with_redirect_uri(redirect_uri)
                  .with_interactive_auth(Secret("client-secret".to_string()), Default::default())
                  .unwrap();

  debug!("{authorization_query_response:#?}");

  let mut confidential_client = credential_builder.with_client_secret(client_secret).build();

  Ok(GraphClient::from(&confidential_client))
}

贡献

请参阅GitHub上的贡献指南

维基

请参阅GitHub Wiki

功能请求或错误报告

对于错误报告,请在GitHub上提交一个问题,我们将尽快给出响应或修复。

GitHub上的讨论标签已启用,所以请随时前往那里提出任何问题或功能请求。对于错误,请先提交问题。功能可以通过问题或讨论请求。两种方式都适用。除此之外,您可以随时提问,为他人提供技巧,并就项目进行一般讨论。

依赖关系

~22–63MB
~1M SLoC