Skip to content

Commit 7c162b8

Browse files
committed
fix(node): correct scoped permissions wire codec
Scoped permissions omitted list continuation markers, causing the server to reject any user permissions containing streams. Encode and consume the canonical markers, correct absent-permission framing, and cover the wire shape and end-to-end round trip.
1 parent 5a5bc9a commit 7c162b8

5 files changed

Lines changed: 213 additions & 70 deletions

File tree

foreign/node/src/e2e/tcp.user.e2e.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,38 @@ describe('e2e -> user', async () => {
4141
PollMessages: true,
4242
SendMessages: true
4343
},
44-
streams: []
44+
streams: [
45+
{
46+
streamId: 1,
47+
permissions: {
48+
manageStream: true,
49+
readStream: true,
50+
manageTopics: true,
51+
readTopics: true,
52+
pollMessages: true,
53+
sendMessages: true
54+
},
55+
topics: [
56+
{
57+
topicId: 1,
58+
permissions: {
59+
manage: true,
60+
read: true,
61+
pollMessages: true,
62+
sendMessages: true
63+
}
64+
}
65+
]
66+
}
67+
]
4568
};
4669

4770
const cUser = { userId, username, password, status, permissions };
4871

4972
it('e2e -> user::create', async () => {
5073
const user = await c.user.create(cUser);
5174
assert.ok(user);
75+
assert.deepEqual(user.permissions, permissions);
5276
});
5377

5478
it('e2e -> user::list', async () => {
@@ -89,12 +113,20 @@ describe('e2e -> user', async () => {
89113
it('e2e -> user::updatePermissions', async () => {
90114
const user = await c.user.get({ userId: username });
91115
assert.ok(user);
92-
const perms2 = { ...permissions };
93-
perms2.global.ReadServers = true;
116+
const perms2 = {
117+
...permissions,
118+
global: {
119+
...permissions.global,
120+
ReadServers: true
121+
}
122+
};
94123
const u2 = await c.user.updatePermissions({
95124
userId: user.id, permissions: perms2
96125
});
97126
assert.ok(u2);
127+
128+
const updatedUser = await c.user.get({ userId: user.id });
129+
assert.deepEqual(updatedUser?.permissions, perms2);
98130
});
99131

100132
it('e2e -> user::delete', async () => {

foreign/node/src/wire/user/create-user.command.test.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,49 @@
1919
import { describe, it } from 'node:test';
2020
import assert from 'node:assert/strict';
2121
import { CREATE_USER } from './create-user.command.js';
22+
import type { UserPermissions } from './permissions.utils.js';
2223

2324
describe('CreateUser', () => {
2425

2526
describe('serialize', () => {
2627

2728
const u1 = {
28-
id: 1,
2929
username: 'test-user',
3030
password: 'test-pwd',
3131
status: 1, // Active,
32-
// perms: undefined // @TODO
3332
};
3433

35-
it('serialize username, password, status, permissions into buffer', () => {
36-
assert.deepEqual(
37-
CREATE_USER.serialize(u1).length,
38-
1 + u1.username.length + 1 + u1.password.length + 1 + 1 + 4 + 1
39-
);
34+
const baseLength = 1 + Buffer.byteLength(u1.username)
35+
+ 1 + Buffer.byteLength(u1.password) + 1;
36+
37+
it('serialize without permissions', () => {
38+
const serialized = CREATE_USER.serialize(u1);
39+
assert.equal(serialized.length, baseLength + 1);
40+
assert.equal(serialized.readUInt8(baseLength), 0);
41+
});
42+
43+
it('serialize with permissions', () => {
44+
const permissions: UserPermissions = {
45+
global: {
46+
ManageServers: false,
47+
ReadServers: false,
48+
ManageUsers: false,
49+
ReadUsers: false,
50+
ManageStreams: false,
51+
ReadStreams: false,
52+
ManageTopics: false,
53+
ReadTopics: false,
54+
PollMessages: false,
55+
SendMessages: false
56+
},
57+
streams: []
58+
};
59+
const serialized = CREATE_USER.serialize({ ...u1, permissions });
60+
61+
assert.equal(serialized.length, baseLength + 1 + 4 + 11);
62+
assert.equal(serialized.readUInt8(baseLength), 1);
63+
assert.equal(serialized.readUInt32LE(baseLength + 1), 11);
64+
assert.deepEqual(serialized.subarray(baseLength + 5), Buffer.alloc(11));
4065
});
4166

4267
it('throw on username < 1', () => {
@@ -83,4 +108,3 @@ describe('CreateUser', () => {
83108

84109
});
85110
});
86-

foreign/node/src/wire/user/create-user.command.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,21 @@ export const CREATE_USER = {
5555
if (bPassword.length < 1 || bPassword.length > 255)
5656
throw new Error('User password should be between 1 and 255 bytes');
5757

58-
const bPermissions = serializePermissions(permissions);
59-
60-
return Buffer.concat([
58+
const bUser = Buffer.concat([
6159
uint8ToBuf(bUsername.length),
6260
bUsername,
6361
uint8ToBuf(bPassword.length),
6462
bPassword,
6563
uint8ToBuf(status),
66-
boolToBuf(!!permissions),
64+
]);
65+
66+
if (!permissions)
67+
return Buffer.concat([bUser, boolToBuf(false)]);
68+
69+
const bPermissions = serializePermissions(permissions);
70+
return Buffer.concat([
71+
bUser,
72+
boolToBuf(true),
6773
uint32ToBuf(bPermissions.length),
6874
bPermissions
6975
]);

foreign/node/src/wire/user/permissions.utils.test.ts

Lines changed: 99 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,110 @@
1818

1919
import { describe, it } from 'node:test';
2020
import assert from 'node:assert/strict';
21-
import { serializePermissions, deserializePermissions } from './permissions.utils.js';
21+
import {
22+
deserializePermissions,
23+
serializePermissions,
24+
type UserPermissions
25+
} from './permissions.utils.js';
2226

2327
describe('Permissions', () => {
2428

25-
const permissions = {
26-
global: {
27-
ManageServers: true,
28-
ReadServers: true,
29-
ManageUsers: true,
30-
ReadUsers: true,
31-
ManageStreams: true,
32-
ReadStreams: true,
33-
ManageTopics: true,
34-
ReadTopics: true,
35-
PollMessages: true,
36-
SendMessages: true
37-
},
38-
streams: []
29+
const globalPermissions = {
30+
ManageServers: true,
31+
ReadServers: true,
32+
ManageUsers: true,
33+
ReadUsers: true,
34+
ManageStreams: true,
35+
ReadStreams: true,
36+
ManageTopics: true,
37+
ReadTopics: true,
38+
PollMessages: true,
39+
SendMessages: true
3940
};
4041

41-
it('serialize/deserialize', () => {
42-
const s = serializePermissions(permissions);
43-
const d = deserializePermissions(s);
44-
assert.deepEqual(permissions, d);
42+
const globalOnlyPermissions = {
43+
global: globalPermissions,
44+
streams: []
45+
} satisfies UserPermissions;
46+
47+
const scopedPermissions = {
48+
global: globalPermissions,
49+
streams: [
50+
{
51+
streamId: 1,
52+
permissions: {
53+
manageStream: true,
54+
readStream: false,
55+
manageTopics: true,
56+
readTopics: false,
57+
pollMessages: true,
58+
sendMessages: false
59+
},
60+
topics: [
61+
{
62+
topicId: 10,
63+
permissions: {
64+
manage: true,
65+
read: false,
66+
pollMessages: true,
67+
sendMessages: false
68+
}
69+
},
70+
{
71+
topicId: 20,
72+
permissions: {
73+
manage: false,
74+
read: true,
75+
pollMessages: false,
76+
sendMessages: true
77+
}
78+
}
79+
]
80+
},
81+
{
82+
streamId: 2,
83+
permissions: {
84+
manageStream: false,
85+
readStream: true,
86+
manageTopics: false,
87+
readTopics: true,
88+
pollMessages: false,
89+
sendMessages: true
90+
},
91+
topics: []
92+
}
93+
]
94+
} satisfies UserPermissions;
95+
96+
const serializedScopedPermissions = Buffer.from([
97+
// Global permissions and has_streams.
98+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
99+
// Stream 1, its permissions, and has_topics.
100+
1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1,
101+
// Topic 10 and has_next_topic.
102+
10, 0, 0, 0, 1, 0, 1, 0, 1,
103+
// Topic 20 and has_next_topic.
104+
20, 0, 0, 0, 0, 1, 0, 1, 0,
105+
// has_next_stream.
106+
1,
107+
// Stream 2, its permissions, has_topics, and has_next_stream.
108+
2, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0
109+
]);
110+
111+
it('round-trips global permissions', () => {
112+
const serialized = serializePermissions(globalOnlyPermissions);
113+
const deserialized = deserializePermissions(serialized);
114+
assert.deepEqual(deserialized, globalOnlyPermissions);
115+
});
116+
117+
it('serializes stream and topic continuation markers', () => {
118+
const serialized = serializePermissions(scopedPermissions);
119+
assert.deepEqual(serialized, serializedScopedPermissions);
120+
});
121+
122+
it('round-trips multiple streams and topics', () => {
123+
const deserialized = deserializePermissions(serializedScopedPermissions);
124+
assert.deepEqual(deserialized, scopedPermissions);
45125
});
46126

47127
});

0 commit comments

Comments
 (0)