Skip to content

Commit 705c84e

Browse files
committed
feat: deliver secret-carrying lifecycle events synchronously, drop the retry engine
Only two events still carry a secret — an app being registered and an operator rotating its secret — and both happen with a human at a dialog. So the delivery is now synchronous and its outcome comes back in the response the dialog renders: "the app received these automatically" or "couldn't reach the app, copy the secret now". A failure is a value, never thrown, because the credentials were returned in the response too. The installed and uninstalled deliveries are gone (they carried no secret; an app sees its installs change in its own discovery call), and with them the whole fire-and-forget machinery: the dispatcher, the retry scheduler, the pending-delivery holder, the AppDelivery entity, its repository, model, assembler, the owner deliveries endpoint and dialog, the app_delivery table and the retry-tuning properties. Self-registration and app-initiated rotation deliver nothing — the caller is the app and reads the credentials from its own response.
1 parent 2b9f40e commit 705c84e

34 files changed

Lines changed: 263 additions & 1262 deletions

backend/api/src/main/kotlin/io/tolgee/api/v2/controllers/organization/OrganizationOwnedAppsController.kt

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package io.tolgee.api.v2.controllers.organization
22

33
import io.swagger.v3.oas.annotations.Operation
44
import io.swagger.v3.oas.annotations.tags.Tag
5-
import io.tolgee.hateoas.apps.AppDeliveryModel
6-
import io.tolgee.hateoas.apps.AppDeliveryModelAssembler
75
import io.tolgee.hateoas.apps.AppSecretModel
86
import io.tolgee.hateoas.apps.AppSecretModelAssembler
97
import io.tolgee.hateoas.organization.apps.OwnedAppModel
@@ -14,7 +12,6 @@ import io.tolgee.service.apps.AppOwnerRemovalService
1412
import io.tolgee.service.apps.AppSecretRotationService
1513
import io.tolgee.service.apps.AppSecretService
1614
import io.tolgee.service.apps.AppService
17-
import io.tolgee.service.apps.lifecycle.AppLifecycleDeliveryService
1815
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
1916
import org.springframework.hateoas.CollectionModel
2017
import org.springframework.web.bind.annotation.CrossOrigin
@@ -27,8 +24,7 @@ import org.springframework.web.bind.annotation.RestController
2724

