Repository navigation
fix(api)!: return 403 for authorization failures, reserve 401 for missing session #41590
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| '@rocket.chat/meteor': minor | ||
| --- | ||
|
|
||
| Stops the REST API from answering `401 Unauthorized` for authorization failures, so that a 401 only ever means "no valid session". | ||
|
|
||
| Every `error-unauthorized` / `error-not-authorized` throw in the codebase follows an authorization check (`hasPermissionAsync`, `canAccessRoom`, `canDeleteFile`, agent department scope), never a missing session, but the error switch in `server/api/ApiClass.ts` mapped `error-unauthorized` to 401 once the 9.0.0 breaking-change flag turned on — while the permission middleware answered 403 for the very same condition. A client could not infer session state from the status code, which is what broke live sessions in the `sendMessage` / `getReadReceipts` migration. | ||
|
|
||
| - `error-unauthorized` / `unauthorized` now always map to 403, dropping the 401 they would have been promoted to in 9.0.0 (403 today, unchanged). | ||
| - `error-not-authorized` / `not-authorized` (the dominant permission code, previously unmapped and falling through to 400) map to 403 from 9.0.0. | ||
| - `rooms.membersOrderedByRole` (broadcast member list), `rooms.hide` and `rooms.bannedUsers` returned 401 for access/permission failures; they return 403 from 9.0.0, and their response schemas now declare it. | ||
| - Removed a dead `if (applyBreakingChanges)` fork in the permissions middleware whose branches were identical. | ||
|
|
||
| The unauthenticated side is unchanged here: those checks still throw `error-invalid-user` and answer 400. Splitting them out needs a dedicated error code and is tracked separately. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -898,12 +898,17 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record< | |
| switch (e.error) { | ||
| case 'error-too-many-requests': | ||
| return api.tooManyRequests(typeof e === 'string' ? e : e.message); | ||
| // every `error-unauthorized`/`error-not-authorized` throw in the codebase follows an authorization check, | ||
| // so they map to 403; 401 is reserved for a missing or invalid session | ||
| case 'unauthorized': | ||
| case 'error-unauthorized': | ||
| return api.forbidden(typeof e === 'string' ? e : e.message); | ||
| case 'not-authorized': | ||
| case 'error-not-authorized': | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Authorization failures thrown as native Prompt for AI agents |
||
| if (applyBreakingChanges) { | ||
| return api.unauthorized(typeof e === 'string' ? e : e.message); | ||
| return api.forbidden(typeof e === 'string' ? e : e.message); | ||
| } | ||
| return api.forbidden(typeof e === 'string' ? e : e.message); | ||
| return api.failure(typeof e === 'string' ? e : e.message, e.error, process.env.TEST_MODE ? e.stack : undefined, e); | ||
| case 'forbidden': | ||
| case 'error-forbidden': | ||
| if (applyBreakingChanges) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4663,6 +4663,7 @@ describe('[Rooms]', () => { | |
| .set(nonMemberCredentials) | ||
| .send({ roomId: roomB._id }) | ||
| .expect('Content-Type', 'application/json') | ||
| // TODO: MAJOR 403 — currently 401 until the 9.0.0 breaking changes are applied | ||
| .expect(401) | ||
| .expect((res) => { | ||
| expect(res.body).to.have.property('success', false); | ||
|
|
@@ -5101,6 +5102,28 @@ describe('[Rooms]', () => { | |
| await Promise.all(bannedUsers.map((user) => deleteUser(user))); | ||
| }); | ||
|
|
||
| it('should return forbidden if user does not have access to the room', async () => { | ||
| const privateRoom = (await createRoom({ type: 'p', name: `banned-users-private-${Date.now()}-${Math.random()}` })).body.group; | ||
| const nonMember = await createUser({ joinDefaultChannels: false }); | ||
| const nonMemberCredentials = await login(nonMember.username, password); | ||
|
|
||
| try { | ||
| await request | ||
| .get(api('rooms.bannedUsers')) | ||
| .set(nonMemberCredentials) | ||
| .query({ roomId: privateRoom._id }) | ||
| .expect('Content-Type', 'application/json') | ||
| // TODO: MAJOR 403 — currently 401 until the 9.0.0 breaking changes are applied | ||
| .expect(401) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Make the expected status version-aware.
🤖 Prompt for AI Agents |
||
| .expect((res) => { | ||
| expect(res.body).to.have.property('success', false); | ||
| }); | ||
| } finally { | ||
| await deleteRoom({ type: 'p', roomId: privateRoom._id }); | ||
| await deleteUser(nonMember); | ||
| } | ||
| }); | ||
|
|
||
| it('should list every banned user of the room with only the projected fields', async () => { | ||
| const res = await request | ||
| .get(api('rooms.bannedUsers')) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2:
rooms.changeArchivationStatecan now emit 403 for its archive/unarchive permission check, but its declared responses omit 403. Add the forbidden response schema for this and other routes that propagateerror-not-authorized, so test validation and generated API contracts match runtime behavior.Prompt for AI agents