Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion src/graphql/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@

use std::str::FromStr;

use async_graphql::{Context, Guard};
use axum_extra::headers::authorization::Bearer;
use axum_extra::headers::Authorization;
use derive_more::{Display, Error, From};
use serde::{Deserialize, Serialize};
use tracing::info;
use tracing::{info, trace};

use crate::cli::PolicyOptions;

Expand Down Expand Up @@ -176,6 +177,49 @@ impl PolicyCheck {
}
}

#[derive(Debug)]
pub(crate) enum AuthGuard<'a> {
Access {
instrument: &'a str,
instrument_session: &'a str,
},
InstrumentAdmin {
instrument: &'a str,
},
Admin,
}

impl<'a> Guard for AuthGuard<'a> {
async fn check(&self, ctx: &Context<'_>) -> async_graphql::Result<()> {
if let Some(policy) = ctx.data::<Option<PolicyCheck>>()? {
trace!("Auth enabled: checking token");
let token = ctx.data::<Option<Authorization<Bearer>>>()?;
let check = match self {
AuthGuard::Access {
instrument,
instrument_session,
} => {
policy
.check_access(token.as_ref(), instrument, instrument_session)
.await
}
AuthGuard::InstrumentAdmin { instrument } => {
policy
.check_instrument_admin(token.as_ref(), instrument)
.await
}
AuthGuard::Admin => policy.check_admin(token.as_ref()).await,
};
check
.inspect_err(|e| info!("Authorization failed: {e:?}"))
.map_err(async_graphql::Error::from)
} else {
trace!("No authorization configured");
Ok(())
}
}
}

#[derive(Debug, Display, Error, From)]
pub enum AuthError {
#[display("Invalid authorization configuration")]
Expand Down
40 changes: 7 additions & 33 deletions src/graphql/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

use std::any;
use std::borrow::Cow;
use std::future::Future;
use std::io::Write;
use std::path::{Component, PathBuf};

Expand All @@ -25,7 +24,7 @@ use async_graphql::{
Object, Scalar, ScalarType, Schema, SimpleObject, TypeName, Value,
};
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
use auth::{AuthError, PolicyCheck};
use auth::{AuthGuard, PolicyCheck};
use axum::http::StatusCode;
use axum::response::{Html, IntoResponse};
use axum::routing::{get, post};
Expand Down Expand Up @@ -342,16 +341,13 @@ impl Query {
}

/// Get the current configuration for the given instrument
#[graphql(guard = "AuthGuard::InstrumentAdmin{instrument: &instrument}")]
#[instrument(skip(self, ctx))]
async fn configuration(
&self,
ctx: &Context<'_>,
instrument: String,
) -> async_graphql::Result<CurrentConfiguration> {
check_auth(ctx, |policy, token| {
policy.check_instrument_admin(token, &instrument)
})
.await?;
let db = ctx.data::<SqliteScanPathService>()?;
let nt = ctx.data::<NumTracker>()?;
trace!("Getting config for {instrument:?}");
Expand All @@ -361,13 +357,13 @@ impl Query {

/// Get the configurations for all available instruments
/// Can be filtered to provide one or more specific instruments
#[graphql(guard = "AuthGuard::Admin")]
#[instrument(skip(self, ctx))]
async fn configurations(
&self,
ctx: &Context<'_>,
instrument_filters: Option<Vec<String>>,
) -> async_graphql::Result<Vec<CurrentConfiguration>> {
check_auth(ctx, |policy, token| policy.check_admin(token)).await?;
let db = ctx.data::<SqliteScanPathService>()?;
let nt = ctx.data::<NumTracker>()?;
let configurations = match instrument_filters {
Expand All @@ -390,6 +386,9 @@ impl Query {
/// Queries that modify the state of the numtracker configuration in some way
impl Mutation {
/// Generate scan file locations for the next scan
#[graphql(
guard = "AuthGuard::Access{instrument: &instrument, instrument_session: &instrument_session}"
)]
#[instrument(skip(self, ctx))]
async fn scan(
&self,
Expand All @@ -398,10 +397,6 @@ impl Mutation {
instrument_session: String,
sub: Option<Subdirectory>,
) -> async_graphql::Result<ScanPaths> {
check_auth(ctx, |policy, token| {
policy.check_access(token, &instrument, &instrument_session)
})
.await?;
let db = ctx.data::<SqliteScanPathService>()?;
let nt = ctx.data::<NumTracker>()?;
// There is a race condition here if a process increments the file
Expand Down Expand Up @@ -430,17 +425,14 @@ impl Mutation {
}

/// Add or modify the stored configuration for an instrument
#[graphql(guard = "AuthGuard::InstrumentAdmin{instrument: &instrument}")]
#[instrument(skip(self, ctx))]
async fn configure(
&self,
ctx: &Context<'_>,
instrument: String,
config: ConfigurationUpdates,
) -> async_graphql::Result<CurrentConfiguration> {
check_auth(ctx, |pc, token| {
pc.check_instrument_admin(token, &instrument)
})
.await?;
let db = ctx.data::<SqliteScanPathService>()?;
let nt = ctx.data::<NumTracker>()?;
trace!("Configuring: {instrument}: {config:?}");
Expand All @@ -453,24 +445,6 @@ impl Mutation {
}
}

async fn check_auth<'ctx, Check, R>(ctx: &Context<'ctx>, check: Check) -> async_graphql::Result<()>
where
Check: Fn(&'ctx PolicyCheck, Option<&'ctx Authorization<Bearer>>) -> R,
R: Future<Output = Result<(), AuthError>>,
{
if let Some(policy) = ctx.data::<Option<PolicyCheck>>()? {
trace!("Auth enabled: checking token");
let token = ctx.data::<Option<Authorization<Bearer>>>()?;
check(policy, token.as_ref())
.await
.inspect_err(|e| info!("Authorization failed: {e:?}"))
.map_err(async_graphql::Error::from)
} else {
trace!("No authorization configured");
Ok(())
}
}

/// Changes that should be made to an instrument's configuration
#[derive(Debug, InputObject)]
struct ConfigurationUpdates {
Expand Down
Loading