role-impersonation.test.ts232 lines · main
| 1 | import { safeSql } from '@supabase/pg-meta' |
| 2 | import { describe, expect, it } from 'vitest' |
| 3 | |
| 4 | import type { RoleImpersonationState } from './role-impersonation' |
| 5 | import { |
| 6 | getExp1HourFromNow, |
| 7 | getPostgrestClaims, |
| 8 | wrapWithRoleImpersonation, |
| 9 | } from './role-impersonation' |
| 10 | |
| 11 | const 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 | |
| 22 | const 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 | |
| 31 | describe('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 | |
| 40 | describe('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 | |
| 122 | describe('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 | }) |