Skip to content

Commit 2bc5925

Browse files
committed
secrets plumbing
1 parent 399ede5 commit 2bc5925

File tree

6 files changed

+96
-17
lines changed

6 files changed

+96
-17
lines changed

Diff for: nexus/db-model/src/schema.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -2126,8 +2126,8 @@ table! {
21262126
table! {
21272127
webhook_rx_secret (rx_id, signature_id) {
21282128
rx_id -> Uuid,
2129-
signature_id -> Text,
2130-
secret -> Binary,
2129+
signature_id -> Uuid,
2130+
secret -> Text,
21312131
time_created -> Timestamptz,
21322132
time_deleted -> Nullable<Timestamptz>,
21332133
}

Diff for: nexus/db-model/src/webhook_rx.rs

+70-4
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,60 @@ use crate::Generation;
1212
use crate::WebhookDelivery;
1313
use chrono::{DateTime, Utc};
1414
use db_macros::Resource;
15+
use nexus_types::external_api::views;
1516
use omicron_common::api::external::Error;
16-
use omicron_uuid_kinds::{WebhookReceiverKind, WebhookReceiverUuid};
17+
use omicron_uuid_kinds::{
18+
WebhookReceiverKind, WebhookReceiverUuid, WebhookSecretKind,
19+
WebhookSecretUuid,
20+
};
1721
use serde::{Deserialize, Serialize};
1822
use std::str::FromStr;
1923
use uuid::Uuid;
2024

21-
/// A webhook receiver configuration.
25+
/// The full configuration of a webhook receiver, including the
26+
/// [`WebhookReceiver`] itself and its subscriptions and secrets.
27+
pub struct WebhookReceiverConfig {
28+
pub rx: WebhookReceiver,
29+
pub secrets: Vec<WebhookRxSecret>,
30+
pub events: Vec<WebhookSubscriptionKind>,
31+
}
32+
33+
impl TryFrom<WebhookReceiverConfig> for views::Webhook {
34+
type Error = Error;
35+
fn try_from(
36+
WebhookReceiverConfig { rx, secrets, events }: WebhookReceiverConfig,
37+
) -> Result<views::Webhook, Self::Error> {
38+
let secrets = secrets
39+
.iter()
40+
.map(|WebhookRxSecret { signature_id, .. }| {
41+
views::WebhookSecretId { id: signature_id.to_string() }
42+
})
43+
.collect();
44+
let events = events
45+
.into_iter()
46+
.map(WebhookSubscriptionKind::into_event_class_string)
47+
.collect();
48+
let WebhookReceiver { identity, endpoint, probes_enabled, rcgen: _ } =
49+
rx;
50+
let WebhookReceiverIdentity { id, name, description, .. } = identity;
51+
let endpoint = endpoint.parse().map_err(|e| Error::InternalError {
52+
// This is an internal error, as we should not have ever allowed
53+
// an invalid URL to be inserted into the database...
54+
internal_message: format!("invalid webhook URL {endpoint:?}: {e}",),
55+
})?;
56+
Ok(views::Webhook {
57+
id: id.into(),
58+
name: name.to_string(),
59+
description,
60+
endpoint,
61+
secrets,
62+
events,
63+
disable_probes: !probes_enabled,
64+
})
65+
}
66+
}
67+
68+
/// A row in the `webhook_rx` table.
2269
#[derive(
2370
Clone,
2471
Debug,
@@ -78,12 +125,24 @@ impl DatastoreCollectionConfig<WebhookDelivery> for WebhookReceiver {
78125
#[diesel(table_name = webhook_rx_secret)]
79126
pub struct WebhookRxSecret {
80127
pub rx_id: DbTypedUuid<WebhookReceiverKind>,
81-
pub signature_id: String,
82-
pub secret: Vec<u8>,
128+
pub signature_id: DbTypedUuid<WebhookSecretKind>,
129+
pub secret: String,
83130
pub time_created: DateTime<Utc>,
84131
pub time_deleted: Option<DateTime<Utc>>,
85132
}
86133

134+
impl WebhookRxSecret {
135+
pub fn new(rx_id: WebhookReceiverUuid, secret: String) -> Self {
136+
Self {
137+
rx_id: rx_id.into(),
138+
signature_id: WebhookSecretUuid::new_v4().into(),
139+
secret,
140+
time_created: Utc::now(),
141+
time_deleted: None,
142+
}
143+
}
144+
}
145+
87146
#[derive(
88147
Clone, Debug, Queryable, Selectable, Insertable, Serialize, Deserialize,
89148
)]
@@ -132,6 +191,13 @@ impl WebhookSubscriptionKind {
132191
Ok(Self::Exact(value))
133192
}
134193
}
194+
195+
fn into_event_class_string(self) -> String {
196+
match self {
197+
Self::Exact(class) => class,
198+
Self::Glob(WebhookGlob { glob, .. }) => glob,
199+
}
200+
}
135201
}
136202

