mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 18:13:42 +00:00
feat: bind signed rosters to assignments
This commit is contained in:
@@ -3,8 +3,13 @@ package store
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/cosmic-clash/cosmic-clash/server/domain"
|
||||
)
|
||||
|
||||
// DurableAssignment is the persistence form of a verified assignment-ready
|
||||
@@ -124,6 +129,36 @@ func SaveAssignments(ctx context.Context, db *sql.DB, assignments []DurableAssig
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// SaveVerifiedAssignmentRoster converts the backend-verified signed roster to
|
||||
// player-scoped rows. It rechecks the claims at this persistence boundary so a
|
||||
// caller cannot accidentally publish a token for another match or slot.
|
||||
func SaveVerifiedAssignmentRoster(ctx context.Context, db *sql.DB, assignment domain.Assignment, roster []domain.SignedJoinAuthorisation) error {
|
||||
if assignment.Allocation.State != domain.ServerAllocated || len(roster) == 0 {
|
||||
return fmt.Errorf("invalid verified assignment roster")
|
||||
}
|
||||
digest := domain.ManifestDigest(assignment.Manifest)
|
||||
rows := make([]DurableAssignment, 0, len(roster))
|
||||
for _, signed := range roster {
|
||||
auth := signed.Authorisation
|
||||
if len(signed.Signature) == 0 || auth.MatchID != assignment.Allocation.MatchID || auth.ServerID != assignment.Allocation.ServerID || auth.Protocol != strconv.Itoa(assignment.Allocation.Protocol) || auth.PlayerID == "" || auth.Slot < 0 || auth.Slot > 5 || auth.ExpiresAt.IsZero() {
|
||||
return fmt.Errorf("invalid signed assignment roster")
|
||||
}
|
||||
envelope, err := json.Marshal(signed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode signed assignment roster: %w", err)
|
||||
}
|
||||
rows = append(rows, DurableAssignment{
|
||||
MatchID: assignment.Allocation.MatchID, PlayerID: auth.PlayerID,
|
||||
AllocationID: assignment.Allocation.AllocationID, ServerID: assignment.Allocation.ServerID,
|
||||
Slot: auth.Slot, Region: assignment.Allocation.Region, ClientBuild: assignment.Allocation.Build,
|
||||
ProtocolVersion: assignment.Allocation.Protocol, Transport: assignment.Allocation.Transport,
|
||||
Endpoint: assignment.Endpoint, JoinAuthorisation: base64.RawURLEncoding.EncodeToString(envelope),
|
||||
ManifestDigest: digest[:], ExpiresAt: auth.ExpiresAt, Revision: 1,
|
||||
})
|
||||
}
|
||||
return SaveAssignments(ctx, db, rows)
|
||||
}
|
||||
|
||||
func GetAssignment(ctx context.Context, db *sql.DB, playerID, matchID string, now time.Time) (DurableAssignment, error) {
|
||||
if db == nil || playerID == "" || matchID == "" || now.IsZero() {
|
||||
return DurableAssignment{}, fmt.Errorf("invalid assignment recovery arguments")
|
||||
|
||||
@@ -3,6 +3,8 @@ package store
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cosmic-clash/cosmic-clash/server/domain"
|
||||
)
|
||||
|
||||
func TestAssignmentSQLBindsPlayerAndPreservesIdenticalReplay(t *testing.T) {
|
||||
@@ -37,4 +39,7 @@ func TestAssignmentStoreRejectsInvalidBatches(t *testing.T) {
|
||||
if err := SaveAssignments(nil, nil, []DurableAssignment{{MatchID: "match-1", PlayerID: "player-1"}}); err == nil {
|
||||
t.Fatal("invalid assignment batch accepted")
|
||||
}
|
||||
if err := SaveVerifiedAssignmentRoster(nil, nil, domain.Assignment{}, nil); err == nil {
|
||||
t.Fatal("empty verified roster accepted")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user