SQL Database Restrict View of Data - sql

Okay so I am new to SQL and was just wondering if there where a way to limit who would have access to what type of data in terms of when retriving information from the tables. Like for example I had a table with information about a client which would have his name, phone, address, SSN, salary, and etc. I want to figure out if there is a way to limit what is being shown based on who is viewing that data. As in a Person A can see everything while Person B can see everything except for SSN and Salary
EDIT:
Could the use of a trigger to limit the view of a certain group work?

You can use views. After that it depends on usage, how the persons access to data (some access rights or user groups etc.)

To make columns not appear for certain users, you'd either need distinct views for each user group (ie view A that didnt have SSN, salary for users in the restricted group etc). or use fine grained access control
FGAC: http://docs.oracle.com/cd/B19306_01/network.102/b14266/apdvcntx.htm
fine grained access control applies straight to the table, so you wouldn't need views to implement that.
e.g. a very simple test to show you. lets say anyone with the Oracle role "SSN_AUTH" can view SSN/Salary for all rows. those without it,cannot.
SQL> create table person(id number, name varchar2(200), ssn varchar2(20), salary number(*,2));
Table created.
now we create a function (by all means, put this in a package for real code). the function will apply a silent predicate exists (select null from session_roles where role = 'SSN_AUTH') for every query fired on the person table. i.e. that predicate will mean that if you don't have an enabled role called SSN_AUTH, you won't see the data.
SQL> create or replace function person_rls (p_owner in varchar2, p_name in varchar2)
2 return varchar2 as
3 v_sql varchar2(2000);
4 begin
5 v_sql := 'exists (select null from session_roles where role = ''SSN_AUTH'')';
6 return v_sql;
7 end person_rls;
8 /
Function created.
now, i dont want to supress rows (though we could do). we just want to supress column data. so we add this function as a policy to the table and tell it the columns to secure:
SQL> BEGIN
2 DBMS_RLS.ADD_POLICY(object_schema=>user, object_name=>'PERSON',
3 policy_name=>'SEC_PERSON', function_schema=>user,
4 policy_function=>'PERSON_RLS',--our function
5 sec_relevant_cols=>'ssn,salary', -- secure these cols.
6 sec_relevant_cols_opt=>dbms_rls.ALL_ROWS);
7 END;
8 /
PL/SQL procedure successfully completed.
SQL> insert into person values (1, 'DazzaL', 'asdklakjd', 10000.12);
1 row created.
SQL> commit;
Commit complete.
now if we don't have the role set:
SQL> set role none;
Role set.
SQL> select * from person;
ID NAME SSN SALARY
---------- -------------------- -------------------- ----------
1 DazzaL
the salary + SSN is blank..but if we enable the role.
SQL> set role all;
Role set.
SQL> select * From session_roles;
ROLE
------------------------------
CONNECT
RESOURCE
SELECT_CATALOG_ROLE
HS_ADMIN_SELECT_ROLE
PLUSTRACE
SSN_AUTH <--- we have it now.
SQL> select * from person;
ID NAME SSN SALARY
---------- -------------------- -------------------- ----------
1 DazzaL asdklakjd 10000.12
the data magically appears.

Related

Calculating and store derived attribute in Oracle Sql Developer

I am just trying to calculate and store age attribute derivedly from dob(date of birth). How can ı do that in Oracle SQL Developer?
CREATE TABLE Patient (
dob DATE,
age NUMBER(3));
I mean when de dob inserted like that 01/01/2000 the age will automatically calculated to 21.
No, you're planning to do it in a wrong way. Why would you so badly want to do that?
It's the same as people - who fill their profile - say "I've got 5 years of experience with Oracle". That was probably true at the moment of writing that text, but - is it still valid now? Two years later it should be "I've got 7 years of experience".
So, what kind of information would be storing fixed age into that column? That person was 21 years old at the moment of insert, but they won't stay forever young. Believe me, I know.
You were suggested to create a virtual column. Well, you can't. Why? Because - in order to calculate someone's age - you have to refer to today's date. Function that returns it is SYSDATE. Bad luck - it is not deterministic, which means that it doesn't return the same value each time you call it with the same parameters. It doesn't accept any, that's true, but it doesn't matter. If you try it, it'll fail:
SQL> create table patient
2 (id number constraint pk_pat primary key,
3 date_of_birth date not null,
4 age number generated always as
5 (round(months_between(sysdate, date_of_birth)/12, 0))
6 virtual
7 );
(round(months_between(sysdate, date_of_birth)/12, 0))
*
ERROR at line 5:
ORA-54002: only pure functions can be specified in a virtual column expression
SQL>
So, what can you do? You can create a view.
SQL> create table patient
2 (id number constraint pk_pat primary key,
3 date_of_birth date not null
4 );
Table created.
SQL> create or replace view v_patient as
2 select id,
3 date_of_birth,
4 round(months_between(sysdate, date_of_birth)/12, 0) as age
5 from patient;
View created.
SQL>
Let's test it.
SQL> insert into patient (id, date_of_birth) values (1, date '2000-01-01');
1 row created.
SQL> select * from v_patient;
ID DATE_OF_BI AGE
---------- ---------- ----------
1 01.01.2000 21
SQL>
Looks OK to me.

