1use spicedb_rs_proto::authzed::api::{
2 materialize::v0::{
3 watch_permission_sets_service_client::WatchPermissionSetsServiceClient,
4 watch_permissions_service_client::WatchPermissionsServiceClient,
5 },
6 v1::{
7 experimental_service_client::ExperimentalServiceClient,
8 permissions_service_client::PermissionsServiceClient,
9 schema_service_client::SchemaServiceClient, watch_service_client::WatchServiceClient,
10 },
11};
12use tonic::{
13 Request, Status,
14 metadata::{Ascii, MetadataValue, errors::InvalidMetadataValue},
15 service::{Interceptor, interceptor::InterceptedService},
16 transport::{Channel, Endpoint},
17};
18
19pub use spicedb_rs_proto::authzed::api::{materialize::v0, v1};
20
21#[derive(Debug, thiserror::Error)]
22pub enum ClientError {
23 #[error("invalid endpoint: {0}")]
24 InvalidEndpoint(#[from] http::uri::InvalidUri),
25 #[error("invalid token metadata: {0}")]
26 InvalidTokenMetadata(#[from] InvalidMetadataValue),
27 #[error("transport error: {0}")]
28 Transport(#[from] tonic::transport::Error),
29}
30
31#[doc(hidden)]
32#[derive(Clone, Debug)]
33pub struct AuthInterceptor {
34 authorization_header: Option<MetadataValue<Ascii>>,
35}
36
37impl AuthInterceptor {
38 fn from_token(token: Option<&str>) -> Result<Self, InvalidMetadataValue> {
39 let authorization_header = token
42 .map(|token| format!("Bearer {token}").parse())
43 .transpose()?;
44 Ok(Self {
45 authorization_header,
46 })
47 }
48}
49
50impl Interceptor for AuthInterceptor {
51 fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
52 if let Some(authorization_header) = &self.authorization_header {
53 request
54 .metadata_mut()
55 .insert("authorization", authorization_header.clone());
56 }
57 Ok(request)
58 }
59}
60
61pub type InterceptedChannel = InterceptedService<Channel, AuthInterceptor>;
62
63#[derive(Debug, Clone)]
64pub struct ClientBuilder {
65 endpoint: String,
66 token: Option<String>,
67 insecure: bool,
68}
69
70impl Default for ClientBuilder {
71 fn default() -> Self {
72 Self {
73 endpoint: "grpc.authzed.com:443".to_string(),
74 token: None,
75 insecure: false,
76 }
77 }
78}
79
80impl ClientBuilder {
81 pub fn new(endpoint: impl Into<String>) -> Self {
82 Self {
83 endpoint: endpoint.into(),
84 ..Self::default()
85 }
86 }
87
88 pub fn with_token(mut self, token: impl Into<String>) -> Self {
92 self.token = Some(token.into());
93 self
94 }
95
96 pub fn insecure(mut self, insecure: bool) -> Self {
100 self.insecure = insecure;
101 self
102 }
103
104 pub async fn connect(self) -> Result<Client, ClientError> {
106 let endpoint = endpoint_with_scheme(&self.endpoint, self.insecure);
107 let channel = Endpoint::from_shared(endpoint)?.connect().await?;
108 build_client(channel, self.token.as_deref())
109 }
110
111 pub fn connect_lazy(self) -> Result<Client, ClientError> {
113 let endpoint = endpoint_with_scheme(&self.endpoint, self.insecure);
114 let channel = Endpoint::from_shared(endpoint)?.connect_lazy();
115 build_client(channel, self.token.as_deref())
116 }
117}
118
119fn endpoint_with_scheme(endpoint: &str, insecure: bool) -> String {
120 if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
121 endpoint.to_string()
122 } else if insecure {
123 format!("http://{endpoint}")
124 } else {
125 format!("https://{endpoint}")
126 }
127}
128
129fn build_client(channel: Channel, token: Option<&str>) -> Result<Client, ClientError> {
130 let interceptor = AuthInterceptor::from_token(token)?;
131
132 Ok(Client {
133 permissions: PermissionsServiceClient::with_interceptor(
134 channel.clone(),
135 interceptor.clone(),
136 ),
137 schema: SchemaServiceClient::with_interceptor(channel.clone(), interceptor.clone()),
138 watch: WatchServiceClient::with_interceptor(channel.clone(), interceptor.clone()),
139 experimental: ExperimentalServiceClient::with_interceptor(
140 channel.clone(),
141 interceptor.clone(),
142 ),
143 watch_permissions: WatchPermissionsServiceClient::with_interceptor(
144 channel.clone(),
145 interceptor.clone(),
146 ),
147 watch_permission_sets: WatchPermissionSetsServiceClient::with_interceptor(
148 channel,
149 interceptor,
150 ),
151 })
152}
153
154#[derive(Debug, Clone)]
155pub struct Client {
156 permissions: PermissionsServiceClient<InterceptedChannel>,
157 schema: SchemaServiceClient<InterceptedChannel>,
158 watch: WatchServiceClient<InterceptedChannel>,
159 experimental: ExperimentalServiceClient<InterceptedChannel>,
160 watch_permissions: WatchPermissionsServiceClient<InterceptedChannel>,
161 watch_permission_sets: WatchPermissionSetsServiceClient<InterceptedChannel>,
162}
163
164impl Client {
165 pub fn builder() -> ClientBuilder {
166 ClientBuilder::default()
167 }
168
169 pub fn permissions(&self) -> PermissionsServiceClient<InterceptedChannel> {
171 self.permissions.clone()
172 }
173
174 pub fn schema(&self) -> SchemaServiceClient<InterceptedChannel> {
176 self.schema.clone()
177 }
178
179 pub fn watch(&self) -> WatchServiceClient<InterceptedChannel> {
181 self.watch.clone()
182 }
183
184 pub fn experimental(&self) -> ExperimentalServiceClient<InterceptedChannel> {
186 self.experimental.clone()
187 }
188
189 pub fn watch_permissions(&self) -> WatchPermissionsServiceClient<InterceptedChannel> {
191 self.watch_permissions.clone()
192 }
193
194 pub fn watch_permission_sets(&self) -> WatchPermissionSetsServiceClient<InterceptedChannel> {
196 self.watch_permission_sets.clone()
197 }
198}