Showing posts with label security. Show all posts
Showing posts with label security. Show all posts

Sunday, February 21, 2010

Database Application Security - A Visualization

Security, in regards to the database, is pretty broad. There are a multitude of ways to secure your database. From physical or logical access to SQL Injection.

This is a (not so) pretty picture of how I visualize security from an application point of view.

Secure OLTP Application

The base of any application are the tables. Access to those tables should be limited to database views and PL/SQL packages (API) that are owned by the application (schema).

SELECT privileges on those views should be given to only users who need it (users also includes other applications). Views present the data in the format that is required by the application. VPD (column or row) can be used to further restrict access to sensitive data.

EXECUTE privileges on the APIs (PL/SQL) should only be given to those applications that need it. Under no circumstances, should direct INSERT/UPDATE/DELETE privileges be given to another user.

Much of my thoughts on this matter are directly traceable to reading AskTom for so many years. I can't point to any one specific article, I think it's the accumulation/internalization of reading for so long.

It makes sense though.

Who better to decide the functions (INSERT/UPDATE/DELETE) that can be performed against a given set of tables than the person(s) who created them? If said person has left, make someone else responsible for them. I've fought hard over the years to implement this "pattern" and have been fairly unsuccessful. It is difficult to go against years of doing it one way (full access to the underlying tables) to forcing someone or something to use the API.

The idea to visualize it came to me recently and I need to put it down.

What do you think? Am I full of it? Is this reasonable? Anything I left out?

Monday, February 15, 2010

Database Security?

I'm not sure what are the question marks are lately...I can't think of better titles so perhaps it's me guessing at the title.

I'm sadly dependent on SQL*Plus...and when I mean "sadly" I mean I go to great lengths to get the Windows version which was obsolesced (sp?) in 11gR1. I could never get it quite right, the output I mean, but it was just so easy. Type in your SELECT statement, hit Enter and go. Easy. Yes, yes, I know, you can use the shell version too...I just liked the Windows version despite all it's quirks.

Anyway, as part of my login.sql script, I have the following 2 queries:
prompt ***roles granted***
SELECT role FROM session_roles;
prompt
prompt ***system privileges granted***
SELECT privilege
FROM user_sys_privs
UNION
SELECT privilege
FROM role_sys_privs
WHERE role IN ( SELECT role FROM session_roles );
Each time I login or connect to a different database, this script runs. The results of those 2 queries, the first shows the roles I have been granted and the second query returns the system privileges I (or one of my granted roles) have. I'm sure I got this from Tom Kyte...I've been using this for years and I can't remember when or where I began to use it.

Having changed jobs quite a few times over the past few years, I've gotten to see many different environments. These scripts give me a quick glance at what I am able to do in a particular database, which is very helpful.

Where am I going? Not really sure.

I know that system level privileges are usually a no-no. CREATE TABLE, CREATE VIEW, CREATE PROCEDURE, those are the basic items that you need to build objects in your own schema. CREATE ANY TABLE, CREATE ANY VIEW and CREATE ANY PROCEDURE will allow you create said objects in any schema (except SYS...I think, I don't have my power cable thus I can't start up my vm which contains my database).

Having read AskTom for so many years, I know or understand that system level privileges are bad, very bad.

At one former job, in the production database, not only did I have CREATE ANY TRIGGER, I had EXECUTE ANY PROCEDURE. I won't go into why or how those are very, very bad, you can research those yourself. Or you can just buy David Litchfield's The Oracle Hacker's Handbook: Hacking and Defending Oracle.

Pointing those types of things out though can be tricky...to put it mildly. Lots of politics involved. Trust me on this.

Hopefully you can glean a point or at least some information from this post. My mind is not all there due to the massive brain infection I have.

Thursday, August 6, 2009

Users and Roles - Revisited

In a previous post, How To: Users and Roles (Simple), I went through and demonstrated one way to build a simple security system using users and roles.

I'm using the Database Authentication security scheme in Apex. Patrick Wolf [ blog ] pointed out that I should create database roles instead of using my own. That is the end game of course as I've limited the role_name column to VARCHAR2(30). But for now, this will have to do.

38 lines is now 27

The previous code looked like this:
FUNCTION is_authorized
( p_username IN VARCHAR2,
p_rolename IN VARCHAR2 ) RETURN BOOLEAN
IS
l_count INTEGER;
l_rolename VARCHAR2(30);
BEGIN
SELECT COUNT(*)
INTO l_count
FROM v_active_user_roles
WHERE user_name = p_username
AND role_name = p_rolename
AND rownum < 2;

IF l_count = 1 THEN
--user has this role assigned
RETURN TRUE;
ELSE
--user does not have this role assigned
--check to see if user's role inherits these privs
l_count := NULL;

SELECT COUNT( DISTINCT role_name )
INTO l_count
FROM v_roles
WHERE status = 'A'
AND end_date IS NULL
AND INSTR( role_path, p_rolename ) > 0
AND INSTR( role_path, p_rolename ) < INSTR( role_path, role_name );

IF l_count > 0 THEN
RETURN TRUE;
ELSE
RETURN FALSE;
END IF;

END IF;
END is_authorized;
and I found it didn't work too well.

Specifically, the person with the least privileges, TAB_ADMIN, could see everything that only the ADMIN could see.

There was another small problem, using INSTR was screwed up to. Can you guess why?

'ADMIN'?

That would return a hit back for 3 roles: ADMIN, SECTION_ADMIN and TAB_ADMIN.

Bad developer, bad. (Another great reason you should NOT roll your own security!)

What was the problem again?

Let me quickly recap what I am trying to do with a "pretty" picture:



