added authentication

This commit is contained in:
Paul Zinselmeyer 2023-04-30 14:15:04 +02:00
parent f368a85317
commit dbd581cece
Signed by: pfzetto
GPG key ID: 4EEF46A5B276E648
6 changed files with 251 additions and 26 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
.env
graph.dat
graph.dat.bak
api_keys.dat

79
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -251,14 +251,21 @@ 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);
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),
}
}
})
})
.reduce(|acc, e| acc + ", " + &e)
@ -277,16 +284,17 @@ impl Graph {
let mut lines = reader.lines();
let mut graph = Graph::default();
let mut node: Option<ObjectRef> = 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(),
));
}

View file

@ -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<Mutex<HashMap<String, String>>>,
pub graph: Arc<Mutex<Graph>>,
pub save_trigger: Sender<()>,
}
@ -23,6 +27,15 @@ impl ObjectService for GraphService {
async fn create(&self, request: Request<Object>) -> Result<Response<Empty>, 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<Object>) -> Result<Response<Empty>, 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<Object>) -> Result<Response<ExistsResponse>, 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<Relation>) -> Result<Response<Empty>, 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<Relation>) -> Result<Response<Empty>, 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<Relation>) -> Result<Response<ExistsResponse>, 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<Response<IsRelatedToResponse>, 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<Response<GetRelatedToResponse>, 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<Mutex<HashMap<String, String>>>,
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(())
}
}

View file

@ -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(),
};