Skip to content

Commit

Permalink
Merge pull request #4778 from systeminit/spicedb_grpc_part2
Browse files Browse the repository at this point in the history
feat: add spicedb client relationship and permission calls
  • Loading branch information
sprutton1 authored Oct 14, 2024
2 parents 111a569 + 44f13c4 commit 973898e
Show file tree
Hide file tree
Showing 7 changed files with 376 additions and 5 deletions.
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion component/spicedb/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env sh
set -eu

spicedb serve >>/tmp/spicedb.log 2>&1 &
spicedb serve-testing >>/tmp/spicedb.log 2>&1 &
tail -f /tmp/spicedb.log &
sleep 3

Expand Down
3 changes: 3 additions & 0 deletions lib/si-data-spicedb/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ rust_library(
deps = [
"//lib/si-std:si-std",
"//lib/telemetry-rs:telemetry",
"//third-party/rust:futures",
"//third-party/rust:remain",
"//third-party/rust:serde",
"//third-party/rust:spicedb-client",
"//third-party/rust:spicedb-grpc",
"//third-party/rust:strum",
"//third-party/rust:thiserror",
"//third-party/rust:tokio",
"//third-party/rust:url",
Expand All @@ -27,6 +29,7 @@ rust_test(
name = "test-integration",
deps = [
"//third-party/rust:indoc",
"//third-party/rust:rand",
"//third-party/rust:tokio",
":si-data-spicedb",
],
Expand Down
3 changes: 3 additions & 0 deletions lib/si-data-spicedb/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,18 @@ rust-version.workspace = true
publish.workspace = true

[dependencies]
futures = { workspace = true }
remain = { workspace = true }
serde = { workspace = true }
si-std = { path = "../../lib/si-std" }
spicedb-client = { workspace = true }
spicedb-grpc = { workspace = true }
strum = { workspace = true }
telemetry = { path = "../../lib/telemetry-rs" }
thiserror = { workspace = true }
tokio = { workspace = true }
url = { workspace = true }

[dev-dependencies]
indoc = { workspace = true }
rand = { workspace = true }
153 changes: 151 additions & 2 deletions lib/si-data-spicedb/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@
// )]
// #![allow(clippy::missing_errors_doc)]

use futures::TryStreamExt;
use std::{io, net::ToSocketAddrs, result, sync::Arc};

use serde::{Deserialize, Serialize};
use si_std::SensitiveString;
use spicedb_client::SpicedbClient;
use spicedb_client::{builder::WriteRelationshipsRequestBuilder, SpicedbClient};
use spicedb_grpc::authzed::api::v1::{relationship_update::Operation, WriteRelationshipsRequest};
use telemetry::prelude::*;
use thiserror::Error;
use types::Relationships;
use url::Url;

mod types;

pub use types::{ReadSchemaResponse, ZedToken};
pub use types::{Permission, ReadSchemaResponse, Relationship, ZedToken};

#[remain::sorted]
#[derive(Error, Debug)]
Expand All @@ -29,6 +32,8 @@ pub enum Error {
EndpointNoHost(Url),
#[error("cannot determine spicedb endpoint port number: {0}")]
EndpointUnknownPort(Url),
#[error("GRPC streaming error: {0}")]
GRPC(#[source] spicedb_client::result::Error),
#[error("error resolving ip addr for spicedb endpoint hostname: {0}")]
ResolveHostname(#[source] io::Error),
#[error("resolved hostname returned no entries")]
Expand Down Expand Up @@ -201,6 +206,150 @@ impl Client {
span.record_ok();
Ok(resp)
}

#[instrument(
name = "spicedb_client.read_relationships",
level = "debug",
skip_all,
fields(
db.connection_string = %self.metadata.db_connection_string(),
db.system = %self.metadata.db_system(),
network.peer.address = self.metadata.network_peer_address(),
network.protocol.name = self.metadata.network_protocol_name(),
network.transport = self.metadata.network_transport(),
otel.kind = SpanKind::Client.as_str(),
otel.status_code = Empty,
otel.status_message = Empty,
server.address = self.metadata.server_address(),
server.port = self.metadata.server_port(),
),
)]
pub async fn read_relationship(&mut self, relationship: Relationship) -> Result<Relationships> {
let span = current_span_for_instrument_at!("debug");
let mut relationships = vec![];

let results: result::Result<Vec<_>, _> = self
.inner
.read_relationships(relationship.into_request())
.await?
.try_collect()
.await;

for r in results.map_err(|e| span.record_err(Error::GRPC(e.into())))? {
if let Some(relationship) = r.relationship {
relationships.push(relationship.into());
}
}

span.record_ok();
Ok(relationships)
}

#[instrument(
name = "spicedb_client.create_relationships",
level = "debug",
skip_all,
fields(
db.connection_string = %self.metadata.db_connection_string(),
db.system = %self.metadata.db_system(),
network.peer.address = self.metadata.network_peer_address(),
network.protocol.name = self.metadata.network_protocol_name(),
network.transport = self.metadata.network_transport(),
otel.kind = SpanKind::Client.as_str(),
otel.status_code = Empty,
otel.status_message = Empty,
server.address = self.metadata.server_address(),
server.port = self.metadata.server_port(),
),
)]
pub async fn create_relationships(
&mut self,
relationships: Relationships,
) -> Result<Option<ZedToken>> {
self.update_relationships(relationships, Operation::Create)
.await
}

#[instrument(
name = "spicedb_client.delete_relationships",
level = "debug",
skip_all,
fields(
db.connection_string = %self.metadata.db_connection_string(),
db.system = %self.metadata.db_system(),
network.peer.address = self.metadata.network_peer_address(),
network.protocol.name = self.metadata.network_protocol_name(),
network.transport = self.metadata.network_transport(),
otel.kind = SpanKind::Client.as_str(),
otel.status_code = Empty,
otel.status_message = Empty,
server.address = self.metadata.server_address(),
server.port = self.metadata.server_port(),
),
)]
pub async fn delete_relationships(
&mut self,
relationships: Relationships,
) -> Result<Option<ZedToken>> {
self.update_relationships(relationships, Operation::Delete)
.await
}

async fn update_relationships(
&mut self,
relationships: Relationships,
operation: Operation,
) -> Result<Option<ZedToken>> {
let span = current_span_for_instrument_at!("debug");

let request: WriteRelationshipsRequest = WriteRelationshipsRequest::new(
relationships
.into_iter()
.map(|r| r.into_relationship_update(operation)),
);

let resp = self
.inner
.write_relationships(request)
.await
.map_err(|err| span.record_err(Error::SpiceDb(err)))?
.written_at
.map(|value| value.into());

span.record_ok();
Ok(resp)
}

#[instrument(
name = "spicedb_client.check_permissions",
level = "debug",
skip_all,
fields(
db.connection_string = %self.metadata.db_connection_string(),
db.system = %self.metadata.db_system(),
network.peer.address = self.metadata.network_peer_address(),
network.protocol.name = self.metadata.network_protocol_name(),
network.transport = self.metadata.network_transport(),
otel.kind = SpanKind::Client.as_str(),
otel.status_code = Empty,
otel.status_message = Empty,
server.address = self.metadata.server_address(),
server.port = self.metadata.server_port(),
),
)]
pub async fn check_permissions(&mut self, permission: Permission) -> Result<bool> {
let span = current_span_for_instrument_at!("debug");

let resp = self
.inner
.check_permission(permission.into_request())
.await
.map_err(|err| span.record_err(Error::SpiceDb(err)))?
.permissionship;

span.record_ok();
Ok(Permission::has_permission(resp))
}
}

#[derive(Clone, Debug, Deserialize, Serialize)]
Expand Down
109 changes: 108 additions & 1 deletion lib/si-data-spicedb/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::ops;

use spicedb_grpc::authzed::api::v1;
use spicedb_client::builder::{RelationshipBuilder, RelationshipFilterBuilder};
use spicedb_grpc::authzed::api::v1::{self, ObjectReference, SubjectReference};

/// ZedToken is used to provide causality metadata between Write and Check requests.
///
Expand Down Expand Up @@ -49,3 +50,109 @@ impl From<v1::ReadSchemaResponse> for ReadSchemaResponse {
}
}
}

pub type Relationships = Vec<Relationship>;
#[derive(Clone, Debug)]
pub struct Relationship(pub(crate) v1::Relationship);

impl Relationship {
pub fn new(
object_type: impl ToString,
object_id: impl ToString,
relation: impl ToString,
subject_type: impl ToString,
subject_id: impl ToString,
) -> Self {
Self(<v1::Relationship as RelationshipBuilder>::new(
object_type,
object_id,
relation,
subject_type,
subject_id,
))
}

pub fn into_request(self) -> v1::ReadRelationshipsRequest {
let inner = self.0;
let mut builder = <v1::ReadRelationshipsRequest as RelationshipFilterBuilder>::new();

if let Some(resource) = inner.resource {
builder.resource_type(resource.object_type);
}

builder.relation(inner.relation);

builder
}

pub(crate) fn inner(self) -> v1::Relationship {
self.0
}

pub(crate) fn into_relationship_update(
self,
operation: v1::relationship_update::Operation,
) -> v1::RelationshipUpdate {
spicedb_grpc::authzed::api::v1::RelationshipUpdate {
operation: operation.into(),
relationship: Some(self.inner()),
}
}
}

impl From<v1::Relationship> for Relationship {
fn from(value: v1::Relationship) -> Self {
Relationship(value)
}
}

#[derive(Clone, Debug)]
pub struct Permission {
resource_type: String,
resource_id: String,
permission: String,
subject_type: String,
subject_id: String,
}

impl Permission {
pub fn new(
resource_type: impl ToString,
resource_id: impl ToString,
permission: impl ToString,
subject_type: impl ToString,
subject_id: impl ToString,
) -> Self {
Self {
resource_type: resource_type.to_string(),
resource_id: resource_id.to_string(),
permission: permission.to_string(),
subject_type: subject_type.to_string(),
subject_id: subject_id.to_string(),
}
}

pub(crate) fn into_request(self) -> v1::CheckPermissionRequest {
v1::CheckPermissionRequest {
consistency: None,
resource: Some(ObjectReference {
object_type: self.resource_type,
object_id: self.resource_id,
}),
permission: self.permission,
subject: Some(SubjectReference {
object: Some(ObjectReference {
object_type: self.subject_type,
object_id: self.subject_id,
}),
optional_relation: "".to_string(),
}),
context: None,
with_tracing: false,
}
}

pub(crate) fn has_permission(permissionship: i32) -> bool {
i32::from(v1::check_permission_response::Permissionship::HasPermission) == permissionship
}
}
Loading

0 comments on commit 973898e

Please sign in to comment.