I'm going to try and articulate this with words, please don't laugh. ADMIN is the all powerful, it should be able to do anything it wants to do. There are 5 authorization schemes, one for each role; placed on different objects (pages, items, tabs, etc).

If a button has the TAB_ADMIN authorization schema, ADMIN should be able to run it through inheritance. The picture above is trying to depict that with those red arrows.

So, what's the point of this post? I've forgotten, so I'm skipping to the end.

I refactored the IS_AUTHORIZED procedure from 38 lines to 27 and I believe it's a bit more intuitive than it was. The key for me was included ROLE_LEVEL which is just an alias for the pseudo-column LEVEL.

If the ROLE_LEVEL, of the enabled roles for a user, is less than the ROLE_LEVEL where ROLE_NAME is part of the ROLE_PATH (SYS_CONNECT_BY_PATH), you're in.

Or something like that.

Here's the final product:
FUNCTION is_authorized
( p_username IN VARCHAR2,
p_rolename IN VARCHAR2 ) RETURN BOOLEAN
IS
l_count INTEGER;
BEGIN
FOR i IN ( SELECT role_name, role_path, role_level
FROM v_active_user_roles
WHERE user_name = p_username )
LOOP
IF p_rolename = i.role_name THEN
RETURN TRUE;
ELSE
SELECT COUNT(*)
INTO l_count
FROM v_roles
WHERE INSTR( role_path, i.role_name, -1 ) > 0
AND role_level > i.role_level;

IF l_count > 0 THEN
RETURN TRUE;
ELSE
RETURN FALSE;
END IF;
END IF;
END LOOP;
END is_authorized;
I'll also be updating the other post to point to this one.

Small reminder: TESTING, TESTING, TESTING

Monday, July 20, 2009

How To: Users and Roles (Simple)

I'm using Apex again, which is nice. I love the database work; cleaning, refactoring, etc. but there's just something very cool about being able to put a "face" on that database stuff.

I'm mostly done with the application and I'm going through adding in security. It's not an after thought, I'm just not going to add it first as it will make my life very difficult.

I would love to use Database Authentication, but it only checks to see if you have an account. Your database roles do not carry over into Apex (yet). I don't want to use Apex Authentication mostly because I've never used it.

I could create my own table based security, but not yet. I'm trying to keep it as simple as possible for now.

I decided on a mix of Database Authentication and table based users and roles. I will not be storing passwords nor writing any custom authentication. I'll just rely on Oracle to do that. I will however capture the username and put it in an Application Item. In the Administrator's interface, the users will be able to create users...but only if they already exist in the database. I will be creating the following roles:

--ADMIN - this role can do anything. full access to the application.
--SECTION_DEVELOPER - as it stands, we can not completely hand this off to the business. There will be some intervention required by IT. This role should be able to do all that the business person can do and a couple of extra actions.
--SECTION_ADMIN - this will be the role assigned to the business person.
--TAB_DEVELOPER - similar to SECTION_DEVELOPER, just another section of the application
--TAB_ADMIN - same as SECTION_ADMIN, assigned to business user

Perhaps an easier way to visualize it:
ADMIN
--SECTION_DEVELOPER
----SECTION_ADMIN
--TAB_DEVELOPER
----TAB_ADMIN

Here's the table to hold the roles:
CREATE TABLE t_roles
(
role_name VARCHAR2(30)
CONSTRAINT pk_rolename PRIMARY KEY,
parent_role_name
CONSTRAINT fk_rolename_troles REFERENCES t_roles( role_name )
);

INSERT INTO t_roles( role_name )
VALUES ( 'ADMIN' );
INSERT INTO t_roles( role_name, parent_role_name )
VALUES( 'SECTION_DEVELOPER', 'ADMIN' );
INSERT INTO t_roles( role_name, parent_role_name )
VALUES( 'TAB_DEVELOPER', 'ADMIN' );
INSERT INTO t_roles( role_name, parent_role_name )
VALUES( 'SECTION_ADMIN', 'SECTION_DEVELOPER' );
INSERT INTO t_roles( role_name, parent_role_name )
VALUES( 'TAB_ADMIN', 'TAB_DEVELOPER' );
I'm creating a view on top of this table because I want to use some of the hierarchical capabilities.
CREATE OR REPLACE
VIEW v_roles
AS
SELECT
role_name,
parent_role_name,
SYS_CONNECT_BY_PATH( role_name, '/' ) role_path,
level role_level,
CONNECT_BY_ISLEAF is_leaf,
RPAD( '-', level * 2, '-' ) || role_name display_role_name
FROM t_roles
START WITH parent_role_name IS NULL
CONNECT BY PRIOR role_name = parent_role_name;
I created a procedure, is_authorized which took in the username (Application Item) and a literal; ADMIN, SECTION_DEVELOPER, SECTION_ADMIN, TAB_DEVELOPER, and TAB_ADMIN. It returned either TRUE or FALSE (boolean).

Here's the table definitions for the user and user/role intersection tables:
CREATE TABLE t_users
(
username VARCHAR2(30)
CONSTRAINT pk_username PRIMARY KEY
);

INSERT INTO t_users( username ) VALUES ( 'CJUSTICE' );

CREATE TABLE t_user_roles
(
username
CONSTRAINT fk_username_userroles REFERENCES t_users( username )
CONSTRAINT nn_username_userroles NOT NULL,
role_name
CONSTRAINT fk_rolename_userroles REFERENCES t_roles( role_name )
CONSTRAINT nn_rolename_userroles NOT NULL
);

INSERT INTO t_user_roles( username, role_name )
VALUES( 'CJUSTICE', 'ADMIN' );

CREATE OR REPLACE
VIEW v_active_user_roles
AS
SELECT
ur.username,
r.role_name,
r.parent_role_name,
r.role_path,
r.role_level,
r.is_leaf,
r.display_role_name
FROM
v_roles r,
t_user_roles ur
WHERE r.role_name = ur.role_name;
Now I need a FUNCTION that will tell me whether this person is authorized or not.

I struggled with this a little bit (hence the post).
DECLARE
l_count INTEGER;
BEGIN
SELECT COUNT(*)
INTO l_count
FROM v_active_user_roles
WHERE username = :p_username
AND role_name = :p_role_name;

IF l_count = 1 THEN
RETURN TRUE;
ELSE
RETURN FALSE;
END IF;
END;
OK, so if the code from Apex is passing in CJUSTICE for the username and ADMIN for the role, it works. But ADMIN is the all powerful user. So I need another check in there:
DECLARE
l_count INTEGER;
BEGIN
SELECT COUNT(*)
INTO l_count
FROM v_active_user_roles
WHERE username = :p_username
AND role_name = :p_role_name;

IF l_count = 1 THEN
RETURN TRUE;
ELSE
SELECT COUNT(*)
INTO l_count
FROM v_active_user_roles
WHERE ...
END IF;
END;
That's where I kind of got stuck. The extra SELECT COUNT(*) would have needed at least one more additional IF THEN control statement. Time to step away.

Thinking about it, it might be easier if ADMIN were the bottom-most...leaf? In other words:
SECTION_ADMIN
--SECTION_DEVELOPER
----ADMIN

Using SYS_CONNECT_BY_PATH it would have been easy to check. I think.

If ADMIN is at the bottom though, you'll have to jump through all kinds of code hoops to keep it at the bottom. That's the indicator to me that I'm overthinking it.

What about a loop, using ROLE_PATH (SYS_CONNECT_BY_PATH)? Something like this:
    FOR i IN ( SELECT role_path
FROM v_roles
WHERE INSTR( role_path, l_rolename ) > 0 )
LOOP
IF INSTR( i.role_path, l_rolename ) < INSTR( i.role_path, p_rolename ) THEN
RETURN TRUE;
END IF;
END LOOP;
If the user's role (ADMIN) is at a place closer to the front of the string and the passed in role (TAB_ADMIN) is further back, the user will be authorized to see/view/execute whatever the authorization is applied to. Here's the code all put together:
CREATE OR REPLACE
FUNCTION is_authorized
( p_username IN VARCHAR2,
p_rolename IN VARCHAR2 ) RETURN BOOLEAN
IS
l_count INTEGER;
l_rolename VARCHAR2(30);
BEGIN
--1. see if the user/role exists
--1a. if true, return true
--1b. if false
--1b1. find the role the user is assigned to
--1b2. see what roles inherit from the user's role
SELECT COUNT(*)
INTO l_count
FROM v_active_user_roles
WHERE username = p_username
AND role_name = p_rolename;

IF l_count = 1 THEN
RETURN TRUE;
ELSE
BEGIN
SELECT role_name
INTO l_rolename
FROM v_active_user_roles
WHERE username = p_username;
EXCEPTION
WHEN no_data_found THEN
RETURN FALSE;
END;

FOR i IN ( SELECT role_path
FROM v_roles
WHERE INSTR( role_path, l_rolename ) > 0 )
LOOP
IF INSTR( i.role_path, l_rolename ) < INSTR( i.role_path, p_rolename ) THEN
RETURN TRUE;
END IF;
END LOOP;

RETURN FALSE;
END IF;
END is_authorized;
/
show errors
It's certainly not the best solution in the world. When I have a bit more time, I'll certainly revisit it.

Integration with Apex
Since I haven't worked with Apex regularly since 2.2, I started to use the Conditions section to store the code. What that would mean is possibly storing
RETURN is_authorized( :F_USERNAME, 'ADMIN' /*(or others)*/ );
in about 60 locations throughout the application. Then I saw this:


I had completely forgotten about that! Authorization Schemes allow you to define as many...schemes as you want. I created 5, one for each role.

Added bonus, when I need to change these, I only have to do so in one place.

You can add Authorization Schemes to almost everything. Pages, HTML Regions, Buttons, Items, Columns in reports, etc. As fine a granularity as you would ever need.

Update: 08/07/2009
This function does not work! Sue me. There is a later post addressing the issues I found, Users and Roles - Revisited.

Tuesday, July 14, 2009

DBA: Poor Man's Data Vault

Nothing against women, so don't start with me. By "Man" I mean (hu)Man. Though there are plenty out there that would argue against any DBA, in any way, resembling a human.

Back in March I started this project on Google Code. Here I am 4 months later and I've finally gotten around to it.

What is it?
Version 1.0, or better, Version 0.1, will simply require you to set a context prior to deployment. All that means is this:
CJUSTICE@TESTING>BEGIN
pmdv_work_pkg.create_work
( p_name => 'TICKET #44',
p_description => 'CLEANING UP CJUSTICE SCHEMA' );
END;
/
What's the big deal with that?
Well, it depends. The idea was originally sparked by watching my DBAs deploy my projects at a previous job. After deployment, they would run this script, enter the ticket number, start and stop time and it would spool off this report. I never looked at the report, but guessed that it had something to do with Security or Auditing. So I made the grand leap to assume it was all the objects that were affected by the deployment. It was all an assumption though as I was, on occasion, asked to provide details on objects that I deployed by the Security team.

