///|
/// Whether the load and the merge treat one-field `env` / `file` / `command` /
/// `keyring` structs as secret references.
pub(all) enum SecretSyntax {
Standard
None
} derive(Eq, Debug)
///|
pub extend SecretSyntax with Eq::{equal, not_equal}
///|
pub extend SecretSyntax with @debug.Debug::{to_repr}
///|
/// The Morphir secret-reference shapes. Recognition is on when the syntax is
/// `Standard`.
pub fn SecretSyntax::standard() -> SecretSyntax {
Standard
}
///|
/// Turn secret-reference recognition off.
pub fn SecretSyntax::none() -> SecretSyntax {
None
}
///|
/// A secret reference leaf: one of the Morphir shapes.
pub(all) enum SecretRef {
Env(String)
File(String)
Command(Array[String])
Keyring(service~ : String, account~ : String)
} derive(Eq, Debug)
///|
pub extend SecretRef with Eq::{equal, not_equal}
///|
pub extend SecretRef with @debug.Debug::{to_repr}
///|
/// An opaque resolved secret. `Show` writes ``.
pub struct Secret {
priv text : String
}
///|
/// The secret text. Callers that display a value must not use this.
pub fn Secret::expose(self : Secret) -> String {
self.text
}
///|
pub impl Show for Secret with output(_self, logger) {
logger.write_string("")
}
///|
pub extend Secret with Show::{output, to_string}
///|
/// A failure while resolving a secret. Messages never hold the secret or the
/// backend output.
pub(all) suberror SecretError {
MissingConfigKey(key~ : KeyPath)
EmptyEnvironment(variable~ : String)
FileRead(path~ : String, reason~ : String)
InvalidSecretValue(reason~ : String)
CommandFailed(reason~ : String)
KeyringLookupFailed(reason~ : String)
} derive(Eq, Debug)
///|
pub extend SecretError with Eq::{equal, not_equal}
///|
pub extend SecretError with @debug.Debug::{to_repr}
///|
/// Text for people that names the failure class and never holds a secret.
pub fn SecretError::message(self : SecretError) -> String {
match self {
MissingConfigKey(key~) => "missing-config-key: configuration key \{key} is missing"
EmptyEnvironment(variable~) =>
"empty-environment: environment variable \{variable} is empty or unset"
FileRead(path~, reason~) => "file-read: cannot read \{path}: \{reason}"
InvalidSecretValue(reason~) => "invalid-secret-value: \{reason}"
CommandFailed(reason~) => "command-failed: \{reason}"
KeyringLookupFailed(reason~) => "keyring-lookup-failed: \{reason}"
}
}
///|
/// Resolves a secret reference. The origin lets a file resolver anchor a
/// relative path to the directory of the declaring file.
pub(open) trait SecretResolver {
resolve(Self, SecretRef, Origin) -> Secret raise SecretError
}
///|
/// A resolver over an explicit variable map. It resolves only `Env` references.
pub struct EnvResolver {
priv vars : Map[String, String]
}
///|
/// A resolver over `vars`. File, command, and keyring references raise
/// `InvalidSecretValue`.
pub fn env_resolver(vars : Map[String, String]) -> EnvResolver {
{ vars: vars.copy() }
}
///|
pub impl SecretResolver for EnvResolver with resolve(self, reference, _origin) {
match reference {
Env(variable) => {
guard self.vars.get(variable) is Some(text) && !text.is_empty() else {
raise EmptyEnvironment(variable~)
}
{ text, }
}
File(_) => raise InvalidSecretValue(reason="env_resolver does not read files")
Command(_) =>
raise InvalidSecretValue(reason="env_resolver does not run commands")
Keyring(..) =>
raise InvalidSecretValue(reason="env_resolver does not look up keyrings")
}
}
///|
pub extend EnvResolver with SecretResolver::{resolve}
///|
/// Whether `value` is a secret-reference shape under `syntax`.
fn is_secret_reference(value : ConfigValue, syntax : SecretSyntax) -> Bool {
match syntax {
None => false
Standard => secret_ref_of(value) is Some(_)
}
}
///|
/// Parse a secret reference from `value`, or `None` when it is not one.
fn secret_ref_of(value : ConfigValue) -> SecretRef? {
guard value.ion.kind() is Struct(fields) && fields.length() == 1 else {
return None
}
let field = fields[0]
guard field.name().text() is Some(name) else { return None }
match name {
"env" =>
match field.value().kind() {
String(text) if !text.is_empty() => Some(Env(text))
_ => None
}
"file" =>
match field.value().kind() {
String(text) if !text.is_empty() => Some(File(text))
_ => None
}
"command" => parse_command_ref(field.value())
"keyring" => parse_keyring_ref(field.value())
_ => None
}
}
///|
fn parse_command_ref(value : @ion.Ion) -> SecretRef? {
guard value.kind() is List(items) && !items.is_empty() else { return None }
let parts = []
for item in items {
guard item.kind() is String(text) else { return None }
parts.push(text)
}
guard !parts[0].is_empty() else { return None }
Some(Command(parts))
}
///|
fn parse_keyring_ref(value : @ion.Ion) -> SecretRef? {
guard value.kind() is Struct(fields) && fields.length() == 2 else {
return None
}
let mut service : String? = None
let mut account : String? = None
for field in fields {
guard field.name().text() is Some(name) else { return None }
guard field.value().kind() is String(text) && !text.is_empty() else {
return None
}
match name {
"service" => service = Some(text)
"account" => account = Some(text)
_ => return None
}
}
match (service, account) {
(Some(service), Some(account)) => Some(Keyring(service~, account~))
_ => None
}
}
///|
/// Raise `InvalidSecret` when `value` is a one-field secret-reference shape
/// with a wrong payload. A nested table whose only field is named `env`,
/// `file`, `command`, or `keyring` and whose value is itself a struct is not a
/// reference: it is an ordinary table, and the walk continues.
fn check_secret_payloads(
value : ConfigValue,
origin : String,
syntax : SecretSyntax,
path~ : Array[String] = [],
) -> Unit raise ConfigError {
guard syntax is Standard else { return }
match value.ion.kind() {
Struct(fields) => {
if fields.length() == 1 {
if fields[0].name().text() is Some(name) && is_secret_field(name) {
let payload = fields[0].value()
let nested_table = payload.kind() is Struct(_) && name != "keyring"
if !nested_table {
guard secret_ref_of(value) is Some(_) else {
let key = if path.is_empty() {
KeyPath::{ names: ["(root)"] }
} else {
{ names: path.copy() }
}
raise InvalidSecret(
key~,
origin~,
reason="the \{name} payload has the wrong shape",
)
}
return
}
}
}
for field in fields {
guard field.name().text() is Some(name) else { continue }
check_secret_payloads(
{ ion: field.value() },
origin,
syntax,
path=[..path, name],
)
}
}
List(items) | Sexp(items) =>
for item in items {
check_secret_payloads({ ion: item }, origin, syntax, path~)
}
_ => ()
}
}
///|
fn is_secret_field(name : String) -> Bool {
name == "env" || name == "file" || name == "command" || name == "keyring"
}