I've wasted a fair amount of time recently on shared IMAP folders.
Work has a company SOGo instance. This is backed by a LDAP directory running on OpenLDAP and a Dovecot IMAP server.
The SOGo web mail interfaces allows you to set sharing permissions on IMAP folders. It reads and sets these using the IMAP ACL extensions, activated in Dovecot by loading the necessary plugins:
protocol imap { mail_plugins = acl imap_acl }
This works with individual users, and with anyone
, but not with LDAP groups. I've submitted patches for SOGo which get it to do the right thing for groups, and which should appear in 1.3.8. In conjunction with Dovecot 2.0 (1.2 has problems with group permissions over IMAP), it sets up the right ACLs for groups.
The remaining problem is how to tell Dovecot about which LDAP groups the user belongs to. But first, a brief diversion on LDAP groups.
Under the normal schemas, LDAP knows about two types of group. There is groupOfNames
in core.schema
, which is what you might call LDAP's idea of a group. Then in nis.schema
there is a POSIX-type group, a posixGroup
. These are both structural objects. Any LDAP object must possess one and only one structural object. The upshot here is that you can't have a groupOfNames
which is also a posixGroup
.
We don't want every group to be a POSIX group. They need to have a gid
and I've had trouble with group names that don't consist of a single all-lowercase word. We have groups called All Staff and similar. LDAP directory maintenance tools usually have nice ways of dealing with groupOfNames
.
So we're using the RFC2307bis schema instead. This is exactly the same as nis.schema
but has posixGroup
as an auxiliary class. So you can add posixGroup
as an extra object type to a groupOfNames
and everyone is happy.
The next stage of the puzzle is when a user logs in to the Dovecot IMAP server. You want to determine which groups that user is a member of. A groupOfNames
being just that, a list of users, the only way to work out what groups a user is in is to scan every single group looking for that username.
OpenLDAP has an overlay memberOf
which we have configured and activated. This adds and removes as appropriate memberOf
attributes to user records. Each memberOf
attribute gives the LDAP identity of a group the user belongs to.
Dovecot currently has no way of dealing with multi-value LDAP attributes. But it does have a post-login script to allow you to set up an environment variable ACL_GROUPS
with a comma-separated list of groups that user belongs to.
So in Dovecot configuration I set up a post-login script:
service imap { executable = imap imap-postlogin } service imap-postlogin { # all post-login scripts are executed via script-login binary executable = script-login -d /etc/dovecot/acl_groups.py # the script process runs as the user specified here (v2.0.14+): user = $default_internal_user # this UNIX socket listener must use the same name as given to imap executable unix_listener imap-postlogin { } }
We currently have Maildir
s in the users home directory. script-login -d
runs the after login imap
process as the user. The script acl_groups.py
fishes out the group memberships from LDAP, sets up ACL_GROUPS
and chains to the rest of the IMAP session. Dovecot passes the location of the program to run for the rest of the session on the command line.
#!/usr/bin/env python # # A Dovecot post-login script for IMAP. This creates environment # ACL_GROUPS with a comma-separated list of the user's LDAP group # memberships and then execs the Dovecot IMAP handler. # import ldap, os, sys; ldapUrl = "ldap://localhost" bindAccount = "cn=access,dc=example,dc=com" bindPw = "xxxxxx" searchBase = "ou=People,dc=example,dc=com" searchFilter = "(&(objectClass=posixAccount)(uid={0}))" user = os.environ["USER"] groups = [] l = ldap.initialize(ldapUrl) l.bind(bindAccount, bindPw, ldap.AUTH_SIMPLE) res = l.search_s(searchBase, ldap.SCOPE_SUBTREE, searchFilter.format(os.environ["USER"]), ['memberOf']) for dn, entry in res: try: for g in entry['memberOf']: # Returns 'cn=All UK staff,ou=Groups,dc=example,dc=com' etc. # Fish out 'All UK staff' as group name. groups.append(g.split(',', 1)[0][3:]) except KeyError: pass # User in no groups. os.environ["ACL_GROUPS"] = ",".join(groups) try: os.environ["USERDB_KEYS"] += " acl_groups" except KeyError: os.environ["USERDB_KEYS"] = "acl_groups" os.execv(sys.argv[1], sys.argv[1:]) sys.exit(1) # In case above fails
The final stage is to make sure that the file system permissions allow the user to read from the shared mailbox. This is difficult with Maildir
directories under the user's home directory. Right now I've set the Maildir
directory group to mail
and given the group r-s
permission on the directory. But this has to be done by hand for each home directory created. I'm looking at moving all mail to under /var/mail
instead.