137203
#[derive(

Diff for: nexus/db-queries/src/db/datastore/webhook_rx.rs

+20-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::db::error::public_error_from_diesel;
1414
use crate::db::error::ErrorHandler;
1515
use crate::db::model::Generation;
1616
use crate::db::model::WebhookReceiver;
17+
use crate::db::model::WebhookReceiverConfig;
1718
use crate::db::model::WebhookReceiverIdentity;
1819
use crate::db::model::WebhookRxEventGlob;
1920
use crate::db::model::WebhookRxSecret;
@@ -41,7 +42,7 @@ impl DataStore {
4142
opctx: &OpContext,
4243
params: params::WebhookCreate,
4344
event_classes: &[&str],
44-
) -> CreateResult<WebhookReceiver> {
45+
) -> CreateResult<WebhookReceiverConfig> {
4546
// TODO(eliza): someday we gotta allow creating webhooks with more
4647
// restrictive permissions...
4748
opctx.authorize(authz::Action::CreateChild, &authz::FLEET).await?;
@@ -59,9 +60,8 @@ impl DataStore {
5960
.into_iter()
6061
.map(WebhookSubscriptionKind::new)
6162
.collect::<Result<Vec<_>, _>>()?;
62-
6363
let err = OptionalError::new();
64-
let rx = self
64+
let (rx, secrets) = self
6565
.transaction_retry_wrapper("webhook_rx_create")
6666
.transaction(&conn, |conn| {
6767
// make a fresh UUID for each transaction, in case the
@@ -79,6 +79,7 @@ impl DataStore {
7979
rcgen: Generation::new(),
8080
};
8181
let subscriptions = subscriptions.clone();
82+
let secret_keys = secrets.clone();
8283
let err = err.clone();
8384
async move {
8485
let rx = diesel::insert_into(rx_dsl::webhook_rx)
@@ -100,8 +101,21 @@ impl DataStore {
100101
TransactionError::Database(e) => e,
101102
})?;
102103
}
103-
// TODO(eliza): secrets?
104-
Ok(rx)
104+
let mut secrets = Vec::with_capacity(secret_keys.len());
105+
for secret in secret_keys {
106+
let secret = self
107+
.add_secret_on_conn(
108+
WebhookRxSecret::new(id, secret),
109+
&conn,
110+
)
111+
.await
112+
.map_err(|e| match e {
113+
TransactionError::CustomError(e) => err.bail(e),
114+
TransactionError::Database(e) => e,
115+
})?;
116+
secrets.push(secret);
117+
}
118+
Ok((rx, secrets))
105119
}
106120
})
107121
.await
@@ -117,7 +131,7 @@ impl DataStore {
117131
),
118132
)
119133
})?;
120-
Ok(rx)
134+
Ok(WebhookReceiverConfig { rx, secrets, events: subscriptions })
121135
}
122136

123137
// pub async fn webhook_rx_fetch_all(&self, opctx: &OpContext, authz_rx: &authz::WebhookReceiver) -> Fet

Diff for: nexus/types/src/external_api/views.rs

+1-3
Original file line numberDiff line numberDiff line change
@@ -1047,11 +1047,9 @@ pub struct Webhook {
10471047
pub id: WebhookReceiverUuid,
10481048
/// The identifier assigned to this webhook receiver upon creation.
10491049
pub name: String,
1050+
pub description: String,
10501051
/// The URL that webhook notification requests are sent to.
10511052
pub endpoint: Url,
1052-
/// The UUID of the user associated with this webhook receiver for
1053-
/// role-based ccess control.
1054-
pub actor_id: Uuid,
10551053
// A list containing the IDs of the secret keys used to sign payloads sent
10561054
// to this receiver.
10571055
pub secrets: Vec<WebhookSecretId>,

Diff for: schema/crdb/dbinit.sql

+2-2
Original file line numberDiff line numberDiff line change
@@ -4713,9 +4713,9 @@ CREATE TABLE IF NOT EXISTS omicron.public.webhook_rx_secret (
47134713
-- `omicron.public.webhook_rx`)
47144714
rx_id UUID NOT NULL,
47154715
-- ID of this secret.
4716-
signature_id STRING(63) NOT NULL,
4716+
signature_id UUID NOT NULL,
47174717
-- Secret value.
4718-
secret BYTES NOT NULL,
4718+
secret STRING(512) NOT NULL,
47194719
time_created TIMESTAMPTZ NOT NULL,
47204720
time_deleted TIMESTAMPTZ,
47214721

Diff for: uuid-kinds/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,6 @@ impl_typed_uuid_kind! {
7575
WebhookEvent => "webhook_event",
7676
WebhookReceiver => "webhook_receiver",
7777
WebhookDelivery => "webhook_delivery",
78+
WebhookSecret => "webhook_secret",
7879
Zpool => "zpool",
7980
}

0 commit comments

Comments
 (0)