///|
/// An environment variable name, never its secret value.
pub struct CredentialVariable {
priv name : String
} derive(Eq, Debug)
///|
pub fn CredentialVariable::parse(text : String) -> CredentialVariable? {
if text.is_empty() || text.length() > 128 {
return None
}
for i, c in text.iter() {
if !(c == '_' ||
(c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') ||
(i > 0 && c >= '0' && c <= '9')) {
return None
}
}
Some({ name: text, })
}
///|
pub fn CredentialVariable::to_string(self : CredentialVariable) -> String {
self.name
}
///|
pub(all) enum CredentialSource {
BearerEnvironment(CredentialVariable)
BasicEnvironment(CredentialVariable, CredentialVariable)
} derive(Eq, Debug)
///|
pub(all) struct CredentialBinding {
origin : @resources.HttpOrigin
source : CredentialSource
} derive(Eq, Debug)
///|
fn config_credentials(
document : Json,
) -> Array[CredentialBinding] raise BuildError {
let values = match config_field(document, "credentials") {
None => return []
Some(Array(values)) => values
_ =>
raise InvalidBuildConfig(
"Expected credentials to be an array of origin and environment-variable bindings",
)
}
if values.length() > 32 {
raise InvalidBuildConfig("At most 32 credential origins are supported")
}
let result : Array[CredentialBinding] = []
fn variable(
value : Json,
field : String,
) -> CredentialVariable raise BuildError {
guard config_optional_string(value, field) is Some(text) &&
CredentialVariable::parse(text) is Some(name) else {
raise InvalidBuildConfig(
"Credential references must be literal environment variable names",
)
}
name
}
for value in values {
guard value is Object(fields) &&
config_optional_string(value, "origin") is Some(origin) &&
@resources.HttpOrigin::parse(origin) is Some(origin) else {
raise InvalidBuildConfig(
"Credential bindings require an HTTP(S) origin without path, query or user information",
)
}
if result.any(binding => binding.origin == origin) {
raise InvalidBuildConfig("Duplicate credential origin")
}
// Validate transport security at the same boundary as the library constructor.
if !origin.permits_credentials() {
raise InvalidBuildConfig(
"Credentials require HTTPS, except for loopback fixture origins",
)
}
let source = if fields.contains("bearerTokenEnv") && fields.length() == 2 {
BearerEnvironment(variable(value, "bearerTokenEnv"))
} else if fields.contains("usernameEnv") &&
fields.contains("passwordEnv") &&
fields.length() == 3 {
BasicEnvironment(
variable(value, "usernameEnv"),
variable(value, "passwordEnv"),
)
} else {
raise InvalidBuildConfig(
"Expected only origin with bearerTokenEnv, or origin with usernameEnv and passwordEnv; literal secrets and mixed methods are unsupported",
)
}
result.push({ origin, source, })
}
result
}
///|
/// Resolve secrets only at invocation time. The BuildConfig retains references.
pub fn BuildConfig::resolve_credentials(
self : BuildConfig,
environment : Map[String, String],
) -> @resources.Credentials raise BuildError {
fn secret(name : CredentialVariable) -> String raise BuildError {
guard environment.get(name.name) is Some(value) && !value.is_empty() else {
raise InvalidBuildConfig(
"A configured credential environment variable is missing or empty",
)
}
value
}
let entries = self.credentials.map(binding => {
let authorization = match binding.source {
BearerEnvironment(name) => @resources.Authorization::bearer(secret(name))
BasicEnvironment(user, password) =>
@resources.Authorization::basic(secret(user), secret(password))
}
guard authorization is Some(authorization) else {
raise InvalidBuildConfig("A configured credential has an invalid value")
}
(binding.origin, authorization)
})
@resources.Credentials::new(entries~).unwrap()
}