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