1use grpc::heddle::v1::{
2 ApproveThreadRequest, BeginWebAuthnAuthenticationRequest, CheckMergeEligibilityRequest,
3 CheckMergeEligibilityResponse, CreateGrantRequest, CreateInvitationRequest,
4 CreateRepositoryRequest, DeleteGrantRequest, DeleteNamespaceRequest, DeleteRepositoryRequest,
5 GetCurrentUserNamespaceRequest, GrantSupportAccessRequest, GrantTargetRef,
6 Invitation as ProtoInvitation, ListGrantsRequest, ListNamespacesRequest,
7 ListRepositoriesRequest, ListSupportAccessGrantsRequest, ListThreadApprovalsRequest,
8 RevokeApprovalRequest, RevokeSupportAccessRequest, SupportAccessGrant, ThreadApproval,
9 UpdateGrantRequest, UpdateNamespaceRequest, UpdateRepositoryRequest,
10 grant_target_ref::Target as GrantTargetKind,
11};
12use wire::ProtocolError;
13use tonic::Request;
14
15use super::{
16 HostedGrpcClient,
17 helpers::{
18 status_to_protocol_error, to_protocol_grant, to_protocol_namespace, to_protocol_repository,
19 },
20};
21
22macro_rules! authed_call {
32 ($self:ident, $rpc:ident, $msg:expr) => {{
33 let mut request = Request::new($msg);
34 $self.apply_auth(&mut request)?;
35 $self
36 .user
37 .$rpc(request)
38 .await
39 .map_err(status_to_protocol_error)?
40 .into_inner()
41 }};
42}
43
44impl HostedGrpcClient {
45 pub async fn begin_login(
46 &mut self,
47 username: &str,
48 ) -> Result<(String, String, u64), ProtocolError> {
49 let request = Request::new(BeginWebAuthnAuthenticationRequest {
50 username: username.to_string(),
51 });
52 let response = self
53 .auth
54 .begin_web_authn_authentication(request)
55 .await
56 .map_err(status_to_protocol_error)?
57 .into_inner();
58 let expires_at_secs = response
59 .expires_at
60 .as_ref()
61 .map(|t| t.seconds.max(0) as u64)
62 .unwrap_or(0);
63 Ok((response.challenge_id, response.challenge, expires_at_secs))
64 }
65
66 pub async fn get_current_user_namespace(
67 &mut self,
68 ) -> Result<wire::HostedNamespaceInfo, ProtocolError> {
69 let namespace = authed_call!(
70 self,
71 get_current_user_namespace,
72 GetCurrentUserNamespaceRequest {}
73 );
74 Ok(to_protocol_namespace(namespace))
75 }
76
77 pub async fn list_namespaces(
78 &mut self,
79 ) -> Result<Vec<wire::HostedNamespaceInfo>, ProtocolError> {
80 let response = authed_call!(self, list_namespaces, ListNamespacesRequest {});
81 Ok(response
82 .namespaces
83 .into_iter()
84 .map(to_protocol_namespace)
85 .collect())
86 }
87
88 pub async fn create_namespace(
89 &mut self,
90 kind: &str,
91 slug: &str,
92 parent_path: Option<&str>,
93 display_name: Option<String>,
94 ) -> Result<wire::HostedNamespaceInfo, ProtocolError> {
95 let namespace = authed_call!(
96 self,
97 create_namespace,
98 grpc::heddle::v1::CreateNamespaceRequest {
99 kind: parse_namespace_kind_arg(kind)? as i32,
100 slug: slug.to_string(),
101 parent_path: parent_path.unwrap_or_default().to_string(),
102 display_name: display_name.unwrap_or_default(),
103 client_operation_id: String::new(),
104 }
105 );
106 Ok(to_protocol_namespace(namespace))
107 }
108
109 pub async fn create_repository(
110 &mut self,
111 namespace_path: &str,
112 slug: &str,
113 ) -> Result<wire::HostedRepositoryInfo, ProtocolError> {
114 let repo = authed_call!(
115 self,
116 create_repository,
117 CreateRepositoryRequest {
118 namespace_path: namespace_path.to_string(),
119 slug: slug.to_string(),
120 client_operation_id: String::new(),
121 }
122 );
123 Ok(to_protocol_repository(repo))
124 }
125
126 pub async fn list_repositories(
127 &mut self,
128 namespace_path: Option<&str>,
129 ) -> Result<Vec<wire::HostedRepositoryInfo>, ProtocolError> {
130 let response = authed_call!(
131 self,
132 list_repositories,
133 ListRepositoriesRequest {
134 namespace_path: namespace_path.unwrap_or_default().to_string(),
135 }
136 );
137 Ok(response
138 .repositories
139 .into_iter()
140 .map(to_protocol_repository)
141 .collect())
142 }
143
144 pub async fn update_namespace(
145 &mut self,
146 full_path: &str,
147 new_slug: Option<&str>,
148 display_name: Option<Option<String>>,
149 ) -> Result<wire::HostedNamespaceInfo, ProtocolError> {
150 let (display_name, clear_display_name) = match display_name {
151 Some(Some(value)) => (value, false),
152 Some(None) => (String::new(), true),
153 None => (String::new(), false),
154 };
155 let namespace = authed_call!(
156 self,
157 update_namespace,
158 UpdateNamespaceRequest {
159 full_path: full_path.to_string(),
160 new_slug: new_slug.unwrap_or_default().to_string(),
161 display_name,
162 clear_display_name,
163 client_operation_id: String::new(),
164 }
165 );
166 Ok(to_protocol_namespace(namespace))
167 }
168
169 pub async fn delete_namespace(&mut self, full_path: &str) -> Result<(), ProtocolError> {
170 authed_call!(
171 self,
172 delete_namespace,
173 DeleteNamespaceRequest {
174 full_path: full_path.to_string(),
175 client_operation_id: String::new(),
176 }
177 );
178 Ok(())
179 }
180
181 pub async fn update_repository(
182 &mut self,
183 full_path: &str,
184 new_slug: &str,
185 ) -> Result<wire::HostedRepositoryInfo, ProtocolError> {
186 let repo = authed_call!(
187 self,
188 update_repository,
189 UpdateRepositoryRequest {
190 full_path: full_path.to_string(),
191 new_slug: new_slug.to_string(),
192 client_operation_id: String::new(),
193 }
194 );
195 Ok(to_protocol_repository(repo))
196 }
197
198 pub async fn delete_repository(&mut self, full_path: &str) -> Result<(), ProtocolError> {
199 authed_call!(
200 self,
201 delete_repository,
202 DeleteRepositoryRequest {
203 full_path: full_path.to_string(),
204 client_operation_id: String::new(),
205 }
206 );
207 Ok(())
208 }
209
210 pub async fn create_grant(
211 &mut self,
212 subject: &str,
213 role: &str,
214 namespace_path: Option<&str>,
215 repo_path: Option<&str>,
216 ) -> Result<wire::HostedGrantInfo, ProtocolError> {
217 let target = build_target_ref(namespace_path, repo_path)?;
218 let grant = authed_call!(
219 self,
220 create_grant,
221 CreateGrantRequest {
222 subject: subject.to_string(),
223 role: parse_hosted_role_arg(role)? as i32,
224 target,
225 client_operation_id: String::new(),
226 }
227 );
228 Ok(to_protocol_grant(grant))
229 }
230
231 pub async fn list_grants(
232 &mut self,
233 resource: Option<&str>,
234 ) -> Result<Vec<wire::HostedGrantInfo>, ProtocolError> {
235 let response = authed_call!(
236 self,
237 list_grants,
238 ListGrantsRequest {
239 resource: resource.unwrap_or_default().to_string(),
240 }
241 );
242 Ok(response.grants.into_iter().map(to_protocol_grant).collect())
243 }
244
245 pub async fn update_grant(
246 &mut self,
247 subject: &str,
248 role: &str,
249 namespace_path: Option<&str>,
250 repo_path: Option<&str>,
251 ) -> Result<wire::HostedGrantInfo, ProtocolError> {
252 let target = build_target_ref(namespace_path, repo_path)?;
253 let grant = authed_call!(
254 self,
255 update_grant,
256 UpdateGrantRequest {
257 subject: subject.to_string(),
258 role: parse_hosted_role_arg(role)? as i32,
259 target,
260 client_operation_id: String::new(),
261 }
262 );
263 Ok(to_protocol_grant(grant))
264 }
265
266 pub async fn delete_grant(
267 &mut self,
268 subject: &str,
269 namespace_path: Option<&str>,
270 repo_path: Option<&str>,
271 ) -> Result<(), ProtocolError> {
272 let target = build_target_ref(namespace_path, repo_path)?;
273 authed_call!(
274 self,
275 delete_grant,
276 DeleteGrantRequest {
277 subject: subject.to_string(),
278 target,
279 client_operation_id: String::new(),
280 }
281 );
282 Ok(())
283 }
284
285 pub async fn create_invitation(
288 &mut self,
289 email: &str,
290 namespace_path: &str,
291 role: &str,
292 ) -> Result<ProtoInvitation, ProtocolError> {
293 let invitation = authed_call!(
294 self,
295 create_invitation,
296 CreateInvitationRequest {
297 email: email.to_string(),
298 namespace_path: namespace_path.to_string(),
299 role: parse_hosted_role_arg(role)? as i32,
300 expires_at: None,
301 metadata: String::new(),
302 client_operation_id: String::new(),
303 }
304 );
305 Ok(invitation)
306 }
307
308 pub async fn approve_thread(
313 &mut self,
314 repo_path: &str,
315 source_thread: &str,
316 target_thread: &str,
317 source_state: &str,
318 note: Option<&str>,
319 ) -> Result<ThreadApproval, ProtocolError> {
320 Ok(authed_call!(
321 self,
322 approve_thread,
323 ApproveThreadRequest {
324 repo_path: repo_path.to_string(),
325 source_thread: source_thread.to_string(),
326 target_thread: target_thread.to_string(),
327 source_state: objects::object::ChangeId::parse(source_state)
328 .map(|id| id.as_bytes().to_vec())
329 .unwrap_or_default(),
330 note: note.unwrap_or_default().to_string(),
331 client_operation_id: String::new(),
332 }
333 ))
334 }
335
336 pub async fn revoke_approval(&mut self, id: &str) -> Result<(), ProtocolError> {
337 authed_call!(
338 self,
339 revoke_approval,
340 RevokeApprovalRequest {
341 id: id.to_string(),
342 client_operation_id: String::new(),
343 }
344 );
345 Ok(())
346 }
347
348 pub async fn list_thread_approvals(
349 &mut self,
350 repo_path: &str,
351 source_thread: &str,
352 target_thread: &str,
353 ) -> Result<Vec<ThreadApproval>, ProtocolError> {
354 Ok(authed_call!(
355 self,
356 list_thread_approvals,
357 ListThreadApprovalsRequest {
358 repo_path: repo_path.to_string(),
359 source_thread: source_thread.to_string(),
360 target_thread: target_thread.to_string(),
361 }
362 )
363 .approvals)
364 }
365
366 #[allow(clippy::too_many_arguments)]
371 pub async fn check_merge_eligibility(
372 &mut self,
373 repo_path: &str,
374 source_thread: &str,
375 target_thread: &str,
376 source_state: &str,
377 gated_action: &str,
378 changed_paths: Vec<String>,
379 author_user_id: Option<&str>,
380 ) -> Result<CheckMergeEligibilityResponse, ProtocolError> {
381 Ok(authed_call!(
382 self,
383 check_merge_eligibility,
384 CheckMergeEligibilityRequest {
385 repo_path: repo_path.to_string(),
386 source_thread: source_thread.to_string(),
387 target_thread: target_thread.to_string(),
388 source_state: objects::object::ChangeId::parse(source_state)
389 .map(|id| id.as_bytes().to_vec())
390 .unwrap_or_default(),
391 gated_action: gated_action.to_string(),
392 changed_paths,
393 author_user_id: author_user_id.unwrap_or_default().to_string(),
394 }
395 ))
396 }
397
398 pub async fn grant_support_access(
402 &mut self,
403 operator_email: &str,
404 namespace_path: Option<&str>,
405 repo_path: Option<&str>,
406 ttl_seconds: u32,
407 reason: &str,
408 client_operation_id: String,
409 ) -> Result<SupportAccessGrant, ProtocolError> {
410 let target = build_target_ref(namespace_path, repo_path)?;
411 Ok(authed_call!(
412 self,
413 grant_support_access,
414 GrantSupportAccessRequest {
415 operator_email: operator_email.to_string(),
416 target,
417 ttl_seconds: Some(prost_types::Duration {
418 seconds: i64::from(ttl_seconds),
419 nanos: 0,
420 }),
421 reason: reason.to_string(),
422 client_operation_id,
423 }
424 ))
425 }
426
427 pub async fn list_support_access_grants(
428 &mut self,
429 namespace_path: Option<&str>,
430 repo_path: Option<&str>,
431 include_inactive: bool,
432 ) -> Result<Vec<SupportAccessGrant>, ProtocolError> {
433 let target = build_target_ref(namespace_path, repo_path)?;
434 Ok(authed_call!(
435 self,
436 list_support_access_grants,
437 ListSupportAccessGrantsRequest {
438 target,
439 include_inactive,
440 }
441 )
442 .grants)
443 }
444
445 pub async fn revoke_support_access(
446 &mut self,
447 id: &str,
448 client_operation_id: String,
449 ) -> Result<(), ProtocolError> {
450 authed_call!(
451 self,
452 revoke_support_access,
453 RevokeSupportAccessRequest {
454 id: id.to_string(),
455 client_operation_id,
456 }
457 );
458 Ok(())
459 }
460}
461
462fn build_target_ref(
466 namespace_path: Option<&str>,
467 repo_path: Option<&str>,
468) -> Result<Option<GrantTargetRef>, ProtocolError> {
469 match (
470 namespace_path.filter(|s| !s.is_empty()),
471 repo_path.filter(|s| !s.is_empty()),
472 ) {
473 (Some(ns), None) => Ok(Some(GrantTargetRef {
474 target: Some(GrantTargetKind::NamespacePath(ns.to_string())),
475 })),
476 (None, Some(rp)) => Ok(Some(GrantTargetRef {
477 target: Some(GrantTargetKind::RepoPath(rp.to_string())),
478 })),
479 _ => Err(ProtocolError::InvalidState(
480 "exactly one of namespace_path or repo_path must be set".into(),
481 )),
482 }
483}
484
485fn parse_namespace_kind_arg(value: &str) -> Result<grpc::heddle::v1::NamespaceKind, ProtocolError> {
489 use grpc::heddle::v1::NamespaceKind;
490 match value.trim().to_ascii_lowercase().as_str() {
491 "user" => Ok(NamespaceKind::User),
492 "namespace" | "org" => Ok(NamespaceKind::Org),
493 "team" => Ok(NamespaceKind::Team),
494 other => Err(ProtocolError::InvalidState(format!(
495 "invalid namespace kind '{other}': expected user|namespace|team"
496 ))),
497 }
498}
499
500fn parse_hosted_role_arg(value: &str) -> Result<grpc::heddle::v1::HostedRole, ProtocolError> {
502 use grpc::heddle::v1::HostedRole;
503 match value.trim().to_ascii_lowercase().as_str() {
504 "reader" => Ok(HostedRole::Reader),
505 "developer" => Ok(HostedRole::Developer),
506 "maintainer" => Ok(HostedRole::Maintainer),
507 "admin" => Ok(HostedRole::Admin),
508 "owner" => Ok(HostedRole::Owner),
509 other => Err(ProtocolError::InvalidState(format!(
510 "invalid role '{other}': expected reader|developer|maintainer|admin|owner"
511 ))),
512 }
513}