Section 5.2. Using RLS


5.2. Using RLS

Now that you have seen an example of RLS fundamentals, let's look at some examples that take advantage of different aspects of RLS functionality.

5.2.1. Performing an Update Check

Let's consider a slight twist on our earlier example. Instead of updating the COMM column, the user now updates the SAL column. Because SAL is the column used in the predicate, it will be interesting to see the result.

     SQL> UPDATE hr.emp SET sal = 1200;     7 rows updated.     SQL> UPDATE hr.emp SET sal = 1100;     7 rows updated. 

Only seven rows are updated, as expected. Now, let's change the updated amount. After all, everyone deserves a better salary.

     SQL> UPDATE hr.emp SET sal = 1600;     7 rows updated.     SQL> UPDATE hr.emp SET sal = 1100;     0 rows updated. 

Note the last update. Why were no rows updated?

The answer lies in the first update. The first one updated the SAL column to 1,600, which is not satisfied by the filtering predicate "SAL <= 1500". Thus, after the first update, all of the rows became invisible to the user.

This is a potentially confusing situation: the user can execute a SQL statement against rows, and the statement changes the access to those rows. During application development, this seeming data instability may create bugs or at least introduce a degree of unpredictability that makes debugging a challenge. To counter this behavior, we can take advantage of another ADD_POLICY parameter, update_check. Let's take a look at the impact of setting this parameter to TRUE when we create a policy on the table.

     BEGIN        DBMS_RLS.add_policy (object_name  => 'EMP',               policy_name          => 'EMP_POLICY',               function_schema      => 'HR',               policy_function      => 'AUTHORIZED_EMPS',               statement_types      => 'INSERT, UPDATE, DELETE, SELECT',               update_check         => TRUE               );     END; 

After this policy is placed on the table, if a user attempts to perform the same update, she gets an error.

     SQL> UPDATE hr.emp SET sal = 1600;     update hr.emp set sal = 1600               *     ERROR at line 1:     ORA-28115: policy with check option violation 

The ORA-28115 error is raised because the policy now prevents any updates to the value of columns in a row that will cause a change in the visibility of those rows with the specified predicate. Users can still make changes to other columns; these changes do not affect the visibility of the rows:

     SQL> UPDATE hr.emp SET sal = 1200;     7 rows updated. 

I recommend that you set the update_check parameter to TRUE whenever you declare a policy, to prevent unpredictable and probably undesirable behavior in the application later on.


5.2.2. Static RLS Policies

So far the examples have demonstrated the use of a static policy, which means that the value returned by the predicate function does not change even if the circumstances under which it is called change.

Given this fact, RLS need not execute the function every time a query is issued against the table. The value can be determined only once, cached, and then reused from the cache as many times as needed. To make a policy behave that way, you can define it as a static policy to RLS by passing TRUE for the static_policy argument:

       1  BEGIN       2     DBMS_RLS.ADD_POLICY (       3        object_name      => 'EMP',       4        policy_name      => 'EMP_POLICY',       5        function_schema  => 'HR',       6        policy_function  => 'AUTHORIZED_EMPS',       7        statement_types  => 'INSERT, UPDATE, DELETE, SELECT',       8        update_check     => TRUE,              9        static_policy    => TRUE      10     );          11 END; 

The default value of the static_policy parameter is FALSE: that value makes the policy dynamic rather than static, and it causes the policy function to be called for each operation on the table. I'll describe dynamic policies later in the chapter in the section "Defining a Dynamic Policy." Policy types, in addition to static and dynamic, are supported in Oracle Database 10g. See the section "Other Classes of Dynamism" for more information.

There are many situations that call very precisely for a static policy. Consider a merchandise warehouse that is servicing several customers. Here, a predicate might be used to limit the entries to only the relevant records for that customer. For example, the table BUILDINGS may contain a column named CUSTOMER_ID. A predicate "CUSTOMER_ID = customer_id" must be appended to the queries where customer_id is based on the user who is logged in. When a user logs in, his customer ID can be retrieved via a LOGON trigger, and the RLS policy can use that ID to evaluate which rows should be displayed. During a session, the value of this predicate does not change, so it makes sense to set static_policy to TRUE in such a situation.

