Prisma - How to reference SQL view in schema - postgresql

I am designing a database schema for a multi-tenant project where a single user might have multiple "profiles", one for different tenants. I am using Supabase to provide a Postgres database along with some of their other BaaS features and hoping to use Prisma to help me manage the schema, relations, and migrations.
Supabase provides their own auth service, leveraging their auth.users table (in the auth schema). I want/need my public.profiles table to have a 1-n relation with auth.users so I can link my user to all of their profiles.
Is there a way I can define this in my schema.prisma file? I have tried manually creating a VIEW in the database and then defining a model for it, but when I try to apply my other Prisma schema changes (npx prisma db push or npx prisma db migrate dev) I get an error that the view/model already exists.
When database is initialized, I create the Auth SQL view in the public schema.
CREATE VIEW "Auth" AS SELECT id, email, role, created_at, updated_at, invited_at from auth.users;
Then in my Prisma schema I replicate the model. This seems to be the approach if you are using introspection, but I want Prisma to manage the schema, not the other way around.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// SQL view in public schema of Supabase "auth.users" table
model Auth {
id Int #unique
email String
role String
createdAt DateTime #map("created_at")
updatedAt DateTime #map("updated_at")
invitedAt DateTime #map("invited_at")
profiles Profile[]
}
model Profile {
id Int #id #default(autoincrement())
// This relation should reference the "auth.users" table
user Auth #relation(fields: [uid], references: [id])
uid Int
client Client #relation(fields: [clientId], references: [id])
clientId Int
firstName String
lastName String
}
model Client {
id Int #id #default(autoincrement())
createdAt DateTime #default(now())
updatedAt DateTime #default(now())
name String
type String
preferences Json
profiles Profile[]
}
Essentially, I need to know how I can create a relation to some portion of the schema that Prisma does not control. Can I define a reference-only model? Or a model that should be ignored during push or migrate operations? Can I define an explicit table name in the model relation definition?
There is this issue that talks about adding more support for views, but it's unclear if/when anything will happen. I'm wondering if anyone has a different solution. If this won't work I may just need to look into using something like NextAuth so I can fully manage the auth schema, but I'd prefer not to rebuild an auth system if I can help it.

You could create a public.users table via Prisma and add a Postgres trigger to duplicate the auth.users data to your public schema anytime a user signs up:
/**
* This trigger automatically creates a user entry when a new user signs up via Supabase Auth.
*/
create function public.handle_new_user()
returns trigger as $$
begin
insert into public.users (id, full_name, avatar_url)
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
Does that approach work for you?

Related

gorm different schema relation for tables

To further explain my problem I have a database in postgreSQL. With the intent of the database to look cleaner I split most of the tables in between two schema-s one for the "users" and a second for the so called "teams". As it's expected I've created a many to one relation between the teams.team table and the users.user table by giving the teamID inside the user table. I'm writing the backend for the current software in GoLang and decided to use Gorm as way to handle the database. I've gotten somewhat the hang of gorm and figured out how to use schema-s and to test that Gorm can handle my database solution I decided with the structures, I've created to represent the tables, to create again all my tables in the db.
For the different schema-s I've had to make different connections and specify that I want them for the specific schema and since I have to write that there's a connection to Team inside User it thinks it's in the same schema
type User struct {
gorm.Model
FirstName string
LastName string
Email string
ElsysEmail string
Mobile string
Password string
InfoID uint
Info Info
SecurityID uint
Security Security
RoleID uint
Role Role
TeamID uint
Team Team
LastLogin time.Time
}
For anyone having a problem with creating tables inside a schema here's my solution:
dsn := "host=localhost user=postgres password=password dbname=ht9 port=5432 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{NamingStrategy{TablePrefix: "schemaName."}})
if err != nil {
log.Fatal(err)
}
disclaimer : my problem is almost identical to this one - GORM model foreign key to different Postgres schema - but no one's answered it yet
Try implementing the Tabler interface. as follow
type AppUser struct {
global.GVA_MODEL
Username string `json:"userName" gorm:"index;comment:user'sname"`
}
func (u *AppUser) TableName() string {
return "app.app_users"
}

