summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/openldap300
-rw-r--r--roles/common/files/etc/ldap/schema/fripost.ldif179
-rw-r--r--roles/common/files/var/lib/ldap/fripost/DB_CONFIG5
-rw-r--r--roles/common/handlers/main.yml3
-rw-r--r--roles/common/tasks/ldap.yml66
-rw-r--r--roles/common/tasks/main.yml6
-rw-r--r--roles/common/templates/etc/ldap/database.ldif.j2487
7 files changed, 1046 insertions, 0 deletions
diff --git a/lib/openldap b/lib/openldap
new file mode 100644
index 0000000..cbb58c4
--- /dev/null
+++ b/lib/openldap
@@ -0,0 +1,300 @@
+#!/usr/bin/python
+#
+# Manage OpenLDAP databases
+#
+# Copyright 2013 Guilhem Moulin <guilhem@fripost.org>
+#
+# Licensed under the GNU GPL version 3 or higher.
+#
+
+import ldap, ldap.sasl
+from ldap.filter import filter_format
+from ldap.dn import dn2str,explode_dn,str2dn
+from ldap.modlist import addModlist
+from ldif import LDIFParser
+from functools import partial
+import re
+
+# Dirty hack to check equality between the targetted LDIF and that
+# currently in the directory. The value of some configuration (olc*)
+# attributes is automatically indexed when added; for those we remove
+# the index before checking the equality between the two lists of
+# values.
+idxAttr_re = re.compile( '^\{\d+\}(.*)' )
+indexedAttributes = frozenset([
+ 'olcAttributeTypes',
+ 'olcObjectClasses',
+ 'olcAccess',
+])
+
+
+# Another hack. Configuration entries sometimes pollutes the DNs with
+# indices, thus it's not possible to directly use them as base.
+# Instead, we use their parent as a pase, and search for the *unique*
+# match with the same ObjectClass and the matching extra attributes.
+# ('%s' in the attribute value is replaced with the value of the source
+# entry.)
+indexedDN = {
+ 'olcSchemaConfig': [('cn', '{*}%s')],
+ 'olcHdbConfig': [('olcDbDirectory', '%s' )],
+}
+
+
+# Run the given callback on each DN seen. If its return value is not
+# None, update the changed variable.
+class LDIFCallback(LDIFParser):
+ def __init__(self, module, input, callback):
+ LDIFParser.__init__(self,input)
+ self.callback = callback
+ self.changed = False
+
+ def handle(self,dn,entry):
+ b = self.callback(dn,entry)
+ if b is not None:
+ self.changed |= b
+
+
+# Run slapcat(8) on the given suffix or DB number (suffix takes
+# precedence) with an optional filter. (This is useful for offline
+# searches, or one needs to bypass ACLs.) Returns an open pipe to the
+# subprocess.
+def slapcat(filter=None, suffix=None, idx=0):
+ cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'slapcat') ]
+
+ if filter is not None:
+ cmd.extend([ '-a', filter ])
+
+ if suffix is not None:
+ if type(suffix) is not str:
+ suffix = dn2str(suffix)
+ cmd.extend([ '-b', suffix ])
+ else:
+ cmd.append( '-n%d' % idx )
+
+ return subprocess.Popen( cmd, stdout=subprocess.PIPE
+ , stderr=open(os.devnull, 'wb') )
+
+
+# Start / stop / whatever a service.
+def service(name, state):
+ cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'service'), name, state ]
+ subprocess.check_call( cmd, stdout=open(os.devnull, 'wb')
+ , stderr=subprocess.STDOUT )
+
+
+# Check if the given dn is already present in the directory.
+# Returns None if doesn't exist, and give the dn,entry otherwise
+def flexibleSearch(module, l, dn, entry):
+ idxClasses = set(entry['objectClass']).intersection(indexedDN.keys())
+ if not idxClasses:
+ base = dn
+ scope = ldap.SCOPE_BASE
+ else:
+ # Search on the parent instead, and try to use a precise filter
+ dn = str2dn(dn)
+ h,t,_ = dn.pop(0)[0]
+ base = dn2str(dn)
+ scope = ldap.SCOPE_ONELEVEL
+ f = []
+ for c in idxClasses:
+ f.append ( filter_format('objectClass=%s', [c]) )
+ for a,v in indexedDN[c]:
+ if a == h:
+ v2 = t
+ elif a not in entry.keys() or len(entry[a]) > 1:
+ module.fail_json(msg="Multiple values found! This is a bug. Please report.")
+ else:
+ v2 = entry[a][0]
+ f.append ( filter_format(a+'='+v, [v2]) )
+ if len(f) == 1:
+ f = f[0]
+ else:
+ f = '(&(' + ')('.join(f) + '))'
+
+ r = l.search_s( base, scope, filterstr=f )
+ if len(r) > 1:
+ module.fail_json(msg="Multiple results found! This is a bug. Please report.")
+ elif r:
+ return r.pop()
+
+
+# Add or modify (only the attributes that differ from those in the
+# directory) the entry for that DN.
+# l must be an LDAPObject, and should provide an open connection to the
+# directory with disclose/search/write access.
+def processEntry(module, l, dn, entry):
+ changed = False
+ r = flexibleSearch( module, l, dn, entry )
+ if r is None:
+ changed = True
+ if module.check_mode:
+ module.exit_json(changed=changed, msg="add DN %s" % dn)
+ l.add_s( dn, addModlist(entry) )
+ else:
+ d,e = r
+ fst = str2dn(dn).pop(0)[0][0]
+ diff = []
+ for a,v in e.iteritems():
+ if a == fst:
+ # the first attribute of the DN is implicit
+ continue
+ if a not in entry.keys():
+ diff.append((ldap.MOD_DELETE, a, v))
+ continue
+ if a in indexedAttributes:
+ # remove indices
+ v = [ idxAttr_re.search(v1).group(1) for v1 in v ]
+ if v != entry[a]:
+ # TODO: finer grain: we should modify/add/delete
+ # based on couple (attr,value), not attr only.
+ # The difficulty is that we need to preserve the order
+ # if a in indexedAttributes. Maybe we should index
+ # entry[a] instead, and walk on both lists at once?
+ diff.append((ldap.MOD_REPLACE, a, entry[a]))
+
+ for a in set(entry).difference(e.keys()):
+ diff.append((ldap.MOD_ADD, a, entry[a]))
+
+ if diff:
+ changed = True
+ if module.check_mode:
+ module.exit_json(changed=changed, msg="mod DN %s" % dn)
+ l.modify_s( d, diff )
+ return changed
+
+
+# Load the given module.
+def loadModule(module, name):
+ changed = False
+ l = ldap.initialize( 'ldapi://' )
+ l.sasl_interactive_bind_s('', ldap.sasl.external())
+
+ f = filter_format( '(&(objectClass=olcModuleList)(olcModuleLoad=%s))', [name] )
+ r = l.search_s( 'cn=config', ldap.SCOPE_ONELEVEL, filterstr = f, attrlist = [] )
+
+ if not r:
+ changed = True
+ if module.check_mode:
+ module.exit_json(changed=changed, msg="add module %s" % name)
+ l.modify_s( 'cn=module{0},cn=config'
+ , [(ldap.MOD_ADD, 'olcModuleLoad', name)] )
+
+ l.unbind_s()
+ return changed
+
+
+# Clear the given DB directory and delete the associated database. Fail
+# if non empty, unless all existing DNS are in skipdns.
+def wontRemove(module, skipdns, d, _):
+ if d not in skipdns:
+ module.fail_json(msg="won't remove '%s'" % d)
+def removeDB(module, dbdir, skipdn=None):
+ changed = False
+ if not os.path.exists(dbdir):
+ return False
+
+ l = ldap.initialize( 'ldapi://' )
+ l.sasl_interactive_bind_s('', ldap.sasl.external())
+
+ f = filter_format( '(&(objectClass=olcDatabaseConfig)(olcDbDirectory=%s))', [dbdir] )
+ r = l.search_s( 'cn=config'
+ , ldap.SCOPE_ONELEVEL
+ , filterstr = f
+ , attrlist = ['olcSuffix'] )
+ l.unbind_s()
+
+ if len(r) > 1:
+ module.fail_json(msg="Multiple results found! This is a bug. Please report.")
+ elif r:
+ dn,entry = r.pop()
+ suffix = entry['olcSuffix'][0]
+
+ skipdns = [suffix]
+ if skipdn is not None:
+ skipdns.extend([ "%s,%s" % (s,suffix) for s in skipdn ])
+ # here we need to use slapcat not search_s, because we may
+ # not have read access on the database (even though we're
+ # root!).
+ p = slapcat( suffix=suffix )
+ parser = LDIFCallback( module, p.stdout
+ , partial(wontRemove,module,skipdns) )
+ parser.parse()
+
+ changed = True
+ if module.check_mode:
+ module.exit_json(changed=changed, msg="remove dir %s" % dbdir)
+
+ # slapd doesn't support database deletion, so we need to turn it
+ # off and remove it from slapd.d manually.
+ service( 'slapd', 'stop' )
+ path = [ os.sep, 'etc', 'ldap', 'slapd.d' ]
+ ldif = explode_dn(dn)[::-1]
+ ldif[-1] += ".ldif"
+ path.extend( ldif )
+ os.unlink( os.path.join(*path) )
+
+ # delete all children in path, but not the path directory itself.
+ for file in os.listdir(dbdir):
+ os.unlink( os.path.join(dbdir, file) )
+ service( 'slapd', 'start' )
+ return changed
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec = dict(
+ dbdirectory = dict( default=None ),
+ ignoredn = dict( default=None ),
+ state = dict(default="present", choices=["absent", "present"]),
+ target = dict( default=None ),
+ module = dict( default=None ),
+ ),
+ supports_check_mode=True
+ )
+
+ params = module.params
+ state = params['state']
+ dbdirectory = params['dbdirectory']
+ ignoredn = params['ignoredn']
+ target = params['target']
+ mod = params['module']
+
+ if ignoredn is not None:
+ ignoredn = ignoredn.split(':')
+
+ changed = False
+ try:
+ if state == "absent":
+ if dbdirectory is not None:
+ changed = removeDB(module,dbdirectory,skipdn=ignoredn)
+ # TODO: might be useful to be able remove DNs
+ else:
+ module.fail_json(msg="missing dbdirectory")
+
+ elif state == "present":
+ if target is not None:
+ # bind only once per LDIF file to
+ l = ldap.initialize( 'ldapi://' )
+ l.sasl_interactive_bind_s('', ldap.sasl.external())
+
+ parser = LDIFCallback( module, open(target, 'r')
+ , partial(processEntry,module,l) )
+ parser.parse()
+ l.unbind_s()
+ changed = parser.changed
+ elif mod is not None:
+ changed = loadModule(module, mod)
+ else:
+ module.fail_json(msg="missing target or module")
+
+ except subprocess.CalledProcessError, e:
+ module.fail_json(rv=e.returncode, msg=e.output.rstrip())
+ except ldap.LDAPError, e:
+ module.fail_json(msg=e.args[0]['info'])
+
+ module.exit_json(changed=changed)
+
+
+# this is magic, see lib/ansible/module_common.py
+#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
+main()
diff --git a/roles/common/files/etc/ldap/schema/fripost.ldif b/roles/common/files/etc/ldap/schema/fripost.ldif
new file mode 100644
index 0000000..6ec55dc
--- /dev/null
+++ b/roles/common/files/etc/ldap/schema/fripost.ldif
@@ -0,0 +1,179 @@
+# Fripost's LDAP schema
+# Copyright 2013 Guilhem Moulin <guilhem@fripost.org>
+#
+# Licensed under the GNU GPL version 3 or higher.
+
+# Load this file with
+#
+# ldapadd -Y EXTERNAL -H ldapi:/// -f fripost.ldif
+#
+# It will load the schema. To perform modifications, the easiest way is to
+#
+# * Save the database: slapcat -b 'o=mailHosting,dc=fripost,dc=dev' > /tmp/db.ldif
+# * Save the configuration: slapcat -n0 > /tmp/config.ldif
+# * Backup slap.d: cp -a /etc/ldap/slapd.d/ /tmp/slap.d_back
+# * Edit the schema in /tmp/config.ldif
+# * Load the new config: mkdir -m 0700 /tmp/slapd.d_new && slapadd -F /tmp/slapd.d_new -n0 -l /tmp/config.ldif
+# * Stop slapd: /etc/init.d/slapd stop
+# * Load the new config: rm -rf /etc/ldap/slapd.d/ && mv /tmp/slapd.d_new /etc/ldap/slapd.d && chown -R openldap:openldap /etc/ldap/slapd.d
+# * Create indexes: sudo -u openldap slapindex -b 'o=mailHosting,dc=fripost,dc=dev'
+# * Start slapd: /etc/init.d/slapd start
+# If it fails, remove the existing database and see what's wrong
+# rm -rf /var/lib/ldap/dev/* && sudo -u openldap slapadd -b 'o=mailHosting,dc=fripost,dc=org' -l /tmp/db.ldif
+#
+#
+# /!\ WARN: All modification to the ACL should be reflected to the test
+# /!\ suite as well!
+#
+#
+# References:
+# - http://courier.svn.sourceforge.net/svnroot/courier/trunk/courier-authlib/authldap.schema
+# - http://www.qmail-ldap.org/wiki/index.php/Qmail.schema
+# - http://www.wanderingbarque.com/howtos/mailserver/mailserver.html
+
+
+# 1.3.6.1.4.1.40011 Fripost's OID
+# 1.3.6.1.4.1.40011.1
+# 1.3.6.1.4.1.40011.1.2 fripost LDAP Elements
+# 1.3.6.1.4.1.40011.1.2.1 AttributeTypes
+# 1.3.6.1.4.1.40011.1.2.2 ObjectClasses
+# 1.3.6.1.4.1.40011.1.2.3 Syntax Definitions
+
+# This schema depends on:
+# - core.schema
+# - cosine.schema
+# - nis.schema
+
+
+dn: cn=fripost-master,cn=schema,cn=config
+objectClass: olcSchemaConfig
+#
+# Attributes: 1.3.6.1.4.1.40011.1.1
+#
+olcAttributeTypes: ( 1.3.6.1.4.1.40011.1.2.1.1 NAME 'fvd'
+ DESC 'A virtual mail domain'
+ EQUALITY caseIgnoreIA5Match
+ SUBSTR caseIgnoreIA5SubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} SINGLE-VALUE )
+#
+olcAttributeTypes: ( 1.3.6.1.4.1.40011.1.2.1.2 NAME 'fvl'
+ DESC 'The local part of a virtual user, alias, list or list command'
+ EQUALITY caseIgnoreIA5Match
+ SUBSTR caseIgnoreIA5SubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{64} SINGLE-VALUE )
+#
+# This is redundant since we always use DNs of the form
+# fvl=localpart,fvd=domainpart.tld,...
+# (But Postfix doesn't allow the use of '%u' and '%d' from the query in
+# its 'result_format'.)
+# It is a priori insecure to allow arbitrary values here since users
+# will modify this value themselves, however our Postfix will only
+# accept well-formed values, enforced by a custom filter:
+# query_filter = (&...(fripostLocalAlias=%u#%d))
+# result_attribute = fripostLocalAlias
+olcAttributeTypes: ( 1.3.6.1.4.1.40011.1.2.1.3 NAME 'fripostLocalAlias'
+ DESC 'A local alias, typically localpart#domainpart.tld'
+ EQUALITY caseIgnoreIA5Match
+ SUBSTR caseIgnoreIA5SubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{64} SINGLE-VALUE )
+#
+olcAttributeTypes: ( 1.3.6.1.4.1.40011.1.2.1.4 NAME 'fripostMaildrop'
+ DESC 'An email address the virtual alias should be mapped to'
+ EQUALITY caseIgnoreIA5Match
+ SUBSTR caseIgnoreIA5SubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
+#
+# We are creating a new attribute, optional in virtual domains and
+# users, because the presence index should *not* apply to the
+# mandatory attribute above.
+olcAttributeTypes: ( 1.3.6.1.4.1.40011.1.2.1.5 NAME 'fripostOptionalMaildrop'
+ DESC 'An optional email address for catch-all aliases on domains and users'
+ EQUALITY caseIgnoreIA5Match
+ SUBSTR caseIgnoreIA5SubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
+#
+olcAttributeTypes: ( 1.3.6.1.4.1.40011.1.2.1.6 NAME 'fripostIsStatusActive'
+ DESC 'When present, a token locking the entry in an inactive state'
+ EQUALITY booleanMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
+#
+olcAttributeTypes: ( 1.3.6.1.4.1.40011.1.2.1.7 NAME 'fripostPendingToken'
+ DESC 'Is the entry pending?'
+ EQUALITY caseExactMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{64} SINGLE-VALUE )
+#
+olcAttributeTypes: ( 1.3.6.1.4.1.40011.1.2.1.8 NAME 'fripostUserQuota'
+ DESC 'The quota on a user e.g., "50MB"'
+ EQUALITY caseExactMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32} SINGLE-VALUE )
+#
+olcAttributeTypes: ( 1.3.6.1.4.1.40011.1.2.1.9 NAME 'fripostCanAddDomain'
+ DESC 'A user/domain that can add domains'
+ SUP distinguishedName )
+#
+olcAttributeTypes: ( 1.3.6.1.4.1.40011.1.2.1.10 NAME 'fripostCanAddAlias'
+ DESC 'A user/domain that can add aliases under the parent domain'
+ SUP distinguishedName )
+#
+olcAttributeTypes: ( 1.3.6.1.4.1.40011.1.2.1.11 NAME 'fripostCanAddList'
+ DESC 'A user/domain that can add lists under the parent domain'
+ SUP distinguishedName )
+#
+olcAttributeTypes: ( 1.3.6.1.4.1.40011.1.2.1.12 NAME 'fripostOwner'
+ DESC 'A user that owns under parent domain'
+ SUP distinguishedName )
+#
+olcAttributeTypes: ( 1.3.6.1.4.1.40011.1.2.1.13 NAME 'fripostPostmaster'
+ DESC 'A user that is a postmaster of the parent domain'
+ SUP distinguishedName )
+#
+olcAttributeTypes: ( 1.3.6.1.4.1.40011.1.2.1.14 NAME 'fripostListManager'
+ DESC 'The list manager'
+ EQUALITY caseIgnoreMatch
+ SUBSTR caseIgnoreSubstringsMatch
+ SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{64} SINGLE-VALUE )
+#
+#
+# Objects: 1.3.6.1.4.1.40011.1.2
+#
+olcObjectClasses: ( 1.3.6.1.4.1.40011.1.2.1 NAME 'FripostVirtual'
+ AUXILIARY
+ DESC 'Virtual mail hosting'
+ MAY ( fripostCanAddDomain ) )
+#
+olcObjectClasses: ( 1.3.6.1.4.1.40011.1.2.2 NAME 'FripostVirtualDomain'
+ SUP top STRUCTURAL
+ DESC 'Virtual domain'
+ MUST ( fvd $ fripostIsStatusActive )
+ MAY ( fripostCanAddAlias $ fripostCanAddList $
+ fripostOwner $ fripostPostmaster $
+ fripostOptionalMaildrop $ description ) )
+#
+# | TODO: add limits here
+olcObjectClasses: ( 1.3.6.1.4.1.40011.1.2.3 NAME 'FripostVirtualUser'
+ SUP top STRUCTURAL
+ DESC 'Virtual user'
+ MUST ( fvl $ userPassword $ fripostIsStatusActive )
+ MAY ( fripostUserQuota $ fripostOptionalMaildrop $ description) )
+#
+olcObjectClasses: ( 1.3.6.1.4.1.40011.1.2.4 NAME 'FripostVirtualAlias'
+ SUP top STRUCTURAL
+ DESC 'Virtual alias'
+ MUST ( fvl $ fripostMaildrop $ fripostIsStatusActive )
+ MAY ( fripostOwner $ description ) )
+#
+olcObjectClasses: ( 1.3.6.1.4.1.40011.1.2.5 NAME 'FripostVirtualList'
+ SUP top STRUCTURAL
+ DESC 'Virtual list'
+ MUST ( fvl $ fripostListManager $ fripostIsStatusActive $ fripostLocalAlias )
+ MAY ( fripostOwner $ description ) )
+#
+olcObjectClasses: ( 1.3.6.1.4.1.40011.1.2.6 NAME 'FripostVirtualListCommand'
+ SUP top STRUCTURAL
+ DESC 'Virtual list command'
+ MUST ( fvl $ fripostLocalAlias ) )
+#
+olcObjectClasses: ( 1.3.6.1.4.1.40011.1.2.7 NAME 'FripostPendingEntry'
+ SUP top AUXILIARY
+ DESC 'Virtual pending entry'
+ MAY ( fripostPendingToken ) )
diff --git a/roles/common/files/var/lib/ldap/fripost/DB_CONFIG b/roles/common/files/var/lib/ldap/fripost/DB_CONFIG
new file mode 100644
index 0000000..0bd4e58
--- /dev/null
+++ b/roles/common/files/var/lib/ldap/fripost/DB_CONFIG
@@ -0,0 +1,5 @@
+set_cachesize 0 5242880 1
+# 5MB cachesize, allow defragmentation
+set_lk_max_objects 1500
+set_lk_max_locks 1500
+set_lk_max_lockers 1500
diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml
index 54643ed..51b19f7 100644
--- a/roles/common/handlers/main.yml
+++ b/roles/common/handlers/main.yml
@@ -35,3 +35,6 @@
- name: Reload Postfix
service: name=postfix state=reloaded
+
+- name: Restart slapd
+ service: name=slapd state=restarted
diff --git a/roles/common/tasks/ldap.yml b/roles/common/tasks/ldap.yml
new file mode 100644
index 0000000..b1ced49
--- /dev/null
+++ b/roles/common/tasks/ldap.yml
@@ -0,0 +1,66 @@
+- name: Install OpenLDAP
+ apt: pkg={{ item }}
+ with_items:
+ - slapd
+ - ldap-utils
+ - ldapvi
+ - db-util
+ - python-ldap
+
+# Upon install slapd create and populate a database under /var/lib/ldap.
+# We clear it up and create a children directory to get finer-grain
+# control.
+- name: Clear empty /var/lib/ldap
+ # Don't remove the database (and fail) if it contains something else
+ # than its suffix or cn=admin,...
+ openldap: dbdirectory=/var/lib/ldap ignoredn=cn=admin
+ state=absent
+
+- name: Create directory /var/lib/ldap/fripost
+ file: path=/var/lib/ldap/fripost
+ owner=openldap group=openldap
+ state=directory
+ mode=0700
+
+- name: Copy /var/lib/ldap/fripost/DB_CONFIG
+ copy: src=var/lib/ldap/fripost/DB_CONFIG
+ dest=/var/lib/ldap/fripost/DB_CONFIG
+ owner=openldap group=openldap
+ mode=0600
+ notify:
+ # Not sure if required
+ - Restart slapd
+
+- name: Create directory /etc/ldap/fripost
+ file: path=/etc/ldap/fripost
+ owner=root group=root
+ state=directory
+ mode=0755
+
+- name: Copy fripost database definition
+ template: src=etc/ldap/database.ldif.j2
+ dest=/etc/ldap/fripost/database.ldif
+ owner=root group=root
+ mode=0600
+
+- name: Copy fripost schema
+ copy: src=etc/ldap/schema/fripost.ldif
+ dest=/etc/ldap/schema/fripost.ldif
+ owner=root group=root
+ mode=0644
+
+- name: Create fripost database and load the schema
+ openldap: target=/etc/ldap/{{ item }} state=present
+ with_items:
+ - fripost/database.ldif
+ - schema/fripost.ldif
+
+- name: Load LDAP modules
+ openldap: module={{ item }}.la state=present
+ with_items:
+ # TODO only if provider
+ - syncprov
+ # TODO only if writable
+ - constraint
+
+# TODO: authz constraint syncprov syncrepl
diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml
index 81ef705..ed84cb5 100644
--- a/roles/common/tasks/main.yml
+++ b/roles/common/tasks/main.yml
@@ -15,3 +15,9 @@
when: "'MDA' in group_names or
'webmail' in group_names or
'backup' in group_names"
+- include: ldap.yml tags=slapd,ldap
+ when: "'MDA' in group_names or
+ 'MSA' in group_names or
+ 'lists' in group_names or
+ 'LDAP-producer' in group_names or
+ 'MX' in group_names"
diff --git a/roles/common/templates/etc/ldap/database.ldif.j2 b/roles/common/templates/etc/ldap/database.ldif.j2
new file mode 100644
index 0000000..fc4ce4b
--- /dev/null
+++ b/roles/common/templates/etc/ldap/database.ldif.j2
@@ -0,0 +1,487 @@
+# Fripost's LDAP database definition
+# Copyright 2013 Guilhem Moulin <guilhem@fripost.org>
+#
+# Licensed under the GNU GPL version 3 or higher.
+
+dn: olcDatabase=hdb,cn=config
+objectClass: olcDatabaseConfig
+objectClass: olcHdbConfig
+olcDbDirectory: /var/lib/ldap/fripost
+olcSuffix: o=mailHosting,dc=fripost,dc=org
+olcLastMod: TRUE
+olcDbCheckpoint: 512 15
+# Require LDAPv3 protocol and authentication prior to directory
+# operations.
+olcRequires: LDAPv3 authc
+# We don't want to give "canAdd{Alias,List}" write access to alias/list
+# attributes.
+olcAddContentAcl: FALSE
+# The root user has all rights on the whole database (when SASL-binding
+# on a UNIX socket).
+olcRootDN: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
+#
+#
+########################################################################
+########################################################################
+# Performance considerations
+#
+# To reindex an existing database, you have to
+# * Stop slapd sudo service slapd stop
+# * Reindex su openldap -c "slapindex -b 'o=mailHosting,dc=fripost,dc=org'"
+# * Restart slapd sudo service slapd start
+#
+# References
+# - https://wiki.zimbra.com/wiki/OpenLDAP_Performance_Tuning_5.0
+# - http://www.openldap.org/doc/admin24/tuning.html
+# - http://www.openldap.org/faq/data/cache/42.html
+# - http://www.openldap.org/faq/data/cache/136.html
+# - http://www.zytrax.com/books/ldap/apa/indeces.html
+#
+olcDbIndex: objectClass eq
+# Let us make Postfix's life easier. TODO: only if MX, lists.f.o, MDA, etc.
+olcDbIndex: fripostIsStatusActive,fvd,fvl,fripostLocalAlias eq
+olcDbIndex: fripostOptionalMaildrop pres
+# SyncProv/SyncRepl specific indexing. TODO: only if SyncProv/SyncRepl
+olcDbIndex: entryCSN,entryUUID eq
+#
+#
+#
+# 1. On single- and dual-core systems, change the maximum number of
+# threads to 8. (The default, 16, is fine for 4- and 8-core systems.)
+#
+# dn: cn=config
+# changetype: modify
+# add: olcThreads
+# olcThreads: 8
+#
+#
+# 2. It may be a good idea to modify DB_CONFIG, depending on the output
+# of
+#
+# db_stat -mh /var/lib/ldap/fripost | head -16
+#
+# (For optimal performance, the Requested pages found in the cache
+# should be above 95%, and the dirty/clean pages forced from the cache
+# should be 0.)
+#
+# and
+#
+# db_stat -ch /var/lib/ldap/fripost | head -16
+#
+# (For optimal performance, usage should be within 85% of the configured
+# values.)
+#
+#
+########################################################################
+########################################################################
+# Access control
+# /!\ WARN: All modification to the ACL should be reflected to the test
+# /!\ suite as well!
+#
+# References:
+# - http://www.openldap.org/doc/admin24/access-control.html
+# - http://www.openldap.org/faq/data/cache/189.html
+# - http://www.openldap.org/faq/data/cache/1140.html
+# - http://www.openldap.org/faq/data/cache/1133.html
+# - man 5 slapd.access
+#
+#
+########################################################################
+# Most common services: Postfix, Amavis, SASLauth, Dovecot
+# (Most used ACLs are cheaper when written first.)
+#
+# Postfix have read access to the attribute they need.
+olcAccess: to dn.children="ou=virtual,o=mailHosting,dc=fripost,dc=org"
+ attrs=entry,objectClass,fvd,fvl,fripostMaildrop,fripostOptionalMaildrop,fripostLocalAlias
+ filter=(&(|(objectClass=FripostVirtualDomain)(objectClass=FripostVirtualUser)(objectClass=FripostVirtualAlias)(objectClass=FripostVirtualList)(objectClass=FripostVirtualListCommand))(!(objectClass=FripostPendingEntry))(!(fripostIsStatusActive=FALSE)))
+ by dn.exact="cn=Postfix,ou=services,o=mailHosting,dc=fripost,dc=org" =rsd
+ by users =0 break
+# Search lists and domain owners
+olcAccess: to dn.exact="ou=virtual,o=mailHosting,dc=fripost,dc=org"
+ attrs=entry
+ by dn.exact="cn=Postfix,ou=services,o=mailHosting,dc=fripost,dc=org" =s
+ by dn.exact="gidNumber=8+uidNumber=8,cn=peercred,cn=external,cn=auth" =s
+ by users =0 break
+#
+# Search domain owners / postmasters
+olcAccess: to dn.children="ou=virtual,o=mailHosting,dc=fripost,dc=org"
+ attrs=entry,objectClass,fvd,fvl,fripostPostmaster,fripostOwner
+ filter=(&(objectClass=FripostVirtualDomain)(!(objectClass=FripostPendingEntry))(!(fripostIsStatusActive=FALSE)))
+ by dn.exact="gidNumber=8+uidNumber=8,cn=peercred,cn=external,cn=auth" =rsd
+ by users =0 break
+#
+# Anonymous can authenticate into the services. (But not read or write the password.)
+olcAccess: to dn.one="ou=services,o=mailHosting,dc=fripost,dc=org"
+ attrs=userPassword
+ by realanonymous =xd
+#
+# That's necessary for SASL proxy Authorize the web application.
+olcAccess: to dn.exact="cn=AdminWebPanel,ou=services,o=mailHosting,dc=fripost,dc=org"
+ attrs=entry,objectClass,authzTo
+ by realanonymous =x
+#
+# 1. The WebPanel itself cannot bind, read or write passwords. This
+# guarantees that, if an attacker gains its priviledge, it will *not* be
+# able to change user passwords (which would allow him/her to read every
+# emails). This is a trick to tackle the absence of 'realgroup'.
+# 2. Anonymous users can bind.
+# 3. Users can change their password (but not read it).
+# 4. The postmaster of a domain can change (replace) his/her users' password (but not read it).
+olcAccess: to dn.regex="^fvl=[^,]+,(fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org)$"
+ filter=(objectClass=FripostVirtualUser)
+ attrs=userPassword
+ by realdn.exact="uid=AdminWebPanel@fripost.org,cn=auth" =0
+ by realanonymous =xd
+ by realself =w
+ by group/FripostVirtualDomain/fripostPostmaster.expand="$1" =w
+ by dn.onelevel="ou=managers,o=mailHosting,dc=fripost,dc=org" =w
+#
+# A catch-all, to be sure that noone else have access to the passwords.
+olcAccess: to dn.subtree="o=mailHosting,dc=fripost,dc=org"
+ attrs=userPassword
+ by * =0
+#
+#
+########################################################################
+# Virtual subtree, pending token and general access
+#
+# 1. Users need further access. We use a set to deny all access to non-users without
+# having a need for an expensive LDAP search (URL) in the AuthzTo.
+# /!\ The objectClass "FripostVirtualUser" is case-sensitive in this case!
+# 2,3. Services that need particular access on the tree.
+# 4. Managers have read/write access to the "virtual" subtree.
+olcAccess: to dn.subtree="ou=virtual,o=mailHosting,dc=fripost,dc=org"
+ by set.exact="user/objectClass & [FripostVirtualUser]" =0 break
+ by dn.exact="cn=CreateList,ou=services,o=mailHosting,dc=fripost,dc=org" =0 break
+ by dn.exact="cn=DeletePendingEntries,ou=services,o=mailHosting,dc=fripost,dc=org" =0 break
+ by dn.onelevel="ou=managers,o=mailHosting,dc=fripost,dc=org" =wrscd
+#
+# Only the domain Postmasters and Owners can delete the 'pending' status on domains.
+olcAccess: to dn.regex="^fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org$"
+ filter=(&(objectClass=FripostVirtualDomain)(objectClass=FripostPendingEntry))
+ attrs=objectClass val=FripostPendingEntry
+ by dnattr=fripostPostmaster =z break
+ by dnattr=fripostOwner =z break
+ by * =0 break
+#
+# The list creation service can delete the 'pending' status on lists and list commands.
+olcAccess: to dn.regex="^fvl=[^,]+,fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org$"
+ filter=(&(|(objectClass=FripostVirtualList)(objectClass=FripostVirtualListCommand))(objectClass=FripostPendingEntry))
+ attrs=objectClass val=FripostPendingEntry
+ by dn.exact="cn=CreateList,ou=services,o=mailHosting,dc=fripost,dc=org" =z break
+ by * +0 break
+#
+# ObjectClass is a public attribute: everyone can read and search it.
+olcAccess: to dn.subtree="ou=virtual,o=mailHosting,dc=fripost,dc=org"
+ attrs=objectClass
+ by * +rscd
+#
+# The pending token is not public, but domain owner and postmasters can check their and
+# delete it (if the token matches, but the check is done on the library side).
+olcAccess: to dn.regex="^fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org$"
+ filter=(&(objectClass=FripostVirtualDomain)(objectClass=FripostPendingEntry))
+ attrs=fripostPendingToken
+ by dnattr=fripostPostmaster =zcd break
+ by dnattr=fripostOwner =zcd break
+ by * +0 break
+#
+# The list creation service can delete the 'pending' status on lists and list commands.
+olcAccess: to dn.regex="^fvl=[^,]+,fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org$"
+ filter=(&(|(objectClass=FripostVirtualList)(objectClass=FripostVirtualListCommand))(objectClass=FripostPendingEntry))
+ attrs=fripostPendingToken
+ by dn.exact="cn=CreateList,ou=services,o=mailHosting,dc=fripost,dc=org" +z
+ by * +0
+#
+# The cleaning service can list the (expired) pending entries and delete them.
+olcAccess: to dn.children="ou=virtual,o=mailHosting,dc=fripost,dc=org"
+ filter=(objectClass=FripostPendingEntry)
+ attrs=entry
+ by dn.exact="cn=DeletePendingEntries,ou=services,o=mailHosting,dc=fripost,dc=org" =zrd break
+ by * =0 break
+#
+# One can search search everywhere in the virtual tree.
+olcAccess: to dn.subtree="ou=virtual,o=mailHosting,dc=fripost,dc=org"
+ attrs=entry
+ by dn.exact="cn=DeletePendingEntries,ou=services,o=mailHosting,dc=fripost,dc=org" +s
+ by * =s break
+#
+# We're giving away create/delete access on the children attributes, but we will be carefull
+# with the 'entry' permissions.
+olcAccess: to dn.base="ou=virtual,o=mailHosting,dc=fripost,dc=org"
+ filter=(objectClass=FripostVirtual)
+ attrs=children
+ by dn.children="ou=virtual,o=mailHosting,dc=fripost,dc=org" =w
+ by dn.exact="cn=DeletePendingEntries,ou=services,o=mailHosting,dc=fripost,dc=org" =z
+olcAccess: to dn.one="ou=virtual,o=mailHosting,dc=fripost,dc=org"
+ filter=(objectClass=FripostVirtualDomain)
+ attrs=children
+ by dn.exact="cn=DeletePendingEntries,ou=services,o=mailHosting,dc=fripost,dc=org" =z
+ by * break
+olcAccess: to dn.one="ou=virtual,o=mailHosting,dc=fripost,dc=org"
+ filter=(&(objectClass=FripostVirtualDomain)(!(objectClass=FripostPendingEntry)))
+ attrs=children
+ by dn.children="ou=virtual,o=mailHosting,dc=fripost,dc=org" =w
+#
+# The cleaning service needs to know when entries have been created.
+olcAccess: to dn.children="ou=virtual,o=mailHosting,dc=fripost,dc=org"
+ filter=(objectClass=FripostPendingEntry)
+ attrs=createTimestamp
+ by dn.exact="cn=DeletePendingEntries,ou=services,o=mailHosting,dc=fripost,dc=org" =s
+#
+# Users can use these in filters (e.g., to list the entries they have created).
+olcAccess: to dn.children="ou=virtual,o=mailHosting,dc=fripost,dc=org"
+ filter=(|(objectClass=FripostVirtualDomain)(objectClass=FripostVirtualUser)(objectClass=FripostVirtualAlias)(objectClass=FripostVirtualList))
+ attrs=fripostOwner,fripostPostmaster,fripostCanAddAlias,fripostCanAddList
+ by dn.children="ou=virtual,o=mailHosting,dc=fripost,dc=org" =s break
+#
+#
+########################################################################
+# Virtual subtree, domains
+#
+# 1. The postmaster of a domain can give (or take back) people the right to create
+# aliases.
+# 2,3. People that can create aliases can list the members of the group.
+olcAccess: to dn.regex="^fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org$"
+ filter=(objectClass=FripostVirtualDomain)
+ attrs=fripostCanAddAlias
+ by dnattr=fripostPostmaster =wrscd
+ by dnattr=fripostOwner =rscd
+ by set.exact="this/fripostCanAddAlias & (user | user/-1)" =rscd
+#
+# 1. The postmaster of a domain can give (or take back) people the right to create lists.
+# 2,3. People that can create lists can list the members of the group.
+olcAccess: to dn.regex="^fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org$"
+ filter=(objectClass=FripostVirtualDomain)
+ attrs=fripostCanAddList
+ by dnattr=fripostPostmaster =wrscd
+ by dnattr=fripostOwner =rscd
+ by set.exact="this/fripostCanAddList & (user | user/-1)" =rscd
+#
+# 1-3. Noone (but the managers) can appoint domain Owners or Postmasters.
+# But people that can create aliases and lists can list the members of their group.
+olcAccess: to dn.regex="^fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org$"
+ filter=(objectClass=FripostVirtualDomain)
+ attrs=fripostOwner,fripostPostmaster
+ by dnattr=fripostOwner =rscd
+ by dnattr=fripostPostmaster =rscd
+ by set.exact="(this/fripostCanAddAlias | this/fripostCanAddList) & (user | user/-1)" =rscd
+ by dn.onelevel,expand="$0" +d
+ by * +0
+#
+# 1. Domain owners can edit their entry's attributes.
+# 2. So can domain postmasters.
+# 3. Domain users can read the public domain attributes.
+# 4. So can users with "canAddAlias" or "canAddList" access.
+olcAccess: to dn.regex="^fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org$"
+ filter=(objectClass=FripostVirtualDomain)
+ attrs=fvd,fripostIsStatusActive,description
+ by dnattr=fripostOwner =wrscd
+ by dnattr=fripostPostmaster =wrscd
+ by dn.onelevel,expand="$0" =rscd
+ by set.exact="(this/fripostCanAddAlias | this/fripostCanAddList) & (user | user/-1)" =rscd
+#
+# 1. Domain owners can edit their entry's attributes.
+# 2. So can domain postmasters.
+olcAccess: to dn.regex="^fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org$"
+ filter=(objectClass=FripostVirtualDomain)
+ attrs=@fripostVirtualDomain
+ by dnattr=fripostOwner =wrscd
+ by dnattr=fripostPostmaster =wrscd
+ by * +0
+#
+# Users with "addDomain" access can create new entries, but only if
+# there is a pending token.
+olcAccess: to dn.regex="^fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org$"
+ filter=(&(objectClass=FripostVirtualDomain)(objectClass=FripostPendingEntry)(fripostPendingToken=*))
+ attrs=entry
+ by set.exact="this/-1/fripostCanAddDomain & (user | user/-1)" +a break
+ by * +0 break
+#
+# 1. Domain owners can delete their domain (and read the entry).
+# 2. So can domain postmasters.
+# 3. Domain users can read the domain entry (but not delete it).
+# 4. So can users with "canAddAlias" or "canAddList" rights.
+olcAccess: to dn.regex="^fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org$"
+ filter=(objectClass=FripostVirtualDomain)
+ attrs=entry
+ by dnattr=fripostOwner +zrd
+ by dnattr=fripostPostmaster +zrd
+ by dn.onelevel,expand="$0" +rd
+ by set.exact="(this/fripostCanAddAlias | this/fripostCanAddList) & (user | user/-1)" +rd
+ by dn.children="ou=virtual,o=mailHosting,dc=fripost,dc=org" +0
+#
+# Reserved local parts are reserved. /!\ The case must be insensitive
+# - postmaster: RFC 822, appendix C.6
+# - abuse: RFC 2142, section 4
+olcAccess: to dn.regex="^fvl=(postmaster|abuse),fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org$"
+ by * =0
+#
+#
+########################################################################
+# Virtual subtree, users
+#
+# Users and their postmaster can read the quota (but not change it).
+olcAccess: to dn.regex="^fvl=[^,]+,(fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org)$"
+ filter=(objectClass=FripostVirtualUser)
+ attrs=fripostUserQuota
+ by self =rscd
+ by group/FripostVirtualDomain/fripostPostmaster.expand="$1" =rscd
+#
+# 1. Users can modify their own entry.
+# 2. So can their postmasters.
+olcAccess: to dn.regex="^fvl=[^,]+,(fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org)$"
+ filter=(objectClass=FripostVirtualUser)
+ attrs=@FripostVirtualUser
+ by self =wrscd
+ by group/FripostVirtualDomain/fripostPostmaster.expand="$1" =wrscd
+#
+# 1. Users can read their entry (but not delete it).
+# 2. Postmasters can create users (but not delete them).
+# (Provided that they have +a access to the parent's "children" attribute.)
+olcAccess: to dn.regex="^fvl=[^,]+,(fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org)$"
+ filter=(objectClass=FripostVirtualUser)
+ attrs=entry
+ by self +rd
+ by group/FripostVirtualDomain/fripostPostmaster.expand="$1" +ard
+#
+#
+########################################################################
+# Virtual subtree, aliases
+#
+# 1. The alias owner can list the ownership of the entry.
+# 2. The domain owner can add/delete/change the ownership of the entry.
+# 3. So can the domain postmasters.
+olcAccess: to dn.regex="^fvl=[^,]+,(fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org)$"
+ filter=(objectClass=FripostVirtualAlias)
+ attrs=fripostOwner
+ by dnattr=fripostOwner =rscd continue
+ by group/FripostVirtualDomain/fripostOwner.expand="$1" =wrscd
+ by group/FripostVirtualDomain/fripostPostmaster.expand="$1" =wrscd
+ by * +0
+#
+# 1. The alias owners can edit the rest of their entry's attributes.
+# 2. So can the domain owners.
+# 3. So can the domain postmasters.
+olcAccess: to dn.regex="^fvl=[^,]+,(fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org)$"
+ filter=(objectClass=FripostVirtualAlias)
+ attrs=@FripostVirtualAlias
+ by dnattr=fripostOwner =wrscd
+ by group/FripostVirtualDomain/fripostOwner.expand="$1" =wrscd
+ by group/FripostVirtualDomain/fripostPostmaster.expand="$1" =wrscd
+#
+# 1. The alias owners can read and delete the entry.
+# 2. So can the domain owner.
+# 3. So can the domain postmaster.
+# 4. Users with "canAddAlias" access (either explicitely, or as a wildcard) for the domain can create aliases for that domain.
+# (But *not* delete them, unless also owner.)
+olcAccess: to dn.regex="^fvl=[^,]+,(fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org)$"
+ filter=(objectClass=FripostVirtualAlias)
+ attrs=entry
+ by dnattr=fripostOwner +zrd continue
+ by group/FripostVirtualDomain/fripostOwner.expand="$1" +wrd
+ by group/FripostVirtualDomain/fripostPostmaster.expand="$1" +wrd
+ by set.exact="this/-1/fripostCanAddAlias & (user | user/-1)" +a
+ by dn.children="ou=virtual,o=mailHosting,dc=fripost,dc=org" +0
+#
+#
+########################################################################
+# Virtual subtree, lists
+#
+# 1. The list owner can list the ownership of the entry.
+# 2. The domain owner can add/delete/change the ownership of the entry.
+# 3. So can the domain postmasters.
+olcAccess: to dn.regex="^fvl=[^,]+,(fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org)$"
+ filter=(objectClass=FripostVirtualList)
+ attrs=fripostOwner
+ by dnattr=fripostOwner =rscd continue
+ by group/FripostVirtualDomain/fripostOwner.expand="$1" =wrscd
+ by group/FripostVirtualDomain/fripostPostmaster.expand="$1" =wrscd
+ by * +0
+#
+# 1. The list owner read (but not edit) the transport-related attributes.
+# 2. So can the domain ower.
+# 3. So can the domain postmaster.
+olcAccess: to dn.regex="^fvl=[^,]+,(fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org)$"
+ filter=(objectClass=FripostVirtualList)
+ attrs=fripostListManager
+ by dnattr=fripostOwner =rscd
+ by group/FripostVirtualDomain/fripostOwner.expand="$1" =rscd
+ by group/FripostVirtualDomain/fripostPostmaster.expand="$1" =rscd
+#
+# Local aliases are for internal use only.
+olcAccess: to dn.regex="^fvl=[^,]+,(fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org)$"
+ filter=(objectClass=FripostVirtualList)
+ attrs=fripostLocalAlias
+ by * =0
+#
+# 1. The list owners can edit their entry's attributes.
+# 2. So can the domain owners.
+# 3. So can the domain postmasters.
+olcAccess: to dn.regex="^fvl=[^,]+,(fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org)$"
+ filter=(objectClass=FripostVirtualList)
+ attrs=@FripostVirtualList
+ by dnattr=fripostOwner =wrscd
+ by group/FripostVirtualDomain/fripostOwner.expand="$1" =wrscd
+ by group/FripostVirtualDomain/fripostPostmaster.expand="$1" =wrscd
+#
+# 1. The domain owner can create and delete lists, but only those with a 'pending' status
+# 2. So can the domain postmaster.
+# 3. The list owner can delete pending lists.
+# 4. The entry creator can delete pending lists (needed to be able to rollback).
+# 5. People with "canAddList" access can create lists, but only with a 'pending' status.
+# 6. The list creation service can search and browse the entry.
+olcAccess: to dn.regex="^fvl=[^,]+,(fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org)$"
+ filter=(&(objectClass=FripostVirtualList)(objectClass=FripostPendingEntry))
+ attrs=entry
+ by group/FripostVirtualDomain/fripostOwner.expand="$1" +w break
+ by group/FripostVirtualDomain/fripostPostmaster.expand="$1" +w break
+ by dnattr=fripostOwner +z continue
+ by dnattr=creatorsName +z continue
+ by set.exact="this/-1/fripostCanAddList & (user | user/-1)" +a break
+ by dn.exact="cn=CreateList,ou=services,o=mailHosting,dc=fripost,dc=org" +rd
+ by * +0 break
+#
+# 1. The domain owner can create and delete list commands, but only those with a 'pending' status
+# 2. So can the domain postmaster.
+# 3. The entry creator can delete pending list commands (needed to be able to rollback).
+# 4. People with "canAddList" access can create list commands, but only with a 'pending' status.
+# 5. The list creation service can search and browse the entry.
+olcAccess: to dn.regex="^fvl=[^,]+,(fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org)$"
+ filter=(&(objectClass=FripostVirtualListCommand)(objectClass=FripostPendingEntry))
+ attrs=entry
+ by group/FripostVirtualDomain/fripostOwner.expand="$1" +w
+ by group/FripostVirtualDomain/fripostPostmaster.expand="$1" +w
+ by dnattr=creatorsName +z continue
+ by set.exact="this/-1/fripostCanAddList & (user | user/-1)" +a
+ by dn.exact="cn=CreateList,ou=services,o=mailHosting,dc=fripost,dc=org" +rd
+ by * +0
+#
+# 1. The list owners can read the entry.
+# 2. So can the domain's Owner.
+# 3. So can the domain's Postmaster.
+olcAccess: to dn.regex="^fvl=[^,]+,(fvd=[^,]+,ou=virtual,o=mailHosting,dc=fripost,dc=org)$"
+ filter=(objectClass=FripostVirtualList)
+ attrs=entry
+ by dnattr=fripostOwner +rd
+ by group/FripostVirtualDomain/fripostOwner.expand="$1" +rd
+ by group/FripostVirtualDomain/fripostPostmaster.expand="$1" +rd
+ by * +0
+#
+#
+########################################################################
+# Catchall
+#
+# Users with "canAddDomain" access can see that they have the right
+# to create domains.
+olcAccess: to dn.base="ou=virtual,o=mailHosting,dc=fripost,dc=org"
+ filter=(objectClass=FripostVirtual)
+ attrs=entry
+ by dn.children="ou=virtual,o=mailHosting,dc=fripost,dc=org" +rd
+olcAccess: to dn.base="ou=virtual,o=mailHosting,dc=fripost,dc=org"
+ filter=(objectClass=FripostVirtual)
+ attrs=fripostCanAddDomain
+ by set.exact="this/fripostCanAddDomain & (user | user/-1)" =rscd
+# Catch the break above
+olcAccess: to dn.subtree="ou=virtual,o=mailHosting,dc=fripost,dc=org"
+ by dn.children="ou=virtual,o=mailHosting,dc=fripost,dc=org" +0
+# vim: set filetype=ldif :