Oracle SQL: Trigger to add grants after creating view - sql

I am working with vendor application that uses Oracle database. To save the content it uses database tables, which are queried by using views. I do not have any control over that code.
Because of security I gave access to those views to special reporting user, which can only select entries from it.
Whenever some major change is made in the application it drops the appropriate view and creates it anew. Of course all grants are lost and since the changes are made rarely it is easy to forget to backup up and restore them afterwards.
I consulted DBA and he suggested to write a trigger to save grants in temporary table, after which the entries can be used to restore grants. Saving part works fine as expected:
create or replace TRIGGER RECORD_GRANTS_ONDROP
BEFORE DROP ON MYUSER.SCHEMA
BEGIN
IF ora_dict_obj_owner = 'MYUSER' and ora_dict_obj_name not like 'TEMP_PRIV%' and ora_dict_obj_type='VIEW' then
EXECUTE IMMEDIATE 'CREATE TABLE TEMP_PRIV AS SELECT ''GRANT '' || PRIVILEGE || '' ON MYUSER.'' || TABLE_NAME || '' TO '' || GRANTEE PRIVILEGE_x FROM USER_TAB_PRIVS WHERE GRANTEE not in (''MYUSER'',''PUBLIC'') AND TABLE_NAME=''' || ora_dict_obj_name || '''';
ELSE null;
END IF;
END;
As a result I get a table with all grants assigned to the said view.
For restoring I wanted to run similar trigger:
create or replace TRIGGER RESTORE_GRANTS_AFTERCREATE
AFTER CREATE ON MYUSER.SCHEMA
BEGIN
IF ora_dict_obj_owner = 'MYUSER' and ora_dict_obj_type='VIEW' then
FOR loop_counter IN (select '''' || privilege_x || '''' AS privilege_x from temp_priv)
LOOP
EXECUTE IMMEDIATE loop_counter.privilege_x;
DBMS_OUTPUT.PUT_LINE(loop_counter.privilege_x);
END LOOP;
ELSE null;
END IF;
NULL;
END;
I will note here that is just basic test of concept without any proper checks, so just focus on the big issue here.
When I try to create a view now I get an error:
Error report -
ORA-00604: error occurred at recursive SQL level 1
ORA-00900: invalid SQL statement
ORA-06512: at line 5
00604. 00000 - "error occurred at recursive SQL level %s"
*Cause: An error occurred while processing a recursive SQL statement
(a statement applying to internal dictionary tables).
*Action: If the situation described in the next error on the stack
can be corrected, do so; otherwise contact Oracle Support.
This can only mean that the view is not created at the time when trigger tries to add grants. Syntax wise I successfully ran the command:
BEGIN
EXECUTE IMMEDIATE 'GRANT SELECT ON MYUSER.CR_STATUS_TABLE TO SOMEUSER';
END
But when I try to run it using for, or just by itself in 'after create' trigger I get the same error.
Does anyone knows how to approach this, and I would like to avoid jobs at all costs if possible.

Ignoring whether this is a good idea... you're getting that error because you're overthinking your string manipulation. What you're putting into your table looks OK. The problem is when you get it back out. The value in the table is already a string, so you don't need to then enclose it in another set of quotes.
What you're actually running is the equivalent of:
EXECUTE IMMEDIATE '''GRANT SELECT ON MYUSER.CR_STATUS_TABLE TO SOMEUSER''';
which will also throw "ORA-00900: invalid SQL statement", rather than your standalone, working, version:
EXECUTE IMMEDIATE 'GRANT SELECT ON MYUSER.CR_STATUS_TABLE TO SOMEUSER';
If you swapped the order of your EXECUTE IMMEDIATE and DBMS_OUTPUT calls you'd see the problem statement before it ran, which would be more helpful - you'd see those quotes as part of the string.
So in your second trigger, instead of doing:
FOR loop_counter IN (select '''' || privilege_x || '''' AS privilege_x from temp_priv)
LOOP
EXECUTE IMMEDIATE loop_counter.privilege_x;
DBMS_OUTPUT.PUT_LINE(loop_counter.privilege_x);
END LOOP;
just do:
FOR loop_counter IN (select privilege_x from temp_priv)
LOOP
DBMS_OUTPUT.PUT_LINE(loop_counter.privilege_x);
EXECUTE IMMEDIATE loop_counter.privilege_x;
END LOOP;
However, this still won't work; it will now get ORA-30511: invalid DDL operation in system triggers. This is presumably because of the restrictions shown in the documentation:
Trigger cannot do DDL operations on object that caused event to be generated.
DDL on other objects is limited to compiling an object, creating a trigger, and creating, altering, and dropping a table.
You said "it is easy to forget to backup up and restore them afterwards" but you're going to have to put a robust process around your upgrades to make sure that does happen.
You can change your process to have a separate step at the end of every upgrade that is always run, which executes all of those stored statements for all objects - maybe skipping or ignoring errors from anything that wasn't recreated - and then drops the temp_priv table.
But you don't really want to (try to) create that temporary table in a trigger anyway - if two views are dropped, the first creates it, the second fails because it already exists. A perhaps more realistic approach might be to create that table once now:
create table TEMP_PRIV (PRIVILEGE_X VARCHAR2(4000));
and then utilise it for all subsequent upgrades, either by populating it with all grants for all views as a single step before the upgrade starts:
INSERT INTO TEMP_PRIVS (PRIVILEGE_X)
SELECT 'GRANT ' || PRIVILEGE || ' ON MYUSER.' || TABLE_NAME || ' TO ' || GRANTEE
FROM USER_VIEWS UV
JOIN USER_TAB_PRIVS UTP ON UTP.TABLE_NAME = UV.VIEW_NAME
WHERE UTP.GRANTEE not in ('MYUSER','PUBLIC');
or if you're still worried about possibly forgetting that step then with a trigger to do it one view at a time as they are dropped:
create or replace TRIGGER RECORD_GRANTS_ONDROP
BEFORE DROP ON MYUSER.SCHEMA
BEGIN
IF ora_dict_obj_owner = 'MYUSER' and ora_dict_obj_name not like 'TEMP_PRIV%' and ora_dict_obj_type='VIEW' then
INSERT INTO TEMP_PRIV
SELECT 'GRANT ' || PRIVILEGE || ' ON MYUSER.' || TABLE_NAME || ' TO ' || GRANTEE
FROM USER_TAB_PRIVS
WHERE GRANTEE not in ('MYUSER','PUBLIC')
AND TABLE_NAME = ora_dict_obj_name;
END IF;
END;
/
Then at the end of the upgrade process reissue all the statements in the table and clear it down ready for next time:
DECLARE
missing_view EXCEPTION;
PRAGMA EXCEPTION_INIT(missing_view, -942);
BEGIN
FOR loop_counter IN (select privilege_x from temp_priv)
LOOP
BEGIN
DBMS_OUTPUT.PUT_LINE(loop_counter.privilege_x);
EXECUTE IMMEDIATE loop_counter.privilege_x;
EXCEPTION
WHEN missing_view THEN
-- report but otherwise ignore
DBMS_OUTPUT.PUT_LINE(SQLERRM);
END;
END LOOP;
END;
/
TRUNCATE TABLE temp_priv;
If you go with the simpler non-trigger approach then it will re-grant existing privileges, but that's OK. And the exception handler means it'll report but skip any views that were dropped and not recreated, if that ever happens. (You'll still have to deal with any new view of course; your after-create trigger wouldn't have helped with that anyway.) And note that I've truncated the table, rather than dropped it - so it's still there, empty, when the next upgrade comes around and wants to populate it.

Related

How to execute alter command within select statement in a loop in Oracle?

I am trying to Rebuild Indexes of a schema through a script but I am stucked at a point where I get the string ALTER INDEX OWNER.INDEX_NAME REBUILD NOLOGGING through select statement but I am not getting how to execute the alter command ,please guide :
I tried to assign str the value of select query used in 2nd for loop and then execute it but it gave error .
IS
STR VARCHAR2(5000);
BEGIN
FOR T IN (
SELECT USERNAME FROM DBA_USERS WHERE USERNAME ='REPORT'
)
LOOP
FOR CUR IN
(
SELECT ' ALTER INDEX '||OWNER||'.'||INDEX_NAME|| ' REBUILD NOLOGGING; ' FROM DBA_INDEXES
WHERE OWNER=T.USERNAME AND TEMPORARY='N'
)
LOOP
--- EXECUTE IMMEDIATE STR ;
INSERT INTO INDEX_REBUILD_HISTORY
SELECT DISTINCT OWNER, TRUNC(LAST_DDL_TIME) from DBA_OBJECTS where OBJECT_TYPE = 'INDEX'
AND
OWNER=T.USERNAME ;
COMMIT;
END LOOP;
END LOOP;
END ;
You use dynamic sql. And you don't need your outer loop. The filter on that is available in dba_indexes:
create procedure bld_idx
is
vsql varchar2(500);
for x in (select owner,
index_name
from dba_indexes
where owner = 'REPORT'
and TEMPORARY='N'
)
loop
vsql := ' ALTER INDEX '||x.OWNER||'.'||x.INDEX_NAME|| ' REBUILD NOLOGGING; ';
dbms_output.put_line(vsql); -- debugging only
execute immediate vsql;
end loop;
end;
Note 1: above is off the top of my head. There may be minor syntax issues, but if so you should be able to work them out.
Not 2: Rebuilding indexes is not something that needs to be done in the normal course of things. Richard Foote is probably the foremost authority on the internals of oracle indexes, and he has this to say: https://richardfoote.wordpress.com/2007/12/11/index-internals-rebuilding-the-truth/
"it gave error ." isn't helpful without the actual error you received. That said you've made the same mistake so many others do, you shouldn't include the ";" as part of your dynamic SQL - it's not part of the statement, it's only used by your client to know when to send code to the database.
FOR CUR IN
(
SELECT ' ALTER INDEX '||OWNER||'.'||INDEX_NAME|| ' REBUILD NOLOGGING' ddl_cmd FROM DBA_INDEXES
WHERE OWNER=T.USERNAME AND TEMPORARY='N'
)
...
EXECUTE IMMEDIATE CUR.ddl_cmd ;
(I've also given the column an alias so you can use it in your loop nicely.
Then
INSERT INTO INDEX_REBUILD_HISTORY
SELECT DISTINCT OWNER, TRUNC(LAST_DDL_TIME) from DBA_OBJECTS where OBJECT_TYPE = 'INDEX'
AND
OWNER=T.USERNAME ;
Is not filtering on the index you just rebuilt, it doesn't seem like it's going to get entirely useful information.
That said...
Is it really worth rebuilding all your indexes offline and making them unrecoverable? Probably not, if you're doing this more than once and are benefiting then there's probably something that could be changed with your data model to help. Have a good read of this presentation by Richard Foote, a well established Oracle Indexing Expert https://richardfoote.files.wordpress.com/2007/12/index-internals-rebuilding-the-truth.pdf I doubt you'd come away from it believing that rebuilding all the indexes is a solution.

If I pass a where clause as a parameter will that prevent SQL Injection?

I created an Oracle proc where I create a dynamic sql statement based on the parameters supplied to the proc.
I've done some testing and it appears that I can't perform sql injection.
Is there anything additional I should be safe guarding against?
SELECT 'UPDATE ' || p_table || ' SET MY_FIELD = ''' || p_Value || ''' ' || p_Where
INTO query_string
FROM DUAL;
EDIT:
Scenarios that I've tried.
1. WHERE SOME_VAL IN ('AAA','BBB') - This works
2. WHERE SOME_VAL IN ('AAA','BBB') OR SOME_VAL2 = '123' - This works.
3. WHERE SOME_VAL IN ('AAA','BBB'); DROP TABLE TEST_TABLE; - This errors out.
4. WHERE SOME_VAL IN ('AAA','BBB') OR (DELETE FROM TEST_TABLE) - This errors out.
It depends on how and by whom your procedure is being invoked. Usually you need to worry about SQL injection for something that is open to large number of users in production. And that should not be the case for any database procedure. If your database procedure is accessible by large number of users, then you have potential for malicious use by someone.
In your case, you can mitigate this risk by creating mapping of parameters to hide actual schema object names and some validation.
For example change parameter p_table to table_name as input parameter. Then using case statement map to actual table name. I am giving you example of table name here because you should really restrict who can access which table from db.
CREATE OR REPLACE PROCEDURE test_proc(table_name IN VARCHAR)
IS
p_table varchar2(100);
BEGIN
CASE table_name
WHEN 'A' THEN p_table:='db_table_a';
WHEN 'B' THEN p_table:='db_table_b';
ELSE RAISE 'Invalid table name parameter';
END CASE;
SELECT 'UPDATE ' || p_table || ' SET MY_FIELD = ''' || p_Value || ''' '
|| p_Where
INTO query_string
FROM DUAL;
END;
You should do similar mapping and validation for other parameters too.
SQL injection always opens Pandora's box.
You should always assume a user can break out of a dynamic SQL statement. With full SQL access you should then assume a user can find a way to escalate privileges and own your database. (Depending on how paranoid you are, it might be safe to assume privilege escalation is impossible as long as your database and schemas are constantly patched and thoroughly hardened. In practice the vast majority of Oracle databases are not sufficiently patched and hardened.)
Below are a few simple examples that should scare you. And you should also assume that there are many hackers who are more clever than I am and have better attacks.
Sample Schema
First let's create a simple table with some data for a realistic test.
drop table test1;
create table test1(my_field varchar2(100), some_val varchar2(100));
insert into test1 values('A', 'AAA');
commit;
Obviously Dangerous Function
Are all of the existing functions safe?
create or replace function dangerous_function return number is
pragma autonomous_transaction;
begin
delete from test1;
commit;
return 1;
end;
/
If not, what is stopping the user from calling it like this?
--Safe static part:
update test1
set my_field = 'b'
--Dangerous dynamic part:
where some_val IN ('AAA')
and 1 = (select dangerous_function from dual)
Luckily creating an autonomous function is unusual and you can probably check the code. But can you guarantee the application will not create one in the future?
Custom Function in SQL
Even if there are no objects a clever user can turn your UPDATE into other DML:
--Safe static part:
update --+ WITH_PLSQL
test1
set my_field = 'b'
--Dangerous dynamic part:
where some_val IN ('AAA')
and 1 = (
with function dangerous_function return number is
pragma autonomous_transaction;
begin
delete from test1;
commit;
return 1;
end;
select dangerous_function from dual
);
I did cheat a little, the above code only works for me with the --+ WITH_PLSQL hint. Without that hint the code throws the error ORA-32034: unsupported use of WITH clause. But that's only a version limitation that might be lifted in the future. Or there might be some clever way to work around it, sometimes hints can break out of their part of the query and reference other sections.
Why Risk It?
Maybe there is a safe way to do it. But why risk it? Everybody in the IT world understands SQL injection bugs now. If you mess up and cause an exploit there will be no sympathy for you.

Drop a user that is created with a script in Oracle DB with the (%)

I am trying to drop a user that is created with a sql script that is pass the user with an argument with a PowerShell script each time the script is run. Example: "ZKOracle01", ZKOracle02, and so on.
I was hoping I could get something like this to work at the beginning of the script to clean up the old users.
DROP USER LIKE ZKOracle%;
I know I could just do this at the beginning or end of my sql script,
DROP USER ZKOracle01;
but I need the user to remain in the DB for at least 6 hours.
You could write a bit of dynamic SQL
BEGIN
FOR u IN (SELECT *
FROM dba_users
WHERE username LIKE 'ZKOracle%')
LOOP
EXECUTE IMMEDIATE 'DROP USER ' || u.username;
END LOOP;
END;
I'd probably extend that to add a bit of logging, at least writing out the list of usernames that you're dropping, but that should do what you want. Remember as well that DROP USER is DDL so it implicitly commits before and after the statement-- you cannot rollback your changes.
To show an example of this working (the user I created was not case sensitive)
SQL> ed
Wrote file afiedt.buf
1 begin
2 for u in (select * from dba_users where username like 'ZKORACLE%')
3 loop
4 execute immediate 'drop user ' || u.username;
5 dbms_output.put_line( 'Dropped user ' || u.username );
6 end loop;
7* end;
SQL> /
Dropped user ZKORACLE01
PL/SQL procedure successfully completed.

Inserting a select statement into a table- ORA-06502

I have 6 TEST environments and 1 production environment. I create quite a few different reports as Oracle views, and need a way to sync these between environments.
I am trying to make a script that I can run, which will basically output a list of commands that I can copy and paste into my different environment to create the necessary views/public synonyms and privileges.
I have to put the resultant text into a database table as dbms_output.put_line has a certain limitation on how many characters it can show.
I have the following, but if I try to insert the data, I get ORA-06502: PL/SQL: numeric or value error. I am guessing this is probably got to do with character literals not being escaped and what not.
CREATE OR REPLACE PROCEDURE EXPORT_REPORTS AS
statements CLOB;
tmp_statement CLOB;
CURSOR all_views IS
SELECT
OWNER,
VIEW_NAME,
TEXT
FROM
ALL_VIEWS
WHERE
OWNER = 'PAS'
;
BEGIN
FOR v IN all_views LOOP
tmp_statement := 'CREATE OR REPLACE FORCE VIEW "' || v.OWNER || '"."' || v.VIEW_NAME || '" AS ' || CHR(13) || CHR(10) || v.TEXT;
statements := statements || tmp_statement;
END LOOP;
EXECUTE IMMEDIATE 'INSERT INTO VIEW_EXPORTS VALUES ('''|| statements || ''')';
END EXPORT_REPORTS;
Any idea what I can do to try and fix this?
If it is because some of the text in the statements variable contains single quotes, how can I escape this before inserting the data into a table?
This sounds like a job for Data Pump.
Oracle Data Pump technology enables very high-speed movement of data and metadata from one database to another.
http://docs.oracle.com/cd/B28359_01/server.111/b28319/dp_overview.htm
For getting database object DDL I would recommend using the DBMS_METADATA package.
http://docs.oracle.com/cd/B28359_01/appdev.111/b28419/d_metada.htm#BGBDJAHH
you cannot use CLOB as a datatype for the local variables. Use VARCHAR instead

Create table and call it from sql

I have a PL/SQL function which creates a new temporary table. For creating the table I use execute immediate. When I run my function in oracle sql developer everything is ok; the function creates the temp table without errors. But when U use SQL:
Select function_name from table_name
I get an exceptions:
ORA-14552: cannot perform a DDL, commit or rollback inside a query or DML
ORA-06512: at "SYSTEM.GET_USERS", line 10
14552. 00000 - "cannot perform a DDL, commit or rollback inside a query or DML "
*Cause: DDL operations like creation tables, views etc. and transaction
control statements such as commit/rollback cannot be performed
inside a query or a DML statement.
Update
Sorry, write from tablet PC and have problems with format text. My function:
CREATE OR REPLACE FUNCTION GET_USERS
(
USERID IN VARCHAR2
)
RETURN VARCHAR2
AS
request VARCHAR2(520) := 'CREATE GLOBAL TEMPORARY TABLE ';
BEGIN
request := request || 'temp_table_' || userid ||
'(user_name varchar2(50), user_id varchar2(20), is_administrator varchar2(5)') ||
' ON COMMIT PRESERVE ROWS';
EXECUTE IMMEDIATE (request);
RETURN 'true';
END GET_USERS;
The error is explicit:
ORA-14552: cannot perform a DDL, commit or rollback inside a query or DML
In Oracle, you can't commit inside a query. A likely explanation is that it would make no sense since a query in Oracle is atomic (either succeeds entirely or makes no change) and this couldn't work if you commit in the middle of a DML. For a select query, all rows must be returned from a single logical point-in-time and if you commit in the middle of a select you would have inconsistent results.
Since DDL in Oracle issue an implicit commit, you can't make DDL inside a query.
This should not be a problem in your case though: SQL server-like temporary tables are not equivalent to the GLOBALLY temporary table in Oracle. There is a reason why temp tables in Oracle are always prefixed with GLOBALLY: they are visible to all sessions although the data in the temporary table is private to each session.
In Oracle creating a temporary table is a relatively expensive operation and you should not create individual temporary tables: all sessions should that do the same job should use the same common structure. Instead of creating multiple temporary tables, in Oracle you should create the table once and reuse it in all procedures. If you are going to need it later, why drop it?
In any case, if you decide to do multiple DDL that depend upon a SELECT, you could do it in a PLSQL block instead of a SELECT query:
DECLARE
l VARCHAR2(100);
BEGIN
FOR cc IN (SELECT col FROM tab) LOOP
l := create_temp_table(cc.col);
END LOOP;
END;
I tested below solution on Oracle 10g XE, it works for me.
Create function:
CREATE OR REPLACE FUNCTION GET_USERS
(
USERID IN VARCHAR2
)
RETURN VARCHAR2
AS
request VARCHAR2(255) := 'CREATE GLOBAL TEMPORARY TABLE ';
BEGIN
request := request || 'temp_table_' || userid ||
'(user_name varchar2(50), user_id varchar2(20), is_administrator varchar2(5))' ||
' ON COMMIT PRESERVE ROWS';
EXECUTE IMMEDIATE request;
RETURN 'true';
END GET_USERS;
Run function:
SET SERVEROUTPUT ON
DECLARE
RESULT VARCHAR(255);
BEGIN
RESULT:=gET_USERS('ADMIN3');
dbms_output.put_line(result);
END;
and select from temporary table:
SELECT * FROM temp_table_admin3;