What does it do?
It's pretty simple actually. One trigger, ON DATABASE BEFORE DDL is set up. That calls a package with the relevant exceptions, and determines whether the context has been set (if the other exceptions are not met). If all the exceptions are met and the context is set, you can deploy your changes (DDL). If not, you get a pretty error message like this:
CJUSTICE@TESTING>@packages/pmdv_work_pkg.pkb
CREATE OR REPLACE
*
ERROR at line 1:
ORA-00604: error occurred at recursive SQL level 1
ORA-20001: PMDV: DEPLOYMENTS NOT ALLOWED!
ORA-06512: at line 14
When you first create your work (order), the start date and end date are set (end_date defaults to 30 minutes, but time can be added if necessary). Also, all INVALID objects are captured prior to deployment. Once you are finished, you issue the following commands:
CJUSTICE@TESTING>EXEC pmdv_work_pkg.end_work;

COMMIT;
When you issue the END_WORK command, the INVALID objects are captured again (DURING), the work record updated, then any changed objects during the deployment window are captured (from DBA_ALL_OBJECTS.LAST_DDL_TIME).

For those of you worried about putting such a thing in production, I am also providing a trigger (AFTER DDL) and a table to capture the DDL in your environment. That way you can note the exceptions and add them to the code before ever throwing such a thing in production. Not that I think many of you would do that...especially without more rigorous testing at the very least.

Testing is my next step. I know, it's backwards. I just want this out the door. I want someone to look at it and say, "Hey Chet, that's a piece of crap." or "You're nuts if you think I would ever put this on a production system!"

I hear you. Testing will be done. Test cases will also be provided so that you can verify the results as well. Hopefully, if enough of you nit-picking, anal-retentive DBAs take a look at it and critique it (good and bad), I can make something of it. Something that would be useful to everyone. So go crazy on it please.

Here's the package spec for PMDV_WORK_PKG:
FUNCTION CAN_DEPLOY_DDL RETURNS BOOLEAN
Argument Name Type In/Out Default?
------------------------------ ----------------------- ------ --------
P_ORA_SYSEVENT VARCHAR2 IN
P_ORA_LOGIN_USER VARCHAR2 IN
P_ORA_INSTANCE_NUM NUMBER IN
P_ORA_DATABASE_NAME VARCHAR2 IN
P_ORA_DICT_OBJ_NAME VARCHAR2 IN
P_ORA_DICT_OBJ_TYPE VARCHAR2 IN
P_ORA_DICT_OBJ_OWNER VARCHAR2 IN
CAN_DEPLOY_DDL is used by the trigger, PMDV_BEFORE_DDL to determine whether or not the DDL is allowed. Currently, I have 3 exceptions:
1.  If the ORA_LOGIN_USER = SYS. DDL OK.
2.  If the ORA_LOGIN_USER IS NULL. I noticed this behavior on a 10g instance. DDL OK.
3.  By default Oracle does not allow objects to be created in the SYS schema. Unless your system is compromised in some way, it is reasonably safe to assume that if the ORA_DICT_OBJ_OWNER = SYS, DDL is OK.
4.  PMDV_CONTEXT is set. DDL is OK.

If you see anything in that list that jumps out at you, please let me know. I absolutely do not want to introduce something into the wild that could be harmful.

FUNCTION CREATE_WORK RETURNS NUMBER
Argument Name Type In/Out Default?
------------------------------ ----------------------- ------ --------
P_NAME VARCHAR2 IN
P_DESCRIPTION VARCHAR2 IN
P_ESTIMATED_TIME DATE IN DEFAULT
This gets it all started. It accepts 2 strings, they can be anything, there is no validation check on them. P_ESTIMATE_TIME defaults to 30 minutes. You can either create the work (order) with the default, it give yourself an hour, or however much time you need really.
PROCEDURE END_WORK
This ends your session, captures INVALID and CHANGED objects and clears the context.

PROCEDURE EXTEND_DEPLOYMENT_WINDOW
Argument Name Type In/Out Default?
------------------------------ ----------------------- ------ --------
P_TIME_TO_ADD NUMBER IN
Kind of self explanatory. Run this in the same (SQL*PLus) session to extend your window.

PROCEDURE JOB_LISTENER
When you issue the CREATE_WORK statement, the PMDV_LISTENER job is enabled and runs once a minute to check if your session has expired. If you finish in 5 minutes, you don't necessarily have to issue the END_WORK call. The job will expire the work (order) after the allotted time.
PROCEDURE RECONNECT
Argument Name Type In/Out Default?
------------------------------ ----------------------- ------ --------
P_CLIENT_IDENTIFIER VARCHAR2 IN
RECONNECT allows you to open up another session and re-enable the context (i.e. allow you to deploy). You can find this information in PMDV_WORK.CLIENT_IDENTIFIER column.

Since I'm using DBMS_SESSION.UNIQUE_SESSION_ID, you can only have 1 deployment per SQL*Plus session. All you need to do to get around that is DISCONNECT and login again.

