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