GORM model foreign key to different Postgres schema

I have 2 separate Go APIs that connect to the same Postgres database into different schemas. Each user has all access to all schemas.
In the first API I have the User model with schema1.Users table
In the 2nd I have the Task model with schema2.Tasks table
type User struct {
gorm.Model
}
type Task struct {
gorm.Model
CreatedBy uint
User User `gorm:"foreignKey:CreatedBy;references:User"`
}
When I try to run the 2nd API, the migrations create a Users table in schema2 and connects the foreign key there.
If I use the line below I get an error
User User `gorm:"foreignKey:CreatedBy;references:schema1.User"`
[error] invalid field found for struct github.com/xyz/models.Tasks's field User: define a valid foreign key for relations or implement the Valuer/Scanner interface
Any ideas how to connect the different schemas?

Trigger and function to insert user id into another table

I am using Prisma as my schema and migrating it to supabase with prisma migrate dev
One of my tables Profiles, should reference the auth.users table in supabase, in sql something like this id uuid references auth.users not null,
Now since that table is automatically created in supabase do I still add it to my prisma schema? It's not in public either it is in auth.
model Profiles {
id String #id #db.Uuid
role String
subId String
stripeCustomerId String
refundId String[]
createdAt DateTime #default(now())
updatedAt DateTime #updatedAt
}
The reason I want the relation is because I want a trigger to automatically run a function that inserts an id and role into the profiles table when a new users is invited.
This is that trigger and function
-- inserts a row into public.profiles
create function public.handle_new_user()
returns trigger
language plpgsql
security definer
as $$
begin
insert into public.Profiles (id, role)
values (new.id, 'BASE_USER');
return new;
end;
$$;
-- trigger the function every time a user is created
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
I had this working when I created the profiles table manually in supabase I included the reference to the auth.users, that's the only reason I can think of why the user Id and role won't insert into the profiles db when I invite a user, the trigger and function are failing
create table public.Profiles (
id uuid references auth.users not null,
role text,
primary key (id)
);
Update from comment:
One error I found is
relation "public.profiles" does not exist
I change it to "public.Profiles" with a capital in supabase, but the function seem to still be looking for lowercase.
What you show should just work:
db<>fiddle here
Looks like you messed up capitalization with Postgres identifiers.
If you (or your ORM) created the table as "Profiles" (with double-quotes), non-standard capitalization is preserved and you need to double-quote the name for the rest of its life.
So the trigger function body must read:
...
insert into public."Profiles" (id, role) -- with double-quotes
...
Note that schema and table (and column) have to be quoted separately.
See:
Are PostgreSQL column names case-sensitive?

Only allow read if user has exact document id postgresql row level security/supabase