5.2.2.1. Problems with static policies

Static policies can enhance performance, but they can also introduce bugs in applications. If the predicate derives from or depends on a changing value, such as time, IP address, client identifier, and so on, you will want to define a dynamic policy rather than a static one. Here is an example that shows why.

Let's look again at my original policy function, but let's now assume that the predicate depends on a changing value, such as the value of seconds in the current system timestamp. (This may not be a very likely real-life example, but it is close enough to explain the concept.)

     SQL> CREATE TABLE trigger_fire       2  (       3     val NUMBER       4  );     Table created.     SQL> INSERT INTO trigger_fire       2  VALUES       3  (1);     1 row created.     SQL> COMMIT;     Commit complete.     SQL> CREATE OR REPLACE FUNCTION authorized_emps (       2     p_schema_name   IN   VARCHAR2,       3     p_object_name   IN   VARCHAR2       4  )       5     RETURN VARCHAR2       6  IS       7     l_return_val   VARCHAR2 (2000);       8     PRAGMA AUTONOMOUS_TRANSACTION;       9  BEGIN      10     l_return_val := 'SAL <= ' || TO_NUMBER (TO_CHAR (SYSDATE, 'ss')) * 100;      11      12     UPDATE trigger_fire      13        SET val = val + 1;      14      15     COMMIT;      16     RETURN l_return_val;      17 END;      18  /     Function created. 

In this example, the function takes the seconds part of the current time (line 10), multiplies it by 100, and returns a predicate that shows the value of the column SAL less than or equal to this number. Because the seconds part will change with time, consecutive executions of this function will yield different results.

Now let's define a policy on the EMP table with this function as the policy function. As the policy already exists, I need to drop it first.

     SQL> BEGIN            2     DBMS_RLS.drop_policy (object_name => 'EMP', policy_name => 'EMP_POLICY')     ;       3  END;       4  /     PL/SQL procedure successfully completed.     SQL> BEGIN       2     DBMS_RLS.add_policy (object_name         => 'EMP',       3                          policy_name         => 'EMP_POLICY',       4                          function_schema     => 'HR',       5                          policy_function     => 'AUTHORIZED_EMPS',       6                          statement_types     => 'INSERT, UPDATE, DELETE, SELECT',       7                          update_check        => TRUE,       8                          static_policy       => FALSE       9                         );      10  END;      11  /     PL/SQL procedure successfully completed. 

It's time to test the policy. The user Lenny tries to find out the number of employees in the table.

     SQL> SELECT COUNT(*) FROM hr.emp;       COUNT(*)     ----------              0 

Because the table is under row-level security, the policy function is invoked to provide the predicate string to be applied to the query. It depends on the seconds part of the current timestamp, so it is some value between 0 and 60. In this particular case, the value was such that none of the records matched the predicate; hence, no rows were satisfied.

Because the policy function updates the column VAL in the table TRIGGER_FIRE, I can check to see how many times the function was called. As the user HR, let's check the value of VAL in the table TRIGGER_FIRE.

     SQL> SELECT * FROM trigger_fire;            VAL     ----------              3 

Because the policy function was called twiceonce during the parse phase and once during the execution phasethe value was incremented by 2 from 1. Lenny issues the query again to know the number of employees.

     SQL> SELECT COUNT(*) FROM hr.emp       COUNT(*)     ----------             10 

This time the policy function returned the predicate, which was satisfied by 10 records in the table. Again, let's check the value of VAL in the table TRIGGER_FIRE.

     SQL> SELECT * FROM trigger_fire;            VAL     ----------              5 

The value is incremented by 2 from 3proof that the policy function was executed multiple times. You can repeat the exercise as many times as you wish, to verify that the policy function is executed each time the operation occurs on the table.

