spicedb_client/
client.rs

1use bytes::Bytes;
2use spicedb_grpc::authzed::api::v1::{
3    permissions_service_client::PermissionsServiceClient,
4    schema_service_client::SchemaServiceClient, watch_service_client::WatchServiceClient, *,
5};
6use tonic::{
7    metadata::{Ascii, MetadataValue},
8    service::{interceptor::InterceptedService, Interceptor},
9    transport::Channel,
10    Request, Status, Streaming,
11};
12
13use crate::result::Result;
14
15/// SpiceDB client
16#[derive(Clone, Debug)]
17pub struct SpicedbClient {
18    pub channel: Channel,
19    schemas: SchemaServiceClient<InterceptedService<Channel, SpicedbMiddleware>>,
20    permissions: PermissionsServiceClient<InterceptedService<Channel, SpicedbMiddleware>>,
21    watch: WatchServiceClient<InterceptedService<Channel, SpicedbMiddleware>>,
22}
23
24impl SpicedbClient {
25    /// Create a new [`SpicedbClient`] from the server URL and a preshared key.
26    ///
27    /// ```rust
28    /// # use spicedb_client::SpicedbClient;
29    /// #
30    /// # async fn create_client() {
31    /// let mut client =
32    ///     SpicedbClient::from_url_and_preshared_key("http://localhost:50051", "spicedb")
33    ///         .await
34    ///         .unwrap();
35    /// # }
36    /// ```
37    pub async fn from_url_and_preshared_key(
38        url: impl Into<Bytes>,
39        preshared_key: impl ToString,
40    ) -> Result<Self> {
41        let interceptor = SpicedbMiddleware {
42            preshared_key: Box::new(format!("bearer {}", preshared_key.to_string()).parse()?),
43        };
44
45        let channel = Channel::from_shared(url)?.connect().await?;
46
47        let schemas = SchemaServiceClient::with_interceptor(channel.clone(), interceptor.clone());
48
49        let permissions =
50            PermissionsServiceClient::with_interceptor(channel.clone(), interceptor.clone());
51
52        let watch = WatchServiceClient::with_interceptor(channel.clone(), interceptor.clone());
53
54        Ok(SpicedbClient {
55            channel,
56            schemas,
57            permissions,
58            watch,
59        })
60    }
61
62    /// Read the current Object Definitions for a Permissions System.
63    ///
64    /// Errors include:
65    /// - INVALID_ARGUMENT: a provided value has failed to semantically validate
66    /// - NOT_FOUND: no schema has been defined
67    pub async fn read_schema(&mut self) -> Result<ReadSchemaResponse> {
68        let response = self
69            .schemas
70            .read_schema(ReadSchemaRequest {})
71            .await
72            .unwrap()
73            .into_inner();
74
75        Ok(response)
76    }
77
78    /// Overwrite the current Object Definitions for a Permissions System.
79    pub async fn write_schema(&mut self, schema: impl ToString) -> Result<WriteSchemaResponse> {
80        let response = self
81            .schemas
82            .write_schema(WriteSchemaRequest {
83                schema: schema.to_string(),
84            })
85            .await?
86            .into_inner();
87
88        Ok(response)
89    }
90
91    /// Read a set of the relationships matching one or more filters.
92    pub async fn read_relationships(
93        &mut self,
94        request: ReadRelationshipsRequest,
95    ) -> Result<Streaming<ReadRelationshipsResponse>> {
96        let stream = self
97            .permissions
98            .read_relationships(request)
99            .await?
100            .into_inner();
101
102        Ok(stream)
103    }
104
105    /// Atomically write and/or delete a set of specified relationships. An
106    /// optional set of preconditions can be provided that must be satisfied for
107    /// the operation to commit.
108    pub async fn write_relationships(
109        &mut self,
110        request: WriteRelationshipsRequest,
111    ) -> Result<WriteRelationshipsResponse> {
112        let response = self
113            .permissions
114            .write_relationships(request)
115            .await?
116            .into_inner();
117
118        Ok(response)
119    }
120
121    /// Atomically bulk delete all relationships matching the provided filter.
122    /// If no relationships match, none will be deleted and the operation will
123    /// succeed. An optional set of preconditions can be provided that must be
124    /// satisfied for the operation to commit.
125    pub async fn delete_relationships(
126        &mut self,
127        request: DeleteRelationshipsRequest,
128    ) -> Result<DeleteRelationshipsResponse> {
129        let response = self
130            .permissions
131            .delete_relationships(request)
132            .await?
133            .into_inner();
134
135        Ok(response)
136    }
137
138    /// Determine, for a given resource, whether a subject computes to having a
139    /// permission or is a direct member of a particular relation.
140    pub async fn check_permission(
141        &mut self,
142        request: CheckPermissionRequest,
143    ) -> Result<CheckPermissionResponse> {
144        let response = self
145            .permissions
146            .check_permission(request)
147            .await?
148            .into_inner();
149
150        Ok(response)
151    }
152
153    /// Evaluate the given list of permission checks.
154    pub async fn check_bulk_permissions(
155        &mut self,
156        request: CheckBulkPermissionsRequest,
157    ) -> Result<CheckBulkPermissionsResponse> {
158        let response = self
159            .permissions
160            .check_bulk_permissions(request)
161            .await?
162            .into_inner();
163
164        Ok(response)
165    }
166
167    /// Reveal the graph structure for a resource's permission or relation. This
168    /// RPC does not recurse infinitely deep and may require multiple calls to
169    /// fully unnest a deeply nested graph.
170    pub async fn expand_permission_tree(
171        &mut self,
172        request: ExpandPermissionTreeRequest,
173    ) -> Result<ExpandPermissionTreeResponse> {
174        let response = self
175            .permissions
176            .expand_permission_tree(request)
177            .await?
178            .into_inner();
179
180        Ok(response)
181    }
182
183    /// Return all the resources of a given type that a subject can access
184    /// whether via a computed permission or relation membership.
185    pub async fn lookup_resources(
186        &mut self,
187        request: LookupResourcesRequest,
188    ) -> Result<Streaming<LookupResourcesResponse>> {
189        let response = self
190            .permissions
191            .lookup_resources(request)
192            .await?
193            .into_inner();
194
195        Ok(response)
196    }
197
198    /// Return all the subjects of a given type that have access whether via a
199    /// computed permission or relation membership.
200    pub async fn lookup_subjects(
201        &mut self,
202        request: LookupSubjectsRequest,
203    ) -> Result<Streaming<LookupSubjectsResponse>> {
204        let response = self
205            .permissions
206            .lookup_subjects(request)
207            .await?
208            .into_inner();
209
210        Ok(response)
211    }
212
213    /// Watch the database for mutations.
214    ///
215    /// SpiceDB's Watch API requires [commit timestamp tracking][postgres] be
216    /// enabled for PostgreSQL and the [experimental changefeed][cockroachdb]
217    /// for CockroachDB.
218    ///
219    /// [cockroachdb]:
220    ///     https://authzed.com/docs/spicedb/concepts/datastores#memdb
221    /// [postgres]:
222    ///     https://authzed.com/docs/spicedb/concepts/datastores#postgresql
223    pub async fn watch(&mut self, request: WatchRequest) -> Result<Streaming<WatchResponse>> {
224        let response = self.watch.watch(request).await?.into_inner();
225
226        Ok(response)
227    }
228}
229
230#[derive(Clone)]
231struct SpicedbMiddleware {
232    preshared_key: Box<MetadataValue<Ascii>>,
233}
234
235impl Interceptor for SpicedbMiddleware {
236    fn call(&mut self, mut request: Request<()>) -> Result<tonic::Request<()>, Status> {
237        request
238            .metadata_mut()
239            .insert("authorization", (*self.preshared_key).clone());
240        Ok(request)
241    }
242}
243
244#[cfg(test)]
245mod test {
246    use std::env;
247
248    use tokio::test;
249
250    use crate::reader::*;
251
252    use super::*;
253
254    #[test]
255    pub async fn test_spicedb() {
256        let spicedb_url =
257            env::var("SPICEDB_URL").unwrap_or_else(|_| "http://localhost:50051".to_string());
258
259        let preshared_key =
260            env::var("SPICEDB_PRESHARED_KEY").unwrap_or_else(|_| "spicedb".to_string());
261
262        let mut client = SpicedbClient::from_url_and_preshared_key(spicedb_url, preshared_key)
263            .await
264            .unwrap();
265
266        let schema = r#"
267definition user {}
268
269definition document {
270    relation viewer: user
271    relation editor: user
272
273    permission view = viewer + editor
274    permission edit = editor
275}
276"#;
277
278        // Write schema
279        let response = client.write_schema(schema).await.unwrap();
280        assert!(response.written_at().is_some());
281
282        // Read schema
283        let response = client.read_schema().await.unwrap();
284        assert_eq!(
285            response
286                .schema_text()
287                .split_whitespace()
288                .collect::<Vec<_>>(),
289            schema.split_whitespace().collect::<Vec<_>>()
290        );
291        assert!(response.read_at().is_some());
292    }
293}