feat: port el-ui vessels — rename crates→vessels, add El source + manifests
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
// el-secrets — Secrets management for el-ui.
|
||||
//
|
||||
// A Secret is a value with a redacted display contract. Source plugins
|
||||
// fetch by key from env, Vault, AWS Secrets Manager, or in-memory.
|
||||
// `SecretsResolver` composes a chain and resolves required keys at startup.
|
||||
|
||||
// ── Errors ───────────────────────────────────────────────────────────────────
|
||||
|
||||
let SECRET_ERR_MISSING: String = "secret.missing"
|
||||
let SECRET_ERR_SOURCE: String = "secret.source_error"
|
||||
let SECRET_ERR_PERMISSION: String = "secret.permission_denied"
|
||||
|
||||
// ── Source kinds ─────────────────────────────────────────────────────────────
|
||||
|
||||
let SRC_ENV: String = "env"
|
||||
let SRC_VAULT: String = "vault"
|
||||
let SRC_AWS_SECRETS: String = "aws_secrets"
|
||||
let SRC_IN_MEMORY: String = "in_memory"
|
||||
|
||||
// ── Source descriptor ───────────────────────────────────────────────────────
|
||||
|
||||
type SecretsSource {
|
||||
kind: String // env | vault | aws_secrets | in_memory
|
||||
config_json: String // source-specific (e.g. {"prefix":"EL_SECRET_"} or {"path":"secret/data/app"})
|
||||
}
|
||||
|
||||
fn source_env(prefix: String) -> SecretsSource {
|
||||
{ "kind": "env", "config_json": "{\"prefix\":\"" + prefix + "\"}" }
|
||||
}
|
||||
|
||||
fn source_vault(addr: String, token: String, mount: String) -> SecretsSource {
|
||||
let cfg: String = "{\"addr\":\"" + addr + "\",\"token\":\"" + token
|
||||
+ "\",\"mount\":\"" + mount + "\"}"
|
||||
{ "kind": "vault", "config_json": cfg }
|
||||
}
|
||||
|
||||
fn source_aws(region: String) -> SecretsSource {
|
||||
{ "kind": "aws_secrets", "config_json": "{\"region\":\"" + region + "\"}" }
|
||||
}
|
||||
|
||||
fn source_in_memory() -> SecretsSource {
|
||||
{ "kind": "in_memory", "config_json": "{\"map\":{}}" }
|
||||
}
|
||||
|
||||
// ── Source lookup ───────────────────────────────────────────────────────────
|
||||
//
|
||||
// Each source kind has its own fetch fn. `source_get` dispatches by kind.
|
||||
|
||||
fn source_get(src: SecretsSource, key: String) -> String {
|
||||
if str_eq(src.kind, "env") {
|
||||
let prefix: String = json_get(src.config_json, "prefix")
|
||||
return env(prefix + str_to_upper(str_replace(key, ".", "_")))
|
||||
}
|
||||
if str_eq(src.kind, "vault") {
|
||||
let addr: String = json_get(src.config_json, "addr")
|
||||
let token: String = json_get(src.config_json, "token")
|
||||
let mount: String = json_get(src.config_json, "mount")
|
||||
return vault_kv_get_at(addr, token, mount, key)
|
||||
}
|
||||
if str_eq(src.kind, "aws_secrets") {
|
||||
let region: String = json_get(src.config_json, "region")
|
||||
return aws_secrets_get(region, key)
|
||||
}
|
||||
if str_eq(src.kind, "in_memory") {
|
||||
let map: String = json_get(src.config_json, "map")
|
||||
return json_get(map, key)
|
||||
}
|
||||
""
|
||||
}
|
||||
|
||||
fn in_memory_insert(src: SecretsSource, key: String, value: String) -> SecretsSource {
|
||||
let map: String = json_get(src.config_json, "map")
|
||||
let map = json_set(map, key, "\"" + value + "\"")
|
||||
let cfg = json_set(src.config_json, "map", map)
|
||||
{ "kind": src.kind, "config_json": cfg }
|
||||
}
|
||||
|
||||
// ── Secret<T> — redacted wrapper ────────────────────────────────────────────
|
||||
//
|
||||
// A Secret stores its raw value behind a tag. `secret_display` always
|
||||
// returns `[REDACTED]`. `secret_expose` returns the actual string.
|
||||
// Serializers should call `secret_display` by default.
|
||||
|
||||
type Secret {
|
||||
tag: String // marker: "secret"
|
||||
raw: String // the actual value — never log this directly
|
||||
}
|
||||
|
||||
fn secret_new(value: String) -> Secret {
|
||||
{ "tag": "secret", "raw": value }
|
||||
}
|
||||
|
||||
fn secret_display(s: Secret) -> String {
|
||||
"[REDACTED]"
|
||||
}
|
||||
|
||||
fn secret_expose(s: Secret) -> String {
|
||||
s.raw
|
||||
}
|
||||
|
||||
// Disposable read-once guard — like Rust's `SecretGuard`. Consumes the secret
|
||||
// once and zeroes it. Useful at boundaries: read JWT signing key once, hand to
|
||||
// the signing routine, never readable again.
|
||||
type SecretGuard {
|
||||
consumed: Bool
|
||||
raw: String
|
||||
}
|
||||
|
||||
fn guard_new(s: Secret) -> SecretGuard {
|
||||
{ "consumed": false, "raw": s.raw }
|
||||
}
|
||||
|
||||
fn guard_take(g: SecretGuard) -> String {
|
||||
if g.consumed { return "" }
|
||||
g.raw
|
||||
}
|
||||
|
||||
// ── SecretsResolver — builder ───────────────────────────────────────────────
|
||||
//
|
||||
// Sources are tried in order. First non-empty hit wins.
|
||||
// `require(key)` marks a key as required at startup; `resolve()` fails if any
|
||||
// required key is unresolved.
|
||||
|
||||
type SecretsResolver {
|
||||
sources_json: String // JSON array of SecretsSource
|
||||
required_json: String // JSON array of required keys
|
||||
}
|
||||
|
||||
fn resolver_new() -> SecretsResolver {
|
||||
{ "sources_json": "[]", "required_json": "[]" }
|
||||
}
|
||||
|
||||
fn resolver_source(r: SecretsResolver, src: SecretsSource) -> SecretsResolver {
|
||||
{ "sources_json": json_array_push(r.sources_json, json_encode(src)),
|
||||
"required_json": r.required_json }
|
||||
}
|
||||
|
||||
fn resolver_require(r: SecretsResolver, key: String) -> SecretsResolver {
|
||||
{ "sources_json": r.sources_json,
|
||||
"required_json": json_array_push(r.required_json, "\"" + key + "\"") }
|
||||
}
|
||||
|
||||
// Resolved bag. Keys -> Secret. Failure if any required key is missing.
|
||||
type ResolvedSecrets {
|
||||
map_json: String // JSON: key -> raw value
|
||||
errors: String // JSON array of missing required keys
|
||||
}
|
||||
|
||||
fn resolver_resolve(r: SecretsResolver) -> ResolvedSecrets {
|
||||
let resolved: String = "{}"
|
||||
let errors: String = "[]"
|
||||
let n: Int = json_array_len(r.required_json)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let key: String = json_array_get(r.required_json, i)
|
||||
let value: String = lookup_chain(r.sources_json, key)
|
||||
if str_eq(value, "") {
|
||||
let errors = json_array_push(errors, "\"" + key + "\"")
|
||||
}
|
||||
if !str_eq(value, "") {
|
||||
let resolved = json_set(resolved, key, "\"" + value + "\"")
|
||||
}
|
||||
let i = i + 1
|
||||
}
|
||||
{ "map_json": resolved, "errors": errors }
|
||||
}
|
||||
|
||||
fn lookup_chain(sources_json: String, key: String) -> String {
|
||||
let n: Int = json_array_len(sources_json)
|
||||
let i: Int = 0
|
||||
while i < n {
|
||||
let src_json: String = json_array_get(sources_json, i)
|
||||
let src: SecretsSource = { "kind": json_get(src_json, "kind"),
|
||||
"config_json": json_get(src_json, "config_json") }
|
||||
let v: String = source_get(src, key)
|
||||
if !str_eq(v, "") { return v }
|
||||
let i = i + 1
|
||||
}
|
||||
""
|
||||
}
|
||||
|
||||
fn resolved_require(r: ResolvedSecrets, key: String) -> Secret {
|
||||
let raw: String = json_get(r.map_json, key)
|
||||
secret_new(raw)
|
||||
}
|
||||
|
||||
// ── Entry — smoke test ──────────────────────────────────────────────────────
|
||||
|
||||
let src: SecretsSource = source_in_memory()
|
||||
let src = in_memory_insert(src, "jwt.key", "test-key-for-testing-only")
|
||||
let resolver: SecretsResolver = resolver_source(resolver_new(), src)
|
||||
let resolver = resolver_require(resolver, "jwt.key")
|
||||
let resolved: ResolvedSecrets = resolver_resolve(resolver)
|
||||
let jwt: Secret = resolved_require(resolved, "jwt.key")
|
||||
println("[el-secrets] jwt.key = " + secret_display(jwt))
|
||||
Reference in New Issue
Block a user