storage workflow

This commit is contained in:
Pedro Rodrigues
2026-02-20 15:11:07 +00:00
parent e03bc99ebb
commit 386b9fbb05

View File

@@ -90,28 +90,39 @@ test("storage policy uses foldername or path for user isolation", () => {
test("storage policy uses TO authenticated", () => { test("storage policy uses TO authenticated", () => {
const sql = getMigrationSQL().toLowerCase(); const sql = getMigrationSQL().toLowerCase();
// Storage upload/delete/update policies should use TO authenticated // Storage upload/delete/update policies should target authenticated users.
// Accepted forms:
// 1. Explicit TO authenticated
// 2. auth.uid() in USING/WITH CHECK (implicitly restricts to authenticated)
const policyBlocks = sql.match(/create\s+policy[\s\S]*?;/gi) ?? []; const policyBlocks = sql.match(/create\s+policy[\s\S]*?;/gi) ?? [];
const storagePolicies = policyBlocks.filter((p) => const storagePolicies = policyBlocks.filter((p) =>
p.toLowerCase().includes("storage.objects"), p.toLowerCase().includes("storage.objects"),
); );
// At least one storage policy should have TO authenticated // At least one storage policy should restrict to authenticated users
const hasAuthenticatedPolicy = storagePolicies.some((p) => const hasAuthenticatedPolicy = storagePolicies.some(
/to\s+(authenticated|public)/.test(p.toLowerCase()), (p) =>
/to\s+(authenticated|public)/.test(p.toLowerCase()) ||
/auth\.uid\(\)/.test(p.toLowerCase()),
); );
expect(hasAuthenticatedPolicy).toBe(true); expect(hasAuthenticatedPolicy).toBe(true);
// Specifically, upload/insert policies should be TO authenticated (not public) // Insert policies must restrict to authenticated users (explicit TO or auth.uid() check)
const insertPolicies = storagePolicies.filter((p) => const insertPolicies = storagePolicies.filter((p) =>
/for\s+insert/.test(p.toLowerCase()), /for\s+insert/.test(p.toLowerCase()),
); );
for (const policy of insertPolicies) { for (const policy of insertPolicies) {
expect(policy.toLowerCase()).toMatch(/to\s+authenticated/); const hasExplicitTo = /to\s+authenticated/.test(policy.toLowerCase());
const hasAuthUidCheck = /auth\.uid\(\)/.test(policy.toLowerCase());
expect(hasExplicitTo || hasAuthUidCheck).toBe(true);
} }
}); });
test("public read policy for avatars", () => { test("public read policy for avatars", () => {
const sql = getMigrationSQL().toLowerCase(); const sql = getMigrationSQL().toLowerCase();
// A SELECT policy on storage.objects for avatars bucket should allow public/anon access // A SELECT policy on storage.objects for avatars bucket should allow public/anon access.
// Accepted forms:
// 1. Explicit TO public / TO anon
// 2. No TO clause (defaults to public role, granting all access)
// 3. No auth.uid() restriction in USING (open to everyone)
const policyBlocks = sql.match(/create\s+policy[\s\S]*?;/gi) ?? []; const policyBlocks = sql.match(/create\s+policy[\s\S]*?;/gi) ?? [];
const avatarSelectPolicies = policyBlocks.filter( const avatarSelectPolicies = policyBlocks.filter(
(p) => (p) =>
@@ -120,17 +131,25 @@ test("public read policy for avatars", () => {
p.toLowerCase().includes("avatars"), p.toLowerCase().includes("avatars"),
); );
expect(avatarSelectPolicies.length).toBeGreaterThan(0); expect(avatarSelectPolicies.length).toBeGreaterThan(0);
// Should use TO public (or TO anon) for public read access // Should allow public access: explicit TO public/anon, or no TO clause without auth.uid() restriction
const hasPublicAccess = avatarSelectPolicies.some( const hasPublicAccess = avatarSelectPolicies.some((p) => {
(p) => const lower = p.toLowerCase();
/to\s+public/.test(p.toLowerCase()) || /to\s+anon/.test(p.toLowerCase()), const hasExplicitPublic =
); /to\s+public/.test(lower) || /to\s+anon/.test(lower);
// No TO clause and no auth.uid() restriction means open to all
const hasNoToClause = !/\bto\s+\w+/.test(lower);
const hasNoAuthRestriction = !/auth\.uid\(\)/.test(lower);
return hasExplicitPublic || (hasNoToClause && hasNoAuthRestriction);
});
expect(hasPublicAccess).toBe(true); expect(hasPublicAccess).toBe(true);
}); });
test("documents bucket is fully private", () => { test("documents bucket is fully private", () => {
const sql = getMigrationSQL().toLowerCase(); const sql = getMigrationSQL().toLowerCase();
// All policies for documents bucket should restrict to authenticated owner // All policies for documents bucket should restrict to authenticated owner.
// Accepted forms:
// 1. Explicit TO authenticated
// 2. auth.uid() in USING/WITH CHECK (implicitly restricts to authenticated)
const policyBlocks = sql.match(/create\s+policy[\s\S]*?;/gi) ?? []; const policyBlocks = sql.match(/create\s+policy[\s\S]*?;/gi) ?? [];
const documentPolicies = policyBlocks.filter( const documentPolicies = policyBlocks.filter(
(p) => (p) =>
@@ -143,9 +162,11 @@ test("documents bucket is fully private", () => {
expect(policy).not.toMatch(/to\s+public/); expect(policy).not.toMatch(/to\s+public/);
expect(policy).not.toMatch(/to\s+anon/); expect(policy).not.toMatch(/to\s+anon/);
} }
// All should be scoped to authenticated // All should be scoped to authenticated (explicit TO or auth.uid() check)
for (const policy of documentPolicies) { for (const policy of documentPolicies) {
expect(policy).toMatch(/to\s+authenticated/); const hasExplicitTo = /to\s+authenticated/.test(policy);
const hasAuthUidCheck = /auth\.uid\(\)/.test(policy);
expect(hasExplicitTo || hasAuthUidCheck).toBe(true);
} }
}); });
@@ -186,15 +207,24 @@ test("file_metadata policies use (select auth.uid())", () => {
test("uses timestamptz for time columns", () => { test("uses timestamptz for time columns", () => {
const sql = getMigrationSQL().toLowerCase(); const sql = getMigrationSQL().toLowerCase();
// Match "timestamp" that is NOT followed by "tz" or "with time zone"
const hasPlainTimestamp = /\btimestamp\b(?!\s*tz)(?!\s+with\s+time\s+zone)/;
// Only check if the migration defines time-related columns // Only check if the migration defines time-related columns
if ( if (
sql.includes("created_at") || sql.includes("created_at") ||
sql.includes("updated_at") || sql.includes("updated_at") ||
sql.includes("uploaded_at") sql.includes("uploaded_at")
) { ) {
expect(sql).not.toMatch(hasPlainTimestamp); // Check column definitions for plain "timestamp" (not timestamptz / timestamp with time zone).
// Only match timestamp as a column type — look for column_name followed by timestamp.
// Exclude matches inside trigger/function bodies and RETURNS TRIGGER.
const columnDefs = sql.match(
/(?:created_at|updated_at|uploaded_at)\s+timestamp\b/g,
);
if (columnDefs) {
for (const def of columnDefs) {
// Each match should use timestamptz or "timestamp with time zone"
expect(def).toMatch(/timestamptz|timestamp\s+with\s+time\s+zone/);
}
}
} }
}); });