Admin endpoints for bans and clients on the single listener (closes #27)
check / check (push) Waiting to run
check / check (push) Waiting to run
SWWAF_ADMIN_TOKEN, or its _FILE form, opens GET and POST /_smallwebwaf/bans, DELETE /_smallwebwaf/bans/<client> and GET /_smallwebwaf/clients/<ip>. Unset, they answer 404; a missing or wrong token gets 401, in observe mode too. They go through every check, as the metrics do. POST takes a netblock, not IPv4-mapped and without a zone, or a client's address, a duration or permanent, and a reason, and makes an admin ban even while another lasts. DELETE lifts every active ban covering the address, kept and marked lifted. Bans come back as bans.json entries; a client as clients.json holds it, with its bans. Judgement call: answers leave out bans.json's version field. Judgement call: DELETE takes an address, not a netblock. Rule suppressed: gosec G304 on a test reading bans.json. Model: opus-5-5
This commit was merged in pull request #92.
This commit is contained in:
@@ -50,6 +50,7 @@ const (
|
||||
stateDir = "SWWAF_STATE_DIR"
|
||||
stateWriteDelay = "SWWAF_STATE_WRITE_DELAY"
|
||||
stateCounterInterval = "SWWAF_STATE_COUNTER_INTERVAL"
|
||||
adminToken = "SWWAF_ADMIN_TOKEN" //nolint:gosec // the setting's name
|
||||
metricsToken = "SWWAF_METRICS_TOKEN" //nolint:gosec // the setting's name
|
||||
metricsTopN = "SWWAF_METRICS_TOP_N"
|
||||
instanceName = "SWWAF_INSTANCE_NAME"
|
||||
@@ -81,8 +82,12 @@ rlG9y/jrJb6ORy3kTLWo2EA0BA67vuI=
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
// token is a token of 32 characters, the shortest allowed.
|
||||
const token = "0123456789abcdef0123456789abcdef"
|
||||
// token is a token of 32 characters, the shortest allowed, and
|
||||
// otherToken another.
|
||||
const (
|
||||
token = "0123456789abcdef0123456789abcdef"
|
||||
otherToken = "fedcba9876543210fedcba9876543210"
|
||||
)
|
||||
|
||||
// instance is an SWWAF_INSTANCE_NAME that is a valid app name too, and
|
||||
// remoteURL an SWWAF_LOG_REMOTE_URL, for the tests that send the lines.
|
||||
@@ -693,32 +698,40 @@ func TestShortTokenStopsTheStartWithoutShowingIt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Characters are counted, not bytes: each é takes two.
|
||||
for _, value := range []string{"", token[1:], strings.Repeat("é", 31)} {
|
||||
t.Run(value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, name := range []string{adminToken, metricsToken} {
|
||||
for _, value := range []string{"", token[1:], strings.Repeat("é", 31)} {
|
||||
t.Run(name+"="+value, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := config.FromEnvironment(environment{metricsToken: value}.lookupEnv)
|
||||
_, err := config.FromEnvironment(environment{name: value}.lookupEnv)
|
||||
|
||||
want := metricsToken + ": is shorter than 32 characters"
|
||||
if err == nil || err.Error() != want {
|
||||
t.Errorf("error %v, want %s", err, want)
|
||||
}
|
||||
})
|
||||
want := name + ": is shorter than 32 characters"
|
||||
if err == nil || err.Error() != want {
|
||||
t.Errorf("error %v, want %s", err, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenIsLoggedMasked(t *testing.T) {
|
||||
func TestTokensAreReadAndLoggedMasked(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := fromEnvironment(t, environment{metricsToken: token})
|
||||
cfg := fromEnvironment(t, environment{adminToken: otherToken, metricsToken: token})
|
||||
if cfg.AdminToken != otherToken || cfg.MetricsToken != token {
|
||||
t.Errorf("admin token %q and metrics token %q, want %q and %q",
|
||||
cfg.AdminToken, cfg.MetricsToken, otherToken, token)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
slog.New(slog.NewJSONHandler(&out, nil)).Info("starting", "settings", cfg)
|
||||
|
||||
if strings.Contains(out.String(), token) ||
|
||||
!strings.Contains(out.String(), `"`+metricsToken+`":"********"`) {
|
||||
t.Errorf("the token is not logged masked: %s", out.String())
|
||||
logged := out.String()
|
||||
if strings.Contains(logged, token) || strings.Contains(logged, otherToken) ||
|
||||
!strings.Contains(logged, `"`+adminToken+`":"********"`) ||
|
||||
!strings.Contains(logged, `"`+metricsToken+`":"********"`) {
|
||||
t.Errorf("the tokens are not logged masked: %s", logged)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -901,6 +914,7 @@ func TestLogsEachSettingWithItsValue(t *testing.T) {
|
||||
stateDir: "/var/lib/smallwebwaf",
|
||||
stateWriteDelay: "10s",
|
||||
stateCounterInterval: "15m",
|
||||
adminToken: "",
|
||||
metricsToken: "",
|
||||
metricsTopN: "50",
|
||||
instanceName: hostname,
|
||||
|
||||
Reference in New Issue
Block a user