Credential Management In DBIX::Class


Most companies have a method of storing passwords and configuration details in a configuration file. For programmers working on on DBIx::Class, this usually means writing your own functions to load a configuration file and supplying them to ->connect(). If you're working on something like a web framework, chances are you're writing something similar like Dancer's Plugin::DBIC or Catalyst's Model::DBIC::Schema.

DBIx::Class::Schema::Config can be added to any DBIC Schema to allow it to connect to a database with pre-configured credentials in any format that Config::Any recognizes, takes one additional line of code in a schema file, one change to your ->connect() statement, and takes less than a minute to configure after it's been installed.

That's cool, so how do I use it?

We'll start with three assumptions:

  1. You have a DBIC schema.
  2. You can connect to your DBIC schema.
  3. You have installed DBIx::Class::Schema::Config

I will use an example schema and example DBIx::Class for this demonstration. DBIx::Class::Schema::Loader was used to create the schema based on the following SQL in PostgreSQL.

schema.sql:


CREATE TABLE data(
    key varchar PRIMARY KEY,
    val varchar not null
);

INSERT INTO data( key, val) VALUES( 'Init', 'It works!' );

My/Schema.pm:


package My::Schema;

# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE

use strict;
use warnings;

use base 'DBIx::Class::Schema';

__PACKAGE__->load_namespaces;


# Created by DBIx::Class::Schema::Loader v0.07010 @ 2011-10-02 19:07:53
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:9pRJATkJbKVsD7JMXxShyA


# You can replace this text with custom code or comments.
1;

Now we'll make a test file to show that we can connect and interact with the database.


#!/usr/bin/perl
use warnings;
use strict;
use Test::More;
use My::Schema;

sub run_tests {
    my ( $db ) = @_;

    ok $db->resultset('Data')->create( { key => "Hello", val => "World" }), 
        "Adding a row to the database seems to work.";

    is $db->resultset('Data')->find( { key => "Hello" } )->val, "World",
        "Looking up a row from the database seems to work.";

    ok $db->resultset('Data')->find( { key => "Hello" } )->delete,
        "Deleting a row from the database seems to work.";

    is $db->resultset('Data')->find( {key => "Hello"} ), undef,
        "It seems that a deleted row is actually deleted.";

    is $db->resultset('Data')->find( { key => "Init" } )->val, "It works!", 
        "It seems that the inital row from schema.sql exists.";
}

ok my $db = My::Schema->connect( 
    "dbi:Pg:database=dbic_test", # DSN Line
    "symkat",                    # Username
    'fP37e1Y^21!dV$$y'           # Password
);

run_tests( $db );

done_testing;

Running the test shows that everything seems to be working correctly.


$ prove -l
t/01_connect.t .. ok   
All tests successful.
Files=1, Tests=6,  0 wallclock secs ( 0.02 usr  0.02 sys +  0.15 cusr  0.02 csys =  0.21 CPU)
Result: PASS

Let's practice test driven development to make the changes. We'll modify our test to make a second DBIC instance, connecting to it as connections are made with DBIx::Class::Schema::Config. The following diff outlines the changes we've made to the test file.


--- 01_connect.t.old	2011-10-02 23:12:31.000000000 -0700
+++ 01_connect.t	2011-10-02 22:48:46.000000000 -0700
@@ -26,9 +26,12 @@
 ok my $db = My::Schema->connect( 
     "dbi:Pg:database=dbic_test", # DSN Line
     "symkat",                    # Username
     'fP37e1Y^21!dV$$y'           # Password
 );
 
+ok my $other_db = My::Schema->connect('MY_DATABASE');
+
 run_tests($db);
+run_tests($other_db);
 
 done_testing;

Running the test fails because MY_DATABASE is not a valid DSN.


$ prove -l
t/01_connect.t .. 1/? DBIx::Class::ResultSet::create(): Your storage class (DBIx::Class::Storage::DBI) does not set sql_limit_dialect and you have not supplied an explicit limit_dialect in your connection_info. DBIC will attempt to use the GenericSubQ dialect, which works on most databases but can be (and often is) painfully slow. Please file an RT ticket against 'DBIx::Class::Storage::DBI' . at t/01_connect.t line 10
DBIx::Class::ResultSet::create(): DBI Connection failed: Can't connect to data source 'MY_DATABASE' because I can't work out what driver to use (it doesn't seem to contain a 'dbi:driver:' prefix and the DBI_DRIVER env var is not set) at /home/symkat/perl5/lib/perl5/DBIx/Class/Storage/DBI.pm line 1248 at t/01_connect.t line 10
# Tests were run but no plan was declared and done_testing() was not seen.
t/01_connect.t .. Dubious, test returned 255 (wstat 65280, 0xff00)
All 7 subtests passed 

Test Summary Report
-------------------
t/01_connect.t (Wstat: 65280 Tests: 7 Failed: 0)
  Non-zero exit status: 255
  Parse errors: No plan found in TAP output
Files=1, Tests=7,  0 wallclock secs ( 0.02 usr  0.00 sys +  0.15 cusr  0.02 csys =  0.19 CPU)
Result: FAIL

So to make this work correctly, we'll update My/Schema.pm to use DBIx::Class::Schema::Config.


--- Schema.pm.old	2011-10-02 23:07:34.000000000 -0700
+++ Schema.pm	2011-10-02 23:10:16.000000000 -0700
@@ -14,7 +14,6 @@
 # Created by DBIx::Class::Schema::Loader v0.07010 @ 2011-10-02 19:07:53
 # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:9pRJATkJbKVsD7JMXxShyA
 
-
-# You can replace this text with custom code or comments.
+__PACKAGE__->load_components('Schema::Config');
 
 1;


Now we'll make a configuration file in the current directory called dbic.conf. We could use any file format that Config::Any recognizes, and we could put the file in /home/$USER/dbic.conf or /etc/dbic.conf as well and it would be used.



    dsn         "dbi:Pg:database=dbic_test"
    user        "symkat"
    password    "fP37e1Y^21!dV$$y"


With the change to My/Schema.pm and the addition of dbic.conf, the test file should now pass if we did everything correctly.


$ prove -l
t/01_connect.t .. ok    
All tests successful.
Files=1, Tests=12,  0 wallclock secs ( 0.02 usr  0.01 sys +  0.18 cusr  0.03 csys =  0.24 CPU)
Result: PASS

We'll notice that while we added a second DBIC instance that was made to work with DBIx::Class::Schema::Config, the initial DBIC instance still works as well. BIx::Class::Schema will always allow normal credentials to pass through unadulterated. This allows the component to be loaded when instances that are not configured to work with DBIx::Class::Schema::Config are in use.

We've created a new file, dbic.conf in the current directory, added a load_components statement to the DBIx::Class::Schema, and added a connect statement with the name of a hash in the configuration file. These changes took a matter of seconds to give us a central location to manage our database credentials.

I have a question!

First, check out the DBIx::Class::Schema::Config Documentation. Questions like "how do I change where it looks for configuration files?," and "can I make programatic changes to the credentials before they're loaded?," are answered.

If your question is not answered in the documentation, you might want to submit the question to the DBIx::Class Mailing List. The author is a member of the mailing list.


Contact Me