概要
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())
}
}
}