145 lines
4.4 KiB
Go
145 lines
4.4 KiB
Go
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCodeExchangeFailureDetailsRetainsOnlySafeDiagnostics(t *testing.T) {
|
|
t.Parallel()
|
|
code := "TAPM-12345-ABCDE"
|
|
details := codeExchangeFailureDetails(code, errCodeNotFound)
|
|
|
|
for _, expected := range []string{
|
|
"reason=code_not_found",
|
|
"code_hint=ABCDE",
|
|
"normalized_length=16",
|
|
"format=expected",
|
|
} {
|
|
if !strings.Contains(details, expected) {
|
|
t.Errorf("details %q do not contain %q", details, expected)
|
|
}
|
|
}
|
|
if strings.Contains(details, code) || strings.Contains(details, "12345") {
|
|
t.Fatalf("details contain more of the submitted code than its hint: %q", details)
|
|
}
|
|
}
|
|
|
|
func TestCodeExchangeFailureDetailsDoesNotHintUnrelatedInput(t *testing.T) {
|
|
t.Parallel()
|
|
details := codeExchangeFailureDetails("not-a-deployment-secret", errCodeNotFound)
|
|
if !strings.Contains(details, "code_hint=unavailable") ||
|
|
!strings.Contains(details, "format=unexpected") {
|
|
t.Fatalf("unexpected details for malformed input: %q", details)
|
|
}
|
|
if strings.Contains(details, "ecret") {
|
|
t.Fatalf("details retained part of unrelated input: %q", details)
|
|
}
|
|
}
|
|
|
|
func TestCodeExchangeFailureReasons(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
err error
|
|
reason string
|
|
}{
|
|
{errCodeExpired, "code_expired"},
|
|
{errCodeRevoked, "code_revoked"},
|
|
{errActionNotAuthorized, "action_not_authorized"},
|
|
{errPackageNotAuthorized, "package_not_authorized"},
|
|
{errHostLimitReached, "host_limit_reached"},
|
|
}
|
|
for _, test := range tests {
|
|
if details := codeExchangeFailureDetails("TAPM-12345-ABCDE", test.err); !strings.Contains(details, "reason="+test.reason) {
|
|
t.Errorf("details %q do not contain reason %q", details, test.reason)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMalformedCodeAttemptIsAudited(t *testing.T) {
|
|
server, _ := newAuthorizationTestServer(t)
|
|
body, err := json.Marshal(exchangeRequest{
|
|
Code: "not-a-code",
|
|
HostFingerprint: "0123456789abcdef",
|
|
Hostname: "pve01",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/exchange", bytes.NewReader(body))
|
|
response := httptest.NewRecorder()
|
|
server.handleExchange(response, request)
|
|
if response.Code != http.StatusForbidden {
|
|
t.Fatalf("status = %d, want %d", response.Code, http.StatusForbidden)
|
|
}
|
|
|
|
var hostname, details string
|
|
if err := server.db.QueryRow(
|
|
`SELECT hostname, details
|
|
FROM audit_events
|
|
WHERE event_type = 'code_exchange_failed'`,
|
|
).Scan(&hostname, &details); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if hostname != "pve01" ||
|
|
!strings.Contains(details, "reason=code_not_found") ||
|
|
!strings.Contains(details, "normalized_length=10") ||
|
|
!strings.Contains(details, "format=unexpected") ||
|
|
!strings.Contains(details, "code_hint=unavailable") ||
|
|
strings.Contains(details, "not-a-code") {
|
|
t.Fatalf("unsafe or incomplete malformed-code audit: hostname=%q details=%q", hostname, details)
|
|
}
|
|
}
|
|
|
|
func TestFailedExpiredCodeAttemptIsLinkedToDeployment(t *testing.T) {
|
|
server, _ := newAuthorizationTestServer(t)
|
|
code := "TAPM-12345-ABCDE"
|
|
codeHash := hashValue(code)
|
|
if _, err := server.db.Exec(
|
|
`INSERT INTO authorizations
|
|
(id, code_hash, code_hint, created_by, customer_label, host_limit, expires_at)
|
|
VALUES (42, ?, 'ABCDE', 'taiadmin', 'Acme migration', 3, ?)`,
|
|
codeHash[:], time.Now().UTC().Add(-time.Hour),
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
body, err := json.Marshal(exchangeRequest{
|
|
Code: code,
|
|
HostFingerprint: "0123456789abcdef",
|
|
Hostname: "pve01",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/exchange", bytes.NewReader(body))
|
|
response := httptest.NewRecorder()
|
|
server.handleExchange(response, request)
|
|
if response.Code != http.StatusForbidden {
|
|
t.Fatalf("status = %d, want %d", response.Code, http.StatusForbidden)
|
|
}
|
|
|
|
var authorizationID uint64
|
|
var hostname, details string
|
|
if err := server.db.QueryRow(
|
|
`SELECT authorization_id, hostname, details
|
|
FROM audit_events
|
|
WHERE event_type = 'code_exchange_failed'`,
|
|
).Scan(&authorizationID, &hostname, &details); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if authorizationID != 42 || hostname != "pve01" {
|
|
t.Fatalf("audit event authorization/hostname = %d/%q", authorizationID, hostname)
|
|
}
|
|
if !strings.Contains(details, "reason=code_expired") ||
|
|
!strings.Contains(details, "code_hint=ABCDE") ||
|
|
strings.Contains(details, code) {
|
|
t.Fatalf("unsafe or incomplete audit details: %q", details)
|
|
}
|
|
}
|