【Rust】async-graphqlのguardを試してみた

## 概要
Rustのasync-graphqlで、認証済みのみ実行可なqueryを作成する際につける機能がguardです。基本的にはドキュメントにある[Field Guard](https://async-graphql.github.io/async-graphql/en/field_guard.html)を参考に実装という話なのですが、contextにどうデータを入れるかなど含めて、今回クエリに対しguardを実装してみた内容をメモ書きします。

## 前提など
* 使用したrustcのバージョンは`1.76.0`、async-graphqlのバージョンは`7.0.1`、actix-webを使用したのでそのバージョンは`4.5.1`です。
* あまり今回の件とは関係ないですが、shuttleを実行基盤として使用しています。

## 実装サンプル
まずはmainでの設定部分です。
今回はjwtを前提としているのでsecretを引き回して、authorizationヘッダーを複合化できたらcontextにセットするようにしています。

```rust
async fn index(
    schema: Data<ApiSchema>,
    secrets_data: Data<SecretStore>,
    req: HttpRequest,
    graphql_req: GraphQLRequest,
) -> GraphQLResponse {
    let mut request = graphql_req.into_inner();
    // secretからjwtのシークレットキーを取得
    if let Some(jwt_secret) = secrets_data.clone().get("JWT_SECRET") {
        // ヘッダーから認証情報を取得
        if let Some(auth_context) =
            account_user_service::get_token_from_authorization_header(req.headers(), jwt_secret)
        {
            // 認証情報の取得に成功したらrequestのcontextにセット
            request = request.data(auth_context);
        }
    }
    schema.execute(request).await.into()
}

#[shuttle_runtime::main]
async fn main(
    #[shuttle_secrets::Secrets] secrets: SecretStore,
) -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
・
・
・
    let config = move |cfg: &mut ServiceConfig| {
        cfg.app_data(Data::new(schema.clone()))
            .app_data(Data::new(secrets.clone()))
            .service(
                web::resource("/graphql").to(index),
            );
    };
    Ok(config.into())
}
```

authorizationヘッダーの取得部分です。取得できたら独自の型を設けて結果を入れています。

```rust
pub fn get_token_from_authorization_header(
    headers: &HeaderMap,
    jwt_secret: String,
) -> Option<common_struct::AuthContext> {
    match headers
        .get(actix_web::http::header::AUTHORIZATION)?
        .to_str()
    {
        Ok(auth_header) => auth_header.strip_prefix("Bearer ").and_then(|t| {
            // Bearerトークンをdeocde（処理内容は記載割愛）
            match jwt_service::decode_jwt(t, &jwt_secret) {
			    // 独自の型（AuthContext）に結果を入れて返す
                Ok(claim) => Some(common_struct::AuthContext {
                    account_id: claim.claims.contents,
                }),
                Err(_) => None,
            }
        }),
        Err(_) => None,
    }
}
```


クエリの実行部分です。クエリの前にGuardでチェックが走るので`data_unchecked`でcontextを取り出しています。
```rust
#[Object]
impl Query {
    // ヘッダの認証トークンからユーザを取得する
    #[graphql(guard = "RoleGuard::new(Role::User)")]
    async fn get_user_from_auth_header(
        &self,
        ctx: &Context<'_>,
    ) -> Result<sample_model::AccountUserResponse> {
        let auth_context = &mut ctx.data_unchecked::<common_struct::AuthContext>();
		// Contextからユーザ情報を取得して返す（処理内容は記載割愛）
        return account_user_service::get_account_user_by_id(auth_context.clone().account_id).await;
    }
}
```


最後にGuardの実装部分です。上記の前提の部分で書いたバージョンだと、traitの実装部分で`async_trait`を設定する必要がありました。
また、今回の実装でRoleの使い分けについては適当です。
```rust
#[derive(Eq, PartialEq, Copy, Clone)]
pub enum Role {
    User,
    Anonymous,
}

pub struct RoleGuard {
    pub role: Role,
}

impl RoleGuard {
    pub fn new(role: Role) -> Self {
        Self { role }
    }
}

#[async_trait]
impl Guard for RoleGuard {
    async fn check(&self, ctx: &Context<'_>) -> Result<()> {
        // Roleの使い分けは適当に・・
        if Some(&self.role) == Some(&Role::Anonymous) {
            Ok(())
        } else if let Some(_) = ctx.data_opt::<common_struct::AuthContext>() {
            Ok(())
        } else {
            Err("Forbidden".into())
        }
    }
}
```

## その他参考
* [async-graphql/examples/token-from-header](https://github.com/async-graphql/examples/blob/711d360c6fa5bc4e100a8420a7fc8ccf3a2c3193/actix-web/token-from-header/src/main.rs)