Now, declare the policy as static and repeat the test. Because there is no RLS operation or API to alter a policy, you will need to drop it and then re-create it.

     SQL> BEGIN       2     DBMS_RLS.drop_policy (object_name => 'EMP', policy_name => 'EMP_POLICY');       3  END;       4  /     PL/SQL procedure successfully completed. 

Now let's see the effect.

       1  BEGIN       2     DBMS_RLS.add_policy (object_name          => 'EMP',       3                          policy_name          => 'EMP_POLICY',       4                          function_schema      => 'HR',       5                          policy_function      => 'AUTHORIZED_EMPS',       6                          statement_types      => 'INSERT, UPDATE, DELETE, SELECT',       7                          update_check         => TRUE,       8                          static_policy        => TRUE       9                         );      10  END;      11  /     PL/SQL procedure successfully completed.     SQL> -- Reset the table TRIGGER_FIRE     SQL> UPDATE trigger_fire SET val = 1;     1 row updated.     SQL> COMMIT;     Commit complete. 

As user Lenny, I select the number of rows from the table.

     SQL> SELECT COUNT(*) FROM hr.emp;       COUNT(*)     ----------              8 

As user HR, I check the value of the column VAL in TRIGGER_FIRE.

     SQL> SELECT * FROM trigger_fire;            VAL     ----------              2 

The value was incremented by 1, because the policy function was executed only once, not twice as it was before. I'll repeat the selection from the table EMP several times as user Lenny.

     SQL> SELECT COUNT(*) FROM hr.emp;       COUNT(*)     ----------              8     SQL> SELECT COUNT(*) FROM hr.emp;       COUNT(*)     ----------              8     SQL> SELECT COUNT(*) FROM hr.emp;       COUNT(*)     ----------              8 

In all cases, the same number is returned. Why? It's because the policy function was executed only once and the predicate that was used by the policy was cached. Because the policy function was never executed after the first execution, the predicate did not change. To confirm that, I'll select from the table TRIGGER_FIRE as user HR.

     SQL> SELECT * FROM trigger_fire;            VAL     ----------              2 

The value is still 2; it has not been incremented at all since the first time it was called. This output confirms that the policy function was not called during subsequent SELECTs on the table EMP.

By declaring a policy as static, I have effectively instructed the policy function to execute only once, and the policy to reuse the predicate originally created, even though the predicate might have changed in the course of time. This behavior might produce unexpected results in your application, so you should use static policies with great caution. The only time you're likely to want to use static policies is the case where the function positively results in a definitive predicate regardless of any of the variables, except those set at session startup and never changedfor example, the username.

5.2.2.2. Using a pragma

Another way to enforce the logic we require is to use a packaged function along with a pragma declaration to suppress any database operations. Here is the package specification.

     CREATE OR REPLACE PACKAGE rls_pkg     AS        FUNCTION authorized_emps (           p_schema_name   IN   VARCHAR2,           p_object_name   IN   VARCHAR2        )           RETURN VARCHAR2;             PRAGMA RESTRICT_REFERENCES (authorized_emps, WNDS, RNDS, WNPS, RNPS);     END;     / 

And here is the package body.

     CREATE OR REPLACE PACKAGE BODY rls_pkg     AS        FUNCTION authorized_emps (           p_schema_name   IN   VARCHAR2,           p_object_name   IN   VARCHAR2        )           RETURN VARCHAR2        IS           l_return_val   VARCHAR2 (2000);        BEGIN           l_return_val :=               'SAL <= ' || TO_NUMBER (TO_CHAR (SYSDATE, 'ss')) * 100;           RETURN l_return_val;        END;     END;     / 

In the package specification, I have defined a pragma to bring this function to these purity levels:


WNDS

Write No Database State


RNDS

Read No Database State


WNPS

Write No Package State


RNPS

Read No Package State

When this package body is compiled, the pragma will be violated and the compilation will fail with the following message.

     Warning: Package Body created with compilation errors.     Errors for PACKAGE BODY RLS_PKG:     LINE/COL ERROR     -------- -----------------------------------------------------------------     2/4      PLS-00452: Subprogram 'AUTHORIZED_EMPS' violates its associated              pragma 

