restrict download-bus-stops to authenticated admin user
This commit is contained in:
parent
273a0cd6d6
commit
b4da3350db
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -97,31 +97,48 @@ async function getStops(): Promise<Array<StraetoBusStop>> {
|
||||||
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"}'
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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';
|
Loading…
Reference in New Issue