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
25 changes: 14 additions & 11 deletions raphtory-graphql/src/auth_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,7 @@ impl Ord for GraphPermission {
/// Variants are ordered lowest to highest so that `PartialOrd`/`Ord` reflect the hierarchy.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum NamespacePermission {
/// No access — namespace is invisible.
Denied,
/// Namespace is visible in parent `children()` listings but cannot be browsed.
Discover,
/// Namespace is browseable; graphs inside are visible as MetaGraph in `graphs()`.
Introspect,
/// All descendant graphs are fully readable.
/// Namespace is listable; graphs and child namespaces are visible.
Read,
/// All descendants are writable; `newGraph` is allowed.
Write,
Expand All @@ -126,13 +120,22 @@ pub trait AuthorizationPolicy: Send + Sync + 'static {
) -> Result<GraphPermission, AuthPolicyError>;

/// Resolves the effective namespace permission for a principal.
/// Admin principals always yield `Write`.
/// Empty store yields `Read` (fail open, consistent with graph_permissions).
/// Missing role yields `Denied`.
/// Returns `None` if the principal has no access to this namespace (it is invisible).
/// Admin principals always yield `Some(Write)`.
/// Empty store yields `Some(Read)` (fail open, consistent with graph_permissions).
/// Missing role or no explicit grant yields `None`.
/// The implementation is responsible for extracting principal identity from `ctx`.
fn namespace_permissions(
&self,
ctx: &async_graphql::Context<'_>,
path: &str,
) -> NamespacePermission;
) -> Option<NamespacePermission>;

/// Called after a graph is successfully created to auto-grant `Write` for the creator's role.
/// Returns an error if the grant cannot be persisted; the caller is responsible for rolling
/// back the graph creation so the store and filesystem stay consistent.
/// Default no-op — only meaningful when a policy and a role claim are present.
fn on_graph_created(&self, _role: &str, _path: &str) -> Result<(), String> {
Ok(())
}
}
24 changes: 22 additions & 2 deletions raphtory-graphql/src/data.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use crate::{
auth::ContextValidation,
auth_policy::{AuthorizationPolicy, GraphPermission, NamespacePermission},
auth_policy::{AuthorizationPolicy, GraphPermission},
config::app_config::AppConfig,
graph::GraphWithVectors,
model::{
blocking_io,
graph::{
filtering::{GraphAccessFilter, GraphRowFilter, HiddenKeys},
namespace::Namespace,
namespaced_item::NamespacedItem,
vectorised_graph::GqlVectorisedGraph,
},
},
Expand Down Expand Up @@ -229,6 +231,24 @@ impl Data {
}
}

/// Enumerates all descendants of a namespace as `(path, is_graph)` pairs.
/// `is_graph = true` means the path is a graph; `false` means a sub-namespace.
/// Returns an error if `ns_path` does not exist or is not a namespace.
pub fn enumerate_namespace_descendants(
&self,
ns_path: &str,
) -> Result<Vec<(String, bool)>, PathValidationError> {
let ns = Namespace::try_new(self.work_dir.clone(), ns_path.to_string())?;
let entries = ns
.get_all_children()
.map(|item| match item {
NamespacedItem::Namespace(n) => (n.local_path().to_string(), false),
NamespacedItem::MetaGraph(g) => (g.local_path().to_string(), true),
})
.collect();
Ok(entries)
}

