Skip to content

Commit 92e7e10

Browse files
authored
Merge branch 'main' into feat/redis-command-command
2 parents 3632cdc + 8a9bab4 commit 92e7e10

26 files changed

Lines changed: 4531 additions & 3 deletions

internal/admin/auth_audit_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package admin
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"log/slog"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
"time"
11+
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func newAuthServiceWithAudit(t *testing.T) (*AuthService, *bytes.Buffer) {
16+
t.Helper()
17+
clk := fixedClock(time.Unix(1_700_000_000, 0).UTC())
18+
signer := newSignerForTest(t, 1, clk)
19+
20+
creds := MapCredentialStore{
21+
"AKIA_ADMIN": "ADMIN_SECRET",
22+
}
23+
roles := map[string]Role{
24+
"AKIA_ADMIN": RoleFull,
25+
}
26+
buf := &bytes.Buffer{}
27+
logger := slog.New(slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelInfo}))
28+
svc := NewAuthService(signer, creds, roles, AuthServiceOpts{
29+
Clock: clk,
30+
Logger: logger,
31+
})
32+
return svc, buf
33+
}
34+
35+
func TestAudit_LoginSuccessRecordsActor(t *testing.T) {
36+
svc, buf := newAuthServiceWithAudit(t)
37+
req := postJSON(t, loginRequest{AccessKey: "AKIA_ADMIN", SecretKey: "ADMIN_SECRET"})
38+
rec := httptest.NewRecorder()
39+
svc.HandleLogin(rec, req)
40+
41+
require.Equal(t, http.StatusOK, rec.Code)
42+
out := buf.String()
43+
require.Contains(t, out, `"msg":"admin_audit"`)
44+
require.Contains(t, out, `"action":"login"`)
45+
require.Contains(t, out, `"actor":"AKIA_ADMIN"`)
46+
require.Contains(t, out, `"claimed_actor":"AKIA_ADMIN"`)
47+
require.Contains(t, out, `"status":200`)
48+
}
49+
50+
func TestAudit_LoginFailureRecordsClaimedActor(t *testing.T) {
51+
svc, buf := newAuthServiceWithAudit(t)
52+
req := postJSON(t, loginRequest{AccessKey: "AKIA_ADMIN", SecretKey: "WRONG"})
53+
rec := httptest.NewRecorder()
54+
svc.HandleLogin(rec, req)
55+
56+
require.Equal(t, http.StatusUnauthorized, rec.Code)
57+
out := buf.String()
58+
require.Contains(t, out, `"action":"login"`)
59+
// We did NOT authenticate, so actor is empty.
60+
require.Contains(t, out, `"actor":""`)
61+
// But the claimed actor is still logged so operators can track
62+
// which access key was targeted by brute-force attempts.
63+
require.Contains(t, out, `"claimed_actor":"AKIA_ADMIN"`)
64+
require.Contains(t, out, `"status":401`)
65+
}
66+
67+
func TestAudit_LogoutReadsActorFromContext(t *testing.T) {
68+
svc, buf := newAuthServiceWithAudit(t)
69+
70+
// HandleLogout reads the principal from the request context (the
71+
// production wiring puts SessionAuth in front of it). Mirror that
72+
// here by injecting a principal directly so we can exercise the
73+
// audit branch without standing up the full router.
74+
req := httptest.NewRequest(http.MethodPost, "/admin/api/v1/auth/logout", nil)
75+
req.RemoteAddr = "127.0.0.1:1"
76+
ctx := context.WithValue(req.Context(), ctxKeyPrincipal,
77+
AuthPrincipal{AccessKey: "AKIA_ADMIN", Role: RoleFull})
78+
req = req.WithContext(ctx)
79+
80+
rec := httptest.NewRecorder()
81+
svc.HandleLogout(rec, req)
82+
83+
require.Equal(t, http.StatusNoContent, rec.Code)
84+
out := buf.String()
85+
require.Contains(t, out, `"action":"logout"`)
86+
require.Contains(t, out, `"actor":"AKIA_ADMIN"`)
87+
}
88+
89+
func TestAudit_LogoutWithoutCookieEmptyActor(t *testing.T) {
90+
svc, buf := newAuthServiceWithAudit(t)
91+
req := httptest.NewRequest(http.MethodPost, "/admin/api/v1/auth/logout", nil)
92+
req.RemoteAddr = "127.0.0.1:1"
93+
rec := httptest.NewRecorder()
94+
svc.HandleLogout(rec, req)
95+
96+
require.Equal(t, http.StatusNoContent, rec.Code)
97+
out := buf.String()
98+
require.Contains(t, out, `"action":"logout"`)
99+
require.Contains(t, out, `"actor":""`)
100+
}
101+
102+
func TestAudit_LoginLengthTimingHashed(t *testing.T) {
103+
// Same-length secret mismatch and different-length secret mismatch
104+
// must both reach the failure path without short-circuiting on
105+
// length. We cannot time them precisely in a unit test, but we can
106+
// at least verify both paths emit the same failure response.
107+
svc, _ := newAuthServiceWithAudit(t)
108+
for _, secret := range []string{"x", "much-longer-wrong-secret-value-here"} {
109+
req := postJSON(t, loginRequest{AccessKey: "AKIA_ADMIN", SecretKey: secret})
110+
rec := httptest.NewRecorder()
111+
svc.HandleLogin(rec, req)
112+
require.Equal(t, http.StatusUnauthorized, rec.Code)
113+
require.Contains(t, rec.Body.String(), "invalid_credentials")
114+
}
115+
}

0 commit comments

Comments
 (0)