Declaring a pragma protects you from falling into potentially erroneous situations. However, it's still a good idea to use static policies as long as they are being applied only to deterministic situations, such as the warehouse example we discussed earlier.

When you are creating a static policy, make sure that the predicate returned by the policy function will never have a different value within the session.


5.2.3. Defining a Dynamic Policy

In the previous sections, I talked about a policy that returns a predicate string that is constantfor example, SAL <= 1500. In real life, such a scenario is not very common, except in some specialized applications such as goods warehouses. In most cases, you will need to build a filter based on the user issuing the query. For instance, the HR application may require that users see only their own records, not all records in a table. This is a dynamic requirement, as it needs to be evaluated for each employee who logs in. The policy function can be rewritten as follows.

       1 CREATE OR REPLACE FUNCTION authorized_emps (       2     p_schema_name   IN   VARCHAR2,       3     p_object_name   IN   VARCHAR2       4  )       5     RETURN VARCHAR2       6  IS       7     l_return_val   VARCHAR2 (2000);       8  BEGIN       9     l_return_val := 'ENAME = USER';      10     RETURN l_return_val;      11  END;      12  / 

In line 9, the predicate will compare the ENAME column with the USERthat is, the name of the currently logged-in user. If the user Martin (remember that Martin is the name of an employee in the table EMP) logs in and selects from the table, he sees only one rowhis own.

     SQL> CONN martin/martin     Connected.     SQL> SELECT * FROM hr.emp;     EMPNO ENAME      JOB          MGR HIREDATE     SAL   COMM DEPTNO     ------ ---------- --------- ------ --------- ------ ------ ------       7654 MARTIN     SALESMAN    7698 28-SEP-81  1,250  1,400     30 

Now let's expand this model to let Martin show more recordsnot just his own, but his entire department's records. The policy function now becomes the following.

       1  CREATE OR REPLACE FUNCTION authorized_emps (       2     p_schema_name   IN   VARCHAR2,       3     p_object_name   IN   VARCHAR2       4  )       5     RETURN VARCHAR2       6  IS       7     l_deptno       NUMBER;       8     l_return_val   VARCHAR2 (2000);       9  BEGIN      10     SELECT deptno      11       INTO l_deptno      12       FROM emp      13      WHERE ename = USER;      14      15     l_return_val := 'DEPTNO = ' || l_deptno;      16     RETURN l_return_val;      17  END;      18  / 

I also need to perform an additional step here. In the preceding code, the function selects from the table EMP (lines 1013). However, the table is protected by the RLS policy whose policy function is owned by the user HR. When the function executes under the privileges of the user HR, it will not find any rows, because there is no employee with the name HRmaking the predicate an incorrect one. To prevent this from happening, there are two options:

  • Grant a special privilege to the user HR so that RLS policies do not apply to it, or

  • Inside the policy function, indicate whether the calling user is the schema owner; if so, ignore the check

If I use the first approach, I will not need to change the policy function. As a DBA user, I must grant the special privilege to HR as follows:

     GRANT EXEMPT ACCESS POLICY TO hr; 

This removes the application of any RLS policies from the user HR. Because no policyregardless of which table it is defined onwill be applied, you should use this approach with great caution. In fact, considering the breach it places in the security model, we do not recommend this approach for regular schema owners.

The other approach is to have a special schema named, say, RLSOWNER, which creates all the RLS policies and owns all the policy functions. Only this user, and no others, is granted the EXEMPT ACCESS POLICY system privilege . Because the policy function is owned by RLSOWNER, the policy-creation PL/SQL block looks like the following. This block can be run by any user that has EXECUTE privileges on the DBMS_RLS package .

       1  BEGIN       2     DBMS_RLS.add_policy (object_name => 'EMP',       3            policy_name          => 'EMP_POLICY',       4            function_schema      => 'RLSOWNER',       5            policy_function      => 'AUTHORIZED_EMPS',       6            statement_types      => 'INSERT, UPDATE, DELETE, SELECT',       7            update_check         => TRUE       8            );       9  END;      10  / 