/// # ⚠ Bypasses all permission checks — do not call from resolvers directly.
/// Use `get_graph_with_read_permission`, `get_raw_graph_with_read_permission`, or
/// `get_graph_with_write_permission` instead.
Expand Down Expand Up @@ -456,7 +476,7 @@ fn require_at_least_read(
"Access denied by auth policy"
);
let ns = parent_namespace(path);
if policy.namespace_permissions(ctx, ns) >= NamespacePermission::Introspect {
if policy.namespace_permissions(ctx, ns).is_some() {
Err(msg.into())
} else {
Err(PermissionError::GraphNotFound.into())
Expand Down
8 changes: 6 additions & 2 deletions raphtory-graphql/src/model/graph/namespace.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
auth_policy::{AuthorizationPolicy, NamespacePermission},
auth_policy::AuthorizationPolicy,
data::{get_relative_path, Data},
model::graph::{
collection::GqlCollection, meta_graph::MetaGraph, namespaced_item::NamespacedItem,
Expand Down Expand Up @@ -75,6 +75,10 @@ impl Namespace {
}
}

pub(crate) fn local_path(&self) -> &str {
&self.relative_path
}

pub fn try_new(root: PathBuf, relative_path: String) -> Result<Self, PathValidationError> {
let current_dir = ValidPath::try_new(root, relative_path.as_str())?;
Self::try_from_valid(current_dir, &relative_path)
Expand Down Expand Up @@ -157,7 +161,7 @@ fn is_namespace_visible(
n: &Namespace,
) -> bool {
policy.as_ref().map_or(true, |p| {
p.namespace_permissions(ctx, &n.relative_path) >= NamespacePermission::Discover
p.namespace_permissions(ctx, &n.relative_path).is_some()
})
}

Expand Down
47 changes: 45 additions & 2 deletions raphtory-graphql/src/model/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
auth::ContextValidation,
auth::{Access, ContextValidation},
auth_policy::{AuthorizationPolicy, NamespacePermission},
data::{parent_namespace, require_graph_write, Data, GqlGraphType, PermissionError},
graph::GraphWithVectors,
Expand Down Expand Up @@ -114,6 +114,29 @@ pub enum GqlGraphError {
FailedToCreateDir(String),
}

/// Auto-grants Write on `path` for the creator's role after a graph is created.
/// Returns an error if the grant fails so the caller can roll back the graph.
/// No-op for admin users (they have blanket write access via JWT and need no store entry),
/// when there is no active auth policy, or when the JWT carries no role claim.
fn auto_grant_on_create(
ctx: &Context<'_>,
policy: &Option<Arc<dyn AuthorizationPolicy>>,
path: &str,
) -> async_graphql::Result<()> {
let is_admin = ctx.data::<Access>().is_ok_and(|a| a == &Access::Rw);
if is_admin {
return Ok(());
}
if let Some(policy) = policy {
if let Some(role) = ctx.data::<Option<String>>().ok().and_then(|r| r.as_deref()) {
policy
.on_graph_created(role, path)
.map_err(async_graphql::Error::new)?;
}
}
Ok(())
}

fn require_namespace_write(
ctx: &Context<'_>,
policy: &Option<Arc<dyn AuthorizationPolicy>>,
Expand All @@ -124,7 +147,7 @@ fn require_namespace_write(
match policy {
None => ctx.require_jwt_write_access().map_err(Into::into),
Some(p) => {
if p.namespace_permissions(ctx, ns_path) < NamespacePermission::Write {
if p.namespace_permissions(ctx, ns_path) < Some(NamespacePermission::Write) {
return Err(PermissionError::NamespaceWriteRequired {
namespace: ns_path.to_string(),
graph: new_path.to_string(),
Expand Down Expand Up @@ -391,6 +414,10 @@ impl Mut {
};

data.insert_graph(folder, graph).await?;
if let Err(e) = auto_grant_on_create(ctx, &data.auth_policy, &path) {
let _ = data.delete_graph(&path).await;
return Err(e);
}

Ok(true)
}
Expand Down Expand Up @@ -439,6 +466,10 @@ impl Mut {
let src = data.get_raw_graph_with_read_permission(ctx, path).await?;
let folder = data.validate_path_for_insert(new_path, overwrite)?;
data.insert_graph(folder, src.graph).await?;
if let Err(e) = auto_grant_on_create(ctx, &data.auth_policy, new_path) {
let _ = data.delete_graph(new_path).await;
return Err(e);
}

Ok(true)
}
Expand All @@ -460,6 +491,10 @@ impl Mut {
let in_file = graph.value(ctx)?.content;
let folder = data.validate_path_for_insert(&path, overwrite)?;
data.insert_graph_as_bytes(folder, in_file).await?;
if let Err(e) = auto_grant_on_create(ctx, &data.auth_policy, &path) {
let _ = data.delete_graph(&path).await;
return Err(e);
}

Ok(path)
}
Expand Down Expand Up @@ -490,6 +525,10 @@ impl Mut {
})
.await?;
data.insert_graph(folder, g).await?;
if let Err(e) = auto_grant_on_create(ctx, &data.auth_policy, path) {
let _ = data.delete_graph(path).await;
return Err(e);
}
Ok(path.to_owned())
}

Expand Down Expand Up @@ -524,6 +563,10 @@ impl Mut {
.await?;

data.insert_graph(folder, new_subgraph).await?;
if let Err(e) = auto_grant_on_create(ctx, &data.auth_policy, &new_path) {
let _ = data.delete_graph(&new_path).await;
return Err(e);
}
Ok(new_path)
}

Expand Down
Loading