mirror of
https://github.com/pfzetto/rebacs
synced 2024-11-21 10:42:49 +01:00
openid connect integration
This commit is contained in:
parent
faf4e82f88
commit
66fac9d185
8 changed files with 1304 additions and 1257 deletions
990
Cargo.lock
generated
990
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
14
Cargo.toml
14
Cargo.toml
|
@ -6,16 +6,24 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dotenvy = "0.15.7"
|
||||
log = "0.4.17"
|
||||
env_logger = "0.10.0"
|
||||
|
||||
serde = { version="1.0", features=["derive"] }
|
||||
tokio = { version = "1.27.0", features = ["full"] }
|
||||
log = "0.4.17"
|
||||
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"
|
||||
compact_str = "0.7.0"
|
||||
|
||||
thiserror = "1.0.47"
|
||||
|
||||
jsonwebtoken = "8.3.0"
|
||||
|
||||
reqwest = { version="0.11.20", features=["json", "rustls-tls"], default-features=false}
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.9.2"
|
||||
|
|
129
flake.lock
Normal file
129
flake.lock
Normal file
|
@ -0,0 +1,129 @@
|
|||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": [
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-overlay": [
|
||||
"rust-overlay"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1691423162,
|
||||
"narHash": "sha256-cReUZCo83YEEmFcHX8CcOVTZYUrcWgHQO34zxQzy7WI=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "b5d9d42ea3fa8fea1805d9af1416fe207d0dd1dc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1673956053,
|
||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1689068808,
|
||||
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1691472822,
|
||||
"narHash": "sha256-XVfYZ2oB3lNPVq6sHCY9WkdQ8lHoIDzzbpg8bB6oBxA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "41c7605718399dcfa53dd7083793b6ae3bc969ff",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": [
|
||||
"flake-utils"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1691547503,
|
||||
"narHash": "sha256-l0AIKJucygbDFc2vuAkxmFMjNNJImDd7jYahA88/E+o=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "3380f16b39457b49c8186d5e20e7a68ccf4fc96e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
77
flake.nix
Normal file
77
flake.nix
Normal file
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
description = "rebacs";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs = {
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
flake-utils.follows = "flake-utils";
|
||||
};
|
||||
};
|
||||
crane = {
|
||||
url = "github:ipetkov/crane";
|
||||
inputs = {
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
flake-utils.follows = "flake-utils";
|
||||
rust-overlay.follows = "rust-overlay";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, rust-overlay, crane}:
|
||||
flake-utils.lib.eachDefaultSystem
|
||||
(system:
|
||||
let
|
||||
overlays = [ (import rust-overlay) ];
|
||||
pkgs = import nixpkgs {
|
||||
inherit system overlays;
|
||||
};
|
||||
|
||||
rustToolchain = pkgs.rust-bin.nightly.latest.default;
|
||||
|
||||
protoFilter = path: _type: builtins.match ".*proto$" path != null;
|
||||
tailwindFilter = path: _type: builtins.match "^tailwind.config.js$" path != null;
|
||||
protoOrCargo = path: type: (protoFilter path type) || (tailwindFilter path type) || (craneLib.filterCargoSources path type);
|
||||
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||
src = pkgs.lib.cleanSourceWith {
|
||||
src = craneLib.path ./.;
|
||||
filter = protoOrCargo;
|
||||
};
|
||||
|
||||
nativeBuildInputs = with pkgs; [ rustToolchain pkg-config ];
|
||||
buildInputs = with pkgs; [ protobuf ];
|
||||
|
||||
commonArgs = {
|
||||
inherit src buildInputs nativeBuildInputs;
|
||||
};
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
|
||||
bin = craneLib.buildPackage (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
});
|
||||
|
||||
dockerImage = pkgs.dockerTools.buildImage {
|
||||
name = "rebacs";
|
||||
tag = "latest";
|
||||
config = {
|
||||
Cmd = [ "${bin}/bin/rebacs" ];
|
||||
};
|
||||
};
|
||||
|
||||
in
|
||||
with pkgs;
|
||||
{
|
||||
packages = {
|
||||
inherit bin dockerImage;
|
||||
default = bin;
|
||||
};
|
||||
devShells.default = mkShell {
|
||||
inputsFrom = [ bin ];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,95 +1,45 @@
|
|||
syntax = "proto3";
|
||||
package eu.zettoit.rebacs;
|
||||
|
||||
service RelationService {
|
||||
rpc Create(RelationCreateReq) returns (RelationCreateRes);
|
||||
rpc Delete(RelationDeleteReq) returns (RelationDeleteRes);
|
||||
rpc Exists(RelationExistsReq) returns (RelationExistsRes);
|
||||
service RebacService {
|
||||
rpc Grant(GrantReq) returns (GrantRes);
|
||||
rpc Revoke(RevokeReq) returns (RevokeRes);
|
||||
rpc Exists(ExistsReq) returns (ExistsRes);
|
||||
rpc IsPermitted(IsPermittedReq) returns (IsPermittedRes);
|
||||
}
|
||||
|
||||
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;
|
||||
message GrantReq{
|
||||
Object src = 1;
|
||||
Object dst = 2;
|
||||
string relation = 3;
|
||||
}
|
||||
message RelationCreateRes{}
|
||||
message GrantRes{}
|
||||
|
||||
message RelationDeleteReq{
|
||||
ObjectOrSet src = 1;
|
||||
Object dst = 3;
|
||||
string relation = 4;
|
||||
}
|
||||
message RelationDeleteRes{}
|
||||
|
||||
message RelationExistsReq{
|
||||
ObjectOrSet src = 1;
|
||||
message RevokeReq{
|
||||
Object src = 1;
|
||||
Object dst = 2;
|
||||
string relation = 3;
|
||||
}
|
||||
message RelationExistsRes{
|
||||
message RevokeRes{}
|
||||
|
||||
message ExistsReq{
|
||||
Object src = 1;
|
||||
Object dst = 2;
|
||||
}
|
||||
message ExistsRes{
|
||||
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{
|
||||
message IsPermittedReq{
|
||||
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 IsPermittedRes{
|
||||
bool permitted = 1;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,421 +1,207 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use jsonwebtoken::{decode, DecodingKey, TokenData, Validation};
|
||||
use log::info;
|
||||
use sha2::{Digest, Sha256};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::sync::Mutex;
|
||||
use tonic::metadata::MetadataMap;
|
||||
use tonic::{Request, Response, Status};
|
||||
|
||||
use crate::rebacs_proto::Object;
|
||||
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,
|
||||
rebac_service_server, ExistsReq, ExistsRes, GrantReq, GrantRes, IsPermittedReq, IsPermittedRes,
|
||||
RevokeReq, RevokeRes,
|
||||
};
|
||||
use crate::relation_set::{ObjectOrSet, RelationSet};
|
||||
use crate::relation_set::{NodeId, RelationSet};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GraphService {
|
||||
pub api_keys: Arc<Mutex<HashMap<String, String>>>,
|
||||
pub graph: Arc<Mutex<RelationSet>>,
|
||||
pub struct RebacService {
|
||||
pub graph: Arc<RelationSet>,
|
||||
pub oidc_pubkey: DecodingKey,
|
||||
pub oidc_validation: Validation,
|
||||
pub save_trigger: Sender<()>,
|
||||
}
|
||||
|
||||
const API_KEY_NS: &str = "rebacs_key";
|
||||
const NAMESPACE_NS: &str = "rebacs_ns";
|
||||
const NAMESPACE_NS: &str = "namespace";
|
||||
const USER_NS: &str = "user";
|
||||
const GRANT_RELATION: &str = "grant";
|
||||
const REVOKE_RELATION: &str = "revoke";
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl RelationService for GraphService {
|
||||
async fn create(
|
||||
&self,
|
||||
request: Request<RelationCreateReq>,
|
||||
) -> Result<Response<RelationCreateRes>, Status> {
|
||||
let mut graph = self.graph.lock().await;
|
||||
impl rebac_service_server::RebacService for RebacService {
|
||||
async fn grant(&self, request: Request<GrantReq>) -> Result<Response<GrantRes>, Status> {
|
||||
let token =
|
||||
extract_token(request.metadata(), &self.oidc_pubkey, &self.oidc_validation).await?;
|
||||
|
||||
let api_key = api_key_from_req(request.metadata(), &self.api_keys).await?;
|
||||
let (src, dst) = extract_src_dst(&request.get_ref().src, &request.get_ref().dst)?;
|
||||
|
||||
let req_src = request
|
||||
.get_ref()
|
||||
.src
|
||||
.as_ref()
|
||||
.ok_or(Status::invalid_argument("src must be set"))?;
|
||||
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;
|
||||
|
||||
if req_rel.is_empty() {
|
||||
return Err(Status::invalid_argument("relation must be set"));
|
||||
}
|
||||
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"));
|
||||
}
|
||||
|
||||
if !graph.has_recursive(
|
||||
(API_KEY_NS, &*api_key),
|
||||
"write",
|
||||
(NAMESPACE_NS, &*req_dst.namespace),
|
||||
u32::MAX,
|
||||
) {
|
||||
if !is_permitted(&token, &dst, GRANT_RELATION, &self.graph).await {
|
||||
return Err(Status::permission_denied(
|
||||
"missing dst.namespace write permissions",
|
||||
))?;
|
||||
"token not permitted to grant permissions on dst",
|
||||
));
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
(&*req_src.namespace, &*req_src.id, req_src_relation).into()
|
||||
} else {
|
||||
(&*req_src.namespace, &*req_src.id).into()
|
||||
};
|
||||
|
||||
graph.insert(
|
||||
src.clone(),
|
||||
req_rel.clone(),
|
||||
(req_dst.namespace.clone(), req_dst.id.clone()),
|
||||
info!(
|
||||
"created relation {}:{}#{}@{}:{}#{} for {}",
|
||||
dst.namespace,
|
||||
dst.id,
|
||||
dst.relation.clone().unwrap_or_default(),
|
||||
src.namespace,
|
||||
src.id,
|
||||
src.relation.clone().unwrap_or_default(),
|
||||
token.claims.sub
|
||||
);
|
||||
|
||||
info!("created relation");
|
||||
self.graph.insert(src, dst).await;
|
||||
|
||||
self.save_trigger.send(()).await.unwrap();
|
||||
|
||||
Ok(Response::new(RelationCreateRes {}))
|
||||
Ok(Response::new(GrantRes {}))
|
||||
}
|
||||
async fn delete(
|
||||
&self,
|
||||
request: Request<RelationDeleteReq>,
|
||||
) -> Result<Response<RelationDeleteRes>, Status> {
|
||||
let mut graph = self.graph.lock().await;
|
||||
async fn revoke(&self, request: Request<RevokeReq>) -> Result<Response<RevokeRes>, Status> {
|
||||
let token =
|
||||
extract_token(request.metadata(), &self.oidc_pubkey, &self.oidc_validation).await?;
|
||||
|
||||
let api_key = api_key_from_req(request.metadata(), &self.api_keys).await?;
|
||||
let (src, dst) = extract_src_dst(&request.get_ref().src, &request.get_ref().dst)?;
|
||||
|
||||
let req_src = request
|
||||
.get_ref()
|
||||
.src
|
||||
.as_ref()
|
||||
.ok_or(Status::invalid_argument("src must be set"))?;
|
||||
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;
|
||||
|
||||
if req_rel.is_empty() {
|
||||
return Err(Status::invalid_argument("relation must be set"));
|
||||
}
|
||||
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"));
|
||||
}
|
||||
|
||||
if !graph.has_recursive(
|
||||
(API_KEY_NS, &*api_key),
|
||||
"write",
|
||||
(NAMESPACE_NS, &*req_dst.namespace),
|
||||
u32::MAX,
|
||||
) {
|
||||
if !is_permitted(&token, &dst, REVOKE_RELATION, &self.graph).await {
|
||||
return Err(Status::permission_denied(
|
||||
"missing dst.namespace write permissions",
|
||||
))?;
|
||||
"token not permitted to revoke permissions on dst",
|
||||
));
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
self.graph
|
||||
.remove(
|
||||
(
|
||||
src.namespace.to_string(),
|
||||
src.id.to_string(),
|
||||
src.relation.clone(),
|
||||
),
|
||||
(dst.namespace.clone(), dst.id.clone(), dst.relation.clone()),
|
||||
)
|
||||
.await;
|
||||
|
||||
(&*req_src.namespace, &*req_src.id, req_src_relation).into()
|
||||
} else {
|
||||
(&*req_src.namespace, &*req_src.id).into()
|
||||
};
|
||||
|
||||
graph.remove(src, req_rel.as_str(), (&*req_dst.namespace, &*req_dst.id));
|
||||
|
||||
info!("deleted relation");
|
||||
info!(
|
||||
"delted relation {}:{}#{}@{}:{}#{} for {}",
|
||||
dst.namespace,
|
||||
dst.id,
|
||||
dst.relation.clone().unwrap_or_default(),
|
||||
src.namespace,
|
||||
src.id,
|
||||
src.relation.clone().unwrap_or_default(),
|
||||
token.claims.sub
|
||||
);
|
||||
|
||||
self.save_trigger.send(()).await.unwrap();
|
||||
|
||||
Ok(Response::new(RelationDeleteRes {}))
|
||||
Ok(Response::new(RevokeRes {}))
|
||||
}
|
||||
async fn exists(
|
||||
async fn exists(&self, request: Request<ExistsReq>) -> Result<Response<ExistsRes>, Status> {
|
||||
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 exists = self.graph.has(src, dst).await;
|
||||
|
||||
Ok(Response::new(ExistsRes { exists }))
|
||||
}
|
||||
|
||||
async fn is_permitted(
|
||||
&self,
|
||||
request: Request<RelationExistsReq>,
|
||||
) -> Result<Response<RelationExistsRes>, Status> {
|
||||
let graph = self.graph.lock().await;
|
||||
request: Request<IsPermittedReq>,
|
||||
) -> Result<Response<IsPermittedRes>, Status> {
|
||||
let token =
|
||||
extract_token(request.metadata(), &self.oidc_pubkey, &self.oidc_validation).await?;
|
||||
|
||||
let api_key = api_key_from_req(request.metadata(), &self.api_keys).await?;
|
||||
let (src, dst) = extract_src_dst(&request.get_ref().src, &request.get_ref().dst)?;
|
||||
|
||||
let req_src = request
|
||||
.get_ref()
|
||||
.src
|
||||
.as_ref()
|
||||
.ok_or(Status::invalid_argument("src must be set"))?;
|
||||
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 permitted = self.graph.has_recursive(src, dst, None).await;
|
||||
|
||||
if req_rel.is_empty() {
|
||||
return Err(Status::invalid_argument("relation must be set"));
|
||||
}
|
||||
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"));
|
||||
}
|
||||
|
||||
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 write permissions",
|
||||
))?;
|
||||
}
|
||||
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"));
|
||||
}
|
||||
|
||||
(&*req_src.namespace, &*req_src.id, req_src_relation).into()
|
||||
} else {
|
||||
(&*req_src.namespace, &*req_src.id).into()
|
||||
};
|
||||
|
||||
let exists = graph.has(src, req_rel.as_str(), (&*req_dst.namespace, &*req_dst.id));
|
||||
|
||||
Ok(Response::new(RelationExistsRes { exists }))
|
||||
Ok(Response::new(IsPermittedRes { permitted }))
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl QueryService for GraphService {
|
||||
async fn is_related_to(
|
||||
&self,
|
||||
request: Request<QueryIsRelatedToReq>,
|
||||
) -> Result<Response<QueryIsRelatedToRes>, Status> {
|
||||
let graph = self.graph.lock().await;
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Claims {
|
||||
pub aud: Vec<String>,
|
||||
pub exp: usize,
|
||||
pub iat: usize,
|
||||
pub iss: String,
|
||||
pub sub: String,
|
||||
pub azp: String,
|
||||
|
||||
let api_key = api_key_from_req(request.metadata(), &self.api_keys).await?;
|
||||
|
||||
let req_src = request
|
||||
.get_ref()
|
||||
.src
|
||||
.as_ref()
|
||||
.ok_or(Status::invalid_argument("src must be set"))?;
|
||||
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;
|
||||
|
||||
if req_rel.is_empty() {
|
||||
return Err(Status::invalid_argument("relation must be set"));
|
||||
}
|
||||
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"));
|
||||
}
|
||||
|
||||
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",
|
||||
))?;
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
(&*req_src.namespace, &*req_src.id, req_src_relation).into()
|
||||
} else {
|
||||
(&*req_src.namespace, &*req_src.id).into()
|
||||
};
|
||||
|
||||
let related = graph.has_recursive(
|
||||
src,
|
||||
req_rel.as_str(),
|
||||
(&*req_dst.namespace, &*req_dst.id),
|
||||
u32::MAX,
|
||||
);
|
||||
|
||||
Ok(Response::new(QueryIsRelatedToRes { related }))
|
||||
}
|
||||
async fn get_related(
|
||||
&self,
|
||||
request: Request<QueryGetRelatedReq>,
|
||||
) -> Result<Response<QueryGetRelatedRes>, Status> {
|
||||
let graph = self.graph.lock().await;
|
||||
|
||||
let api_key = api_key_from_req(request.metadata(), &self.api_keys).await?;
|
||||
|
||||
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;
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
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<QueryGetRelationsReq>,
|
||||
) -> Result<Response<QueryGetRelationsRes>, Status> {
|
||||
let graph = self.graph.lock().await;
|
||||
|
||||
let api_key = api_key_from_req(request.metadata(), &self.api_keys).await?;
|
||||
|
||||
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);
|
||||
|
||||
let req_rel = &request.get_ref().relation;
|
||||
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_src.namespace),
|
||||
u32::MAX,
|
||||
) {
|
||||
return Err(Status::permission_denied(
|
||||
"missing src.namespace read permissions",
|
||||
))?;
|
||||
}
|
||||
|
||||
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 }))
|
||||
}
|
||||
pub name: String,
|
||||
pub preferred_username: String,
|
||||
pub given_name: String,
|
||||
pub family_name: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
async fn api_key_from_req(
|
||||
async fn extract_token(
|
||||
metadata: &MetadataMap,
|
||||
api_keys: &Arc<Mutex<HashMap<String, String>>>,
|
||||
) -> Result<String, Status> {
|
||||
let api_key = metadata
|
||||
.get("x-api-key")
|
||||
pubkey: &DecodingKey,
|
||||
validation: &Validation,
|
||||
) -> Result<TokenData<Claims>, Status> {
|
||||
let token = metadata
|
||||
.get("authorization")
|
||||
.map(|x| x.to_str().unwrap())
|
||||
.ok_or(Status::unauthenticated("x-api-key required"))?;
|
||||
.ok_or(Status::unauthenticated("authorization header 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"))?;
|
||||
Ok(api_key.to_string())
|
||||
let token = decode::<Claims>(token, pubkey, validation)
|
||||
.map_err(|_| Status::unauthenticated("authorization header invalid"))?;
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
async fn is_permitted(
|
||||
token: &TokenData<Claims>,
|
||||
dst: &NodeId,
|
||||
relation: &str,
|
||||
graph: &RelationSet,
|
||||
) -> 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<Object>, dst: &Option<Object>) -> 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();
|
||||
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();
|
||||
|
||||
if dst.namespace.is_empty() {
|
||||
return Err(Status::invalid_argument("dst.namespace must be set"));
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
|
80
src/main.rs
80
src/main.rs
|
@ -1,15 +1,16 @@
|
|||
#![feature(btree_cursors)]
|
||||
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use std::{env, sync::Arc, time::Duration};
|
||||
|
||||
use grpc_service::GraphService;
|
||||
use grpc_service::RebacService;
|
||||
use jsonwebtoken::{Algorithm, DecodingKey, Validation};
|
||||
use log::info;
|
||||
use relation_set::RelationSet;
|
||||
//use grpc_service::GraphService;
|
||||
use serde::Deserialize;
|
||||
use tokio::{
|
||||
fs::{self, File},
|
||||
io::{AsyncBufReadExt, BufReader},
|
||||
select,
|
||||
sync::{mpsc::channel, Mutex},
|
||||
sync::mpsc::channel,
|
||||
};
|
||||
use tonic::transport::Server;
|
||||
|
||||
|
@ -17,35 +18,26 @@ pub mod grpc_service;
|
|||
pub mod rebacs_proto;
|
||||
pub mod relation_set;
|
||||
|
||||
use crate::rebacs_proto::{
|
||||
query_service_server::QueryServiceServer, relation_service_server::RelationServiceServer,
|
||||
};
|
||||
use crate::rebacs_proto::rebac_service_server;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct IssuerDiscovery {
|
||||
public_key: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
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);
|
||||
}
|
||||
}
|
||||
env_logger::init();
|
||||
|
||||
info!("loading graph from graph.dat");
|
||||
let graph = if let Ok(mut file) = File::open("graph.dat").await {
|
||||
RelationSet::from_file(&mut file).await
|
||||
} else {
|
||||
RelationSet::new()
|
||||
RelationSet::default()
|
||||
};
|
||||
|
||||
let graph = Arc::new(Mutex::new(graph));
|
||||
let graph = Arc::new(graph);
|
||||
|
||||
let (save_tx, mut save_rx) = channel::<()>(32);
|
||||
let save_thread_graph = graph.clone();
|
||||
|
@ -55,24 +47,48 @@ async fn main() {
|
|||
_ = tokio::time::sleep(Duration::from_secs(30)) => {}
|
||||
_ = save_rx.recv() => {}
|
||||
};
|
||||
let graph = save_thread_graph.lock().await;
|
||||
|
||||
info!("saving graph");
|
||||
let _ = fs::copy("graph.dat", "graph.dat.bak").await;
|
||||
let mut file = File::create("graph.dat").await.unwrap();
|
||||
graph.to_file(&mut file).await;
|
||||
save_thread_graph.to_file(&mut file).await;
|
||||
}
|
||||
});
|
||||
|
||||
let graph_service = GraphService {
|
||||
api_keys: Arc::new(Mutex::new(api_keys)),
|
||||
let issuer = env::var("OIDC_ISSUER").expect("OIDC_ISSUER env var");
|
||||
info!("loading public key from {issuer}");
|
||||
let issuer_key = reqwest::get(&issuer)
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<IssuerDiscovery>()
|
||||
.await
|
||||
.unwrap()
|
||||
.public_key;
|
||||
|
||||
let pem = format!(
|
||||
"-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----",
|
||||
issuer_key
|
||||
);
|
||||
|
||||
let oidc_pubkey = DecodingKey::from_rsa_pem(pem.as_bytes()).unwrap();
|
||||
|
||||
let mut oidc_validation = Validation::new(Algorithm::RS256);
|
||||
oidc_validation.set_issuer(&[&issuer]);
|
||||
oidc_validation.set_audience(&[env::var("OIDC_AUDIENCE").expect("OIDC_AUDIENCE env var")]);
|
||||
|
||||
let rebac_service = RebacService {
|
||||
graph: graph.clone(),
|
||||
save_trigger: save_tx.clone(),
|
||||
oidc_pubkey,
|
||||
oidc_validation,
|
||||
};
|
||||
|
||||
let listen = "[::]:50051";
|
||||
info!("starting grpc server on {listen}");
|
||||
Server::builder()
|
||||
.add_service(RelationServiceServer::new(graph_service.clone()))
|
||||
.add_service(QueryServiceServer::new(graph_service))
|
||||
.serve("[::]:50051".parse().unwrap())
|
||||
.add_service(rebac_service_server::RebacServiceServer::new(
|
||||
rebac_service.clone(),
|
||||
))
|
||||
.serve(listen.parse().unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
|
|
@ -1,399 +1,197 @@
|
|||
use std::{
|
||||
borrow::Borrow,
|
||||
cmp::Ordering,
|
||||
collections::{BTreeMap, BinaryHeap, HashMap, HashSet},
|
||||
ops::{Bound, Deref},
|
||||
collections::{BTreeSet, BinaryHeap, HashSet},
|
||||
fmt::Debug,
|
||||
hash::Hash,
|
||||
ops::Deref,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use compact_str::CompactString;
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
sync::RwLock,
|
||||
};
|
||||
|
||||
#[derive(Hash, PartialEq, Eq, Clone, Debug)]
|
||||
pub struct Object {
|
||||
pub namespace: CompactString,
|
||||
pub id: CompactString,
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct NodeId {
|
||||
pub namespace: String,
|
||||
pub id: String,
|
||||
pub relation: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
|
||||
pub struct ObjectRef<'a> {
|
||||
pub namespace: &'a str,
|
||||
pub id: &'a str,
|
||||
pub struct Node {
|
||||
pub id: NodeId,
|
||||
pub edges_in: RwLock<Vec<Arc<Node>>>,
|
||||
pub edges_out: RwLock<Vec<Arc<Node>>>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||
pub enum ObjectOrSet {
|
||||
Object(Object),
|
||||
Set((Object, Relation)),
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct Distanced<T> {
|
||||
distance: u32,
|
||||
data: T,
|
||||
}
|
||||
#[derive(Hash, PartialEq, Eq, Clone, Debug)]
|
||||
pub struct Relation(pub CompactString);
|
||||
|
||||
type S = ObjectOrSet;
|
||||
type R = Relation;
|
||||
type D = Object;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RelationSet {
|
||||
src_to_dst: BTreeMap<Arc<S>, HashMap<Arc<R>, HashSet<Arc<D>>>>,
|
||||
dst_to_src: BTreeMap<Arc<D>, HashMap<Arc<R>, HashSet<Arc<S>>>>,
|
||||
nodes: RwLock<BTreeSet<Arc<Node>>>,
|
||||
}
|
||||
|
||||
impl RelationSet {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
src_to_dst: BTreeMap::new(),
|
||||
dst_to_src: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, src: impl Into<S>, rel: impl Into<R>, dst: impl Into<D>) {
|
||||
let src = Arc::new(src.into());
|
||||
let rel = Arc::new(rel.into());
|
||||
let dst = Arc::new(dst.into());
|
||||
|
||||
if let Some(rels_dsts) = self.src_to_dst.get_mut(&src) {
|
||||
if let Some(dsts) = rels_dsts.get_mut(&rel) {
|
||||
dsts.insert(dst.clone());
|
||||
} else {
|
||||
let mut dsts = HashSet::new();
|
||||
dsts.insert(dst.clone());
|
||||
rels_dsts.insert(rel.clone(), dsts);
|
||||
}
|
||||
} else {
|
||||
let mut rels_dsts = HashMap::new();
|
||||
let mut dsts = HashSet::new();
|
||||
dsts.insert(dst.clone());
|
||||
rels_dsts.insert(rel.clone(), dsts);
|
||||
self.src_to_dst.insert(src.clone(), rels_dsts);
|
||||
}
|
||||
|
||||
if let Some(rels_srcs) = self.dst_to_src.get_mut(&dst) {
|
||||
if let Some(srcs) = rels_srcs.get_mut(&rel) {
|
||||
srcs.insert(src.clone());
|
||||
} else {
|
||||
let mut srcs = HashSet::new();
|
||||
srcs.insert(src.clone());
|
||||
rels_srcs.insert(rel.clone(), srcs);
|
||||
}
|
||||
} else {
|
||||
let mut rels_srcs = HashMap::new();
|
||||
let mut srcs = HashSet::new();
|
||||
srcs.insert(src.clone());
|
||||
rels_srcs.insert(rel.clone(), srcs);
|
||||
self.dst_to_src.insert(dst.clone(), rels_srcs);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, src: impl Into<S>, rel: impl Into<R>, dst: impl Into<D>) {
|
||||
pub async fn insert(&self, src: impl Into<NodeId>, dst: impl Into<NodeId>) {
|
||||
let src = src.into();
|
||||
let rel = rel.into();
|
||||
let dst = dst.into();
|
||||
|
||||
if let Some(dsts) = self
|
||||
.src_to_dst
|
||||
.get_mut(&src)
|
||||
.and_then(|rels_dsts| rels_dsts.get_mut(&rel))
|
||||
{
|
||||
dsts.remove(&dst);
|
||||
let mut nodes = self.nodes.write().await;
|
||||
|
||||
let src_node = match nodes.get(&src) {
|
||||
Some(node) => node.clone(),
|
||||
None => {
|
||||
let node = Arc::new(Node {
|
||||
id: src,
|
||||
edges_out: RwLock::new(vec![]),
|
||||
edges_in: RwLock::new(vec![]),
|
||||
});
|
||||
nodes.insert(node.clone());
|
||||
node
|
||||
}
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
if let Some(srcs) = self
|
||||
.dst_to_src
|
||||
.get_mut(&dst)
|
||||
.and_then(|rels_srcs| rels_srcs.get_mut(&rel))
|
||||
{
|
||||
srcs.remove(&src);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_by_src(&mut self, src: &S) {
|
||||
for (rel, dsts) in self.src_to_dst.remove(src).iter().flat_map(|x| x.iter()) {
|
||||
for dst in dsts {
|
||||
if let Some(srcs) = self
|
||||
.dst_to_src
|
||||
.get_mut(dst)
|
||||
.and_then(|rels_srcs| rels_srcs.get_mut(rel))
|
||||
{
|
||||
srcs.remove(src);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_by_dst(&mut self, dst: &D) {
|
||||
for (rel, srcs) in self.dst_to_src.remove(dst).iter().flat_map(|x| x.iter()) {
|
||||
for src in srcs {
|
||||
if let Some(dsts) = self
|
||||
.src_to_dst
|
||||
.get_mut(src)
|
||||
.and_then(|rels_dsts| rels_dsts.get_mut(rel))
|
||||
{
|
||||
dsts.remove(dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has(&self, src: impl Into<S>, rel: impl Into<R>, dst: impl Into<D>) -> bool {
|
||||
pub async fn remove(&self, src: impl Into<NodeId>, dst: impl Into<NodeId>) {
|
||||
let src = src.into();
|
||||
let rel = rel.into();
|
||||
let dst = dst.into();
|
||||
|
||||
self.src_to_dst
|
||||
.get(&src)
|
||||
.and_then(|rels_dsts| rels_dsts.get(&rel))
|
||||
.and_then(|dsts| dsts.get(&dst))
|
||||
.is_some()
|
||||
let mut nodes = self.nodes.write().await;
|
||||
|
||||
let src = nodes.get(&src).cloned();
|
||||
let dst = nodes.get(&dst).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);
|
||||
}
|
||||
if dst.edges_in.read().await.is_empty() && dst.edges_out.read().await.is_empty() {
|
||||
nodes.remove(&dst.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_object<'a>(&self, obj: impl Into<&'a Object>) -> bool {
|
||||
let obj = obj.into();
|
||||
let has_dst_obj = self.dst_to_src.contains_key(obj);
|
||||
pub async fn has(&self, src: impl Into<NodeId>, dst: impl Into<NodeId>) -> bool {
|
||||
let src = src.into();
|
||||
let dst = dst.into();
|
||||
|
||||
let cursor = self
|
||||
.src_to_dst
|
||||
.lower_bound(Bound::Included(&ObjectOrSet::Object(obj.clone())));
|
||||
|
||||
let has_src_obj = if let Some(key) = cursor.key() {
|
||||
obj.namespace == key.object().namespace && obj.id == key.object().id
|
||||
} else {
|
||||
false
|
||||
let (src, dst) = {
|
||||
let nodes = self.nodes.read().await;
|
||||
(nodes.get(&src).cloned(), nodes.get(&dst).cloned())
|
||||
};
|
||||
|
||||
has_dst_obj || has_src_obj
|
||||
}
|
||||
|
||||
pub fn has_recursive(
|
||||
&self,
|
||||
src: impl Into<S>,
|
||||
rel: impl Into<R>,
|
||||
dst: impl Into<D>,
|
||||
limit: u32,
|
||||
) -> bool {
|
||||
let src = src.into();
|
||||
let rel = rel.into();
|
||||
let dst = dst.into();
|
||||
|
||||
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
|
||||
.src_to_dst
|
||||
.get(&src)
|
||||
.iter()
|
||||
.flat_map(|x| x.iter())
|
||||
.flat_map(|(r, d)| d.iter().map(|d| (r.clone(), d.clone())))
|
||||
{
|
||||
if *nrel == rel && *ndst == dst {
|
||||
return true;
|
||||
}
|
||||
dist.insert((ndst.clone(), nrel.clone()), 1);
|
||||
q.push(Distanced::one((ndst, nrel)));
|
||||
}
|
||||
|
||||
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
|
||||
.get(&node)
|
||||
.iter()
|
||||
.flat_map(|x| x.iter())
|
||||
.flat_map(|(r, d)| d.iter().map(|d| (r.clone(), d.clone())))
|
||||
{
|
||||
if *nrel == rel && *ndst == dst {
|
||||
return true;
|
||||
}
|
||||
if let Some(existing_node_dist) = dist.get(&*distanced) {
|
||||
if *existing_node_dist <= node_dist {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
dist.insert((ndst.clone(), nrel.clone()), node_dist);
|
||||
q.push(Distanced::one((ndst, nrel)));
|
||||
}
|
||||
}
|
||||
if let (Some(src), Some(dst)) = (src, dst) {
|
||||
src.edges_out.read().await.contains(&dst)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn related_to(
|
||||
pub async fn has_recursive<'a>(
|
||||
&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());
|
||||
src: impl Into<NodeId>,
|
||||
dst: impl Into<NodeId>,
|
||||
limit: Option<u32>,
|
||||
) -> bool {
|
||||
let src = src.into();
|
||||
let dst = dst.into();
|
||||
|
||||
let mut related: Vec<(Relation, Object)> = vec![];
|
||||
let src = self.nodes.read().await.get(&src).unwrap().clone();
|
||||
|
||||
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)
|
||||
let src_neighbors = src
|
||||
.edges_out
|
||||
.read()
|
||||
.await
|
||||
.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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
.map(|x| Distanced::one(x.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut q: BinaryHeap<Distanced<Arc<Node>>> = BinaryHeap::from(src_neighbors);
|
||||
let mut visited: HashSet<Arc<Node>> = HashSet::new();
|
||||
|
||||
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)));
|
||||
}
|
||||
if distanced.id == dst {
|
||||
return true;
|
||||
}
|
||||
if let Some(limit) = limit {
|
||||
if distanced.distance() > limit {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
for neighbor in distanced.edges_out.read().await.iter() {
|
||||
if !visited.contains(neighbor) {
|
||||
q.push(Distanced::new(neighbor.clone(), distanced.distance() + 1))
|
||||
}
|
||||
}
|
||||
|
||||
related
|
||||
visited.insert(distanced.clone());
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
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())
|
||||
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());
|
||||
file.write_all("\n".as_bytes()).await.unwrap();
|
||||
file.write_all(format!("[{}:{}]\n", ¤t.0, ¤t.1).as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
for (rel, srcs) in rels_srcs.iter() {
|
||||
if srcs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let srcs = srcs
|
||||
|
||||
let srcs = node
|
||||
.edges_in
|
||||
.read()
|
||||
.await
|
||||
.iter()
|
||||
.map(|src| {
|
||||
let src_obj = src.object();
|
||||
let src_str = if src_obj.namespace == dst.namespace && src_obj.id == dst.id
|
||||
{
|
||||
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_obj.namespace, src_obj.id)
|
||||
};
|
||||
match &**src {
|
||||
ObjectOrSet::Object(_) => src_str,
|
||||
ObjectOrSet::Set(set) => {
|
||||
format!("{}#{}", src_str, set.1 .0)
|
||||
}
|
||||
format!("{}:{}", &src.id.namespace, &src.id.id)
|
||||
}
|
||||
})
|
||||
.reduce(|acc, x| acc + ", " + &x)
|
||||
.unwrap_or_default();
|
||||
|
||||
file.write_all(format!("{} = [{}]\n", &rel.0, &srcs).as_bytes())
|
||||
if let Some(rel) = &node.id.relation {
|
||||
file.write_all(format!("{} = [ {} ]\n", &rel, &srcs).as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
file.write_all("\n".as_bytes()).await.unwrap();
|
||||
}
|
||||
}
|
||||
pub async fn from_file(file: &mut File) -> Self {
|
||||
let reader = BufReader::new(file);
|
||||
let mut lines = reader.lines();
|
||||
let mut graph = Self::new();
|
||||
let graph = Self::default();
|
||||
let mut node: Option<(String, String)> = None;
|
||||
while let Ok(Some(line)) = lines.next_line().await {
|
||||
if line.starts_with('[') && line.ends_with(']') {
|
||||
|
@ -408,10 +206,10 @@ impl RelationSet {
|
|||
let arr_stop = line.find(']').unwrap();
|
||||
|
||||
let rel = line[..equals_pos].trim();
|
||||
let arr = line[arr_start + 1..arr_stop].split(", ");
|
||||
let arr = line[arr_start + 1..arr_stop].trim().split(", ");
|
||||
|
||||
for obj in arr {
|
||||
let src: ObjectOrSet = if obj.contains('#') {
|
||||
let src: NodeId = if obj.contains('#') {
|
||||
let sep_1 = obj.find(':');
|
||||
let sep_2 = obj.find('#').unwrap();
|
||||
|
||||
|
@ -435,7 +233,9 @@ impl RelationSet {
|
|||
(namespace, id).into()
|
||||
};
|
||||
|
||||
graph.insert(src, rel, dst.clone());
|
||||
graph
|
||||
.insert(src, (dst.0.as_str(), dst.1.as_str(), rel))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -444,13 +244,51 @@ impl RelationSet {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
struct Distanced<T> {
|
||||
distance: u32,
|
||||
data: T,
|
||||
impl Debug for Node {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Node").field("id", &self.id).finish()
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_edge(from: Arc<Node>, to: Arc<Node>) {
|
||||
from.edges_out.write().await.push(to.clone());
|
||||
to.edges_in.write().await.push(from);
|
||||
}
|
||||
|
||||
impl Borrow<NodeId> for Arc<Node> {
|
||||
fn borrow(&self) -> &NodeId {
|
||||
&self.id
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Node {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
impl Eq for Node {}
|
||||
|
||||
impl PartialOrd for Node {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.id.partial_cmp(&other.id)
|
||||
}
|
||||
}
|
||||
impl Ord for Node {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.id.cmp(&other.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Node {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Distanced<T> {
|
||||
pub fn new(data: T, distance: u32) -> Self {
|
||||
Self { distance, data }
|
||||
}
|
||||
pub fn one(data: T) -> Self {
|
||||
Self { distance: 1, data }
|
||||
}
|
||||
|
@ -478,111 +316,62 @@ impl<T: Eq> Ord for Distanced<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Relation {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.0.partial_cmp(&other.0)
|
||||
}
|
||||
}
|
||||
impl Ord for Relation {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.0.cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for ObjectOrSet {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
match (
|
||||
self.object().partial_cmp(other.object()),
|
||||
self.relation(),
|
||||
other.relation(),
|
||||
) {
|
||||
(Some(Ordering::Equal), self_rel, other_rel) => self_rel.partial_cmp(&other_rel),
|
||||
(ord, _, _) => ord,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Ord for ObjectOrSet {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.object()
|
||||
.cmp(other.object())
|
||||
.then(self.relation().cmp(&other.relation()))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Object {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
match self.namespace.partial_cmp(&other.namespace) {
|
||||
Some(core::cmp::Ordering::Equal) => self.id.partial_cmp(&other.id),
|
||||
ord => ord,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Ord for Object {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.namespace
|
||||
.cmp(&other.namespace)
|
||||
.then(self.id.cmp(&other.id))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&str, &str)> for ObjectOrSet {
|
||||
fn from((namespace, id): (&str, &str)) -> Self {
|
||||
ObjectOrSet::Object(Object {
|
||||
namespace: namespace.into(),
|
||||
id: id.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
impl From<(&str, &str, &str)> for ObjectOrSet {
|
||||
fn from((namespace, id, rel): (&str, &str, &str)) -> Self {
|
||||
ObjectOrSet::Set(((namespace, id).into(), Relation(rel.into())))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&str, &str)> for Object {
|
||||
fn from((namespace, id): (&str, &str)) -> Self {
|
||||
impl From<(&str, &str)> for NodeId {
|
||||
fn from(value: (&str, &str)) -> Self {
|
||||
Self {
|
||||
namespace: namespace.into(),
|
||||
id: id.into(),
|
||||
namespace: value.0.to_string(),
|
||||
id: value.1.to_string(),
|
||||
relation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<(String, String)> for Object {
|
||||
fn from((namespace, id): (String, String)) -> Self {
|
||||
|
||||
impl From<(&str, &str, &str)> for NodeId {
|
||||
fn from(value: (&str, &str, &str)) -> Self {
|
||||
Self {
|
||||
namespace: namespace.into(),
|
||||
id: id.into(),
|
||||
namespace: value.0.to_string(),
|
||||
id: value.1.to_string(),
|
||||
relation: Some(value.2.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Relation {
|
||||
fn from(value: &str) -> Self {
|
||||
Relation(value.into())
|
||||
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> for Relation {
|
||||
fn from(value: String) -> Self {
|
||||
Relation(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectOrSet {
|
||||
pub fn object(&self) -> &Object {
|
||||
match self {
|
||||
ObjectOrSet::Object(obj) => obj,
|
||||
ObjectOrSet::Set((obj, _)) => obj,
|
||||
}
|
||||
}
|
||||
pub fn relation(&self) -> Option<&Relation> {
|
||||
match self {
|
||||
ObjectOrSet::Object(_) => None,
|
||||
ObjectOrSet::Set((_, rel)) => Some(rel),
|
||||
impl From<(String, String)> for NodeId {
|
||||
fn from(value: (String, String)) -> Self {
|
||||
Self {
|
||||
namespace: value.0,
|
||||
id: value.1,
|
||||
relation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Relation {
|
||||
pub fn new(relation: &str) -> Self {
|
||||
Self(relation.into())
|
||||
|
||||
impl From<(String, String, String)> for NodeId {
|
||||
fn from(value: (String, String, String)) -> Self {
|
||||
Self {
|
||||
namespace: value.0,
|
||||
id: value.1,
|
||||
relation: Some(value.2),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(String, String, Option<String>)> for NodeId {
|
||||
fn from(value: (String, String, Option<String>)) -> Self {
|
||||
Self {
|
||||
namespace: value.0,
|
||||
id: value.1,
|
||||
relation: value.2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue