CFR21 Part11 compliance

eschaef2

Registered User.
Local time
Today, 09:38
Joined
Jul 25, 2002
Messages
12
Hi All,

I have been tasked with generating a "CFR21 Part 11" compliant database. In a nutshell, I must be able to provide an audit trail for any transaction that changes (creates, modifies, or deletes) any record in the database.

The audit trail must consist of which authorized user performed the said task, what was changed (From -> To), and when the task was performed.

Has anyone else ever tried to tame such a beast? We have seen presentations of 'systems' that are Part 11 compliant, but feel that using such an approach is akin to using a stick of dynamite to open a Styrofoam coffee cup.

I am looking for some guidance in how to structure procedures such that I can trace who did what and when...

Thanks for any advice, and I have attached a word document of the Code of Federal Regulations 21 Part 11 from the Food and Drug Administration for reference.

Best regards,
Eric Schaeffer
 

Attachments

Last edited:
OK, to start with, Access by itself will never be 100.000% compliant, at least in any version of the product I have seen, since you (as Admin) can get into a context where events and underlying code do not exist.

However, if you can prevent the users from seeing the database window, and if you use workgroup (or other equivalent) security logins, you can accomplish most of what you want through code.

In fact, you are going to do a lot of coding. I believe I understand the requirement. I'm in Dept. of Defense, USA, so I know a little about audit logs. Don't know if you use the same terms for FDA, but DoD calls audit logging part of their "C2" security requirements.

First, you need an audit log table. You will need, as a minimum, a [datetime] field, a [username] field, a [tablename] field, a [fieldname] field, a [from] field, and a [to] field. You might also want a [Comments] field. Here's a tip I learned when I did something like this: Don't make the primary key equal to the [datetime] field. In fact, it is not required to have a primary key in this audit table. You won't use it in a way that requires linking in that manner.

You can make an index on the date if you wish, but allow duplicates. Today's machines are fast enough that if you log two edits in a row, you will find it possible to have the same time in both events. The internal clock resolution isn't fine enough to prevent duplicates. If you really, REALLY worry about the security requirements and getting events in absolute event order, use a separate autonumber field.

For tablename & fieldname, you can either (a) use the actual names or (b) use the indexes from the tabledefs and tabledefs(n).fields collections. (Property: tabledefs("myTable").Index) But if you ever change these definitions, the indexes might be considered "unstable" in a data longevity sense. I would use the actual names. That's just me. You know better than I do how stable the names will be long-term.

When logging the From/To values, I would log the CStr of the values. If you ever had to restore from the audit, you would have to look up the field to get its data type, then do a CDate, CInt, CDbl, etc.

Audit tables tend to grow. Proper security considerations require that you set up a procedure to flush the audit table to an external file and empty the table. (To be self-consistent, make an audit event before you flush and another one after.) Remember that [Comment] field I said you might need? The audit flush operation is one case where it might help. Anyway, you do the flush because you don't want the audit table sitting in the same place as the thing you are auditing for any longer than required.

In your application, your dearest friend is going to be each form's BeforeUpdate event. Here, you can put code to scan the controls of the form to see what changed before the update goes through. You might be able to make some of this generic enough to write it in a general module, then just pass the name of the form. Or, you could cut/paste part of this. What I did was make the actual logging code a subroutine and cut/pasted the loop into each form. Then, I passed the control and the recordset into the logger. The logger can do CurrentUser and Now just as easily as the main. The linkage I used passed in a partly formatted string for the table info, and in my case, I did not need field level logging, so my case was easier than yours.

Basically, here is the skeleton of your routine. Not actual code, just concepts.


Open the audit table recordset in a way that allows appending.

Start a loop over all controls in Me.Controls. For each control, test its .ControlType property. If it is a type that has values, compare the .OldValue to the .Value property. If they are equal, forget it. (Types you can easily skip include but are not limited to lines, rectangles, labels, index-tabs, and the individual buttons of a radio-button group - but not the enclosing group, which DOES have an old and new value.) You might also make life easier by skipping fields that aren't enabled or that are locked. But that is your call. And if locked field Y can be changed by updating combo box X, then the "locked field" exception doesn't apply.

If the control could be updated, you can find the Control.Name, .ControlSource, .RowSource etc. If the source is an elementary field name from a table or query, you can find the user (CurrentUser), date&time (Now), the table/query name, field name, old value, and new value. (You could also put the name of the form in the [comments], particularly if the same field can be modified from more than one location. Do a .AddNew on the audit table's recordset, populate the fields, and update the RS, then continue your loop over all controls. When you hit the end of the loop, you are done. Now let the update happen.

(Of course, if there is a pre-update test that might block the operation, do that blocking test first.)

Now, the part that gets ugly. You can NEVER use update, append, make-table, or delete queries directly. ONLY select queries. All other operations, to be properly auditied, must be done through VBA if you want record-level auditing. This is why most folks don't try to implement this sort of thing completely in Access. Yes, you can do it. But at some point you would do better to find an ODBC link to a database manager that has auditing built in as an automatic thing. Unlike Access, that gives you all the tools you need but none of the pre-built infrastructure.

Good luck! I don't envy you the work you have ahead of you.
 
Last edited:
Hi Doc_Man,

Thank you for taking the time to outline a direction that I can get started in. I think I may have to ask for an ‘extension’ on my time to implement this automated database.

Can you make a recommendation regarding documentation standards for printing and / or presenting my progress on this development task? I am an Electrical Engineer who is self-taught in VBA, and have never really had to document a project of this scope and size before. My previous projects have been 'small' enough that a few pages in Word and a couple of screen shots mitigated all questions / concerns.

Thanks again for your advice, and I’ll update again soon
-Eric
 
Milestones I would lay out at a high level:

1. Design & create the audit table. Visit all other tables to identify fields and formats you will have to support. IMPORTANT: Define the types of log events you expect to see.

1.1 Event: Update of record, implies entries in From and To fields. Could have form name or other data in Comment. Will make one audit per changed field only.

1.2 Event: Delete of record, will have Form name in Comment. Might want to log each data-holder field in From fields with one audit per field.

1.3 Event: Add a record, will have Form name in Comment. Might want to log each data-holder field in To fields with one audit per field.

1.4 Event: Audit log flush, probably only have things in Comment.

1.5 Other events?

2. Build logging subroutine. Optional: Build testing subroutine, too. Another option - set up security scheme.

HINT: For security, think "GROUPS" for actual implementation. Define one group name for each level of access your users can have. Then assign object rights to each group. GIVE YOURSELF EXTRA TIME FOR THIS. It is tedious. A rule of thumb I use, which allows for potty breaks and such, is not less than 1 minute per object per group. If you have 100 reports and 5 groups, this means 500 minutes to get the security set up right for reports.

Then, once the groups are set up right, just assign each user to one of your groups. NEVER assign rights to an individual account.

Oh, by the way... to catch folks who refuse to use workgroups...

Build an account that is a member of the Admins group (note, ends with 's' because it is plural.) Then take away 'Admins' group from the Admin (no 's' on the end) account. Make "Admin" a 'User' group rather than the administrator account. If you REALLY want to tick them off, make another group besides User have minimum operational rights. Take away ALL rights from the User group. Treat group name User and account name Admins as pariahs. Don't even give them the time of day.

3. Build / modify each form used to get to data. One line-item per identified form (3.x, x=1,2,3,...) plus separate line-items for new forms you identify when you lose the ability to run queries that would evade your logging. The changes will include updating the Form_BeforeUpdate event routine to have calls to your logging and/or testing routines.

4. Build a switchboard form that links to all your public forms. This isn't as bad as it sounds. The command-button wizard can launch a form for you. If the average user only sees the switchboard, you can prevent them from getting into a context where auditing can't occur. Build one button per form identified in step 3. Whether you want to call these separate line items or not is your call, but if they are separate, they will be very short duration tasks.

Make sure you have a command button that you can use to bypass the switchboard, but have it check for who you are. Disable it if the user isn't you.

You can ALSO have a switchboard launch another switchboard. (A switchboard is, after all, only a form that launches things.) In the SECOND switchboard, you could launch your pre-defined reports. A sneaky thing about using switchboards is that you can sneak some code in the commandbutton_Click routine to make an audit entry for that username and time, showing that "User SmithJT opened report rptShowMeTheWorld" at thus-and-such time and date.

Here is another sneakiness. If the opening switchboard is defined as your STARTUP form and if it contains a QUIT button, you can also audit-log when a person first opens the switchboard (startupform_Load) and when they leave (startupform_Close).

5. Block off database window. This involves setting database options, most of which are in the Tools>>Options>>General path.

6. Testing phase - allow yourself some specific milestones. Perhaps "Test switchboard in user context", "Test swbd in developer context", test access to each command button, etc.

7. First Deployment (possibly Beta test) - same sorts of events as item 6.

8. Production - might include a final replication, moving files to right directory, setting up WorkGroup


NOTE: Before you do this, there is the consideration of making a Design Master as the version you "muck" with. See the topics under Help for Design Master and for Database Replication. It would add some line-items to your work list.
 
Last edited:
If this option is available to you, the simplest solution is to migrate your tables from Jet (Access' RDBMS) to something more mainstream such as Oracle or DB2. The "Big" RDBMS' all support the transaction logging that you need. The logging is done at the database level by the database engine. No application code is required.
 
One small suggestion (which may have already been stated by The_Doc_Man),

You could create a field in a record row called ModDate and use this

Private Sub Form_BeforeUpdate(Cancel As Integer)

Me.txtModDate = Now()

End Sub

to do this...


I have been tasked with generating a "CFR21 Part 11" compliant database. In a nutshell, I must be able to provide an audit trail for any transaction that changes (creates, modifies, or deletes) any record in the database.

I think there are some posts regarded how to tell who is looged in at the time and use a similiar approach as the ModDate for a field called ModUsed (User who did modification?).

Or am I missing the point?

Hopefully I haven't pointed you in a more difficult direction! Just trying to help!
 
Last edited:

Users who are viewing this thread

Back
Top Bottom