docs: reorder & testing
This commit is contained in:
@@ -2,7 +2,6 @@ package integration_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -11,10 +10,10 @@ import (
|
||||
"galaxy/transcoder"
|
||||
)
|
||||
|
||||
// TestSessionRevoke_SubsequentRequestsRejected revokes a session via
|
||||
// the internal endpoint backend exposes (gateway uses the same path)
|
||||
// and asserts the gateway rejects subsequent authenticated requests
|
||||
// bound to that session.
|
||||
// TestSessionRevoke_SubsequentRequestsRejected revokes the caller's
|
||||
// session through the user surface (signed gRPC end-to-end) and
|
||||
// asserts that subsequent authenticated calls bound to that session
|
||||
// are rejected by gateway.
|
||||
func TestSessionRevoke_SubsequentRequestsRejected(t *testing.T) {
|
||||
plat := testenv.Bootstrap(t, testenv.BootstrapOptions{})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
@@ -28,31 +27,36 @@ func TestSessionRevoke_SubsequentRequestsRejected(t *testing.T) {
|
||||
defer gw.Close()
|
||||
|
||||
// Sanity: the authenticated path works before revoke.
|
||||
payload, err := transcoder.GetMyAccountRequestToPayload(&usermodel.GetMyAccountRequest{})
|
||||
getPayload, err := transcoder.GetMyAccountRequestToPayload(&usermodel.GetMyAccountRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("encode payload: %v", err)
|
||||
t.Fatalf("encode get-account payload: %v", err)
|
||||
}
|
||||
if _, err := gw.Execute(ctx, usermodel.MessageTypeGetMyAccount, payload, testenv.ExecuteOptions{}); err != nil {
|
||||
if _, err := gw.Execute(ctx, usermodel.MessageTypeGetMyAccount, getPayload, testenv.ExecuteOptions{}); err != nil {
|
||||
t.Fatalf("pre-revoke call failed: %v", err)
|
||||
}
|
||||
|
||||
// Revoke.
|
||||
internal := testenv.NewBackendInternalClient(plat.Backend.HTTPURL)
|
||||
raw, resp, err := internal.Do(ctx, http.MethodPost, "/api/v1/internal/sessions/"+sess.DeviceSessionID+"/revoke", nil)
|
||||
// Revoke own session through signed gRPC.
|
||||
revokePayload, err := transcoder.RevokeMySessionRequestToPayload(&usermodel.RevokeMySessionRequest{
|
||||
DeviceSessionID: sess.DeviceSessionID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("encode revoke payload: %v", err)
|
||||
}
|
||||
revokeResult, err := gw.Execute(ctx, usermodel.MessageTypeRevokeMySession, revokePayload, testenv.ExecuteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("revoke: %v", err)
|
||||
}
|
||||
if resp.StatusCode/100 != 2 {
|
||||
t.Fatalf("revoke status %d body=%s", resp.StatusCode, string(raw))
|
||||
if revokeResult.ResultCode != "ok" {
|
||||
t.Fatalf("revoke result_code = %q, want ok", revokeResult.ResultCode)
|
||||
}
|
||||
|
||||
// Authenticated requests must now be rejected. Allow up to 2s
|
||||
// for the session-invalidation push frame to propagate to
|
||||
// gateway and close any cached state.
|
||||
// for the session-invalidation push frame to propagate to gateway
|
||||
// and close any cached state.
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
var lastErr error
|
||||
for time.Now().Before(deadline) {
|
||||
_, lastErr = gw.Execute(ctx, usermodel.MessageTypeGetMyAccount, payload, testenv.ExecuteOptions{})
|
||||
_, lastErr = gw.Execute(ctx, usermodel.MessageTypeGetMyAccount, getPayload, testenv.ExecuteOptions{})
|
||||
if lastErr != nil {
|
||||
break
|
||||
}
|
||||
@@ -61,7 +65,98 @@ func TestSessionRevoke_SubsequentRequestsRejected(t *testing.T) {
|
||||
if lastErr == nil {
|
||||
t.Fatalf("post-revoke call still succeeded; expected rejection")
|
||||
}
|
||||
if !testenv.IsUnauthenticated(lastErr) {
|
||||
t.Fatalf("post-revoke status: expected Unauthenticated, got %v", lastErr)
|
||||
// Gateway maps a revoked session to FailedPrecondition ("device
|
||||
// session is revoked"); a session that vanished from the cache
|
||||
// before the call lands as Unauthenticated. Either is a correct
|
||||
// rejection.
|
||||
if !testenv.IsFailedPrecondition(lastErr) && !testenv.IsUnauthenticated(lastErr) {
|
||||
t.Fatalf("post-revoke status: %v", lastErr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSessionRevoke_RejectsForeignSession checks that a caller cannot
|
||||
// revoke a session that belongs to a different user. Backend returns
|
||||
// the same shape as a missing session (no foreign-id probing).
|
||||
func TestSessionRevoke_RejectsForeignSession(t *testing.T) {
|
||||
plat := testenv.Bootstrap(t, testenv.BootstrapOptions{})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
owner := testenv.RegisterSession(t, plat, "owner+foreign@example.com")
|
||||
attacker := testenv.RegisterSession(t, plat, "attacker+foreign@example.com")
|
||||
|
||||
attackerGW, err := attacker.DialAuthenticated(ctx, plat)
|
||||
if err != nil {
|
||||
t.Fatalf("dial attacker: %v", err)
|
||||
}
|
||||
defer attackerGW.Close()
|
||||
|
||||
revokePayload, err := transcoder.RevokeMySessionRequestToPayload(&usermodel.RevokeMySessionRequest{
|
||||
DeviceSessionID: owner.DeviceSessionID,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("encode revoke payload: %v", err)
|
||||
}
|
||||
result, err := attackerGW.Execute(ctx, usermodel.MessageTypeRevokeMySession, revokePayload, testenv.ExecuteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("attacker revoke: %v", err)
|
||||
}
|
||||
if result.ResultCode == "ok" {
|
||||
t.Fatalf("attacker revoke result_code = ok, want a not-found error")
|
||||
}
|
||||
// Decoded error envelope must carry the not-found code so attackers
|
||||
// see the same shape as a genuinely missing session.
|
||||
errResp, err := transcoder.PayloadToErrorResponse(result.PayloadBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("decode error: %v", err)
|
||||
}
|
||||
// Backend's user-side handlers stamp 404 responses with
|
||||
// `httperr.CodeNotFound = "not_found"`; the gateway forwards a
|
||||
// non-empty code as-is and only synthesises `subject_not_found`
|
||||
// when the upstream payload omits the code field. Both shapes
|
||||
// satisfy the "no foreign-id probing" contract — the attacker
|
||||
// learns the same thing for a missing session and a session that
|
||||
// belongs to someone else.
|
||||
if code := errResp.Error.Code; code != "not_found" && code != "subject_not_found" {
|
||||
t.Fatalf("error.code = %q, want not_found or subject_not_found", code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSessionRevoke_RevokeAll covers the bulk logout path. Two
|
||||
// sessions for the same user, then revoke-all, then both sessions
|
||||
// must reject authenticated traffic.
|
||||
func TestSessionRevoke_RevokeAll(t *testing.T) {
|
||||
plat := testenv.Bootstrap(t, testenv.BootstrapOptions{})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
|
||||
const email = "pilot+revoke-all@example.com"
|
||||
first := testenv.RegisterSession(t, plat, email)
|
||||
second := testenv.RegisterSession(t, plat, email)
|
||||
|
||||
firstGW, err := first.DialAuthenticated(ctx, plat)
|
||||
if err != nil {
|
||||
t.Fatalf("dial first: %v", err)
|
||||
}
|
||||
defer firstGW.Close()
|
||||
|
||||
revokeAllPayload, err := transcoder.RevokeAllMySessionsRequestToPayload(&usermodel.RevokeAllMySessionsRequest{})
|
||||
if err != nil {
|
||||
t.Fatalf("encode revoke-all payload: %v", err)
|
||||
}
|
||||
result, err := firstGW.Execute(ctx, usermodel.MessageTypeRevokeAllMySessions, revokeAllPayload, testenv.ExecuteOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("revoke-all: %v", err)
|
||||
}
|
||||
if result.ResultCode != "ok" {
|
||||
t.Fatalf("revoke-all result_code = %q, want ok", result.ResultCode)
|
||||
}
|
||||
|
||||
resp, err := transcoder.PayloadToRevokeAllMySessionsResponse(result.PayloadBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("decode revoke-all payload: %v", err)
|
||||
}
|
||||
if resp.Summary.RevokedCount != 2 {
|
||||
t.Fatalf("summary.revoked_count = %d, want 2 (sessions: %s, %s)", resp.Summary.RevokedCount, first.DeviceSessionID, second.DeviceSessionID)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user