Skip to main content

spiral/
application.rs

1//! Credential-specific application handles and organization administration.
2
3use crate::auth::{CredentialChain, OidcAuthProvider, TokenSource, token_endpoint};
4use crate::resources::applications::{
5    Application as ApplicationInfo, ApplicationConfiguration, ApplicationsClient,
6    Installation as InstallationInfo, InstallationDetails, InstallationStatusChange,
7    InstallationSummary, InstallationsClient, ListApplicationsRequest,
8};
9use crate::{Client, Error, List, Page, Project};
10
11/// The authenticated application. Registration administration is a separate handle.
12///
13/// ```compile_fail
14/// fn delete(app: spiral::Application) { app.delete(); }
15/// ```
16#[derive(Clone)]
17pub struct Application {
18    client: ApplicationsClient,
19}
20
21/// A project-owned application registration, accessed with management credentials.
22#[derive(Clone)]
23pub struct ApplicationRegistration {
24    client: ApplicationsClient,
25    id: String,
26}
27
28/// An installation discovered by its owning application, before authenticating as it.
29///
30/// ```compile_fail
31/// fn projects(installation: spiral::ApplicationInstallation) { installation.projects(); }
32/// ```
33#[derive(Clone)]
34pub struct ApplicationInstallation {
35    client: ApplicationsClient,
36    id: String,
37}
38
39/// An authenticated installation. Project operations retain installation credentials.
40#[derive(Clone)]
41pub struct Installation {
42    client: InstallationsClient,
43}
44
45/// An organization installation accessed with organization management credentials.
46#[derive(Clone)]
47pub struct InstallationRegistration {
48    client: InstallationsClient,
49    id: String,
50}
51
52impl Client {
53    /// Select an application registration for management, without a request.
54    pub fn application(&self, id: impl Into<String>) -> ApplicationRegistration {
55        self.applications().application(id)
56    }
57    /// Select an organization installation for management, without a request.
58    pub fn installation(&self, id: impl Into<String>) -> InstallationRegistration {
59        self.installations().installation(id)
60    }
61}
62impl Application {
63    pub(crate) fn new(client: ApplicationsClient) -> Self {
64        Self { client }
65    }
66    /// Read the authenticated application's metadata.
67    pub async fn get(&self) -> Result<ApplicationInfo, Error> {
68        Ok(self.client.service.client.get_self().await?)
69    }
70    /// Discover installations as handles that can obtain installation credentials.
71    pub fn installations(&self) -> List<'_, ApplicationInstallation> {
72        List::new(move |options| {
73            Box::pin(async move {
74                let page = self
75                    .client
76                    .service
77                    .client
78                    .list_self_installations(ListApplicationsRequest {
79                        page_size: options.size,
80                        page_token: options.token,
81                    })
82                    .await?;
83                Ok(Page {
84                    items: page
85                        .installations
86                        .into_iter()
87                        .map(|info| self.installation(info.id))
88                        .collect(),
89                    next_page_token: page.next_page_token,
90                })
91            })
92        })
93    }
94    /// Select an installation of this application. This does not change credentials.
95    pub fn installation(&self, id: impl Into<String>) -> ApplicationInstallation {
96        ApplicationInstallation {
97            client: self.client.clone(),
98            id: id.into(),
99        }
100    }
101}
102impl ApplicationInstallation {
103    /// Stable installation identifier.
104    pub fn id(&self) -> &str {
105        &self.id
106    }
107    /// Read installation metadata using app authentication.
108    pub async fn get(&self) -> Result<InstallationDetails, Error> {
109        Ok(self
110            .client
111            .service
112            .client
113            .get_self_installation(self.id.clone())
114            .await?)
115    }
116    /// Authenticate as this installation using the app's renewable credentials.
117    /// The returned handle retains the API, data and auth endpoints and embedded library
118    /// configuration. Embedded execution obtains storage credentials as the installation.
119    /// Custom backends cannot be rebound and return an error instead of changing execution.
120    /// The authorization server checks the app binding and active installation registration.
121    pub async fn authenticate(&self) -> Result<Installation, Error> {
122        let service = &self.client.service;
123        let source = service.auth.clone().ok_or(Error::CredentialsNotFound)?;
124        let provider = TokenSource::from(CredentialChain::new(vec![
125            OidcAuthProvider::new(
126                service.http.clone(),
127                token_endpoint(&service.auth_url)?,
128                self.id.clone(),
129            )
130            .with_assertion_provider(source)
131            .into(),
132        ]));
133        let client = service.connection(Some(provider))?;
134        // Fail at the explicit transition, before returning a usable installation handle.
135        client.access_token().await?;
136        Ok(client.installations().current())
137    }
138}
139impl ApplicationRegistration {
140    pub(crate) fn new(client: ApplicationsClient, id: String) -> Self {
141        Self { client, id }
142    }
143    /// Stable registration identifier.
144    pub fn id(&self) -> &str {
145        &self.id
146    }
147    /// Read registration metadata.
148    pub async fn get(&self) -> Result<ApplicationInfo, Error> {
149        Ok(self
150            .client
151            .service
152            .client
153            .get_application(self.id.clone())
154            .await?)
155    }
156    /// Replace registration configuration with registration management permission.
157    pub async fn update(
158        &self,
159        configuration: ApplicationConfiguration,
160    ) -> Result<ApplicationInfo, Error> {
161        Ok(self
162            .client
163            .service
164            .client
165            .put_application(self.id.clone(), configuration)
166            .await?)
167    }
168    /// Delete the registration and uninstall it everywhere.
169    pub async fn delete(&self) -> Result<ApplicationInfo, Error> {
170        Ok(self
171            .client
172            .service
173            .client
174            .delete_application(self.id.clone())
175            .await?)
176    }
177    /// Inspect this registration's installations with developer management permission.
178    pub fn installations(&self) -> List<'_, InstallationInfo> {
179        self.client
180            .service
181            .client
182            .list_application_installations_list(self.id.clone(), Default::default())
183    }
184}
185impl Installation {
186    pub(crate) fn current(client: InstallationsClient) -> Self {
187        Self { client }
188    }
189    /// Read the credential-derived installation's live state.
190    pub async fn get(&self) -> Result<InstallationDetails, Error> {
191        Ok(self
192            .client
193            .service
194            .client
195            .get_current_installation()
196            .await?)
197    }
198    /// List projects authorized for this installation. Every page rechecks access.
199    pub fn projects(&self) -> List<'_, Project> {
200        List::new(move |options| {
201            Box::pin(async move {
202                let service = &self.client.service;
203                let page = service
204                    .client
205                    .list_installation_projects(ListApplicationsRequest {
206                        page_size: options.size,
207                        page_token: options.token,
208                    })
209                    .await?;
210                let client = &self.client.connection;
211                Ok(Page {
212                    items: page
213                        .projects
214                        .into_iter()
215                        .map(|info| client.project(info.id))
216                        .collect::<Result<_, _>>()?,
217                    next_page_token: page.next_page_token,
218                })
219            })
220        })
221    }
222    /// Select a project with installation credentials; server authorization applies to every
223    /// request.
224    pub fn project(&self, id: impl AsRef<str>) -> Result<Project, Error> {
225        self.client.connection.project(id)
226    }
227}
228impl InstallationRegistration {
229    pub(crate) fn new(client: InstallationsClient, id: String) -> Self {
230        Self { client, id }
231    }
232    /// Stable installation identifier.
233    pub fn id(&self) -> &str {
234        &self.id
235    }
236    /// Read the organization's installation configuration.
237    pub async fn get(&self) -> Result<InstallationInfo, Error> {
238        Ok(self
239            .client
240            .service
241            .client
242            .get_installation(self.id.clone())
243            .await?)
244    }
245    /// Approve the latest request as an organization owner.
246    pub async fn approve(&self) -> Result<InstallationInfo, Error> {
247        Ok(self
248            .client
249            .service
250            .client
251            .approve_installation(self.id.clone())
252            .await?)
253    }
254    /// Suspend an installation as an organization owner.
255    pub async fn suspend(&self) -> Result<InstallationInfo, Error> {
256        Ok(self
257            .client
258            .service
259            .client
260            .set_installation_status(self.id.clone(), InstallationStatusChange { enabled: false })
261            .await?)
262    }
263    /// Resume an installation as an organization owner.
264    pub async fn resume(&self) -> Result<InstallationInfo, Error> {
265        Ok(self
266            .client
267            .service
268            .client
269            .set_installation_status(self.id.clone(), InstallationStatusChange { enabled: true })
270            .await?)
271    }
272    /// Uninstall the app as an organization owner.
273    pub async fn uninstall(&self) -> Result<InstallationInfo, Error> {
274        Ok(self
275            .client
276            .service
277            .client
278            .uninstall(self.id.clone())
279            .await?)
280    }
281}
282
283/// The same installation viewed through delegated user credentials, never a machine token.
284///
285/// ```compile_fail
286/// fn approve(installation: spiral::UserInstallation) { installation.approve(); }
287/// ```
288#[derive(Clone)]
289pub struct UserInstallation {
290    client: InstallationsClient,
291    info: InstallationSummary,
292}
293impl UserInstallation {
294    pub(crate) fn new(client: InstallationsClient, info: InstallationSummary) -> Self {
295        Self { client, info }
296    }
297    /// Stable installation identifier.
298    pub fn id(&self) -> &str {
299        &self.info.id
300    }
301    /// Metadata returned during discovery, without another request.
302    pub fn info(&self) -> &InstallationSummary {
303        &self.info
304    }
305    /// Refresh installation metadata, retaining the original installation identity.
306    pub async fn get(&self) -> Result<InstallationSummary, Error> {
307        let service = &self.client.service;
308        service
309            .client
310            .get_user_installation()
311            .await?
312            .installation
313            .filter(|info| info.id == self.info.id)
314            .ok_or_else(|| Error::NotFound(self.info.id.clone()))
315    }
316    /// List project metadata in the user's intersection with this installation.
317    pub fn projects(&self) -> List<'_, Project> {
318        List::new(move |options| {
319            Box::pin(async move {
320                let service = &self.client.service;
321                let page = service
322                    .client
323                    .list_user_installation_projects(
324                        self.info.id.clone(),
325                        ListApplicationsRequest {
326                            page_size: options.size,
327                            page_token: options.token,
328                        },
329                    )
330                    .await?;
331                Ok(Page {
332                    items: page
333                        .projects
334                        .into_iter()
335                        .map(|info| self.project(info.id))
336                        .collect::<Result<_, _>>()?,
337                    next_page_token: page.next_page_token,
338                })
339            })
340        })
341    }
342    /// Select a project using this delegated user credential. The server enforces the live
343    /// user and installation intersection on every project and resource authorization check.
344    pub fn project(&self, id: impl AsRef<str>) -> Result<Project, Error> {
345        self.client.connection.project(id)
346    }
347}