-
Notifications
You must be signed in to change notification settings - Fork 1
Open
Description
Pre-requisites
- Supabase CLI v2.12.1
- Apply the latest KeyHippo migration against a clean database (
supabase db reset --local) - Add 'manage_api_keys' permission to the default scope (see migration code below)
-- Create (or find) a scope called 'default'
insert into keyhippo.scopes(name, description)
values ('default', 'Default scope for user-level permissions')
on conflict (name)
do nothing;
-- Add permissions to that scope
-- Here we add the same permissions the default user has: 'manage_api_keys'
insert into keyhippo.scope_permissions(scope_id, permission_id)
select
s.id,
p.id
from
keyhippo.scopes s
join keyhippo_rbac.permissions p on p.name in ('manage_api_keys')
where
s.name = 'default'
on conflict
do nothing;
Issue
When using the keyhippo.authorize function I would expect keyhippo.authorize('manage_api_keys') === true in the following two scenarios:
- Given that default user groups, roles and permissions are setup and a user is added to the 'User' role.
- Given that default user groups, roles and permissions are setup and a user is added to the 'User' role AND the 'manage_api_key' permission is added to the default scope.
However when I call keyhippo.authorize('manage_api_keys') in both of these scenarios it returns FALSE. I've attached a snippet of a MRE below. Have I misunderstood how authorize and RBAC works with KeyHippo or is there a problem?
Code
import { createClient } from "@supabase/supabase-js";
const main = async () => {
// Pre-requisites:
// 1. Apply KeyHippo migration on clean database
// 2. Add 'manage_api_keys' permission to the default scope
// Setup user, their roles and permissions
// 1. Create a user using the service role and auth admin
// [Flow 1] Email and password based auth flow
// 1. Sign in as that user
// 2. Get the user context (expecting authorize('manage_api_keys') to return true)
// 3. Create an API key
// [Flow 2] API key based auth flow
// 1. Connect to the database (Supabase) using the API key
// 2. Get the user context (expecting authorize('manage_api_keys') to return true)
// Check for URL, anon key and service role key
if (!process.env.SUPABASE_URL) {
console.error("No SUPABASE_URL environment variable found. Exiting...");
process.exit(1);
}
if (!process.env.SUPABASE_SERVICE_ROLE_KEY) {
console.error(
"No SUPABASE_SERVICE_ROLE_KEY environment variable found. Exiting...",
);
process.exit(1);
}
if (!process.env.SUPABASE_ANON_KEY) {
console.error(
"No SUPABASE_ANON_KEY environment variable found. Exiting...",
);
process.exit(1);
}
const serviceClient = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY,
);
// Create a user
const DEFAULT_PASSWORD = "Developer123!";
const adminUserResponse = await serviceClient.auth.admin.createUser({
email: "hello@example.com",
password: DEFAULT_PASSWORD,
email_confirm: true,
user_metadata: {
first_name: "Tom",
last_name: "Titherington",
},
});
if (!adminUserResponse?.data.user) {
console.error(
"No user returned from Supabase signup. Error:",
adminUserResponse.error,
);
console.log("Exiting...");
process.exit(1);
}
// [Flow 1] Email and password based auth flow
// Create the user and generate an API key
const client = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
);
const signIn = async () => {
const { data, error } = await client.auth.signInWithPassword({
email: "hello@example.com",
password: "Developer123!",
});
};
await signIn();
const getUserContext = async () => {
const { data: keyData, error: keyError } = await client.schema(
"keyhippo",
).rpc("current_user_context");
console.log("Context from email + password auth", keyData);
};
await getUserContext();
const checkAuthorized = async () => {
const { data, error } = await client.schema("keyhippo")
.rpc("authorize", {
requested_permission: "manage_api_keys",
});
console.log("Data from email + password authorize: ", data);
if (error) console.error("Error: ", error);
};
await checkAuthorized();
// // Generate a key
const createKey = async () => {
const { data, error } = await client.schema("keyhippo").rpc(
"create_api_key",
{ key_description: "Primary API Key", scope_name: "default" },
);
if (error) {
throw error;
}
console.log("API Key: ", data);
return data[0].api_key;
};
const apiKey = await createKey();
// [Flow 2] API key based auth flow
const apiClient = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
{
global: {
headers: {
"x-api-key": apiKey,
},
},
auth: {
persistSession: false,
detectSessionInUrl: false,
autoRefreshToken: false,
},
},
);
const { data: keyData, error: keyError } = await apiClient.schema(
"keyhippo",
)
.rpc("current_user_context");
console.log("Context from API key auth: ", keyData);
if (keyError) {
console.error("Error: ", keyError);
throw keyError;
}
const checkAuthorizedKey = async () => {
const { data, error } = await apiClient.schema("keyhippo")
.rpc("authorize", {
requested_permission: "manage_api_keys",
});
console.log("Data from API key authorize: ", data);
if (error) console.error("Error: ", error);
};
await checkAuthorizedKey();
};
main();
Outputs
❯ SUPABASE_URL=http://127.0.0.1:54321 SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU pnpx tsx keyhippo-example.ts
Context from email + password auth [
{
user_id: '7cbef48e-5de3-4154-96e1-ee26e0f67e8f',
scope_id: null,
permissions: [ 'manage_api_keys' ]
}
]
Data from email + password authorize: false
API Key: [
{
api_key: 'Xn8M2OVvxNUBp0FCZsRbMR1eMZMT9EFc3a953b8bd3f621fb2587064b47c40baa6b531492e4ce530af101eda17698ff0638aa7536a4ad44b0f5906d255cd3a3d4ad7b239fb70530a436ae07723675ee85',
api_key_id: '7d22db2c-9c59-4bf3-83d9-8f4426ef6c88'
}
]
Context from API key auth: [
{
user_id: '7cbef48e-5de3-4154-96e1-ee26e0f67e8f',
scope_id: 'a858033d-54b1-4870-92b5-e9103b1192a0',
permissions: [ 'manage_api_keys' ]
}
]
Data from API key authorize: false
Metadata
Metadata
Assignees
Labels
No labels