As a side-effect of Migrating to Claws I lost my OSX addressbook usage, at least for mail. In my company we use OpenERP for CRM, invoicing and other business needs. This means that the majority, if not all the email-addresses I need are in our OpenERP database.
So, it made sense to finish an effort I started earlier, which is to link OpenERP to our LDAP server and thus be able to query information from the OpenERP database through an LDAP interface and have every address available in the Claws addressbook (and the OSX addressbook too for that matter).
In an earlier version of OpenERP we used a specific module for this, which basically published an LDAP entry whenever we changed data in the OpenERP database. This worked, but was a less than ideal solution, not to mention it stopped working on an OpenERP upgrade. The solution I wanted was to have the data available in LDAP immediately. This meant making the LDAP server a “client” of the OpenERP database or, said another way, making the OpenERP database function as an SQL backend to the LDAP server.
Defining an SQL backend for LDAP
Creating an SQL backend for slapd is, albeit terse, documented. I mainly used the information at the OpenLDAP FAQ. The basic idea is that the LDAP-server connects through ODBC to the database, the OpenERP database in our case, and translates information found in relational tables to a subtree of the LDAP hierarchy.
To model this information, you have to create at least 3 tables in the database:
ldap_oc_mappings: which objectClass of LDAP is stored in what table;ldap_attr_mappings: how attributeTypes of an objectClass are resolved from RDBMS data;ldap_entries: what’s the DN of an entry, and how the entry relates to its objectClass mapping and to its parent DN;
The FAQ mentions two other tables, which we do not need. The idea is to specify in these three tables how the LDAP server gets to the entries and what they mean. For addressbook-like entries for mail, the defacto objectClass to use for this is something referred to an ‘inetOrgPerson’. The collection of these objects will be below an objectClass ‘organizationalUnit’, giving the simplest ‘tree-relation’we can think of.
Making the SQL backend use OpenERP
The table ldap_oc-mappings is queried by the LDAP server to map these object classes to tables, so the server knows in which tables to look for the attributes of these two classes. The next two statements insert two rows in that table, one for each object Class, mapping them to the tables ldap_inetOrgPerson and ldap_organizationalUnit, expecting a column id to contain the primary key for the objects.
INSERT INTO ldap_oc_mappings(name,keytbl,keycol)
VALUES('inetOrgPerson','ldap_inetOrgPerson','id');
INSERT INTO ldap_oc_mappings(name,keytbl,keycol)
VALUES('organizationalUnit','ldap_ organizationalUnit','id');
The ldap_inetOrgPerson is actually a view over the res_partner_address table in OpenERP, so it uses the data directly.
CREATE OR REPLACE VIEW ldap_inetorgperson AS
SELECT
a.id,
btrim((COALESCE(a.firstname,'')||' ') || a.lastname) AS cn,
btrim((COALESCE(a.firstname,'')||' ') || a.lastname) AS displayname,
a.phone AS telephonenumber,
a.lastname AS sn,
a.firstname AS givenname,
a.fax AS facsimiletelephonenumber,
a.mobile,
a.private_phone AS homephone,
lower(a.email) AS mail,
a.street,
a.zip AS postalcode
FROM res_partner_address a
WHERE
a.email '' AND
a.email '"' AND
a.type = 'contact';
This gives a dataset of all people who actually have an email-address registered in the OpenERP database. The column aliases are not needed as such, but make the construction of the ldap_attr_mappings table a bit easier. The second objectClass we registered in ldap_oc_mappings, organizationalUnit can be modelled with one simple row in the table ldap_organizationalUnit:
INSERT INTO ldap_organizationalUnit(name) VALUES ('addressbook');
With that row, we basically define one organizational unit in our simple tree named addressbook under which all our objects of type inetOrgPerson will be placed.
So, at this point we have 2 objectClasses registered, we have created the raw data for them. What’s left? Two things, first, we need to define how the attributes of the raw data relate to the object attributes. For this, the table ldap_attr_mappings contains a row for each attribute. For the telephoneNumber attribute, the data row is as follows:
INSERT INTO ldap_attr_mappings(
oc_map_id,name,sel_expr,from_tbls,join_where
)
VALUES(
1,'telephoneNumber','telephoneNumber','ldap_inetOrgPerson','1=1'
);
This says basically to the ldap server that in order to get to the telephoneNumber attribute for inetOrgPerson (oc_map_id 1 refers to the first row in the ldap_oc_mappings table), it needs to look in the table ldap_inetOrgPerson, use the same attribute name for the column and apply no special where clause. It is basically a recipe for the server to translate an LDAP request into an SQL query.
For each of the columns in the ldap_inetOrgPerson view, such a row needs to be present in the ldap_attr_mappings table.
Still here? The final step is to create the third meta table ldap_entries. This table is basically the lookup table to map ldap-index values to rdbms-index values. I have defined ldap_entries as a view on the raw data as follows:
CREATE OR REPLACE VIEW ldap_entries AS
SELECT
0 AS id,
'ou=addressbook,dc=hsdev,dc=com' AS dn,
2 AS oc_map_id,
0 AS parent,
0 AS keyval
UNION
SELECT
ldap_inetorgperson.id,
('cn=' || ldap_inetorgperson.cn) ||
',ou=addressbook,dc=hsdev,dc=com' AS dn,
1 AS oc_map_id,
0 AS parent,
ldap_inetorgperson.id AS keyval
FROM ldap_inetorgperson;
This does 2 things, it refers the ldap addres ou=addressbook,dc=hsdev,dc=com as the organizational unit (oc_map_id = 2) and assigns that ID 0. The second part of the UNION then formats each of the inetOrgPerson addresses as an LDAP address in the constructed tree (mine would be: cn=Marcel van der Boom,ou=addressbook,dc=hsdev,dc=com) and maps it to the ID in the raw data table (keyval) and puts the organizational unit as its parent.
With the above a minimal proof of concept can be constructed so that each partner address which has an email-address shows up in LDAP. After this, it’s a matter of configuring the email-client using the ou=addressbook,dc=hsdev,dc=com as a search base.
Here’s a picture of my record in the claws address book:
The same concept can be applied to other data in OpenERP (the partner records themselves come to mind or user accounts). It would not be that hard to wrap the above into an OpenERP module to manage this. Once the LDAP server has a configuration to use an SQL backend, all configuration can be done in OpenERP itself, modelling access using the meta tables. Perhaps I’ll do that at some point, if some of my customers would benefit from this too.

