LoginSignup
1
0

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

Posted at

概要

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

前提など

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

実装サンプル

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

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ヘッダーの取得部分です。取得できたら独自の型を設けて結果を入れています。

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を取り出しています。

#[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の使い分けについては適当です。

#[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())
        }
    }
}

その他参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0