Skip to content

Commit 1848d18

Browse files
authored
[external API] Add created time to webhook secret views (#8100)
When a webhook secret is created, it is assigned a UUID. In the API, secrets are always referenced by UUIDs, and do not have names. This is fine for programmatic access to secrets, such as when a receiver implementation adds or rotates its secrets automatically. However, it's less useful for a human user creating or deleting secrets in the UI, as they would have to keep the various UUIDs straight, which can be challenging. See discussion in #8083. While we _could_ add names to the secret resource so that human users can identify them more easily, this is not ideal either. Secrets, unlike other resouces, are basically fungible: the only thing a user would really ever do with them is rotate them, and they have no real semantic meaning beyond being "the old secret" or "the new secret". Therefore, giving them names basically just forces the user to come up with a unique string like "secret1" and "secret2" and so on, which is a bit unfortunate. Instead, we should display the creation timestamps of secrets in the UI. This way, users need not come up with unique names for every secret, but they can distinguish them beyond their UUID, and the timestamp communicates the one thing about webhook secrets that the user will actually care about: is it "the old one" or "the new one"? This branch updates the external API view for webhook secrets to add a `time_created` field. The view type itself is renamed from `WebhookSecretId` to just `WebhookSecret`, since it now contains more than an ID. Fixes #8083
1 parent 6e3fe96 commit 1848d18

File tree

7 files changed

+54
-35
lines changed

7 files changed

+54
-35
lines changed

nexus/db-model/src/webhook_rx.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,7 @@ impl TryFrom<WebhookReceiverConfig> for views::WebhookReceiver {
4141
fn try_from(
4242
WebhookReceiverConfig { rx, secrets, subscriptions }: WebhookReceiverConfig,
4343
) -> Result<views::WebhookReceiver, Self::Error> {
44-
let secrets = secrets
45-
.iter()
46-
.map(|WebhookSecret { identity, .. }| views::WebhookSecretId {
47-
id: identity.id.into_untyped_uuid(),
48-
})
49-
.collect();
44+
let secrets = secrets.iter().map(views::WebhookSecret::from).collect();
5045
let subscriptions = subscriptions
5146
.into_iter()
5247
.map(shared::WebhookSubscription::try_from)
@@ -156,9 +151,18 @@ impl WebhookSecret {
156151
}
157152
}
158153

159-
impl From<WebhookSecret> for views::WebhookSecretId {
154+
impl From<&'_ WebhookSecret> for views::WebhookSecret {
155+
fn from(secret: &WebhookSecret) -> Self {
156+
Self {
157+
id: secret.identity.id.into_untyped_uuid(),
158+
time_created: secret.identity.time_created,
159+
}
160+
}
161+
}
162+
163+
impl From<WebhookSecret> for views::WebhookSecret {
160164
fn from(secret: WebhookSecret) -> Self {
161-
Self { id: secret.identity.id.into_untyped_uuid() }
165+
Self::from(&secret)
162166
}
163167
}
164168

nexus/external-api/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3690,7 +3690,7 @@ pub trait NexusExternalApi {
36903690
rqctx: RequestContext<Self::Context>,
36913691
query_params: Query<params::WebhookReceiverSelector>,
36923692
params: TypedBody<params::WebhookSecretCreate>,
3693-
) -> Result<HttpResponseCreated<views::WebhookSecretId>, HttpError>;
3693+
) -> Result<HttpResponseCreated<views::WebhookSecret>, HttpError>;
36943694

36953695
/// Remove secret from webhook receiver
36963696
#[endpoint {

nexus/src/app/webhook.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -464,21 +464,21 @@ impl Nexus {
464464
opctx: &OpContext,
465465
rx: lookup::WebhookReceiver<'_>,
466466
secret: String,
467-
) -> Result<views::WebhookSecretId, Error> {
467+
) -> Result<views::WebhookSecret, Error> {
468468
let (authz_rx,) = rx.lookup_for(authz::Action::CreateChild).await?;
469469
let secret = WebhookSecret::new(authz_rx.id(), secret);
470-
let WebhookSecret { identity, .. } = self
470+
let secret = self
471471
.datastore()
472472
.webhook_rx_secret_create(opctx, &authz_rx, secret)
473473
.await?;
474-
let secret_id = identity.id;
475474
slog::info!(
476475
&opctx.log,
477476
"added secret to webhook receiver";
478477
"rx_id" => ?authz_rx.id(),
479-
"secret_id" => ?secret_id,
478+
"secret_id" => ?secret.identity.id,
479+
"time_created"=> %secret.identity.time_created,
480480
);
481-
Ok(views::WebhookSecretId { id: secret_id.into_untyped_uuid() })
481+
Ok(secret.into())
482482
}
483483

484484
pub async fn webhook_receiver_secret_delete(

nexus/src/external_api/http_entrypoints.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8089,7 +8089,7 @@ impl NexusExternalApi for NexusExternalApiImpl {
80898089
rqctx: RequestContext<Self::Context>,
80908090
query_params: Query<params::WebhookReceiverSelector>,
80918091
params: TypedBody<params::WebhookSecretCreate>,
8092-
) -> Result<HttpResponseCreated<views::WebhookSecretId>, HttpError> {
8092+
) -> Result<HttpResponseCreated<views::WebhookSecret>, HttpError> {
80938093
let apictx = rqctx.context();
80948094
let handler = async {
80958095
let nexus = &apictx.context.nexus;

nexus/tests/integration_tests/webhooks.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,10 @@ async fn secret_add(
191191
ctx: &ControlPlaneTestContext,
192192
webhook_id: WebhookReceiverUuid,
193193
params: &params::WebhookSecretCreate,
194-
) -> views::WebhookSecretId {
194+
) -> views::WebhookSecret {
195195
resource_helpers::object_create::<
196196
params::WebhookSecretCreate,
197-
views::WebhookSecretId,
197+
views::WebhookSecret,
198198
>(
199199
&ctx.external_client,
200200
&format!("{SECRETS_BASE_PATH}/?receiver={webhook_id}"),

nexus/types/src/external_api/views.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,7 +1079,7 @@ pub struct WebhookReceiver {
10791079
pub endpoint: Url,
10801080
// A list containing the IDs of the secret keys used to sign payloads sent
10811081
// to this receiver.
1082-
pub secrets: Vec<WebhookSecretId>,
1082+
pub secrets: Vec<WebhookSecret>,
10831083
/// The list of event classes to which this receiver is subscribed.
10841084
pub subscriptions: Vec<shared::WebhookSubscription>,
10851085
}
@@ -1093,13 +1093,21 @@ pub struct WebhookSubscriptionCreated {
10931093
/// A list of the IDs of secrets associated with a webhook.
10941094
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
10951095
pub struct WebhookSecrets {
1096-
pub secrets: Vec<WebhookSecretId>,
1096+
pub secrets: Vec<WebhookSecret>,
10971097
}
10981098

1099-
/// The public ID of a secret key assigned to a webhook.
1099+
/// A view of a shared secret key assigned to a webhook receiver.
1100+
///
1101+
/// Once a secret is created, the value of the secret is not available in the
1102+
/// API, as it must remain secret. Instead, secrets are referenced by their
1103+
/// unique IDs assigned when they are created.
11001104
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
1101-
pub struct WebhookSecretId {
1105+
pub struct WebhookSecret {
1106+
/// The public unique ID of the secret.
11021107
pub id: Uuid,
1108+
1109+
/// The UTC timestamp at which this secret was created.
1110+
pub time_created: DateTime<Utc>,
11031111
}
11041112

11051113
/// A delivery of a webhook event.

openapi/nexus.json

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12692,7 +12692,7 @@
1269212692
"content": {
1269312693
"application/json": {
1269412694
"schema": {
12695-
"$ref": "#/components/schemas/WebhookSecretId"
12695+
"$ref": "#/components/schemas/WebhookSecret"
1269612696
}
1269712697
}
1269812698
}
@@ -25949,7 +25949,7 @@
2594925949
"secrets": {
2595025950
"type": "array",
2595125951
"items": {
25952-
"$ref": "#/components/schemas/WebhookSecretId"
25952+
"$ref": "#/components/schemas/WebhookSecret"
2595325953
}
2595425954
},
2595525955
"subscriptions": {
@@ -26026,29 +26026,36 @@
2602626026
}
2602726027
}
2602826028
},
26029-
"WebhookSecretCreate": {
26029+
"WebhookSecret": {
26030+
"description": "A view of a shared secret key assigned to a webhook receiver.\n\nOnce a secret is created, the value of the secret is not available in the API, as it must remain secret. Instead, secrets are referenced by their unique IDs assigned when they are created.",
2603026031
"type": "object",
2603126032
"properties": {
26032-
"secret": {
26033-
"description": "The value of the shared secret key.",
26034-
"type": "string"
26033+
"id": {
26034+
"description": "The public unique ID of the secret.",
26035+
"type": "string",
26036+
"format": "uuid"
26037+
},
26038+
"time_created": {
26039+
"description": "The UTC timestamp at which this secret was created.",
26040+
"type": "string",
26041+
"format": "date-time"
2603526042
}
2603626043
},
2603726044
"required": [
26038-
"secret"
26045+
"id",
26046+
"time_created"
2603926047
]
2604026048
},
26041-
"WebhookSecretId": {
26042-
"description": "The public ID of a secret key assigned to a webhook.",
26049+
"WebhookSecretCreate": {
2604326050
"type": "object",
2604426051
"properties": {
26045-
"id": {
26046-
"type": "string",
26047-
"format": "uuid"
26052+
"secret": {
26053+
"description": "The value of the shared secret key.",
26054+
"type": "string"
2604826055
}
2604926056
},
2605026057
"required": [
26051-
"id"
26058+
"secret"
2605226059
]
2605326060
},
2605426061
"WebhookSecrets": {
@@ -26058,7 +26065,7 @@
2605826065
"secrets": {
2605926066
"type": "array",
2606026067
"items": {
26061-
"$ref": "#/components/schemas/WebhookSecretId"
26068+
"$ref": "#/components/schemas/WebhookSecret"
2606226069
}
2606326070
}
2606426071
},

0 commit comments

Comments
 (0)