For the love of Pete, give me some feedback. Is this something you could ever use? Am I silly (well, you don't have to answer that specifically) for thinking this up? Any and all comments will be most appreciated. I've spent a significant amount of time over the last couple of weeks getting this together...hopefully to some end.

Where do I find this?
The code can be found here. I'll be wrapping it up all nice and pretty over the next couple of days. I'll create a download package as well. The wiki page can be found here.

Oh yeah, did I mention, I built an Apex front end for it?

Here's what that looks like.

The Work (Order) Screen:


And the Work (Order) Details Screen:

Sunday, July 5, 2009

SQL: SYS_CONTEXT

In my experience so far, I've seen very few places where SYS_CONTEXT is used. It is typically used in conjunction with CONTEXTs and also Virtual Private Database (VPD).

Oracle has a built in namespace called USERENV and their is a wealth of information you can retrieve from there. I wrote up a quick view to demonstrate the use of SYS_CONTEXT:
SELECT
SYS_CONTEXT( 'USERENV', 'ACTION' ) action,
SYS_CONTEXT( 'USERENV', 'AUTHENTICATION_METHOD' ) authentication_method,
SYS_CONTEXT( 'USERENV', 'BG_JOB_ID' ) bg_job_id,
SYS_CONTEXT( 'USERENV', 'CLIENT_IDENTIFIER' ) client_identifier,
SYS_CONTEXT( 'USERENV', 'CLIENT_INFO' ) client_info,
SYS_CONTEXT( 'USERENV', 'CURRENT_SCHEMAID' ) current_schemaid,
SYS_CONTEXT( 'USERENV', 'CURRENT_SCHEMA' ) current_schema,
SYS_CONTEXT( 'USERENV', 'CURRENT_USER' ) current_user,
SYS_CONTEXT( 'USERENV', 'DB_DOMAIN' ) db_domain,
SYS_CONTEXT( 'USERENV', 'DB_NAME' ) db_name,
SYS_CONTEXT( 'USERENV', 'DB_UNIQUE_NAME' ) db_unique_name,
SYS_CONTEXT( 'USERENV', 'HOST' ) host,
SYS_CONTEXT( 'USERENV', 'INSTANCE' ) instance,
SYS_CONTEXT( 'USERENV', 'INSTANCE_NAME' ) instance_name,
SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ) ip_address,
SYS_CONTEXT( 'USERENV', 'ISDBA' ) isdba,
SYS_CONTEXT( 'USERENV', 'LANG' ) lang,
SYS_CONTEXT( 'USERENV', 'LANGUAGE' ) language,
SYS_CONTEXT( 'USERENV', 'NETWORK_PROTOCOL' ) network_protocol,
SYS_CONTEXT( 'USERENV', 'NLS_CALENDAR' ) nls_calendar,
SYS_CONTEXT( 'USERENV', 'NLS_CURRENCY' ) nls_currency,
SYS_CONTEXT( 'USERENV', 'OS_USER' ) os_user,
SYS_CONTEXT( 'USERENV', 'SERVER_HOST' ) server_host,
SYS_CONTEXT( 'USERENV', 'TERMINAL' ) terminal
FROM dual
Which yields the following:
ACTION:  NULL
AUTHENTICATION_METHOD: PASSWORD
BG_JOB_ID: NULL
CLIENT_IDENTIFIER: NULL
CLIENT_INFO: NULL
CURRENT_SCHEMAID: 88
CURRENT_SCHEMA: CJUSTICE
CURRENT_USER: CJUSTICE
DB_DOMAIN: NULL
DB_NAME: testing
DB_UNIQUE_NAME: testing
HOST: cdj-laptop
INSTANCE: 1
INSTANCE_NAME: TESTING
IP_ADDRESS: 192.168.1.4
ISDBA: FALSE
LANG: US
LANGUAGE: AMERICAN_AMERICA.WE8MSWIN1252
NETWORK_PROTOCOL: tcp
NLS_CALENDAR: GREGORIAN
NLS_CURRENCY: $
OS_USER: chet
SERVER_HOST: oracledb
TERMINAL: UNKNOWN
Basically, these are global variables that Oracle stores on the current session/user.

You can create a context for just about anything you want. I've used them in the past along with table based security in a stateless environment. In essence, I create a record in a table with a SESSION_ID, I then store that in the context (global variable) and I use that to reconnect to the database each time.

I promise to have an example of using it in the near future.

Tuesday, April 7, 2009

Poor Man's Data Vault - Update

It's been a little over a month since I've written anything about it and seeing as how I have not posted anything Oracle related in what seems like years.

A quick reminder, Poor Man's Data Vault, PMDV for short, is a very basic tool to allow you lockdown your production environment. In essence, you are required to "submit" a ticket before any DDL is allowed in your specified schemas. When you "submit" or run a procedure you must supply a ticket number or description. Obviously there is no way to verify (yet) that this is an actual ticket, but it is a start. The ideal in the regard would be to query your ticketing/bug tracking database to validate. Currently we have a pretty sweet integration of Subversion, Fisheye, Bamboo and Jira. If we add the ticket number to the Subversion comments, we can then see (via Fisheye) what code is attached and even better do a diff right in the browser.

So, using Jira would be nice, but it's out of scope at this time.

Here's the final model I came up with:



PMDV_WORK is the driving table. It stores the records on the deployment or fix. PMDV_INVALID_OBJECTS takes a snapshot before, during and after the deployment of objects in an INVALID state. PMDV_CHANGED_OBJECTS will capture all the objects that were affected by the deployment (new and altered).

I'm starting with those 3 (because I forgot what I was going to do with PMDV_PRIVILEGES). I have created those 3 tables and checked them into source control (Google Code). You can find the project here. If you would like to join the "project" (yes, it's in quotes because I haven't done a whole lot but would love to actually finish one. Besides, I need all the help I can get.) drop me a line chet at oraclenerd dot com (does that really work? spelling out the email address I mean).

Wednesday, March 4, 2009

Poor Man's Data Vault

That's what I'm calling it anyway.

Part I.

Part II.

Part III.

I've started the project on Google Code
The goal is simple, have a simple, easy to use version of Oracle's Data Vault. By no means is this trying to replace their product, it's just a simple solution to helping lockdown your Oracle database.
I've read through a bit of Oracle's documentation on Data Vault and it seems like pretty cool stuff. I started this as something simple for our environment and was told it resembles (loosely) Data Vault by a friend.

