diff --git a/.gitignore b/.gitignore index f82c255..ad6bc70 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .env graph.dat graph.dat.bak +api_keys.dat diff --git a/Cargo.lock b/Cargo.lock index 9424323..4985a44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,6 +124,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.12.1" @@ -148,6 +157,35 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cpufeatures" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -254,6 +292,16 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.9" @@ -320,6 +368,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "0.2.9" @@ -884,6 +938,17 @@ dependencies = [ "syn 2.0.15", ] +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -979,10 +1044,12 @@ name = "themis" version = "0.1.0" dependencies = [ "dotenvy", + "hex", "log", "pretty_env_logger", "prost", "serde", + "sha2", "tokio", "tonic", "tonic-build", @@ -1177,6 +1244,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + [[package]] name = "unicode-ident" version = "1.0.8" @@ -1189,6 +1262,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "want" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index f9e594e..dd329bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,8 @@ pretty_env_logger = "0.4.0" dotenvy = "0.15.7" tonic = { version="0.9.2", features=["tls"] } prost = "0.11.9" +sha2 = "0.10.6" +hex = "0.4.3" [build-dependencies] tonic-build = "0.9.2" diff --git a/src/graph.rs b/src/graph.rs index 2170e77..6798684 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -251,13 +251,20 @@ impl Graph { let arr = arr .iter() .filter_map(|x| { - let obj_ref = x.object_ref(); - self.nodes.get_by_b(obj_ref).map(|obj| { - let (namespace, id) = (&obj.namespace, &obj.id); + let rel_obj_ref = x.object_ref(); + self.nodes.get_by_b(rel_obj_ref).map(|rel_obj| { + let (namespace, id) = (&rel_obj.namespace, &rel_obj.id); - match x.relation() { - None => format!("{}:{}", &namespace, &id), - Some(rel) => format!("{}:{}#{}", &namespace, &id, &rel.0), + if *namespace == obj.namespace && *id == obj.id { + match x.relation() { + None => "self".to_string(), + Some(rel) => format!("self#{}", &rel.0), + } + } else { + match x.relation() { + None => format!("{}:{}", &namespace, &id), + Some(rel) => format!("{}:{}#{}", &namespace, &id, &rel.0), + } } }) }) @@ -277,16 +284,17 @@ impl Graph { let mut lines = reader.lines(); let mut graph = Graph::default(); - let mut node: Option = None; + let mut node: Option<(ObjectRef, String, String)> = None; let mut relations = vec![]; 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 obj_ref = - graph.add_node(Object::new(line.next().unwrap(), line.next().unwrap())); - node = Some(obj_ref); + let namespace = line.next().unwrap(); + let id = line.next().unwrap(); + let obj_ref = graph.add_node(Object::new(namespace, id)); + node = Some((obj_ref, namespace.to_string(), id.to_string())); } else if line.contains('=') && line.contains('[') && line.contains(']') { - if let Some(dst) = node { + if let Some(dst) = &node { let equals_pos = line.find('=').unwrap(); let arr_start = line.find('[').unwrap(); let arr_stop = line.find(']').unwrap(); @@ -296,20 +304,26 @@ impl Graph { for obj in arr { let (src_namespace, src_id, src_rel) = if obj.contains('#') { - let sep_1 = obj.find(':').unwrap(); + let sep_1 = obj.find(':'); let sep_2 = obj.find('#').unwrap(); - let namespace = &obj[..sep_1]; - let id = &obj[sep_1 + 1..sep_2]; + let (namespace, id) = if let Some(sep_1) = sep_1 { + (&obj[..sep_1], &obj[sep_1 + 1..sep_2]) + } else { + (dst.1.as_str(), dst.2.as_str()) + }; + let rel = &obj[sep_2 + 1..]; (namespace, id, Some(rel)) } else { - let sep_1 = obj.find(':').unwrap(); - - let namespace = &obj[..sep_1]; - let id = &obj[sep_1 + 1..]; + let sep_1 = obj.find(':'); + let (namespace, id) = if let Some(sep_1) = sep_1 { + (&obj[..sep_1], &obj[sep_1 + 1..]) + } else { + (dst.1.as_str(), dst.2.as_str()) + }; (namespace, id, None) }; @@ -317,7 +331,7 @@ impl Graph { src_namespace.to_string(), src_id.to_string(), src_rel.map(String::from), - dst, + dst.0, rel.to_string(), )); } diff --git a/src/grpc_service.rs b/src/grpc_service.rs index 04f5bf0..6e8ed88 100644 --- a/src/grpc_service.rs +++ b/src/grpc_service.rs @@ -1,8 +1,11 @@ +use std::collections::HashMap; use std::sync::Arc; use log::info; +use sha2::{Digest, Sha256}; use tokio::sync::mpsc::Sender; use tokio::sync::Mutex; +use tonic::metadata::MetadataMap; use tonic::{Request, Response, Status}; use crate::graph::{self, Graph, ObjectRelation}; @@ -14,6 +17,7 @@ use crate::themis_proto::{ #[derive(Clone)] pub struct GraphService { + pub api_keys: Arc>>, pub graph: Arc>, pub save_trigger: Sender<()>, } @@ -23,6 +27,15 @@ impl ObjectService for GraphService { async fn create(&self, request: Request) -> Result, Status> { let mut graph = self.graph.lock().await; + authenticate( + request.metadata(), + &graph, + &self.api_keys, + &request.get_ref().namespace, + "write", + ) + .await?; + if request.get_ref().namespace.is_empty() || request.get_ref().id.is_empty() { return Err(Status::invalid_argument("namespace and id must be set")); } @@ -45,6 +58,15 @@ impl ObjectService for GraphService { async fn delete(&self, request: Request) -> Result, Status> { let mut graph = self.graph.lock().await; + authenticate( + request.metadata(), + &graph, + &self.api_keys, + &request.get_ref().namespace, + "write", + ) + .await?; + if request.get_ref().namespace.is_empty() || request.get_ref().id.is_empty() { return Err(Status::invalid_argument("namespace and id must be set")); } @@ -67,6 +89,15 @@ impl ObjectService for GraphService { async fn exists(&self, request: Request) -> Result, Status> { let graph = self.graph.lock().await; + authenticate( + request.metadata(), + &graph, + &self.api_keys, + &request.get_ref().namespace, + "read", + ) + .await?; + if request.get_ref().namespace.is_empty() || request.get_ref().id.is_empty() { return Err(Status::invalid_argument("namespace and id must be set")); } @@ -84,7 +115,16 @@ impl RelationService for GraphService { async fn create(&self, request: Request) -> Result, Status> { let mut graph = self.graph.lock().await; - let (src, dst) = transform_relation(request.get_ref(), &graph)?; + let (src, dst, dst_namespace) = transform_relation(request.get_ref(), &graph)?; + + authenticate( + request.metadata(), + &graph, + &self.api_keys, + &dst_namespace, + "write", + ) + .await?; graph.add_relation(src, dst); @@ -97,7 +137,16 @@ impl RelationService for GraphService { async fn delete(&self, request: Request) -> Result, Status> { let mut graph = self.graph.lock().await; - let (src, dst) = transform_relation(request.get_ref(), &graph)?; + let (src, dst, dst_namespace) = transform_relation(request.get_ref(), &graph)?; + + authenticate( + request.metadata(), + &graph, + &self.api_keys, + &dst_namespace, + "write", + ) + .await?; graph.remove_relation(src, dst); @@ -110,7 +159,16 @@ impl RelationService for GraphService { async fn exists(&self, request: Request) -> Result, Status> { let graph = self.graph.lock().await; - let (src, dst) = transform_relation(request.get_ref(), &graph)?; + let (src, dst, dst_namespace) = transform_relation(request.get_ref(), &graph)?; + + authenticate( + request.metadata(), + &graph, + &self.api_keys, + &dst_namespace, + "read", + ) + .await?; let exists = graph.has_relation(src, dst); @@ -126,7 +184,7 @@ impl QueryService for GraphService { ) -> Result, Status> { let graph = self.graph.lock().await; - let related = if let Ok((src, dst)) = transform_relation(request.get_ref(), &graph) { + let related = if let Ok((src, dst, _)) = transform_relation(request.get_ref(), &graph) { graph.is_related_to(src, dst) } else { false @@ -140,9 +198,19 @@ impl QueryService for GraphService { ) -> Result, Status> { let graph = self.graph.lock().await; + authenticate( + request.metadata(), + &graph, + &self.api_keys, + &request.get_ref().namespace, + "read", + ) + .await?; + let obj = graph .get_node(&request.get_ref().namespace, &request.get_ref().id) .ok_or(Status::not_found("object not found"))?; + let rel = graph::Relation::new(&request.get_ref().relation); Ok(Response::new(GetRelatedToResponse { @@ -174,6 +242,16 @@ impl QueryService for GraphService { .object .as_ref() .ok_or(Status::invalid_argument("object must be set"))?; + + authenticate( + request.metadata(), + &graph, + &self.api_keys, + &obj.namespace, + "read", + ) + .await?; + let obj = graph .get_node(&obj.namespace, &obj.id) .ok_or(Status::not_found("object not found"))?; @@ -200,7 +278,7 @@ impl QueryService for GraphService { fn transform_relation( rel: &Relation, graph: &Graph, -) -> Result<(graph::ObjectOrSet, graph::ObjectRelation), Status> { +) -> Result<(graph::ObjectOrSet, graph::ObjectRelation, String), Status> { let src = match rel .src .as_ref() @@ -223,10 +301,46 @@ fn transform_relation( .dst .as_ref() .ok_or(Status::invalid_argument("dst must be set"))?; + let dst_namespace = dst.namespace.to_string(); let dst = graph .get_node(&dst.namespace, &dst.id) .ok_or(Status::not_found("dst object could not be found"))?; let dst = ObjectRelation(dst, graph::Relation::new(&rel.relation)); - Ok((src, dst)) + Ok((src, dst, dst_namespace)) +} + +async fn authenticate( + metadata: &MetadataMap, + graph: &Graph, + api_keys: &Arc>>, + namespace: &str, + relation: &str, +) -> Result<(), Status> { + let api_key = metadata + .get("x-api-key") + .map(|x| x.to_str().unwrap()) + .ok_or(Status::unauthenticated("x-api-key required"))?; + + let mut hasher = Sha256::new(); + hasher.update(api_key); + let api_key = hex::encode(hasher.finalize()); + let api_keys = api_keys.lock().await; + let api_key = api_keys + .get(&api_key) + .ok_or(Status::unauthenticated("api-key invalid"))?; + + let api_key = graph + .get_node("themis_key", api_key) + .ok_or(Status::unauthenticated("api-key invalid"))?; + + let ns_ref = graph + .get_node("themis_ns", namespace) + .ok_or(Status::permission_denied("no permission for namespace"))?; + + if !graph.is_related_to(api_key, (ns_ref, graph::Relation::new(relation))) { + Err(Status::permission_denied("no permission for namespace")) + } else { + Ok(()) + } } diff --git a/src/main.rs b/src/main.rs index b444145..2d058a8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,10 @@ -use std::{sync::Arc, time::Duration}; +use std::{collections::HashMap, sync::Arc, time::Duration}; use graph::Graph; use grpc_service::GraphService; use tokio::{ fs::{self, File}, + io::{AsyncBufReadExt, BufReader}, select, sync::{mpsc::channel, Mutex}, }; @@ -23,6 +24,19 @@ async fn main() { dotenvy::dotenv().ok(); pretty_env_logger::init(); + let mut api_keys = HashMap::new(); + if let Ok(file) = File::open("api_keys.dat").await { + let reader = BufReader::new(file); + let mut lines = reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + let line = line.replace(' ', ""); + let mut line = line.split('='); + let name = line.next().unwrap().to_string(); + let hash = line.next().unwrap().to_string(); + api_keys.insert(hash, name); + } + } + let graph = if let Ok(mut file) = File::open("graph.dat").await { Graph::from_file(&mut file).await } else { @@ -48,6 +62,7 @@ async fn main() { }); let graph_service = GraphService { + api_keys: Arc::new(Mutex::new(api_keys)), graph: graph.clone(), save_trigger: save_tx.clone(), };