supabase / realtime-js

An isomorphic Javascript client for Supabase Realtime server.
https://supabase.com
MIT License
321 stars 58 forks source link

Custom Headers not present in RLS #233

Closed riderx closed 1 year ago

riderx commented 1 year ago

Bug report

Describe the bug

Custom header in Supabse SDK are not present to use in RLS as it is for query.

To Reproduce

Create a table test_rls with real-time and RLS

CREATE TABLE "public"."test_realtime_rls" (
    "id" int8 NOT NULL,
    "created_at" timestamptz DEFAULT now(),
    PRIMARY KEY ("id")
);

Add this RLS on ALL

(((current_setting('request.headers'::text, true))::json ->> 'capgkey'::text) = '8fd6f83fd1842b0d79cc212a133c4f10'::text)

Then set the SDK like that:

  const options: SupabaseClientOptions<'public'> = {
    global: {
      headers: {
        capgkey: '8fd6f83fd1842b0d79cc212a133c4f10',
      },
    },
  }
  const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, options)

Now register a real-time listener:

    supabase
      .channel('table-db-changes')
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'test_realtime_rls',
        },
        (payload) => {
          console.log('payload', payload)
        })
      .subscribe()

And do insert with default value

supabase
      .from('test_realtime_rls')
      .insert({created_at: new Date()})

Expected behavior

Insert should work, and a log payload should appear.

Where, in fact, only the insert works.

If I disable RLS and do insert again, the payload is logged.

System information

Additional context

I did try many solutions unsuccessfully: https://github.com/supabase/realtime/issues/470 https://github.com/supabase/supabase-js/issues/553#issuecomment-1432161145 https://discord.com/channels/839993398554656828/1087461400647049377/1087461400647049377

This is coming from a use case of my product Capgo. Where I created apikey ( access key) system for my users to use in the CLI of Capgo who is used in their CI/CD. Using the login/password, seems not reliable to me in that case. I know that made me an edge case of the SDK.

Hope I found an easy way to explain how to reproduce it.

riderx commented 1 year ago

Current solution used:

    let loop = true
    let now = new Date().toISOString()
    while (loop) {
        const { data, error } = await supabase
            .from('test_realtime_rls')
            .select()
            .single()
        if (data && !error) {
          console.log('payload', data)
        }
        now = new Date().toISOString()
        await wait(1000)
    }

It kind of self DDOS, but this is only used during onboarding of my users, so that will be like that until something better appear

w3b6x9 commented 1 year ago

This is a followup to https://github.com/supabase/realtime/issues/470#issuecomment-1475446726.

@riderx Yes, something like https://github.com/supabase/realtime/issues/470#issuecomment-1476237539.

https://github.com/supabase/realtime/issues/470#issuecomment-1476250161

did you sign your own token and pass that in?

riderx commented 1 year ago

@w3b6x9 that will require to my side to refactor the whole system who work already for normal query with header, the latest solution is not optimal but works. Do allowing the header to pass is planned in the near future? otherwise , I will try to plan a change to forge JWT, but for the CLI context it looks huge work compared to the actual super simple solution.

w3b6x9 commented 1 year ago

Do allowing the header to pass is planned in the near future?

@riderx we do not plan to support this in the near term.

I think your looping query above is a nice workaround.

Let me know if you decide to sign your own JWT or continue with the looping query you shared above.

riderx commented 1 year ago

Ok thanks !

NoRKin commented 8 months ago

@w3b6x9 do you still not have any plans to support this? Would you be open to a PR for that?

A warning in the supabase documentation to never use the request headers in RLS would be useful to avoid people designing the wrong solution only to find out later that it will never work with realtime.

filipecabaco commented 8 months ago

@NoRKin we're changing some things around regarding how we use RLS for authorization and this feature is planned to be added (this is a draft PR, the real implementation is yet to be implemented but it will be similar)

https://github.com/supabase/realtime/pull/757/files#diff-9542ac1036d63c2de0cf927b9cebf2c4d045ce6388432f56a0ba26ca83920c8bR47

NoRKin commented 6 months ago

@filipecabaco does this PR add support to RLS working with request headers in realtime?

https://github.com/supabase/realtime/pull/796/files#diff-9542ac1036d63c2de0cf927b9cebf2c4d045ce6388432f56a0ba26ca83920c8bR33

lauri865 commented 6 months ago

Also running into this issue.

Is there a way to detect in postgres when a function is being called within the realtime context? Could at least add a temporary workaround for RLS rules. But I tried e.g. current_setting('realtime.channel_name', TRUE) and it returns NULL.

It's impossible to have a multi-account setup without headers. Only option seems to be issuing a new JWT per account, but that doesn't work well across tabs either.

GaryAustin1 commented 6 months ago

@lauri865 You might need to turn on the feature by adding the channel to have RLS...

From the temp docs at github discussions... image https://github.com/orgs/supabase/discussions/22484

But just a guess.

lauri865 commented 6 months ago

Just to reiterate, this is our setup: We have a multi-account setup whereby a JWT is issued for an array of accounts. Which account is active depends on the URL slug or subdomain. This can be achieved with PostgREST without an issue with headers (either reading referrer or setting a custom header with selected account_id).

Now, the problem is with realtime, where it's not possible to set request headers. Which makes computing the RLS impossible, as we don't know which account is selected from the JWT.

And before anyone suggests that we forge a new token, it's a strict requirement to be able to use different accounts across tabs. That would make the token refreshing logic quite complex, and another thing we would have to handle on our own without supabase.

I think it would be great if realtime.subscription table also stored either supabase.realtime.params or supabase.realtime.headers values (which would be loaded into request.headers or request.params), which could then be used to compute RLS policies against. That would close the gap between REST + realtime.


On a side-note, the only way I could find to check if postgres function is called within the realtime context is the following – quite contrived, but essentially adding a custom claim to the subscription table, which then becomes accessible through request.jwt.claims.realtime. It's not a solution to the above, but enabled us to add a temporary workaround that doesn't make realtime completely unusable (we can only use it for the primary account for the time being):

CREATE OR REPLACE FUNCTION app.set_realtime_token()
    RETURNS TRIGGER
    LANGUAGE plpgsql
    AS $function$
BEGIN
    NEW.claims := jsonb_set(NEW.claims, '{realtime}', to_jsonb(true), true);
    RETURN NEW;
END
$function$;

CREATE OR REPLACE TRIGGER set_realtime_token
BEFORE INSERT OR UPDATE ON realtime.subscription
FOR EACH ROW
EXECUTE FUNCTION app.set_realtime_token();