role-impersonation.test.ts232 lines · main
1import { safeSql } from '@supabase/pg-meta'
2import { describe, expect, it } from 'vitest'
3
4import type { RoleImpersonationState } from './role-impersonation'
5import {
6 getExp1HourFromNow,
7 getPostgrestClaims,
8 wrapWithRoleImpersonation,
9} from './role-impersonation'
10
11const createBaseUser = (overrides = {}) => ({
12 id: 'user123',
13 email: 'test@example.com',
14 phone: undefined,
15 role: 'authenticated',
16 is_anonymous: false,
17 raw_app_meta_data: { provider: 'email' },
18 raw_user_meta_data: { name: 'Tester' },
19 ...overrides,
20})
21
22const createTestClaims = (overrides = {}) => ({
23 ref: 'test-project',
24 exp: getExp1HourFromNow(),
25 iat: Math.floor(Date.now() / 1000),
26 iss: 'https://test-project.supabase.co/auth/v1',
27 role: 'authenticated' as const,
28 ...overrides,
29})
30
31describe('getExp1HourFromNow', () => {
32 it('returns a timestamp 1 hour in the future', () => {
33 const now = Math.floor(Date.now() / 1000)
34 const exp = getExp1HourFromNow()
35 expect(exp).toBeGreaterThan(now)
36 expect(exp).toBeLessThanOrEqual(now + 3600)
37 })
38})
39
40describe('getPostgrestClaims', () => {
41 describe('native user claims', () => {
42 it('returns basic user claims', () => {
43 const claims = getPostgrestClaims('test-project', {
44 type: 'postgrest',
45 role: 'authenticated',
46 userType: 'native',
47 user: createBaseUser() as any,
48 })
49
50 expect(claims.aud).toBe('authenticated')
51 expect(claims.email).toBe('test@example.com')
52 expect(claims.sub).toBe('user123')
53 })
54
55 it('handles missing user data gracefully', () => {
56 const claims = getPostgrestClaims('test-project', {
57 type: 'postgrest',
58 role: 'authenticated',
59 userType: 'native',
60 } as any)
61
62 expect(claims.role).toBe('authenticated')
63 expect(claims.ref).toBe('test-project')
64 })
65 })
66
67 describe('external user claims', () => {
68 it('returns claims with additional data', () => {
69 const claims = getPostgrestClaims('test-project', {
70 type: 'postgrest',
71 role: 'authenticated',
72 userType: 'external',
73 externalAuth: {
74 sub: 'ext123',
75 additionalClaims: { foo: 'bar', custom: 'value' },
76 },
77 })
78
79 expect(claims.sub).toBe('ext123')
80 expect((claims as any).foo).toBe('bar')
81 expect((claims as any).custom).toBe('value')
82 })
83
84 it('handles missing additional claims', () => {
85 const claims = getPostgrestClaims('test-project', {
86 type: 'postgrest',
87 role: 'authenticated',
88 userType: 'external',
89 externalAuth: {
90 sub: 'ext123',
91 },
92 })
93
94 expect(claims.sub).toBe('ext123')
95 expect((claims as any).foo).toBeUndefined()
96 })
97 })
98
99 describe('system roles', () => {
100 it('returns basic claims for anon role', () => {
101 const claims = getPostgrestClaims('test-project', {
102 type: 'postgrest',
103 role: 'anon',
104 })
105
106 expect(claims.role).toBe('anon')
107 expect(claims.ref).toBe('test-project')
108 })
109
110 it('returns basic claims for service_role', () => {
111 const claims = getPostgrestClaims('test-project', {
112 type: 'postgrest',
113 role: 'service_role',
114 })
115
116 expect(claims.role).toBe('service_role')
117 expect(claims.ref).toBe('test-project')
118 })
119 })
120})
121
122describe('wrapWithRoleImpersonation', () => {
123 const sql = safeSql`select * from colors;`
124 const ref = 'default'
125
126 describe('postgres role (undefined)', () => {
127 it('returns SQL as is when no role is selected', () => {
128 const roleImpersonationState: RoleImpersonationState = {
129 role: undefined,
130 claims: undefined,
131 }
132 const result = wrapWithRoleImpersonation(sql, roleImpersonationState)
133 expect(result).toBe(sql)
134 })
135 })
136
137 describe('anon role', () => {
138 it('wraps SQL with anon user configuration', () => {
139 const claims = createTestClaims({
140 iss: 'briven',
141 ref,
142 role: 'anon' as const,
143 })
144
145 const roleImpersonationState: RoleImpersonationState = {
146 role: { type: 'postgrest', role: 'anon' },
147 claims,
148 }
149 const result = wrapWithRoleImpersonation(sql, roleImpersonationState)
150
151 expect(result).toContain("set_config('role', 'anon', true)")
152 expect(result).toContain('request.jwt.claims')
153 expect(result).toContain('ROLE_IMPERSONATION_NO_RESULTS')
154 expect(result).toContain(sql)
155 })
156 })
157
158 describe('authenticated user', () => {
159 it('wraps SQL with native user configuration', () => {
160 const claims = createTestClaims({
161 iss: `https://${ref}.supabase.co/auth/v1`,
162 role: 'authenticated' as const,
163 })
164
165 const roleImpersonationState: RoleImpersonationState = {
166 role: {
167 type: 'postgrest',
168 role: 'authenticated',
169 aal: 'aal1',
170 userType: 'native',
171 user: {
172 email: 'test@email.com',
173 id: 'abc',
174 providers: [],
175 },
176 },
177 claims,
178 }
179 const result = wrapWithRoleImpersonation(sql, roleImpersonationState)
180
181 expect(result).toContain("set_config('role', 'authenticated', true)")
182 expect(result).toContain('request.jwt.claims')
183 expect(result).toContain('ROLE_IMPERSONATION_NO_RESULTS')
184 expect(result).toContain(sql)
185 })
186
187 it('wraps SQL with external user configuration', () => {
188 const claims = createTestClaims({
189 aal: 'aal1' as const,
190 aud: 'authenticated',
191 role: 'authenticated' as const,
192 session_id: 'ecab6bfd-3707-4e63-9b3b-d37af69449d9',
193 sub: 'user123',
194 })
195
196 const roleImpersonationState: RoleImpersonationState = {
197 role: {
198 type: 'postgrest',
199 role: 'authenticated',
200 userType: 'external',
201 externalAuth: {
202 sub: 'user123',
203 additionalClaims: {},
204 },
205 aal: 'aal1',
206 },
207 claims,
208 }
209 const result = wrapWithRoleImpersonation(sql, roleImpersonationState)
210
211 expect(result).toContain("set_config('role', 'authenticated', true)")
212 expect(result).toContain('request.jwt.claims')
213 expect(result).toContain('ROLE_IMPERSONATION_NO_RESULTS')
214 expect(result).toContain(sql)
215 })
216 })
217
218 describe('custom role', () => {
219 it('wraps SQL with custom role configuration', () => {
220 const customRole = 'test'
221 const roleImpersonationState: RoleImpersonationState = {
222 role: { type: 'custom', role: customRole },
223 claims: undefined,
224 }
225 const result = wrapWithRoleImpersonation(sql, roleImpersonationState)
226
227 expect(result).toContain(`set local role '${customRole}'`)
228 expect(result).toContain('ROLE_IMPERSONATION_NO_RESULTS')
229 expect(result).toContain(sql)
230 })
231 })
232})