rename to rebacs, proto rework and implemented missing functions

This commit is contained in:
Paul Zinselmeyer 2023-06-05 22:17:22 +02:00
parent 0b1af2771c
commit 21f0c705fe
9 changed files with 413 additions and 255 deletions

View file

@ -1,5 +1,5 @@
[package]
name = "themis"
name = "rebacs"
version = "0.1.0"
edition = "2021"

View file

@ -1,6 +1,6 @@
fn main() {
tonic_build::configure()
.build_server(true)
.compile(&["proto/themis.proto"], &["proto"])
.compile(&["proto/rebacs.proto"], &["proto"])
.unwrap();
}

95
proto/rebacs.proto Normal file
View file

@ -0,0 +1,95 @@
syntax = "proto3";
package eu.zettoit.rebacs;
service RelationService {
rpc Create(RelationCreateReq) returns (RelationCreateRes);
rpc Delete(RelationDeleteReq) returns (RelationDeleteRes);
rpc Exists(RelationExistsReq) returns (RelationExistsRes);
}
service QueryService {
// check if one object or objectset is related to another by a relation
rpc IsRelatedTo(QueryIsRelatedToReq) returns (QueryIsRelatedToRes);
// get all objects that are related to one object by a relation
rpc GetRelated(QueryGetRelatedReq) returns (QueryGetRelatedRes);
// get all objects that the given object has a relation with
rpc GetRelations(QueryGetRelationsReq) returns (QueryGetRelationsRes);
}
message RelationCreateReq{
ObjectOrSet src = 1;
Object dst = 2;
string relation = 3;
}
message RelationCreateRes{}
message RelationDeleteReq{
ObjectOrSet src = 1;
Object dst = 3;
string relation = 4;
}
message RelationDeleteRes{}
message RelationExistsReq{
ObjectOrSet src = 1;
Object dst = 2;
string relation = 3;
}
message RelationExistsRes{
bool exists = 1;
}
message QueryIsRelatedToReq{
ObjectOrSet src = 1;
Object dst = 2;
string relation = 3;
}
message QueryIsRelatedToRes{
bool related = 1;
}
message QueryGetRelatedReq{
Object dst = 1;
optional string relation = 2;
optional string namespace = 3;
optional uint32 depth = 4;
}
message QueryGetRelatedRes{
repeated QueryGetRelatedItem objects = 1;
}
message QueryGetRelatedItem{
string relation = 1;
Object src = 2;
}
message QueryGetRelationsReq{
Object src = 1;
optional string relation = 2;
optional string namespace = 3;
optional uint32 depth = 4;
}
message QueryGetRelationsRes{
repeated QueryGetRelationsItem related = 1;
}
message QueryGetRelationsItem{
string relation = 1;
Object dst = 2;
}
message Object{
string namespace = 1;
string id = 2;
}
message Set{
string namespace = 1;
string id = 2;
string relation = 3;
}
message ObjectOrSet {
string namespace = 1;
string id = 2;
optional string relation = 3;
}

View file

@ -1,67 +0,0 @@
syntax = "proto3";
package eu.zettoit.themis;
service RelationService {
rpc Create(Relation) returns (Empty);
rpc Delete(Relation) returns (Empty);
rpc Exists(Relation) returns (ExistsResponse);
}
service QueryService {
// check if one object or objectset is related to another by a relation
rpc IsRelatedTo(Relation) returns (IsRelatedToResponse);
// get all objects that are related to one object by a relation
rpc GetRelatedTo(Set) returns (GetRelatedToResponse);
// get all objects that the given object has a relation with
rpc GetRelations(GetRelationsRequest) returns (GetRelationsResponse);
}
message ExistsResponse {
bool exists = 1;
}
message IsRelatedToResponse{
bool related = 1;
}
message GetRelatedToResponse{
repeated Object objects = 1;
}
message GetRelationsRequest{
Object object = 1;
string relation = 2;
}
message GetRelationsResponse{
repeated Object objects = 1;
}
message Object{
string namespace = 1;
string id = 2;
}
message Set{
string namespace = 1;
string id = 2;
string relation = 3;
}
message ObjectOrSet {
oneof object_or_set{
Object object = 1;
Set set = 2;
};
}
message Relation{
oneof src{
Object src_obj = 1;
Set src_set = 2;
};
Object dst = 3;
string relation = 4;
}
message Empty{}

