lenso_secrets_env_plugin/
lib.rs1use std::{collections::BTreeMap, error::Error, fmt, rc::Rc};
4
5use lenso_capability_secrets::{
6 ResolveError, ResolveRequest, ResolveResponse, Secrets, SecretsEndpoint,
7 SecretsInvocationError, SecretsProvider,
8};
9use lenso_kernel::{
10 InvocationContext, NativeRequestEndpoint, NativeRequestFuture, PluginFuture, PluginLifecycle,
11 PrepareContext, RuntimeFailure,
12};
13use lenso_native_adapter::{NativePluginFactory, NativePluginFactoryContext, NativePluginInstance};
14
15#[inline(never)]
17pub fn link() -> &'static str {
18 PLUGIN_DESCRIPTOR_JSON
19}
20
21pub const MAX_REFERENCE_LENGTH: usize = 256;
23
24#[derive(Clone, Debug, Eq, PartialEq)]
26pub enum EnvSecretsConfigError {
27 Empty,
29 InvalidReference,
31 InvalidEnvironmentVariable,
33 DuplicateReference,
35}
36
37impl fmt::Display for EnvSecretsConfigError {
38 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39 match self {
40 Self::Empty => formatter.write_str("at least one secret reference is required"),
41 Self::InvalidReference => formatter.write_str("invalid logical secret reference"),
42 Self::InvalidEnvironmentVariable => {
43 formatter.write_str("invalid environment-variable name")
44 }
45 Self::DuplicateReference => {
46 formatter.write_str("logical secret reference is already configured")
47 }
48 }
49 }
50}
51
52impl Error for EnvSecretsConfigError {}
53
54#[derive(Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
56#[serde(deny_unknown_fields)]
57pub struct EnvSecretsConfig {
58 #[serde(rename = "references", deserialize_with = "deserialize_unique_sources")]
59 sources: BTreeMap<String, String>,
60}
61
62impl EnvSecretsConfig {
63 #[must_use]
65 pub const fn new() -> Self {
66 Self {
67 sources: BTreeMap::new(),
68 }
69 }
70
71 pub fn insert(
73 &mut self,
74 reference: impl Into<String>,
75 environment_variable: impl Into<String>,
76 ) -> Result<(), EnvSecretsConfigError> {
77 let reference = reference.into();
78 let environment_variable = environment_variable.into();
79 if !valid_reference(&reference) {
80 return Err(EnvSecretsConfigError::InvalidReference);
81 }
82 if !valid_environment_variable(&environment_variable) {
83 return Err(EnvSecretsConfigError::InvalidEnvironmentVariable);
84 }
85 if self.sources.contains_key(&reference) {
86 return Err(EnvSecretsConfigError::DuplicateReference);
87 }
88 self.sources.insert(reference, environment_variable);
89 Ok(())
90 }
91
92 pub fn with_reference(
94 mut self,
95 reference: impl Into<String>,
96 environment_variable: impl Into<String>,
97 ) -> Result<Self, EnvSecretsConfigError> {
98 self.insert(reference, environment_variable)?;
99 Ok(self)
100 }
101
102 #[must_use]
104 pub fn is_empty(&self) -> bool {
105 self.sources.is_empty()
106 }
107
108 #[must_use]
110 pub fn len(&self) -> usize {
111 self.sources.len()
112 }
113
114 fn source_for(&self, reference: &str) -> Option<&str> {
115 self.sources.get(reference).map(String::as_str)
116 }
117
118 fn validate(&self) -> Result<(), EnvSecretsConfigError> {
119 if self.sources.is_empty() {
120 return Err(EnvSecretsConfigError::Empty);
121 }
122 for (reference, environment_variable) in &self.sources {
123 if !valid_reference(reference) {
124 return Err(EnvSecretsConfigError::InvalidReference);
125 }
126 if !valid_environment_variable(environment_variable) {
127 return Err(EnvSecretsConfigError::InvalidEnvironmentVariable);
128 }
129 }
130 Ok(())
131 }
132}
133
134impl fmt::Debug for EnvSecretsConfig {
135 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136 formatter
137 .debug_struct("EnvSecretsConfig")
138 .field("references", &self.sources.keys().collect::<Vec<_>>())
139 .finish()
140 }
141}
142
143#[derive(Clone, Debug)]
145pub struct EnvSecretsFactory {
146 source: Rc<dyn SecretSource>,
147}
148
149impl EnvSecretsFactory {
150 #[must_use]
152 pub fn new() -> Self {
153 Self {
154 source: Rc::new(ProcessEnvironment),
155 }
156 }
157
158 #[cfg(test)]
159 fn with_source(source: Rc<dyn SecretSource>) -> Self {
160 Self { source }
161 }
162}
163
164impl Default for EnvSecretsFactory {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170impl NativePluginFactory for EnvSecretsFactory {
171 fn package_id(&self) -> &'static str {
172 PACKAGE_ID
173 }
174
175 fn package_version(&self) -> &'static str {
176 PACKAGE_VERSION
177 }
178
179 fn instantiate(
180 &self,
181 context: NativePluginFactoryContext<'_>,
182 ) -> Result<NativePluginInstance, RuntimeFailure> {
183 instantiate_with_source(context, self.source.clone())
184 }
185}
186
187#[lenso_native_adapter::plugin(
189 descriptor = r#"{"provided_capabilities":[{"capability_id":"lenso.secrets@1","descriptor_version":"1.0.0","operations":["resolve"],"operation_kinds":{},"default_admission":{"queue_capacity":2,"max_concurrency":1},"operation_admissions":{},"event_admission":null,"cross_lane_transfer":false}],"required_capabilities":[]}"#,
190 configuration_schema = "config.schema.json"
191)]
192fn instantiate(
193 context: NativePluginFactoryContext<'_>,
194) -> Result<NativePluginInstance, RuntimeFailure> {
195 instantiate_with_source(context, Rc::new(ProcessEnvironment))
196}
197
198fn instantiate_with_source(
199 context: NativePluginFactoryContext<'_>,
200 source: Rc<dyn SecretSource>,
201) -> Result<NativePluginInstance, RuntimeFailure> {
202 if context.entrypoint() != "default" {
203 return Err(RuntimeFailure::InvalidResolvedPlan {
204 detail: "unsupported Env Secrets Plugin entrypoint".to_owned(),
205 });
206 }
207 let config =
208 serde_json::from_str::<EnvSecretsConfig>(context.configuration()).map_err(|error| {
209 RuntimeFailure::InvalidResolvedPlan {
210 detail: format!("Env Secrets Plugin configuration is invalid: {error}"),
211 }
212 })?;
213 config
214 .validate()
215 .map_err(|error| RuntimeFailure::InvalidResolvedPlan {
216 detail: format!("Env Secrets Plugin configuration is invalid: {error}"),
217 })?;
218 let provider = EnvSecretsProvider::new(config, source);
219 let endpoint = Rc::new(SecretsEndpoint::new(provider.clone())) as Rc<dyn NativeRequestEndpoint>;
220 Ok(NativePluginInstance::with_lifecycle(
221 vec![endpoint],
222 EnvSecretsLifecycle { provider },
223 ))
224}
225
226#[derive(Clone)]
227struct EnvSecretsProvider {
228 config: Rc<EnvSecretsConfig>,
229 source: Rc<dyn SecretSource>,
230}
231
232impl EnvSecretsProvider {
233 fn new(config: EnvSecretsConfig, source: Rc<dyn SecretSource>) -> Self {
234 Self {
235 config: Rc::new(config),
236 source,
237 }
238 }
239
240 fn verify_sources(&self) -> Result<(), RuntimeFailure> {
241 for reference in self.config.sources.keys() {
242 self.read(reference)?;
243 }
244 Ok(())
245 }
246
247 fn read(&self, reference: &str) -> Result<String, RuntimeFailure> {
248 let source = self
249 .config
250 .source_for(reference)
251 .ok_or_else(|| RuntimeFailure::Internal {
252 detail: "Env Secrets attempted to read an unbound reference".to_owned(),
253 })?;
254 self.source
255 .read(source)
256 .map_err(|SourceUnavailable| RuntimeFailure::PluginFailure {
257 detail: format!("configured secret reference `{reference}` is unavailable"),
258 })
259 }
260}
261
262impl fmt::Debug for EnvSecretsProvider {
263 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
264 formatter
265 .debug_struct("EnvSecretsProvider")
266 .field("config", &self.config)
267 .finish_non_exhaustive()
268 }
269}
270
271impl SecretsProvider for EnvSecretsProvider {
272 fn resolve(
273 &self,
274 _context: InvocationContext,
275 request: ResolveRequest,
276 ) -> NativeRequestFuture<Secrets> {
277 let result = if !valid_reference(&request.reference) {
278 Err(SecretsInvocationError::Domain(
279 ResolveError::InvalidReference,
280 ))
281 } else if self.config.source_for(&request.reference).is_none() {
282 Err(SecretsInvocationError::Domain(
283 ResolveError::UnknownReference,
284 ))
285 } else {
286 self.read(&request.reference)
287 .map(|value| ResolveResponse { value })
288 .map_err(SecretsInvocationError::Runtime)
289 };
290 let result = match result {
291 Ok(response) => Ok(Ok(response)),
292 Err(SecretsInvocationError::Domain(error)) => Ok(Err(error)),
293 Err(SecretsInvocationError::Runtime(error)) => Err(error),
294 };
295 Box::pin(futures::future::ready(result))
296 }
297}
298
299#[derive(Debug)]
300struct EnvSecretsLifecycle {
301 provider: EnvSecretsProvider,
302}
303
304impl PluginLifecycle for EnvSecretsLifecycle {
305 fn prepare(&self, _context: PrepareContext) -> PluginFuture {
306 Box::pin(futures::future::ready(self.provider.verify_sources()))
307 }
308}
309
310trait SecretSource: fmt::Debug + 'static {
311 fn read(&self, environment_variable: &str) -> Result<String, SourceUnavailable>;
312}
313
314#[derive(Clone, Copy, Debug, Eq, PartialEq)]
315struct SourceUnavailable;
316
317#[derive(Debug)]
318struct ProcessEnvironment;
319
320impl SecretSource for ProcessEnvironment {
321 fn read(&self, environment_variable: &str) -> Result<String, SourceUnavailable> {
322 std::env::var(environment_variable).map_err(|_| SourceUnavailable)
323 }
324}
325
326fn deserialize_unique_sources<'de, D>(deserializer: D) -> Result<BTreeMap<String, String>, D::Error>
327where
328 D: serde::Deserializer<'de>,
329{
330 struct UniqueSources;
331
332 impl<'de> serde::de::Visitor<'de> for UniqueSources {
333 type Value = BTreeMap<String, String>;
334
335 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
336 formatter.write_str("a logical-reference to environment-variable map")
337 }
338
339 fn visit_map<A>(self, mut access: A) -> Result<Self::Value, A::Error>
340 where
341 A: serde::de::MapAccess<'de>,
342 {
343 let mut sources = BTreeMap::new();
344 while let Some((reference, source)) = access.next_entry::<String, String>()? {
345 if sources.insert(reference.clone(), source).is_some() {
346 return Err(serde::de::Error::custom(format!(
347 "duplicate logical secret reference `{reference}`"
348 )));
349 }
350 }
351 Ok(sources)
352 }
353 }
354
355 deserializer.deserialize_map(UniqueSources)
356}
357
358fn valid_reference(reference: &str) -> bool {
359 !reference.is_empty()
360 && reference.len() <= MAX_REFERENCE_LENGTH
361 && !reference.starts_with('/')
362 && !reference.ends_with('/')
363 && !reference.contains("//")
364 && reference
365 .split('/')
366 .all(|segment| segment != "." && segment != "..")
367 && reference
368 .bytes()
369 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-' | b'/'))
370}
371
372fn valid_environment_variable(name: &str) -> bool {
373 let mut bytes = name.bytes();
374 bytes
375 .next()
376 .is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
377 && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
378}
379
380#[cfg(test)]
381mod tests;