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