Skip to content

keyhippo.authorize doesn't appear to check user permissions or API key permissions #43

@tomtitherington

Description

@tomtitherington

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:

  1. Given that default user groups, roles and permissions are setup and a user is added to the 'User' role.
  2. 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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions