I'm looking for a way to create a function that takes in two parameters for user name and password and creates a read only role with it. I've tried something like:
create or replace function create_user_readonly (
unm varchar,
pwd varchar
)
returns varchar(10) as $$
begin
create role unm login password #pwd;
return 'success';
end;
$$ language plpgsql;
This throws the error:
[42601] ERROR: syntax error at or near "#" Position: 151
I thought of using dynamic SQL to construct the query but ran into this here (https://www.postgresql.org/docs/9.1/static/plpgsql-statements.html):
Another restriction on parameter symbols is that they only work in
SELECT, INSERT, UPDATE, and DELETE commands. In other statement types
(generically called utility statements), you must insert values textually
even if they are just data values.
here is an example:
create or replace function create_user_readonly (
unm varchar,
pwd varchar
)
returns varchar(10) as $$
begin
execute format($f$create role %I login password '%s'$f$,unm,pwd);
execute format('alter role %I set transaction_read_only to on',unm);
return 'success';
end;
$$ language plpgsql;
keep in mind though you will need to alter user to set transaction_read_only also to make it read only.
also:
CREATE ROLE does not offer setting RO default to role.
ALTER ROLE does
And keep in mind that overcoming uset configuration transaction_read_only is as easy as running one statement.
and create role won't give CONNECT permission, use CREATE USER instead if you want one.
Related
I'm new to PostgreSQL, and trying to learn about stored procedure with PostgreSQL. Here are the steps I followed.
Installed pgAdmin4
Created the Database
Created the table "Users" under public schema
Created the procedure "GetUserByEmail"
CREATE OR REPLACE PROCEDURE GetUserByEmail
(
Email Varchar(100)
)
LANGUAGE plpgsql AS
$$
BEGIN
Select * from public."Users" where "Email" = Email
END
$$;
When calling it from query tool, I get an error.
CALL public.GetUserByEmail('d#d.com')
ERROR: procedure public.getuserbyemail(unknown) does not exist LINE
1: CALL public.GetUserByEmail('d#d.com')
^ HINT: No procedure matches the given name and argument types. You might need to add explicit type casts. SQL state: 42883
Character: 6
Checked the permission, and the user has execution rights.
Tried different ways but not sure what is wrong.
Are PostgreSQL column names case-sensitive?
if you create table "users"(a int...) then you stick with "users" every time you select/update/delete "users" table.
You can easily imitate 38.5.9. SQL Functions Returning Sets(https://www.postgresql.org/docs/current/xfunc-sql.html)
CREATE FUNCTION getusers(text) RETURNS SETOF "users" AS $$
SELECT * FROM "users" WHERE email = $1;
$$ LANGUAGE SQL;
SELECT * FROM getusers('hi') AS t1;
stored procedure versus function
demo
newbie in SQL coming from a JS world needing some advice on triggers.
My user has an id column and my GraphQL API always calls INSERT INTO .... RETURNING * and then doing the transforms on the GraphQL layer to return what I want.
The goal is to allow a query like INSERT INTO .... RETURNING * work with RLS in place.
The policies are:
CREATE POLICY USER_SELECT_RESTRICTIONS
ON "user"
FOR SELECT
USING ("id" = current_user_id() OR current_user_role() = 'admin' OR current_user_role() = 'partner');
CREATE POLICY USER_INSERT_RESTRICTIONS
ON "user"
FOR INSERT
WITH CHECK (true);
This breaks for guest users (more context at the bottom) because they are allowed to INSERT but cannot SELECT (because of the restriction that only authorized users can select their own rows).
So I had the idea to somehow set the user setting manually in a trigger before/after insert (don't know which because I couldn't move forward due to the syntax error).
I've tried this but I get a syntax error at the NEW.id? (it just says "Syntax Error" to me)
CREATE OR REPLACE FUNCTION after_user_insert()
RETURNS TRIGGER AS $$
BEGIN
SET LOCAL jwt.claims.userId NEW.id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_user_id_on_insert
AFTER INSERT ON "user"
FOR EACH ROW
EXECUTE PROCEDURE after_user_insert();
Searched around a lot and to be honest I think I may just not find anything because I am missing the correct terminology to find what I need to find.
Would appreciate not only help on what on this specific problem but also any related advice on policies for guest users that need priviliges.
Context:
These are the relevant tables and functions
CREATE TYPE "user_role" AS ENUM (
'customer',
'partner',
'admin'
);
CREATE TABLE "user" (
"id" uuid UNIQUE PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
"first_name" varchar NOT NULL,
"last_name" varchar NOT NULL,
"email" text UNIQUE NOT NULL,
"role" user_role NOT NULL DEFAULT 'customer',
"created_at" timestamp NOT NULL DEFAULT NOW(),
"updated_at" timestamp NOT NULL DEFAULT NOW()
);
CREATE FUNCTION current_user_id() RETURNS uuid AS $$
SELECT nullif(current_setting('jwt.claims.userId', true), '')::uuid;
$$ LANGUAGE SQL stable;
CREATE FUNCTION current_user_role() RETURNS user_role AS $$
SELECT nullif(current_setting('jwt.claims.role', true), '')::user_role;
$$ LANGUAGE SQL stable
The RLS restricts SELECT to rows where the id column of the user table matches current_setting('jwt.claims.userId').
This was set previously by Postgraphile as seen here (https://www.graphile.org/postgraphile/security/).
One possible workaround I thought of would be this but I don't know if this would lead to some kind of vulnerabilities because of the obvious role elevation:
CREATE OR REPLACE FUNCTION after_user_insert()
RETURNS TRIGGER AS $$
BEGIN
SET LOCAL jwt.claims.role TO "customer";
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION before_user_insert()
RETURNS TRIGGER AS $$
BEGIN
SET LOCAL jwt.claims.role TO "admin";
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER before_insert
BEFORE INSERT ON "user"
FOR EACH ROW
EXECUTE PROCEDURE before_user_insert();
CREATE TRIGGER after_insert
AFTER INSERT ON "user"
FOR EACH ROW
EXECUTE PROCEDURE after_user_insert();
Your don't say what a guest is, but your approach seems wrong.
Rather than disabling your checks on a low level, which gives you a bad feeling for good reasons, you should choose one of the following approaches:
fix your policies to allow the necessary operation (perhaps by adding a permissive policy)
have certain operations performed by a SECURITY DEFINER function that belongs to a user not subject to the restrictions.
If you insist on a trigger based solution, you have to use a BEFORE trigger. Also, consider that you cannot use parameterss with a SET statement. You'd either have to use dynamic SQL or (better) use a function:
CREATE OR REPLACE FUNCTION before_user_insert() RETURNS TRIGGER AS
$$BEGIN
SELECT set_config('jwt.claims.userId', NEW.id::text, TRUE);
RETURN NEW;
END;$$ LANGUAGE plpgsql;
CREATE TRIGGER set_user_id_on_insert BEFORE INSERT ON "user"
FOR EACH ROW EXECUTE PROCEDURE before_user_insert();
I'm writing an online videogame Database in SQL using ORACLE for an accademic project, and i'm trying to create a trigger that for every user that submit their information in my ACCCOUNT TABLE
CREATE TABLE ACCOUNT (
USERNAME VARCHAR(20),
PASSWORD VARCHAR(20) NOT NULL,
NATIONALITY VARCHAR(15),
CREATION DATE DATE,
EMAIL_ACCOUNT VARCHAR(35) NOT NULL,
CONSTRAINT KEYACCOUNT PRIMARY KEY(USERNAME),
CONSTRAINT NO_USER_CSPEC CHECK(REGEXP_LIKE(USERNAME, '^[a-zA-Z0-9._]+$') AND USERNAME NOT LIKE '% %'),
CONSTRAINT NO_EASY_PASS CHECK(REGEXP_LIKE(PASSWORD, '^[a-zA-Z0-9._!#£$%&/()=?]') AND PASSWORD NOT LIKE '% %'),
CONSTRAINT LENGHTUSER CHECK(LENGTH(USERNAME)>3),
CONSTRAINT LENGHTPASS CHECK(LENGTH(PASSWORD)>5),
CONSTRAINT FK_EMAIL FOREIGN KEY(EMAIL_ACCOUNT) REFERENCES PERSONA(EMAIL) ON DELETE CASCADE
);
Will fire a trigger that will create a new user with the new username and password just inserted.
this is the code i tried to wrote
CREATE OR REPLACE TRIGGER NEW_USER
AFTER INSERT ON ACCOUNT
FOR EACH ROW
BEGIN
CREATE USER :NEW.USERNAME IDENTIFIED BY :NEW.PASSWORD;
GRANT ROLE_NAME TO :NEW.USERNAME
END;
Why i'm tyring to do this ?
Basically because i'd like to give specific view on specific row that regards only the specific user. ( imagine if, while managing your account you can access to every other row stored in the table ACCOUNT )
After creating that specific user i can create some procedure that have in input the username ( of a successfully created user ) and give back the view on that specific row.
is there a way to do this ?
At first, you can't use DDL statement in trigger body as a open source, you should put it in execute immediate command. And also you should pay attention to user privileges which will execute then trigger, and role which will be granted to user, are there all priveleges granted, for create session, execute statements and so on. But if I were you I'll put user opening process in separate procedure, I think it won't be so simple code, so it will be easy to edit package procedure.
You can create context for you user sessions, wrap all your table where you want to control access into views and then filter view by user context.
For example you table TAB_A with many rows, in table you store column ACS_USER and wrap table to V_TAB_A , when you can control access to table via view, all user access object will use views like
select * from V_TAB_A where ACSUSER = SYS_CONTEXT('USERENV','SESSION_USER')
The main problem I see here is grant to create user. You probably don't want your schema to be able to create users. So trigger (of course as other answers states this need to be execute immediate) shouldn't directly call create user. I would create procedure that create user in other schema than your working schema. That external schema would have grants to create user and your schema would have only grant to execute that one procedure from strong priviledged schema. In that case trigger will only call single procedure from external schema.
So to recap:
CREATE OR REPLACE TRIGGER your_schema.NEW_USER
AFTER INSERT ON ACCOUNT
FOR EACH ROW
BEGIN
STRONG.CREATE_USER(:NEW.PASSWORD,:NEW.USERNAME);
END;
CREATE OR REPLACE PROCEDURE STRONG.CREATE_USER(PASS VARCHAR2, USERNAME VARCHAR2) AS
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
execute immediate 'CREATE USER ' || USERNAME || ' IDENTIFIED BY ' || PASS;
execute immediate 'GRANT ROLE_NAME, CONNECT, RESOURCE TO ' || USERNAME; --and whatever user needs more
END;
Where STRONG user have rights to create user and your_schema has grant to execute STRONG.CREATE_USER
Additional thing. Never store passwords in plain text. Use some hash.
Alternatively, you can use a database stored procedure instead of a trigger to do the DDL operations.
This is a pseudo code, make necessary changes as per your requirement. You can build your logic on top of this and if you are stuck, always post a question here in SO.
Table
CREATE TABLE account_info
(
user_name VARCHAR (20),
user_password VARCHAR (20) NOT NULL
);
Procedure
CREATE OR REPLACE PROCEDURE test_procedure (
user_name IN account_info.user_name%TYPE,
user_password IN account_info.user_password%TYPE)
AS
BEGIN
INSERT INTO account_info (user_name, user_password)
VALUES ('ABC', 'password123');
-- check the user exists or not, if yes proceed
EXECUTE IMMEDIATE
'CREATE USER ' || user_name || ' IDENTIFIED BY ' || user_password;
-- do the rest of the activities such as grant roles, privilges etc.
END;
/
I need to set the search_path before a query. The new search path should be based on a function parameter. How exactly can I Do it? Right now I have:
CREATE FUNCTION get_sections(integer) RETURNS
table(id integer, name varchar, type varchar) as $$
SET search_path to $1, public;
select id, name, type from sections;
$$ language 'sql';
But it simply won't accept $1. I also tried with quote_ident($1) but it did not work.
Thanks!
Generic'ish solution
I created a pure sql function by using set_config().
This solution supports setting multiple schemas in a comma separated string. By default
the change applies to the current session. Setting the "is_local" parameter to true makes the change only apply to the current transaction, see http://www.postgresql.org/docs/9.4/static/functions-admin.html for more details.
CREATE OR REPLACE FUNCTION public.set_search_path(path TEXT, is_local BOOLEAN DEFAULT false) RETURNS TEXT AS $$
SELECT set_config('search_path', regexp_replace(path, '[^\w ,]', '', 'g'), is_local);
$$ LANGUAGE sql;
Since we're not running any dynamic sql there should be less chance of sql injection. Just to be sure I added some naive sanitizing of the text by removing all characters except alphanumerics,space and comma. Escaping/quoting the string was not trivial, but I'm not an expert, so.. =)
Remember that there is no feedback if you set a malformed path.
Here is some sample code for testing:
DROP SCHEMA IF EXISTS testschema CASCADE;
CREATE SCHEMA testschema;
CREATE TABLE testschema.mytable ( id INTEGER );
SELECT set_search_path('testschema, public');
SHOW search_path;
INSERT INTO mytable VALUES(123);
SELECT * FROM mytable;
A test based on OP's original code
Since we don't know the schema for mytable in advance, we need to use dynamic sql. I embedded the set_config-oneliner into the get_sections()-function instead of using the generic'ish function.
Note: I had to set is_local=false in set_config() for this to work. That means the modified path remains after the function is run. I'm not sure why.
DROP SCHEMA IF EXISTS testschema CASCADE;
CREATE SCHEMA testschema;
SET search_path TO public;
CREATE TABLE testschema.mytable ( id INTEGER, name varchar, type varchar );
INSERT INTO testschema.mytable VALUES (123,'name', 'some-type');
INSERT INTO testschema.mytable VALUES (567,'name2', 'beer');
CREATE OR REPLACE FUNCTION get_sections(schema_name TEXT) RETURNS
TABLE(id integer, name varchar, type varchar) AS $$
BEGIN
PERFORM set_config('search_path', regexp_replace(schema_name||', public', '[^\w ,]', '', 'g'), true);
EXECUTE 'SELECT id, name, type FROM mytable';
END;
$$ LANGUAGE plpgsql;
SET search_path TO public;
SELECT * FROM get_sections('testschema');
SHOW search_path; -- Unfortunately this has modified the search_path for the whole session.
Below code will work
CREATE FUNCTION get_sections(num integer) RETURNS
table(id integer, name varchar, type varchar) as $$
EXECUTE FORMAT('SET search_path to %L::INTEGER, public;'num);
select id, name, type from sections;
$$ language 'sql';
I have problem with importing documents to postgres db. I have plpgsql function, simplier version could look like that:
create function add_file(flag integer, sth varchar) returns void as
begin
if flag = 1 then
insert into tab_one values (my_file_oid, sth);
else
insert into tab_two values (my_file_oid, sth);
end if;
end;
And psql command:
\lo_import('path/to/file');
Both code in one file. I cant put lo_import() to insert statement, becouse I need client-site lo_import. There is variable LASTOID, but it is not avaible in add_file function. And it wouldnt be updating on every call add_file().
So, how can I put oid to database with, in our example, 'flag' and 'sth' by insert statement and everything in function with arguments? File is in client computer.
psql's \lo_import returns the OID resulting from the import. You need to hand that in as parameter to the function, which could look like this:
CREATE FUNCTION add_file(_flag integer, _sth varchar, _oid oid)
RETURNS void LANGUAGE plpgsql AS
BEGIN
IF _flag = 1 THEN
INSERT INTO tab_one(file_oid, sth) VALUES (_oid, _sth);
ELSE
INSERT INTO tab_two(file_oid, sth) VALUES (_oid, _sth);
END IF;
END;
As an aside: always add a column list to your table with an INSERT command (except for ad-hoc calls, maybe).
From within a plpgsql function you can make use to the also provided server side functions. Could look like this:
INSERT INTO tab_one(file_oid, sth) VALUES (lo_import('/etc/motd'), _sth);
Note that this operates within the file system of the database server with the privileges of the owner (usually system user postgres). Therefore, use is restricted to superusers.