2825
/**
2926
* What an organization may do with an app it **registered**, as opposed to one it merely installed:
30-
* rotate the app-level credentials, see why a lifecycle delivery failed, and take the app off every
31-
* organization on the server.
27+
* rotate the app-level credentials and take the app off every organization on the server.
3228
*
3329
* Every endpoint resolves the app within the organization, so an organization that installed
3430
* somebody else's app reaches none of this — app-level credentials are the owner's alone.
@@ -43,10 +39,8 @@ class OrganizationOwnedAppsController(
4339
private val appSecretService: AppSecretService,
4440
private val appSecretRotationService: AppSecretRotationService,
4541
private val appOwnerRemovalService: AppOwnerRemovalService,
46-
private val appLifecycleDeliveryService: AppLifecycleDeliveryService,
4742
private val ownedAppModelAssembler: OwnedAppModelAssembler,
4843
private val appSecretModelAssembler: AppSecretModelAssembler,
49-
private val appDeliveryModelAssembler: AppDeliveryModelAssembler,
5044
) {
5145
@GetMapping
5246
@RequiresOrganizationRole(OrganizationRoleType.OWNER)
@@ -96,17 +90,22 @@ class OrganizationOwnedAppsController(
9690
summary = "Issue an additional app-level client secret",
9791
description =
9892
"Phase one of an app-level rotation: mints a second secret while every existing one keeps " +
99-
"working. The app's installs, their own secrets, their organization availability and their " +
100-
"per-project enablements are all untouched. The new secret is both returned here — the " +
101-
"only place it is ever disclosed — and pushed to the app over the lifecycle channel.",
93+
"working. The app's installs, their organization availability and their per-project " +
94+
"enablements are all untouched. The new secret is both returned here — the only place it " +
95+
"is ever disclosed — and pushed to the app over the lifecycle channel; the `delivery` " +
96+
"field says whether the app took it.",
10297
)
10398
fun issueSecret(
10499
@PathVariable organizationId: Long,
105100
@PathVariable appId: Long,
106101
): AppSecretModel {
107102
val app = appService.getOwned(organizationId, appId)
108-
val issued = appSecretRotationService.issue(app)
109-
return appSecretModelAssembler.toModelWithSecret(issued.secret, issued.plaintextSecret)
103+
val rotation = appSecretRotationService.issueAndDeliver(app)
104+
return appSecretModelAssembler.toModelWithSecret(
105+
rotation.issued.secret,
106+
rotation.issued.plaintextSecret,
107+
rotation.delivery,
108+
)
110109
}
111110

112111
@DeleteMapping("/{appId}/secrets/{secretId}")
@@ -127,31 +126,14 @@ class OrganizationOwnedAppsController(
127126
return appSecretModelAssembler.toModel(appSecretService.revoke(app.id, secretId, allowRevokingLast = true))
128127
}
129128

130-
@GetMapping("/{appId}/deliveries")
131-
@RequiresOrganizationRole(OrganizationRoleType.OWNER)
132-
@Operation(
133-
summary = "List the app's lifecycle deliveries",
134-
description =
135-
"Every signed POST Tolgee has made to the app's base URL, with the outcome of each. This is " +
136-
"where an owner finds out that an install's credentials never reached the app.",
137-
)
138-
fun listDeliveries(
139-
@PathVariable organizationId: Long,
140-
@PathVariable appId: Long,
141-
): CollectionModel<AppDeliveryModel> {
142-
val app = appService.getOwned(organizationId, appId)
143-
return appDeliveryModelAssembler.toCollectionModel(appLifecycleDeliveryService.listForApp(app.appId))
144-
}
145-
146129
@DeleteMapping("/{appId}")
147130
@RequiresOrganizationRole(OrganizationRoleType.OWNER)
148131
@Operation(
149132
summary = "Remove the app from every organization",
150133
description =
151134
"Deregisters the app and uninstalls it from every organization that installed it, revoking " +
152-
"both the app-level and every per-install credential, and announcing an uninstall to the " +
153-
"app for each of those organizations. Only the owner may do this; an organization that " +
154-
"installed the app removes only its own install through `DELETE /apps/{installId}`.",
135+
"its credentials. Only the owner may do this; an organization that installed the app " +
136+
"removes only its own install through `DELETE /apps/{installId}`.",
155137
)
156138
fun removeEverywhere(
157139
@PathVariable organizationId: Long,

backend/api/src/main/kotlin/io/tolgee/hateoas/apps/AppDeliveryModel.kt

Lines changed: 0 additions & 26 deletions
This file was deleted.

backend/api/src/main/kotlin/io/tolgee/hateoas/apps/AppDeliveryModelAssembler.kt

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.tolgee.hateoas.apps
2+
3+
import io.swagger.v3.oas.annotations.media.Schema
4+
5+
/**
6+
* Whether Tolgee managed to hand the just-disclosed credentials to the app. Present only in the
7+
* registration and rotation responses, so the dialog can tell the operator whether the app got them
8+
* or whether they still have to copy the secret by hand.
9+
*/
10+
@Schema(description = "Outcome of pushing the disclosed credentials to the app's base URL")
11+
data class AppDeliveryOutcomeModel(
12+
@Schema(description = "False when there was nothing to deliver to")
13+
val attempted: Boolean,
14+
val delivered: Boolean,
15+
@Schema(description = "Short reason when the delivery was attempted and failed; null otherwise")
16+
val error: String?,
17+
)

backend/api/src/main/kotlin/io/tolgee/hateoas/apps/AppModel.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,11 @@ open class AppModel(
2929
"to registering the app.",
3030
)
3131
val webhookSecret: String? = null,
32+
@Schema(
33+
description =
34+
"Whether Tolgee managed to push these just-issued credentials to the app's base URL. Present " +
35+
"only in the response to registering the app, so the dialog can tell the operator whether " +
36+
"the app got them or they still have to copy the secret by hand.",
37+
)
38+
val delivery: AppDeliveryOutcomeModel? = null,
3239
) : RepresentationModel<AppModel>()