Hi Marcel,
This looks really good as I am looking to do the same functionality and publish a OpenERP partner list as an ldap database.
I followed the OpenLDAP FAQ and your instructions but got stuck somewhere.
I have created an SQL file which contains your instructions.
INSERT INTO ldap_oc_mappings(name,keytbl,keycol)
VALUES('inetOrgPerson','ldap_inetOrgPerson','id');
INSERT INTO ldap_oc_mappings(name,keytbl,keycol)
VALUES('organizationalUnit','ldap_ organizationalUnit','id');
CREATE OR REPLACE VIEW ldap_inetorgperson AS
SELECT
a.id,
btrim((COALESCE(a.firstname,'')||' ') || a.lastname) AS cn,
btrim((COALESCE(a.firstname,'')||' ') || a.lastname) AS displayname,
a.phone AS telephonenumber,
a.lastname AS sn,
a.firstname AS givenname,
a.fax AS facsimiletelephonenumber,
a.mobile,
a.private_phone AS homephone,
lower(a.email) AS mail,
a.street,
a.zip AS postalcode
FROM res_partner_address a
WHERE
a.email '' AND
a.email '"' AND
a.type = 'contact';
INSERT INTO ldap_organizationalUnit(name) VALUES ('addressbook');
INSERT INTO ldap_attr_mappings(
oc_map_id,name,sel_expr,from_tbls,join_where
)
VALUES(
1,'telephoneNumber','telephoneNumber',ldap_inetOrgPerson,'1=1'
);
CREATE OR REPLACE VIEW ldap_entries AS
SELECT
0 AS id,
'ou=addressbook,dc=xxxxx,dc=local' AS dn,
2 AS oc_map_id,
0 AS parent,
0 AS keyval
UNION
SELECT
ldap_inetorgperson.id,
('cn=' || ldap_inetorgperson.cn) ||
',ou=addressbook,dc=xxxxx,dc=local' AS dn,
1 AS oc_map_id,
0 AS parent,
ldap_inetorgperson.id AS keyval
FROM ldap_inetorgperson;
but it throws up an error;
ERROR: null value in column "expect_return" violates not-null constraint
ERROR: null value in column "expect_return" violates not-null constraint
ERROR: column a.firstname does not exist
LINE 4: btrim((COALESCE(a.firstname,'')||' ') || a.lastname) AS ...
^
ERROR: relation "ldap_organizationalunit" does not exist
ERROR: column "ldap_inetorgperson" does not exist
LINE 5: 1,'telephoneNumber','telephoneNumber',ldap_inetOrgPerson...
^
ERROR: relation "ldap_inetorgperson" does not exist
etechadmin@mail:~/ldap-openerp$ psql -h localhost -d fbw -U openerp < create-openerpdb.sql
Password for user openerp:
ERROR: null value in column "expect_return" violates not-null constraint
ERROR: null value in column "expect_return" violates not-null constraint
ERROR: column a.firstname does not exist
LINE 4: btrim((COALESCE(a.firstname,'')||' ') || a.lastname) AS ...
^
ERROR: relation "ldap_organizationalunit" does not exist
ERROR: column "ldap_inetorgperson" does not exist
LINE 5: 1,'telephoneNumber','telephoneNumber',ldap_inetOrgPerson...
^
From my basic understanding I don't see where ldap_organizationalunit is created? Did I miss a step?
~
The table ldap_organizational unit is indeed not mentioned, it’s a simple table which just holds the names of organization units (just one, in my case), here’s the definition of our OpenERP database:
CREATE TABLE ldap_organizationalunit
(
id serial NOT NULL,
"name" character(255),
CONSTRAINT "ldap_organizationalUnit_pkey" PRIMARY KEY (id)
)
The errors about expect_return are likely to be specific to your install, as they get generated by the definition of the view ‘ldap_inetorgperson’which references the standard OpenERP table ‘res_partner_address’. That table may have different constraints in your install depending on what OpenERP modules you have installed. By the looks of it, there is a column expect_return which wants to have a value.
Ok, I have made some changes. I realised I was trying to map columns that did not exist in my version of OpenERP so have modified the mapping slightly.
INSERT INTO ldap_oc_mappings(name,keytbl,keycol,expect_return) VALUES('inetOrgPerson','ldap_inetOrgPerson','id',0);
INSERT INTO ldap_oc_mappings(name,keytbl,keycol,expect_return) VALUES('organizationalUnit','ldap_organizationalUnit','id',0);CREATE OR REPLACE VIEW ldap_inetorgperson ASSELECT
a.id,
a.name AS cn,
a.name AS displayname,
a.phone AS telephonenumber,
a.name AS sn,
a.name AS givenname,
a.fax AS facsimiletelephonenumber,
a.mobile,
lower(a.email) AS mail,
a.street,
a.zip AS postalcode
FROM res_partner_address a
WHERE
a.email '' AND
a.email '"' AND
a.type = 'contact';
CREATE TABLE ldap_organizationalUnit
(
id serial NOT NULL,
"name" character(255),
CONSTRAINT "ldap_organizationalUnit_pkey" PRIMARY KEY (id)
)
INSERT INTO ldap_organizationalUnit(name) VALUES ('addressbook');
INSERT INTO ldap_attr_mappings(oc_map_id,name,sel_expr,from_tbls,join_where,expect_return) VALUES(1,'telephoneNumber','telephoneNumber',ldap_inetOrgPerson,'1=1',0);
CREATE OR REPLACE VIEW ldap_entries AS
SELECT
0 AS id,
'ou=addressbook,dc=fbwgroup,dc=local' AS dn,
2 AS oc_map_id,
0 AS parent,
0 AS keyval
UNION
SELECT
ldap_inetorgperson.id,
('cn=' || ldap_inetorgperson.cn) ||
',ou=addressbook,dc=fbwgroup,dc=local' AS dn,
1 AS oc_map_id,
0 AS parent,
ldap_inetorgperson.id AS keyval
FROM ldap_inetOrgPerson;
~
~
~
I now get the following errors;
etechadmin@mail:~/ldap-openerp$ psql -h localhost -d fbw -U openerp < create-openerpdb.sqlPassword for user openerp:
INSERT 0 1
INSERT 0 1
ERROR: schema "a" does not exist
ERROR: syntax error at or near "INSERT"
LINE 7: INSERT INTO ldap_organizationalUnit(name) VALUES ('addressbo...
^
ERROR: column "ldap_inetorgperson" does not exist
LINE 1: ...urn) VALUES(1,'telephoneNumber','telephoneNumber',ldap_inetO...
There are some typos in there. For one, after the CREATE TABLE statement for the ldap_organizationalUnit table there is a semicolon missing, leading to the syntax error.
Not sure about the error ‘schema a not found’you’ll have to dig around a bit to see what’s wrong there.
Should
INSERT INTO ldap_attr_mappings(oc_map_id,name,sel_expr,from_tbls,join_where,expect_return) VALUES(1,'telephoneNumber','telephoneNumber',ldap_inetOrgPerson,'1=1',0);read
INSERT INTO ldap_attr_mappings(oc_map_id,name,sel_expr,from_tbls,join_where,expect_return) VALUES(1,'telephoneNumber','telephoneNumber','ldap_inetOrgPerson','1=1',0); <---Missing quotes?I now have another missing not null column param ;-)
ERROR: null value in column "param_order" violates not-null constraint
Let me look it up and adjust
I think the main difference is I used the script inside openldap openldap-2.4.23/servers/slapd/back-sql/rdbms_depend/pgsql/backsql_create.sql which is slightly different from yours.
Yep, I have corrected it in the post as well.
it seems to be the
WHERE
a.email ” AND
a.email ‘”‘ AND
a.type = ‘contact’;
So I have subsituted it to. I take it this just filters out all the records without an email address and not type contact?
WHERE
a.email IS NOT NULL AND
a.type = ‘contact’;
Yep, I think I have changed that too since writing the blog post. For us, it didn’t make sense to include names of people for which we had no email-address in the database.
Hitting a new snag now. I install openldap with back-sql as per the instructions at http://www.darold.net/projects/ldap_pg/HOWTO/index.html
So now I am getting nativeErrCode=0 SQLengineState=IM002 msg=”[unixODBC][Driver Manager]Data source name not found, and no default driver specified”
When I run slad from the commandline.
any thoughts?
Sounds like you need to define an odbc datasource for the sql backend of slapd.
Woops typo spotted, now getting a bit further.
==>backsql_load_schema_map()
backsql_load_schema_map(): oc_query “SELECT id,name,keytbl,keycol,create_proc,delete_proc,expect_return FROM ldap_oc_mappings”
backsql_load_schema_map(): error executing oc_query:
Return code: -1
nativeErrCode=7 SQLengineState=S1000 msg=”ERROR: permission denied for relation ldap_oc_mappings;
Error while executing the query”
I was wondering if case sensitivity is at work;
the data inside ldap_oc_mappings is
“1″;”inetOrgPerson”;”ldap_inetOrgPerson”;”id”;”";”";0
“2″;”organizationalUnit”;”ldap_organizationalUnit”;”id”;”";”";0
but the view is ldap_inetorgperson?
Sussed it. Dropped and recreated all db’s with correct user.
Ok scrub that didn’t work. Slapd start but did not run, when I checked the db some of he views were missing.
Here is my latest combined sql file. I am trying to create a all in one file to make this simpler;
drop table ldap_oc_mappings cascade;
drop sequence ldap_oc_mappings_id_seq;
create table ldap_oc_mappings
(
id serial not null primary key,
name varchar(64) not null,
keytbl varchar(64) not null,
keycol varchar(64) not null,
create_proc varchar(255),
delete_proc varchar(255),
expect_return int not null
);
drop table ldap_attr_mappings;
drop sequence ldap_attr_mappings_id_seq;
create table ldap_attr_mappings
(
id serial not null primary key,
oc_map_id integer not null references ldap_oc_mappings(id),
name varchar(255) not null,
sel_expr varchar(255) not null,
sel_expr_u varchar(255),
from_tbls varchar(255) not null,
join_where varchar(255),
add_proc varchar(255),
delete_proc varchar(255),
param_order int not null,
expect_return int not null
);
drop table ldap_entries;
drop sequence ldap_entries_id_seq;
create table ldap_entries
(
id serial not null primary key,
dn varchar(255) not null,
oc_map_id integer not null references ldap_oc_mappings(id),
parent int NOT NULL,
keyval int NOT NULL,
UNIQUE ( oc_map_id, keyval ),
UNIQUE ( dn )
);
drop table ldap_entry_objclasses;
create table ldap_entry_objclasses
(
entry_id integer not null references ldap_entries(id),
oc_name varchar(64)
);
INSERT INTO ldap_oc_mappings(name,keytbl,keycol,expect_return) VALUES(‘inetOrgPerson’,'ldap_inetOrgPerson’,'id’,0);
INSERT INTO ldap_oc_mappings(name,keytbl,keycol,expect_return) VALUES(‘organizationalUnit’,'ldap_organizationalUnit’,'id’,0);
/* DROP VIEW ldap_inetOrgPerson CASCADE; */
CREATE OR REPLACE VIEW ldap_inetorgperson AS
SELECT
a.id,
a.name as cn,
a.phone AS telephonenumber,
a.name AS sn,
a.name AS givenname,
a.fax AS facsimiletelephonenumber,
a.mobile,
a.phone AS homephone,
lower(a.email) AS mail,
a.street,
a.zip AS postalcode
FROM res_partner_address as a
WHERE
a.email IS NOT NULL AND
a.type = ‘contact’;
DROP TABLE ldap_organizationalUnit;
drop sequence ldap_organizationalUnit_id_seq;
CREATE TABLE ldap_organizationalUnit
(
id serial NOT NULL,
“name” character(255),
CONSTRAINT “ldap_organizationalUnit_pkey” PRIMARY KEY (id)
);
INSERT INTO ldap_organizationalUnit(name) VALUES (‘addressbook’);
INSERT INTO ldap_attr_mappings(oc_map_id,name,sel_expr,from_tbls,join_where,param_order,expect_return) VALUES(1,’telephoneNumber’,'telephoneNumber’,'ldap_inetOrgPerson’,’1=1′,3,0);
/* drop view ldap_entries;*/
CREATE OR REPLACE VIEW ldap_entries AS
SELECT
0 AS id,
‘ou=addressbook,dc=fbwgroup,dc=local’AS dn,
2 AS oc_map_id,
0 AS parent,
0 AS keyval
UNION
SELECT
ldap_inetorgperson.id,
(‘cn=’|| ldap_inetorgperson.cn) ||
‘,ou=addressbook,dc=fbwgroup,dc=local’AS dn,
1 AS oc_map_id,
0 AS parent,
ldap_inetorgperson.id AS keyval
FROM ldap_inetorgperson;
There seems to be a conflict between the table ldap_entries and the view ldap_entries?
Output when I run the sql script;
NOTICE: drop cascades to constraint ldap_attr_mappings_oc_map_id_fkey on table ldap_attr_mappings
DROP TABLE
ERROR: sequence “ldap_oc_mappings_id_seq” does not exist
NOTICE: CREATE TABLE will create implicit sequence “ldap_oc_mappings_id_seq” for serial column “ldap_oc_mappings.id”
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index “ldap_oc_mappings_pkey” for table “ldap_oc_mappings”
CREATE TABLE
DROP TABLE
ERROR: sequence “ldap_attr_mappings_id_seq” does not exist
NOTICE: CREATE TABLE will create implicit sequence “ldap_attr_mappings_id_seq” for serial column “ldap_attr_mappings.id”
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index “ldap_attr_mappings_pkey” for table “ldap_attr_mappings”
CREATE TABLE
NOTICE: drop cascades to constraint ldap_entry_objclasses_entry_id_fkey on table ldap_entry_objclasses
DROP TABLE
ERROR: sequence “ldap_entries_id_seq” does not exist
NOTICE: CREATE TABLE will create implicit sequence “ldap_entries_id_seq” for serial column “ldap_entries.id”
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index “ldap_entries_pkey” for table “ldap_entries”
NOTICE: CREATE TABLE / UNIQUE will create implicit index “ldap_entries_oc_map_id_key” for table “ldap_entries”
NOTICE: CREATE TABLE / UNIQUE will create implicit index “ldap_entries_dn_key” for table “ldap_entries”
CREATE TABLE
DROP TABLE
CREATE TABLE
INSERT 0 1
INSERT 0 1
CREATE VIEW
DROP TABLE
ERROR: sequence “ldap_organizationalunit_id_seq” does not exist
NOTICE: CREATE TABLE will create implicit sequence “ldap_organizationalunit_id_seq” for serial column “ldap_organizationalunit.id”
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index “ldap_organizationalUnit_pkey” for table “ldap_organizationalunit”
CREATE TABLE
INSERT 0 1
INSERT 0 1
ERROR: “ldap_entries” is not a view
ldap_entries is a *view* not a table (in my implementation) Perhaps reread the last few paragraphs in the post on this?