Fine Grained Access Control

advertisement
Fine Grained Access Control
Fine Grained Access Control (FGAC) in Oracle 8i gives you the ability to dynamically attach, at runtime, a predicate (the WHERE clause) to all queries issued against a database table or view. You now have
the ability to procedurally modify the query at run-time – a dynamic view capability. You may evaluate
who is running the query, which terminal they are running the query from, when they are running it (as
in time of day), and then build the predicate based on those specific set of circumstances. With the use
of application contexts, you may securely add additional information to the environment (such as an
application role the user may have), and access this in your procedure or predicate as well.
You will see FGAC referred to with various names in different publications. The following are
synonymous terms for this feature:
❑
Fine Grained Access Control
❑
Virtual Private Database (VPD)
❑
Row Level Security or DBMS_RLS (based on the PL/SQL package DBMS_RLS that implements
this feature)
In order to execute the examples found in this chapter, you will need Oracleion 8.1.5), or higher. In
addition, this feature is available only in the Enterprise and Personal Editions of Oracle; these examples
will not work in the Standard Edition.
In this chapter, we will cover:
5254ch21cmp2.pdf 1
❑
The reasons why it would be advantageous to use this feature, such as it's ease of maintenance,
the fact that it is performed in the server itself, takes into account the evolution of application
and also allows for easier development, and so on.
❑
Two examples in the How it Works section, demonstrating both security policies and
application contexts.
2/28/2005 6:52:47 PM
Chapter 21
❑
An extensive list of issues you should be aware of such as, FGAC's behavior with respect to
referential integrity, cursor caching, import and export matters, and debugging nuances.
❑
Some of the errors you may come across when attempting to implementing FGAC in your
applications.
An Example
Say you have a security policy that determines what rows different groups of people may see. Your
security policy will develop and return a predicate based on who is logged in, and what role they have.
FGAC will allow you to rewrite the basic query SELECT * FROM EMP as follows:
Logged in
user
Query rewritten to
Comments
Employee
select *
Employees may only see
their own records.
from ( select * from emp
where ename = USER )
Manager
select *
from ( select *
Managers may see their
record, and the records of
people that work for them.
from emp
where mgr =
( select empno
from emp
where ename = USER )
or ename = USER
)
HR rep.
select *
from (select *
from emp
where deptno =
SYS_CONTEXT( 'OurApp', ptno' )
)
HR representatives may see
anyone in a given
department. This introduces
the syntax for retrieving
variables from an
application context, the
SYS_CONTEXT() built-in
function.
Why Use this Feature?
In this section we will explore the various reasons and cases where you might choose to use this feature.
914
5254ch21cmp2.pdf 2
2/28/2005 6:52:47 PM
Fine Grained Access Control
Ease of Maintenance
FGAC allows you to have one table and one stored procedure, to manage what used to take many
views, or database triggers, or lots of application logic.
The multiple-view approach was common. The application developers would create many different
database accounts, such as EMPLOYEE, MANAGER, HR_REP, and install into each of these accounts, a
complete set of views that selected exactly the right data. Using the above introductory example, each
database account would have a separate EMP view, with a customized predicate specifically for that
group of users. In order to control what the end users could see, create, modify, and remove they would
also need up to four different views for the EMP table, one each for SELECT, INSERT, UPDATE, and
DELETE. This quickly leads to a proliferation of database objects – every time you need to add another
group of users, it means another set of views to manage and maintain.
If you change the security policy (for example, if you want managers to see not only their direct reports,
but also the ones that are two levels down), you would have to recreate the view in the database,
invalidating all objects that refer to it. Not only did this approach lead to a proliferation of views in the
database, it also forced users to log in using various common accounts, which compromised
accountability. A further side effect of this approach is that a lot of code must be duplicated in the
database. If I have a stored procedure that operates on the EMP table, I would have to install the stored
procedure into each individual account. The same would hold true for many other objects (triggers,
functions, packages, and so on). Now I must update N accounts every time I roll out a patch to my
software, to ensure everyone is executing the same code base.
Another approach uses database triggers along with views. Here, instead of creating a database view for
each of SELECT, INSERT, UPDATE, and DELETE, we would use a database trigger to review, row-by-row,
the changes an individual was making and either accept or reject them. This implementation would not
only cause the same proliferation of views as outlined above, but it would also add the overhead of a
trigger (sometimes complex trigger) firing for each, and every, row modified.
A final option is to put all of the security in the application, be it a client application in a client-server
environment, or a middle-tier server application. The application would look at who is logged on, and
use the right query for that user. The application in effect, implements its own FGAC. The serious
drawback to this approach (and any approach that uses an application to enforce access to data) is that
the data in the database is useful only to the application. It precludes the use of any ad-hoc query tools,
report generation tools, and the like because the data is not secured unless accessed via the application.
When the security is tied up in the application, it is not easy to extend the application to new interfaces
– the usability of the data is reduced.
FGAC allows you to manage this complexity and avoid any sort of loss of functionality using just two
objects – the original table or view, and a database package or function. You can update the database
package at any point in time to immediately put in place a new set of security polices. Rather than
having to look at dozens of views to see all of the security policies in place on an object, you can get all
of this information in one place.
Performed in the Server
Many times, given the complexity of managing and maintaining so many views, developers will encode
the application logic into the application itself, as discussed above. The application will look at who is
logged on and what they are requesting, and then submit the appropriate query. This protects the data
only when the data is accessed via the application thus the probability that the data will be
compromised at some point is increased, since all one needs to do is log into the database with some
tool, other than your application, and query the data.
915
5254ch21cmp2.pdf 3
2/28/2005 6:52:47 PM
Chapter 21
Using FGAC we place the security logic, which determines what data the user should see, in the
database. In this way we are ensuring that the data is protected, regardless of the tool used to access it.
The need for this is clearly visible today. In the early to mid-1990s, the client-server model ruled (and
before that, host-based programming was the norm). Most client-server applications (and pretty much
all host-based ones) have embedded within them, the logic that is used to access the application. Today,
it is very much 'en vogue' to use an application server and host the application logic there. As these
client-server applications are migrated to the new architecture, people are extracting the security logic
from the client-server applications, and embedding it in the application server. This has lead to a double
implementation of the security logic (the client-server applications don't totally go away), so there are
two places to maintain and debug the logic. Even worse, it doesn't solve the problem when the next
programming paradigm comes along. What happens after application servers go out of fashion? What
happens when your users want to use a third-party tool that can access the data directly? If the security
is all locked up in the middle-tier logic, they won't be able to. If the security is right there with the data,
then not only are you ready for any yet to be invented technology, but you are also ready right now for
free, secure access to the data.
Easier Application Development
FGAC takes the security logic out of the application logic. The application developer can concentrate
on the application itself, not the logic of accessing the underlying data to keep it secure. Since FGAC is
done entirely in the database server, the applications immediately inherit this logic. In the past, the
application developers had to encode the security logic into the application, making the applications
harder to develop initially, and also making them especially hard to maintain. If the application is
responsible for mediating access to the data, and you access the same data from many locations in the
application, a simple change to your security policy may affect dozens of application modules. Using
FGAC, all relevant application modules automatically inherit your new security policies without having
to be modified.
Evolutionary Application Development
In many environments, security policies are not well defined initially, and can change over time. As
companies merge, or as health care providers tighten the access to patient databases, or as privacy laws
are introduced, these security policies will need to change. Placing the access control as close to the data
as possible, allows for this evolution with minimal impact on applications and tools. There is one place
where the new security logic is implemented, and all applications and tools that access the database
automatically inherit the new logic.
Avoids Shared User Accounts
Using FGAC, each user can and should, log in with unique credentials. This supplies complete
accountability, and you can audit actions at the user level. In the past, many applications, when faced
with having different views of the data for different users, make the choice to set up shared accounts.
For example, every employee would use the EMPLOYEE account to access the employee views; every
manager would use the MANAGER account, and so on. This removes the ability to audit actions at the
user level. You can no longer see that TKYTE is logged in (as an employee), but only that EMPLOYEE
(whoever that may be) is logged in.
You can use still use FGAC with shared accounts if desired. However, this feature removes the necessity
for shared accounts.
916
5254ch21cmp2.pdf 4
2/28/2005 6:52:47 PM
Fine Grained Access Control
Supports Shared User Accounts
This is a corollary to the preceding section. FGAC does not mandate the use of a logon per user; it
simply facilitates it. Using a feature called an application context, as we will see below, we'll be able to
use FGAC in a 'single account' environment, as may arise in a connection pool with an application
server. Some connection pools mandate that you use a single database account to log in with. FGAC
works well with those environments as well.
Hosting an Application as an ASP
FGAC allows you to take an existing application and host it for many different customers in an
Application Service Provider (ASP) environment, without having to change the application. Lets say
you had an HR application, which you wanted to put on the Internet, and charge access fees. Since you
have many different customers, and each wants to ensure their data cannot be seen by anyone else, you
have to come up with a plan for protecting this information. Your choices are:
❑
Install, configure, and maintain a separate database instance per customer base.
❑
Recode any stored procedure the application uses to be an invoker rights routine, as
described in Chapter 23 on Invoker and Definers Rights, and implement a schema per customer
base.
❑
Use a single install of the database instance, and a single schema with FGAC.
The first option is less than desirable. The overhead of having a database instance per customer base,
where a customer might only have a handful of users, prevents this option from being viable. For very
large customers, with hundreds or thousands of users, this makes sense. For the plethora of small
customers, each of which contribute five or six end users, a database per customer is not workable. You
probably couldn't afford to do it.
The second option potentially involves recoding the application. The goal here would be to for each
customer schema to have its own set of database tables. Any stored procedures would have to be coded
in such a fashion that they would operate on the tables visible to the currently logged in user account
(the customer). Normally, stored procedures see the same objects that the definer of the procedure would
see – we would have to go out of our way to ensure we used invokers rights routines, and never use any
hard-coded schema names in the application. For example, we could never SELECT * FROM
SCOTT.EMP, only SELECT * FROM EMP. This would apply not only to PL/SQL routines, but any
external code in languages such as Java or Visual Basic would have to follow these rules as well (since
they have no schema names). It is not desirable for this reason, as well as the fact that you now have
many hundreds of schemas to manage.
The third option, using FGAC, is the least intrusive of the three and also the easiest to implement. Here,
for example, we could add a column to each table that must be protected, and this column would
contain the company identifier. We would utilize a trigger to maintain this column (so the application
does not have to do so). The trigger would utilize an application context, set by an ON LOGON trigger, to
supply this value. The security policy would return a predicate that selected only the rows for the
company you are allowed to see. The security policy would not only limit the data by company, but
would also add any other predicates necessary to limit access to data. Returning to our HR application –
not only would we add WHERE COMPANY = VALUE, but also the predicates defined according to whether
you were an employee, manager or HR representative. Going a step further, you could implement
partitioning to physically segregate the data of larger customers for recoverability and availability
options.
917
5254ch21cmp2.pdf 5
2/28/2005 6:52:47 PM
Chapter 21
How it Works
FGAC is implemented in Oracle 8i with two primary constructs:
❑
An application context – This is a namespace with a corresponding set of attribute/value pairs.
For example, in the context named OurApp, we could access variables DeptNo, Mgr, and so
on. An application context is always bound to some PL/SQL package. This package is the
only method for setting values in the context. To get the DeptNo attribute set to a value in the
OurApp context, you must call a specific package – the one bound to the OurApp context.
This package is trusted to set values correctly in the OurApp context (you wrote it, that's why
it is trusted to set the context correctly). This prevents users with malicious intent, from setting
values in an application context that would give them access to information they should not
have access to. Anyone can read the values of an application context, but only one package
may set these values.
❑
A security policy – A security policy is simply a function you develop that will return a
predicate used to filter data dynamically when a query is executed. This function will be
bound to a database table or view, and may be invoked for some or all of the statements that
access the table. What this means is you can have one policy that is used for SELECT, another
for INSERT, and a third for UPDATE and DELETE. Typically this function will make use of
values in an application context to determine the correct predicate to return (for example, it
will look at 'who' is logged in, 'what' they are trying to do, and restrict the rows they can
operate on to some set). It should be noted that the user SYS (or INTERNAL) never have
security policies applied to them (the policy functions are quite simply never invoked), and
they will be able to see/modify all of the data.
Also worth mentioning are other Oracle 8i features that enhance the implementation of FGAC, such as:
❑
SYS_CONTEXT function – this function is used in SQL or PL/SQL to access the application
context values. See the Oracle SQL Reference manual for all of the details on this function, and a
list of default values you'll find in the USERENV context that Oracle automatically sets up.
You'll find things like the session username, the IP address of the client, and other goodies
hidden in there.
❑
Database logon triggers – This function allows you to run some code upon a user logging into
the database. This is extremely useful for setting up the initial, default application context.
❑
DBMS_RLS package – This package provides the API for us to add, remove, refresh, enable,
and disable security policies. It is callable from any language/environment that can connect to
Oracle.
In order to use this feature, the developer will need the following privileges in addition to the standard
CONNECT and RESOURCE (or equivalent) roles:
❑
EXECUTE_CATALOG_ROLE – This allows the developer to execute the DBMS_RLS package.
Alternatively, you may just grant execute on DBMS_RLS to the account when connected as
SYS.
❑
CREATE ANY CONTEXT – This allows the developer to create application contexts.
An application context is created using a simple SQL command:
SQL> create or replace context OurApp using Our_Context_Pkg;
918
5254ch21cmp2.pdf 6
2/28/2005 6:52:48 PM
Fine Grained Access Control
Here OurApp is the name of the context and Our_Context_Pkg is the PL/SQL package that is allowed
to set values in the context. Application contexts are an important feature for the FGAC implementation
for two reasons:
❑
It supplies you with a trusted way to set variables in a namespace – Only the PL/SQL package
associated with a context may set values in that context. This ensures the integrity of the
values in this context. Since you will use this context to restrict or permit access to data, the
integrity of the values in the context must be assured.
❑
References to the application context values in a SQL query are treated as bind variables –
For example, if you set value of a DeptNo attribute in the context OurApp, and implemented
a policy to return a WHERE clause deptno = SYS_CONTEXT('OurApp','DeptNo'), it will be
subject to shared SQL usage since the SYS_CONTEXT reference is similar to deptno = :b1.
Everyone might use different values for Deptno, but they will all reuse the same parsed and
optimized query plans.
Example 1: Implementing a Security Policy
We will implement a very simple security policy as a quick demonstration of this feature. The policy we
will implement is:
❑
If the current user is the OWNER of the table, you can see all rows in the table, else,
❑
You can only see rows 'you own', rows where the name in the OWNER column is your
username.
❑
Additionally, you can only add rows such that you are the OWNER of that row. If you attempt
to add a row for someone else, it will be rejected.
The PL/SQL function we would need to create for this would look like this:
tkyte@TKYTE816> create or replace
2 function security_policy_function( p_schema in varchar2,
3
p_object in varchar2 )
4 return varchar2
5 as
6 begin
7
if ( user = p_schema ) then
8
return '';
9
else
10
return 'owner = USER';
11
end if;
12 end;
13 /
Function created.
The above shows the general structure of a security policy function. This will always be a function that
returns a VARCHAR2. The return value will be a predicate that will be added to the query against the
table. It will in effect, be added as a predicate against the table or view you applied this security policy
to using an inline view as follows:
919
5254ch21cmp2.pdf 7
2/28/2005 6:52:48 PM
Chapter 21
The query:
SELECT * FROM T
Will be rewritten as: SELECT * FROM ( SELECT * FROM T WHERE owner = USER)
or:
SELECT * FROM ( SELECT * FROM T )
Additionally, all security policy functions must accept two IN parameters – the name of the schema that
owns the object and the name of the object, to which function is being applied. These may be used in
any way you see fit in the security policy function.
So in this example, the predicate owner = USER will be dynamically appended to all queries against the
table to which this function is bound, effectively restricting the number of rows that would be available
to the user. Only if the currently logged in user is owner of the table will an empty predicate be
returned. Returning an empty predicate is like returning 1=1 or True. Returning Null is the same as
returning an empty predicate as well. The above could have returned Null instead of an empty string to
achieve the same effect.
To tie this function to a table, we use the PL/SQL procedure DBMS_RLS.ADD_POLICY, which will be
shown later. In the example we have the following table set up, and the user is logged in as TKYTE:
tkyte@TKYTE816> create table data_table
2 (
some_data
varchar2(30),
3
OWNER
varchar2(30) default USER
4 )
5 /
Table created.
tkyte@TKYTE816> grant all on data_table to public;
Grant succeeded.
tkyte@TKYTE816> create public synonym data_table for data_table;
Synonym created.
tkyte@TKYTE816> insert into data_table ( some_data ) values ( 'Some Data' );
1 row created.
tkyte@TKYTE816> insert into data_table ( some_data, owner )
2 values ( 'Some Data Owned by SCOTT', 'SCOTT' );
1 row created.
tkyte@TKYTE816> commit;
Commit complete.
tkyte@TKYTE816> select * from data_table;
SOME_DATA
-----------------------------Some Data
Some Data Owned by SCOTT
OWNER
-----------------------------TKYTE
SCOTT
920
5254ch21cmp2.pdf 8
2/28/2005 6:52:48 PM
Fine Grained Access Control
Now we would attach the security function that we wrote to this table with the following call to the
package DBMS_RLS:
tkyte@TKYTE816> begin
2
dbms_rls.add_policy
3
( object_schema
=>
4
object_name
=>
5
policy_name
=>
6
function_schema =>
7
policy_function =>
8
statement_types =>
9
update_check
=>
10
enable
=>
11
);
12 end;
13 /
'TKYTE',
'data_table',
'MY_POLICY',
'TKYTE',
'security_policy_function',
'select, insert, update, delete' ,
TRUE,
TRUE
PL/SQL procedure successfully completed.
The ADD_POLICY routine is one of the key routines in the DBMS_RLS package we'll be using. It is what
allows you to add your security policy to the table in question. The parameters we passed in detail are:
❑
OBJECT_SCHEMA – The name of the owner of the table or view. If left as Null (this is the
default), it will be interpreted as the currently logged in user. I passed in my username for
completeness in the above example.
❑
OBJECT_NAME – The name of the table or view the policy will be placed on.
❑
POLICY_NAME – Any unique name you would like to assign to this policy. You will use this
name later if you wish to enable/disable, refresh, or drop this policy.
❑
FUNCTION_SCHEMA – The name of the owner of the function that returns the predicate. It
works in the same way as the OBJECT_SCHEMA. If left to its default of Null, the currently
logged in username will be used.
❑
POLICY_FUNCTION – The name of the function that returns the predicate.
❑
STATEMENT_TYPES – Lists the types of statements this policy will be applied to. Can be any
combination of INSERT, UPDATE, SELECT, and DELETE. The default is all four – I've listed
them here for completeness.
❑
UPDATE_CHECK – This applies to the processing of INSERTs and UPDATEs only. If set to True
(the default is False), this will verify that the data you just INSERTed or UPDATEd is visible by
you, using that predicate. That is, when set to True, you cannot INSERT any data that would
not be SELECTed from that table using the returned predicate.
❑
ENABLE – Specifies whether the policy is enabled or not. This defaults to True.
Now after executing the ADD_POLICY call, all DML against the DATA_TABLE table will have the
predicate which is returned by SECURITY_POLICY_FUNCTION applied to it, regardless of the
environment submitting the DML operation. In other words, regardless of the application accessing the
data. To see this in action:
tkyte@TKYTE816> connect system/manager
system@TKYTE816> select * from data_table;
921
5254ch21cmp2.pdf 9
2/28/2005 6:52:48 PM
Chapter 21
no rows selected
system@TKYTE816> connect scott/tiger
scott@TKYTE816> select * from data_table;
SOME_DATA
OWNER
------------------------------ -------------------Some Data Owned by SCOTT
SCOTT
So, this shows that we have effectively filtered the rows – the user SYSTEM sees no data in this table.
This is because the predicate WHERE OWNER = USER is satisfied by none of the existing rows of data.
When we log in as SCOTT however, the single row owned by SCOTT becomes visible. Going further
with some DML against the table:
sys@TKYTE816> connect scott/tiger
scott@TKYTE816> insert into data_table ( some_data )
2 values ( 'Some New Data' );
1 row created.
scott@TKYTE816> insert into data_table ( some_data, owner )
2 values ( 'Some New Data Owned by SYS', 'SYS' )
3 /
insert into data_table ( some_data, owner )
*
ERROR at line 1:
ORA-28115: policy with check option violation
scott@TKYTE816> select * from data_table;
SOME_DATA
-----------------------------Some Data Owned by SCOTT
Some New Data
OWNER
-----------------------------SCOTT
SCOTT
We are allowed to create data we can see, but the error ORA-28115 is raised because when we added
the policy we specified, we made the call to dbms_rls.add_policy
...
9
...
update_check
=> TRUE );
This is analogous to creating a view with the CHECK OPTION enabled. This will enable us to only create
data that we can also select. The default is to allow you to create data you cannot select.
Now, due to the way in which we coded our security policy, we know the OWNER of the table can see all
rows, and can create any row. To see this in action, we'll just log in as TKYTE and attempt these
operations:
scott@TKYTE816> connect tkyte/tkyte
922
5254ch21cmp2.pdf 10
2/28/2005 6:52:48 PM
Fine Grained Access Control
tkyte@TKYTE816> insert into data_table ( some_data, owner )
2 values ( 'Some New Data Owned by SYS', 'SYS' )
3 /
1 row created.
tkyte@TKYTE816> select * from data_table
2 /
SOME_DATA
-----------------------------Some Data
Some Data Owned by SCOTT
Some New Data
Some New Data Owned by SYS
OWNER
-----------------------------TKYTE
SCOTT
SCOTT
SYS
So, this shows that TKYTE is not affected by this policy. One interesting thing to note is that if we log in
as SYS, the following behavior is noted:
tkyte@TKYTE816> connect sys/change_on_install
Connected.
sys@TKYTE816> select * from data_table;
SOME_DATA
-----------------------------Some Data
Some Data Owned by SCOTT
Some New Data
Some New Data Owned by SYS
OWNER
-----------------------------TKYTE
SCOTT
SCOTT
SYS
The security policy is not used when logged in as the special user SYS (or INTERNAL, or as SYSDBA).
This is the expected, desired behavior. The SYSDBA accounts are powerful administrative accounts, and
are allowed to see all data. This is particularly important to note when exporting information. Unless
you do the export as a SYSDBA, you must be aware that the security policy will be applied when you
export. You will not get all of the data if you use a non-SYSDBA account and a conventional path export!
Example 2: Using Application Contexts
In this example, we would like to implement a Human Resources Security Policy. We will use the
sample EMP and DEPT tables, which are owned by SCOTT and add one additional table that allows us to
designate people to be HR representatives for various departments. Our requirements for this are:
❑
A manager of a department can:
Read their own record, the records of all the employees that report directly to them, the
records of all the people that report to these employees, and so on (hierarchy).
Update records of the employees that report directly to them.
❑
An employee can:
Read their own record.
923
5254ch21cmp2.pdf 11
2/28/2005 6:52:48 PM
Chapter 21
❑
An HR representative can:
Read all the records for the department they are working in (HR reps only work on one
department at a time in our application).
Update all records for the given department.
Insert into the given department.
Delete from the given department.
As stated, our application will use copies of the existing EMP and DEPT tables from the SCOTT schema,
with the addition of a HR_REPS table to allow us to assign an HR representative to a department. When
you log in, we would like your role to be automatically assigned and set up for you. That is, upon login,
if you are an HR representative, the HR representative role will be in place, and so on
To begin, we will need some accounts in our database. These accounts will represent the application
owner and the application end users. In this example, TKYTE is the owner of the application, and will
have a copy of the EMP and DEPT tables from the SCOTT demo account. The end users are named after
people in the EMP table (in other words, KING, BLAKE, and so on). We used the following script to set
this up. First we drop and recreate the user TKYTE and grant CONNECT and RESOURCE to him:
sys@TKYTE816> drop user tkyte cascade;
User dropped.
sys@TKYTE816> create user tkyte identified by tkyte
2 default tablespace data
3 temporary tablespace temp;
User created.
sys@TKYTE816> grant connect, resource to tkyte;
Grant succeeded.
Next are the minimum privileges required to set up FGAC. The role EXECUTE_CATALOG may be used
in place of EXECUTE ON DBMS_RLS:
sys@TKYTE816> grant execute on dbms_rls to tkyte;
Grant succeeded.
sys@TKYTE816> grant create any context to tkyte;
Grant succeeded.
The next privilege is needed to create the database trigger on logon, which we will need to create later:
sys@TKYTE816> grant administer database trigger to tkyte;
Grant succeeded.
924
5254ch21cmp2.pdf 12
2/28/2005 6:52:48 PM
Fine Grained Access Control
Now we create the employee and manager accounts to represent the application users. Every user in the
EMP table will have an account named after them with the exception of SCOTT. In some databases the
user SCOTT already exist:
sys@TKYTE816> begin
2
for x in (select ename
3
from scott.emp where ename <> 'SCOTT')
4
loop
5
execute immediate 'grant connect to ' || x.ename
6
' identified by ' || x.ename;
7
end loop;
8 end;
9 /
||
PL/SQL procedure successfully completed.
sys@TKYTE816> connect scott/tiger
scott@TKYTE816> grant select on emp to tkyte;
Grant succeeded.
scott@TKYTE816> grant select on dept to tkyte;
Grant succeeded.
The simple application schema we will use is as follows. It starts with the EMP and DEPT tables copied
from the SCOTT schema. We've added declarative referential integrity to these tables as well:
scott@TKYTE816> connect tkyte/tkyte
tkyte@TKYTE816> create table dept as select * from scott.dept;
Table created.
tkyte@TKYTE816> alter table dept add constraint dept_pk primary key(deptno);
Table altered.
tkyte@TKYTE816> create table emp_base_table as select * from scott.emp;
Table created.
tkyte@TKYTE816> alter table emp_base_table add constraint
2 emp_pk primary key(empno);
Table altered.
tkyte@TKYTE816> alter table emp_base_table add constraint emp_fk_to_dept
2 foreign key (deptno) references dept(deptno);
Table altered.
Now we'll add some indexes and additional constraints. We create indexes that will be used our
application context functions for performance. We need to find out quickly if a specific user is a
manager of a department:
925
5254ch21cmp2.pdf 13
2/28/2005 6:52:49 PM
Chapter 21
tkyte@TKYTE816> create index emp_mgr_deptno_idx on emp_base_table(mgr);
Index created.
Also, we need to convert a username into an EMPNO quickly, and enforce uniqueness of the usernames
in this application:
tkyte@TKYTE816> alter table emp_base_table
2 add constraint
3 emp_ename_unique unique(ename);
Table altered.
Next, we create a view EMP from the EMP_BASE_TABLE. We will place our security policy on this view,
and our applications will use it to query, insert, update, and delete. Why we are using a view will be
explained later:
tkyte@TKYTE816> create view emp as select * from emp_base_table;
View created.
Now we will create the table that manages our assigned HR representatives. We are using an indexed
organized table (IOT) for this, since we only the query, SELECT * FROM HR_REPS WHERE USERNAME =
:X AND DEPTNO = :Y, we have no need for a traditional table structure:
tkyte@TKYTE816> create table hr_reps
2 ( username
varchar2(30),
3
deptno
number,
4
primary key(username,deptno)
5 )
6 organization index;
Table created.
We now make the assignments of HR representatives:
tkyte@TKYTE816> insert into hr_reps values ( 'KING', 10 );
1 row created.
tkyte@TKYTE816> insert into hr_reps values ( 'KING', 20 );
1 row created.
tkyte@TKYTE816> insert into hr_reps values ( 'KING', 30 );
1 row created.
tkyte@TKYTE816> insert into hr_reps values ( 'BLAKE', 10 );
1 row created.
926
5254ch21cmp2.pdf 14
2/28/2005 6:52:49 PM
Fine Grained Access Control
tkyte@TKYTE816> insert into hr_reps values ( 'BLAKE', 20 );
1 row created.
tkyte@TKYTE816> commit;
Commit complete.
Now that we have the application tables EMP, DEPT, and HR_REPS created, let's create a procedure that
will let us set an application context. This application context will contain three pieces of information;
the currently logged in users EMPNO, USERNAME, and the role they are using (one of EMP, MGR, or
HR_REP). Our dynamic predicate routine will use the role stored in application context to decide what
the WHERE clause should look like for the given user.
We use the EMP_BASE_TABLE and HR_REPS tables to make this determination. This answers the
question you were thinking about, 'why do we have a table EMP_BASE_TABLE and a view EMP that is
simply SELECT * FROM EMP_BASE_TABLE?' There are two reasons for this:
❑
We use the data in the employee table to enforce our security policy.
❑
We read this table while attempting to set an application context.
In order to read the employee data, we need the application context to be set, but in order to set the
application context we need to read the employee data. It's a 'chicken and egg' problem, which comes
first? Our solution is to create a view that all applications will use (the EMP view) and enforce our
security on this view. The original EMP_BASE_TABLE will be used by our security policy to enforce the
rules. From the EMP_BASE_TABLE we can discover who is a manager of a given department, and who
works for a given manager. The application and the end users will never use the EMP_BASE_TABLE,
only our security policy will. This last point is achieved by not granting any privileges on the base table
– the database will enforce that for us.
In this example, we have chosen to have the context set automatically upon logon. This is a standard
procedure, when possible, to have the application context set up automatically. There may be times
when you need to override this behavior. If upon logon, you do not have sufficient information to
determine what the context should be, you may need to manually set the context via a procedure call.
This would frequently occur when using a middle tier that logs all users in via the same common user.
This middle tier would have to call a database procedure passing in the name of the 'real' user to get the
application context set correctly.
The following is our 'trusted' procedure to set the context. It is trusted in that we have confidence in its
functionality since we wrote it. It helps to enforce our policies by only setting the appropriate username,
role name, and employee number in our context. Later, when we access these values, we can trust that it
was set accurately and safely. This procedure will be executed automatically by an ON LOGON trigger. As
it is coded, it is ready to support a 3-tier application that uses a connection pool, with a single database
account as well. We would grant execute on this procedure to the user account used by the connection
pool, and it would execute this procedure, sending the username as a parameter, instead of letting the
procedure use the currently logged in username.
tkyte@TKYTE816> create or replace
2 procedure set_app_role( p_username in varchar2
3
default sys_context('userenv','session_user') )
4 as
927
5254ch21cmp2.pdf 15
2/28/2005 6:52:49 PM
Chapter 21
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
l_empno
number;
l_cnt
number;
l_ctx
varchar2(255) default 'Hr_App_Ctx';
begin
dbms_session.set_context( l_ctx, 'UserName', p_username );
begin
select empno into l_empno
from emp_base_table
where ename = p_username; ;
dbms_session.set_context( l_ctx, 'Empno', l_empno );
exception
when NO_DATA_FOUND then
-- Person not in emp table - might be an HR rep.
NULL;
end;
-- First, let's see if this person is a HR_REP, if not, then
-- try MGR, if not, then set the EMP role on.
select count(*) into l_cnt
from dual
where exists
( select NULL
from hr_reps
where username = p_username
);
if ( l_cnt <> 0 )
then
dbms_session.set_context( l_ctx, 'RoleName', 'HR_REP' );
else
-- Lets see if this person is a MGR, if not, give them
-- the EMP role.
select count(*) into l_cnt
from dual
where exists
( select NULL
from emp_base_table
where mgr = to_number(sys_context(l_ctx,'Empno'))
);
if ( l_cnt <> 0 )
then
dbms_session.set_context(l_ctx, 'RoleName', 'MGR');
else
-- Everyone may use the EMP role.
dbms_session.set_context( l_ctx, 'RoleName', 'EMP' );
end if;
end if;
end;
/
Procedure created.
928
5254ch21cmp2.pdf 16
2/28/2005 6:52:49 PM
Fine Grained Access Control
Next, we create our application context. The name of the context is HR_APP_CTX (the same as we used
in the preceding procedure). When we create the context, notice how we bind it to the procedure we
just created – only that procedure can set attribute values in that context now:
tkyte@TKYTE816> create or replace context Hr_App_Ctx using SET_APP_ROLE
2 /
Context created.
Lastly, to make everything automatic, we'll use an on logon database event trigger to automatically call
our procedure to set the context values:
tkyte@TKYTE816> create or replace trigger APP_LOGON_TRIGGER
2 after logon on database
3 begin
4
set_app_role;
5 end;
6 /
Trigger created.
What we've done so far is to create a procedure that finds the correct role for the currently logged in
user. As designed, this procedure will be called at most once per session, ensuring the RoleName
attribute is set once upon logon to a static value. Since we will return different predicates based on the
value of RoleName in our security policy, we cannot permit a user to change their role after it has been
set, in Oracle 8.1.5 and 8.1.6. If we did, we would have a potential problem with cached cursors and
'old' predicates (see the Caveats section for a description of the problem we would encounter – it is
mostly solved in 8.1.7). Additionally, we look up the current user's EMPNO. This does two things for us:
❑
It verifies the end user is an employee – If we get an error NO_DATA_FOUND, we know the
person is not an employee. Since their EMPNO attribute never gets set, this person will see no
data unless they are an HR representative.
❑
It puts frequently used values into the application context. – We can now quickly access the
EMP table by the current users EMPNO, which we will do in the predicate function below.
Next, we create the database application context object, and bind it to the SET_APP_ROLE procedure we
just created. This makes it so only that procedure can set values in this context. This is what makes an
application context secure and trustworthy. We know exactly what piece of code can set values in it,
and we trust it to do this correctly (we wrote it after all). The following demonstrates what happens
when any other procedure attempts to set our context:
tkyte@TKYTE816> begin
2
dbms_session.set_context( 'Hr_App_Ctx',
3
'RoleName', 'MGR' );
4 end;
5 /
begin
*
ERROR at line 1:
ORA-01031: insufficient privileges
ORA-06512: at “SYS.DBMS_SESSION”, line 58
ORA-06512: at line 2
929
5254ch21cmp2.pdf 17
2/28/2005 6:52:49 PM
Chapter 21
Now, to test the logic of our procedure, we will attempt to use the stored procedure as various users, and
see what roles we can set and what values are placed into the context. We'll begin with SMITH. This
person is just an EMP. They manage no one and are not an HR representative. We'll use the
SESSION_CONTEXT view that is publicly available, to see what values get set in our context:
tkyte@TKYTE816> connect smith/smith
smith@TKYTE816>
smith@TKYTE816>
smith@TKYTE816>
smith@TKYTE816>
NAMESPACE
---------HR_APP_CTX
HR_APP_CTX
HR_APP_CTX
column
column
column
select
ATTRIBUTE
---------ROLENAME
USERNAME
EMPNO
namespace format a10
attribute format a10
value
format a10
* from session_context;
VALUE
---------EMP
SMITH
7369
We can see that this works as expected. SMITH gets his username, employee number, and RoleName
attribute set in the HR_APP_CTX context successfully.
Next, connecting as a different user, we see how the procedure works, and can look at a different way to
inspect a session's context values:
smith@TKYTE816> connect blake/blake
blake@TKYTE816> declare
2
l_AppCtx
dbms_session.AppCtxTabTyp;
3
l_size
number;
4 begin
5
dbms_session.list_context( l_AppCtx, l_size );
6
for i in 1 .. l_size loop
7
dbms_output.put( l_AppCtx(i).namespace || '.' );
8
dbms_output.put( l_AppCtx(i).attribute || ' = ' );
9
dbms_output.put_line( l_AppCtx(i).value );
10
end loop;
11 end;
12 /
HR_APP_CTX.ROLENAME = HR_REP
HR_APP_CTX.USERNAME = BLAKE
HR_APP_CTX.EMPNO = 7698
PL/SQL procedure successfully completed.
This time, we logged in as BLAKE who is the manager for department 30, and HR representative for
departments 10 and 30. When BLAKE logs in, we see the context is set appropriately – he is an HR_REP,
and his employee number and username are set. This also demonstrates how to list the attribute/value
pairs in a session's context using the DMBS_SESSION.LIST_CONTEXT package. This package is
executable by the general public, hence all users will be able to use this method to inspect their session's
context values, in addition to the SESSION_CONTEXT view above.
Now that we have our session context being populated the way we want, we can set about to write our
security policy function. These are the functions that will be called by the database engine at run-time to
930
5254ch21cmp2.pdf 18
2/28/2005 6:52:49 PM
Fine Grained Access Control
provide a dynamic predicate. The dynamic predicate will restrict what the user can read or write. We
have a separate function for SELECTs versus UPDATEs versus INSERT/DELETE. This is because each of
these statements allows access to different sets of rows. We are allowed to SELECT more data than we
can UPDATE (we can see our employee record but not modify it, for example). Only special users can
INSERT or DELETE, hence those predicates are different from the other two:
blake@TKYTE816> connect tkyte/tkyte
tkyte@TKYTE816> create or replace package hr_predicate_pkg
2 as
3
function select_function( p_schema in varchar2,
4
p_object in varchar2 ) return varchar2;
5
6
function update_function( p_schema in varchar2,
7
p_object in varchar2 ) return varchar2;
8
9
function insert_delete_function( p_schema in varchar2,
10
p_object in varchar2 ) return varchar2;
11 end;
12 /
Package created.
The implementation of the HR_PREDICATE_PKG is as follows. We begin with some global variables:
tkyte@TKYTE816> create or replace package body hr_predicate_pkg
2 as
3
4 g_app_ctx constant varchar2(30) default 'Hr_App_Ctx';
5
6 g_sel_pred
varchar2(1024) default NULL;
7 g_upd_pred
varchar2(1024) default NULL;
8 g_ins_del_pred varchar2(1024) default NULL;
9
The G_APP_CTX is the name of our application context. In the event that we might want to rename this
at some point in time, we used a constant global variable to hold this name and use the variable in the
subsequent code. This will let us simply change the constant, and recompile the package to use a
different context name if we ever want to. The other three global variables will hold our predicates. This
particular example was coded in Oracle 8.1.6. In this release, there is an issue with regards to cursor
caching and FGAC (see the Caveats section for all of the details). In Oracle 8.1.7 and up, this
programming technique does not need to be employed. In this case, what it means to us is that you
cannot change your role after logging in. We generate the predicates once per session, and return the
same ones for every query. We do not generate them once per query, so any changes to the role will not
take effect until you log off and log back in (or reset your session state via a call to
DBMS_SESSION.RESET_PACKAGE).
Now, for the first of our predicate functions. This one generates the predicate for a SELECT on our EMP
view. Notice that all it does is set the global variable G_SEL_PRED (Global SELect PREDicate)
depending on the value of the RoleName attribute in our context. If the context attribute is not set, this
routine raises an error, which will subsequently fail the query:
931
5254ch21cmp2.pdf 19
2/28/2005 6:52:49 PM
Chapter 21
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function select_function( p_schema in varchar2,
p_object in varchar2) return varchar2
is
begin
if ( g_sel_pred is NULL )
then
if ( sys_context( g_app_ctx, 'RoleName' ) = 'EMP' )
then
g_sel_pred:=
'empno=sys_context('''||g_app_ctx||''',''EmpNo'')';
elsif ( sys_context( g_app_ctx, 'RoleName' ) = 'MGR' )
then
g_sel_pred :=
'empno in ( select empno
from emp_base_table
start with empno =
sys_context('''||g_app_ctx||''',''EmpNo'')
connect by prior empno = mgr)';
elsif ( sys_context( g_app_ctx, 'RoleName' ) = 'HR_REP' )
then
g_sel_pred := 'deptno in
( select deptno
from hr_reps
where username =
sys_context('''||g_app_ctx||''',''UserName'') )';
else
raise_application_error( -20005, 'No Role Set' );
end if;
end if;
return g_sel_pred;
end;
And now for the routine that provides the predicate for updates. The logic is much the same as the
previous routine, however the predicates returned are different. Notice the use of 1=0 for example,
when the RoleName is set to EMP. An employee cannot update any information. MGRs can update the
records of those that work for them (but not their own record). The HR_REPs can update anyone in the
departments they manage:
47
48
49
50
51
52
53
54
55
56
57
58
function update_function( p_schema in varchar2,
p_object in varchar2 ) return varchar2
is
begin
if ( g_upd_pred is NULL )
then
if ( sys_context( g_app_ctx, 'RoleName' ) = 'EMP' )
then
g_upd_pred := '1=0';
elsif ( sys_context( g_app_ctx, 'RoleName' ) = 'MGR' )
then
932
5254ch21cmp2.pdf 20
2/28/2005 6:52:49 PM
Fine Grained Access Control
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
g_upd_pred :=
' empno in ( select empno
from emp_base_table
where mgr =
sys_context('''||g_app_ctx||
''',''EmpNo'') )';
elsif ( sys_context( g_app_ctx, 'RoleName' ) = 'HR_REP' )
then
g_upd_pred := 'deptno in
( select deptno
from hr_reps
where username =
sys_context('''||g_app_ctx||''',''UserName'') )';
else
raise_application_error( -20005, 'No Role Set' );
end if;
end if;
return g_upd_pred;
end;
Lastly, is the predicate function for INSERTs and DELETEs. In this case, 1=0 is returned for both EMPs
and MGRs alike – neither are allowed to CREATE or DELETE records, only HR_REPS may do so:
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
function insert_delete_function( p_schema in varchar2,
p_object in varchar2) return varchar2
is
begin
if ( g_ins_del_pred is NULL )
then
if ( sys_context(g_app_ctx, 'RoleName' ) in ( 'EMP', 'MGR' ) )
then
g_ins_del_pred := '1=0';
elsif ( sys_context( g_app_ctx, 'RoleName' ) = 'HR_REP' )
then
g_ins_del_pred := 'deptno in
( select deptno
from hr_reps
where username =
sys_context('''||g_app_ctx||''',''UserName'') )';
else
raise_application_error( -20005, 'No Role Set' );
end if;
end if;
return g_ins_del_pred;
end;
end;
/
Package body created.
933
5254ch21cmp2.pdf 21
2/28/2005 6:52:50 PM
Chapter 21
In the past, before we had FGAC, having one table with the above three predicates could only have
been achieved with the use of many views, one each to SELECT, UPDATE, and INSERT/DELETE from
for each role. FGAC simplifies this to just one view with a dynamic predicate.
The last step in the process is to associate our predicates with each of the DML operations, and the EMP
table itself. This is accomplished as follows:
tkyte@TKYTE816> begin
2
dbms_rls.add_policy
3
( object_name
=>
4
policy_name
=>
5
policy_function =>
6
statement_types =>
7 end;
8 /
'EMP',
'HR_APP_SELECT_POLICY',
'HR_PREDICATE_PKG.SELECT_FUNCTION',
'select' );
PL/SQL procedure successfully completed.
tkyte@TKYTE816> begin
2
dbms_rls.add_policy
3
( object_name
4
policy_name
5
policy_function
6
statement_types
7
update_check
8 end;
9 /
=>
=>
=>
=>
=>
'EMP',
'HR_APP_UPDATE_POLICY',
'HR_PREDICATE_PKG.UPDATE_FUNCTION',
'update' ,
TRUE );
PL/SQL procedure successfully completed.
tkyte@TKYTE816> begin
2
dbms_rls.add_policy
3
( object_name
=>
4
policy_name
=>
5
policy_function =>
6
statement_types =>
7
update_check
=>
8 end;
9 /
'EMP',
'HR_APP_INSERT_DELETE_POLICY',
'HR_PREDICATE_PKG.INSERT_DELETE_FUNCTION',
'insert, delete' ,
TRUE );
PL/SQL procedure successfully completed.
So, for each of the DML operations, we have associated a different predicate function. When the user
queries the EMP table, the predicate generated by the HR_PREDICATE_PKG.SELECT_FUNCTION will be
invoked. When the user updates the table, the update function in that package will be used, and so on.
Now, to test the application. We will create a package HR_APP. This package represents our application.
It has entry points to:
❑
Retrieve data (procedure listEmps)
❑
Update data (procedure updateSal)
❑
Delete data (procedure deleteAll)
❑
Insert new data (procedure insertNew)
934
5254ch21cmp2.pdf 22
2/28/2005 6:52:50 PM
Fine Grained Access Control
We will log in as various users, with different roles, and monitor the behavior of our application. This
will show us FGAC at work.
The following is our specification for our application:
tkyte@TKYTE816> create or replace package hr_app
2 as
3
procedure listEmps;
4
5
procedure updateSal;
6
7
procedure deleteAll;
8
9
procedure insertNew( p_deptno in number );
10 end;
11 /
Package created.
And now the package body. It is a somewhat contrived example as the UPDATE routine attempts to
update as many rows as possible with a constant value. This is so that we can see exactly how many, and
also which, rows are affected. The other routines are similar in nature – reporting back what they are
able to do, and how many rows they are able to do it to:
tkyte@TKYTE816> create or replace package body hr_app
2 as
3
4 procedure listEmps
5 as
6
l_cnt number default 0;
7 begin
8
dbms_output.put_line
9
( rpad('ename',10) || rpad('sal', 6 ) || ' ' ||
10
rpad('dname',10) || rpad('mgr',5) || ' ' ||
11
rpad('dno',3) );
12
for x in ( select ename, sal, dname, mgr, emp.deptno
13
from emp, dept
14
where emp.deptno = dept.deptno )
15
loop
16
dbms_output.put_line(rpad(nvl(x.ename,'(null)'),10) ||
17
to_char(x.sal,'9,999') || ' ' ||
18
rpad(x.dname,10) ||
19
to_char(x.mgr,'9999') || ' ' ||
20
to_char(x.deptno,'99') );
21
l_cnt := l_cnt + 1;
22
end loop;
23
dbms_output.put_line( l_cnt || ' rows selected' );
24 end;
25
26
27 procedure updateSal
28 is
29 begin
30
update emp set sal = 9999;
31
dbms_output.put_line( sql%rowcount || ' rows updated' );
935
5254ch21cmp2.pdf 23
2/28/2005 6:52:50 PM
Chapter 21
32 end;
33
34 procedure deleteAll
35 is
36 begin
37
delete from emp where empno <> sys_context('Hr_app_Ctx','EMPNO' );
38
dbms_output.put_line( sql%rowcount || ' rows deleted' );
39 end;
40
41 procedure insertNew( p_deptno in number )
42 as
43 begin
44
insert into emp (empno, deptno, sal) values (123, p_deptno, 1111);
45 end;
46
47 end hr_app;
48 /
Package body created.
tkyte@TKYTE816> grant execute on hr_app to public
2 /
Grant succeeded.
So, this is our 'application'. The listEmps routine shows every record we can see in the EMP view. The
updateSal routine updates every record we are allowed to. The deleteAll routine deletes every
record we are allowed to, with the exception of our own record. The insertNew routine tries to create
a new employee in the department we request. This application simply tests all of the DML operations
we might attempt on the EMP view (it is a rather contrived application to say the least).
Now, as different users, we will log in, and test the functionality of our application. First, we will log in
and review our application context values:
tkyte@TKYTE816> connect adams/adams
adams@TKYTE816>
adams@TKYTE816>
adams@TKYTE816>
adams@TKYTE816>
NAMESPACE
---------HR_APP_CTX
HR_APP_CTX
HR_APP_CTX
column
column
column
select
ATTRIBUTE
---------ROLENAME
USERNAME
EMPNO
namespace format a10
attribute format a10
value
format a10
* from session_context;
VALUE
---------EMP
ADAMS
7876
adams@TKYTE816> set serveroutput on
Now, since we are just an EMP we expect that listEmps will show our record, and nothing else:
adams@TKYTE816> exec tkyte.hr_app.listEmps
ename
sal
dname
mgr
dno
ADAMS
1,100 RESEARCH
7788 20
936
5254ch21cmp2.pdf 24
2/28/2005 6:52:50 PM
Fine Grained Access Control
1 rows selected
PL/SQL procedure successfully completed.
Again, since we are a just an EMP, we do not expect to be able to UPDATE and DELETE. The following
tests that:
adams@TKYTE816> exec tkyte.hr_app.updateSal
0 rows updated
PL/SQL procedure successfully completed.
adams@TKYTE816> exec tkyte.hr_app.deleteAll
0 rows deleted
PL/SQL procedure successfully completed.
Lastly, we'll test the INSERT. Here, we will get an error back from the database. This differs from the
UPDATE and DELETE cases above, in this particular example. The attempts to UPDATE or DELETE did
not fail, since we precluded the user from seeing any data to UPDATE or DELETE in the first place. When
we go to INSERT however, the row is created, found to be in violation of the policy, and then removed.
The database raises an error in this case:
adams@TKYTE816> exec tkyte.hr_app.insertNew(20);
BEGIN tkyte.hr_app.insertNew(20); END;
*
ERROR at line 1:
ORA-28115: policy with check option violation
ORA-06512: at “TKYTE.HR_APP”, line 36
ORA-06512: at line 1
So, the above shows we can see only our record. We cannot UPDATE any data whatsoever, we cannot
DELETE any records, and INSERTing a new employee fails as well. This is exactly what we intended and
it happens transparently. The application, HR_APP, does nothing special to enforce these rules. The
database is doing it for us now, from logon to logoff, no matter what tool or environment we use to
connect.
Next, we log in as a MGR and see what happens. First, we will once again print out our context to see
what is in there, and then list out the employees we can 'see':
adams@TKYTE816> @connect jones/jones
jones@TKYTE816> set serveroutput on
jones@TKYTE816> select * from session_context;
NAMESPACE
---------HR_APP_CTX
HR_APP_CTX
ATTRIBUTE
---------ROLENAME
USERNAME
VALUE
---------MGR
JONES
937
5254ch21cmp2.pdf 25
2/28/2005 6:52:50 PM
Chapter 21
HR_APP_CTX EMPNO
7566
jones@TKYTE816> exec tkyte.hr_app.listEmps
ename
sal
dname
mgr
dno
SMITH
800 RESEARCH
7902 20
JONES
2,975 RESEARCH
7839 20
SCOTT
9,999 RESEARCH
7566 20
ADAMS
1,100 RESEARCH
7788 20
FORD
3,000 RESEARCH
7566 20
5 rows selected
PL/SQL procedure successfully completed.
This shows that this time, we can see many more than one record in the EMP table. In fact, we 'see' all of
department 20 – JONES is the MGR of department 20 in the EMP table. Next, we'll run the UPDATE
routine and review the changes made:
jones@TKYTE816> exec tkyte.hr_app.updateSal
2 rows updated
PL/SQL procedure successfully completed.
jones@TKYTE816> exec tkyte.hr_app.listEmps
ename
sal
dname
mgr
dno
SMITH
800 RESEARCH
7902 20
JONES
2,975 RESEARCH
7839 20
SCOTT
9,999 RESEARCH
7566 20
ADAMS
1,100 RESEARCH
7788 20
FORD
9,999 RESEARCH
7566 20
5 rows selected
As per our logic, we can UPDATE only the records of our direct reports. The UPDATE affected only the
two records that represent the employees reporting directly to JONES. Next we try to DELETE and
INSERT. Since we are a MGR and not an HR_REP, we will not be able to DELETE any records and the
INSERT will fail:
jones@TKYTE816> exec tkyte.hr_app.deleteAll
0 rows deleted
PL/SQL procedure successfully completed.
jones@TKYTE816> exec tkyte.hr_app.insertNew(20)
BEGIN tkyte.hr_app.insertNew(20); END;
*
ERROR at line 1:
ORA-28115: policy with check option violation
ORA-06512: at “TKYTE.HR_APP”, line 44
ORA-06512: at line 1
938
5254ch21cmp2.pdf 26
2/28/2005 6:52:50 PM
Fine Grained Access Control
So, this time as a MGR we can:
❑
See more than just our data. We see everyone who reports to us, and their reports, and so on
(a hierarchy).
❑
UPDATE some of the data. Specifically, we can UPDATE only those records belonging to people
who report directly to us, as required.
❑
Still not DELETE or INSERT any data, as required.
Lastly, we'll log in as an HR_REP and review the behavior of our application in this role. We'll start
again by showing the application context state and print out the rows we can see. This time, we'll see
the entire EMP table – KING has access to all three departments:
jones@TKYTE816> connect king/king
king@TKYTE816> select * from session_context;
NAMESPACE
---------HR_APP_CTX
HR_APP_CTX
HR_APP_CTX
ATTRIBUTE
---------ROLENAME
USERNAME
EMPNO
VALUE
---------HR_REP
KING
7839
king@TKYTE816> exec tkyte.hr_app.listEmps
ename
sal
dname
mgr
dno
CLARK
2,450 ACCOUNTING 7839 10
KING
5,000 ACCOUNTING 10
MILLER
1,300 ACCOUNTING 7782 10
SMITH
800 RESEARCH
7902 20
JONES
2,975 RESEARCH
7839 20
SCOTT
9,999 RESEARCH
7566 20
ADAMS
1,100 RESEARCH
7788 20
FORD
9,999 RESEARCH
7566 20
ALLEN
1,600 SALES
7698 30
WARD
1,250 SALES
7698 30
MARTIN
1,250 SALES
7698 30
BLAKE
2,850 SALES
7839 30
TURNER
1,500 SALES
7698 30
JAMES
950 SALES
7698 30
14 rows selected
PL/SQL procedure successfully completed.
Now, we'll execute an UPDATE to see what data we can modify. In this case, every row will be updated:
king@TKYTE816> exec tkyte.hr_app.updateSal
14 rows updated
PL/SQL procedure successfully completed.
king@TKYTE816> exec tkyte.hr_app.listEmps
ename
sal
dname
mgr
dno
CLARK
9,999 ACCOUNTING 7839 10
KING
9,999 ACCOUNTING 10
939
5254ch21cmp2.pdf 27
2/28/2005 6:52:50 PM
Chapter 21
MILLER
9,999
SMITH
9,999
JONES
9,999
SCOTT
9,999
ADAMS
9,999
FORD
9,999
ALLEN
9,999
WARD
9,999
MARTIN
9,999
BLAKE
9,999
TURNER
9,999
JAMES
9,999
14 rows selected
ACCOUNTING
RESEARCH
RESEARCH
RESEARCH
RESEARCH
RESEARCH
SALES
SALES
SALES
SALES
SALES
SALES
7782
7902
7839
7566
7788
7566
7698
7698
7698
7839
7698
7698
10
20
20
20
20
20
30
30
30
30
30
30
PL/SQL procedure successfully completed.
The value of 9,999 in the SAL column verifies that we modified every row in the table. Next, we'll try
out the DELETE. Remember, the DeleteAll API call we developed earlier will not DELETE the
currently logged in users record by design:
king@TKYTE816> exec tkyte.hr_app.deleteAll
13 rows deleted
PL/SQL procedure successfully completed.
This shows we can DELETE records for the first time. Let's try creating one:
king@TKYTE816> exec tkyte.hr_app.insertNew(20)
PL/SQL procedure successfully completed.
king@TKYTE816> exec tkyte.hr_app.listEmps
ename
sal
dname
mgr
dno
KING
9,999 ACCOUNTING 10
(null)
1,111 RESEARCH
20
2 rows selected
PL/SQL procedure successfully completed.
Sure enough, it worked this time, as the rules for an HR_Rep were implemented. This completes the
testing of our three roles. Our requirements have been met, we secured the data, and did it
transparently to the application.
Caveats
As with any feature, there are some nuances that need to be noted in the way this feature functions. This
section attempts to address them, each in turn.
940
5254ch21cmp2.pdf 28
2/28/2005 6:52:51 PM
Fine Grained Access Control
Referential Integrity
FGAC may or may not work the way you expect it to with regards to referential integrity. It depends on
what you think should happen I suppose. I myself wasn't really sure what would happen right away.
As it turns out, referential integrity will bypass FGAC. With it, I can read a table, delete from it, and
update it, even if I cannot issue the SELECT, DELETE, or INSERT against that table. This is the way it is
supposed to work, so it is something you must consider in your design when using FGAC.
We will look at the cases of:
❑
Discovering data values I should not be able to see. This is what is known as a covert channel.
I cannot directly query the data. However, I can prove the existence (or lack thereof) of some
data values in a table using a foreign key.
❑
Being able to delete from a table via an ON DELETE CASCADE integrity constraint.
❑
Being able to update a table via an ON UPDATE SET NULL integrity constraint.
We will look at these three cases using a somewhat contrived example with two tables P (parent), and C
(child):
tkyte@TKYTE816> create table p ( x int primary key );
Table created.
tkyte@TKYTE816> create table c ( x int references p on delete cascade );
Table created.
The Covert Channel
The covert channel here is that we can discover primary key values of rows in P by inserting into C, and
watching what happens. I'll be able to determine if a row exists in P or not, via this method. We'll start
by implementing a predicate function that always returns a WHERE clause that evaluates to False:
tkyte@TKYTE816> create or replace function pred_function
2 ( p_schema in varchar2, p_object in varchar2 )
3 return varchar2
4 as
5 begin
6
return '1=0';
7 end;
8 /
Function created.
and using this predicate function to restrict SELECT access on P:
tkyte@TKYTE816> begin
2
dbms_rls.add_policy
3
( object_name => 'P',
4
policy_name => 'P_POLICY',
941
5254ch21cmp2.pdf 29
2/28/2005 6:52:51 PM
Chapter 21
5
6
7
8
policy_function => 'pred_function',
statement_types => 'select' );
end;
/
PL/SQL procedure successfully completed.
Now, we can still INSERT into P (and UPDATE/DELETE from P), we just cannot SELECT anything from
it. We'll start by putting a value into P:
tkyte@TKYTE816> insert into p values ( 1 );
1 row created.
tkyte@TKYTE816> select * from p;
no rows selected
Our predicate prevents us from seeing this row, but we can tell it is there simply by inserting into C:
tkyte@TKYTE816> insert into c values ( 1 );
1 row created.
tkyte@TKYTE816> insert into c values ( 2 );
insert into c values ( 2 )
*
ERROR at line 1:
ORA-02291: integrity constraint (TKYTE.SYS_C003873) violated - parent key not
found
So, we can now see that the value 1 must be in P, and the value 2 is not, by the fact that C can have a
row with 1 but not 2. Referential integrity is able to read through FGAC. This may be confusing to an
application such as an ad-hoc query tool that generates queries based on relationships in the data
dictionary. If it queries C, all rows come back. If it joins P and C, no data will be found.
It should also be noted that a similar covert channel exists from the parent to the child. If the policy
above were placed on C instead of P, and C did not have the ON DELETE CASCADE clause (in other
words, just a references), we would be able to determine what values of X were in C by deleting from P.
DELETEs on P would raise an error if there were child rows in C and succeed otherwise, even though we
cannot SELECT any rows from C normally.
Deleting Rows
This is exposed via the ON DELETE CASCADE referential integrity clause. If we drop the policy on P and
instead, use the same function as a DELETE policy on C as follows:
tkyte@TKYTE816> begin
2
dbms_rls.drop_policy
3
( 'TKYTE', 'P', 'P_POLICY' );
4 end;
5 /
PL/SQL procedure successfully completed.
942
5254ch21cmp2.pdf 30
2/28/2005 6:52:51 PM
Fine Grained Access Control
tkyte@TKYTE816> begin
2
dbms_rls.add_policy
3
( object_name => 'C',
4
policy_name => 'C_POLICY',
5
policy_function => 'pred_function',
6
statement_types => 'DELETE' );
7 end;
8 /
PL/SQL procedure successfully completed.
we'll find we can delete no rows in C using SQL:
tkyte@TKYTE816> delete from C;
0 rows deleted.
The policy we put in place prevents this. We can see that there is a row in C (from the prior INSERT
above):
tkyte@TKYTE816> select * from C;
X
---------1
The simple act of deleting the parent row:
tkyte@TKYTE816> delete from P;
1 row deleted.
will in fact read through the FGAC policy once again, and DELETE that row in C for us:
tkyte@TKYTE816> select * from C;
no rows selected
Updating Rows
A very similar condition to the DELETE example exists with regards to ON DELETE SET NULL. Here, we
will change the example so that we can use referential integrity to update rows in C we cannot update
via SQL. We'll start by rebuilding C with an ON DELETE SET NULL constraint:
tkyte@TKYTE816> drop table c;
Table dropped.
tkyte@TKYTE816> create table c ( x int references p on delete set null );
Table created.
943
5254ch21cmp2.pdf 31
2/28/2005 6:52:51 PM
Chapter 21
tkyte@TKYTE816> insert into p values ( 1 );
1 row created.
tkyte@TKYTE816> insert into c values ( 1 );
1 row created.
Next, we'll associate that same predicate function from above with the table C on UPDATE, and set the
UPDATE_CHECK flag to TRUE. This will prevent us from updating any rows:
tkyte@TKYTE816> begin
2
dbms_rls.add_policy
3
( object_name => 'C',
4
policy_name => 'C_POLICY',
5
policy_function => 'pred_function',
6
statement_types => 'UPDATE',
7
update_check => TRUE );
8 end;
9 /
PL/SQL procedure successfully completed.
tkyte@TKYTE816> update c set x = NULL;
0 rows updated.
tkyte@TKYTE816> select * from c;
X
---------1
So, we are not able to update any rows in C using SQL. However, a simple DELETE on the parent table
P shows us:
tkyte@TKYTE816> delete from p;
1 row deleted.
tkyte@TKYTE816> select * from c;
X
----------
In this fashion, we can update C in a roundabout way. There is another way to demonstrate this, so we'll
start by resetting the example:
tkyte@TKYTE816> delete from c;
1 row deleted.
tkyte@TKYTE816> insert into p values ( 1 );
944
5254ch21cmp2.pdf 32
2/28/2005 6:52:51 PM
Fine Grained Access Control
1 row created.
tkyte@TKYTE816> insert into c values ( 1 );
1 row created.
and then rewriting the function so we can update rows in C to any value except Null:
tkyte@TKYTE816> create or replace function pred_function
2 ( p_schema in varchar2, p_object in varchar2 )
3 return varchar2
4 as
5 begin
6
return 'x is not null';
7 end;
8 /
Function created.
tkyte@TKYTE816> update c set x = NULL;
update c set x = NULL
*
ERROR at line 1:
ORA-28115: policy with check option violation
This update failed because the predicate X IS NOT NULL was not satisfied after the update. Now when
we DELETE from P again:
tkyte@TKYTE816> delete from p;
1 row deleted.
tkyte@TKYTE816> select * from c;
X
----------
the row in C is set to the value we could not set using SQL.
Cursor Caching
One important implementation feature of our security predicate function shown earlier, in section
Example 1: Implementing a Security Policy, is the fact that during a given session, this function returns a
constant predicate – this is critical. If we look at the function we used above once more, we see the logic
is:
...
5
6
7
8
9
10
as
begin
if ( user = p_schema ) then
return '';
else
return 'owner = USER';
945
5254ch21cmp2.pdf 33
2/28/2005 6:52:51 PM
Chapter 21
11
12
...
end if;
end;
This predicate function returns either no predicate or owner = USER. During a given session it will
consistently return the same predicate. There is no chance that we would retrieve the predicate owner =
USER and, later in that same session, retrieve the empty predicate. To understand why this is absolutely
critical to a correctly designed FGAC application, we must understand when the predicate is associated
with a query, and how different environments such as PL/SQL, Pro*C, OCI, JDBC, ODBC, and so on,
handle this.
Let's say we wrote a predicate function that looked something like this:
SQL>
2
3
4
5
6
7
8
9
10
11
12
13
create or replace function rls_examp
( p_schema in varchar2, p_object in varchar2 )
return varchar2
as
begin
if ( sys_context( 'myctx', 'x' ) is not null )
then
return 'x > 0';
else
return '1=0';
end if;
end;
/
Function created.
This says that if the attribute x is set in the context, the predicate should be x > 0. If the context
attribute x is not set, the predicate is 1=0. If we create a table T, put data into it, and add the policy and
context as follows:
SQL> create table t ( x int );
Table created.
SQL> insert into t values ( 1234 );
1 row created.
SQL> begin
2
dbms_rls.add_policy
3
( object_schema
=> user,
4
object_name => 'T',
5
policy_name => 'T_POLICY',
6
function_schema => user,
7
policy_function => 'rls_examp',
8
statement_types => 'select' );
9 end;
10 /
PL/SQL procedure successfully completed.
946
5254ch21cmp2.pdf 34
2/28/2005 6:52:51 PM
Fine Grained Access Control
SQL>
2
3
4
5
6
create or replace procedure set_ctx( p_val in varchar2 )
as
begin
dbms_session.set_context( 'myctx', 'x', p_val );
end;
/
Procedure created.
SQL> create or replace context myctx using set_ctx;
Context created.
it would appear that if the context is set, we would see one row. If the context is not set, we would see
zero rows. In fact, if we test in SQL*PLUS using just SQL, the following would be the case:
SQL> exec set_ctx( null );
PL/SQL procedure successfully completed.
SQL> select * from t;
no rows selected
SQL> exec set_ctx( 1 );
PL/SQL procedure successfully completed.
SQL> select * from t;
X
---------1234
So, it would appear that we are set to go. The dynamic predicate is working as we expected. In fact, if
we use PL/SQL (or Pro*C, or well-coded OCI/JDBC/ODBC applications, as well as many other
execution environments) we find that the above does not hold true. For example, lets code a small
PL/SQL routine:
SQL>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
create or replace procedure dump_t
( some_input in number default NULL )
as
begin
dbms_output.put_line
( '*** Output from SELECT * FROM T' );
for x in (select * from t ) loop
dbms_output.put_line( x.x );
end loop;
if ( some_input is not null )
then
dbms_output.put_line
( '*** Output from another SELECT * FROM T' );
947
5254ch21cmp2.pdf 35
2/28/2005 6:52:51 PM
Chapter 21
16
17
18
19
20
21
22
for x in (select * from t ) loop
dbms_output.put_line( x.x );
end loop;
end if;
end;
/
Procedure created.
This routine simply issues a SELECT * FROM T once in the procedure if no inputs are passed, and twice
in the procedure if some input is passed. Let's execute this procedure and observe the outcome. We'll
start by running the procedure with the context value set to Null (hence the predicate would be 1=0, in
other words, no rows):
SQL> set serveroutput on
SQL> exec set_ctx( NULL )
PL/SQL procedure successfully completed.
SQL> exec dump_t
*** Output from SELECT * FROM T
PL/SQL procedure successfully completed.
As expected, no data was returned. Now, let's set the context value so the predicate will be x > 0. We
will call DUMP_T in a manner so has it execute both queries this time. What will happen in Oracle 8.1.5
and 8.1.6 is the following:
SQL> exec set_ctx( 1 )
PL/SQL procedure successfully completed.
SQL> exec dump_t( 0 )
*** Output from SELECT * FROM T
*** Output from another SELECT * FROM T
1234
PL/SQL procedure successfully completed.
The first query, the one that was executed with the Null context initially, still returns no data. Its cursor
was cached; it was not re-parsed.
When we run the procedure with the context attribute 'x' set to Null, we get the expected results
(because it's the first time in this session we are running this procedure). We set the context attribute
'x' to a non-Null value, and find we get 'ambiguous' results. The first SELECT * FROM T in the
procedure still returns no rows – it is apparently still using the predicate 1=0. The second query (which
we did not execute the first time) returns, what appears to be, the correct results. It is apparently using
the predicate x > 0 as we expect.
948
5254ch21cmp2.pdf 36
2/28/2005 6:52:52 PM
Fine Grained Access Control
Why did the first SELECT in this procedure not use the predicate we anticipated? It is because of an
optimization called cursor caching. PL/SQL, and many other execution environments, do not really
'close' a cursor when you close a cursor. The above example may be easily reproduced in Pro*C for
example if, the pre-compile option release_cursor is left to default to NO. If you take the same code
and pre-compile with release_cursor=YES, the Pro*C program would behave more like queries in
SQL*PLUS. The predicate used by DBMS_RLS is assigned to a query during the PARSE phase. The first
query SELECT * FROM T is getting parsed during the first execution of the stored procedure, when the
predicate was in fact 1=0. The PL/SQL engine is caching this parsed cursor for you. The second time
we execute the stored procedure, PL/SQL simply reused the parsed cursor from the first SELECT *
FROM T. This parsed query has the predicate 1=0. The predicate function was not invoked at all this
time around. Since we also passed some inputs to the procedure, PL/SQL executed the second query.
This query however, did not already have an opened, parsed cursor for it, so it parsed during this
execution, when the context attribute was not Null. The second SELECT * FROM T has the predicate x>0
associated with it. This is the cause of the ambiguity. Since we have no control over the caching of these
cursors in general, a security predicate function that may return more then one predicate per session
should be avoided at all cost. Subtle, hard to detect bugs in your application will be the result otherwise.
Earlier, in the HR example, we demonstrated how to implement a security predicate function that
cannot return more than one predicate per session. This ensured that:
❑
Your results are consistent from query to query with respect to FGAC.
❑
You are never tempted to change the predicate in the middle of a session. Strange, and
unpredictable results will be the outcome if you do.
❑
You are made to enforce your security policy in this single predicate for a user, rather than
attempting to return a predicate customized for the current environment in which the user is
running.
In Oracle 8.1.7 and up, you should expect the following outcome:
tkyte@dev817> exec dump_t( 0 )
*** Output from SELECT * FROM T
1234
*** Output from another SELECT * FROM T
1234
PL/SQL procedure successfully completed.
In 8.1.7 and up, the database will now re-parse this query if the session context has changed, and it has
a security policy associated with it to avoid issues as described above. We need to stress the session
context changing bit of the previous statement. If we do not use a session context to determine our
predicate, this cursor caching issue comes back into play. Consider a system where the predicates are
stored as data in a database table, a sort of a table driven policy function. Here, if the contents of the
data table changes, causing the predicate that is returned to change, we get into the same issues with
8.1.7 as we did in 8.1.6 and before. If we change the above example to include a database table:
tkyte@TKYTE816> create table policy_rules_table
2 ( predicate_piece varchar2(255)
3 );
Table created.
tkyte@TKYTE816> insert into policy_rules_table values ( 'x > 0' );
1 row created.
949
5254ch21cmp2.pdf 37
2/28/2005 6:52:52 PM
Chapter 21
and change the policy function to be table driven:
tkyte@TKYTE816> create or replace function rls_examp
2 ( p_schema in varchar2, p_object in varchar2 )
3 return varchar2
4 as
5
l_predicate_piece varchar2(255);
6 begin
7
select predicate_piece into l_predicate_piece
8
from policy_rules_table;
9
10
return l_predicate_piece;
11 end;
12 /
Function created.
we will now expect the following output from DUMP_T if we change the predicate after executing DUMP_T
with no inputs, but before executing it with inputs:
tkyte@DEV817> exec dump_t
*** Output from SELECT * FROM T
1234
PL/SQL procedure successfully completed.
tkyte@DEV817> update policy_rules_table set predicate_piece = '1=0';
1 row updated.
tkyte@DEV817> exec dump_t(0)
*** Output from SELECT * FROM T
1234
*** Output from another SELECT * FROM T
PL/SQL procedure successfully completed.
Notice how during the first execution, the predicate was x>0; this returned the row from table T. After
we executed this procedure, we modified the predicate (this update could be done from another session,
by an administrator for example). When we executed DUMP_T for the second time, passing it an input so
as to have it execute the second query in addition to the first, we see that the first query is still using the
old predicate x>0, whereas the second query is obviously using the second predicate 1=0 we just put
into the POLICY_RULES table. You must use caution with regards to this cursor caching, even in 8.1.7
and up unless you use an application context as well as the table.
I would like to point out that it is very safe to change the value of SYS_CONTEXT in the middle of an
application. Their changes will take effect and be used on the next execution of the query. Since they
are bind variables, they are evaluated during the 'execute' phase of the query, and not during the parse,
so their values do not remain fixed at parse-time. It is only the text of the predicate itself that should not
change during the execution of an application. Here is a small example demonstrating this. We will log
out, and log back in (to clear out our previous session from above with the cached cursors), and reimplement our RLS_EXAMP function. Then, we'll do the same sort of logic we did above, and see what
happens:
950
5254ch21cmp2.pdf 38
2/28/2005 6:52:52 PM
Fine Grained Access Control
tkyte@TKYTE816> connect tkyte/tkyte
tkyte@TKYTE816> create or replace function rls_examp
2 ( p_schema in varchar2, p_object in varchar2 )
3 return varchar2
4 as
5 begin
6
return 'x > sys_context(''myctx'',''x'')';
7 end;
8 /
Function created.
tkyte@TKYTE816> set serveroutput on
tkyte@TKYTE816> exec set_ctx( NULL )
PL/SQL procedure successfully completed.
tkyte@TKYTE816> exec dump_t
*** Output from SELECT * FROM T
PL/SQL procedure successfully completed.
tkyte@TKYTE816> exec set_ctx( 1 )
PL/SQL procedure successfully completed.
tkyte@TKYTE816> exec dump_t( 0 )
*** Output from SELECT * FROM T
1234
*** Output from another SELECT * FROM T
1234
PL/SQL procedure successfully completed.
This time, both queries return the same result. This simply because they both use the same WHERE
clause, and dynamically access the value of the application context in the query itself.
I should mention that there are cases where changing the predicate in the middle of a session may be
desirable. The client applications that access objects, which employ policies that can change predicates
in the middle of a session must be coded in a specific fashion to take advantage of this. For example, in
PL/SQL, we would have to code the application using dynamic SQL entirely, to avoid the cursor
caching. If you are employing this dynamic predicate method, then you should bear in mind that the
results will depend on how the client application is coded, therefore you should not be enforcing a
security policy with this use of the feature. We will not be discussing this possible use of the DBMS_RLS
feature, but rather will concentrate on its intended use, which is to secure data.
Export/Import
We mentioned this issue previously. Care must be taken when using the EXP tool to export data, and
IMP to import it. Since the two issues are different, we'll look at each in turn. For this caveat, we'll
extend the prior example by changing the policy T_POLICY. We'll have it so that it will be in effect for
INSERTs, as well as SELECTs this time:
951
5254ch21cmp2.pdf 39
2/28/2005 6:52:52 PM
Chapter 21
tkyte@TKYTE816> begin
2
dbms_rls.drop_policy( 'TKYTE', 'T', 'T_POLICY' );
3 end;
4 /
PL/SQL procedure successfully completed.
tkyte@TKYTE816> begin
2
dbms_rls.add_policy
3
( object_name => 'T',
4
policy_name => 'T_POLICY',
5
policy_function => 'rls_examp',
6
statement_types => 'select, insert',
7
update_check
=> TRUE );
8 end;
9 /
PL/SQL procedure successfully completed.
Once we do this, the following behavior will be observed:
tkyte@TKYTE816> delete from t;
1 row deleted.
tkyte@TKYTE816> commit;
Commit complete.
tkyte@TKYTE816> exec set_ctx( null );
PL/SQL procedure successfully completed.
tkyte@TKYTE816> insert into t values ( 1 );
insert into t values ( 1 )
*
ERROR at line 1:
ORA-28115: policy with check option violation
tkyte@TKYTE816> exec set_ctx( 0 ) ;
PL/SQL procedure successfully completed.
tkyte@TKYTE816> insert into t values ( 1 );
1 row created.
So now the context must be set to SELECT and INSERT data.
Export Issues
By default EXP will execute in a 'conventional' path mode. It will use SQL to read all of the data. If we
use EXP to extract the table T from the database, the following will be observed (note that T has 1 row in
it right now due to our INSERT above):
952
5254ch21cmp2.pdf 40
2/28/2005 6:52:52 PM
Fine Grained Access Control
C:\fgac>exp userid=tkyte/tkyte tables=t
Export: Release 8.1.6.0.0 - Production on Mon Apr 16 16:29:25 2001
(c) Copyright 1999 Oracle Corporation. All rights reserved.
Connected to: Oracle8i Enterprise Edition Release 8.1.6.0.0 - Production
With the Partitioning option
JServer Release 8.1.6.0.0 - Production
Export done in WE8ISO8859P1 character set and WE8ISO8859P1 NCHAR character set
About to export specified tables via Conventional Path ...
EXP-00079: Data in table “T” is protected. Conventional path may only be exporting
partial table.
. . exporting table
T
0 rows exported
Export terminated successfully with warnings.
Notice that EXP was kind enough to notify us that the table we exported may be only partially exported,
since the conventional path was used. The solution to this is to use the SYS (or any account connected
as SYSDBA) account to export. FGAC is not in effect for the SYS user:
C:\fgac>exp userid=sys/manager tables=tkyte.t
Export: Release 8.1.6.0.0 - Production on Mon Apr 16 16:35:21 2001
(c) Copyright 1999 Oracle Corporation. All rights reserved.
Connected to: Oracle8i Enterprise Edition Release 8.1.6.0.0 - Production
With the Partitioning option
JServer Release 8.1.6.0.0 - Production
Export done in WE8ISO8859P1 character set and WE8ISO8859P1 NCHAR character set
About to export specified tables via Conventional Path ...
Current user changed to TKYTE
. . exporting table
T
Export terminated successfully without warnings.
1 rows exported
Another valid option would be to use DBMS_RLS.ENABLE_POLICY to disable the policy temporarily,
and re-enable it after the export. This is not entirely desirable as the table is left unprotected during this
period of time.
In some versions of Oracle 8.1.5, a direct path export bypassed FGAC erroneously. That is, by
adding direct=true, all of the data would be exported. You should not rely on this, as it has
since been corrected in all later releases. In these releases you will get:
About to export specified tables via Direct Path ...
EXP-00080: Data in table “T” is protected. Using conventional mode.
EXP-00079: Data in table “T” is protected. Conventional path may only...
EXP will automatically drop into a conventional path export for protected tables.
953
5254ch21cmp2.pdf 41
2/28/2005 6:52:52 PM
Chapter 21
Import Issues
This is only an issue if you have a FGAC policy on a table that is in effect for INSERTs with the
UPDATE_CHECK set to True. In this case, IMP may reject some rows if your predicate function returns a
predicate they cannot satisfy. In the above example, this is the case. Unless we set the context, no rows
can be inserted (the context value is Null). Hence, if we take the above EXP we created, and try to
import the data back in:
C:\fgac>imp userid=tkyte/tkyte full=y ignore=y
Import: Release 8.1.6.0.0 - Production on Mon Apr 16 16:37:33 2001
(c) Copyright 1999 Oracle Corporation.
All rights reserved.
Connected to: Oracle8i Enterprise Edition Release 8.1.6.0.0 - Production
With the Partitioning option
JServer Release 8.1.6.0.0 - Production
Export file created by EXPORT:V08.01.06 via conventional path
Warning: the objects were exported by SYS, not by you
import done in WE8ISO8859P1 character set and WE8ISO8859P1 NCHAR character set
. importing SYS's objects into TKYTE
. . importing table
“T”
IMP-00058: ORACLE error 28115 encountered
ORA-28115: policy with check option violation
IMP-00017: following statement failed with ORACLE error 28101:
“BEGIN
DBMS_RLS.ADD_POLICY('TKYTE', 'T','T_POLICY','TKYTE','RLS_EXAMP','SE”
“LECT,INSERT',TRUE,TRUE); END;”
IMP-00003: ORACLE error 28101 encountered
ORA-28101: policy already exists
ORA-06512: at “SYS.DBMS_RLS”, line 0
ORA-06512: at line 1
Import terminated successfully with warnings.
and our rows are not inserted. Once again, the work around is to import as SYS or SYSDBA:
C:\fgac>imp userid=sys/manager full=y ignore=y
Import: Release 8.1.6.0.0 - Production on Mon Apr 16 16:40:56 2001
(c) Copyright 1999 Oracle Corporation.
All rights reserved.
Connected to: Oracle8i Enterprise Edition Release 8.1.6.0.0 - Production
With the Partitioning option
JServer Release 8.1.6.0.0 - Production
Export file created by EXPORT:V08.01.06 via conventional path
import done in WE8ISO8859P1 character set and WE8ISO8859P1 NCHAR character set
. importing SYS's objects into SYS
. importing TKYTE's objects into TKYTE
. . importing table
“T”
1 rows imported
954
5254ch21cmp2.pdf 42
2/28/2005 6:52:52 PM
Fine Grained Access Control
Another valid option would be to use DBMS_RLS.ENABLE_POLICY to disable the policy temporarily,
and re-enable it after the import. As with EXP, this is not entirely desirable as the table is left
unprotected during that period of time.
Debugging
One utility I use frequently when writing predicate functions is a simple 'debug' package. This package,
authored by Christopher Beck, also of Oracle, allows us to instrument our code with 'print' statements.
This package also allows us to liberally put in our code, statements like:
create function foo ...
as
...
begin
debug.f( 'Enter procedure foo' );
if ( some_condition ) then
l_predicate := 'x=1';
end if;
debug.f( 'Going to return the predicate ''%s''', l_predicate );
return l_predicate;
end;
So, debug.f works similarly to the C printf function, and is implemented using UTL_FILE. It creates
programmer-managed trace files on the database server. These trace files contain your debug
statements, things you can use to see what is happening in your code. Since the database kernel is
invoking your code in the background, debugging it can be hard. Traditional tools like DBMS_OUTPUT
and the PL/SQL debugger are not very useful here. Having these trace files can save lots of time. The
scripts you can download (see the Apress web site) contain this debug package, and comments on
setting it up and using it.
This package is extremely invaluable is diagnosing exactly what is going on in your security policy
functions, and I strongly urge you to use it, or something like it. Without a tracing facility like this,
figuring out exactly what is going wrong is nearly impossible.
Errors You Might Encounter
During the implementation of the above application, I ran into many errors, and had to debug my
application. Since FGAC happens totally in the server, it can be a little obtuse to diagnose errors, and
debug your application. The following sections will help you successfully debug and diagnose errors.
ORA-28110: policy function or package <function name> has error.
This indicates that the package or function the policy is bound to has an error, and cannot be
recompiled. If you issue the SQL*PLUS command, SHOW ERRORS FUNCTION <FUNCTION NAME> or
SHOW ERRORS PACKAGE BODY <PACKAGE NAME>, you will discover what the errors are.
This invalidation may happen because:
❑
Some object your function references was dropped, or is itself invalid.
❑
The code you compiled into the database has a syntactical error, or cannot be compiled for
some reason.
955
5254ch21cmp2.pdf 43
2/28/2005 6:52:53 PM
Chapter 21
The most common cause of this error is that the predicate function associated with a table has an error.
For example, consider the function from the previous examples:
tkyte@TKYTE816> create or replace function rls_examp
2 ( p_schema in varchar2, p_object in varchar2 )
3 return varchar2
4 as
5 begin
6
this is an error
7
return 'x > sys_context(''myctx'',''x'')';
8 end;
9 /
Warning: Function created with compilation errors.
Let's say we didn't notice at compile-time that the function did not compile cleanly. We assume the
function compiled, and we could execute it as normal. Now, whenever we execute any queries on T we
receive:
tkyte@TKYTE816> exec set_ctx( 0 ) ;
PL/SQL procedure successfully completed.
tkyte@TKYTE816> select * from t;
select * from t
*
ERROR at line 1:
ORA-28110: policy function or package TKYTE.RLS_EXAMP has error
So, this is telling us that we have an error, specifically that the function TKYTE.RLS_EXAMP is in error (it
cannot be successfully compiled). A query you might find useful in discovering these issues before they
happen is:
tkyte@TKYTE816> column pf_owner format a10
tkyte@TKYTE816> column package format a10
tkyte@TKYTE816> column function format a10
tkyte@TKYTE816> select pf_owner, package, function
2
from user_policies a
3
where exists ( select null
4
from all_objects
5
where owner = pf_owner
6
and object_type in ( 'FUNCTION', 'PACKAGE',
7
'PACKAGE BODY' )
8
and status = 'INVALID'
9
and object_name in ( a.package, a.function )
10
)
11 /
PF_OWNER
PACKAGE
FUNCTION
---------- ---------- ---------TKYTE
RLS_EXAMP
This query lists all invalid security policy functions for you. So, currently it confirms what we already
know – that TKYTE.RLS_EXAMP is invalid. The solution now is pretty straightforward. We issue:
956
5254ch21cmp2.pdf 44
2/28/2005 6:52:53 PM
Fine Grained Access Control
tkyte@TKYTE816> show errors function rls_examp
Errors for FUNCTION RLS_EXAMP:
LINE/COL ERROR
-------- ----------------------------------------------------------------6/10
PLS-00103: Encountered the symbol “AN” when expecting one of the
following:
:= . ( @ % ;
Looking at line 6, it is the line that reads this is an error. Correct this and the ORA-28110 will go
away.
ORA-28112: failed to execute policy function.
An ORA-28112: failed to execute policy function results if SELECT or DML is performed on a
table with an associated policy function, and the policy function has policy-related (not predicate)
errors. This means the function is valid (it can be executed), but it raised some exception and did not it,
allowing the database kernel to receive the exception.
An ORA-28112 will generate a trace file in the directory specified by the USER_DUMP_DEST init.ora
parameter. This file will not have the ORA-28112, but it will have the phrase Policy function
execution error.
For example, let's say we had coded the following logic (continuing the example from earlier):
tkyte@TKYTE816> create or replace function rls_examp
2 ( p_schema in varchar2, p_object in varchar2 )
3 return varchar2
4 as
5
l_uid number;
6 begin
7
select user_id
8
into l_uid
9
from all_users
10
where username = 'SOME_USER_WHO_DOESNT_EXIST';
11
12
return 'x > sys_context(''myctx'',''x'')';
13 end;
14 /
Function created.
The intention of the above routine is to raise the exception NO_DATA_FOUND, and not to handle it. This
is to see what happens when an exception is allowed to propagate back to the database kernel. Now let's
cause this routine to be invoked:
tkyte@TKYTE816> exec set_ctx( 0 ) ;
PL/SQL procedure successfully completed.
tkyte@TKYTE816> select * from t;
select * from t
*
ERROR at line 1:
ORA-28112: failed to execute policy function
957
5254ch21cmp2.pdf 45
2/28/2005 6:52:53 PM
Chapter 21
This indicates that the policy function exists, and is valid, but raised an error during execution. A trace
file accompanies this error. If we look in the directory specified by the init.ora parameter,
USER_DUMP_DEST and find our trace file, we'll find at the bottom of this file:
...
*** SESSION ID:(8.405) 2001-04-16 17:03:00.193
*** 2001-04-16 17:03:00.193
---------------------------------------------------------Policy function execution error:
Logon user
: TKYTE
Table or View : TKYTE.T
Policy name
: T_POLICY
Policy function: TKYTE.RLS_EXAMP
ORA-01403: no data found
ORA-06512: at “TKYTE.RLS_EXAMP”, line 7
ORA-06512: at line 1
This information is critical in determining the error in our procedure. It points us right to line 7, the
SELECT ... INTO statement, and tells us that it returned NO DATA FOUND.
ORA-28113: policy predicate has error.
An ORA-28113: policy predicate has error results if SELECT or DML is performed on a table
with an associated policy function, and the policy function returns a predicate that is syntactically
incorrect. This predicate, when merged with the original query, is not valid SQL.
An ORA-28113 will generate a trace file in the directory specified by the USER_DUMP_DEST init.ora
parameter. This file will have the ORA-28113 error message, as well as information about the current
session and the predicate that failed.
For example, let's say we had coded the following logic. It returns a predicate that compares X to a nonexistent column in the table (at least it will try to):
tkyte@TKYTE816> create or replace function rls_examp
2 ( p_schema in varchar2, p_object in varchar2 )
3 return varchar2
4 as
5 begin
6
return 'x = nonexistent_column';
7 end;
8 /
Function created.
so a query such as:
select * from t
will be rewritten as:
select * from ( select * from t where x = nonexistent_column )
958
5254ch21cmp2.pdf 46
2/28/2005 6:52:53 PM
Fine Grained Access Control
Obviously, since our table T does not have this column, it will fail. The query cannot execute.
tkyte@TKYTE816> select * from t;
select * from t
*
ERROR at line 1:
ORA-28113: policy predicate has error
This indicates that the predicate was successfully retrieved from the function, but when used in the
query, it raised some other error. In reviewing the trace file retrieved from the database server machine,
we find at the bottom:
...
*** SESSION ID:(8.409) 2001-04-16 17:08:10.669
*** 2001-04-16 17:08:10.669
...
----------------------------------------------------------Error information for ORA-28113:
Logon user
: TKYTE
Table or View : TKYTE.T
Policy name
: T_POLICY
Policy function: TKYTE.RLS_EXAMP
RLS predicate :
x = nonexistent_column
ORA-00904: invalid column name
This shows us the information we need, at a minimum, to fix the problem – the predicate that caused
the error, as well as the SQL error message that accompanies the incorrect predicate.
ORA-28106: input value for argument #2 is not valid.
You will receive this error from a call to DBMS_SESSION.SET_CONTEXT if the attribute name is not a
valid Oracle identifier. An application context's attribute names must be valid Oracle identifiers (in
other words, you could use them for names of columns in tables, or as PL/SQL variable names). The
only solution is to change the name of your attribute. For example, you cannot have a context attribute
named SELECT, so you would have to pick an alternate name instead.
Summary
In this chapter we thoroughly explored FGAC. There are many pros to this feature, and very few cons.
In fact, it is hard to think of any cons to this feature at all. We have seen how this feature:
❑
Simplifies application development. It separates access control from the application, and puts
it with the data.
❑
Ensures data in the database is always protected. No matter what tool accesses the data, we
are ensured our security policy is invoked and cannot be bypassed.
❑
Allows for evolutionary changes to security policies with no impact on client applications.
❑
Simplifies the management of database objects. It reduces the total number of database objects
needed to support an application.
959
5254ch21cmp2.pdf 47
2/28/2005 6:52:53 PM
Chapter 21
❑
It performs well. Actually, it performs as well as the SQL you add will allow it to. If the
predicate you return makes it very hard for the optimizer to develop a fast plan, it is not the
fault of FGAC – this is a SQL query tuning issue. The use of the application contexts allow us
to reap the benefits of shared SQL, and reduce the number of database objects we must have.
FGAC will not impact performance any more than performing this operation in any other
fashion.
We have also seen that it may be difficult to debug, as FGAC happens in the background and the
conventional tools such as a debugger or DBMS_OUTPUT will not work. Packages such as debug.f,
referred to in the Errors You Might Encounter section, make debugging and tracing this feature much
easier.
960
5254ch21cmp2.pdf 48
2/28/2005 6:52:53 PM
Fine Grained Access Control
961
5254ch21cmp2.pdf 49
2/28/2005 6:52:53 PM
Download