feat: add permission control
This commit is contained in:
27
apps/recorder/src/graphql/extention.rs
Normal file
27
apps/recorder/src/graphql/extention.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_graphql::{
|
||||
ServerResult, Value,
|
||||
extensions::{Extension, ExtensionContext, ExtensionFactory, NextResolve, ResolveInfo},
|
||||
};
|
||||
|
||||
pub struct GraphqlAuthExtension;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Extension for GraphqlAuthExtension {
|
||||
async fn resolve(
|
||||
&self,
|
||||
ctx: &ExtensionContext<'_>,
|
||||
info: ResolveInfo<'_>,
|
||||
next: NextResolve<'_>,
|
||||
) -> ServerResult<Option<Value>> {
|
||||
dbg!(info.field);
|
||||
next.run(ctx, info).await
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtensionFactory for GraphqlAuthExtension {
|
||||
fn create(&self) -> Arc<dyn Extension> {
|
||||
Arc::new(GraphqlAuthExtension)
|
||||
}
|
||||
}
|
||||
199
apps/recorder/src/graphql/guard.rs
Normal file
199
apps/recorder/src/graphql/guard.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_graphql::dynamic::{ResolverContext, ValueAccessor};
|
||||
use sea_orm::EntityTrait;
|
||||
use seaography::{BuilderContext, FnGuard, GuardAction};
|
||||
|
||||
use super::util::get_entity_key;
|
||||
use crate::{
|
||||
auth::{AuthError, AuthUserInfo},
|
||||
graphql::util::get_column_key,
|
||||
};
|
||||
|
||||
fn guard_data_object_accessor_with_subscriber_id(
|
||||
value: ValueAccessor<'_>,
|
||||
column_name: &str,
|
||||
subscriber_id: i32,
|
||||
) -> async_graphql::Result<()> {
|
||||
let obj = value.object()?;
|
||||
|
||||
let subscriber_id_value = obj.try_get(column_name)?;
|
||||
|
||||
let id = subscriber_id_value.i64()?;
|
||||
|
||||
if id == subscriber_id as i64 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(async_graphql::Error::new("subscriber permission denied"))
|
||||
}
|
||||
}
|
||||
|
||||
fn guard_data_object_accessor_with_optional_subscriber_id(
|
||||
value: ValueAccessor<'_>,
|
||||
column_name: &str,
|
||||
subscriber_id: i32,
|
||||
) -> async_graphql::Result<()> {
|
||||
if value.is_null() {
|
||||
return Ok(());
|
||||
}
|
||||
let obj = value.object()?;
|
||||
|
||||
if let Some(subscriber_id_value) = obj.get(column_name) {
|
||||
let id = subscriber_id_value.i64()?;
|
||||
if id == subscriber_id as i64 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(async_graphql::Error::new("subscriber permission denied"))
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn guard_filter_object_accessor_with_subscriber_id(
|
||||
value: ValueAccessor<'_>,
|
||||
column_name: &str,
|
||||
subscriber_id: i32,
|
||||
) -> async_graphql::Result<()> {
|
||||
let obj = value.object()?;
|
||||
let subscriber_id_filter_input_value = obj.try_get(column_name)?;
|
||||
|
||||
let subscriber_id_filter_input_obj = subscriber_id_filter_input_value.object()?;
|
||||
|
||||
let subscriber_id_value = subscriber_id_filter_input_obj.try_get("eq")?;
|
||||
|
||||
let id = subscriber_id_value.i64()?;
|
||||
if id == subscriber_id as i64 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(async_graphql::Error::new("subscriber permission denied"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn guard_entity_with_subscriber_id<T>(context: &BuilderContext, column: &T::Column) -> FnGuard
|
||||
where
|
||||
T: EntityTrait,
|
||||
<T as EntityTrait>::Model: Sync,
|
||||
{
|
||||
let entity_key = get_entity_key::<T>(context);
|
||||
let entity_name = context.entity_query_field.type_name.as_ref()(&entity_key);
|
||||
let column_key = get_column_key::<T>(context, column);
|
||||
let column_name = Arc::new(context.entity_object.column_name.as_ref()(
|
||||
&entity_key,
|
||||
&column_key,
|
||||
));
|
||||
let entity_create_one_mutation_field_name = Arc::new(format!(
|
||||
"{}{}",
|
||||
entity_name, context.entity_create_one_mutation.mutation_suffix
|
||||
));
|
||||
let entity_create_one_mutation_data_field_name =
|
||||
Arc::new(context.entity_create_one_mutation.data_field.clone());
|
||||
let entity_create_batch_mutation_field_name = Arc::new(format!(
|
||||
"{}{}",
|
||||
entity_name,
|
||||
context.entity_create_batch_mutation.mutation_suffix.clone()
|
||||
));
|
||||
let entity_create_batch_mutation_data_field_name =
|
||||
Arc::new(context.entity_create_batch_mutation.data_field.clone());
|
||||
let entity_delete_mutation_field_name = Arc::new(format!(
|
||||
"{}{}",
|
||||
entity_name,
|
||||
context.entity_delete_mutation.mutation_suffix.clone()
|
||||
));
|
||||
let entity_delete_filter_field_name =
|
||||
Arc::new(context.entity_delete_mutation.filter_field.clone());
|
||||
let entity_update_mutation_field_name = Arc::new(format!(
|
||||
"{}{}",
|
||||
entity_name, context.entity_update_mutation.mutation_suffix
|
||||
));
|
||||
let entity_update_mutation_filter_field_name =
|
||||
Arc::new(context.entity_update_mutation.filter_field.clone());
|
||||
let entity_update_mutation_data_field_name =
|
||||
Arc::new(context.entity_update_mutation.data_field.clone());
|
||||
let entity_query_field_name = Arc::new(entity_name);
|
||||
let entity_query_filter_field_name = Arc::new(context.entity_query_field.filters.clone());
|
||||
Box::new(move |context: &ResolverContext| -> GuardAction {
|
||||
match context.ctx.data::<AuthUserInfo>() {
|
||||
Ok(user_info) => {
|
||||
let subscriber_id = user_info.subscriber_auth.subscriber_id;
|
||||
let validation_result = match context.field().name() {
|
||||
field if field == entity_create_one_mutation_field_name.as_str() => context
|
||||
.args
|
||||
.try_get(&entity_create_one_mutation_data_field_name)
|
||||
.and_then(|data_value| {
|
||||
guard_data_object_accessor_with_subscriber_id(
|
||||
data_value,
|
||||
&column_name,
|
||||
subscriber_id,
|
||||
)
|
||||
}),
|
||||
field if field == entity_create_batch_mutation_field_name.as_str() => context
|
||||
.args
|
||||
.try_get(&entity_create_batch_mutation_data_field_name)
|
||||
.and_then(|data_value| {
|
||||
data_value.list().and_then(|data_list| {
|
||||
data_list.iter().try_for_each(|data_item_value| {
|
||||
guard_data_object_accessor_with_subscriber_id(
|
||||
data_item_value,
|
||||
&column_name,
|
||||
subscriber_id,
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
field if field == entity_delete_mutation_field_name.as_str() => context
|
||||
.args
|
||||
.try_get(&entity_delete_filter_field_name)
|
||||
.and_then(|filter_value| {
|
||||
guard_filter_object_accessor_with_subscriber_id(
|
||||
filter_value,
|
||||
&column_name,
|
||||
subscriber_id,
|
||||
)
|
||||
}),
|
||||
field if field == entity_update_mutation_field_name.as_str() => context
|
||||
.args
|
||||
.try_get(&entity_update_mutation_filter_field_name)
|
||||
.and_then(|filter_value| {
|
||||
guard_filter_object_accessor_with_subscriber_id(
|
||||
filter_value,
|
||||
&column_name,
|
||||
subscriber_id,
|
||||
)
|
||||
})
|
||||
.and_then(|_| {
|
||||
match context.args.get(&entity_update_mutation_data_field_name) {
|
||||
Some(data_value) => {
|
||||
guard_data_object_accessor_with_optional_subscriber_id(
|
||||
data_value,
|
||||
&column_name,
|
||||
subscriber_id,
|
||||
)
|
||||
}
|
||||
None => Ok(()),
|
||||
}
|
||||
}),
|
||||
field if field == entity_query_field_name.as_str() => context
|
||||
.args
|
||||
.try_get(&entity_query_filter_field_name)
|
||||
.and_then(|filter_value| {
|
||||
guard_filter_object_accessor_with_subscriber_id(
|
||||
filter_value,
|
||||
&column_name,
|
||||
subscriber_id,
|
||||
)
|
||||
}),
|
||||
field => Err(async_graphql::Error::new(format!(
|
||||
"unsupport graphql field {}",
|
||||
field
|
||||
))),
|
||||
};
|
||||
match validation_result.map_err(AuthError::GraphQLPermissionError) {
|
||||
Ok(_) => GuardAction::Allow,
|
||||
Err(err) => GuardAction::Block(Some(err.to_string())),
|
||||
}
|
||||
}
|
||||
Err(err) => GuardAction::Block(Some(err.message)),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
pub mod config;
|
||||
pub mod query_root;
|
||||
pub mod extention;
|
||||
pub mod guard;
|
||||
pub mod schema_root;
|
||||
pub mod service;
|
||||
pub mod util;
|
||||
|
||||
pub use query_root::schema;
|
||||
pub use schema_root::schema;
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
use async_graphql::dynamic::*;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use seaography::{Builder, BuilderContext};
|
||||
|
||||
lazy_static::lazy_static! { static ref CONTEXT : BuilderContext = {
|
||||
BuilderContext {
|
||||
..Default::default()
|
||||
}
|
||||
}; }
|
||||
|
||||
pub fn schema(
|
||||
database: DatabaseConnection,
|
||||
depth: Option<usize>,
|
||||
complexity: Option<usize>,
|
||||
) -> Result<Schema, SchemaError> {
|
||||
use crate::models::*;
|
||||
let mut builder = Builder::new(&CONTEXT, database.clone());
|
||||
|
||||
seaography::register_entities!(
|
||||
builder,
|
||||
[
|
||||
bangumi,
|
||||
downloaders,
|
||||
downloads,
|
||||
episodes,
|
||||
subscribers,
|
||||
subscription_bangumi,
|
||||
subscription_episode,
|
||||
subscriptions
|
||||
]
|
||||
);
|
||||
|
||||
{
|
||||
builder.register_enumeration::<downloads::DownloadStatus>();
|
||||
builder.register_enumeration::<subscriptions::SubscriptionCategory>();
|
||||
builder.register_enumeration::<downloaders::DownloaderCategory>();
|
||||
builder.register_enumeration::<downloads::DownloadMime>();
|
||||
}
|
||||
|
||||
let schema = builder.schema_builder();
|
||||
|
||||
let schema = if let Some(depth) = depth {
|
||||
schema.limit_depth(depth)
|
||||
} else {
|
||||
schema
|
||||
};
|
||||
let schema = if let Some(complexity) = complexity {
|
||||
schema.limit_complexity(complexity)
|
||||
} else {
|
||||
schema
|
||||
};
|
||||
schema
|
||||
.data(database)
|
||||
.finish()
|
||||
.inspect_err(|e| tracing::error!(e = ?e))
|
||||
}
|
||||
146
apps/recorder/src/graphql/schema_root.rs
Normal file
146
apps/recorder/src/graphql/schema_root.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use async_graphql::dynamic::*;
|
||||
use once_cell::sync::OnceCell;
|
||||
use sea_orm::{DatabaseConnection, EntityTrait, Iterable};
|
||||
use seaography::{Builder, BuilderContext, FilterType, FnGuard};
|
||||
|
||||
use super::util::{get_entity_column_key, get_entity_key};
|
||||
use crate::graphql::guard::guard_entity_with_subscriber_id;
|
||||
|
||||
static CONTEXT: OnceCell<BuilderContext> = OnceCell::new();
|
||||
|
||||
fn restrict_filter_input_for_entity<T>(
|
||||
context: &mut BuilderContext,
|
||||
column: &T::Column,
|
||||
filter_type: Option<FilterType>,
|
||||
) where
|
||||
T: EntityTrait,
|
||||
<T as EntityTrait>::Model: Sync,
|
||||
{
|
||||
let key = get_entity_column_key::<T>(context, column);
|
||||
context.filter_types.overwrites.insert(key, filter_type);
|
||||
}
|
||||
|
||||
fn restrict_subscriber_for_entity<T>(
|
||||
context: &mut BuilderContext,
|
||||
column: &T::Column,
|
||||
entity_guard: impl FnOnce(&BuilderContext, &T::Column) -> FnGuard,
|
||||
) where
|
||||
T: EntityTrait,
|
||||
<T as EntityTrait>::Model: Sync,
|
||||
{
|
||||
let entity_key = get_entity_key::<T>(context);
|
||||
context
|
||||
.guards
|
||||
.entity_guards
|
||||
.insert(entity_key, entity_guard(context, column));
|
||||
}
|
||||
|
||||
pub fn schema(
|
||||
database: DatabaseConnection,
|
||||
depth: Option<usize>,
|
||||
complexity: Option<usize>,
|
||||
) -> Result<Schema, SchemaError> {
|
||||
use crate::models::*;
|
||||
let context = CONTEXT.get_or_init(|| {
|
||||
let mut context = BuilderContext::default();
|
||||
restrict_subscriber_for_entity::<bangumi::Entity>(
|
||||
&mut context,
|
||||
&bangumi::Column::SubscriberId,
|
||||
guard_entity_with_subscriber_id::<bangumi::Entity>,
|
||||
);
|
||||
restrict_subscriber_for_entity::<downloaders::Entity>(
|
||||
&mut context,
|
||||
&downloaders::Column::SubscriberId,
|
||||
guard_entity_with_subscriber_id::<downloaders::Entity>,
|
||||
);
|
||||
restrict_subscriber_for_entity::<downloads::Entity>(
|
||||
&mut context,
|
||||
&downloads::Column::SubscriberId,
|
||||
guard_entity_with_subscriber_id::<downloads::Entity>,
|
||||
);
|
||||
restrict_subscriber_for_entity::<episodes::Entity>(
|
||||
&mut context,
|
||||
&episodes::Column::SubscriberId,
|
||||
guard_entity_with_subscriber_id::<episodes::Entity>,
|
||||
);
|
||||
restrict_subscriber_for_entity::<subscriptions::Entity>(
|
||||
&mut context,
|
||||
&subscriptions::Column::SubscriberId,
|
||||
guard_entity_with_subscriber_id::<subscriptions::Entity>,
|
||||
);
|
||||
restrict_subscriber_for_entity::<subscribers::Entity>(
|
||||
&mut context,
|
||||
&subscribers::Column::Id,
|
||||
guard_entity_with_subscriber_id::<subscribers::Entity>,
|
||||
);
|
||||
restrict_subscriber_for_entity::<subscription_bangumi::Entity>(
|
||||
&mut context,
|
||||
&subscription_bangumi::Column::SubscriberId,
|
||||
guard_entity_with_subscriber_id::<subscription_bangumi::Entity>,
|
||||
);
|
||||
restrict_subscriber_for_entity::<subscription_episode::Entity>(
|
||||
&mut context,
|
||||
&subscription_episode::Column::SubscriberId,
|
||||
guard_entity_with_subscriber_id::<subscription_episode::Entity>,
|
||||
);
|
||||
for column in subscribers::Column::iter() {
|
||||
if !matches!(column, subscribers::Column::Id) {
|
||||
restrict_filter_input_for_entity::<subscribers::Entity>(
|
||||
&mut context,
|
||||
&column,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
context
|
||||
});
|
||||
let mut builder = Builder::new(context, database.clone());
|
||||
|
||||
{
|
||||
builder.register_entity::<subscribers::Entity>(
|
||||
<subscribers::RelatedEntity as sea_orm::Iterable>::iter()
|
||||
.map(|rel| seaography::RelationBuilder::get_relation(&rel, builder.context))
|
||||
.collect(),
|
||||
);
|
||||
builder = builder.register_entity_dataloader_one_to_one(subscribers::Entity, tokio::spawn);
|
||||
builder = builder.register_entity_dataloader_one_to_many(subscribers::Entity, tokio::spawn);
|
||||
}
|
||||
|
||||
seaography::register_entities!(
|
||||
builder,
|
||||
[
|
||||
bangumi,
|
||||
downloaders,
|
||||
downloads,
|
||||
episodes,
|
||||
subscription_bangumi,
|
||||
subscription_episode,
|
||||
subscriptions
|
||||
]
|
||||
);
|
||||
|
||||
{
|
||||
builder.register_enumeration::<downloads::DownloadStatus>();
|
||||
builder.register_enumeration::<subscriptions::SubscriptionCategory>();
|
||||
builder.register_enumeration::<downloaders::DownloaderCategory>();
|
||||
builder.register_enumeration::<downloads::DownloadMime>();
|
||||
}
|
||||
|
||||
let schema = builder.schema_builder();
|
||||
|
||||
let schema = if let Some(depth) = depth {
|
||||
schema.limit_depth(depth)
|
||||
} else {
|
||||
schema
|
||||
};
|
||||
let schema = if let Some(complexity) = complexity {
|
||||
schema.limit_complexity(complexity)
|
||||
} else {
|
||||
schema
|
||||
};
|
||||
schema
|
||||
.data(database)
|
||||
// .extension(GraphqlAuthExtension)
|
||||
.finish()
|
||||
.inspect_err(|e| tracing::error!(e = ?e))
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use loco_rs::app::{AppContext, Initializer};
|
||||
use once_cell::sync::OnceCell;
|
||||
use sea_orm::DatabaseConnection;
|
||||
|
||||
use super::{config::AppGraphQLConfig, query_root};
|
||||
use super::{config::AppGraphQLConfig, schema_root};
|
||||
use crate::config::AppConfigExt;
|
||||
|
||||
static APP_GRAPHQL_SERVICE: OnceCell<AppGraphQLService> = OnceCell::new();
|
||||
@@ -16,7 +16,7 @@ pub struct AppGraphQLService {
|
||||
|
||||
impl AppGraphQLService {
|
||||
pub fn new(config: AppGraphQLConfig, db: DatabaseConnection) -> Result<Self, SchemaError> {
|
||||
let schema = query_root::schema(db, config.depth_limit, config.complexity_limit)?;
|
||||
let schema = schema_root::schema(db, config.depth_limit, config.complexity_limit)?;
|
||||
Ok(Self { schema })
|
||||
}
|
||||
|
||||
|
||||
30
apps/recorder/src/graphql/util.rs
Normal file
30
apps/recorder/src/graphql/util.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use sea_orm::{EntityName, EntityTrait, IdenStatic};
|
||||
use seaography::BuilderContext;
|
||||
|
||||
pub fn get_entity_key<T>(context: &BuilderContext) -> String
|
||||
where
|
||||
T: EntityTrait,
|
||||
<T as EntityTrait>::Model: Sync,
|
||||
{
|
||||
context.entity_object.type_name.as_ref()(<T as EntityName>::table_name(&T::default()))
|
||||
}
|
||||
|
||||
pub fn get_column_key<T>(context: &BuilderContext, column: &T::Column) -> String
|
||||
where
|
||||
T: EntityTrait,
|
||||
<T as EntityTrait>::Model: Sync,
|
||||
{
|
||||
let entity_name = get_entity_key::<T>(context);
|
||||
context.entity_object.column_name.as_ref()(&entity_name, column.as_str())
|
||||
}
|
||||
|
||||
pub fn get_entity_column_key<T>(context: &BuilderContext, column: &T::Column) -> String
|
||||
where
|
||||
T: EntityTrait,
|
||||
<T as EntityTrait>::Model: Sync,
|
||||
{
|
||||
let entity_name = get_entity_key::<T>(context);
|
||||
let column_name = get_column_key::<T>(context, column);
|
||||
|
||||
format!("{}.{}", &entity_name, &column_name)
|
||||
}
|
||||
Reference in New Issue
Block a user