backend/api/src/main/kotlin/io/tolgee/hateoas/apps/AppSecretModel.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,10 @@ open class AppSecretModel(
2121
"endpoint exchanges it for the short-lived tokens that reach translation data.",
2222
)
2323
val secret: String? = null,
24+
@Schema(
25+
description =
26+
"Whether the new secret reached the app over the lifecycle channel. Present only in the " +
27+
"response to an owner issuing it; null on the app-initiated path and when listing.",
28+
)
29+
val delivery: AppDeliveryOutcomeModel? = null,
2430
) : RepresentationModel<AppSecretModel>()
Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
11
package io.tolgee.hateoas.apps
22

3+
import io.tolgee.dtos.apps.AppLifecycleDeliveryOutcome
34
import io.tolgee.model.apps.AppSecret
45
import org.springframework.hateoas.server.RepresentationModelAssembler
56
import org.springframework.stereotype.Component
67

78
@Component
89
class AppSecretModelAssembler : RepresentationModelAssembler<AppSecret, AppSecretModel> {
910
override fun toModel(entity: AppSecret): AppSecretModel {
10-
return build(entity, plaintextSecret = null)
11+
return build(entity, plaintextSecret = null, delivery = null)
1112
}
1213

1314
/** Builds the model including the plaintext — used once, in the response to issuing the secret. */
1415
fun toModelWithSecret(
1516
entity: AppSecret,
1617
plaintextSecret: String,
18+
delivery: AppLifecycleDeliveryOutcome? = null,
1719
): AppSecretModel {
18-
return build(entity, plaintextSecret)
20+
return build(entity, plaintextSecret, delivery)
1921
}
2022

2123
private fun build(
2224
entity: AppSecret,
2325
plaintextSecret: String?,
26+
delivery: AppLifecycleDeliveryOutcome?,
2427
): AppSecretModel {
2528
return AppSecretModel(
2629
id = entity.id,
@@ -29,6 +32,10 @@ class AppSecretModelAssembler : RepresentationModelAssembler<AppSecret, AppSecre
2932
lastUsedAt = entity.lastUsedAt?.time,
3033
revokedAt = entity.revokedAt?.time,
3134
secret = plaintextSecret,
35+
delivery = delivery?.toModel(),
3236
)
3337
}
3438
}
39+
40+
fun AppLifecycleDeliveryOutcome.toModel(): AppDeliveryOutcomeModel =
41+
AppDeliveryOutcomeModel(attempted = attempted, delivered = delivered, error = error)

backend/api/src/main/kotlin/io/tolgee/hateoas/organization/apps/AppInstallModelAssembler.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package io.tolgee.hateoas.organization.apps
22

3+
import io.tolgee.dtos.apps.AppLifecycleDeliveryOutcome
34
import io.tolgee.dtos.apps.AppManifest
45
import io.tolgee.hateoas.apps.AppModel
6+
import io.tolgee.hateoas.apps.toModel
57
import io.tolgee.model.apps.AppInstall
68
import io.tolgee.service.apps.AppInstallService
79
import io.tolgee.service.apps.AppService
@@ -25,16 +27,17 @@ class AppInstallModelAssembler(
2527
* installed somebody else's app from ever seeing its app-level credentials.
2628
*/
2729
fun toModel(result: AppInstallService.RegisterResult): AppInstallModel {
28-
return build(result.install, appModel(result.app, result.appCredentials))
30+
return build(result.install, appModel(result.app, result.appCredentials, result.delivery))
2931
}
3032

3133
fun toModel(result: AppInstallService.SelfRegisterResult): AppInstallModel {
32-
return build(result.install, appModel(result.app, result.appCredentials), created = result.created)
34+
return build(result.install, appModel(result.app, result.appCredentials, delivery = null), created = result.created)
3335
}
3436

3537
private fun appModel(
3638
app: AppService.AppSummary,
3739
credentials: AppService.AppCredentials?,
40+
delivery: AppLifecycleDeliveryOutcome?,
3841
): AppModel {
3942
return AppModel(
4043
id = app.id,
@@ -43,6 +46,7 @@ class AppInstallModelAssembler(
4346
clientId = credentials?.clientId,
4447
clientSecret = credentials?.clientSecret,
4548
webhookSecret = credentials?.webhookSecret,
49+
delivery = delivery?.toModel(),
4650
)
4751
}
4852

0 commit comments

Comments
 (0)