diff --git a/rebacs_core/src/lib.rs b/rebacs_core/src/lib.rs index cc08c32..3f3beff 100644 --- a/rebacs_core/src/lib.rs +++ b/rebacs_core/src/lib.rs @@ -1,5 +1,5 @@ use std::{ - borrow::Borrow, + borrow::{Borrow, Cow}, cmp::Ordering, collections::{BTreeSet, HashSet}, fmt::Debug, @@ -15,87 +15,183 @@ use tokio::{ #[cfg(test)] mod tests; -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct NodeId { - pub namespace: String, - pub id: String, - pub relation: Option, +const WILDCARD_ID: &str = "*"; +const WRITE_RELATION: &str = "grant"; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] +pub struct VertexId { + namespace: String, + id: String, + relation: Option, +} +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] +pub struct VertexIdRef<'a> { + namespace: &'a str, + id: &'a str, + relation: Option<&'a str>, } -pub struct Node { - pub id: NodeId, - pub edges_in: RwLock>>, - pub edges_out: RwLock>>, +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RObjectOrSet<'a> { + Object(Cow<'a, RObject>), + Set(Cow<'a, RSet>), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RSet(VertexId); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RObject(VertexId); + +struct Vertex { + id: VertexId, + edges_in: RwLock>>, + edges_out: RwLock>>, } #[derive(Default)] pub struct RelationGraph { - nodes: RwLock>>, + verticies: RwLock>>, +} + +trait VertexIdentifier { + fn namespace(&self) -> &str; + fn id(&self) -> &str; + fn relation(&self) -> Option<&str>; + fn vertex_id(&self) -> &VertexId; +} + +impl RObject { + pub fn namespace(&self) -> &str { + &self.0.namespace + } + + pub fn id(&self) -> &str { + &self.0.id + } + + pub fn relation(&self) -> Option<&str> { + None + } + fn vertex_id(&self) -> &VertexId { + &self.0 + } +} + +impl RSet { + pub fn namespace(&self) -> &str { + &self.0.namespace + } + + pub fn id(&self) -> &str { + &self.0.id + } + + pub fn relation(&self) -> &str { + self.0.relation.as_deref().unwrap_or("") + } + + fn vertex_id(&self) -> &VertexId { + &self.0 + } +} + +impl<'a> RObjectOrSet<'a> { + pub fn namespace(&self) -> &str { + match self { + Self::Object(obj) => obj.namespace(), + Self::Set(set) => set.namespace(), + } + } + pub fn id(&self) -> &str { + match self { + Self::Object(obj) => obj.id(), + Self::Set(set) => set.id(), + } + } + + pub fn relation(&self) -> Option<&str> { + match self { + Self::Object(_) => None, + Self::Set(set) => set.0.relation.as_deref(), + } + } + + fn vertex_id(&self) -> &VertexId { + match self { + Self::Object(obj) => obj.vertex_id(), + Self::Set(set) => set.vertex_id(), + } + } } impl RelationGraph { - pub async fn insert(&self, src: impl Into, dst: impl Into) { - let src = src.into(); - let dst = dst.into(); + pub async fn insert(&self, src: impl Into>, dst: &RSet) { + let src: RObjectOrSet<'_> = src.into(); + let mut verticies = self.verticies.write().await; - let mut nodes = self.nodes.write().await; - - let src_node = match nodes.get(&src) { - Some(node) => node.clone(), + let mut get_or_create = |vertex: &VertexId| match verticies.get(vertex) { + Some(vertex) => vertex.clone(), None => { - let node = Arc::new(Node { - id: src, - edges_out: RwLock::new(vec![]), - edges_in: RwLock::new(vec![]), + let vertex = Arc::new(Vertex { + id: vertex.clone(), + edges_out: RwLock::new(HashSet::new()), + edges_in: RwLock::new(HashSet::new()), }); - nodes.insert(node.clone()); - node + verticies.insert(vertex.clone()); + vertex } }; - let dst_node = match nodes.get(&dst).cloned() { - Some(node) => node.clone(), - None => { - let node = Arc::new(Node { - id: dst, - edges_out: RwLock::new(vec![]), - edges_in: RwLock::new(vec![]), - }); - nodes.insert(node.clone()); - node - } - }; - add_edge(src_node, dst_node).await; + + let src_without_relation = src.relation().is_none(); + + let src_wildcard: RObjectOrSet = (src.namespace(), WILDCARD_ID, src.relation()).into(); + let src_wildcard = get_or_create(src_wildcard.vertex_id()); + let src_vertex = get_or_create(src.vertex_id()); + + let dst_wildcard: RSet = (dst.namespace(), WILDCARD_ID, dst.relation()).into(); + let dst_wildcard = get_or_create(dst_wildcard.vertex_id()); + let dst_vertex = get_or_create(dst.vertex_id()); + + if src_without_relation && src_vertex.id.id != WILDCARD_ID { + add_edge(src_vertex.clone(), src_wildcard).await; + } else if !src_without_relation { + add_edge(src_wildcard, src_vertex.clone()).await; + } + + add_edge(dst_wildcard, dst_vertex.clone()).await; + add_edge(src_vertex, dst_vertex).await; } - pub async fn remove(&self, src: impl Into, dst: impl Into) { - let src = src.into(); - let dst = dst.into(); + pub async fn remove(&self, src: impl Into>, dst: &RSet) { + let src: RObjectOrSet<'_> = src.into(); + let mut verticies = self.verticies.write().await; - let mut nodes = self.nodes.write().await; - - let src = nodes.get(&src).cloned(); - let dst = nodes.get(&dst).cloned(); + let src = verticies.get(src.vertex_id()).cloned(); + let dst = verticies.get(dst.vertex_id()).cloned(); if let (Some(src), Some(dst)) = (src, dst) { src.edges_out.write().await.retain(|x| x != &dst); dst.edges_in.write().await.retain(|x| x != &src); if src.edges_in.read().await.is_empty() && src.edges_out.read().await.is_empty() { - nodes.remove(&src.id); + verticies.remove(&src.id); } if dst.edges_in.read().await.is_empty() && dst.edges_out.read().await.is_empty() { - nodes.remove(&dst.id); + verticies.remove(&dst.id); } } } - pub async fn has(&self, src: impl Into, dst: impl Into) -> bool { - let src = src.into(); - let dst = dst.into(); - + /// does a edge from src to dst exist + pub async fn has(&self, src: impl Into>, dst: &RSet) -> bool { + let src: RObjectOrSet<'_> = src.into(); let (src, dst) = { - let nodes = self.nodes.read().await; - (nodes.get(&src).cloned(), nodes.get(&dst).cloned()) + let verticies = self.verticies.read().await; + ( + verticies.get(src.vertex_id()).cloned(), + verticies.get(dst.vertex_id()).cloned(), + ) }; if let (Some(src), Some(dst)) = (src, dst) { @@ -106,50 +202,63 @@ impl RelationGraph { } /// checks if there is a path between src and dst using BFS - pub async fn has_recursive<'a>( + pub async fn check<'a>( &self, - src: impl Into, - dst: impl Into, + src: impl Into>, + dst: &RSet, limit: Option, ) -> bool { - let src = src.into(); - let dst = dst.into(); - - let src = if let Some(src) = self.nodes.read().await.get(&src) { - src.clone() - } else { - return false; - }; - + let src: RObjectOrSet<'_> = src.into(); let mut distance = 1; - let mut neighbors = src - .edges_out - .read() - .await - .iter() - .cloned() - .collect::>(); + let mut neighbors: Vec> = if let Some(src) = + self.verticies.read().await.get(src.vertex_id()) + { + src.edges_out.read().await.iter().cloned().collect() + } else { + let wildcard_src: RObject = (src.namespace(), WILDCARD_ID).into(); + if let Some(wildcard_src) = self.verticies.read().await.get(wildcard_src.vertex_id()) { + wildcard_src + .edges_out + .read() + .await + .iter() + .cloned() + .collect() + } else { + return false; + } + }; - let mut visited: HashSet> = HashSet::new(); + let mut visited: HashSet> = HashSet::new(); while !neighbors.is_empty() { + if let Some(limit) = limit { + if distance > limit { + return false; + } + } + let mut next_neighbors = vec![]; for neighbor in neighbors { if distance > 1 && visited.contains(&neighbor) { continue; } - if neighbor.id == dst { + + //check if the current vertex is the dst vertex or the wildcard vertex for the dst + //namespace. Without checking the wildcard vertex, not initialized dsts that should + //be affected by the wildcard wouldn't be found. + if &neighbor.id == dst + || (neighbor.id.namespace == dst.namespace() + && neighbor.id.id == WILDCARD_ID + && neighbor.id.relation.as_deref() == Some(dst.relation())) + { return true; } - if let Some(limit) = limit { - if distance > limit { - return false; - } - } - let mut node_neighbors = neighbor.edges_out.read().await.clone(); - next_neighbors.append(&mut node_neighbors); + let mut vertex_neighbors = + neighbor.edges_out.read().await.iter().cloned().collect(); + next_neighbors.append(&mut vertex_neighbors); visited.insert(neighbor); } @@ -159,11 +268,93 @@ impl RelationGraph { false } + pub async fn can_write( + &self, + src: impl Into>, + dst: &RSet, + limit: Option, + ) -> bool { + let mut dst = dst.clone(); + + dst.0.relation = Some(WRITE_RELATION.to_string()); + self.check(src, &dst, limit).await + } + + pub async fn expand(&self, dst: &RSet) -> Vec<(RObject, Vec)> { + let start_vertex = { + let verticies = self.verticies.read().await; + match verticies.get(dst.vertex_id()) { + Some(v) => v.clone(), + None => { + let wildcard_dst: RSet = (dst.namespace(), WILDCARD_ID, dst.relation()).into(); + + match verticies.get(wildcard_dst.vertex_id()) { + Some(v) => v.clone(), + None => return vec![], + } + } + } + }; + + let mut visited: HashSet> = HashSet::new(); + + let mut neighbors: Vec<(Arc, Vec>)> = start_vertex + .edges_in + .read() + .await + .iter() + .map(|v| (v.clone(), vec![start_vertex.clone()])) + .collect(); + + visited.insert(start_vertex); + + let mut expanded_verticies: Vec<(Arc, Vec>)> = vec![]; + + while !neighbors.is_empty() { + let mut next_neighbors = vec![]; + for (neighbor, mut neighbor_path) in neighbors { + if visited.contains(&neighbor) { + continue; + } + + if neighbor.id.relation.is_none() { + expanded_verticies.push((neighbor, neighbor_path)); + continue; + } + + neighbor_path.push(neighbor.clone()); + + next_neighbors.append( + &mut neighbor + .edges_in + .read() + .await + .iter() + .map(|v| (v.clone(), neighbor_path.clone())) + .collect(), + ); + + visited.insert(neighbor); + } + neighbors = next_neighbors; + } + + expanded_verticies + .into_iter() + .map(|(v, path)| { + ( + RObject(v.id.clone()), + path.into_iter().map(|w| RSet(w.id.clone())).collect(), + ) + }) + .collect() + } + pub async fn write_savefile(&self, writeable: &mut (impl AsyncWriteExt + Unpin)) { let mut current: (String, String) = (String::new(), String::new()); - for node in self.nodes.read().await.iter() { - if current != (node.id.namespace.clone(), node.id.id.clone()) { - current = (node.id.namespace.clone(), node.id.id.clone()); + for vertex in self.verticies.read().await.iter() { + if current != (vertex.id.namespace.clone(), vertex.id.id.clone()) { + current = (vertex.id.namespace.clone(), vertex.id.id.clone()); writeable.write_all("\n".as_bytes()).await.unwrap(); writeable .write_all(format!("[{}:{}]\n", ¤t.0, ¤t.1).as_bytes()) @@ -171,24 +362,29 @@ impl RelationGraph { .unwrap(); } - let srcs = node + let srcs = vertex .edges_in .read() .await .iter() + .filter(|x| x.id.id != WILDCARD_ID) .map(|src| { - if src.id.namespace == current.0 && src.id.id == current.1 { + let obj = if src.id.namespace == current.0 && src.id.id == current.1 { "self".to_string() - } else if let Some(rel) = &src.id.relation { - format!("{}:{}#{}", &src.id.namespace, &src.id.id, &rel) } else { format!("{}:{}", &src.id.namespace, &src.id.id) + }; + + if let Some(rel) = &src.id.relation { + format!("{}#{}", &obj, &rel) + } else { + obj } }) .reduce(|acc, x| acc + ", " + &x) .unwrap_or_default(); - if let Some(rel) = &node.id.relation { + if let Some(rel) = &vertex.id.relation { writeable .write_all(format!("{} = [ {} ]\n", &rel, &srcs).as_bytes()) .await @@ -199,15 +395,15 @@ impl RelationGraph { pub async fn read_savefile(readable: &mut (impl AsyncBufReadExt + Unpin)) -> Self { let mut lines = readable.lines(); let graph = Self::default(); - let mut node: Option<(String, String)> = None; + let mut vertex: Option<(String, String)> = None; while let Ok(Some(line)) = lines.next_line().await { if line.starts_with('[') && line.ends_with(']') { let line = &mut line[1..line.len() - 1].split(':'); let namespace = line.next().unwrap(); let id = line.next().unwrap(); - node = Some((namespace.to_string(), id.to_string())); + vertex = Some((namespace.to_string(), id.to_string())); } else if line.contains('=') && line.contains('[') && line.contains(']') { - if let Some(dst) = &node { + if let Some(dst) = &vertex { let equals_pos = line.find('=').unwrap(); let arr_start = line.find('[').unwrap(); let arr_stop = line.find(']').unwrap(); @@ -216,7 +412,7 @@ impl RelationGraph { let arr = line[arr_start + 1..arr_stop].trim().split(", "); for obj in arr { - let src: NodeId = if obj.contains('#') { + let src: RObjectOrSet = if obj.contains('#') { let sep_1 = obj.find(':'); let sep_2 = obj.find('#').unwrap(); @@ -228,7 +424,7 @@ impl RelationGraph { let rel = &obj[sep_2 + 1..]; - (namespace, id, rel).into() + RObjectOrSet::Set(Cow::Owned((namespace, id, rel).into())) } else { let sep_1 = obj.find(':'); @@ -237,11 +433,11 @@ impl RelationGraph { } else { (dst.0.as_str(), dst.1.as_str()) }; - (namespace, id).into() + RObjectOrSet::Object(Cow::Owned((namespace, id).into())) }; graph - .insert(src, (dst.0.as_str(), dst.1.as_str(), rel)) + .insert(src, &(dst.0.as_str(), dst.1.as_str(), rel).into()) .await; } } @@ -251,103 +447,159 @@ impl RelationGraph { } } -impl Debug for Node { +impl Debug for Vertex { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Node").field("id", &self.id).finish() + f.debug_struct("vertex").field("id", &self.id).finish() } } -async fn add_edge(from: Arc, to: Arc) { - from.edges_out.write().await.push(to.clone()); - to.edges_in.write().await.push(from); +async fn add_edge(from: Arc, to: Arc) { + if !from.edges_out.read().await.contains(&to) { + from.edges_out.write().await.insert(to.clone()); + } + if !to.edges_in.read().await.contains(&from) { + to.edges_in.write().await.insert(from); + } } -impl Borrow for Arc { - fn borrow(&self) -> &NodeId { +impl Borrow for Arc { + fn borrow(&self) -> &VertexId { &self.id } } -impl PartialEq for Node { +impl PartialEq for Vertex { fn eq(&self, other: &Self) -> bool { self.id == other.id } } -impl Eq for Node {} +impl Eq for Vertex {} -impl PartialOrd for Node { +impl PartialOrd for Vertex { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Ord for Node { +impl Ord for Vertex { fn cmp(&self, other: &Self) -> Ordering { self.id.cmp(&other.id) } } -impl Hash for Node { +impl Hash for Vertex { fn hash(&self, state: &mut H) { self.id.hash(state); } } -impl From<(&str, &str)> for NodeId { +impl From<(&str, &str)> for RObject { fn from(value: (&str, &str)) -> Self { - Self { + Self(VertexId { namespace: value.0.to_string(), id: value.1.to_string(), relation: None, - } + }) } } -impl From<(&str, &str, &str)> for NodeId { +impl From<(String, String)> for RObject { + fn from(value: (String, String)) -> Self { + Self(VertexId { + namespace: value.0, + id: value.1, + relation: None, + }) + } +} + +impl From<(&str, &str, &str)> for RSet { fn from(value: (&str, &str, &str)) -> Self { - Self { + Self(VertexId { namespace: value.0.to_string(), id: value.1.to_string(), relation: Some(value.2.to_string()), - } + }) } } -impl From<(&str, &str, Option<&str>)> for NodeId { - fn from(value: (&str, &str, Option<&str>)) -> Self { - Self { - namespace: value.0.to_string(), - id: value.1.to_string(), - relation: value.2.map(|x| x.to_string()), - } - } -} - -impl From<(String, String)> for NodeId { - fn from(value: (String, String)) -> Self { - Self { - namespace: value.0, - id: value.1, - relation: None, - } - } -} - -impl From<(String, String, String)> for NodeId { +impl From<(String, String, String)> for RSet { fn from(value: (String, String, String)) -> Self { - Self { + Self(VertexId { namespace: value.0, id: value.1, relation: Some(value.2), + }) + } +} + +impl From<(&str, &str, Option<&str>)> for RObjectOrSet<'_> { + fn from(value: (&str, &str, Option<&str>)) -> Self { + match value.2 { + Some(r) => Self::Set(Cow::Owned((value.0, value.1, r).into())), + None => Self::Object(Cow::Owned((value.0, value.1).into())), } } } -impl From<(String, String, Option)> for NodeId { +impl From<(String, String, Option)> for RObjectOrSet<'_> { fn from(value: (String, String, Option)) -> Self { - Self { - namespace: value.0, - id: value.1, - relation: value.2, + match value.2 { + Some(r) => Self::Set(Cow::Owned((value.0, value.1, r).into())), + None => Self::Object(Cow::Owned((value.0, value.1).into())), + } + } +} + +impl Borrow for RSet { + fn borrow(&self) -> &VertexId { + &self.0 + } +} + +impl PartialEq for RSet { + fn eq(&self, other: &VertexId) -> bool { + self.0.eq(other) + } +} +impl PartialEq for VertexId { + fn eq(&self, other: &RSet) -> bool { + self.eq(&other.0) + } +} + +impl Borrow for RObject { + fn borrow(&self) -> &VertexId { + &self.0 + } +} + +impl From for RObjectOrSet<'_> { + fn from(value: RSet) -> Self { + Self::Set(Cow::Owned(value)) + } +} +impl From for RObjectOrSet<'_> { + fn from(value: RObject) -> Self { + Self::Object(Cow::Owned(value)) + } +} + +impl<'a> From<&'a RSet> for RObjectOrSet<'a> { + fn from(value: &'a RSet) -> Self { + Self::Set(Cow::Borrowed(value)) + } +} +impl<'a> From<&'a RObject> for RObjectOrSet<'a> { + fn from(value: &'a RObject) -> Self { + Self::Object(Cow::Borrowed(value)) + } +} + +impl<'a> From<&'a RObjectOrSet<'a>> for RObjectOrSet<'a> { + fn from(value: &'a RObjectOrSet<'a>) -> Self { + match value { + Self::Object(obj) => Self::Object(Cow::Borrowed(obj.borrow())), + Self::Set(set) => Self::Set(Cow::Borrowed(set.borrow())), } } } diff --git a/rebacs_core/src/tests.rs b/rebacs_core/src/tests.rs index 06af760..eadfed9 100644 --- a/rebacs_core/src/tests.rs +++ b/rebacs_core/src/tests.rs @@ -1,52 +1,71 @@ -//hello world - -use crate::{Distanced, NodeId, RelationGraph}; - -#[test] -fn distanced_ordering() { - let a = Distanced::new((), 0); - let b = Distanced::one(()); - let c = Distanced::new((), 1); - let d = Distanced::new((), 2); - - assert!(a < b); - assert!(b == c); - assert!(c < d); - assert!(a < d); -} +use crate::{RObject, RSet, RelationGraph, WILDCARD_ID}; #[tokio::test] async fn simple_graph() { let graph = RelationGraph::default(); - let alice = ("user", "alice"); - let bob = ("user", "bob"); - let charlie = ("user", "charlie"); + let alice: RObject = ("user", "alice").into(); + let bob: RObject = ("user", "bob").into(); + let charlie: RObject = ("user", "charlie").into(); - let foo_read = ("application", "foo", "read"); - let bar_read = ("application", "bar", "read"); + let foo_read: RSet = ("application", "foo", "read").into(); + let bar_read: RSet = ("application", "bar", "read").into(); - graph.insert(alice, foo_read).await; - graph.insert(bob, bar_read).await; + graph.insert(&alice, &foo_read).await; + graph.insert(&bob, &bar_read).await; - assert!(graph.has_recursive(alice, foo_read, None).await); - assert!(!graph.has_recursive(alice, bar_read, None).await); + assert!(graph.check(&alice, &foo_read, None).await); + assert!(!graph.check(&alice, &bar_read, None).await); - assert!(!graph.has_recursive(bob, foo_read, None).await); - assert!(graph.has_recursive(bob, bar_read, None).await); + assert!(!graph.check(&bob, &foo_read, None).await); + assert!(graph.check(&bob, &bar_read, None).await); - assert!(!graph.has_recursive(charlie, foo_read, None).await); - assert!(!graph.has_recursive(charlie, bar_read, None).await); + assert!(!graph.check(&charlie, &foo_read, None).await); + assert!(!graph.check(&charlie, &bar_read, None).await); - graph.remove(alice, foo_read).await; - graph.remove(alice, bar_read).await; + graph.remove(&alice, &foo_read).await; + graph.remove(&alice, &bar_read).await; - assert!(!graph.has_recursive(alice, foo_read, None).await); - assert!(!graph.has_recursive(alice, bar_read, None).await); + assert!(!graph.check(&alice, &foo_read, None).await); + assert!(!graph.check(&alice, &bar_read, None).await); - graph.insert(charlie, foo_read).await; - graph.insert(charlie, bar_read).await; + graph.insert(&charlie, &foo_read).await; + graph.insert(&charlie, &bar_read).await; - assert!(graph.has_recursive(charlie, foo_read, None).await); - assert!(graph.has_recursive(charlie, bar_read, None).await); + assert!(graph.check(&charlie, &foo_read, None).await); + assert!(graph.check(&charlie, &bar_read, None).await); +} + +#[tokio::test] +async fn wildcard() { + let graph = RelationGraph::default(); + + let alice: RObject = ("user", "alice").into(); + let bob: RObject = ("user", "bob").into(); + let charlie: RObject = ("user", "charlie").into(); + + let user_wildcard: RObject = ("user", WILDCARD_ID).into(); + + let foo_read: RSet = ("application", "foo", "read").into(); + let bar_read: RSet = ("application", "bar", "read").into(); + + let app_read: RSet = ("application", WILDCARD_ID, "read").into(); + + let some_app_read: RSet = ("application", "bla", "read").into(); + + graph.insert(&alice, &foo_read).await; + graph.insert(&user_wildcard, &foo_read).await; + graph.insert(&bob, &bar_read).await; + + assert!(graph.check(&alice, &foo_read, None).await); + assert!(graph.check(&bob, &foo_read, None).await); + assert!(graph.check(&charlie, &foo_read, None).await); + assert!(graph.check(&bob, &bar_read, None).await); + + graph.insert(&alice, &app_read).await; + + assert!(graph.check(&alice, &some_app_read, None).await); + assert!(graph.check(&alice, &bar_read, None).await); + assert!(!graph.check(&bob, &some_app_read, None).await); + assert!(!graph.check(&charlie, &some_app_read, None).await); } diff --git a/rebacs_server/proto/rebacs.proto b/rebacs_server/proto/rebacs.proto index 2e4276d..716d5cb 100644 --- a/rebacs_server/proto/rebacs.proto +++ b/rebacs_server/proto/rebacs.proto @@ -6,40 +6,71 @@ service RebacService { rpc Revoke(RevokeReq) returns (RevokeRes); rpc Exists(ExistsReq) returns (ExistsRes); rpc IsPermitted(IsPermittedReq) returns (IsPermittedRes); + rpc Expand(ExpandReq) returns (ExpandRes); } message GrantReq{ - Object src = 1; - Object dst = 2; + oneof src { + Object src_obj = 1; + Set src_set = 2; + } + Set dst = 3; } message GrantRes{} message RevokeReq{ - Object src = 1; - Object dst = 2; + oneof src { + Object src_obj = 1; + Set src_set = 2; + } + Set dst = 3; } message RevokeRes{} message ExistsReq{ - Object src = 1; - Object dst = 2; + oneof src { + Object src_obj = 1; + Set src_set = 2; + } + Set dst = 3; } message ExistsRes{ bool exists = 1; } message IsPermittedReq{ - Object src = 1; - Object dst = 2; + oneof src { + Object src_obj = 1; + Set src_set = 2; + } + Set dst = 3; } message IsPermittedRes{ bool permitted = 1; } +message ExpandReq { + Set dst = 1; +} + +message ExpandRes { + repeated ExpandResItem expanded = 1; +} + +message ExpandResItem { + Object src = 1; + repeated Set path = 2; +} + message Object{ string namespace = 1; string id = 2; - optional string relation = 3; +} + +message Set{ + string namespace = 1; + string id = 2; + string relation = 3; } diff --git a/rebacs_server/src/grpc_service.rs b/rebacs_server/src/grpc_service.rs index 28ac38b..ebf09c3 100644 --- a/rebacs_server/src/grpc_service.rs +++ b/rebacs_server/src/grpc_service.rs @@ -2,16 +2,16 @@ use std::sync::Arc; use jsonwebtoken::{decode, DecodingKey, TokenData, Validation}; use log::info; -use rebacs_core::{NodeId, RelationGraph}; +use rebacs_core::{RObject, RObjectOrSet, RSet, RelationGraph}; use serde::Deserialize; use tokio::sync::mpsc::Sender; use tonic::metadata::MetadataMap; use tonic::{Request, Response, Status}; -use crate::rebacs_proto::Object; use crate::rebacs_proto::{ - rebac_service_server, ExistsReq, ExistsRes, GrantReq, GrantRes, IsPermittedReq, IsPermittedRes, - RevokeReq, RevokeRes, + exists_req, grant_req, is_permitted_req, rebac_service_server, revoke_req, ExistsReq, + ExistsRes, ExpandReq, ExpandRes, ExpandResItem, GrantReq, GrantRes, IsPermittedReq, + IsPermittedRes, Object, RevokeReq, RevokeRes, Set, }; #[derive(Clone)] @@ -22,37 +22,63 @@ pub struct RebacService { pub save_trigger: Sender<()>, } -const NAMESPACE_NS: &str = "namespace"; const USER_NS: &str = "user"; -const GRANT_RELATION: &str = "grant"; -const REVOKE_RELATION: &str = "revoke"; + +macro_rules! extract { + ($type:ident::Src($src:expr)) => {{ + let src = $src + .as_ref() + .ok_or(Status::invalid_argument("src must be set"))?; + let src: RObjectOrSet = match src { + $type::Src::SrcObj(obj) => (obj.namespace.clone(), obj.id.clone(), None).into(), + $type::Src::SrcSet(set) => ( + set.namespace.clone(), + set.id.clone(), + Some(set.relation.clone()), + ) + .into(), + }; + + if src.namespace().is_empty() && src.id().is_empty() { + None + } else if src.namespace().is_empty() { + return Err(Status::invalid_argument("src.namespace must be set")); + } else if src.id().is_empty() { + return Err(Status::invalid_argument("src.id must be set")); + } else { + Some(src) + } + }}; +} #[tonic::async_trait] impl rebac_service_server::RebacService for RebacService { async fn grant(&self, request: Request) -> Result, Status> { let token = extract_token(request.metadata(), &self.oidc_pubkey, &self.oidc_validation).await?; + let user: RObject = (USER_NS, token.claims.sub.as_str()).into(); - let (src, dst) = extract_src_dst(&request.get_ref().src, &request.get_ref().dst)?; + let src = extract!(grant_req::Src(request.get_ref().src)) + .unwrap_or(("user", token.claims.sub.as_str(), None).into()); + let dst = extract_dst(request.get_ref().dst.as_ref())?; - if !is_permitted(&token, &dst, GRANT_RELATION, &self.graph).await { + if !self.graph.can_write(&user, &dst, None).await { return Err(Status::permission_denied( "token not permitted to grant permissions on dst", )); } - info!( "created relation {}:{}#{}@{}:{}#{} for {}", - dst.namespace, - dst.id, - dst.relation.clone().unwrap_or_default(), - src.namespace, - src.id, - src.relation.clone().unwrap_or_default(), + dst.namespace(), + dst.id(), + dst.relation(), + src.namespace(), + src.id(), + src.relation().map(|x| x.to_string()).unwrap_or_default(), token.claims.sub ); - self.graph.insert(src, dst).await; + self.graph.insert(src, &dst).await; self.save_trigger.send(()).await.unwrap(); @@ -61,34 +87,28 @@ impl rebac_service_server::RebacService for RebacService { async fn revoke(&self, request: Request) -> Result, Status> { let token = extract_token(request.metadata(), &self.oidc_pubkey, &self.oidc_validation).await?; + let user: RObject = (USER_NS, token.claims.sub.as_str()).into(); - let (src, dst) = extract_src_dst(&request.get_ref().src, &request.get_ref().dst)?; + let src = extract!(revoke_req::Src(request.get_ref().src)) + .unwrap_or(("user", token.claims.sub.as_str(), None).into()); + let dst = extract_dst(request.get_ref().dst.as_ref())?; - if !is_permitted(&token, &dst, REVOKE_RELATION, &self.graph).await { + if !self.graph.can_write(&user, &dst, None).await { return Err(Status::permission_denied( "token not permitted to revoke permissions on dst", )); } - self.graph - .remove( - ( - src.namespace.to_string(), - src.id.to_string(), - src.relation.clone(), - ), - (dst.namespace.clone(), dst.id.clone(), dst.relation.clone()), - ) - .await; + self.graph.remove(&src, &dst).await; info!( "delted relation {}:{}#{}@{}:{}#{} for {}", - dst.namespace, - dst.id, - dst.relation.clone().unwrap_or_default(), - src.namespace, - src.id, - src.relation.clone().unwrap_or_default(), + dst.namespace(), + dst.id(), + dst.relation(), + src.namespace(), + src.id(), + src.relation().map(|x| x.to_string()).unwrap_or_default(), token.claims.sub ); @@ -100,9 +120,11 @@ impl rebac_service_server::RebacService for RebacService { let token = extract_token(request.metadata(), &self.oidc_pubkey, &self.oidc_validation).await?; - let (src, dst) = extract_src_dst(&request.get_ref().src, &request.get_ref().dst)?; + let src = extract!(exists_req::Src(request.get_ref().src)) + .unwrap_or(("user", token.claims.sub.as_str(), None).into()); + let dst = extract_dst(request.get_ref().dst.as_ref())?; - let exists = self.graph.has(src, dst).await; + let exists = self.graph.has(src, &dst).await; Ok(Response::new(ExistsRes { exists })) } @@ -114,12 +136,50 @@ impl rebac_service_server::RebacService for RebacService { let token = extract_token(request.metadata(), &self.oidc_pubkey, &self.oidc_validation).await?; - let (src, dst) = extract_src_dst(&request.get_ref().src, &request.get_ref().dst)?; + let src = extract!(is_permitted_req::Src(request.get_ref().src)) + .unwrap_or(("user", token.claims.sub.as_str(), None).into()); + let dst = extract_dst(request.get_ref().dst.as_ref())?; - let permitted = self.graph.has_recursive(src, dst, None).await; + let permitted = self.graph.check(src, &dst, None).await; Ok(Response::new(IsPermittedRes { permitted })) } + + async fn expand(&self, request: Request) -> Result, Status> { + let token = + extract_token(request.metadata(), &self.oidc_pubkey, &self.oidc_validation).await?; + let dst = extract_dst(request.get_ref().dst.as_ref())?; + + let user: RObject = (USER_NS, token.claims.sub.as_str()).into(); + if !self.graph.can_write(&user, &dst, None).await { + return Err(Status::permission_denied( + "token not permitted to expand permissions on dst", + )); + } + + let expanded = self + .graph + .expand(&dst) + .await + .into_iter() + .map(|(v, path)| ExpandResItem { + src: Some(Object { + namespace: v.namespace().to_string(), + id: v.id().to_string(), + }), + path: path + .into_iter() + .map(|w| Set { + namespace: w.namespace().to_string(), + id: w.id().to_string(), + relation: w.relation().to_string(), + }) + .collect(), + }) + .collect(); + + Ok(Response::new(ExpandRes { expanded })) + } } #[derive(Debug, Clone, Deserialize)] @@ -154,54 +214,18 @@ async fn extract_token( Ok(token) } -async fn is_permitted( - token: &TokenData, - dst: &NodeId, - relation: &str, - graph: &RelationGraph, -) -> bool { - let s1 = graph - .has_recursive( - (USER_NS, token.claims.sub.as_str()), - (dst.namespace.as_str(), dst.id.as_str(), relation), - None, - ) - .await; - - let s2 = graph - .has_recursive( - (USER_NS, token.claims.sub.as_str()), - (NAMESPACE_NS, dst.namespace.as_str(), relation), - None, - ) - .await; - - s1 || s2 -} - -fn extract_src_dst(src: &Option, dst: &Option) -> Result<(NodeId, NodeId), Status> { - let src = src - .as_ref() - .ok_or(Status::invalid_argument("src must be set"))?; - let src: NodeId = (src.namespace.clone(), src.id.clone(), src.relation.clone()).into(); +fn extract_dst(dst: Option<&Set>) -> Result { let dst = dst .as_ref() .ok_or(Status::invalid_argument("dst must be set"))?; - let dst: NodeId = (dst.namespace.clone(), dst.id.clone(), dst.relation.clone()).into(); + let dst: RSet = (dst.namespace.clone(), dst.id.clone(), dst.relation.clone()).into(); - if dst.namespace.is_empty() { + if dst.namespace().is_empty() { return Err(Status::invalid_argument("dst.namespace must be set")); } - if dst.id.is_empty() { + if dst.id().is_empty() { return Err(Status::invalid_argument("dst.id must be set")); } - if src.namespace.is_empty() { - return Err(Status::invalid_argument("src.namespace must be set")); - } - if src.id.is_empty() { - return Err(Status::invalid_argument("src.id must be set")); - } - - Ok((src, dst)) + Ok(dst) } diff --git a/rebacs_server/src/main.rs b/rebacs_server/src/main.rs index 4fc1c84..4f5aade 100644 --- a/rebacs_server/src/main.rs +++ b/rebacs_server/src/main.rs @@ -16,7 +16,10 @@ use tokio::{ use tonic::transport::Server; pub mod grpc_service; -pub mod rebacs_proto; +pub mod rebacs_proto { + + tonic::include_proto!("eu.zettoit.rebacs"); +} use crate::rebacs_proto::rebac_service_server; diff --git a/rebacs_server/src/rebacs_proto.rs b/rebacs_server/src/rebacs_proto.rs deleted file mode 100644 index 5c61ec7..0000000 --- a/rebacs_server/src/rebacs_proto.rs +++ /dev/null @@ -1 +0,0 @@ -tonic::include_proto!("eu.zettoit.rebacs");