Using the second approach, the policy function has to include the logic to bypass the filter for the schema owner, as shown below. Again, this block may be run by any user with EXECUTE privileges on the package DBMS_RLS.

       1  CREATE OR REPLACE FUNCTION authorized_emps (       2     p_schema_name   IN   VARCHAR2,       3     p_object_name   IN   VARCHAR2       4  )       5     RETURN VARCHAR2       6  IS       7     l_deptno       NUMBER;       8     l_return_val   VARCHAR2 (2000);       9  BEGIN      10     IF (p_schema_name = USER)      11     THEN      12        l_return_val := NULL;      13     ELSE      14        SELECT deptno      15          INTO l_deptno      16          FROM emp      17         WHERE ename = USER;      18      19        l_return_val := 'DEPTNO = ' || l_deptno;      20     END IF;      21      22     RETURN l_return_val;      23  END;      24  / 

This version of the function is very similar to the previous ones; the new lines are shown in bold (line 10). Here I am checking to see if the calling user is the owner of the table; if so, I return NULL (line 12). A NULL value in the predicate returned by the function is equivalent to no policy at allthat is, no rows are filtered.

Now Martin executes the same query as before:

     SQL> SELECT * FROM hr.emp;      EMPNO ENAME      JOB          MGR HIREDATE     SAL   COMM DEPTNO     ------ ---------- --------- ------ --------- ------ ------ ------       7499 ALLEN      SALESMAN    7698 20-FEB-81  1,600    300     30       7521 WARD       SALESMAN    7698 22-FEB-81  1,250    500     30       7654 MARTIN     SALESMAN    7698 28-SEP-81  1,250  1,400     30       7698 BLAKE      MANAGER     7839 01-MAY-81  2,850            30       7844 TURNER     SALESMAN    7698 08-SEP-81  1,500      0     30       7900 JAMES      CLERK       7698 03-DEC-81    950            30     6 rows selected. 

Note how all of the returned rows are from Martin's department (30).

As you can see, the policy function is crucial in building the RLS policy. The policy will be able to place filters on the rows with whatever predicate value the function can spin out, as long as it is syntactically correct. You can create quite elaborate and sophisticated predicates using policy functions.

Following the same approach, you can have the RLS filters applied to any table in the database. For instance, you could have a policy on the table DEPT as shown below.

       1  BEGIN       2     DBMS_RLS.add_policy (object_schema  => 'HR',       3            object_name          => 'DEPT',       4            policy_name          => 'DEPT_POLICY',       5            function_schema      => 'RLSOWNER',       6            policy_function      => 'AUTHORIZED_EMPS',       7            statement_types      => 'SELECT, INSERT, UPDATE, DELETE',       8            update_check         => TRUE       9           );      10  END;      11  / 

Here the same functionAUTHORIZED_EMPSis used as the policy function. Because the function returns a predicate "DEPTNO = deptno", it can easily be used in the table DEPT as well as any other table containing a DEPTNO column.

A table that does not have a DEPTNO column probably has another column through which it has a foreign-key relationship with the EMP table. For instance, the table BONUS has a column ENAME through which it is tied to the EMP table. So I can rewrite my policy function as follows:

     CREATE OR REPLACE FUNCTION allowed_enames (        p_schema_name   IN   VARCHAR2,        p_object_name   IN   VARCHAR2     )        RETURN VARCHAR2     IS        l_deptno       NUMBER;        l_return_val   VARCHAR2 (2000);        l_str          VARCHAR2 (2000);     BEGIN        IF (p_schema_name = USER)        THEN           l_return_val := NULL;        ELSE           SELECT deptno             INTO l_deptno             FROM hr.emp            WHERE ename = USER;           l_str := '(';           FOR emprec IN (SELECT ename                            FROM hr.emp                           WHERE deptno = l_deptno)           LOOP              l_str := '''' || emprec.ename || ''',';           END LOOP;           l_str := RTRIM (l_str, ',');           l_str := ')';           l_return_val := 'ENAME IN ' || l_str;        END IF;        RETURN l_return_val;     END;     / 

