mirror of
https://github.com/supabase/agent-skills.git
synced 2026-03-27 10:09:26 +08:00
storage workflow
This commit is contained in:
@@ -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/);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user