So check it out if you want. Feedback, both good and bad, is welcome and appreciated. As the Generalissimo stated, I'm a big boy.

Thursday, February 19, 2009

No DDL in Production Environment - III

Part I.

Part II.

Alright, I've officially hit scope creep.

We've recently implemented VPD to further protect senstive data. We're using roles to manage the security as opposed to user level privileges. We as DBAs don't have (nor want) access to that sensitive data, but we're DBAs, we can do whatever we want right?

Anyway, by default we will NOT have these roles. They will only be enabled when needed. How do you manage that though? Auditing is already turned on so that's a good thing. But it would also be nice to know when someone granted one of these secure roles to a user or role. What better way than event based triggers?

I've come to appreciate triggers of late, at least the database or event level triggers. I still think application triggers are a means of last resort.

How does this tie in with No DDL in Production? Of course a GRANT statement is DDL but it's more of an access or security type. It's outside of the original intent of this "project." It would nice to be able to track it though. And shouldn't you be required to have a ticket prior to giving someone one of these secure roles?

So I've added 2 new tables, SECURE_ROLES and SECURE_GRANTS.

SECURE_ROLES
Stores the role name of...you guessed it, your secure roles.

    SECURE_GRANTS
    Tracks the history of GRANTs on those secure roles. If temporary, it would be easy enough to create a job to scan the table and send out alerts. Which just made me think of another attribute, amount of time needed. Perhaps to default at 1 hour or something.

    Here's the model so far:



    I've actually left off 3 other tables I just added. Serious scope creep. The good thing is I haven't written a lick of code yet. Let's call this the requirements/design phase.

    I think I need a better name for this project too. Any suggestions out there?

    Wednesday, February 18, 2009

    No DDL in Production Environment - II

    Part I.

    I said I was going to flesh that out a bit more. After some more thought about it, it could be a nice feature for DBAs.

    I'll start with what needs to be stored.

    DEPLOYMENTS

    • DEPLOYMENTID - Surrogate key
    • NAME - Name to give the deployment. Could be a ticket number or a change control number. Your choice.
    • DESCRIPTION - What are you doing? Maintenance, the description from the change control ticket, etc.
    • START_DATE - Defaults to SYSDATE. Used to gather the AFFECTED_OBJECTS at the end.
    • STARTED_BY - DEFAULTS to USER
    • END_DATE - Date the deployment ends.
    • ENDED_BY - Will typically be the same as the STARTED_BY, but we'll capture it at the end just in case.
    INVALID_OBJECTS and STAGE_CODES (not to be confused with stage coach).


    STAGE_CODE
    • STAGECODE - Nothing really special about this one here. Just a way to maintain data integrity without throwing it into a CHECK CONSTRAINT.
    INVALID_OBJECTS
    • INVALIDOBJECTID - Surrogate Key
    • DEPLOYMENTID - Foreign Key from the DEPLOYMENTS table.
    • STAGECODE -
    • OWNER - Self explanatory
    • OBJECT_NAME - Self explanatory
    And finally, AFFECTED_OBJECTS:


    AFFECTED_OBJECTS
    • AFFECTEDOBJECTID - Surrogate key.
    • DEPLOYMENTID - FK from DEPLOYMENTS
    • OWNER - Self explanatory
    • OBJECT_NAME - Self explanatory
    And here's how it all fits together:


    Any ideas on what else I can capture here?

    Monday, February 16, 2009

    No DDL in Production Environment

    There might be a couple of reasons you want to disable DDL in a production environment (other than the normal operations of the database). One being you don't have your schemas locked down (i.e. no service accounts). Another might be the need to track all changes in the environment for reporting/auditing purposes.

    It's fairly easy to do.

    First, create an Application Context:
    CREATE OR REPLACE
    CONTEXT secure_ddl
    USING secure_ddl
    ACCESSED GLOBALLY;
    The USING clause is the package/procedure/function that will set the secure_ddl context.

    Next, create a procedure that will set the context using DBMS_SESSION:
    CREATE OR REPLACE
    PROCEDURE secure_ddl( p_ticket IN VARCHAR2 )
    AS
    l_sessionid NUMBER := SYS_CONTEXT( 'USERENV', 'SESSIONID' );
    BEGIN
    dbms_session.set_identifier( l_sessionid );

    dbms_session.set_context
    ( namespace => 'SECURE_DDL_CONTEXT',
    attribute => 'CAN_DO_DDL',
    value => 'Y',
    username => SYS_CONTEXT( 'USERENV', 'SESSION_USER' ),
    client_id => l_sessionid );
    END secure_ddl;
    /
    Finally, create a trigger that fires BEFORE DDL.
    CREATE OR REPLACE
    TRIGGER enable_ddl
    BEFORE DDL ON DATABASE
    WHEN ( ora_dict_obj_owner = 'APPLICATION_OWNER' )
    BEGIN
    IF SYS_CONTEXT( 'SECURE_DDL_CONTEXT', 'CAN_DO_DDL' ) <> 'Y'
    OR SYS_CONTEXT( 'SECURE_DDL_CONTEXT', 'CAN_DO_DDL' ) IS NULL
    THEN
    raise_application_error( -20001, 'must enable DDL' );
    END IF;
    END enable_ddl;
    /
    So let's test it out.

    I'll create 2 users, the APPLICATION_OWNER (see trigger) and TESTUSER. APPLICATION_OWNER is the schema that will store all your application objects and code. TESTUSER will be granted the DBA role which will allow them access to create objects in the APPLICATION_OWNER schema.
    CREATE USER application_owner IDENTIFIED BY application_owner
    DEFAULT TABLESPACE users
    QUOTA 1G ON users;

    GRANT create session, resource TO application_owner;

    CREATE USER testuser IDENTIFIED BY testuser
    DEFAULT TABLESPACE users
    QUOTA 5M ON users;

    GRANT dba TO testuser;
    Let's test it out. Login as TESTUSER and create a table.
    TESTUSER@RMDEV2>set sqlprompt TESTUSER@11G>
    TESTUSER@11G>CREATE TABLE application_owner.t ( x NUMBER );
    CREATE TABLE application_owner.t ( x NUMBER )
    *
    ERROR at line 1:
    ORA-00604: error occurred at recursive SQL level 1
    ORA-20001: must enable DDL
    ORA-06512: at line 5
    Nice. A user with DBA privileges cannot perform DDL in the APPLICATION_OWNER schema. Since the SECURE_DDL procedure is not in the SYS schema, TESTUSER can call it to enable DDL.
    TESTUSER@11G>EXEC CJUSTICE.SECURE_DDL( 'TICKET 1' );

    PL/SQL procedure successfully completed.

    Elapsed: 00:00:00.01
    TESTUSER@11G>CREATE TABLE application_owner.t( x NUMBER );

    Table created.
    So what about APPLICATION_OWNER? Can that user perform DDL on objects in it's own schema (see #1 above)? I've logged into the APPLICATION_OWNER schema:
    APPLICATION_OWNER@11G>CREATE TABLE t ( x NUMBER );
    CREATE TABLE t ( x NUMBER )
    *
    ERROR at line 1:
    ORA-00604: error occurred at recursive SQL level 1
    ORA-20001: must enable DDL
    ORA-06512: at line 5
    Very cool.

    Again, this method could be helpful if:
    1. You don't have your schema (application) accounts locked down
    2. You want to track all DDL in your production environment.

    The procedure can be easily expanded into a series of calls, specifically ENABLE_DDL and DISABLE_DDL. When you call ENABLE_DDL, a record is written to a table with the ticket number and the start time. When you call DISABLE_DDL, the end time is captured and you can further query DBA_OBJECTS and capture all objects that changed during that window.

    In a future post, I'll work to expand on this concept to provide this kind of tracking.

    Monday, February 9, 2009

    Audit Failed Logon Attempts

    I've been looking at how to send out an alert to the DBA group on failed login attempts.

    In searching through the Database and Client events, the closest thing I could find initially was AFTER LOGON. That didn't make sense because of that little word AFTER. I then realized today that I could use SERVERERROR.

    The SERVERERROR event gives you access to the following Attribute Functions:
    ora_sysevent
    ora_login_user
    ora_instance_num
    ora_database_name
    ora_server_error
    ora_is_servererror
    space_error_info

    I created the trigger using ORA_LOGIN_USER but when I received the email, it would be blank.

    That makes sense, they're not logged in.

    What could I do?

    Google it.

    Well, nothing really there. Some people discussing the same problem, the inability to retrieve the username attempting to login.

    Then I went to Metalink and found Doc ID: 352389.1 (not sure how to link up now with the "new" site). The note talks about enabling auditing (done) and an init.ora parameter, but the username was still illusive.

    I don't know the internals, but Oracle needs to know the username or how else would it know is logging in?

    Anyway, I settled on the following for the time being.
    CREATE OR REPLACE
    TRIGGER failed_logon_notifications
    AFTER SERVERERROR ON DATABASE
    DECLARE
    b VARCHAR2(3) := UTL_TCP.CRLF;
    l_username VARCHAR2(30);
    l_failed_attempts INTEGER;
    l_subject VARCHAR2(40) := 'Alert - Failed Login';
    l_message VARCHAR2(500);
    BEGIN
    IF ora_is_servererror( 01017 ) THEN
    l_message := 'Date/Time: ' || TO_CHAR( SYSDATE, 'mm/dd/yyyy hh24:mi:ssss' ) || b;
    l_message := l_message || 'OS User: ' || SYS_CONTEXT( 'USERENV', 'OS_USER' ) || b;
    l_message := l_message || 'Host: ' || SYS_CONTEXT( 'USERENV', 'HOST' ) || b;
    l_message := l_message || 'Terminal: ' || SYS_CONTEXT( 'USERENV', 'TERMINAL' ) || b;
    l_message := l_message || 'IP Address: ' || SYS_CONTEXT( 'USERENV', 'IP_ADDRESS' ) || b;
    l_message := l_message || 'Protocol: ' || SYS_CONTEXT( 'USERENV', 'NETWORK_PROTOCOL' ) || b;
    l_message := l_message || 'Database Instance: ' || ora_instance_num || b;
    l_message := l_message || 'Database Name: ' || ora_database_name || b;

    BEGIN
    utl_mail.send
    ( sender => ora_database_name || '@revolutionmoney.com',
    recipients => 'dbas@email.com',
    subject => l_subject,
    message => l_message );
    EXCEPTION
    WHEN others THEN
    RAISE;
    END;
    END IF;
    END failed_logon_notifications;
    /
    Now I didn't really want this to fire on every single event, but I had problems with the WHEN clause.

    So if you know how to 1, capture the username or 2, apply the WHEN clause to only fire on ORA-01017, please comment.

    Monday, December 15, 2008

    DDL Triggers and Security

    I've mentioned in the past that I don't particularly care for trigger. That's a shocker, I know, because Tom Kyte loves them so much (here, here, and here).

    We're trying to move to a named account approach (for developers too) and the only way to do that is to grant system level privileges. That's not good.

    I had read about DDL triggers in the past but wasn't sure exactly how they could be used. Well, let's just say you can do quite a lot. There are database level events and client level events. The database level events include: STARTUP, SHUTDOWN, DB_ROLE_CHANT and SERVERERROR. A few of the client level events include (BEFORE and AFTER): DDL, LOGOFF, LOGON, GRANT, DROP, CREATE, ALTER, etc.

    Being a developer my entire career (with certain DBA skills), I had never quite wandered into this territory. I was rather impressed.

    The three I thought would make the most sense were ALTER, DROP and CREATE.

    If you have system level privileges my biggest concern would be CREATE ANY PROCEDURE and EXECUTE ANY PROCEDURE (though the latter is not one that is currently granted). I've used those 2 privileges to get the DBA role in the past.

    Here's what I need to do:
    1. Does the user have the DBA role?
    SELECT grantee
    INTO l_grantee
    FROM dba_role_privs
    WHERE grantee = ora_login_user
    AND granted_role = 'DBA';

    If they do have the role, it exits right then.
    If not,
    2. Where is the user trying to create/drop/alter an object? Is it in an approved schema? Their own?
    3. raise_application_error if it's not approved or not their own object

    Easy enough:
    CREATE OR REPLACE 
    TRIGGER system.no_create_in_dba_schema
    BEFORE CREATE
    ON DATABASE
    DECLARE
    l_user VARCHAR2(30) := ora_login_user;
    l_owner VARCHAR2(30) := ora_dict_obj_owner;
    l_object_name VARCHAR2(128) := ora_dict_obj_name;
    l_dba VARCHAR2(30);
    l_can_create BOOLEAN;
    BEGIN
    --is DBA?
    SELECT grantee
    INTO l_dba
    FROM dba_role_privs
    WHERE granted_role = 'DBA'
    AND grantee = l_user;

    EXCEPTION
    WHEN no_data_found THEN
    l_can_create := can_user_create_alter_drop
    ( p_user => l_user,
    p_owner => l_owner );

    IF l_can_create = FALSE THEN
    raise_application_error( -20001, 'cannot CREATE objects in ' || l_owner );
    END IF;
    END no_create_in_dba_schema;
    /
    show errors
    In preparation for this post I found two very similar articles by Arup Nanda and Tom Kyte. I think they've trained me well.

    Tuesday, November 4, 2008

    My Virtual [Column] Failure

    Well, I wouldn't really say a failure, but it's certainly been a trial.

    We're trying to create a new process that will detect fraud. Much of the data is sensitive in nature ('natch, thanks Jake).

    Step 1: Create a encrypted tablespace
    CREATE TABLESPACE encrypted_data
    DATAFILE 'c:\oracle\oradata\eleven\encrypted_data_001.dbf'
    ENCRYPTION USING '3DES168'
    DEFAULT STORAGE (ENCRYPT);

    Note, I did not do that as our production DBA won't let me near the physical machine (thankfully).

    OK, an encrypted tablespace. What else can we do to protect the data? Encrypt it using DBMS_CRYPTO!

    Step 2: Create the encryption/decryption routine. For added security, put it in the SYS schema. I decided on that because even if you have EXECUTE ANY PROCEDURE, you can't execute procedures owned by SYS (without being SYS of course). I'll skip the example as it's linked above.

    This has to be fast, really fast. How can I index?

    Well, I could index the RAW column, encrypt the incoming text/number and then compare...but we need the ability to do partial matches.

    Virtual Columns to the rescue! It's not that exciting really, but a good opportunity to put them to use.

    So here's the definition of the table:
    CREATE TABLE t
    (
    id NUMBER(10)
    CONSTRAINT pk_id PRIMARY KEY,
    name_e RAW(256),
    name AS ( CAST( sys.e_pkg.decrypt( name_e ) AS VARCHAR2(20) ) ) VIRTUAL,
    dob_e RAW(256),
    dob AS ( CAST( sys.e_pkg.decrypt( name_e ) AS VARCHAR2(11) ) ) VIRTUAL
    );

    Instead of doing the decryption in a view I can do it in the table and the best part of all is that you can index those virtual columns.
    CREATE INDEX name_idx ON t( name );

    CREATE INDEX ssn_idx ON t( ssn );

    I loaded some records into the table and began testing. Wall clock results weren't too bad, less than a second for searches (~1 million records) on SSN.

    This is where the problems began.

    The actual table has some 60 columns. Since I'm lazy, I created a procedure with an input parameter of T%ROWTYPE. You can use the CAST columns (VARCHAR2) to hold the incoming values, encrypt them and put them in their proper column.

    I initially had difficulties using the %ROWTYPE so I went back to named parameters (p_name, p_ssn, etc). What I didn't realize is that the length of the values were no longer constrained. Running some tests I managed to successfully insert some data. Much of it longer than the CAST would allow. When performing a SELECT on the table, Oracle gave me the wonderful
    ORA-06502: PL/SQL: numeric or value error: character string buffer too small
    .After a bit of searching, I found the offending records and tried to DELETE them:
    CJUSTICE@SANDBOX>DELETE FROM t WHERE id = 10;
    DELETE FROM t WHERE decisionid = 10
    *

    ERROR at line 1:
    ORA-06502: PL/SQL: numeric or value error: character string buffer too small
    ORA-06512: at "SYS.E_PKG", line 42

    What? I'm just trying to DELETE a row...not SELECT from it.

    So I leave this to the internals experts out there. Why does Oracle attempt to decrypt the column on a DELETE?