If I define a policy on the BONUS table with the following policy function, this places the RLS policy on the BONUS table, as well:

     BEGIN        DBMS_RLS.add_policy (object_schema  => 'HR',               object_name          => 'BONUS',               policy_name          => 'BONUS_POLICY',               function_schema      => 'RLSOWNER',               policy_function      => 'ALLOWED_ENAMES',               statement_types      => 'SELECT, INSERT, UPDATE, DELETE',                update_check         => TRUE               );     END;     / 

In this manner, I can define RLS polices on all related tables in the database driven from one table. Because the facility I've described in this section essentially provides a private view of the tables in the database based on the user or other parameters (like the time of day or IP address), it is also known as Virtual Private Database (VPD).

5.2.4. Improving Performance

Let's assume that our requirements have changed again. (That's not surprising in a typical organization, is it?) Now I have to set up the policy in such a way that all employees and departments will be visible to a user who is a manager; otherwise, only the employees of the user's department are visible. To accommodate this requirement, my policy function might look like this.

     CREATE OR REPLACE FUNCTION authorized_emps (        p_schema_name   IN   VARCHAR2,        p_object_name   IN   VARCHAR2     )        RETURN VARCHAR2     IS        l_deptno       NUMBER;        l_return_val   VARCHAR2 (2000);        l_mgr          BOOLEAN;        l_empno        NUMBER;        l_dummy        CHAR (1);     BEGIN        IF (p_schema_name = USER)        THEN           l_return_val := NULL;        ELSE           SELECT DISTINCT deptno, empno                      INTO l_deptno, l_empno                      FROM hr.emp                     WHERE ename = USER;           BEGIN              SELECT '1'                INTO l_dummy                FROM hr.emp               WHERE mgr = l_empno AND ROWNUM < 2;              l_mgr := TRUE;           EXCEPTION              WHEN NO_DATA_FOUND              THEN                 l_mgr := FALSE;              WHEN OTHERS              THEN                 RAISE;           END;           IF (l_mgr)           THEN              l_return_val := NULL;           ELSE              l_return_val := 'DEPTNO = ' || l_deptno;           END IF;        END IF;        RETURN l_return_val;     END; 

Look at the complexity in selecting the data. This complexity will surely add to the response time (and, of course, in your real-world applications, the logic will be considerably more complex). Can I simplify the code and improve performance?

I certainly can. Look at the first requirementchecking to see if the employee is a manager. In the above code, we checked the EMP table for that information, but the fact that an employee is a manager does not change very often. Similarly, a manager's manager might change, but the status of a manager remains the same. So, the title of a manager is actually more like an attribute of the employee when she logs in, not something that changes during a session. Therefore, if we can somehow, during the login process, pass to the database the fact that the user is a manager, that check will not be needed later in the policy function.

How do I pass a value of this kind? Global variables come to mind. I could assign the value "Y" or "N" to designate the manager status and create a package to hold the variable.

     CREATE OR REPLACE PACKAGE mgr_check     IS        is_mgr CHAR (1);     END; 

The policy function looks like this.

     CREATE OR REPLACE FUNCTION authorized_emps (        p_schema_name   IN   VARCHAR2,        p_object_name   IN   VARCHAR2     )        RETURN VARCHAR2     IS        l_deptno       NUMBER;        l_return_val   VARCHAR2 (2000);     BEGIN        IF (p_schema_name = USER)        THEN           l_return_val := NULL;        ELSE           SELECT DISTINCT deptno                      INTO l_deptno                      FROM hr.emp                     WHERE ename = USER;           IF (mgr_check.is_mgr = 'Y')           THEN              l_return_val := NULL;           ELSE              l_return_val := 'DEPTNO = ' || l_deptno;           END IF;        END IF;        RETURN l_return_val;     END; 