Writing a Trigger to find and fill the age of a patient whenever a patient record is inserted into patients table

In this question, I have to add value of age with the help of trigger.
please help me implement this question.
create or replace trigger ia_patient
after insert on patient
for each row
enable
declare
age_value number(10);
begin
DBMS_OUTPUT.PUT_LINE('Please Insert Age');
update patient set age=&age_value where ??;
end;
I am not sure what I should write after "where"
Unless it is a homework question, it really doesn't make much sense as "age" depends on current date.
Anyway, see if this helps.
SQL> create table patient
2 (id number primary key,
3 date_of_birth date,
4 age number
5 );
Table created.
Trigger: calculate age as months between sysdate and date of birth divided by 12 (as there are 12 months in a year); certainly, this is just an approximate value, but - as far as I understood - your task isn't to correctly calculate age, but to learn how to use triggers.
SQL> create or replace trigger ia_patient
2 before insert on patient
3 for each row
4 begin
5 :new.age := round(months_between(sysdate, :new.date_of_birth) / 12);
6 end;
7 /
Trigger created.
Testing:
SQL> insert into patient (id, date_of_birth) values (2, date '2000-10-05');
1 row created.
SQL> select * from patient;
ID DATE_OF_BI AGE
---------- ---------- ----------
2 2000-10-05 20
SQL>
This is written as a frame challenge to the question.
Don't use a trigger; if you do it'll insert the age now and then tomorrow, or in 6 months, or a year the person will have a birthday and your table will be incorrect. This should be something you calculate in a query at the time you want the data and could be put into a view.
CREATE TABLE people (
id INTEGER
GENERATED AS IDENTITY
CONSTRAINT people__id__pk PRIMARY KEY,
name VARCHAR2(50)
NOT NULL,
date_of_birth DATE
NOT NULL
);
Then you can create a view to calculate the ages:
CREATE VIEW people_ages ( id, name, date_of_birth, age ) AS
SELECT id,
name,
date_of_birth,
FLOOR( MONTHS_BETWEEN( SYSDATE, date_of_birth ) / 12 )
FROM people;
Which, if you have the sample data:
INSERT INTO people ( name, date_of_birth )
SELECT 'Alice', DATE '1970-01-01' FROM DUAL UNION ALL
SELECT 'Bob', DATE '2020-10-21' FROM DUAL UNION ALL
SELECT 'Carol', DATE '2000-10-21' FROM DUAL;
Then the output from the view would be:
SELECT * FROM people_ages;
ID | NAME | DATE_OF_BIRTH | AGE
-: | :---- | :------------------ | --:
1 | Alice | 1970-01-01 00:00:00 | 50
2 | Bob | 2020-10-21 00:00:00 | 0
3 | Carol | 2000-10-21 00:00:00 | 20
db<>fiddle here
Your trigger has serious defects. Despite being a totally inappropriate use of a trigger I'll go over them.
Attribute: Age. Lets just say others have sufficiently covered it. But i will return to it.
DBMS_OUTPUT: This is not output when issued. The pl/sql engine does not even output this at all. It builds an internal buffer which can later be read back and processed by the client after the pl/sql has completed. However, there is no requirement that the client do so. Typically used for debugging purposes and not available in production system.
&age_value: This is an attempt at an interactive processing. Pl/sql is not and cannot do interactive processing. This is called a substitution variable and is totally the responsibly of the client. The client sees the &... and prompts for a value. It then physically alters the statement to contain that value. For example if you reply 42 to the prompt what is sent to the compiler is age=42. The compiler actually never sees &age_value.
Update: you CAN NOT do this; it will throw the exception "ORA-04091: table ... is mutating, trigger/function may not see it". Within a trigger you cannot issue DML on the table that caused the trigger to fire. Moreover it is completely unnecessary.
where ??: Since you cannot have the update statement this is moot.
In a trigger you refer to table columns through the pesudo rows :new and/or :old. So assuming you need to stay with a trigger you rewrite as (following assumes a date_of_birth column (dob) name being inserted):
create or replace trigger ia_patient
after insert on patient
for each row
enable
begin
:new.age := trunc(months_between(sysdate, :new.dob)/12);
end;
However the problem being age is now a static value. Which will need regular updates to keep current. A much better approach just let Oracle do the calculation when needed with a view as suggested by #MT0 and abandon the trigger altogether.

table mutating in oracle

I have a table, participated, which has a trigger that returns the total damage amount for a driver id when a new record is inserted into the table.
create or replace trigger display
after insert on participated
for each row
declare
t_id integer;
total integer;
begin
select driver_id, sum(damage_amount)
into t_id, total
from participated
where :new.driver_id = driver_id
group by driver_id;
dbms_output.put_line(' total amount ' || total' driver id' || t_id);
end;
/
The trigger is created, but it returns this error:
ORA-04091: table SQL_HSATRWHKNJHKDFMGWCUISUEEE.PARTICIPATED is mutating,
trigger/function may not see it ORA-06512: at
"SQL_HSATRWHKNJHKDFMGWCUISUEEE.DISPLAY", line 5
ORA-06512: at "SYS.DBMS_SQL", line 1721
Please help with this trigger.
As commented above, this feels like a code-smell. A row level trigger cannot change the table being changed, since that would fire another trigger, which would end up in an endless loop of calling triggers.
Changing this to a statement level trigger is not doing the same thing.
Preferred solution:
1) put this to the application logic, and calculate after row has been inserted - this is trivial as #kfinity mentioned.
2) earmark newly inserted rows and use a statement level trigger. For example, have an extra column, say is_new default 1 - therefore all new inserted rows will have this flag. Then use a statement level trigger suggested by #hbourchi to calculate and update all drivers that is_new is 1, and then set this flag back to zero
3) the logic in 2) can be implemented using pl/sql and in-memory pl/sql tables. The pl/sql table collects affected driver ids using a row level trigger, and then updates the totals of the selected drivers. Tom Kyte has lots of examples on this, this is not a rocket science, however if you lack of PL/SQL knowledge, then this is probably not your way. (For the note: PL/SQL is super important when using Oracle - without that Oracle is just an expensive Excel sheet like any other db. Worth of using it.)
4) probably, you shall revise your data model - and the problem solves itself. The participated table shows multiple rows per driver id. You want to calculate one total row per driver id - why would you put that summary to the same table? Simply add a new table, participated_total which has driver_id and damaged_amount fields. Then feel free to insert or update that from your trigger as you planned originally!
5) in fact, you can calculate these totals on the fly (depending on the number of rows and your performance expectations), by simply crafting the right SQL when querying - this way no need to store the pre-calculated totals.
6) but if you wish Oracle to store these totals for you, you can do 5) and use materialized views. These are in-fact tables, which are updated and maintained automatically by Oracle, so your actual query at 5) does not need to calculate anything on the fly but can get the automatically pre-calculated data from the materialized view.
How about just no triggers at all ?
SQL> create table participated (
2 incident_id int primary key,
3 driver_id int not null,
4 damage_amount int not null
5 );
Table created.
SQL>
SQL> insert into participated
2 select rownum, mod(rownum,10), dbms_random.value(1000,2000)
3 from dual
4 connect by level <= 200;
200 rows created.
SQL> create materialized view log on participated with rowid, (driver_id,damage_amount), sequence including new values;
Materialized view log created.
SQL> create materialized view driver_tot
2 refresh fast on commit
3 with primary key
4 as
5 select driver_id,
6 sum(damage_amount) tot,
7 count(*) cnt
8 from participated
9 group by driver_id;
Materialized view created.
SQL> select driver_id, tot
2 from driver_tot
3 order by 1;
DRIVER_ID TOT
---------- ----------
0 32808
1 29847
2 28585
3 29714
4 32148
5 30491 <====
6 29258
7 32103
8 30131
9 26834
10 rows selected.
SQL>
SQL> insert into participated values (9999,5,1234);
1 row created.
SQL> commit;
Commit complete.
SQL>
SQL> select driver_id, tot
2 from driver_tot
3 order by 1;
DRIVER_ID TOT
---------- ----------
0 32808
1 29847
2 28585
3 29714
4 32148
5 31725 <====
6 29258
7 32103
8 30131
9 26834
10 rows selected.
SQL>
SQL>
You didn't post your trigger definition but normally you can not query a table, inside trigger when updating records in the same table.
Try using after update trigger. it might work in your case. Something like this:
CREATE OR REPLACE TRIGGER my_trigger AFTER UPDATE ON my_table FOR EACH ROW
DECLARE
...
BEGIN
...
END;
another option would be to make your trigger AUTONOMOUS_TRANSACTION:
CREATE OR REPLACE TRIGGER my_trigger BEFORE INSERT ON my_table FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
...
END;
However read this before choosing for this option:
https://docs.oracle.com/cd/B14117_01/appdev.101/b10807/13_elems002.htm

