use crate::{SecretResolver, SecuritySecretRef};
use appcore_dnt::{DntContext, DntKeyError, DntKeyProvider, KeyId, SecretKey};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DntSecretRefPolicy {
prefix: String,
}
impl DntSecretRefPolicy {
pub fn new(prefix: impl Into<String>) -> Self {
Self {
prefix: prefix.into(),
}
}
pub fn provider_default() -> Self {
Self::new("provider:dnt-key/")
}
pub fn reference_for(&self, key_id: &KeyId, context: &DntContext) -> SecuritySecretRef {
let tenant = context
.tenant_id
.as_ref()
.map(|value| value.as_str())
.unwrap_or("_");
SecuritySecretRef(format!(
"{}{}/{}/{}",
self.prefix,
context.application_id.as_str(),
tenant,
key_id.as_str()
))
}
}
impl Default for DntSecretRefPolicy {
fn default() -> Self {
Self::provider_default()
}
}
pub struct DntSecretKeyProvider<R> {
resolver: R,
policy: DntSecretRefPolicy,
}
impl<R> fmt::Debug for DntSecretKeyProvider<R> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("DntSecretKeyProvider")
.field("policy", &self.policy)
.finish_non_exhaustive()
}
}
impl<R> DntSecretKeyProvider<R>
where
R: SecretResolver,
{
pub fn new(resolver: R) -> Self {
Self {
resolver,
policy: DntSecretRefPolicy::default(),
}
}
pub fn with_policy(resolver: R, policy: DntSecretRefPolicy) -> Self {
Self { resolver, policy }
}
pub fn policy(&self) -> &DntSecretRefPolicy {
&self.policy
}
}
impl<R> DntKeyProvider for DntSecretKeyProvider<R>
where
R: SecretResolver,
{
fn resolve_key(&self, key_id: &KeyId, context: &DntContext) -> Result<SecretKey, DntKeyError> {
let reference = self.policy.reference_for(key_id, context);
let secret = self
.resolver
.resolve(&reference)
.map_err(|_| DntKeyError::Unavailable)?;
SecretKey::from_slice(secret.as_bytes())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{SecretBytes, StaticSecretResolver};
use appcore_contracts::ApplicationId;
use appcore_dnt::{
open, seal, BytesCodec, ContentType, DntOpenOptions, DntSealOptions, DNT_CONTENT_SECRET,
};
use appcore_types::TenantId;
use std::collections::HashMap;
#[test]
fn secret_resolver_key_provider_opens_dnt_secret() {
let key_id = KeyId::new("local-root").unwrap();
let mut secrets = HashMap::new();
secrets.insert(
format!("provider:dnt-key/app-a/tenant-a/{}", key_id.as_str()),
SecretBytes::new(vec![5; 32]),
);
let resolver = StaticSecretResolver::new(secrets);
let provider = DntSecretKeyProvider::new(resolver);
let codec = BytesCodec;
let seal_options = DntSealOptions {
application_id: ApplicationId::new("app-a").unwrap(),
tenant_id: Some(TenantId::new("tenant-a").unwrap()),
content_type: ContentType::new(DNT_CONTENT_SECRET).unwrap(),
schema_version: 1,
key_id,
created_at_ms: 1,
public_metadata: Vec::new(),
encrypted_metadata: Vec::new(),
flags: 0,
max_payload_bytes: Some(1024),
};
let open_options = DntOpenOptions {
application_id: ApplicationId::new("app-a").unwrap(),
tenant_id: Some(TenantId::new("tenant-a").unwrap()),
content_type: ContentType::new(DNT_CONTENT_SECRET).unwrap(),
max_payload_bytes: Some(1024),
};
let sealed = seal(b"secret", &provider, &codec, seal_options).unwrap();
let opened = open(&sealed, &provider, &codec, &open_options).unwrap();
assert_eq!(opened.payload, b"secret");
}
#[test]
fn missing_dnt_key_fails_closed() {
let resolver = StaticSecretResolver::new(HashMap::new());
let provider = DntSecretKeyProvider::new(resolver);
let context = DntContext {
application_id: ApplicationId::new("app-a").unwrap(),
tenant_id: None,
content_type: ContentType::new(DNT_CONTENT_SECRET).unwrap(),
codec_id: appcore_dnt::CodecId::new("bytes").unwrap(),
schema_version: 1,
};
assert_eq!(
provider.resolve_key(&KeyId::new("missing").unwrap(), &context),
Err(DntKeyError::Unavailable)
);
}
}