Notice how much less code is now required to check for manager status. It merely checks the status from a global packaged variable. This variable has to be set during the login process and thus is a perfect job for an AFTER LOGON database trigger.

     CREATE OR REPLACE TRIGGER tr_set_mgr        AFTER LOGON ON DATABASE     DECLARE        l_empno   NUMBER;        l_dummy   CHAR (1);     BEGIN        SELECT DISTINCT empno                   INTO l_empno                   FROM hr.emp                  WHERE ename = USER;        SELECT '1'          INTO l_dummy          FROM hr.emp         WHERE mgr = l_empno AND ROWNUM < 2;        rlsowner.mgr_check.is_mgr := 'Y';     EXCEPTION        WHEN NO_DATA_FOUND        THEN           rlsowner.mgr_check.is_mgr := 'N';        WHEN OTHERS        THEN           RAISE;     END;     / 

The trigger sets the value of the packaged variable to designate the manager status of the employee, which is then picked up by the policy function. Let's do a quick test. Connecting as King (who is a manager) and Martin (who is not), I can see that the setup works.

     SQL> CONN martin/martin     Connected.     SQL> SELECT COUNT(*) FROM hr.emp;       COUNT(*)     ----------         262145     SQL> CONN king/king     Connected.     SQL> SELECT COUNT(*) FROM hr.emp;       COUNT(*)     ----------         589825 

Martin's query retrieves fewer employees, as expected, whereas King's query retrieves all rows.

You can often use this packaged variable approach to improve performance. In the first example, where the check for manager status was done inside the policy function, the query took 199 centiseconds. Using the global variable approach, it took only 132 centiseconds, which represents a significant improvement.

5.2.5. Controlling the Type of Table Access

RLS has many uses beyond security and the simplification of application development models. RLS is also very helpful if you need to switch a table between read-only and read/write status, as determined by a variety of circumstances. Without RLS, DBAs could make an entire tablespacebut not the individual tables inside itread-only or read/write. Even if a DBA wanted to take this approach, a tablespace could not be made read-only if it had any active transactions. As it may be impossible to find a period of time during which there are no transactions in a database, a tablespace may never actually be able to be made read-only. In such cases, RLS is the only viable solution.

Now, to be honest, RLS does not actually make a table read-only; it simply allows us to emulate that behavior by denying any attempts to change the contents of the table. The simplest way to do this is to apply a predicate to any UPDATE, DELETE, and INSERT that will always evaluate to FALSEfor example, 1=2.

Here is an example of making the EMP table read-only with this most basic of predicate functions:

     CREATE OR REPLACE FUNCTION make_read_only (        p_schema_name   IN   VARCHAR2,        p_object_name   IN   VARCHAR2      )        RETURN VARCHAR2     IS     BEGIN        -- Only the owner of the predicate policy function can change        -- the data in the table.        IF (p_schema_name = USER)        THEN           RETURN NULL;        ELSE           RETURN '1=2';        END IF;     END; 

Using this policy function, I can create an RLS policy on the table EMP for the DML statements that change data: INSERT, UPDATE, and DELETE.

     BEGIN        DBMS_RLS.add_policy (object_name          => 'EMP',                             policy_name          => 'EMP_READONLY_POLICY',                             function_schema      => 'HR',                             policy_function      => 'MAKE_READ_ONLY',                                                          statement_types      => 'INSERT, UPDATE, DELETE',                             update_check         => TRUE                            );     END; 

In the line shown in bold, note that the statement_types parameter does not include the SELECT statement, as that statement will be freely allowed.

And now it is only possible to query information on the EMP table if the current user is not the owner of the policy's function:

     SQL> SHOW user     USER is "MARTIN"     SQL> DELETE hr.emp;     0 rows deleted.     SQL> SELECT COUNT(*) FROM hr.emp;       COUNT(*)     ----------             14 

When the time comes to make the table read/write again, I can simply disable the policy.

     BEGIN        DBMS_RLS.enable_policy (object_name      => 'EMP',                                policy_name      => 'EMP_READONLY_POLICY',                                ENABLE           => FALSE                               );     END;     / 