Is there a way for a user to only be able to read a document only if they have the exact document ID?
I want to avoid creating users, so the only security is a random guid saved in browser memory - settings will be saved in "settings" table with id=guid.
So when page opens it will fetch with
supabase.from('settings').select('*').eq('id', guid)
How do I secure that setting (without creating (dummy) user)
Like this in Firebase:
Firebase firestore only allow read if user has exact document ID but for postgresql/supabase
This is doable, but I would:
Disallow all access to the table for anon users via RLS (return false from the RLS policy)
Write a postgres function using security definer that takes a uuid as a parameter and only returns a single row from the table based on that parameter. (return nothing if the row doesn't exist)
Call the function using the supabase .rpc() format.
Example:
create table people (id uuid primary key default gen_random_uuid(), name text);
alter table people enable row level security;
-- now, with no RLS policy, no anon or authenticated users can access the table
create or replace function get_person(person_id uuid)
return table (id uuid, name text) security definer
language sql AS $$
select id, name from people where id = person_id;
$$;
In your client code:
const { data, error } =
await supabase.rpc('get_person', { person_id: 'some-uuid' });
return { data, error };
Simply, you need to create a function in your current schema, which returning data from id and also you need to create only one user, that doesn't have any privilege except usage to that function.
Via this that person can use that function but can not select from your table.
For example codes: how-postgresql-give-permission-what-execute-a-function-in-schema-to-user

Best practice to handle column level select grants in PostGraphile

PostGraphile does NOT recommend column-level SELECT grants, instead recommends to
split your concerns into multiple tables and use the
one-to-one relationship feature to link them.
Now I want my users table to have a role field that can be accessed by role_admin but not by role_consumer. Based on the above recommendation, I created two tables. users table (in public schema) contains all fields that both roles can see, and user_accounts (in private schema) contains role field that only role_admin must be able to see. role field is added to the user GraphQL type via computed columns.
CREATE SCHEMA demo_public;
CREATE SCHEMA demo_private;
/* users table*/
CREATE TABLE demo_public.users (
user_id SERIAL PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
);
/* user_accounts */
CREATE TABLE demo_private.user_accounts (
user_id INT PRIMARY KEY REFERENCES demo_public.users (user_id) ON DELETE CASCADE,
role text not null default 'role_consumer',
);
/* role as computed column */
CREATE FUNCTION demo_public.users_role
(
u demo_public.users
)
RETURNS TEXT as $$
<code>
$$ LANGUAGE SQL STRICT STABLE;
Now basically I have two potions to set permissions.
1) The first option is to use table level security. IOW to grant select access on table user_accounts to ONLY role_admin.
GRANT SELECT ON TABLE demo_private.user_accounts TO role_admin;
GRANT EXECUTE ON FUNCTION demo_public.users_role(demo_public.users) TO role_admin;
ALTER TABLE demo_private.user_accounts ENABLE ROW LEVEL SECURITY;
CREATE POLICY select_any_user_accounts ON demo_private.user_accounts FOR SELECT TO role_admin using (true);
The problem with this approach is that when role_consumer runs a query that contains role field
{
me {
firstname
role
}
}
The above query returns an error. This is not good since the error affect the whole result hiding the result of other sibling fields.
2) The other option is to use row level security besides table level; IOW on table level, to grant select access on table user_accounts to both role_admin and role_consumer but in row level only allow admins to access rows of user_accounts.
GRANT USAGE ON SCHEMA demo_private TO role_consumer;
GRANT SELECT ON TABLE demo_private.user_accounts TO role_consumer;
GRANT EXECUTE ON FUNCTION demo_public.users_role(demo_public.users) TO role_consumer;
ALTER TABLE demo_private.user_accounts ENABLE ROW LEVEL SECURITY;
CREATE POLICY select_user_accounts ON demo_private.user_accounts FOR SELECT
USING ('role_admin' = nullif(current_setting('role', true), ''));
Now if the user with consumer_role runs the aforementioned query, the role field will be null, not affecting its sibling fields. But two questions:
Should we always avoid errors to prevent them affecting their siblings?
If yes, should we always handle things in Row Level and never only in Table Level?
For option 1, throwing an error from PostgreSQL during a query is not a good idea in PostGraphile because we compile the entire GraphQL tree into a single SQL query, so an error aborts the entire query. Instead, I would factor the permissions into the function and simply return null (rather than an error) if the user is not allowed to view it. One way to do this is with an additional WHERE clause:
CREATE FUNCTION demo_public.users_role (
u demo_public.users
) RETURNS TEXT AS $$
select role
from demo_private.user_accounts
where user_id = u.id
and current_setting('jwt.claims.role') = 'role_admin';
$$ LANGUAGE SQL STABLE;
For option 2: this is a perfectly valid solution.
Should we always avoid errors to prevent them affecting their siblings?
It's rare to throw errors when querying things in GraphQL - normally you return null instead. Think of it like visiting a private repository on GitHub when logged out - they don't return the "forbidden" error which reveals that the resource exists, instead they return the 404 error suggesting that it doesn't - unless you know better!
If yes, should we always handle things in Row Level and never only in Table Level?
I personally only use one role with PostGraphile, app_visitor, and that has been sufficient for all applications I've built with PostGraphile so far.