// TODO: This whole thing depends on having spice server running...which I do not want // in a public secd library (or really at all). We will eventually get rid of this in // favor of a light weight solution that leverages the Zanzibar API but disregards the // scaling part. #[allow(clippy::module_inception)] pub mod spice { tonic::include_proto!("authzed.api.v1"); } use spice::permissions_service_client::PermissionsServiceClient; use spice::schema_service_client::SchemaServiceClient; use spice::WriteSchemaRequest; use std::matches; use tonic::metadata::MetadataValue; use tonic::transport::Channel; use tonic::{Request, Response, Status, Streaming}; use crate::auth::z::{self, Subject}; use crate::client::spice::spice::{ relationship_update, ObjectReference, Relationship, RelationshipUpdate, SubjectReference, }; use self::spice::check_permission_response::Permissionship; use self::spice::{ consistency, CheckPermissionRequest, Consistency, LookupResourcesRequest, LookupResourcesResponse, WriteRelationshipsRequest, }; #[derive(Debug, thiserror::Error, derive_more::Display)] pub enum SpiceError { TonicTransport(#[from] tonic::transport::Error), TonicStatus(#[from] tonic::Status), } pub(crate) struct Spice { channel: Channel, secret: String, } impl Spice { pub async fn new(secret: String, server: String) -> Self { let channel = Channel::from_shared(server) .expect("invalid SPICE_SERVER uri") .connect() .await .expect("initialization error: Spice failed to connect to DB."); Spice { channel, secret } } pub async fn lookup_resources( &self, ns: &str, relation: &str, subj: &Subject, ) -> Result, SpiceError> { let mut client = PermissionsServiceClient::with_interceptor(self.channel.clone(), |req: Request<()>| { self.intercept(req) }); let request = tonic::Request::new(LookupResourcesRequest { consistency: Some(Consistency { requirement: Some(consistency::Requirement::MinimizeLatency(true)), }), resource_object_type: ns.to_string(), permission: relation.to_string(), subject: Some(SubjectReference::from(subj)), context: None, }); let mut res = vec![]; let mut response: Streaming = client.lookup_resources(request).await?.into_inner(); if let Some(d) = response.message().await? { res.push(d.resource_object_id); } Ok(res) } pub async fn check_permission(&self, r: &z::Relationship) -> Result { let mut client = PermissionsServiceClient::with_interceptor(self.channel.clone(), |req: Request<()>| { self.intercept(req) }); let request = tonic::Request::new(CheckPermissionRequest { consistency: Some(Consistency { requirement: Some(consistency::Requirement::MinimizeLatency(true)), }), resource: Some(ObjectReference::from(&r.object)), permission: r.relation.clone(), subject: Some(SubjectReference::from(&r.subject)), context: None, }); let response = client.check_permission(request).await?.into_inner(); Ok(matches!( Permissionship::from_i32(response.permissionship), Some(Permissionship::HasPermission) )) } pub async fn write_relationship(&self, rs: &[z::Relationship]) -> Result<(), SpiceError> { let mut client = PermissionsServiceClient::with_interceptor(self.channel.clone(), |req: Request<()>| { self.intercept(req) }); let request = tonic::Request::new(WriteRelationshipsRequest { updates: rs .iter() .map(|t| RelationshipUpdate { operation: (relationship_update::Operation::Touch as i32), relationship: Some(Relationship { resource: Some(ObjectReference::from(&t.object)), relation: t.relation.clone(), subject: Some(SubjectReference::from(&t.subject)), optional_caveat: None, }), }) .collect(), optional_preconditions: vec![], }); client.write_relationships(request).await?; Ok(()) } pub async fn write_schema(&self, schema: &str) -> Result<(), SpiceError> { let mut client = SchemaServiceClient::with_interceptor(self.channel.clone(), |req: Request<()>| { self.intercept(req) }); let request = tonic::Request::new(WriteSchemaRequest { schema: schema.into(), }); client.write_schema(request).await?; Ok(()) } fn intercept(&self, mut req: Request<()>) -> Result, Status> { req.metadata_mut().insert( "authorization", MetadataValue::from_str(&format!("Bearer {}", self.secret)).unwrap(), ); Ok(req) } } impl From<&z::Subject> for SubjectReference { fn from(s: &z::Subject) -> Self { let tup = match s { Subject::User(u) => (u.0.clone(), u.1.clone().to_string(), "".to_string()), Subject::UserSet { user, relation } => { (user.0.clone(), user.1.clone().to_string(), relation.clone()) } }; SubjectReference { object: Some(ObjectReference { object_type: tup.0, object_id: tup.1, }), optional_relation: tup.2, } } } impl From<&z::Object> for ObjectReference { fn from(o: &z::Object) -> Self { ObjectReference { object_type: o.0.clone(), object_id: o.1.clone().to_string(), } } }