Are SCHEMA and USER the same thing in Oracle?

Are SCHEMA and USER the same thing in Oracle? Is there is situation where a single user can have two or more schemas?
Strictly speaking a SCHEMA is the set of objects owned by a USER. However, a USER can have only one SCHEMA, so people often use the terms interchangeably.
Here's the difference between USER and SCHEMA. User "A" has granted rights on its PRODUCT table to user "B". User "B" does not have a table of that name.
SQL> show user
USER is "B"
SQL> select count(*) from products
2 /
select count(*) from products
*
ERROR at line 1:
ORA-00942: table or view does not exist
SQL> select count(*) from a.product
2 /
COUNT(*)
----------
2
SQL>
Without a synonym "B" needs to prefix the schema to reference the PRODUCT table. Unless they choose to change the current schema:
SQL> alter session set current_schema = "A"
2 /
Session altered.
SQL> select count(*) from product
2 /
COUNT(*)
----------
2
SQL> show user
USER is "B"
SQL>
Now the user is "B" but the current (default) schema is "A". Which menas they need to prefix the schema when referencing their own tables!
SQL> select table_name from user_tables;
TABLE_NAME
------------------------------
YOUR_TABLE
SQL> select count(*) from your_table;
select count(*) from your_table
*
ERROR at line 1:
ORA-00942: table or view does not exist
SQL> select count(*) from b.your_table;
COUNT(*)
----------
6
SQL>
"Whether a schema can be created separately Without any user ."
No. A SCHEMA must belong to a USER. When we create user Oracle creates a schema, an empty one, automatically.
Confusingly there is a create schema syntax, but that is just a short hand for creating a number of tables (and indexes) in one statement. Find out more.

How can I prevent multiple records of a type are not allowed in a many-to-many relationship?

