Skip to main content

mz_deploy/secret_resolver/
aws_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
10//! AWS Secrets Manager secret provider.
11//!
12//! Resolves secret values by reading from AWS Secrets Manager. Returns the
13//! raw secret string, or — when a second argument is given — extracts that
14//! top-level field from a JSON-shaped secret (e.g. the `{"username":"…",
15//! "password":"…"}` blobs that RDS-style secrets use).
16//!
17//! Requires `aws_profile` to be set in `project.toml`.
18
19use super::json_field::extract_json_field;
20use super::{SecretProvider, SecretResolveError};
21use async_trait::async_trait;
22use aws_sdk_secretsmanager::Client;
23use std::ops::RangeInclusive;
24use tokio::sync::OnceCell;
25
26/// Function name shared by both the real and unconfigured providers.
27const PROVIDER_NAME: &str = "aws_secret";
28
29/// Resolves secrets from AWS Secrets Manager.
30///
31/// Usage in SQL:
32///
33/// - `CREATE SECRET x AS aws_secret('my-secret-name')` — returns the raw
34///   secret string.
35/// - `CREATE SECRET x AS aws_secret('my-secret-name', 'password')` — parses
36///   the secret as JSON and returns the value of the top-level `password`
37///   field.
38///
39/// The AWS SDK config (including credential resolution) is loaded lazily
40/// on the first `resolve()` call, so projects that set `aws_profile` but
41/// never use `aws_secret()` pay no startup cost.
42pub(super) struct AwsSecretProvider {
43    profile: String,
44    client: OnceCell<Client>,
45}
46
47impl AwsSecretProvider {
48    pub(super) fn new(profile: &str) -> Self {
49        Self {
50            profile: profile.to_string(),
51            client: OnceCell::new(),
52        }
53    }
54
55    async fn client(&self) -> &Client {
56        self.client
57            .get_or_init(|| async {
58                let config = mz_aws_util::defaults()
59                    .profile_name(&self.profile)
60                    .load()
61                    .await;
62                Client::new(&config)
63            })
64            .await
65    }
66}
67
68#[async_trait]
69impl SecretProvider for AwsSecretProvider {
70    fn name(&self) -> &str {
71        PROVIDER_NAME
72    }
73
74    fn accepted_args(&self) -> RangeInclusive<usize> {
75        1..=2
76    }
77
78    async fn resolve(&self, args: &[String]) -> Result<String, SecretResolveError> {
79        let secret_name = &args[0];
80        let client = self.client().await;
81        let result = client
82            .get_secret_value()
83            .secret_id(secret_name)
84            .send()
85            .await
86            .map_err(|e| SecretResolveError::ResolutionFailed {
87                name: self.name().to_string(),
88                reason: format!("failed to fetch secret '{}': {}", secret_name, e),
89            })?;
90
91        let secret_string =
92            result
93                .secret_string()
94                .ok_or_else(|| SecretResolveError::ResolutionFailed {
95                    name: self.name().to_string(),
96                    reason: format!(
97                        "secret '{}' is a binary secret; only text secrets are supported",
98                        secret_name
99                    ),
100                })?;
101
102        match args.get(1) {
103            None => Ok(secret_string.to_string()),
104            Some(json_key) => {
105                extract_json_field(secret_string, json_key, secret_name).map_err(|reason| {
106                    SecretResolveError::ResolutionFailed {
107                        name: self.name().to_string(),
108                        reason,
109                    }
110                })
111            }
112        }
113    }
114}
115
116/// Placeholder provider registered when `aws_profile` is not set in `project.toml`.
117///
118/// Always returns an error directing the user to configure `aws_profile`.
119pub(super) struct UnconfiguredAwsProvider;
120
121#[async_trait]
122impl SecretProvider for UnconfiguredAwsProvider {
123    fn name(&self) -> &str {
124        PROVIDER_NAME
125    }
126
127    fn accepted_args(&self) -> RangeInclusive<usize> {
128        1..=2
129    }
130
131    async fn resolve(&self, _args: &[String]) -> Result<String, SecretResolveError> {
132        Err(SecretResolveError::ResolutionFailed {
133            name: self.name().to_string(),
134            reason: "AWS Secrets Manager is not configured. Set 'aws_profile' under [<profile>.security] in project.toml to enable aws_secret().".to_string(),
135        })
136    }
137}