spicedb_rust/
client.rs

1use crate::grpc::{BearerTokenInterceptor, GrpcResult};
2use crate::permission::{
3    CheckPermissionRequest, DeleteRelationshipsRequest, LookupResourcesRequest,
4    LookupSubjectsRequest, ReadRelationshipsRequest, SpiceDBPermissionClient,
5    WriteRelationshipsRequest,
6};
7use crate::schema::SpiceDBSchemaClient;
8use crate::spicedb::wrappers::{Consistency, ReadSchemaResponse};
9use crate::spicedb::{self, object_reference};
10use crate::{Actor, Entity, Resource};
11
12#[derive(Clone, Debug)]
13pub struct SpiceDBClient {
14    schema_service_client: SpiceDBSchemaClient,
15    permission_service_client: SpiceDBPermissionClient,
16}
17
18impl SpiceDBClient {
19    /// Reads the following env variables:
20    /// - `ZED_TOKEN`
21    /// - `ZED_ENDPOINT`
22    pub async fn from_env() -> anyhow::Result<Self> {
23        let token = std::env::var("SPICEDB_TOKEN")?;
24        let addr = std::env::var("SPICEDB_ENDPOINT")?;
25        Self::new(addr, &token).await
26    }
27
28    #[cfg(any(feature = "integration-test", test))]
29    pub async fn new_isolated(addr: impl Into<String>) -> anyhow::Result<Self> {
30        let token = uuid::Uuid::new_v4().to_string();
31        Self::new(addr, token).await
32    }
33
34    pub async fn new(addr: impl Into<String>, token: impl AsRef<str>) -> anyhow::Result<Self> {
35        let token = format!("Bearer {}", token.as_ref()).parse()?;
36        let interceptor = BearerTokenInterceptor::new(token);
37        let channel = tonic::transport::Channel::from_shared(addr.into())?
38            .connect()
39            .await?;
40        Ok(SpiceDBClient {
41            schema_service_client:
42                spicedb::schema_service_client::SchemaServiceClient::with_interceptor(
43                    channel.clone(),
44                    interceptor.clone(),
45                ),
46            permission_service_client:
47                spicedb::permissions_service_client::PermissionsServiceClient::with_interceptor(
48                    channel,
49                    interceptor,
50                ),
51        })
52    }
53
54    pub fn leak(self) -> &'static Self {
55        Box::leak(Box::new(self))
56    }
57
58    pub fn schema_service_client(&self) -> SpiceDBSchemaClient {
59        self.schema_service_client.clone()
60    }
61
62    pub fn permission_service_client(&self) -> SpiceDBPermissionClient {
63        self.permission_service_client.clone()
64    }
65
66    pub fn create_relationships_request(&self) -> WriteRelationshipsRequest {
67        WriteRelationshipsRequest::new(self.permission_service_client())
68    }
69
70    pub fn delete_relationships_request<R>(&self) -> DeleteRelationshipsRequest<R>
71    where
72        R: Resource,
73    {
74        DeleteRelationshipsRequest::new(self.permission_service_client())
75    }
76
77    pub fn read_relationships_request(&self) -> ReadRelationshipsRequest {
78        ReadRelationshipsRequest::new(self.permission_service_client())
79    }
80
81    pub fn check_permission_request<R>(&self) -> CheckPermissionRequest<R>
82    where
83        R: Resource,
84    {
85        CheckPermissionRequest::new(self.permission_service_client())
86    }
87
88    pub fn lookup_resources_request<R>(&self) -> LookupResourcesRequest<R>
89    where
90        R: Resource,
91    {
92        LookupResourcesRequest::new(self.permission_service_client())
93    }
94
95    pub fn lookup_subjects_request<S, R>(&self) -> LookupSubjectsRequest<S, R>
96    where
97        S: Entity,
98        R: Resource,
99    {
100        LookupSubjectsRequest::new(self.permission_service_client())
101    }
102
103    pub async fn delete_relationships<R>(
104        &self,
105        id: Option<R::Id>,
106        relation: Option<R::Relations>,
107        subject_filter: Option<spicedb::SubjectFilter>,
108    ) -> GrpcResult<spicedb::ZedToken>
109    where
110        R: Resource,
111    {
112        let mut request = self.delete_relationships_request::<R>();
113        if let Some(id) = id {
114            request.with_id(id);
115        }
116        if let Some(relation) = relation {
117            request.with_relation(relation);
118        }
119        if let Some(subject_filter) = subject_filter {
120            request.with_subject_filter(subject_filter);
121        }
122        request.send().await.map(|resp| resp.0)
123    }
124
125    pub async fn create_relationships<R, P>(
126        &self,
127        relationships: R,
128        preconditions: P,
129    ) -> GrpcResult<spicedb::ZedToken>
130    where
131        R: IntoIterator<Item = spicedb::RelationshipUpdate>,
132        P: IntoIterator<Item = spicedb::Precondition>,
133    {
134        let mut request = self.create_relationships_request();
135        for precondition in preconditions {
136            request.add_precondition_raw(precondition);
137        }
138        for relationship in relationships {
139            request.add_relationship_raw(relationship);
140        }
141        request.send().await
142    }
143
144    /// Shortcut for the most common use case of looking up resources, to quickly collect all ID's
145    /// returned in one call.
146    pub async fn lookup_resources<R>(
147        &self,
148        actor: &impl Actor,
149        permission: R::Permissions,
150    ) -> GrpcResult<Vec<R::Id>>
151    where
152        R: Resource,
153    {
154        let mut request = self.lookup_resources_request::<R>();
155        request.permission(permission);
156        request.actor(actor);
157        request.send_collect_ids().await
158    }
159
160    pub async fn lookup_resources_at<R>(
161        &self,
162        actor: &impl Actor,
163        permission: R::Permissions,
164        token: spicedb::ZedToken,
165    ) -> GrpcResult<Vec<R::Id>>
166    where
167        R: Resource,
168    {
169        let mut request = self.lookup_resources_request::<R>();
170        request.permission(permission);
171        request.actor(actor);
172        request.with_consistency(Consistency::AtLeastAsFresh(token));
173        request.send_collect_ids().await
174    }
175
176    pub async fn lookup_subjects<S, R>(
177        &self,
178        id: impl Into<R::Id>,
179        permission: R::Permissions,
180    ) -> GrpcResult<Vec<S::Id>>
181    where
182        R: Resource,
183        S: Entity,
184    {
185        let mut request = self.lookup_subjects_request::<S, R>();
186        request.resource(id, permission);
187        request.send_collect_ids().await
188    }
189
190    pub async fn lookup_subjects_at<S, R>(
191        &self,
192        id: impl Into<R::Id>,
193        permission: R::Permissions,
194        token: spicedb::ZedToken,
195    ) -> GrpcResult<Vec<S::Id>>
196    where
197        S: Entity,
198        R: Resource,
199    {
200        let mut request = self.lookup_subjects_request::<S, R>();
201        request.resource(id, permission);
202        request.with_consistency(Consistency::AtLeastAsFresh(token));
203        request.send_collect_ids().await
204    }
205
206    /// Shortcut for the most common use case of checking a permission for an actor in the system
207    /// on a specific resource `R` with default consistency.
208    pub async fn check_permission<R>(
209        &self,
210        actor: &impl Actor,
211        resource_id: impl Into<R::Id>,
212        permission: R::Permissions,
213    ) -> GrpcResult<bool>
214    where
215        R: Resource,
216    {
217        let mut request = self.check_permission_request::<R>();
218        request.subject(actor.to_subject());
219        request.resource(object_reference::<R>(resource_id.into()));
220        request.permission(permission);
221        let resp = request.send().await?;
222        Ok(resp.permissionship
223            == spicedb::check_permission_response::Permissionship::HasPermission as i32)
224    }
225
226    pub async fn check_permission_at<R>(
227        &self,
228        actor: &impl Actor,
229        resource_id: impl Into<R::Id>,
230        permission: R::Permissions,
231        token: spicedb::ZedToken,
232    ) -> GrpcResult<bool>
233    where
234        R: Resource,
235    {
236        let mut request = self.check_permission_request::<R>();
237        request.subject(actor.to_subject());
238        request.resource(object_reference::<R>(resource_id.into()));
239        request.permission(permission);
240        request.consistency(Consistency::AtLeastAsFresh(token));
241        let resp = request.send().await?;
242        Ok(resp.permissionship
243            == spicedb::check_permission_response::Permissionship::HasPermission as i32)
244    }
245
246    pub async fn write_schema(&self, schema: String) -> Result<spicedb::ZedToken, tonic::Status> {
247        let resp = self
248            .schema_service_client()
249            .write_schema(spicedb::WriteSchemaRequest { schema })
250            .await?
251            .into_inner();
252        resp.written_at
253            .ok_or_else(|| tonic::Status::internal("ZedToken expected"))
254    }
255
256    pub async fn read_schema(&self) -> Result<ReadSchemaResponse, tonic::Status> {
257        let resp = self
258            .schema_service_client()
259            .read_schema(spicedb::ReadSchemaRequest {})
260            .await?
261            .into_inner()
262            .into();
263        Ok(resp)
264    }
265}