|
Learn all about a reliable mechanism for passing data from
within a session securely to help with a variety of cases such
as Virtual Private Database, Application User Management in
Connection Pools, and more.
A
few months ago, I wrote an article on the Virtual Private
Database (VPD) feature in Oracle Magazine, available on
the
Oracle Technology Network. The article had a good reception
from the readers. Of course, I’m not going to reproduce the
article here; from the feedback of the readers, I came to
realize another powerful concept is grossly overlooked and not
well understood by most users. This feature — application
contexts — allows you to transfer sensitive bits of information
securely across a session, and is the focus of this article.
A
database with VPD restricts the rows a user sees based on his
credentials. The restriction is enforced by a
WHERE clause
automatically appended to the query. This clause, called a
predicate, is generated by a user-defined function called policy
function. A VPD policy on the table
EMP may create a
predicate in such a way that the query
SELECT *
FROM ACCOUNTS
becomes
SELECT *
FROM ACCOUNTS
WHERE AM_NAME = USER
In this case,
we
assume that the logged-in user is also an account manager (AM).
Hence, only those rows that record her username (returned by the
function USER) as
the Account Manager (shown by the column
AM_NAME) are
displayed; nothing else. So, if Scott logs in and issues this
query, the query effectively becomes
SELECT *
FROM ACCOUNTS
WHERE AM_NAME = ‘SCOTT’
We wanted to make sure that when the user Scott logs in, he sees
only those records for which he is the account manager, not
others. The automatic application of the predicate via the
clause WHERE AM_NAME =
‘SCOTT’ guaranteed that. This is what we wanted, right?
Let’s pause for a moment. In this case, the requirement was
rather simple: The Account Manager’s Oracle login ids were used
to identify the accounts they represent. This makes sense in a
client-server application in which a stand-alone client
application connects to the database. Suppose the bank had a
different requirement; instead of a stand-alone client
application, the bank used a Web-based application in which the
users log in to the application server. The application server,
such as WebSphere, Weblogic, or Oracle Application Server,
connected to the database using a generic userid (such as
CONNPOOL) to form
a connection pool, and the regular user’s browser simply used
one of the existing connections from the pool. In such a case,
the Oracle userid will be
CONNPOOL, not
SCOTT. The VPD
policy would not return a useful predicate, and the query would
always be rewritten to
SELECT *
FROM EMP
WHERE AM_NAME = ‘CONNPOOL’
regardless of which user is logged in to the application server
(e.g., SCOTT). As
the readers of my earlier article pointed out, a VPD policy
would be quite useless here. This simply gives credence to the
model in which the application, not the database, enforces the
security, they asserted.
Not so fast, I responded. I have never been a proponent of
application-based security models. The application is just one
of the gateways to the data; there are plenty more, including
the ubiquitous SQL*Plus. What prevents a user from circumventing
the application to get to the data directly? To enable the
database-centric security model, the predicate must be
different. What if there were a way to pass the information
about the logged-in user to the database? Wouldn’t that make it
possible to use VPD in all cases? For instance, if the query
were rewritten to
SELECT *
FROM EMP
WHERE (
AM_NAME = <oracle userid>
OR
AM_NAME = <application user id>
)
wouldn’t it enforce the same type of security? In fact, it would
be far safer than application-based security enforcement.
Regardless of how users connected to the database — using the
application server, Web browser, SQL*Plus, TOAD, SQL Worksheet,
even homegrown tools — this VPD policy
would still be invoked and applied. So, the trick is to pass the
extra information — the application userid
— to the database reliably and securely.
Note the importance of the words, the information must be
passed with assurance that it has not been tampered;
otherwise, Joe can pass himself off as Scott and access all that
Scott has access to.
So, we need a sort of global variable to pass the information on
from the application user to the database. A mechanism such as a
package variable comes to mind. Here is one example:
create or replace package myvars
as
app_userid varchar2(20);
end;
Then you simply assign the app userid to this package through
begin
myvars.app_userid := 'SCOTT';
end;
This assignment could be done in the application after the user
is authenticated. In the policy predicate, the package variable
would then be referenced:
SELECT *
FROM EMP
WHERE (
AM_NAME = USER
OR
AM_NAME = MYVARS.APP_USERID
)
Since a package variable, once set in a session, persists
throughout the session, this works to enforce the security.
However, there is a huge security hole. The assignment of the
package variable is done in the application, but what if I log
in to the database as a non-critical user, and then set the
package variable myself?
execute
myvars.app_userid := ‘SCOTT’
And that makes me Scott immediately! Not just Scott; I
can be anyone I wish to be — Tom, Dick, Harry, anyone,
as long as they are named as users of the system. Thus, the
security goes down the drain, and it falls back on
application-based security for a foolproof implementation,
argued the critics.
But, as I already mentioned, I’m not a huge fan of the model; I
still think the database should handle authorization, not the
application. This is a failure of the reliability aspects of the
mechanism; the method of passing the information about the user
was neither reliable nor secure enough. We’ve got to find a
different way.
A Different Type of Global Variable
This is where application contexts come into the
picture. Simply put, application contexts are analogous to
global variables in the sense that once a value is assigned,
they can be referenced throughout the session. However, that’s
where the similarity ends. The biggest differentiator is the way
the assignment is made — and that’s where the security is
provided.
You may have been using application contexts without realizing
it. Note the clause,
am_name =
USER
Here the function USER is actually an implementation of
SYS_CONTEXT(‘USERENV’,’SESSION_USER’)
The built-in function
SYS_CONEXT is a function that reads a certain part of the
memory called Application Context, which can be defined
by the user. The context can hold values that are set for a
session and can be referenced throughout the session. A
different session may hold a different value of the variable
inside an application context.
Here an attribute named
SESSION_USER of the application context
USERENV has been
accessed to get the user information. The context
SYS_CONTEXT was
already defined when the database was created. It has two
distinct parts:
1. Namespace
2. Attribute
In this example, the namespace was
USERENV and the
attribute was
SESSION_USER. The value of the attribute is set at the
runtime. When the Oracle user Scott logs in, the value is set to
SCOTT. Similarly,
there are several other attributes of the namespace
USERENV as well.
Application Contexts also resemble the table record structure —
they have attributes, which are analogous to columns. The
difference is the attributes are not named during creation. You
define an application context as
create
context acc_ctx
using set_acc_ctx;
Note the second line, “using …” carefully. This indicates that
the context attributes can only be set by the procedure
set_acc_ctx.
Obviously, we need to create one such procedure.
1 create procedure set_acc_ctx
2 (
3 p_attribute_name in varchar2,
4 p_attribute_value in varchar2
5 ) is
6 begin
7 dbms_session.set_context(
8 'acc_ctx',
9 p_attribute_name,
10 p_attribute_value);
11* end;
Note line 7, through which the context attributes are set.
Instead of setting a specific value, we have left a placeholder
in form of a parameter, which can be passed to the procedure.
Since attributes are not named at runtime, but rather, during
the assignment, we can pass any attribute name and its
associated value. For instance, we can pass the attribute named
app_userid and
its associated value, such as “SCOTT” as in:
execute
set_acc_ctx (‘app_userid’,’SCOTT’)
This sets the value of the
attribute app_userid
to SCOTT. If
after calling the procedure, you want to check the contexts set
in the session, you could issue
select *
from session_context
/
NAMESPACE ATTRIBUTE VALUE
----------------- ---------- -----
ACC_CTX APP_USERID SCOTT
ACC_CTX ATTRIBUTE2 VAL1
As in case of USERENV
contexts, you can also check the value of a specific attribute
of a specific context in a session. To check the attribute
APP_USERID in
ACC_CTX, you
could use:
SELECT
SYS_CONTEXT('ACC_CTX','APP_USERID')
FROM DUAL;
This returns the value of the attribute. If we can set the value
to the application userid, the policy function in the VPD policy
should return a predicate such as:
SELECT *
FROM EMP
WHERE (
AM_NAME = USER
OR
AM_NAME = SYS_CONTEXT(‘ACC_CTX’,’APP_USERID’)
)
Trusted Procedure
The
attribute is actually set in line seven by calling the
dbms_session.set_context
supplied PL/SQL procedure. So, you may ask, what is the big deal
about writing another wrapper procedure around it? Can’t we just
call
dbms_session.set_context directly?
Let’s see ... suppose Joe tries to set the context directly:
begin
dbms_session.set_context(
namespace => 'acc_ctx',
attribute => 'app_userid',
value => 'SCOTT'
He gets an error.
ERROR at
line 1:
ORA-01031: insufficient privileges
ORA-06512: at "SYS.DBMS_SESSION", line 78
ORA-06512: at "ANANDA.SET_CLAIM_APP_CTX", line 7
ORA-06512: at line 1
The errors show insufficient privileges on
dbms_session.
However, granting the execute privileges to the user does not
make the error go away. The user simply cannot directly set the
context. This makes this procedure a trusted one —
meaning that only the procedure can set the associated context.
This is a very important concept in a discussion about contexts.
So far you have seen that a context can only be set using its
associated trusted procedure, no other way. So, how does that
make the context more secure? First, consider the mechanism to
authenticate and authorize. Since executing the procedure is the
only way to set the context, we can place all types of security
check inside this procedure to make sure all the checks are
satisfied before the context is set. If any of the checks fail,
the procedure raises an error, and the context is never set.
These checks can be:
1. Making sure that the connections are coming from the right
client (IP, client name, and so on)
2. Making sure that the connections are requesting the type of
data usually expected at that time of the day. For instance,
brokerage transactions are not executed after the stock market
closes. So, those types of transactions can be set to return a
null predicate for the policy function. However, some brokerage
companies use alternative exchanges such as Instinet and Island,
where the transactions are permitted up to a later time; those
transactions can be allowed until such time.
3. Making sure the user making a request is supposed to be
making requests from those clients. For instance, the connection
pool exists for application servers appsvr1 through appsvr5, and
they connect to the database using the Oracle userid
CONNPOOL. The
checks could ensure that the when userid is
CONNPOOL, the
client machines are between appsvr1 to appsvr5. The same test
could be other way around as well, i.e., all connections coming
from appsvr1 should be only as user
CONNPOOL, and so
on.
Other Uses of Application Contexts
In VPD, the predicate string is constructed by the policy
function. Suppose, instead of having a dedicated account manager
for an account, a department is allocated a chunk of accounts
that anyone in the department can view. This makes the policy
predicate look like
where
account_no in (111,222,333,…etc…)
Sure, the string of account numbers separated by commas can be
easily
constructed by a PL/SQL procedure by looping through the
numbers, but this creates a serious performance problem. Each of
the strings will be unique, and a bind variable can’t be used.
This increases parsing rate for the statement. As you know from
any performance tuning exercise, more parsing leads to more CPU
consumption, latch contention and library cache lock waits. One
way to reduce is to make the latter part of the string an
attribute of a context. For instance, the context acc_ctx could
have an attribute named acc_list, which can be populated with
the account numbers separated by commas and then the predicate
can be written as:
where
account_no in sys_context(‘acc_ctx.’,’acc_list’)
When the statement is parsed, it is done so as
where
account_no in :b1
or something similar. This reduces hard parses, CPU cycles and
shared-pool fragmentations, among other things.
Generic User Privilege Management
Now that you understand how powerful application contexts can
be, let’s see more examples of it in action. One of the thorny
problems in managing generic users such as those used by
connection pools is deciding their privileges. Application users
Scott, Joe, and John have three different types of authority
levels. Scott has privileges to read any table, but update the
ones in Checking Account only; Joe can read checking account
customers only, and cannot update anything, and John can update
any account. Since the user
CONNPOOL must
have all the privileges required by the application users, at
the minimum, it must have the largest common denominator of the
privilege list for the user to work properly. In this case,
CONNPOOL must
have the privilege to update all the tables, or else John can’t
have the privileges he needs. However, that means that Joe also
has the same privileges that John has, which Joe does not need.
This is unacceptable from a security point of view. What can be
done?
The answer lies in using application contexts and roles
effectively. Here you need to create three roles with the
associated privileges:
1. UPDATE_ANY –
update any table
2. SELECT_ANY –
select any table
3. SELECT_CHECKING
– select checking account tables only
These roles are then granted to the user
CONNPOOL:
grant
update_any, select_any, select_checking to connpool;
Well, you might ask, this does not resolve the issue. Instead of
granting the privileges directly, we assigned it via roles,
which means CONNPOOL
still has the privilege to update any table, and since Joe uses
a connection from the pool, he can update any table — the very
thing we are trying to avoid.
The trick lies in making the roles disabled. To use the
privileges granted, a user must not only be granted the role,
but the role must be enabled as well. If a role is granted but
disabled, the privileges are not available. Take the following
example:
SQL> connect
connpool/******
Connected.
SQL> Update
savings.accounts set acc_name = ‘DICK’ where acc_no = 1;
1 row
updated.
The user can
do the operation. Now disable all the roles and retry.
SQL> set
role none;
Role set.
SQL> update
savings.accounts set acc_name = ‘DICK’ where acc_no = 1;
ERROR at
line 1:
ORA-00942: table or view does not exist
The operation failed, since the role
UPDATE_ANY, which
allows the update, was not enabled. Now enable it, and retry the
operation:
SQL> set
role update_any;
Role set.
SQL> update
savings.accounts set acc_name = ‘DICK’ where acc_no = 1;
1 row
updated.
The operation was successful. The roles must be enabled to have
the privileges in place; granting them to a user is not enough.
However, we didn’t enable the role during the very first case;
so why were the role privileges in place?
This is
because of
the
way a role is defined for a user. A user can have one or more
default roles, which means that those roles are automatically
enabled when the user logs in. If the user has no default role,
then a role granted to him must be explicitly enabled using the
SET ROLE command.
Be default, the roles granted to a user are default. When
CONNPOOL was
granted the roles, all of them became default roles. This can be
prevented by issuing
alter user
CONNPOOL default role none;
Now CONNPOOL has
no default role and when it logs in no roles will be enabled.
This is the trick we are going to use for the privilege
management.
Before we go further, let’s see how to check the roles enabled
in a session. We can check it by issuing:
select *
from session_roles;
no rows
selected
No roles have been enabled in the session — yet. Now, we can
build a procedure to set the role:
1 create or replace procedure set_user_role
2 authid current_user
3 is
4 l_user_role varchar2(2000);
5 begin
6 select sys_context('role_ctx','user_role')
7 into l_user_role
8 from dual;
9 dbms_session.set_role (l_user_role);
10 end;
In line nine, the equivalent of
SET ROLE command,
dbms_session.set_role,
was used. It does the same thing — it enables a role in the
session. Note some very important points about this procedure.
1. First, there are no arguments to the procedure. This means
that when the user calls the procedure, he does not specify the
role to enable; it will be retrieved from the application
context known as
ROLE_CTX. The value of the attribute
USER_ROLE will be
read by this procedure and that role will be enabled by default.
2.
Second, the execute privileges for the user
on
dbms_session
package are revoked are revoked; he can’t
just call
dbms_session.set_role (‘UPDATE_ANY’).
Also, the SET
command has been disabled by the product user profile tables, so
SET ROLE will not
work.
3.
Third, we will not let the user set the context by calling its
trusted procedure. Note that this is a different context —
ROLE_CTX, not
APP_CTX, so we
can control the access. This will be called from the context
setting procedure after extensive checks. When checks are
successfully
completed, the attribute
USER_ROLE will be set to
UPDATE_ANY when
the application user is
John. If the application user is
Joe, the
application context will be set to
SELECT_CHECKING.
Now let’s see this in action. Joe is trying to update some
records, a clear violation of the policies.
1. First he logs in to the database as user
CONNPOOL.
2. Then he tries to update the table, but since there are no
default roles for the user
CONNPOOL, the
session has no privilege to do anything, and the update fails
due to insufficient privileges.
3. He tries to set the role by
dbms_session.set_role(‘UPDATE_ANY’), but since
CONNPOOL has no
privileges on that package, it will fail.
4. Then he realizes that application user John has privileges to
update any table. He tries to pretend he is John to set the
context by calling
set_acc_ctx(‘acc_ctx’,’app_userid’,’JOHN’). This
operation fails since the checks inside the procedure fail. The
application context attribute
app_userid is
null and so is the attribute
user_role of the
context role_ctx.
5. Then he tries to set the role by calling
set_user_role,
but it does nothing, since the value of
role_ctx.user_role
is null.
At the end of these tries, Joe is still connected as a user
CONNPOOL, but
with no privileges. He can, of course, log in as himself, which
was allowed, anyway.
From this example, you can see how powerful application contexts
can be to manage complex requirements such as the user role
management.
Conclusion
In
summary, application contexts are akin to global
variables that are accessible in a session once set. Unlike
global variables, they can be set through a special procedure
known as trusted procedure, which is defined when the
context is
created.
Since the trusted procedure is the only way to set a context, it
can be designed with a variety of checks and balances to prevent
unauthorized access. The applications contexts can be used in
Virtual Private Databases to provide a predicate string that
does not need to be hard parsed, saving considerable CPU
resources. Using non-default roles for generic userids,
applications can manage varied privileges for diverse sets of
application users.
The
type of application contexts discussed here are visible only
from within a session. Since they reside in PGA, not SGA, other
sessions can’t access them, making them highly secure. Another
type of application contexts — global contexts —
aid in development of secure, Web-based applications. Global
contexts will be discussed in a later article.
|