Skip to main content

greentic_deploy_spec/
credentials.rs

1//! `greentic.credentials.v1` (`ยง5.5`).
2//!
3//! Admin credentials are never intentionally persisted; only
4//! `admin_credential_consumed_at` records the fact that they were used.
5
6use crate::capability_slot::PackDescriptor;
7use crate::error::SpecError;
8use crate::refs::SecretRef;
9use crate::version::SchemaVersion;
10use chrono::{DateTime, Utc};
11use greentic_types::EnvId;
12use serde::{Deserialize, Serialize};
13use std::path::PathBuf;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum CredentialsMode {
18    Requirements,
19    Bootstrap,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "lowercase")]
24pub enum CredentialsValidationResult {
25    Pass,
26    Fail,
27}
28
29#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
30pub struct CredentialsValidation {
31    pub last_run_at: DateTime<Utc>,
32    pub result: CredentialsValidationResult,
33    #[serde(default)]
34    pub missing_capabilities: Vec<String>,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
38pub struct CredentialsBootstrap {
39    pub admin_credential_consumed_at: DateTime<Utc>,
40    /// Env-relative path to the generated rules pack.
41    pub rules_pack_ref: PathBuf,
42    pub generated_credentials_ref: SecretRef,
43}
44
45#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
46pub struct CredentialsExpiry {
47    pub expires_at: DateTime<Utc>,
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub rotate_at: Option<DateTime<Utc>>,
50}
51
52#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
53pub struct Credentials {
54    pub schema: SchemaVersion,
55    pub env_id: EnvId,
56    pub deployer_kind: PackDescriptor,
57    pub mode: CredentialsMode,
58    pub provided_credentials_ref: SecretRef,
59    pub validation: CredentialsValidation,
60    /// Populated only when `mode = bootstrap`.
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub bootstrap: Option<CredentialsBootstrap>,
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub expiry: Option<CredentialsExpiry>,
65}
66
67impl Credentials {
68    pub fn schema_str() -> &'static str {
69        SchemaVersion::CREDENTIALS_V1
70    }
71
72    /// Validate the credentials document against its own invariants:
73    /// schema discriminator equals `greentic.credentials.v1`, and every
74    /// embedded [`SecretRef`] is scoped to `self.env_id`. Without the
75    /// scope check a Credentials document for env A could carry pointers
76    /// into env B's secrets backend, bypassing tenant isolation.
77    pub fn validate(&self) -> Result<(), SpecError> {
78        if self.schema.as_str() != SchemaVersion::CREDENTIALS_V1 {
79            return Err(SpecError::SchemaMismatch {
80                expected: SchemaVersion::CREDENTIALS_V1,
81                actual: self.schema.as_str().to_string(),
82            });
83        }
84        check_secret_ref_env(
85            "credentials.provided_credentials_ref",
86            &self.provided_credentials_ref,
87            &self.env_id,
88        )?;
89        if let Some(bootstrap) = &self.bootstrap {
90            check_secret_ref_env(
91                "credentials.bootstrap.generated_credentials_ref",
92                &bootstrap.generated_credentials_ref,
93                &self.env_id,
94            )?;
95        }
96        Ok(())
97    }
98}
99
100fn check_secret_ref_env(
101    context: &'static str,
102    secret_ref: &SecretRef,
103    expected_env: &EnvId,
104) -> Result<(), SpecError> {
105    let actual = secret_ref.env_segment();
106    if actual != expected_env.as_str() {
107        return Err(SpecError::CrossEnvRef {
108            context,
109            uri: secret_ref.as_str().to_string(),
110            expected_env: expected_env.clone(),
111            actual_env: actual.to_string(),
112        });
113    }
114    Ok(())
115}