openid connect integration

This commit is contained in:
Paul Zinselmeyer 2023-08-27 22:10:16 +02:00
parent faf4e82f88
commit 66fac9d185
Signed by: pfzetto
GPG Key ID: 4EEF46A5B276E648
8 changed files with 1304 additions and 1257 deletions

990
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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
View 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 ];
};
}
);
}

View File

@ -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;
}

View File

@ -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))
}

View File

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

View File

@ -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;
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);
}
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
}
}
}
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);
}
};
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;
}
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
if let (Some(src), Some(dst)) = (src, dst) {
src.edges_out.read().await.contains(&dst)
} else {
false
}
}
pub fn has_recursive(
pub async fn has_recursive<'a>(
&self,
src: impl Into<S>,
rel: impl Into<R>,
dst: impl Into<D>,
limit: u32,
src: impl Into<NodeId>,
dst: impl Into<NodeId>,
limit: Option<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();
let src = self.nodes.read().await.get(&src).unwrap().clone();
for (nrel, ndst) in self
.src_to_dst
.get(&src)
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())))
{
if *nrel == rel && *ndst == dst {
return true;
}
dist.insert((ndst.clone(), nrel.clone()), 1);
q.push(Distanced::one((ndst, nrel)));
}
.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;
if distanced.id == dst {
return true;
}
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(limit) = limit {
if distanced.distance() > limit {
return false;
}
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)));
}
for neighbor in distanced.edges_out.read().await.iter() {
if !visited.contains(neighbor) {
q.push(Distanced::new(neighbor.clone(), distanced.distance() + 1))
}
}
visited.insert(distanced.clone());
}
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())
.await
.unwrap();
for (rel, srcs) in rels_srcs.iter() {
if srcs.is_empty() {
continue;
}
let srcs = srcs
.iter()
.map(|src| {
let src_obj = src.object();
let src_str = if src_obj.namespace == dst.namespace && src_obj.id == dst.id
{
"self".to_string()
} else {
format!("{}:{}", src_obj.namespace, src_obj.id)
};
match &**src {
ObjectOrSet::Object(_) => src_str,
ObjectOrSet::Set(set) => {
format!("{}#{}", src_str, set.1 .0)
}
}
})
.reduce(|acc, x| acc + ", " + &x)
.unwrap_or_default();
file.write_all(format!("{} = [{}]\n", &rel.0, &srcs).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", &current.0, &current.1).as_bytes())
.await
.unwrap();
}
let srcs = node
.edges_in
.read()
.await
.iter()
.map(|src| {
if src.id.namespace == current.0 && src.id.id == current.1 {
"self".to_string()
} else if let Some(rel) = &src.id.relation {
format!("{}:{}#{}", &src.id.namespace, &src.id.id, &rel)
} else {
format!("{}:{}", &src.id.namespace, &src.id.id)
}
})
.reduce(|acc, x| acc + ", " + &x)
.unwrap_or_default();
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<String> for Relation {
fn from(value: String) -> 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 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,
}
}
}