Skip to content

Commit 2fbce96

Browse files
committed
Remove legacy, fix checks
1 parent 393a2d5 commit 2fbce96

9 files changed

Lines changed: 113 additions & 57 deletions

File tree

api/lib/src/event/client.dart

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -224,15 +224,10 @@ final class ServerRoleChangeRequest extends ClientWorldEvent
224224
with ServerRoleChangeRequestMappable {
225225
final Channel player;
226226
final Set<String> roles;
227-
final String? role;
228227

229-
ServerRoleChangeRequest(this.player, {this.roles = const {}, this.role});
228+
ServerRoleChangeRequest(this.player, {this.roles = const {}});
230229

231-
Set<String> get effectiveRoles => {
232-
kDefaultServerRole,
233-
...roles,
234-
if (role?.isNotEmpty == true) role!,
235-
};
230+
Set<String> get effectiveRoles => {kDefaultServerRole, ...roles};
236231
}
237232

238233
@MappableClass()

api/lib/src/event/event.mapper.dart

Lines changed: 2 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/lib/src/event/process/client.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,18 @@ const _maxIdentifierLength = 256;
99
const _maxImagesPerRequest = 100;
1010
const _maxPacksPerRequest = 128;
1111
const _maxBoardsPerRequest = 256;
12+
const _maxRolesPerRequest = 32;
13+
const _maxBanDuration = Duration(days: 3650);
1214

1315
bool _isReasonableIdentifier(String value) =>
1416
value.isNotEmpty && value.length <= _maxIdentifierLength;
1517

18+
bool _isPlausibleBanExpiry(DateTime? expiresAt) {
19+
if (expiresAt == null) return true;
20+
final now = DateTime.now();
21+
return expiresAt.isAfter(now) && !expiresAt.isAfter(now.add(_maxBanDuration));
22+
}
23+
1624
bool isValidClientEvent(
1725
WorldEvent event,
1826
Channel channel,
@@ -88,6 +96,21 @@ bool isValidClientEvent(
8896
event.packs.every(_isReasonableIdentifier),
8997
MessageRequest() =>
9098
event.message.isNotEmpty && event.message.length <= _maxTextLength,
99+
KickPlayerRequest() =>
100+
(channel == kAuthorityChannel || allowManagementRequests) &&
101+
(event.reason?.length ?? 0) <= _maxTextLength,
102+
BanPlayerRequest() =>
103+
(channel == kAuthorityChannel || allowManagementRequests) &&
104+
(event.reason?.length ?? 0) <= _maxTextLength &&
105+
_isPlausibleBanExpiry(event.expiresAt),
106+
ServerRoleChangeRequest() =>
107+
(channel == kAuthorityChannel || allowManagementRequests) &&
108+
event.roles.length <= _maxRolesPerRequest &&
109+
event.roles.every(_isReasonableIdentifier),
110+
GameRolesChangeRequest() =>
111+
(channel == kAuthorityChannel || allowManagementRequests) &&
112+
event.roles.length <= _maxRolesPerRequest &&
113+
event.roles.every(_isReasonableIdentifier),
91114
BoardsSpawnRequest() =>
92115
_isReasonableIdentifier(event.table) &&
93116
event.assets.length <= _maxBoardsPerRequest &&

api/test/roles_test.dart

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,9 @@ void main() {
1313
expect(decoded.roles, {'player', 'moderator', 'blackjack:dealer'});
1414
});
1515

16-
test(
17-
'role change requests retain player and accept legacy role payloads',
18-
() {
19-
final current = ServerRoleChangeRequest(2, roles: {'moderator'});
20-
final legacy = ServerRoleChangeRequestMapper.fromMap({
21-
'player': 2,
22-
'role': 'moderator',
23-
});
16+
test('role change requests retain the player role', () {
17+
final request = ServerRoleChangeRequest(2, roles: {'moderator'});
2418

25-
expect(current.effectiveRoles, {'player', 'moderator'});
26-
expect(legacy.effectiveRoles, {'player', 'moderator'});
27-
},
28-
);
19+
expect(request.effectiveRoles, {'player', 'moderator'});
20+
});
2921
}

app/pubspec.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -732,8 +732,8 @@ packages:
732732
dependency: "direct main"
733733
description:
734734
path: "packages/lw_sysapi"
735-
ref: d7d00d81291d6a2716da40b9f1d004ac4973771d
736-
resolved-ref: d7d00d81291d6a2716da40b9f1d004ac4973771d
735+
ref: "4504590d871958c48a93969461193691ea88cb18"
736+
resolved-ref: "4504590d871958c48a93969461193691ea88cb18"
737737
url: "https://github.com/LinwoodDev/dart_pkgs.git"
738738
source: git
739739
version: "0.0.1"

app/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ dependencies:
8585
lw_sysapi:
8686
git:
8787
url: https://github.com/LinwoodDev/dart_pkgs.git
88-
ref: d7d00d81291d6a2716da40b9f1d004ac4973771d
88+
ref: 4504590d871958c48a93969461193691ea88cb18
8989
path: packages/lw_sysapi
9090
setonix_api:
9191
path: ../api

