Skip to main content

mz_adapter/coord/sequencer/inner/
secret.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::collections::BTreeSet;
11use std::sync::Arc;
12
13use mz_catalog::memory::error::ErrorKind;
14use mz_catalog::memory::objects::{CatalogItem, Secret};
15use mz_expr::{Eval, MirScalarExpr};
16use mz_ore::collections::CollectionExt;
17use mz_ore::instrument;
18use mz_repr::{CatalogItemId, Datum, RowArena};
19use mz_sql::ast::display::AstDisplay;
20use mz_sql::ast::{ConnectionOption, ConnectionOptionName, Statement, Value, WithOptionValue};
21use mz_sql::catalog::{CatalogError, ObjectType};
22use mz_sql::plan::{self, CreateSecretPlan};
23use mz_sql::session::metadata::SessionMetadata;
24use mz_ssh_util::keys::SshKeyPairSet;
25use tracing::{Instrument, Span, warn};
26
27use crate::coord::sequencer::inner::return_if_err;
28use crate::coord::{
29    AlterSecret, Coordinator, CreateSecretEnsure, CreateSecretFinish, Message, PlanValidity,
30    RotateKeysSecretEnsure, RotateKeysSecretFinish, SecretStage, StageResult, Staged,
31};
32use crate::optimize::dataflows::{EvalTime, ExprPrep, ExprPrepOneShot};
33use crate::session::Session;
34use crate::{AdapterError, AdapterNotice, ExecuteContext, ExecuteResponse, catalog};
35
36impl Staged for SecretStage {
37    type Ctx = ExecuteContext;
38
39    fn validity(&mut self) -> &mut crate::coord::PlanValidity {
40        match self {
41            SecretStage::CreateFinish(stage) => &mut stage.validity,
42            SecretStage::RotateKeysFinish(stage) => &mut stage.validity,
43            SecretStage::RotateKeysEnsure(stage) => &mut stage.validity,
44            SecretStage::CreateEnsure(stage) => &mut stage.validity,
45            SecretStage::Alter(stage) => &mut stage.validity,
46        }
47    }
48
49    async fn stage(
50        self,
51        coord: &mut Coordinator,
52        ctx: &mut ExecuteContext,
53    ) -> Result<crate::coord::StageResult<Box<Self>>, AdapterError> {
54        match self {
55            SecretStage::CreateEnsure(stage) => {
56                coord.create_secret_ensure(ctx.session(), stage).await
57            }
58            SecretStage::CreateFinish(stage) => {
59                coord.create_secret_finish(ctx.session(), stage).await
60            }
61            SecretStage::RotateKeysEnsure(stage) => coord.rotate_keys_ensure(stage),
62            SecretStage::RotateKeysFinish(stage) => {
63                coord.rotate_keys_finish(ctx.session(), stage).await
64            }
65            SecretStage::Alter(stage) => coord.alter_secret(ctx.session(), stage.plan),
66        }
67    }
68
69    fn message(self, ctx: ExecuteContext, span: tracing::Span) -> Message {
70        Message::SecretStageReady {
71            ctx,
72            span,
73            stage: self,
74        }
75    }
76
77    fn cancel_enabled(&self) -> bool {
78        // Because secrets operations call out to external services and transact the catalog
79        // separately, disable cancellation.
80        false
81    }
82}
83
84impl Coordinator {
85    #[instrument]
86    pub(crate) async fn sequence_create_secret(
87        &mut self,
88        ctx: ExecuteContext,
89        plan: plan::CreateSecretPlan,
90    ) {
91        let stage = return_if_err!(self.create_secret_validate(ctx.session(), plan).await, ctx);
92        self.sequence_staged(ctx, Span::current(), stage).await;
93    }
94
95    #[instrument]
96    async fn create_secret_validate(
97        &self,
98        session: &Session,
99        plan: plan::CreateSecretPlan,
100    ) -> Result<SecretStage, AdapterError> {
101        // No dependencies.
102        let validity = PlanValidity::new(
103            self.catalog(),
104            BTreeSet::new(),
105            None,
106            None,
107            session.role_metadata().clone(),
108        );
109        Ok(SecretStage::CreateEnsure(CreateSecretEnsure {
110            validity,
111            plan,
112        }))
113    }
114
115    #[instrument]
116    async fn create_secret_ensure(
117        &mut self,
118        session: &Session,
119        CreateSecretEnsure { validity, mut plan }: CreateSecretEnsure,
120    ) -> Result<StageResult<Box<SecretStage>>, AdapterError> {
121        let (item_id, global_id) = self.allocate_user_id().await?;
122
123        let secrets_controller = Arc::clone(&self.secrets_controller);
124        let payload = self.extract_secret(session, &mut plan.secret.secret_as)?;
125        let span = Span::current();
126        Ok(StageResult::Handle(mz_ore::task::spawn(
127            || "create secret ensure",
128            async move {
129                secrets_controller.ensure(item_id, &payload).await?;
130                let stage = SecretStage::CreateFinish(CreateSecretFinish {
131                    validity,
132                    item_id,
133                    global_id,
134                    plan,
135                });
136                Ok(Box::new(stage))
137            }
138            .instrument(span),
139        )))
140    }
141
142    fn extract_secret(
143        &self,
144        session: &Session,
145        secret_as: &mut MirScalarExpr,
146    ) -> Result<Vec<u8>, AdapterError> {
147        let temp_storage = RowArena::new();
148        let style = ExprPrepOneShot {
149            logical_time: EvalTime::NotAvailable,
150            session,
151            catalog_state: self.catalog().state(),
152        };
153        // Preparing and evaluating the secret expression can fail with an error
154        // that embeds the value being evaluated (e.g. a `bytea` parse error quotes
155        // its input). That value is the secret itself, so we discard the underlying
156        // error and surface a vague message, to avoid including the secret in the
157        // statement log (or some other log file somewhere). We wrap both calls,
158        // rather than just the ones that leak today, so a future error added to
159        // either is covered by default.
160        let secret_eval_err =
161            || AdapterError::Unstructured(anyhow::anyhow!("could not evaluate secret value"));
162        style
163            .prep_scalar_expr(secret_as)
164            .map_err(|_| secret_eval_err())?;
165        let evaled = secret_as
166            .eval(&[], &temp_storage)
167            .map_err(|_| secret_eval_err())?;
168
169        if evaled == Datum::Null {
170            coord_bail!("secret value can not be null");
171        }
172
173        let payload = evaled.unwrap_bytes();
174
175        // Limit the size of a secret to 512 KiB
176        // This is the largest size of a single secret in Consul/Kubernetes
177        // We are enforcing this limit across all types of Secrets Controllers
178        // Most secrets are expected to be roughly 75B
179        if payload.len() > 1024 * 512 {
180            coord_bail!("secrets can not be bigger than 512KiB")
181        }
182
183        // Enforce that all secrets are valid UTF-8 for now. We expect to lift
184        // this restriction in the future, when we discover a connection type
185        // that requires binary secrets, but for now it is convenient to ensure
186        // here that `SecretsReader::read_string` can never fail due to invalid
187        // UTF-8.
188        //
189        // If you want to remove this line, verify that no caller of
190        // `SecretsReader::read_string` will panic if the secret contains
191        // invalid UTF-8.
192        if std::str::from_utf8(payload).is_err() {
193            // Similarly to above, intentionally produce a vague error message
194            // (rather than  including the invalid bytes, for example), to avoid
195            // including secret material in the error message.
196            coord_bail!("secret value must be valid UTF-8");
197        }
198
199        Ok(Vec::from(payload))
200    }
201
202    #[instrument]
203    async fn create_secret_finish(
204        &mut self,
205        session: &Session,
206        CreateSecretFinish {
207            item_id,
208            global_id,
209            plan,
210            validity: _,
211        }: CreateSecretFinish,
212    ) -> Result<StageResult<Box<SecretStage>>, AdapterError> {
213        let CreateSecretPlan {
214            name,
215            secret,
216            if_not_exists,
217        } = plan;
218        let secret = Secret {
219            create_sql: secret.create_sql,
220            global_id,
221        };
222
223        let ops = vec![catalog::Op::CreateItem {
224            id: item_id,
225            name: name.clone(),
226            item: CatalogItem::Secret(secret),
227            owner_id: *session.current_role_id(),
228        }];
229
230        let res = match self.catalog_transact(Some(session), ops).await {
231            Ok(()) => Ok(ExecuteResponse::CreatedSecret),
232            Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
233                kind: ErrorKind::Sql(CatalogError::ItemAlreadyExists(_, _)),
234            })) if if_not_exists => {
235                // Clean up the secret we already persisted, since the catalog
236                // item was not created.
237                if let Err(e) = self.secrets_controller.delete(item_id).await {
238                    warn!(
239                        "Dropping newly created secrets has encountered an error: {}",
240                        e
241                    );
242                } else {
243                    self.caching_secrets_reader.invalidate(item_id);
244                }
245                session.add_notice(AdapterNotice::ObjectAlreadyExists {
246                    name: name.item,
247                    ty: "secret",
248                });
249                Ok(ExecuteResponse::CreatedSecret)
250            }
251            Err(err) => {
252                if let Err(e) = self.secrets_controller.delete(item_id).await {
253                    warn!(
254                        "Dropping newly created secrets has encountered an error: {}",
255                        e
256                    );
257                } else {
258                    self.caching_secrets_reader.invalidate(item_id);
259                }
260                Err(err)
261            }
262        };
263        res.map(StageResult::Response)
264    }
265
266    #[instrument]
267    pub(crate) async fn sequence_alter_secret(
268        &mut self,
269        ctx: ExecuteContext,
270        plan: plan::AlterSecretPlan,
271    ) {
272        // Notably this does not include `plan.id` in `dependency_ids` because `alter_secret()` just
273        // calls `ensure()` and returns a success result to the client. If there's a concurrent
274        // delete of the secret, the persisted secret is in an unknown state (but will be cleaned up
275        // if needed at next envd boot), but we will still return success.
276        let validity = PlanValidity::new(
277            self.catalog(),
278            BTreeSet::new(),
279            None,
280            None,
281            ctx.session().role_metadata().clone(),
282        );
283        let stage = SecretStage::Alter(AlterSecret { validity, plan });
284        self.sequence_staged(ctx, Span::current(), stage).await;
285    }
286
287    #[instrument]
288    fn alter_secret(
289        &self,
290        session: &Session,
291        plan: plan::AlterSecretPlan,
292    ) -> Result<StageResult<Box<SecretStage>>, AdapterError> {
293        let plan::AlterSecretPlan { id, mut secret_as } = plan;
294        let caching_secrets_reader = self.caching_secrets_reader.clone();
295        let secrets_controller = Arc::clone(&self.secrets_controller);
296        let payload = self.extract_secret(session, &mut secret_as)?;
297        let contents = std::str::from_utf8(&payload).expect("validated as UTF-8 by extract_secret");
298        self.check_secret_content_guards_of_dependents(id, contents)?;
299        let span = Span::current();
300        Ok(StageResult::HandleRetire(mz_ore::task::spawn(
301            || "alter secret ensure",
302            async move {
303                secrets_controller.ensure(id, &payload).await?;
304                caching_secrets_reader.invalidate(id);
305                Ok(ExecuteResponse::AlteredObject(ObjectType::Secret))
306            }
307            .instrument(span),
308        )))
309    }
310
311    #[instrument]
312    pub(crate) async fn sequence_rotate_keys(&mut self, ctx: ExecuteContext, id: CatalogItemId) {
313        // If the secret is deleted from the catalog during
314        // `rotate_keys_ensure()`, this will prevent `rotate_keys_finish()` from
315        // issuing the catalog update for the change. The state of the persisted
316        // secret is unknown, and if the rotate ensure'd after the delete (i.e.,
317        // the secret is persisted to the secret store but not the catalog), the
318        // secret will be cleaned up during next envd boot.
319        //
320        // ROTATE KEYS is a read-modify-write of the connection's `create_sql`
321        // (see `rotate_keys_ensure` below: it re-reads the entry, rewrites the
322        // PUBLIC KEY options, and writes the result back via `Op::UpdateItem`).
323        // Arm the `create_sql`-hash check so a concurrent `ALTER CONNECTION SET`
324        // committing inside the off-thread window fails ROTATE with
325        // `ConcurrentDependencyMutation` instead of being silently clobbered by
326        // the blind write at the end.
327        let validity = PlanValidity::new(
328            self.catalog(),
329            BTreeSet::from_iter(std::iter::once(id)),
330            None,
331            None,
332            ctx.session().role_metadata().clone(),
333        )
334        .with_dependency_hash_check(self.catalog());
335        let stage = SecretStage::RotateKeysEnsure(RotateKeysSecretEnsure { validity, id });
336        self.sequence_staged(ctx, Span::current(), stage).await;
337    }
338
339    #[instrument]
340    fn rotate_keys_ensure(
341        &self,
342        RotateKeysSecretEnsure { validity, id }: RotateKeysSecretEnsure,
343    ) -> Result<StageResult<Box<SecretStage>>, AdapterError> {
344        let caching_secrets_reader = self.caching_secrets_reader.clone();
345        let secrets_controller = Arc::clone(&self.secrets_controller);
346        let entry = self.catalog().get_entry(&id).clone();
347        let span = Span::current();
348        Ok(StageResult::Handle(mz_ore::task::spawn(
349            || "rotate keys ensure",
350            async move {
351                let secret = secrets_controller.reader().read(id).await?;
352                let previous_key_set = SshKeyPairSet::from_bytes(&secret)?;
353                let new_key_set = previous_key_set.rotate()?;
354                secrets_controller
355                    .ensure(id, &new_key_set.to_bytes())
356                    .await?;
357                caching_secrets_reader.invalidate(id);
358
359                let mut to_item = entry.item;
360                match &mut to_item {
361                    CatalogItem::Connection(c) => {
362                        let mut stmt = match mz_sql::parse::parse(&c.create_sql)
363                            .expect("invalid create sql persisted to catalog")
364                            .into_element()
365                            .ast
366                        {
367                            Statement::CreateConnection(stmt) => stmt,
368                            _ => coord_bail!("internal error: persisted SQL for {id} is invalid"),
369                        };
370
371                        stmt.values.retain(|v| {
372                            v.name != ConnectionOptionName::PublicKey1
373                                && v.name != ConnectionOptionName::PublicKey2
374                        });
375                        stmt.values.push(ConnectionOption {
376                            name: ConnectionOptionName::PublicKey1,
377                            value: Some(WithOptionValue::Value(Value::String(
378                                new_key_set.primary().ssh_public_key(),
379                            ))),
380                        });
381                        stmt.values.push(ConnectionOption {
382                            name: ConnectionOptionName::PublicKey2,
383                            value: Some(WithOptionValue::Value(Value::String(
384                                new_key_set.secondary().ssh_public_key(),
385                            ))),
386                        });
387
388                        c.create_sql = stmt.to_ast_string_stable();
389                    }
390                    _ => coord_bail!(
391                        "internal error: rotate keys called on non-connection object {id}"
392                    ),
393                }
394
395                let ops = vec![catalog::Op::UpdateItem {
396                    id,
397                    name: entry.name,
398                    to_item,
399                }];
400
401                // Pause between the secret-store write and the catalog write so a concurrent ALTER
402                // CONNECTION SET can commit. Runs in the off-thread ensure task, not the
403                // coordinator main loop, so the pause won't freeze the coordinator (which would
404                // block that SET).
405                fail::fail_point!("rotate_keys_between_ensure_and_finish");
406
407                let stage = SecretStage::RotateKeysFinish(RotateKeysSecretFinish { validity, ops });
408                Ok(Box::new(stage))
409            }
410            .instrument(span),
411        )))
412    }
413
414    #[instrument]
415    async fn rotate_keys_finish(
416        &mut self,
417        session: &Session,
418        RotateKeysSecretFinish { ops, validity: _ }: RotateKeysSecretFinish,
419    ) -> Result<StageResult<Box<SecretStage>>, AdapterError> {
420        self.catalog_transact(Some(session), ops).await?;
421        Ok(StageResult::Response(ExecuteResponse::AlteredObject(
422            ObjectType::Connection,
423        )))
424    }
425}