Skip to main content

trellis_auth/
client.rs

1use crate::{
2    save_admin_session, AdminSessionState, ApprovalEntryRecord, AuthGetInstalledContractRequest,
3    AuthGetInstalledContractResponse, AuthInstallServiceRequest, AuthUpgradeServiceContractRequest,
4    AuthValidateRequestRequest, AuthValidateRequestResponse, AuthenticatedUser, BoundSession,
5    ListApprovalsRequest, RenewBindingTokenResponse, RevokeApprovalRequest, ServiceListEntry,
6    TrellisAuthError,
7};
8use serde::{de::DeserializeOwned, Deserialize, Serialize};
9use serde_json::Value;
10use std::collections::BTreeMap;
11use trellis_client::{TrellisClient, UserConnectOptions};
12
13use crate::protocol::{
14    AuthInstallServiceResponse, AuthUpgradeServiceContractResponse, ListApprovalsResponse,
15    ListServicesResponse, LogoutResponse, MeResponse, RevokeApprovalResponse,
16};
17
18#[derive(Debug, Clone, Deserialize, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct PortalRecord {
21    pub portal_id: String,
22    pub app_contract_id: Option<String>,
23    pub entry_url: String,
24    pub disabled: bool,
25}
26
27#[derive(Debug, Clone, Deserialize, Serialize)]
28#[serde(rename_all = "camelCase")]
29pub struct PortalDefaultRecord {
30    pub portal_id: Option<String>,
31}
32
33#[derive(Debug, Clone, Deserialize, Serialize)]
34#[serde(rename_all = "camelCase")]
35pub struct LoginPortalSelectionRecord {
36    pub contract_id: String,
37    pub portal_id: Option<String>,
38}
39
40#[derive(Debug, Clone, Deserialize, Serialize)]
41#[serde(rename_all = "camelCase")]
42pub struct WorkloadPortalSelectionRecord {
43    pub profile_id: String,
44    pub portal_id: Option<String>,
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
48#[serde(rename_all = "camelCase")]
49struct CreatePortalResponse {
50    portal: PortalRecord,
51}
52
53#[derive(Debug, Clone, Deserialize, Serialize)]
54struct ListPortalsResponse {
55    portals: Vec<PortalRecord>,
56}
57
58#[derive(Debug, Clone, Deserialize, Serialize)]
59struct DisablePortalResponse {
60    success: bool,
61}
62
63#[derive(Debug, Clone, Deserialize, Serialize)]
64#[serde(rename_all = "camelCase")]
65struct GetPortalDefaultResponse {
66    default_portal: PortalDefaultRecord,
67}
68
69#[derive(Debug, Clone, Deserialize, Serialize)]
70#[serde(rename_all = "camelCase")]
71struct SetPortalDefaultRequest {
72    portal_id: Option<String>,
73}
74
75#[derive(Debug, Clone, Deserialize, Serialize)]
76#[serde(rename_all = "camelCase")]
77struct SetPortalDefaultResponse {
78    default_portal: PortalDefaultRecord,
79}
80
81#[derive(Debug, Clone, Deserialize, Serialize)]
82struct ListLoginPortalSelectionsResponse {
83    selections: Vec<LoginPortalSelectionRecord>,
84}
85
86#[derive(Debug, Clone, Deserialize, Serialize)]
87#[serde(rename_all = "camelCase")]
88struct SetLoginPortalSelectionRequest {
89    contract_id: String,
90    portal_id: Option<String>,
91}
92
93#[derive(Debug, Clone, Deserialize, Serialize)]
94struct SetLoginPortalSelectionResponse {
95    selection: LoginPortalSelectionRecord,
96}
97
98#[derive(Debug, Clone, Deserialize, Serialize)]
99#[serde(rename_all = "camelCase")]
100struct ClearLoginPortalSelectionRequest {
101    contract_id: String,
102}
103
104#[derive(Debug, Clone, Deserialize, Serialize)]
105struct ClearLoginPortalSelectionResponse {
106    success: bool,
107}
108
109#[derive(Debug, Clone, Deserialize, Serialize)]
110struct ListWorkloadPortalSelectionsResponse {
111    selections: Vec<WorkloadPortalSelectionRecord>,
112}
113
114#[derive(Debug, Clone, Deserialize, Serialize)]
115#[serde(rename_all = "camelCase")]
116struct SetWorkloadPortalSelectionRequest {
117    profile_id: String,
118    portal_id: Option<String>,
119}
120
121#[derive(Debug, Clone, Deserialize, Serialize)]
122struct SetWorkloadPortalSelectionResponse {
123    selection: WorkloadPortalSelectionRecord,
124}
125
126#[derive(Debug, Clone, Deserialize, Serialize)]
127#[serde(rename_all = "camelCase")]
128struct ClearWorkloadPortalSelectionRequest {
129    profile_id: String,
130}
131
132#[derive(Debug, Clone, Deserialize, Serialize)]
133struct ClearWorkloadPortalSelectionResponse {
134    success: bool,
135}
136
137#[derive(Debug, Clone, Deserialize, Serialize)]
138#[serde(rename_all = "camelCase")]
139struct CreatePortalRequest {
140    portal_id: String,
141    app_contract_id: Option<String>,
142    entry_url: String,
143}
144
145/// Connect an authenticated admin client from the stored session state.
146pub async fn connect_admin_client_async(
147    state: &AdminSessionState,
148) -> Result<TrellisClient, TrellisAuthError> {
149    Ok(TrellisClient::connect_user(UserConnectOptions {
150        servers: &state.nats_servers,
151        sentinel_jwt: &state.sentinel_jwt,
152        sentinel_seed: &state.sentinel_seed,
153        session_key_seed_base64url: &state.session_seed,
154        binding_token: &state.binding_token,
155        timeout_ms: 5_000,
156    })
157    .await?)
158}
159
160/// Persist a renewed binding token and sentinel credentials into the admin session state.
161pub fn persist_renewed_admin_session(
162    state: &mut AdminSessionState,
163    renewed: RenewBindingTokenResponse,
164) -> Result<(), TrellisAuthError> {
165    let renewed = BoundSession {
166        binding_token: renewed.binding_token,
167        inbox_prefix: renewed.inbox_prefix,
168        expires: renewed.expires,
169        sentinel: renewed.sentinel,
170    };
171
172    state.binding_token = renewed.binding_token;
173    state.expires = renewed.expires;
174    state.sentinel_jwt = renewed.sentinel.jwt;
175    state.sentinel_seed = renewed.sentinel.seed;
176    save_admin_session(state)
177}
178
179/// Thin typed client for Trellis auth/admin RPCs used by the CLI.
180pub struct AuthClient<'a> {
181    inner: &'a TrellisClient,
182}
183
184impl<'a> AuthClient<'a> {
185    /// Wrap an already-connected low-level Trellis client.
186    pub fn new(inner: &'a TrellisClient) -> Self {
187        Self { inner }
188    }
189
190    async fn call<Input, Output>(
191        &self,
192        subject: &str,
193        input: &Input,
194    ) -> Result<Output, TrellisAuthError>
195    where
196        Input: Serialize,
197        Output: DeserializeOwned,
198    {
199        let request = serde_json::to_value(input)?;
200        let response = self.inner.request_json_value(subject, &request).await?;
201        Ok(serde_json::from_value(response)?)
202    }
203
204    /// Return the currently authenticated user.
205    pub async fn me(&self) -> Result<AuthenticatedUser, TrellisAuthError> {
206        Ok(self
207            .call::<_, MeResponse>("rpc.v1.Auth.Me", &Empty {})
208            .await?
209            .user)
210    }
211
212    /// List stored app approval decisions.
213    pub async fn list_approvals(
214        &self,
215        user: Option<&str>,
216        digest: Option<&str>,
217    ) -> Result<Vec<ApprovalEntryRecord>, TrellisAuthError> {
218        let request = ListApprovalsRequest {
219            user: user.map(ToOwned::to_owned),
220            digest: digest.map(ToOwned::to_owned),
221        };
222        Ok(self
223            .call::<_, ListApprovalsResponse>("rpc.v1.Auth.ListApprovals", &request)
224            .await?
225            .approvals)
226    }
227
228    /// Revoke one stored approval decision.
229    pub async fn revoke_approval(
230        &self,
231        digest: &str,
232        user: Option<&str>,
233    ) -> Result<bool, TrellisAuthError> {
234        let request = RevokeApprovalRequest {
235            contract_digest: digest.to_string(),
236            user: user.map(ToOwned::to_owned),
237        };
238        Ok(self
239            .call::<_, RevokeApprovalResponse>("rpc.v1.Auth.RevokeApproval", &request)
240            .await?
241            .success)
242    }
243
244    /// List registered portals.
245    pub async fn list_portals(&self) -> Result<Vec<PortalRecord>, TrellisAuthError> {
246        Ok(self
247            .call::<_, ListPortalsResponse>("rpc.v1.Auth.ListPortals", &trellis_sdk_auth::Empty {})
248            .await?
249            .portals)
250    }
251
252    /// Create or replace a portal record.
253    pub async fn create_portal(
254        &self,
255        portal_id: &str,
256        app_contract_id: Option<&str>,
257        entry_url: &str,
258    ) -> Result<PortalRecord, TrellisAuthError> {
259        Ok(self
260            .call::<_, CreatePortalResponse>(
261                "rpc.v1.Auth.CreatePortal",
262                &CreatePortalRequest {
263                    portal_id: portal_id.to_string(),
264                    app_contract_id: app_contract_id.map(ToOwned::to_owned),
265                    entry_url: entry_url.to_string(),
266                },
267            )
268            .await?
269            .portal)
270    }
271
272    /// Disable a portal.
273    pub async fn disable_portal(&self, portal_id: &str) -> Result<bool, TrellisAuthError> {
274        Ok(self
275            .call::<_, DisablePortalResponse>(
276                "rpc.v1.Auth.DisablePortal",
277                &trellis_sdk_auth::AuthDisablePortalRequest {
278                    portal_id: portal_id.to_string(),
279                },
280            )
281            .await?
282            .success)
283    }
284
285    /// Get the deployment-wide login portal default.
286    pub async fn get_login_portal_default(
287        &self,
288    ) -> Result<PortalDefaultRecord, TrellisAuthError> {
289        Ok(self
290            .call::<_, GetPortalDefaultResponse>(
291                "rpc.v1.Auth.GetLoginPortalDefault",
292                &trellis_sdk_auth::Empty {},
293            )
294            .await?
295            .default_portal)
296    }
297
298    /// Set the deployment-wide login portal default.
299    pub async fn set_login_portal_default(
300        &self,
301        portal_id: Option<&str>,
302    ) -> Result<PortalDefaultRecord, TrellisAuthError> {
303        Ok(self
304            .call::<_, SetPortalDefaultResponse>(
305                "rpc.v1.Auth.SetLoginPortalDefault",
306                &SetPortalDefaultRequest {
307                    portal_id: portal_id.map(ToOwned::to_owned),
308                },
309            )
310            .await?
311            .default_portal)
312    }
313
314    /// List contract-specific login portal selections.
315    pub async fn list_login_portal_selections(
316        &self,
317    ) -> Result<Vec<LoginPortalSelectionRecord>, TrellisAuthError> {
318        Ok(self
319            .call::<_, ListLoginPortalSelectionsResponse>(
320                "rpc.v1.Auth.ListLoginPortalSelections",
321                &trellis_sdk_auth::Empty {},
322            )
323            .await?
324            .selections)
325    }
326
327    /// Create or replace a contract-specific login portal selection.
328    pub async fn set_login_portal_selection(
329        &self,
330        contract_id: &str,
331        portal_id: Option<&str>,
332    ) -> Result<LoginPortalSelectionRecord, TrellisAuthError> {
333        Ok(self
334            .call::<_, SetLoginPortalSelectionResponse>(
335                "rpc.v1.Auth.SetLoginPortalSelection",
336                &SetLoginPortalSelectionRequest {
337                    contract_id: contract_id.to_string(),
338                    portal_id: portal_id.map(ToOwned::to_owned),
339                },
340            )
341            .await?
342            .selection)
343    }
344
345    /// Clear a contract-specific login portal selection.
346    pub async fn clear_login_portal_selection(
347        &self,
348        contract_id: &str,
349    ) -> Result<bool, TrellisAuthError> {
350        Ok(self
351            .call::<_, ClearLoginPortalSelectionResponse>(
352                "rpc.v1.Auth.ClearLoginPortalSelection",
353                &ClearLoginPortalSelectionRequest {
354                    contract_id: contract_id.to_string(),
355                },
356            )
357            .await?
358            .success)
359    }
360
361    /// Get the deployment-wide workload portal default.
362    pub async fn get_workload_portal_default(
363        &self,
364    ) -> Result<PortalDefaultRecord, TrellisAuthError> {
365        Ok(self
366            .call::<_, GetPortalDefaultResponse>(
367                "rpc.v1.Auth.GetWorkloadPortalDefault",
368                &trellis_sdk_auth::Empty {},
369            )
370            .await?
371            .default_portal)
372    }
373
374    /// Set the deployment-wide workload portal default.
375    pub async fn set_workload_portal_default(
376        &self,
377        portal_id: Option<&str>,
378    ) -> Result<PortalDefaultRecord, TrellisAuthError> {
379        Ok(self
380            .call::<_, SetPortalDefaultResponse>(
381                "rpc.v1.Auth.SetWorkloadPortalDefault",
382                &SetPortalDefaultRequest {
383                    portal_id: portal_id.map(ToOwned::to_owned),
384                },
385            )
386            .await?
387            .default_portal)
388    }
389
390    /// List profile-specific workload portal selections.
391    pub async fn list_workload_portal_selections(
392        &self,
393    ) -> Result<Vec<WorkloadPortalSelectionRecord>, TrellisAuthError> {
394        Ok(self
395            .call::<_, ListWorkloadPortalSelectionsResponse>(
396                "rpc.v1.Auth.ListWorkloadPortalSelections",
397                &trellis_sdk_auth::Empty {},
398            )
399            .await?
400            .selections)
401    }
402
403    /// Create or replace a profile-specific workload portal selection.
404    pub async fn set_workload_portal_selection(
405        &self,
406        profile_id: &str,
407        portal_id: Option<&str>,
408    ) -> Result<WorkloadPortalSelectionRecord, TrellisAuthError> {
409        Ok(self
410            .call::<_, SetWorkloadPortalSelectionResponse>(
411                "rpc.v1.Auth.SetWorkloadPortalSelection",
412                &SetWorkloadPortalSelectionRequest {
413                    profile_id: profile_id.to_string(),
414                    portal_id: portal_id.map(ToOwned::to_owned),
415                },
416            )
417            .await?
418            .selection)
419    }
420
421    /// Clear a profile-specific workload portal selection.
422    pub async fn clear_workload_portal_selection(
423        &self,
424        profile_id: &str,
425    ) -> Result<bool, TrellisAuthError> {
426        Ok(self
427            .call::<_, ClearWorkloadPortalSelectionResponse>(
428                "rpc.v1.Auth.ClearWorkloadPortalSelection",
429                &ClearWorkloadPortalSelectionRequest {
430                    profile_id: profile_id.to_string(),
431                },
432            )
433            .await?
434            .success)
435    }
436
437    /// List workload profiles.
438    pub async fn list_workload_profiles(
439        &self,
440        contract_id: Option<&str>,
441        disabled: bool,
442    ) -> Result<Vec<trellis_sdk_auth::AuthListWorkloadProfilesResponseProfilesItem>, TrellisAuthError> {
443        Ok(self
444            .call::<_, trellis_sdk_auth::AuthListWorkloadProfilesResponse>(
445                "rpc.v1.Auth.ListWorkloadProfiles",
446                &trellis_sdk_auth::AuthListWorkloadProfilesRequest {
447                    contract_id: contract_id.map(ToOwned::to_owned),
448                    disabled: if disabled { Some(true) } else { None },
449                },
450            )
451            .await?
452            .profiles)
453    }
454
455    /// Create a workload profile.
456    pub async fn create_workload_profile(
457        &self,
458        profile_id: &str,
459        contract_id: &str,
460        allow_digests: &[String],
461        review_mode: Option<&str>,
462        contract: Option<BTreeMap<String, Value>>,
463    ) -> Result<trellis_sdk_auth::AuthCreateWorkloadProfileResponseProfile, TrellisAuthError> {
464        #[derive(Debug, Clone, Deserialize, Serialize)]
465        #[serde(rename_all = "camelCase")]
466        struct CreateWorkloadProfileRequest {
467            allowed_digests: Vec<String>,
468            contract_id: String,
469            #[serde(skip_serializing_if = "Option::is_none")]
470            contract: Option<BTreeMap<String, Value>>,
471            #[serde(skip_serializing_if = "Option::is_none")]
472            review_mode: Option<Value>,
473            profile_id: String,
474        }
475
476        Ok(self
477            .call::<_, trellis_sdk_auth::AuthCreateWorkloadProfileResponse>(
478                "rpc.v1.Auth.CreateWorkloadProfile",
479                &CreateWorkloadProfileRequest {
480                    profile_id: profile_id.to_string(),
481                    contract_id: contract_id.to_string(),
482                    allowed_digests: allow_digests.to_vec(),
483                    review_mode: review_mode.map(|value| serde_json::json!(value)),
484                    contract,
485                },
486            )
487            .await?
488            .profile)
489    }
490
491    /// Disable a workload profile.
492    pub async fn disable_workload_profile(
493        &self,
494        profile_id: &str,
495    ) -> Result<bool, TrellisAuthError> {
496        Ok(self
497            .call::<_, trellis_sdk_auth::AuthDisableWorkloadProfileResponse>(
498                "rpc.v1.Auth.DisableWorkloadProfile",
499                &trellis_sdk_auth::AuthDisableWorkloadProfileRequest {
500                    profile_id: profile_id.to_string(),
501                },
502            )
503            .await?
504            .success)
505    }
506
507    /// Provision a workload instance.
508    pub async fn provision_workload_instance(
509        &self,
510        profile_id: &str,
511        public_identity_key: &str,
512        activation_key: &str,
513    ) -> Result<trellis_sdk_auth::AuthProvisionWorkloadInstanceResponseInstance, TrellisAuthError> {
514        Ok(self
515            .call::<_, trellis_sdk_auth::AuthProvisionWorkloadInstanceResponse>(
516                "rpc.v1.Auth.ProvisionWorkloadInstance",
517                &trellis_sdk_auth::AuthProvisionWorkloadInstanceRequest {
518                    profile_id: profile_id.to_string(),
519                    public_identity_key: public_identity_key.to_string(),
520                    activation_key: activation_key.to_string(),
521                },
522            )
523            .await?
524            .instance)
525    }
526
527    /// Get workload activation status for one handoff.
528    pub async fn get_workload_activation_status(
529        &self,
530        handoff_id: &str,
531    ) -> Result<trellis_sdk_auth::AuthGetWorkloadActivationStatusResponse, TrellisAuthError> {
532        self.call(
533            "rpc.v1.Auth.GetWorkloadActivationStatus",
534            &trellis_sdk_auth::AuthGetWorkloadActivationStatusRequest {
535                handoff_id: handoff_id.to_string(),
536            },
537        )
538        .await
539    }
540
541    /// List workload instances.
542    pub async fn list_workload_instances(
543        &self,
544        profile_id: Option<&str>,
545        state: Option<&str>,
546    ) -> Result<Vec<trellis_sdk_auth::AuthListWorkloadInstancesResponseInstancesItem>, TrellisAuthError> {
547        Ok(self
548            .call::<_, trellis_sdk_auth::AuthListWorkloadInstancesResponse>(
549                "rpc.v1.Auth.ListWorkloadInstances",
550                &trellis_sdk_auth::AuthListWorkloadInstancesRequest {
551                    profile_id: profile_id.map(ToOwned::to_owned),
552                    state: state.map(|value| serde_json::json!(value)),
553                },
554            )
555            .await?
556            .instances)
557    }
558
559    /// Disable a workload instance.
560    pub async fn disable_workload_instance(
561        &self,
562        instance_id: &str,
563    ) -> Result<bool, TrellisAuthError> {
564        Ok(self
565            .call::<_, trellis_sdk_auth::AuthDisableWorkloadInstanceResponse>(
566                "rpc.v1.Auth.DisableWorkloadInstance",
567                &trellis_sdk_auth::AuthDisableWorkloadInstanceRequest {
568                    instance_id: instance_id.to_string(),
569                },
570            )
571            .await?
572            .success)
573    }
574
575    /// List workload activations.
576    pub async fn list_workload_activations(
577        &self,
578        instance_id: Option<&str>,
579        profile_id: Option<&str>,
580        state: Option<&str>,
581    ) -> Result<Vec<trellis_sdk_auth::AuthListWorkloadActivationsResponseActivationsItem>, TrellisAuthError> {
582        Ok(self
583            .call::<_, trellis_sdk_auth::AuthListWorkloadActivationsResponse>(
584                "rpc.v1.Auth.ListWorkloadActivations",
585                &trellis_sdk_auth::AuthListWorkloadActivationsRequest {
586                    instance_id: instance_id.map(ToOwned::to_owned),
587                    profile_id: profile_id.map(ToOwned::to_owned),
588                    state: state.map(|value| serde_json::json!(value)),
589                },
590            )
591            .await?
592            .activations)
593    }
594
595    /// Revoke a workload activation.
596    pub async fn revoke_workload_activation(
597        &self,
598        instance_id: &str,
599    ) -> Result<bool, TrellisAuthError> {
600        Ok(self
601            .call::<_, trellis_sdk_auth::AuthRevokeWorkloadActivationResponse>(
602                "rpc.v1.Auth.RevokeWorkloadActivation",
603                &trellis_sdk_auth::AuthRevokeWorkloadActivationRequest {
604                    instance_id: instance_id.to_string(),
605                },
606            )
607            .await?
608            .success)
609    }
610
611    /// List workload activation reviews.
612    pub async fn list_workload_activation_reviews(
613        &self,
614        instance_id: Option<&str>,
615        profile_id: Option<&str>,
616        state: Option<&str>,
617    ) -> Result<Vec<trellis_sdk_auth::AuthListWorkloadActivationReviewsResponseReviewsItem>, TrellisAuthError> {
618        Ok(self
619            .call::<_, trellis_sdk_auth::AuthListWorkloadActivationReviewsResponse>(
620                "rpc.v1.Auth.ListWorkloadActivationReviews",
621                &trellis_sdk_auth::AuthListWorkloadActivationReviewsRequest {
622                    instance_id: instance_id.map(ToOwned::to_owned),
623                    profile_id: profile_id.map(ToOwned::to_owned),
624                    state: state.map(|value| serde_json::json!(value)),
625                },
626            )
627            .await?
628            .reviews)
629    }
630
631    /// Decide one workload activation review.
632    pub async fn decide_workload_activation_review(
633        &self,
634        review_id: &str,
635        decision: &str,
636        reason: Option<&str>,
637    ) -> Result<trellis_sdk_auth::AuthDecideWorkloadActivationReviewResponse, TrellisAuthError> {
638        self.call(
639            "rpc.v1.Auth.DecideWorkloadActivationReview",
640            &trellis_sdk_auth::AuthDecideWorkloadActivationReviewRequest {
641                review_id: review_id.to_string(),
642                decision: serde_json::json!(decision),
643                reason: reason.map(ToOwned::to_owned),
644            },
645        )
646        .await
647    }
648
649    /// Install one service contract remotely.
650    pub async fn install_service(
651        &self,
652        input: &AuthInstallServiceRequest,
653    ) -> Result<AuthInstallServiceResponse, TrellisAuthError> {
654        self.call("rpc.v1.Auth.InstallService", input).await
655    }
656
657    /// Upgrade one installed service contract remotely.
658    pub async fn upgrade_service_contract(
659        &self,
660        input: &AuthUpgradeServiceContractRequest,
661    ) -> Result<AuthUpgradeServiceContractResponse, TrellisAuthError> {
662        self.call("rpc.v1.Auth.UpgradeServiceContract", input).await
663    }
664
665    /// Fetch one installed contract by digest.
666    pub async fn get_installed_contract(
667        &self,
668        input: &AuthGetInstalledContractRequest,
669    ) -> Result<AuthGetInstalledContractResponse, TrellisAuthError> {
670        self.call("rpc.v1.Auth.GetInstalledContract", input).await
671    }
672
673    /// Validate one request payload.
674    pub async fn validate_request(
675        &self,
676        input: &AuthValidateRequestRequest,
677    ) -> Result<AuthValidateRequestResponse, TrellisAuthError> {
678        self.call("rpc.v1.Auth.ValidateRequest", input).await
679    }
680
681    /// List installed services.
682    pub async fn list_services(&self) -> Result<Vec<ServiceListEntry>, TrellisAuthError> {
683        Ok(self
684            .call::<_, ListServicesResponse>("rpc.v1.Auth.ListServices", &Empty {})
685            .await?
686            .services)
687    }
688
689    /// Log out the current admin session remotely.
690    pub async fn logout(&self) -> Result<bool, TrellisAuthError> {
691        Ok(self
692            .call::<_, LogoutResponse>("rpc.v1.Auth.Logout", &Empty {})
693            .await?
694            .success)
695    }
696
697    /// Mint and persist a fresh binding token for the current session.
698    pub async fn renew_binding_token(
699        &self,
700        state: &mut AdminSessionState,
701    ) -> Result<(), TrellisAuthError> {
702        persist_renewed_admin_session(
703            state,
704            self.call("rpc.v1.Auth.RenewBindingToken", &Empty {})
705                .await?,
706        )
707    }
708}
709
710#[derive(Debug, Serialize)]
711struct Empty {}
712
713#[cfg(test)]
714mod tests {
715    use serde_json::{json, Value};
716
717    use super::{
718        CreatePortalRequest, GetPortalDefaultResponse, LoginPortalSelectionRecord, PortalDefaultRecord,
719        PortalRecord, SetLoginPortalSelectionRequest, SetWorkloadPortalSelectionRequest,
720        SetWorkloadPortalSelectionResponse, WorkloadPortalSelectionRecord,
721    };
722
723    #[test]
724    fn portal_create_requests_serialize_with_camel_case_fields() {
725        let value = serde_json::to_value(CreatePortalRequest {
726            portal_id: "main".to_string(),
727            app_contract_id: Some("trellis.portal@v1".to_string()),
728            entry_url: "https://portal.example.com/auth".to_string(),
729        })
730        .expect("serialize portal create request");
731
732        assert_eq!(
733            value,
734            json!({
735                "portalId": "main",
736                "appContractId": "trellis.portal@v1",
737                "entryUrl": "https://portal.example.com/auth"
738            })
739        );
740    }
741
742    #[test]
743    fn portal_records_and_defaults_deserialize_from_camel_case_fields() {
744        let portal: PortalRecord = serde_json::from_value(json!({
745            "portalId": "main",
746            "appContractId": "trellis.portal@v1",
747            "entryUrl": "https://portal.example.com/auth",
748            "disabled": false
749        }))
750        .expect("deserialize portal record");
751        assert_eq!(portal.portal_id, "main");
752        assert_eq!(portal.app_contract_id.as_deref(), Some("trellis.portal@v1"));
753        assert_eq!(portal.entry_url, "https://portal.example.com/auth");
754
755        let response: GetPortalDefaultResponse = serde_json::from_value(json!({
756            "defaultPortal": {
757                "portalId": Value::Null
758            }
759        }))
760        .expect("deserialize portal default response");
761        assert_eq!(response.default_portal.portal_id, None);
762
763        let default_value = serde_json::to_value(PortalDefaultRecord {
764            portal_id: Some("main".to_string()),
765        })
766        .expect("serialize portal default record");
767        assert_eq!(default_value, json!({ "portalId": "main" }));
768    }
769
770    #[test]
771    fn portal_selection_types_use_camel_case_json() {
772        let login_request = serde_json::to_value(SetLoginPortalSelectionRequest {
773            contract_id: "trellis.console@v1".to_string(),
774            portal_id: Some("main".to_string()),
775        })
776        .expect("serialize login portal selection request");
777        assert_eq!(
778            login_request,
779            json!({
780                "contractId": "trellis.console@v1",
781                "portalId": "main"
782            })
783        );
784
785        let login_record: LoginPortalSelectionRecord = serde_json::from_value(json!({
786            "contractId": "trellis.console@v1",
787            "portalId": "main"
788        }))
789        .expect("deserialize login portal selection record");
790        assert_eq!(login_record.contract_id, "trellis.console@v1");
791        assert_eq!(login_record.portal_id.as_deref(), Some("main"));
792
793        let workload_request = serde_json::to_value(SetWorkloadPortalSelectionRequest {
794            profile_id: "reader.default".to_string(),
795            portal_id: None,
796        })
797        .expect("serialize workload portal selection request");
798        assert_eq!(
799            workload_request,
800            json!({
801                "profileId": "reader.default",
802                "portalId": Value::Null
803            })
804        );
805
806        let workload_response: SetWorkloadPortalSelectionResponse = serde_json::from_value(json!({
807            "selection": {
808                "profileId": "reader.default",
809                "portalId": "main"
810            }
811        }))
812        .expect("deserialize workload portal selection response");
813        assert_eq!(workload_response.selection.profile_id, "reader.default");
814        assert_eq!(workload_response.selection.portal_id.as_deref(), Some("main"));
815
816        let workload_record_value = serde_json::to_value(WorkloadPortalSelectionRecord {
817            profile_id: "reader.default".to_string(),
818            portal_id: Some("main".to_string()),
819        })
820        .expect("serialize workload portal selection record");
821        assert_eq!(
822            workload_record_value,
823            json!({
824                "profileId": "reader.default",
825                "portalId": "main"
826            })
827        );
828    }
829}