milestone 20: declarative configuration for iac
This commit is contained in:
+56
-2
@@ -101,6 +101,7 @@ pub const ValidateError = error{
|
||||
MissingKeyPath,
|
||||
MissingLogPath,
|
||||
PasswordAndHashBothSet,
|
||||
EmptyWebPassword,
|
||||
};
|
||||
|
||||
/// What `validate` returns: a verdict on the configuration, or the allocation
|
||||
@@ -114,7 +115,19 @@ pub const Error = ValidateError || Allocator.Error;
|
||||
/// same channel so a syntax error's line/column reaches the operator's output,
|
||||
/// plus the warnings — which are never returned by `validate` and so are not
|
||||
/// `ValidateError` members.
|
||||
pub const ProblemError = ValidateError || error{ ParseZon, SourceInNoGroup };
|
||||
/// Wider than `ValidateError`: the diagnostic channel also carries the problems
|
||||
/// found before the validator ever sees a `Config` — the ZON parse, the managed
|
||||
/// file that would not open (`config/loader.zig`), the file above the size limit
|
||||
/// — and the one found after it, an import whose diff would delete rows. The
|
||||
/// validator itself records only `ValidateError` members, which is what makes
|
||||
/// `validate`'s `@errorCast` of its own findings checked-safe.
|
||||
pub const ProblemError = ValidateError || error{
|
||||
ParseZon,
|
||||
SourceInNoGroup,
|
||||
ManagedConfigUnreadable,
|
||||
ConfigTooLarge,
|
||||
DestructiveImport,
|
||||
};
|
||||
|
||||
/// `.fail` rejects the configuration and is what an exit code is computed from.
|
||||
/// `.warn` reports something legal that is almost certainly not what the
|
||||
@@ -392,7 +405,10 @@ fn checkScalars(cfg: Config, diags: *Diagnostics) error{OutOfMemory}!void {
|
||||
|
||||
try checkBind(diags, cfg.web.bind, "web.bind", .any);
|
||||
try checkPort(diags, cfg.web.port, "web.port");
|
||||
if (cfg.web.password.len != 0 and cfg.web.password_hash.len != 0) {
|
||||
// Both fields are optional, and absence is the third state: a file that
|
||||
// states neither keeps the stored hash. So the test is on presence, not on
|
||||
// length.
|
||||
if (cfg.web.password != null and cfg.web.password_hash != null) {
|
||||
try diags.add(
|
||||
error.PasswordAndHashBothSet,
|
||||
"web.password",
|
||||
@@ -401,6 +417,22 @@ fn checkScalars(cfg: Config, diags: *Diagnostics) error{OutOfMemory}!void {
|
||||
.{},
|
||||
);
|
||||
}
|
||||
// A present-but-empty password would hash the empty string into a non-empty
|
||||
// PHC — authentication on — while every login with an empty password is
|
||||
// refused: authentication on and unreachable. The remedy is named, because
|
||||
// the operator who wrote this meant one of two other things.
|
||||
if (cfg.web.password) |password| {
|
||||
if (password.len == 0) {
|
||||
try diags.add(
|
||||
error.EmptyWebPassword,
|
||||
"web.password",
|
||||
.{},
|
||||
"password is set to the empty string; omit the field to keep the stored password, " ++
|
||||
"or set password_hash = \"\" to disable authentication",
|
||||
.{},
|
||||
);
|
||||
}
|
||||
}
|
||||
// A session TTL is a TTL; `BadTtl` is its bucket.
|
||||
if (cfg.web.session_ttl_hours < 1) {
|
||||
try diags.add(error.BadTtl, "web.session_ttl_hours", .{}, "must be at least 1", .{});
|
||||
@@ -1968,6 +2000,28 @@ test "error.PasswordAndHashBothSet" {
|
||||
try expectProblem(cfg, error.PasswordAndHashBothSet, "web.password");
|
||||
}
|
||||
|
||||
test "error.EmptyWebPassword names password_hash as the way to disable auth" {
|
||||
var cfg = baseConfig();
|
||||
cfg.web.password = "";
|
||||
try expectProblem(cfg, error.EmptyWebPassword, "web.password");
|
||||
|
||||
// The remedy has to be in the text: the operator who wrote `password = ""`
|
||||
// meant either "keep the current one" or "turn authentication off", and the
|
||||
// diagnostic is the only place that distinction is spelled out.
|
||||
var diags: Diagnostics = .init(testing.allocator);
|
||||
defer diags.deinit();
|
||||
try testing.expectError(error.EmptyWebPassword, validate(cfg, &diags));
|
||||
try testing.expect(std.mem.indexOf(u8, diags.problems.items[0].message, "password_hash = \"\"") != null);
|
||||
|
||||
// Absence is not emptiness: a file that states no password is legal and
|
||||
// means "keep the stored hash".
|
||||
var absent = baseConfig();
|
||||
absent.web.password = null;
|
||||
var quiet: Diagnostics = .init(testing.allocator);
|
||||
defer quiet.deinit();
|
||||
try validate(absent, &quiet);
|
||||
}
|
||||
|
||||
test "a config with five distinct problems yields five diagnostics and the first error" {
|
||||
var cfg = baseConfig();
|
||||
cfg.dns.port = 0; // BadPort, first in check order
|
||||
|
||||
Reference in New Issue
Block a user