I have four tables.
PERSON DELIVERY_MAPPING GENERATION_SYSTEM DELIVERY_METHOD
------ ---------------- ----------------- ---------------
ID PERSON_ID ID ID
NAME GENERATION_SYSTEM_ID NAME NAME
DELIVERY_METHOD_ID IS_SPECIAL
Example data:
PERSON DELIVERY_MAPPING GENERATION_SYSTEM DELIVERY_METHOD
------ ---------------- ----------------- ---------------
1. TOM 1 1 1. COLOR PRINTER 1 1. EMAIL N
2. DICK 1 2 2. BW PRINTER 1 2. POST N
3. HARRY 2 3 3. HANDWRITTEN 3 3. PIGEONS Y
A DELIVERY_METHOD contains ways to deliver new letters — EMAIL, POST, PIGEON. The IS_SPECIAL column marks a record as a means of a special delivery. It is indicated by a simple value of Y or N. Only PIGEON is a special delivery method i.e. Y, the others are not i.e. N.
The GENERATION_SYSTEM has the information that will finally print the letter. Example values are COLOR PRINTER and DOT MATRIX PRINTER. Each GENERATION_SYSTEM will always be delivered using one of the DELIVERY_METHODs. There's a foreign key-between GENERATION_SYSTEM and DELIVERY_METHOD.
Now, each PERSON can have his letters generated by different GENERATION_SYSTEMs and since, it is a many-to-many relation, we have the DELIVERY_MAPPING table and that's that's why we have foreign key's on both ends.
So far, so good.
I need to ensure that it if a person has his letters generated by a system that uses a special delivery method then he cannot be allowed to have multiple generation systems in the mappings list. For example, Dick can't have his letters generated using the colour printer because he already gets all his handwritten letters delivery by a pigeon (which is a marked a special delivery method).
How would I accomplish such a constraint? I tried doing it with a before-insert-or-update trigger on the DELIVERY_MAPPING table but that causes the mutating trigger problem when updating.
Can is normalise this scenario even more? Maybe it is just that i haven't normalised my table properly.
Either way, I'd love to hear your take on this issue. I hope I've been verbose enough (...and if you can propose a better title for this post, that would be great)
For a complicated constraint like this, I think you need to use triggers. I don't think the mutating table problem is an issue, because you are either going to do an update or do nothing.
The only table you need to worry about is Delivery_Mapping. Before allowing a change to this table, you need to run a query on the existing table to get the number of specials and gs's:
select SUM(case when dme.is_special = 'Y' then 1 else 0 end) as NumSpecial,
count(distinct gs.id) as NumGS,
MIN(gs.id) as GSID
from delivery_mapping dm join
generation_system gs
on dm.generation_system_id = gs.id join
delivery_method dme
on gs.delivery_method_id = dme.id
where dm.person_id = PERSONID
With this information, you can check if the insert/update can proceed. I think you need to
check the conditions:
If NumSpecial = 0 and the new delivery method is not special, then proceed.
If NumSpecial = 0 and NumGS = 0, then proceed.
Otherwise fail.
The logic is a bit more complicated for updates.
By the way, I prefer to wrap updates/inserts/deletes in stored procedures, so logic like this doesn't get hidden in triggers. I find that debugging and maintaining procedures is much easier than dealing with triggers, which may be possibly cascading.
I'd avoid triggers on the base tables for this unless you can guarantee serialization.
you could use an API (best way) as Gordon says (again, be sure to serialize) or if that isn't suitable, use a materialized view (we don't need to serialize here, as the check is done on commit):
SQL> create materialized view log on person with rowid, primary key including new values;
Materialized view log created.
SQL> create materialized view log on delivery_mapping with rowid, primary key including new values;
Materialized view log created.
SQL> create materialized view log on generation_system with rowid, primary key (delivery_method_id) including new values;
Materialized view log created.
SQL> create materialized view log on delivery_method with rowid, primary key (is_special) including new values;
Materialized view log created.
we create a materialized view to show the counts of special + non special links for each user:
SQL> create materialized view check_del_method
2 refresh fast on commit
3 with primary key
4 as
5 select pers.id, count(case del_meth.is_special when 'Y' then 1 end) special_count,
6 count(case del_meth.is_special when 'N' then 1 end) non_special_count
7 from person pers
8 inner join delivery_mapping del_map
9 on pers.id = del_map.person_id
10 inner join generation_system gen
11 on gen.id = del_map.generation_system_id
12 inner join delivery_method del_meth
13 on del_meth.id = gen.delivery_method_id
14 group by pers.id;
Materialized view created.
the MView is defined as fast refresh on commit, so the modified rows are rebuilt on commit. now the rule is that if special+non special counts are non-zero, that's an error condition.
SQL> create trigger check_del_method_aiu
2 after insert or update on check_del_method
3 for each row
4 declare
5 begin
6 if (:new.special_count > 0 and :new.non_special_count > 0)
7 then
8 raise_application_error(-20000, 'Cannot have a mix of special and non special delivery methods for user ' || :new.id);
9 end if;
11 end;
12 /
Trigger created.
SQL> set serverout on
SQL> insert into delivery_mapping values (1, 3);
1 row created.
SQL> commit;
commit
*
ERROR at line 1:
ORA-12008: error in materialized view refresh path
ORA-20000: Cannot have a mix of special and non special delivery methods for
user 1
ORA-06512: at "TEST.CHECK_DEL_METHOD_AIU", line 6
ORA-04088: error during execution of trigger 'TEST.CHECK_DEL_METHOD_AIU'
CREATE MATERIALIZED VIEW special_queues_mv
NOLOGGING
CACHE
BUILD IMMEDIATE
REFRESH ON COMMIT
ENABLE QUERY REWRITE
AS SELECT dmap.person_id
, SUM(DECODE(dmet.is_special, 'Y', 1, 0)) AS special_queues
, SUM(DECODE(dmet.is_special, 'N', 1, 0)) AS regular_queues
FROM delivery_mapping dmap
, generation_system gsys
, delivery_method dmet
WHERE dmap.generation_system_id = gsys.id
AND gsys.delevery_method_id = dmet.id
GROUP
BY dmap.person_id
/
ALTER MATERIALIZED VIEW special_queues_mv
ADD ( CONSTRAINT special_queues_mv_chk1 CHECK ((special_queues = 1 AND regular_queues = 0) OR ( regular_queues > 0 AND special_queues = 0 ) ) ENABLE VALIDATE)
/
That's how I did it. DazzaL's answer gave me a hint on how to do it.