Oracle: grant to role if role exists - sql

How to execute
GRANT SELECT ON <ownschema>.<sometable> TO <somerole>;
but backing off gracefully if somerole does not exist. The user executing the statement is a standard user (think SCOTT) without any special privileges.
Version: Oracle Database 19 or later

I don't think you can.
If you're running it at SQL level, then Oracle will raise an error if role doesn't exist.
If you want to check whether it exists, you'll need some kind of a PL/SQL procedure (which is not a problem), but - DBA should grant you select privilege on dba_roles so that you could check it. Then, if it exists, you'd grant that privilege; otherwise, return some kind of an information (dbms_output.put_line in my example is pretty much stupid; it wouldn't be visible out of SQL*Plus, SQL Developer, TOAD and similar), but you got the idea, I hope.
Something like this:
create or replace procedure p_grant_to_role (par_role in varchar2) is
l_cnt number;
begin
select count(*)
into l_cnt
from dba_roles
where role_name = par_role;
if l_cnt > 0 then
execute immediate 'grant select on emp to ' || par_role;
else
dbms_output.put_line('Role does not exist');
end if;
end;
/

It all depends on the tool, but you can do something like this (very crude as usually you should have better exception handling):
begin
execute immediate 'grant .....';
exception
when others then null;
end;

Related

Oracle Procedure with an IF-THEN-ELSE produces error

I have created this procedure in Oracle, to assign a role to a user based on the grade stored in the grade column of the marketing table. However, when I run it I get errors.
Initial Problem
CREATE OR REPLACE PROCEDURE proc_assign_role IS
vn_grade NUMBER(5);
CURSOR cur_user_grade IS
SELECT grade, username
FROM marketing
WHERE grade BETWEEN 1 AND 3;
BEGIN
FOR rec_cur_user_grade IN cur_user_grade
vn_grade:=
IF grade= 1
THEN
GRANT ROLE admin_staff;
ELSIF grade= 2 THEN
GRANT ROLE marketing_staff;
ELSIF grade= 3 THEN
GRANT ROLE event_staff;
END IF;
DBMS_OUTPUT.PUT_LINE(username||'YOU ARE A GRADE '||vn_grade|| 'USER');
END proc_assign_role;
/
This is the error I get:
ERROR at line 11: PLS-00103: Encountered the symbol "VN_GRADE" when expecting one of the following:
. ( * # % & - + / at loop mod remainder range rem ..
|| multiset
1. CREATE OR REPLACE PROCEDURE proc_assign_role IS
2. vn_grade NUMBER(5);
vn_grade:=
You need to assign a value to that line, or get rid of it. You can't assign an IF statement to a number variable. Probably get rid of it, then change your IF statement to look at the grade from the cursor. You also need to end your loop.
Additionally, you can't do a grant directly within a PL/SQL code block. You have to use the execute immediate statement for that. And you have to tell it who you're granting the role to.
FOR rec_cur_user_grade IN cur_user_grade LOOP
IF rec_cur_user_grade.grade= 1 THEN
execute immediate 'GRANT ROLE admin_staff to ' || rec_cur_user_grade.username;
ELSIF rec_cur_user_grade.grade= 2 THEN
execute immediate 'GRANT ROLE marketing_staff to ' || rec_cur_user_grade.username;
ELSIF rec_cur_user_grade.grade= 3 THEN
execute immediate 'GRANT ROLE event_staff to ' || rec_cur_user_grade.username;
END IF;
DBMS_OUTPUT.PUT_LINE(username||'YOU ARE A GRADE '||rec_cur_user_grade.grade|| 'USER');
END LOOP;
I'm seeing a few things that would keep this from working:
After your FOR statement, there's no LOOP statement (which is what the error is complaining about). There's also no END LOOP after your DBMS_OUTPUT.
vn_grade is followed by the := assignment operator, but nothing is being assigned to it.
The GRANT statements are written as bare DDL, which isn't allowed in PL/SQL. They need to be wrapped in EXECUTE IMMEDIATE.
grade and username need to be qualified by the cursor variable (e.g., rec_cur_user_grade.grade and rec_cur_user_grade.username).
Try something like this (which runs as an anonymous block, rather than a procedure, and uses an implicit cursor):
BEGIN
FOR rec_cur_user_grade IN (
SELECT grade, username
FROM marketing
WHERE grade BETWEEN 1 AND 3
)
LOOP
CASE rec_cur_user_grade.grade
WHEN 1 THEN
EXECUTE IMMEDIATE 'GRANT ROLE admin_staff TO ' || rec_cur_user_grade.username;
WHEN 2 THEN
EXECUTE IMMEDIATE 'GRANT ROLE marketing_staff TO ' || rec_cur_user_grade.username;
WHEN 3 THEN
EXECUTE IMMEDIATE 'GRANT ROLE event_staff TO ' || rec_cur_user_grade.username;
END CASE;
DMBS_OUTPUT.PUT_LINE(rec_cur_user_grade.username || ' YOU ARE A GRADE ' || rec_cur_user_grade.grade || ' USER');
END LOOP;
END;
/
grant is DDL and therefore cannot be used in PL/SQL directly. In order to accomplish this, the DDL needs to be executed dynamically, using execute immediately. Additionally, grant always requires you to specify the recipient of the role. The result would be something like this:
execute immediate 'GRANT ROLE admin_staff to ' || rec_cur_user_grade.username;
An ORA-00990: missing or invalid privilege error is fairly self-descriptive: the owner of the procedure does not have the necessary privileges to take the actions being attempted by the procedure.
The most likely culprit here is roles: permissions granted by a role cannot be used in a procedure. The first step you should take is to make sure that the owner of the procedure has been explicitly granted permission to administer the roles involved.

Detect role in Postgresql dynamically

I have been trying to create a script that detects that a role already excists and if it does it should revoke all privileges. This works fine doing it like this:
DO $$DECLARE count int;
BEGIN
SELECT count(*) INTO count FROM pg_roles WHERE rolname = 'superman';
IF count > 0 THEN
REVOKE ALL PRIVILEGES ON TABLE FROM superman;
END IF;
END$$;
But now I want this to be dynamic per environment since I will be using different role names per environment. So I tried to use the \set mechanism but that doesn't seem to work when using pl/sql so if I would do something like the following Postgresql is complaining with syntax errors:
/set environment _int
DO $$DECLARE count int;
BEGIN
SELECT count(*) INTO count FROM pg_roles WHERE rolname = 'superman';
IF count > 0 THEN
REVOKE ALL PRIVILEGES ON TABLE FROM superman:environment;
END IF;
END$$;
Although if I would not do it in pl/sql the revoke statment works just fine. So my question is how can I make my script dynamic by passing parameters to it so they will be replaced?
You have to use EXECUTE for dynamic SQL. Also, a DO statement cannot take parameters. Create a plpgsql function:
CREATE OR REPLACE FUNCTION f_revoke_all_from_role(_role text)
RETURNS void AS
$BODY$
BEGIN
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = _role) THEN
EXECUTE 'REVOKE ALL PRIVILEGES ON TABLE x FROM ' || quote_ident(_role);
END IF;
END;
$BODY$ LANGUAGE plpgsql;
Call:
SELECT f_revoke_all_from_role('superman');
IF block is simpler with EXISTS.
I use quote_ident() to avoid SQLi.
The table name could be the second parameter of the function ...

PL/SQL: bind variable does not exist

How to modify this procedure to let it use bind variables
PROCEDURE KILL(user IN VARCHAR2) AS
BEGIN
FOR REC IN (SELECT sid,serial# serial FROM V$SESSION WHERE username = user)
LOOP
execute immediate 'alter system kill session '' :1 , :2 '' immediate'
using rec.sid, rec.serial;
END LOOP;
END;
It gives:
bind variable does not exist
The bind variables in your statement are being treated as literal strings rather than place holders. If you output the statement you're generating:
BEGIN
FOR REC IN (SELECT sid,serial# serial FROM V$SESSION WHERE username = user)
LOOP
dbms_output.put_line('alter system kill session '':1,:2'' immediate');
END LOOP;
END;
/
... you see lines like:
alter system kill session ':1,:2' immediate
The ':1,:2' is treated as a static value and not as two bind variables. You can't use bind variables in dynamic DDL, and I'm not sure if that applies to alter commands, so this may be impossible anyway.
The simplest way to achieve this may be to generate the whole statement in the cursor:
BEGIN
FOR REC IN (
SELECT 'alter system kill session ''' || sid ||','|| serial#
||''' immediate' stmt
FROM V$SESSION WHERE username = user)
LOOP
dbms_output.put_line(rec.stmt);
--execute immediate rec.stmt;
END LOOP;
END;
/
With the execute commented out (I don't really want to kill my sessions just now) you can just see the commands it will run, like:
alter system kill session '58,47157' immediate
Your approach may still be flawed though as it will kill the session that is executing the block, and it may or may not kill it last. I think this is in the realms of undefined behaviour, and I don't really want to try it to find out what happens... I doubt that's what you actually want anyway.
Edit: 'flawed' comment was based on using user, which in my anonymous block would be the executing user; in your proc it would be the user from the parameter. Using a keyword as a parameter name is confusing though, so I'd recommend changing the name to something like p_user, in the args and the statement.
I believe this might work
PROCEDURE KILL(user IN VARCHAR2) AS
BEGIN
FOR REC IN (SELECT sid,serial# serial FROM V$SESSION WHERE username = user)
LOOP
execute immediate 'alter system kill session :1 '||','|| ':2 immediate'
using rec.sid, rec.serial;
END LOOP;
END;

Create or replace role?

How do you create or replace a role (that might or might not exist) in Oracle? For example, the following does not work:
CREATE OR REPLACE ROLE role_name;
GRANT SELECT ON SCM1_VIEW_OBJECT_VW TO role_name;
Any way to do this without PL/SQL?
Solution
A combination of the given answers and a pragma control accomplishes this task for Oracle 10g.
CREATE OR REPLACE PROCEDURE create_role( role_name IN VARCHAR2 ) IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
EXECUTE IMMEDIATE 'CREATE ROLE '||role_name;
EXCEPTION
WHEN OTHERS THEN
-- ORA-01921: If The role name exists, ignore the error.
IF SQLCODE <> -01921 THEN
RAISE;
END IF;
END create_role;
Test
This sequence works:
DROP ROLE role_name;
CREATE ROLE role_name;
CALL create_role( 'role_name' );
CALL create_role( 'role_name' );
The final create role statement fails, as expected:
DROP ROLE role_name;
CALL create_role( 'role_name' );
CREATE ROLE role_name;
Best practice is to attempt the creation of the role, then handle the appropriate exception gracefully if it occurs; this means you don't need to run potentially expensive data dictionary queries:
begin
execute immediate 'create role role_name';
exception
when others then
--"ORA-01921: role name 'x' conflicts with another user or role name"
if sqlcode = -01921 then
null;
else
raise;
end if;
end;
And yes, you need PL/SQL to do this - it's the best tool for this job, anyway.
DECLARE
v_dummy NUMBER;
BEGIN
SELECT 1
INTO v_dummy
FROM dba_roles
WHERE role = 'MY_ROLE_NAME';
EXCEPTION
WHEN no_data_found THEN
EXECUTE IMMEDIATE 'CREATE ROLE my_role_name';
END;
/
There is no syntax for "create or replace" for roles. Not sure of your version of Oracle but this hasn't changed much that I can recall. http://download.oracle.com/docs/cd/B19306_01/server.102/b14200/statements_6012.htm
You can grant select to the role multiple times and it will accept the grant every time provided the role exists.
You could do an anonymous block and ignore the execption if the role already exists or something else where you see if the role exists by querying DBA_ROLES.

Execute Immediate within a stored procedure keeps giving insufficient priviliges error

Here is the definition of the stored procedure:
CREATE OR REPLACE PROCEDURE usp_dropTable(schema VARCHAR, tblToDrop VARCHAR) IS
BEGIN
DECLARE v_cnt NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_cnt
FROM all_tables
WHERE owner = schema
AND table_name = tblToDrop;
IF v_cnt > 0 THEN
EXECUTE IMMEDIATE('DROP TABLE someschema.some_table PURGE');
END IF;
END;
END;
Here is the call:
CALL usp_dropTable('SOMESCHEMA', 'SOME_TABLE');
For some reason, I keep getting insufficient privileges error for the EXECUTE IMMEDIATE command. I looked online and found out that the insufficient privileges error usually means the oracle user account does not have privileges for the command used in the query that is passes, which in this case is DROP. However, I have drop privileges. I am really confused and I can't seem to find a solution that works for me.
Thanks to you in advance.
SOLUTION:
As Steve mentioned below, Oracle security model is weird in that it needs to know explicitly somewhere in the procedure what kind of privileges to use. The way to let Oracle know that is to use AUTHID keyword in the CREATE OR REPLACE statement. If you want the same level of privileges as the creator of the procedure, you use AUTHID DEFINER. If you want Oracle to use the privileges of the user currently running the stored procedure, you want to use AUTHID CURRENT_USER. The procedure declaration looks as follows:
CREATE OR REPLACE PROCEDURE usp_dropTable(schema VARCHAR, tblToDrop VARCHAR)
AUTHID CURRENT_USER IS
BEGIN
DECLARE v_cnt NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_cnt
FROM all_tables
WHERE owner = schema
AND table_name = tblToDrop;
IF v_cnt > 0 THEN
EXECUTE IMMEDIATE('DROP TABLE someschema.some_table PURGE');
END IF;
END;
END;
Thank you everyone for responding. This was definitely very annoying problem to get to the solution.
Oracle's security model is such that when executing dynamic SQL using Execute Immediate (inside the context of a PL/SQL block or procedure), the user does not have privileges to objects or commands that are granted via role membership. Your user likely has "DBA" role or something similar. You must explicitly grant "drop table" permissions to this user. The same would apply if you were trying to select from tables in another schema (such as sys or system) - you would need to grant explicit SELECT privileges on that table to this user.
You should use this example with AUTHID CURRENT_USER :
CREATE OR REPLACE PROCEDURE Create_sequence_for_tab (VAR_TAB_NAME IN VARCHAR2)
AUTHID CURRENT_USER
IS
SEQ_NAME VARCHAR2 (100);
FINAL_QUERY VARCHAR2 (100);
COUNT_NUMBER NUMBER := 0;
cur_id NUMBER;
BEGIN
SEQ_NAME := 'SEQ_' || VAR_TAB_NAME;
SELECT COUNT (*)
INTO COUNT_NUMBER
FROM USER_SEQUENCES
WHERE SEQUENCE_NAME = SEQ_NAME;
DBMS_OUTPUT.PUT_LINE (SEQ_NAME || '>' || COUNT_NUMBER);
IF COUNT_NUMBER = 0
THEN
--DBMS_OUTPUT.PUT_LINE('DROP SEQUENCE ' || SEQ_NAME);
-- EXECUTE IMMEDIATE 'DROP SEQUENCE ' || SEQ_NAME;
-- ELSE
SELECT 'CREATE SEQUENCE COMPTABILITE.' || SEQ_NAME || ' START WITH ' || ROUND (DBMS_RANDOM.VALUE (100000000000, 999999999999), 0) || ' INCREMENT BY 1'
INTO FINAL_QUERY
FROM DUAL;
DBMS_OUTPUT.PUT_LINE (FINAL_QUERY);
cur_id := DBMS_SQL.OPEN_CURSOR;
DBMS_SQL.parse (cur_id, FINAL_QUERY, DBMS_SQL.v7);
DBMS_SQL.CLOSE_CURSOR (cur_id);
-- EXECUTE IMMEDIATE FINAL_QUERY;
END IF;
COMMIT;
END;
/
you could use "AUTHID CURRENT_USER" in body of your procedure definition for your requirements.
Alternatively you can grant the user DROP_ANY_TABLE privilege if need be and the procedure will run as is without the need for any alteration. Dangerous maybe but depends what you're doing :)