Fix OIDC subject collisions by introducing canonical provider-plus-subject principals
- Add canonical_principal(provider, sub) => 'oidc:v1:<provider>:<sub>' - Preserve provider identity across HTTP sessions and WebSockets - Apply principals throughout: game creation/joining/seating, WebSocket views/actions, Redis engine state, match history, leaderboard focus, ratings, /api/me, OpenAPI, and telemetry - Widen identity columns to 1024 chars with schema-only aerich migration (5_20261024221522_canonical_principals.py) - Document provider names as stable security identifiers - Add regression tests: same subject at different providers cannot share games, private hands, join codes, moves, history, or ratings; distinct seat and rating ownership verified - All platform, integration, game, and frontend tests pass; mypy clean
This commit is contained in:
+14
-14
@@ -23,7 +23,7 @@ from tavolo.scopa.state import (
|
||||
ScopaState,
|
||||
)
|
||||
from tavolo.platform import GameSession, Seat
|
||||
from tests.helpers import async_test, make_user, oidc_user, ws_users
|
||||
from tests.helpers import async_test, make_principal, make_user, oidc_user, ws_users
|
||||
|
||||
|
||||
class ScopaRoutesTest(unittest.TestCase):
|
||||
@@ -249,11 +249,11 @@ class ScopaWebSocketTest(unittest.TestCase):
|
||||
turn=0,
|
||||
players=[
|
||||
PlayerState(
|
||||
sub="alice", name="Alice", seat=0,
|
||||
sub=make_principal("alice"), name="Alice", seat=0,
|
||||
hand=[Card.parse("01D"), Card.parse("09C")],
|
||||
),
|
||||
PlayerState(
|
||||
sub="bob", name="Bob", seat=1,
|
||||
sub=make_principal("bob"), name="Bob", seat=1,
|
||||
hand=[Card.parse("04D")],
|
||||
),
|
||||
],
|
||||
@@ -264,10 +264,10 @@ class ScopaWebSocketTest(unittest.TestCase):
|
||||
id="scopa-asso-pigliatutto-1",
|
||||
game_type="scopa",
|
||||
join_code="SA0001",
|
||||
creator_sub="alice",
|
||||
creator_sub=make_principal("alice"),
|
||||
players=[
|
||||
Seat(user_sub="alice", display_name="Alice", team="A"),
|
||||
Seat(user_sub="bob", display_name="Bob", team="B"),
|
||||
Seat(user_sub=make_principal("alice"), display_name="Alice", team="A"),
|
||||
Seat(user_sub=make_principal("bob"), display_name="Bob", team="B"),
|
||||
],
|
||||
)
|
||||
session.state = state
|
||||
@@ -304,10 +304,10 @@ class ScopaWebSocketTest(unittest.TestCase):
|
||||
turn=0,
|
||||
players=[
|
||||
PlayerState(
|
||||
sub="alice", name="Alice", seat=0,
|
||||
sub=make_principal("alice"), name="Alice", seat=0,
|
||||
hand=[Card.parse("02D")],
|
||||
),
|
||||
PlayerState(sub="bob", name="Bob", seat=1),
|
||||
PlayerState(sub=make_principal("bob"), name="Bob", seat=1),
|
||||
],
|
||||
table=[Card.parse("02C")],
|
||||
)
|
||||
@@ -315,10 +315,10 @@ class ScopaWebSocketTest(unittest.TestCase):
|
||||
id="scopa-hand-end-1",
|
||||
game_type="scopa",
|
||||
join_code="SH0001",
|
||||
creator_sub="alice",
|
||||
creator_sub=make_principal("alice"),
|
||||
players=[
|
||||
Seat(user_sub="alice", display_name="Alice", team="A"),
|
||||
Seat(user_sub="bob", display_name="Bob", team="B"),
|
||||
Seat(user_sub=make_principal("alice"), display_name="Alice", team="A"),
|
||||
Seat(user_sub=make_principal("bob"), display_name="Bob", team="B"),
|
||||
],
|
||||
)
|
||||
session.state = state
|
||||
@@ -374,14 +374,14 @@ class ScopaTurnTimeoutTest(unittest.TestCase):
|
||||
id="scopa-turn-timeout-1",
|
||||
game_type=engine.id,
|
||||
join_code="ST0001",
|
||||
creator_sub="alice",
|
||||
creator_sub=make_principal("alice"),
|
||||
players=[
|
||||
Seat(user_sub="alice", display_name="Alice", team="A"),
|
||||
Seat(user_sub=make_principal("alice"), display_name="Alice", team="A"),
|
||||
],
|
||||
)
|
||||
engine.create(session, {})
|
||||
session.state.turn_timeout = 1
|
||||
engine.join(session, "bob", "Bob")
|
||||
engine.join(session, make_principal("bob"), "Bob")
|
||||
await game_store.save(session)
|
||||
|
||||
ws_transport = ASGIWebSocketTransport(app=app)
|
||||
|
||||
Reference in New Issue
Block a user