server/lib/src/bloc.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,20 @@ class WorldBloc extends Bloc<PlayableWorldEvent, WorldState>
218218
);
219219
return;
220220
}
221+
if (!isValidClientEvent(
222+
data,
223+
packet.channel,
224+
state,
225+
assetManager: assetManager,
226+
allowManagementRequests: true,
227+
)) {
228+
server.log(
229+
'Rejected invalid ${data.runtimeType} payload from channel '
230+
'${packet.channel}.',
231+
level: LogLevel.warning,
232+
);
233+
return;
234+
}
221235
if (await _handleManagementEvent(data, packet.channel)) return;
222236
}
223237
ServerResponse? process;

server/lib/src/services/user/remote.dart

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,6 @@ final class RemoteUserService extends UserService {
1717
? {'Authorization': 'Bearer $endpointSecret'}
1818
: {};
1919

20-
SetonixUser _decodeUser(String body) {
21-
final data = jsonDecode(body) as Map<String, dynamic>;
22-
final legacyRole = data['role'];
23-
if (data['roles'] == null &&
24-
legacyRole is String &&
25-
legacyRole.isNotEmpty) {
26-
data['roles'] = [kDefaultServerRole, legacyRole];
27-
}
28-
return SetonixUserMapper.fromMap(data);
29-
}
30-
31-
String? _legacyRole(Set<String>? roles) {
32-
if (roles == null) return null;
33-
return roles.firstWhere(
34-
(role) => role != kDefaultServerRole,
35-
orElse: () => kDefaultServerRole,
36-
);
37-
}
38-
3920
@override
4021
Future<SetonixUser?> getUser(String fingerprint) async {
4122
final response = await http
@@ -46,7 +27,7 @@ final class RemoteUserService extends UserService {
4627
.timeout(requestTimeout);
4728
if (response.statusCode >= 200 && response.statusCode < 300) {
4829
if (response.body.isEmpty) return null; // No user found
49-
return _decodeUser(response.body);
30+
return SetonixUserMapper.fromJson(response.body);
5031
}
5132
throw KickMessage.fromString(response.body);
5233
}
@@ -60,7 +41,7 @@ final class RemoteUserService extends UserService {
6041
)
6142
.timeout(requestTimeout);
6243
if (response.statusCode == 200) {
63-
return _decodeUser(response.body);
44+
return SetonixUserMapper.fromJson(response.body);
6445
}
6546
return null;
6647
}
@@ -80,7 +61,6 @@ final class RemoteUserService extends UserService {
8061
final body = jsonEncode({
8162
'name': name,
8263
'onWhitelist': onWhitelist,
83-
'role': _legacyRole(roles),
8464
'roles': roles?.toList(growable: false),
8565
'banned': banned,
8666
'bannedUntil': bannedUntil?.millisecondsSinceEpoch,
@@ -103,7 +83,6 @@ final class RemoteUserService extends UserService {
10383
'fingerprint': fingerprint,
10484
'name': name,
10585
'onWhitelist': onWhitelist,
106-
'role': _legacyRole(roles),
10786
'roles': roles?.toList(growable: false),
10887
'banned': banned,
10988
'bannedUntil': bannedUntil?.millisecondsSinceEpoch,

server/test/authorization_test.dart

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,69 @@ void main() {
7777
expect(response.main?.data, isA<WorldInitialized>());
7878
});
7979

80+
test('management payloads enforce text, role, and expiry limits', () {
81+
final state = WorldState(data: SetonixData.empty());
82+
final assets = _EmptyAssetManager();
83+
bool valid(ClientWorldEvent event) => isValidClientEvent(
84+
event,
85+
2,
86+
state,
87+
assetManager: assets,
88+
allowManagementRequests: true,
89+
);
90+
String text(int length) => List.filled(length, 'a').join();
91+
92+
expect(valid(KickPlayerRequest(3, reason: text(2000))), isTrue);
93+
expect(valid(KickPlayerRequest(3, reason: text(2001))), isFalse);
94+
expect(
95+
valid(
96+
BanPlayerRequest(
97+
3,
98+
expiresAt: DateTime.now().add(const Duration(days: 30)),
99+
),
100+
),
101+
isTrue,
102+
);
103+
expect(
104+
valid(
105+
BanPlayerRequest(
106+
3,
107+
expiresAt: DateTime.now().subtract(const Duration(minutes: 1)),
108+
),
109+
),
110+
isFalse,
111+
);
112+
expect(valid(BanPlayerRequest(3)), isTrue);
113+
expect(
114+
valid(
115+
BanPlayerRequest(
116+
3,
117+
expiresAt: DateTime.now().add(const Duration(days: 3651)),
118+
),
119+
),
120+
isFalse,
121+
);
122+
expect(
123+
valid(
124+
ServerRoleChangeRequest(
125+
3,
126+
roles: {for (var i = 0; i < 33; i++) 'test:role$i'},
127+
),
128+
),
129+
isFalse,
130+
);
131+
expect(valid(GameRolesChangeRequest(3, {text(257)})), isFalse);
132+
expect(
133+
isValidClientEvent(
134+
ServerRoleChangeRequest(3, roles: {'moderator'}),
135+
2,
136+
state,
137+
assetManager: assets,
138+
),
139+
isFalse,
140+
);
141+
});
142+
80143
test('new unclassified client events default to play permission', () {
81144
final event = ToolbarActionRequest('test');
82145

0 commit comments

Comments
 (0)