restrict download-bus-stops to authenticated admin user

This commit is contained in:
projectmoon 2024-04-11 00:03:32 +02:00
parent 273a0cd6d6
commit b4da3350db
4 changed files with 137 additions and 19 deletions

View File

@ -1,5 +1,5 @@
TRIGGER_API_KEY=tr_dev_f9kjPUxxfZO79OSjy1su TRIGGER_API_KEY=tr_dev_f9kjPUxxfZO79OSjy1su
TRIGGER_API_URL=https://api.trigger.dev TRIGGER_API_URL=https://api.trigger.dev
NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY=pk_dev_0wewTNYuGo93XkoHaU9s NEXT_PUBLIC_TRIGGER_PUBLIC_API_KEY=pk_dev_0wewTNYuGo93XkoHaU9s
NEXT_PUBLIC_SUPABASE_URL=https://yexfstvjfxhursmqcqcu.supabase.co NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlleGZzdHZqZnhodXJzbXFjcWN1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTI2ODg2OTMsImV4cCI6MjAyODI2NDY5M30.Mq1aKeO_SlwS-kEdsSn7VhHxHxNx1eeejd6jnBC7VJw NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU

View File

@ -12,12 +12,13 @@ client.defineJob({
id: "bus-stop-sanity-check", id: "bus-stop-sanity-check",
name: "Sanity Check Potential Job Arrivals", name: "Sanity Check Potential Job Arrivals",
version: "0.1.1", version: "0.1.1",
enabled: false, // 1 run per insert is a bit too insane.
trigger: db.onInserted({ trigger: db.onInserted({
table: 'potential_arrivals', table: 'potential_arrivals',
}), }),
run: async (payload, io, ctx) => { run: async (payload, io, ctx) => {
await io.runTask("log-payload", async function() { await io.runTask("log-payload", async function() {
console.log(payload); io.logger.log(JSON.stringify(payload));
}); });
}, },
}); });

View File

