Skip to content

Commit 1331341

Browse files
authored
Fix: impersonate a user result in 403 error page (baserow#5418)
* fix: Impersonating a user results in a 403 error page * chore: add changelong entry for impersonate bug fix * chore(changelog): fix typo in file name * fix: change approach to fix impersonate user issue
1 parent 87fbf1f commit 1331341

3 files changed

Lines changed: 156 additions & 11 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "bug",
3+
"message": "Fixes a 403 error that could appear when impersonating a user from the admin area",
4+
"issue_origin": "github",
5+
"issue_number": null,
6+
"domain": "core",
7+
"bullet_points": [],
8+
"created_at": "2026-05-23"
9+
}
Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
11
import UserService from '@baserow/modules/core/services/admin/users'
22

3-
/**
4-
* We only want to allow impersonation when a page loads for the first time because
5-
* on first load several endpoints are called to fetch initial data like workspace,
6-
* applications, etc. Starting the impersonation when the page first loads, makes
7-
* sure that we never have to take this situation into account because it only
8-
* happens on first page load before everything is fetched.
9-
*/
10-
export default defineNuxtRouteMiddleware(async (to) => {
11-
if (!import.meta.server) return
12-
13-
const nuxtApp = useNuxtApp()
3+
export const impersonateMiddleware = async (to, { nuxtApp }) => {
144
const store = nuxtApp.$store
155

166
// If the query param is not provided, we don't want to do anything.
177
if (!Object.prototype.hasOwnProperty.call(to.query, '__impersonate-user')) {
188
return
199
}
2010

11+
// No session yet — `authentication` middleware normally redirects first,
12+
// but bail if it didn't so we don't fire a request that will 401.
13+
if (!store.getters['auth/isAuthenticated']) {
14+
return
15+
}
16+
2117
const userId = to.query['__impersonate-user']
2218

19+
// Already impersonating this user. A redirect after impersonation (e.g.
20+
// `dashboardRedirect`) makes Nuxt re-run middlewares with the impersonated
21+
// user in the store; without this guard we'd call the endpoint again as
22+
// that user.
23+
if (String(store.getters['auth/getUserId']) === String(userId)) {
24+
return
25+
}
26+
2327
// Request the impersonate user data, this contains the `token` and `user` object.
2428
// This is needed to impersonate the user.
2529
const { data } = await UserService(nuxtApp.$client).impersonate(userId)
@@ -34,4 +38,18 @@ export default defineNuxtRouteMiddleware(async (to) => {
3438
// Set the impersonating state to true so that the warning in the top left corner
3539
// is visible.
3640
store.dispatch('impersonating/setImpersonating', true)
41+
}
42+
43+
/**
44+
* We only want to allow impersonation when a page loads for the first time because
45+
* on first load several endpoints are called to fetch initial data like workspace,
46+
* applications, etc. Starting the impersonation when the page first loads, makes
47+
* sure that we never have to take this situation into account because it only
48+
* happens on first page load before everything is fetched.
49+
*/
50+
export default defineNuxtRouteMiddleware(async (to) => {
51+
if (!import.meta.server) return
52+
53+
const nuxtApp = useNuxtApp()
54+
return impersonateMiddleware(to, { nuxtApp })
3755
})
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { describe, test, expect, vi, beforeEach } from 'vitest'
2+
3+
import { impersonateMiddleware } from '@baserow/modules/core/middleware/impersonate'
4+
import UserService from '@baserow/modules/core/services/admin/users'
5+
6+
vi.mock('@baserow/modules/core/services/admin/users', () => ({
7+
default: vi.fn(),
8+
}))
9+
10+
/**
11+
* Builds a minimal `nuxtApp` stub backed by configurable store getters and a
12+
* spy-able `dispatch`. Mirrors the surface area the middleware actually uses.
13+
*/
14+
const buildNuxtApp = ({ isAuthenticated, userId } = {}) => {
15+
const dispatch = vi.fn()
16+
const getters = {
17+
'auth/isAuthenticated': !!isAuthenticated,
18+
'auth/getUserId': userId,
19+
}
20+
return {
21+
nuxtApp: {
22+
$store: { getters, dispatch },
23+
$client: {},
24+
},
25+
dispatch,
26+
}
27+
}
28+
29+
describe('impersonate middleware', () => {
30+
let impersonate
31+
32+
beforeEach(() => {
33+
impersonate = vi.fn().mockResolvedValue({
34+
data: { user: { id: 42 }, access_token: 'a', refresh_token: 'b' },
35+
})
36+
UserService.mockReturnValue({ impersonate })
37+
})
38+
39+
test('does nothing when the __impersonate-user query param is missing', async () => {
40+
const { nuxtApp, dispatch } = buildNuxtApp({
41+
isAuthenticated: true,
42+
userId: 1,
43+
})
44+
45+
await impersonateMiddleware({ query: {} }, { nuxtApp })
46+
47+
expect(impersonate).not.toHaveBeenCalled()
48+
expect(dispatch).not.toHaveBeenCalled()
49+
})
50+
51+
test('bails out when the user is not authenticated, even if the query param is set', async () => {
52+
const { nuxtApp, dispatch } = buildNuxtApp({
53+
isAuthenticated: false,
54+
})
55+
56+
await impersonateMiddleware(
57+
{ query: { '__impersonate-user': '5' } },
58+
{ nuxtApp }
59+
)
60+
61+
expect(impersonate).not.toHaveBeenCalled()
62+
expect(dispatch).not.toHaveBeenCalled()
63+
})
64+
65+
test('bails out when the current user is already the impersonated user', async () => {
66+
const { nuxtApp, dispatch } = buildNuxtApp({
67+
isAuthenticated: true,
68+
userId: 5,
69+
})
70+
71+
await impersonateMiddleware(
72+
{ query: { '__impersonate-user': '5' } },
73+
{ nuxtApp }
74+
)
75+
76+
expect(impersonate).not.toHaveBeenCalled()
77+
expect(dispatch).not.toHaveBeenCalled()
78+
})
79+
80+
test('matches when the store id is already a string equal to the query param', async () => {
81+
const { nuxtApp, dispatch } = buildNuxtApp({
82+
isAuthenticated: true,
83+
userId: '5',
84+
})
85+
86+
await impersonateMiddleware(
87+
{ query: { '__impersonate-user': '5' } },
88+
{ nuxtApp }
89+
)
90+
91+
expect(impersonate).not.toHaveBeenCalled()
92+
expect(dispatch).not.toHaveBeenCalled()
93+
})
94+
95+
test('impersonates the user and updates the store when authenticated as a different user', async () => {
96+
const { nuxtApp, dispatch } = buildNuxtApp({
97+
isAuthenticated: true,
98+
userId: 1,
99+
})
100+
101+
await impersonateMiddleware(
102+
{ query: { '__impersonate-user': '5' } },
103+
{ nuxtApp }
104+
)
105+
106+
expect(impersonate).toHaveBeenCalledWith('5')
107+
expect(dispatch).toHaveBeenCalledWith('auth/forceSetUserData', {
108+
user: { id: 42 },
109+
access_token: 'a',
110+
refresh_token: 'b',
111+
})
112+
expect(dispatch).toHaveBeenCalledWith('auth/preventSetToken')
113+
expect(dispatch).toHaveBeenCalledWith(
114+
'impersonating/setImpersonating',
115+
true
116+
)
117+
})
118+
})

0 commit comments

Comments
 (0)