View file

@ -8,12 +8,14 @@ use tokio::sync::Mutex;
use tonic::metadata::MetadataMap;
use tonic::{Request, Response, Status};
use crate::relation_set::{ObjectOrSet, RelationSet};
use crate::themis_proto::{
query_service_server::QueryService, relation::Src, relation_service_server::RelationService,
Empty, ExistsResponse, GetRelatedToResponse, GetRelationsRequest, GetRelationsResponse,
IsRelatedToResponse, Relation, Set,
use crate::rebacs_proto::{
query_service_server::QueryService, relation_service_server::RelationService, Object,
QueryGetRelatedItem, QueryGetRelatedReq, QueryGetRelatedRes, QueryGetRelationsItem,
QueryGetRelationsReq, QueryGetRelationsRes, QueryIsRelatedToReq, QueryIsRelatedToRes,
RelationCreateReq, RelationCreateRes, RelationDeleteReq, RelationDeleteRes, RelationExistsReq,
RelationExistsRes,
};
use crate::relation_set::{ObjectOrSet, RelationSet};
#[derive(Clone)]
pub struct GraphService {
@ -22,9 +24,15 @@ pub struct GraphService {
pub save_trigger: Sender<()>,
}
const API_KEY_NS: &str = "rebacs_key";
const NAMESPACE_NS: &str = "rebacs_ns";
#[tonic::async_trait]
impl RelationService for GraphService {
async fn create(&self, request: Request<Relation>) -> Result<Response<Empty>, Status> {
async fn create(
&self,
request: Request<RelationCreateReq>,
) -> Result<Response<RelationCreateRes>, Status> {
let mut graph = self.graph.lock().await;
let api_key = api_key_from_req(request.metadata(), &self.api_keys).await?;
@ -52,9 +60,9 @@ impl RelationService for GraphService {
}
if !graph.has_recursive(
("themis_key", &*api_key),
(API_KEY_NS, &*api_key),
"write",
("themis_ns", &*req_dst.namespace),
(NAMESPACE_NS, &*req_dst.namespace),
u32::MAX,
) {
return Err(Status::permission_denied(
@ -62,32 +70,21 @@ impl RelationService for GraphService {
))?;
}
let src: Result<ObjectOrSet, Status> = match req_src {
Src::SrcObj(obj) => {
if obj.namespace.is_empty() {
return Err(Status::invalid_argument("src.namespace must be set"));
}
if obj.id.is_empty() {
return Err(Status::invalid_argument("src.id must be set"));
}
Ok((&*obj.namespace, &*obj.id).into())
if req_src.namespace.is_empty() {
return Err(Status::invalid_argument("src.namespace must be set"));
}
if req_src.id.is_empty() {
return Err(Status::invalid_argument("src.id must be set"));
}
let src: ObjectOrSet = if let Some(req_src_relation) = req_src.relation.as_deref() {
if req_src_relation.is_empty() {
return Err(Status::invalid_argument("src.relation must be set"));
}
Src::SrcSet(set) => {
if set.namespace.is_empty() {
return Err(Status::invalid_argument("src.namespace must be set"));
}
if set.id.is_empty() {
return Err(Status::invalid_argument("src.id must be set"));
}
if set.relation.is_empty() {
return Err(Status::invalid_argument("src.relation must be set"));
}
Ok((&*set.namespace, &*set.id, &*set.relation).into())
}
(&*req_src.namespace, &*req_src.id, req_src_relation).into()
} else {
(&*req_src.namespace, &*req_src.id).into()
};
let src = src?;
graph.insert(
src.clone(),
@ -99,9 +96,12 @@ impl RelationService for GraphService {
self.save_trigger.send(()).await.unwrap();
Ok(Response::new(Empty {}))
Ok(Response::new(RelationCreateRes {}))
}
async fn delete(&self, request: Request<Relation>) -> Result<Response<Empty>, Status> {
async fn delete(
&self,
request: Request<RelationDeleteReq>,
) -> Result<Response<RelationDeleteRes>, Status> {
let mut graph = self.graph.lock().await;
let api_key = api_key_from_req(request.metadata(), &self.api_keys).await?;
@ -129,41 +129,31 @@ impl RelationService for GraphService {
}
if !graph.has_recursive(
("themis_key", &*api_key),
(API_KEY_NS, &*api_key),
"write",
("themis_ns", &*req_dst.namespace),
(NAMESPACE_NS, &*req_dst.namespace),
u32::MAX,
) {
return Err(Status::permission_denied(
"missing dst.namespace write permissions",
))?;
}
let src: Result<ObjectOrSet, Status> = match req_src {
Src::SrcObj(obj) => {
if obj.namespace.is_empty() {
return Err(Status::invalid_argument("src.namespace must be set"));
}
if obj.id.is_empty() {
return Err(Status::invalid_argument("src.id must be set"));
}
Ok((&*obj.namespace, &*obj.id).into())
if req_src.namespace.is_empty() {
return Err(Status::invalid_argument("src.namespace must be set"));
}
if req_src.id.is_empty() {
return Err(Status::invalid_argument("src.id must be set"));
}
let src: ObjectOrSet = if let Some(req_src_relation) = req_src.relation.as_deref() {
if req_src_relation.is_empty() {
return Err(Status::invalid_argument("src.relation must be set"));
}
Src::SrcSet(set) => {
if set.namespace.is_empty() {
return Err(Status::invalid_argument("src.namespace must be set"));
}
if set.id.is_empty() {
return Err(Status::invalid_argument("src.id must be set"));
}
if set.relation.is_empty() {
return Err(Status::invalid_argument("src.relation must be set"));
}
Ok((&*set.namespace, &*set.id, &*set.relation).into())
}
(&*req_src.namespace, &*req_src.id, req_src_relation).into()
} else {
(&*req_src.namespace, &*req_src.id).into()
};
let src = src?;
graph.remove(src, req_rel.as_str(), (&*req_dst.namespace, &*req_dst.id));
@ -171,9 +161,12 @@ impl RelationService for GraphService {
self.save_trigger.send(()).await.unwrap();
Ok(Response::new(Empty {}))
Ok(Response::new(RelationDeleteRes {}))
}
async fn exists(&self, request: Request<Relation>) -> Result<Response<ExistsResponse>, Status> {
async fn exists(
&self,
request: Request<RelationExistsReq>,
) -> Result<Response<RelationExistsRes>, Status> {
let graph = self.graph.lock().await;
let api_key = api_key_from_req(request.metadata(), &self.api_keys).await?;
@ -201,45 +194,34 @@ impl RelationService for GraphService {
}
if !graph.has_recursive(
("themis_key", &*api_key),
(API_KEY_NS, &*api_key),
"read",
("themis_ns", &*req_dst.namespace),
(NAMESPACE_NS, &*req_dst.namespace),
u32::MAX,
) {
return Err(Status::permission_denied(
"missing dst.namespace write permissions",
))?;
}
let src: Result<ObjectOrSet, Status> = match req_src {
Src::SrcObj(obj) => {
if obj.namespace.is_empty() {
return Err(Status::invalid_argument("src.namespace must be set"));
}
if obj.id.is_empty() {
return Err(Status::invalid_argument("src.id must be set"));
}
Ok((&*obj.namespace, &*obj.id).into())
if req_src.namespace.is_empty() {
return Err(Status::invalid_argument("src.namespace must be set"));
}
if req_src.id.is_empty() {
return Err(Status::invalid_argument("src.id must be set"));
}
let src: ObjectOrSet = if let Some(req_src_relation) = req_src.relation.as_deref() {
if req_src_relation.is_empty() {
return Err(Status::invalid_argument("src.relation must be set"));
}
Src::SrcSet(set) => {
if set.namespace.is_empty() {
return Err(Status::invalid_argument("src.namespace must be set"));
}
if set.id.is_empty() {
return Err(Status::invalid_argument("src.id must be set"));
}
if set.relation.is_empty() {
return Err(Status::invalid_argument("src.relation must be set"));
}
Ok((&*set.namespace, &*set.id, &*set.relation).into())
}
(&*req_src.namespace, &*req_src.id, req_src_relation).into()
} else {
(&*req_src.namespace, &*req_src.id).into()
};
let src = src?;
let exists = graph.has(src, req_rel.as_str(), (&*req_dst.namespace, &*req_dst.id));
Ok(Response::new(ExistsResponse { exists }))
Ok(Response::new(RelationExistsRes { exists }))
}
}
@ -247,8 +229,8 @@ impl RelationService for GraphService {
impl QueryService for GraphService {
async fn is_related_to(
&self,
request: Request<Relation>,
) -> Result<Response<IsRelatedToResponse>, Status> {
request: Request<QueryIsRelatedToReq>,
) -> Result<Response<QueryIsRelatedToRes>, Status> {
let graph = self.graph.lock().await;
let api_key = api_key_from_req(request.metadata(), &self.api_keys).await?;
@ -276,9 +258,9 @@ impl QueryService for GraphService {
}
if !graph.has_recursive(
("themis_key", &*api_key),
(API_KEY_NS, &*api_key),
"read",
("themis_ns", &*req_dst.namespace),
(NAMESPACE_NS, &*req_dst.namespace),
u32::MAX,
) {
return Err(Status::permission_denied(
@ -286,32 +268,21 @@ impl QueryService for GraphService {
))?;
}
let src: Result<ObjectOrSet, Status> = match req_src {
Src::SrcObj(obj) => {
if obj.namespace.is_empty() {
return Err(Status::invalid_argument("src.namespace must be set"));
}
if obj.id.is_empty() {
return Err(Status::invalid_argument("src.id must be set"));
}
Ok((&*obj.namespace, &*obj.id).into())
if req_src.namespace.is_empty() {
return Err(Status::invalid_argument("src.namespace must be set"));
}
if req_src.id.is_empty() {
return Err(Status::invalid_argument("src.id must be set"));
}
let src: ObjectOrSet = if let Some(req_src_relation) = req_src.relation.as_deref() {
if req_src_relation.is_empty() {
return Err(Status::invalid_argument("src.relation must be set"));
}
Src::SrcSet(set) => {
if set.namespace.is_empty() {
return Err(Status::invalid_argument("src.namespace must be set"));
}
if set.id.is_empty() {
return Err(Status::invalid_argument("src.id must be set"));
}
if set.relation.is_empty() {
return Err(Status::invalid_argument("src.relation must be set"));
}
Ok((&*set.namespace, &*set.id, &*set.relation).into())
}
(&*req_src.namespace, &*req_src.id, req_src_relation).into()
} else {
(&*req_src.namespace, &*req_src.id).into()
};
let src = src?;
let related = graph.has_recursive(
src,
@ -320,90 +291,113 @@ impl QueryService for GraphService {
u32::MAX,
);
Ok(Response::new(IsRelatedToResponse { related }))
Ok(Response::new(QueryIsRelatedToRes { related }))
}
async fn get_related_to(
async fn get_related(
&self,
request: Request<Set>,
) -> Result<Response<GetRelatedToResponse>, Status> {
//let graph = self.graph.lock().await;
request: Request<QueryGetRelatedReq>,
) -> Result<Response<QueryGetRelatedRes>, Status> {
let graph = self.graph.lock().await;
//authenticate(
// request.metadata(),
// &graph,
// &self.api_keys,
// &request.get_ref().namespace,
// "read",
//)
//.await?;
let api_key = api_key_from_req(request.metadata(), &self.api_keys).await?;
//let obj = graph
// .get_node(&request.get_ref().namespace, &request.get_ref().id)
// .ok_or(Status::not_found("object not found"))?;
let req_dst = request
.get_ref()
.dst
.as_ref()
.ok_or(Status::invalid_argument("dst must be set"))?;
let req_rel = &request.get_ref().relation;
//let rel = graph::Relation::new(&request.get_ref().relation);
if req_dst.namespace.is_empty() {
return Err(Status::invalid_argument("dst.namespace must be set"));
}
if req_dst.id.is_empty() {
return Err(Status::invalid_argument("dst.id must be set"));
}
//Ok(Response::new(GetRelatedToResponse {
// objects: graph
// .related_to(obj, rel)
// .into_iter()
// .map(|x| {
// let obj = graph.object_from_ref(&x);
// Object {
// namespace: obj.namespace.to_string(),
// id: obj.id,
// }
// })
// .collect::<Vec<_>>(),
//}))
todo!()
let req_namespace = &request.get_ref().namespace;
let req_depth = &request.get_ref().depth;
if !graph.has_recursive(
(API_KEY_NS, &*api_key),
"read",
(NAMESPACE_NS, &*req_dst.namespace),
u32::MAX,
) {
return Err(Status::permission_denied(
"missing dst.namespace read permissions",
))?;
}
let dst = (req_dst.namespace.as_ref(), req_dst.id.as_ref());
let objects = graph
.related_to(
dst,
req_rel.as_deref(),
req_namespace.as_deref(),
req_depth.unwrap_or(u32::MAX),
)
.into_iter()
.map(|x| QueryGetRelatedItem {
src: Some(Object {
namespace: x.1.namespace.to_string(),
id: x.1.id.to_string(),
}),
relation: x.0 .0.to_string(),
})
.collect::<_>();
Ok(Response::new(QueryGetRelatedRes { objects }))
}
async fn get_relations(
&self,
request: Request<GetRelationsRequest>,
) -> Result<Response<GetRelationsResponse>, Status> {
//let graph = self.graph.lock().await;
request: Request<QueryGetRelationsReq>,
) -> Result<Response<QueryGetRelationsRes>, Status> {
let graph = self.graph.lock().await;
//if request.get_ref().relation.is_empty() {
// return Err(Status::invalid_argument("relation must be set"));
//}
let api_key = api_key_from_req(request.metadata(), &self.api_keys).await?;
//let obj = request
// .get_ref()
// .object
// .as_ref()
// .ok_or(Status::invalid_argument("object must be set"))?;
let req_src = request
.get_ref()
.src
.as_ref()
.ok_or(Status::invalid_argument("src must be set"))?;
let src = (&*req_src.namespace, &*req_src.id);
//authenticate(
// request.metadata(),
// &graph,
// &self.api_keys,
// &obj.namespace,
// "read",
//)
//.await?;
let req_rel = &request.get_ref().relation;
let req_namespace = &request.get_ref().namespace;
let req_depth = &request.get_ref().depth;
//let obj = graph
// .get_node(&obj.namespace, &obj.id)
// .ok_or(Status::not_found("object not found"))?;
if !graph.has_recursive(
(API_KEY_NS, &*api_key),
"read",
(NAMESPACE_NS, &*req_src.namespace),
u32::MAX,
) {
return Err(Status::permission_denied(
"missing src.namespace read permissions",
))?;
}
//Ok(Response::new(GetRelationsResponse {
// objects: graph
// .relations(ObjectRelation(
// obj,
// graph::Relation::new(&request.get_ref().relation),
// ))
// .into_iter()
// .map(|x| {
// let obj = graph.object_from_ref(&x);
// Object {
// namespace: obj.namespace.to_string(),
// id: obj.id,
// }
// })
// .collect::<Vec<_>>(),
//}))
todo!()
let related = graph
.relations(
src,
req_rel.as_deref(),
req_namespace.as_deref(),
req_depth.unwrap_or(u32::MAX),
)
.into_iter()
.map(|x| QueryGetRelationsItem {
dst: Some(Object {
namespace: x.1.namespace.to_string(),
id: x.1.id.to_string(),
}),
relation: x.0 .0.to_string(),
})
.collect::<_>();
Ok(Response::new(QueryGetRelationsRes { related }))
}
}

View file

@ -14,10 +14,10 @@ use tokio::{
use tonic::transport::Server;
pub mod grpc_service;
pub mod rebacs_proto;
pub mod relation_set;
pub mod themis_proto;
use crate::themis_proto::{
use crate::rebacs_proto::{
query_service_server::QueryServiceServer, relation_service_server::RelationServiceServer,
};

1
src/rebacs_proto.rs Normal file
View file

@ -0,0 +1 @@
tonic::include_proto!("eu.zettoit.rebacs");

View file

@ -195,6 +195,9 @@ impl RelationSet {
while let Some(distanced) = q.pop() {
let node_dist = distanced.distance() + 1;
if node_dist > limit {
break;
}
let node = ObjectOrSet::Set(((*distanced.0).clone(), (*distanced.1).clone()));
for (nrel, ndst) in self
.src_to_dst
@ -207,7 +210,7 @@ impl RelationSet {
return true;
}
if let Some(existing_node_dist) = dist.get(&*distanced) {
if *existing_node_dist <= node_dist || node_dist >= limit {
if *existing_node_dist <= node_dist {
continue;
}
}
@ -218,6 +221,139 @@ impl RelationSet {
false
}
pub fn related_to(
&self,
dst: impl Into<D>,
rel: Option<impl Into<R>>,
namespace: Option<&str>,
limit: u32,
) -> Vec<(Relation, Object)> {
let rel = rel.map(|x| x.into());
let dst = dst.into();
let mut related: Vec<(Relation, Object)> = vec![];
let mut dist: HashMap<(Arc<Object>, Arc<Relation>), u32> = HashMap::new();
let mut q: BinaryHeap<Distanced<(Arc<Object>, Arc<Relation>)>> = BinaryHeap::new();
for (nrel, ndst) in self
.dst_to_src
.get(&dst)
.iter()
.flat_map(|x| x.iter())
.flat_map(|(r, d)| d.iter().map(|d| (r.clone(), d.clone())))
{
match &*ndst {
ObjectOrSet::Object(obj) => {
if (rel.is_none() || rel.as_ref() == Some(&nrel))
&& (namespace.is_none() || namespace == Some(&obj.namespace))
{
related.push(((*nrel).clone(), obj.clone()));
}
}
ObjectOrSet::Set((obj, rel)) => {
let obj = Arc::new(obj.clone());
let rel = Arc::new(rel.clone());
dist.insert((obj.clone(), rel.clone()), 1);
q.push(Distanced::one((obj, rel)));
}
}
}
while let Some(distanced) = q.pop() {
let node_dist = distanced.distance() + 1;
if node_dist > limit {
break;
}
for ndst in self
.dst_to_src
.get(&distanced.0)
.and_then(|x| x.get(&distanced.1))
.iter()
.flat_map(|x| x.iter())
{
match &**ndst {
ObjectOrSet::Object(obj) => {
if (rel.is_none() || rel.as_ref() == Some(&distanced.1))
&& (namespace.is_none() || namespace == Some(&obj.namespace))
{
related.push(((*distanced.1).clone(), obj.clone()));
}
}
ObjectOrSet::Set((obj, rel)) => {
let obj = Arc::new(obj.clone());
let rel = Arc::new(rel.clone());
dist.insert((obj.clone(), rel.clone()), node_dist);
q.push(Distanced::one((obj, rel)));
}
}
}
}
related
}
pub fn relations(
&self,
src: impl Into<S>,
rel: Option<impl Into<R>>,
namespace: Option<&str>,
limit: u32,
) -> Vec<(Relation, Object)> {
let rel = rel.map(|x| x.into());
let src = src.into();
let mut related: Vec<(Relation, Object)> = vec![];
let mut dist: HashMap<Arc<ObjectOrSet>, u32> = HashMap::new();
let mut q: BinaryHeap<Distanced<Arc<ObjectOrSet>>> = BinaryHeap::new();
for (nrel, ndst) in self
.src_to_dst
.get(&src)
.iter()
.flat_map(|x| x.iter())
.flat_map(|(r, d)| d.iter().map(|d| (r.clone(), d.clone())))
{
if (rel.is_none() || rel.as_ref() == Some(&nrel))
&& (namespace.is_none() || namespace == Some(&ndst.namespace))
{
related.push(((*nrel).clone(), (*ndst).clone()));
}
let obj = Arc::new(ObjectOrSet::Set(((*ndst).clone(), (*nrel).clone())));
dist.insert(obj.clone(), 1);
q.push(Distanced::one(obj));
}
while let Some(distanced) = q.pop() {
let node_dist = distanced.distance() + 1;
if node_dist > limit {
break;
}
for (nrel, ndsts) in self
.src_to_dst
.get(&*distanced)
.iter()
.flat_map(|x| x.iter())
{
for ndst in ndsts {
if (rel.is_none() || rel.as_ref() == Some(nrel))
&& (namespace.is_none() || namespace == Some(&ndst.namespace))
{
related.push(((**nrel).clone(), (**ndst).clone()));
}
let obj = Arc::new(ObjectOrSet::Set(((**ndst).clone(), (**nrel).clone())));
dist.insert(obj.clone(), node_dist);
q.push(Distanced::one(obj));
}
}
}
related
}
pub async fn to_file(&self, file: &mut File) {
for (dst, rels_srcs) in self.dst_to_src.iter() {
file.write_all(format!("[{}:{}]\n", &dst.namespace, &dst.id).as_bytes())

View file

@ -1 +0,0 @@
tonic::include_proto!("eu.zettoit.themis");