Now non-HR users can successfully complete DML operations on the table.

The table is never actually set to read-only; the policy just makes sure that no rows are affected when the user issues DML statements against the table. Because no error is returned and the policy simply ignores any DML statements, you need to be careful to examine all of the application code that uses this functionality. Programmers may inadvertently mistake the no-error condition as a successful DML operation.


The power of this feature is not limited to just making tables read-only or read/write on demand; it can be created dynamically and applied automatically based on any user-defined conditions that you wish. For instance, you could write a policy function that makes the table read-only between 5:00 P.M. and 9:00 A.M. for all users, except for the batch job user, BATCHUSER, and read-only for the batch user between 9:00 A.M. and 5:00 P.M. The body of such a function might look like this:

       BEGIN        IF (p_schema_name = USER)        THEN           l_return_val := NULL;        ELSE           l_hr := TO_NUMBER (TO_CHAR (SYSDATE, 'HH24'));           IF (USER = 'BATCHUSER')           -- you can list all users here that should be           -- read only during the daytime.           THEN              IF (l_hr BETWEEN 9 AND 17)              THEN                 -- make the table read only                 l_return_val := '1=2';              ELSE                 l_return_val := NULL;              END IF;           ELSE              -- users which need to be read only during after-hours              IF (l_hr >= 17 AND l_hr <= 9)              THEN                 -- make the table read only                 l_return_val := '1=2';              ELSE                 l_return_val := NULL;              END IF;           END IF;        END IF;        RETURN l_return_val;     END; 

Based on the timestamp, you can let the table be controlled granularly on multiple fronts. The example shown here can be extended to cover other events as well (e.g., IP address, authentication type, client information, terminal, OS user, and many others). All you have to do is get the appropriate variable from the system context (SYS_CONTEXT; this feature is explored later in the chapter) of the session and check it. For example, assume that you have a requirement that the user King (the president of the company) is allowed to see every record only if he does both of the following:

  • Connects from his laptop KINGLAP with a fixed IP address (192.168.1.1) and from the Windows NT domain ACMEBANK

  • Connects to Windows as user King

The policy function would now look like this:

     CREATE OR REPLACE FUNCTION emp_policy (        p_schema_name   IN   VARCHAR2,        p_object_name   IN   VARCHAR2     )        RETURN VARCHAR2     IS        l_deptno       NUMBER;        l_return_val   VARCHAR2 (2000);     BEGIN        IF (p_schema_name = USER)        THEN           l_return_val := NULL;        ELSIF (USER = 'KING')        THEN           IF (                  -- check client machine name                  SYS_CONTEXT ('USERENV', 'HOST') = 'ACMEBANK\KINGLAP'               OR                  -- check OS username                  SYS_CONTEXT ('USERENV', 'OS_USER') = 'king'               OR                  -- check IP address                  SYS_CONTEXT ('USERENV', 'IP_ADDRESS') = '192.168.1.1'              )           THEN              -- all checks satisfied for KING; allow unrestricted access.              l_return_val := NULL;           ELSE              -- return the usual predicate              l_return_val := 'SAL <= 1500';           END IF;        ELSE  -- All other users           l_return_val := 'SAL <= 1500';        END IF;        RETURN l_return_val;     END; 

Here I am using the built-in function SYS_CONTEXT to return the context attributes. I'll discuss the use of system contexts later (see the "Application Contexts" section); all you need to understand now is that the function call returns the name of the client terminal from which the user is connected. The other lines using the function call also return the appropriate values.

You can use SYS_CONTEXT to get a variety of information about the user connection. By using this information, you can easily customize your policy function to build a filter to cater to your specific needs. For a complete list of attributes available via SYS_CONTEXT, refer to Oracle's SQL Reference Manual.




Oracle PL(s)SQL For DBAs
Oracle PL(s)SQL For DBAs
ISBN: N/A
EAN: N/A
Year: 2005
Pages: 122

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net