@ -1,4 +1,4 @@
import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; import { SupabaseClient, createClient } from "https://esm.sh/@supabase/supabase-js@2";
interface StraetoVariables { interface StraetoVariables {
[key: string]: number | string; [key: string]: number | string;
@ -93,35 +93,52 @@ async function getStops(): Promise<Array<StraetoBusStop>> {
}; };
const resp = await fetch(apiUrl, opts); const resp = await fetch(apiUrl, opts);
const body = await resp.json(); const body = await resp.json();
return body.data.GtfsStops.results; return body.data.GtfsStops.results;
} }
Deno.serve(async (req) => { async function verifyUserIsAdmin(userClient: SupabaseClient): Promise<boolean> {
const busStops = await getStops(); const user = await userClient.auth.getUser();
const metadata = user.data.user?.app_metadata;
if (!metadata || (metadata && !metadata.claims_admin)) {
return false;
}
return true;
}
Deno.serve(async (req) => {
const authHeader = req.headers.get("Authorization")!; const authHeader = req.headers.get("Authorization")!;
const supabaseClient = createClient(
const userClient = createClient(
Deno.env.get("SUPABASE_URL") ?? "", Deno.env.get("SUPABASE_URL") ?? "",
Deno.env.get("SUPABASE_ANON_KEY") ?? "", Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
{ global: { headers: { Authorization: authHeader } } }, { global: { headers: { Authorization: authHeader } } },
); );
const secureClient = createClient(
Deno.env.get("SUPABASE_URL") ?? "",
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
);
if (!(await verifyUserIsAdmin(userClient))) {
return new Response(
JSON.stringify({ status: "denied", error: "must be an admin" }),
{ status: 403, headers: { "Content-Type": "application/json" } },
);
}
const busStops = await getStops();
console.log("inserting", busStops.length, "rows"); console.log("inserting", busStops.length, "rows");
const toInsert = busStops.map(toDbBusStop); const toInsert = busStops.map(toDbBusStop);
const insert = await supabaseClient.from("bus_stops") const insert = await secureClient.from("bus_stops")
.upsert(toInsert, { ignoreDuplicates: true }); .upsert(toInsert, { ignoreDuplicates: true });
console.log(insert.statusText, insert.error?.code, insert.error?.message); console.log(insert.statusText, insert.error?.code, insert.error?.message);
console.log("rows inserted", insert.count);
// const { name } = await req.json()
// const data = {
// message: `Hello ${name}!`,
// }
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
"status": insert.statusText, "status": insert.statusText,
@ -139,7 +156,6 @@ Deno.serve(async (req) => {
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/download-bus-stops' \ curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/download-bus-stops' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
--header 'Content-Type: application/json' \ --header 'Content-Type: application/json'
--data '{"name":"Functions"}'
*/ */

View File

@ -0,0 +1,101 @@
CREATE OR REPLACE FUNCTION is_claims_admin() RETURNS "bool"
LANGUAGE "plpgsql"
AS $$
BEGIN
IF session_user = 'authenticator' THEN
--------------------------------------------
-- To disallow any authenticated app users
-- from editing claims, delete the following
-- block of code and replace it with:
-- RETURN FALSE;
--------------------------------------------
IF extract(epoch from now()) > coalesce((current_setting('request.jwt.claims', true)::jsonb)->>'exp', '0')::numeric THEN
return false; -- jwt expired
END IF;
If current_setting('request.jwt.claims', true)::jsonb->>'role' = 'service_role' THEN
RETURN true; -- service role users have admin rights
END IF;
IF coalesce((current_setting('request.jwt.claims', true)::jsonb)->'app_metadata'->'claims_admin', 'false')::bool THEN
return true; -- user has claims_admin set to true
ELSE
return false; -- user does NOT have claims_admin set to true
END IF;
--------------------------------------------
-- End of block
--------------------------------------------
ELSE -- not a user session, probably being called from a trigger or something
return true;
END IF;
END;
$$;
CREATE OR REPLACE FUNCTION get_my_claims() RETURNS "jsonb"
LANGUAGE "sql" STABLE
AS $$
select
coalesce(nullif(current_setting('request.jwt.claims', true), '')::jsonb -> 'app_metadata', '{}'::jsonb)::jsonb
$$;
CREATE OR REPLACE FUNCTION get_my_claim(claim TEXT) RETURNS "jsonb"
LANGUAGE "sql" STABLE
AS $$
select
coalesce(nullif(current_setting('request.jwt.claims', true), '')::jsonb -> 'app_metadata' -> claim, null)
$$;
CREATE OR REPLACE FUNCTION get_claims(uid uuid) RETURNS "jsonb"
LANGUAGE "plpgsql" SECURITY DEFINER SET search_path = public
AS $$
DECLARE retval jsonb;
BEGIN
IF NOT is_claims_admin() THEN
RETURN '{"error":"access denied"}'::jsonb;
ELSE
select raw_app_meta_data from auth.users into retval where id = uid::uuid;
return retval;
END IF;
END;
$$;
CREATE OR REPLACE FUNCTION get_claim(uid uuid, claim text) RETURNS "jsonb"
LANGUAGE "plpgsql" SECURITY DEFINER SET search_path = public
AS $$
DECLARE retval jsonb;
BEGIN
IF NOT is_claims_admin() THEN
RETURN '{"error":"access denied"}'::jsonb;
ELSE
select coalesce(raw_app_meta_data->claim, null) from auth.users into retval where id = uid::uuid;
return retval;
END IF;
END;
$$;
CREATE OR REPLACE FUNCTION set_claim(uid uuid, claim text, value jsonb) RETURNS "text"
LANGUAGE "plpgsql" SECURITY DEFINER SET search_path = public
AS $$
BEGIN
IF NOT is_claims_admin() THEN
RETURN 'error: access denied';
ELSE
update auth.users set raw_app_meta_data =
raw_app_meta_data ||
json_build_object(claim, value)::jsonb where id = uid;
return 'OK';
END IF;
END;
$$;
CREATE OR REPLACE FUNCTION delete_claim(uid uuid, claim text) RETURNS "text"
LANGUAGE "plpgsql" SECURITY DEFINER SET search_path = public
AS $$
BEGIN
IF NOT is_claims_admin() THEN
RETURN 'error: access denied';
ELSE
update auth.users set raw_app_meta_data =
raw_app_meta_data - claim where id = uid;
return 'OK';
END IF;
END;
$$;
NOTIFY pgrst, 'reload schema';