mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix(studio): encode special characters in project securing policies links (#45849)
Closes #45847. ## Summary `ProjectNeedsSecuringView.tsx` built the `View policies` href on the first-time security gate by interpolating `table.schema` and `table.name` directly into the URL. A table or schema containing `&`, `=`, `+`, or `#` corrupted the destination and routed the user to the wrong policies filter on what is meant to be a guided onboarding flow. Extracts the URL into `getTablePoliciesHref` in `ProjectNeedsSecuring.utils.ts` with `encodeURIComponent` wraps, and replaces the inline interpolation. Same pattern as #45385. ## Test plan Added `ProjectNeedsSecuring.utils.test.ts` covering `getTablePoliciesHref` (plain values, special chars in name, special chars in schema, both, undefined inputs) and pulling in the previously-untested `getTableKey`, `formatRlsDescription`, `sortTables`, and `buildSecurityPromptMarkdown` utilities. Ten tests total. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Added comprehensive test coverage for security utilities, including URL construction, formatting, sorting, and markdown report generation. * **Refactor** * Extracted URL building logic into a centralized utility function for improved consistency and maintainability. [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45849) <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
461c1c1783
commit
29bfa7b75b
3 files changed
+112
-1
No files matched your search
+96
@@ -0,0 +1,96 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { ProjectSecurityTable } from './ProjectNeedsSecuring.types'
|
||||
import {
|
||||
buildSecurityPromptMarkdown,
|
||||
formatRlsDescription,
|
||||
getTableKey,
|
||||
getTablePoliciesHref,
|
||||
sortTables,
|
||||
} from './ProjectNeedsSecuring.utils'
|
||||
|
||||
const table = (overrides: Partial<ProjectSecurityTable>): ProjectSecurityTable => ({
|
||||
id: 1,
|
||||
name: 't',
|
||||
schema: 'public',
|
||||
rlsEnabled: false,
|
||||
hasRlsIssue: false,
|
||||
dataApiAccessible: false,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('ProjectNeedsSecuring.utils: getTableKey', () => {
|
||||
it('joins schema and name with a dot', () => {
|
||||
expect(getTableKey({ schema: 'public', name: 'users' })).toBe('public.users')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ProjectNeedsSecuring.utils: formatRlsDescription', () => {
|
||||
it('returns the singular form when count is 1', () => {
|
||||
expect(formatRlsDescription(1)).toContain('1 table has RLS disabled')
|
||||
expect(formatRlsDescription(1)).toContain('its data')
|
||||
})
|
||||
|
||||
it('returns the plural form for any other count', () => {
|
||||
expect(formatRlsDescription(3)).toContain('3 tables have RLS disabled')
|
||||
expect(formatRlsDescription(3)).toContain('their data')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ProjectNeedsSecuring.utils: sortTables', () => {
|
||||
it('puts tables with active RLS issues first', () => {
|
||||
const tables = [
|
||||
table({ id: 1, name: 'a', hasRlsIssue: false, rlsEnabled: true }),
|
||||
table({ id: 2, name: 'b', hasRlsIssue: true, rlsEnabled: false }),
|
||||
table({ id: 3, name: 'c', hasRlsIssue: false, rlsEnabled: false }),
|
||||
]
|
||||
const sorted = sortTables(tables)
|
||||
expect(sorted.map((t) => t.name)).toEqual(['b', 'c', 'a'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('ProjectNeedsSecuring.utils: buildSecurityPromptMarkdown', () => {
|
||||
it('builds a markdown report with a header row and one row per table', () => {
|
||||
const markdown = buildSecurityPromptMarkdown(1, [
|
||||
table({ name: 'invoices', schema: 'public', dataApiAccessible: true, rlsEnabled: false }),
|
||||
])
|
||||
expect(markdown).toContain('## Project security review')
|
||||
expect(markdown).toContain('1 table has RLS disabled')
|
||||
expect(markdown).toContain('| invoices | public | Yes | Disabled |')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ProjectNeedsSecuring.utils: getTablePoliciesHref', () => {
|
||||
it('builds the policies href with plain values', () => {
|
||||
expect(getTablePoliciesHref('abc', 'public', 'invoices')).toBe(
|
||||
'/project/abc/auth/policies?schema=public&search=invoices'
|
||||
)
|
||||
})
|
||||
|
||||
it('preserves special characters in the table name', () => {
|
||||
const href = getTablePoliciesHref('abc', 'public', 'user_data&secret=1')
|
||||
const parsed = new URL(href, 'http://example.com')
|
||||
expect(parsed.searchParams.get('search')).toBe('user_data&secret=1')
|
||||
expect(parsed.searchParams.get('schema')).toBe('public')
|
||||
})
|
||||
|
||||
it('preserves special characters in the schema', () => {
|
||||
const href = getTablePoliciesHref('abc', 'my schema+x', 'users')
|
||||
const parsed = new URL(href, 'http://example.com')
|
||||
expect(parsed.searchParams.get('schema')).toBe('my schema+x')
|
||||
expect(parsed.searchParams.get('search')).toBe('users')
|
||||
})
|
||||
|
||||
it('encodes both values together', () => {
|
||||
const href = getTablePoliciesHref('abc', 'a&b=c', 'd e+f')
|
||||
const parsed = new URL(href, 'http://example.com')
|
||||
expect(parsed.searchParams.get('schema')).toBe('a&b=c')
|
||||
expect(parsed.searchParams.get('search')).toBe('d e+f')
|
||||
})
|
||||
|
||||
it('falls back to empty strings for undefined inputs', () => {
|
||||
expect(getTablePoliciesHref(undefined, undefined, undefined)).toBe(
|
||||
'/project//auth/policies?schema=&search='
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -6,6 +6,16 @@ const DEFAULT_EXPOSED_SCHEMA = 'public'
|
||||
export const getTableKey = ({ schema, name }: { schema: string; name: string }) =>
|
||||
`${schema}.${name}`
|
||||
|
||||
export const getTablePoliciesHref = (
|
||||
projectRef: string | undefined,
|
||||
schema: string | undefined,
|
||||
name: string | undefined
|
||||
): string => {
|
||||
return `/project/${projectRef ?? ''}/auth/policies?schema=${encodeURIComponent(
|
||||
schema ?? ''
|
||||
)}&search=${encodeURIComponent(name ?? '')}`
|
||||
}
|
||||
|
||||
export const getExposedSchemas = (dbSchema: string | null | undefined) => {
|
||||
const schemas = dbSchema ? parseDbSchemaString(dbSchema) : []
|
||||
return schemas.length > 0 ? schemas : [DEFAULT_EXPOSED_SCHEMA]
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
buildSecurityPromptMarkdown,
|
||||
formatRlsDescription,
|
||||
getTableKey,
|
||||
getTablePoliciesHref,
|
||||
} from './ProjectNeedsSecuring.utils'
|
||||
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
|
||||
import { AiAssistantDropdown } from '@/components/ui/AiAssistantDropdown'
|
||||
@@ -179,7 +180,11 @@ export const ProjectNeedsSecuringView = ({
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tables.map((table) => {
|
||||
const policiesHref = `/project/${projectRef}/auth/policies?schema=${table.schema}&search=${table.name}`
|
||||
const policiesHref = getTablePoliciesHref(
|
||||
projectRef,
|
||||
table.schema,
|
||||
table.name
|
||||
)
|
||||
const handleNavigation = createNavigationHandler(policiesHref, router)
|
||||
const trackViewPolicies = () =>
|
||||
onTrackAction('view_policies', {
|
||||
|
||||
Reference in new issue
Block a user