My current project uses Oracle RLS (Row Level Security) to ensure each user can only see the data they’re allowed to see. This has made a potentially very complex application very simple. My APEX pages can query any table, no WHERE clauses needed (except for user-entered filters), and I can be confident they won’t be able to inadvertently see rows they’re not supposed to see if I forget to add the right predicate on every page that queries each table.
Now, remember that APEX is a stateless environment as far as database sessions are concerned: when a page is requested, it might be generated by any session running on the database; it might re-use a session that had just finished rendering a page for someone else entirely. Therefore, it is imperative that all my database code assumes nothing about the state of package variables, for example. Instead, we use Oracle Application Contexts to hold session-specific information. For example, a context might hold the current user’s identifier, or a flag that indicates they have been granted a particular privilege. As an added advantage, when we use contexts, we can easily pass data into views.
How do we guarantee that the context has been set correctly for the current user’s session? We could follow the guidelines in the APEX help (quoted below) to set the context at the start of every page call, then clear them at the end of the page call. If we were to do this, these are the steps we’d take (these are taken from APEX 4.1.1 but should be comparable in other releases):
- Create the context:
CREATE OR REPLACE CONTEXT MY_CONTEXT USING MY_PACKAGE;
- Create a database package with a procedure to Set the context variable(s), e.g.:
PROCEDURE set_session IS BEGIN DBMS_SESSION.set_context (namespace => 'MY_CONTEXT' ,attribute => 'is_admin' ,value => 'Y'); END new_session;
- Edit Application Properties; under Security, in the Database Session section, set Initialization PL/SQL Code to call MY_PACKAGE.set_session.
It is interesting that the APEX help page for Initialization PL/SQL Code has this to say:
“Use this attribute to enter a PL/SQL block that sets a context for the database session associated with the current “show page” or “accept page” request. The block you enter here is executed at a very early point during the page request, immediately after the APP_USER value is established. The value of APP_USER (using :APP_USER or v(‘APP_USER’)) may be used within the block. Values of other items in session state may be referenced as well, but any such items must have been established in session state before the initiation of the current page request. Consider the following example:
It sets the value of USERPRIV in the context named CTX_USER_QRY to the value returned by the function my_function in package my_package. The function is passed the current value of APP_USER as an input argument. Presumably, the named context would be used in a VPD policy ( created within the application’s parsing schema) to effect the generation of predicates appropriate to the authenticated user.
Virtual Private Database, also know as Fine-Grained Access Control or FGAC, is an Oracle database feature that provides an application programming interface (API) that enables developers to assign security policies to database tables and views. Using PL/SQL, developers can create security policies with stored procedures and bind the procedures to a table or view by means of a call to an RDBMS package. Such policies are based on the content of application data stored within the database, or based on context variables provided by Oracle database. In this way, VPD permits access security mechanisms to be removed from applications, and to be situated closer to particular schemas.
This works fine. Before every page is rendered, APEX calls our package to set the context variables. All our procedures, views, etc. as well as our RLS package can use these context variables as required.
Is there a simpler way? Yes.
Just this morning I attended an “Odds and Ends” session presented by Connor McDonald at the AUSOUG Monthly meeting in Perth. He briefly mentioned a cool feature of Oracle contexts – the fact that they can be made globally accessible. I had seen this in the documentation but didn’t know what the words meant or how it might be useful.
What Connor’s talk alerted me to was that what a Globally Accessible context does could be very useful for our APEX application: it allows a context to be simultaneously available from any session on the database, to any user who logs in and sets the correct Client Identifier. We can use this to avoid having to call any code from Initialization PL/SQL Code, which would be run before every page request, which may have a performance impact; instead, the context variables will follow the user around, no matter which database session services their page request.
To do this, you simply:
- Create our context to be globally accessible:
CREATE OR REPLACE CONTEXT MY_CONTEXT USING MY_PACKAGE ACCESSED GLOBALLY;
- Alter our database package to set the context variables, this time sending the client_id parameter, e.g.:
PROCEDURE set_session IS v_real_client_id VARCHAR2(200); BEGIN v_real_client_id := REPLACE( SYS_CONTEXT('USERENV','CLIENT_IDENTIFIER') ,'nobody' ,v('APP_USER')); DBMS_SESSION.set_context (namespace => 'MY_CONTEXT' ,attribute => 'is_admin' ,value => 'Y' ,client_id => v_real_client_id); END set_session;
- In APEX, remove the Initialization PL/SQL Code – we don’t need it anymore.
- In the Authentication scheme, set Post-Authentication Procedure Name to MY_PACKAGE.set_session.
Now, our context is only set once – just after login. Excellent.
“Now,” you’ll no doubt want to ask me, “where did you actually set the Client Identifier? And what’s the deal with the REPLACE?”
We don’t have to worry about setting Client Identifier – APEX already sets it for us automatically for each page request. If you log into an ordinary APEX application, and examine the Client Identifier, you’ll get something like this:
Where “SCOTT” is the login username, and “15135454564654” is the APEX session ID. We can bind our context to this combination of values simply by passing the Client Identifier to the client_id parameter of DBMS_SESSION.set_context. In fact, most of the time this sort of code will work fine:
DBMS_SESSION.set_context (namespace => 'MY_CONTEXT' ,attribute => 'my_attribute' ,value => 'xyz' ,client_id => SYS_CONTEXT('USERENV','CLIENT_IDENTIFIER'));
Unfortunately, however, if we look at CLIENT_IDENTIFIER from the post-login procedure (i.e. immediately after the user has successfully logged in), we instead find it is something like this:
So the session ID is correct, but the username hasn’t been set yet. Looking at the APEX debug logs it’s apparent that the client identifier gets set with the username very soon after the post-login procedure is run – which is a tad inconvenient. However, APP_USER is set to SCOTT prior to the post-login procedure; therefore, my set_session procedure simply replaces “nobody” with v(‘APP_USER’), so that the client_id will agree with the client identifier that will be used from that point on.
That works great – my views, packages and RLS predicates can use SYS_CONTEXT to get context variables with negligible performance impact (remembering that context variables are always strings, so don’t forget to convert your datatypes if necessary).
As an added bonus, as a developer or DBA we can log into the database and diagnose an active session by pretending to be it. For example, Joe sends us an email with a screenshot of an APEX page showing “No Data Found”. When you log into your tool of choice and run the same query, you can see plenty of data. You want your session to pretend to be Joe. To do this, you would get Joe’s username and the session ID from the URL on the APEX page (it’s the 3rd parameter after the f?p= bit in the URL), and run this:
What this does is tell Oracle that your session is Joe’s session. Your session will automatically see the same context variables that Joe’s session currently sees. You run the query again, and you can see that it returns no rows – ah, maybe there’s something wrong with the RLS predicate your application is generating. You now have a testbed under which you can test any change you might want to make in order to resolve the issue (or at least, to be able to tell Joe why he’s not allowed to see any data).