summaryrefslogtreecommitdiffstats
path: root/lib/openldap
diff options
context:
space:
mode:
authorGuilhem Moulin <guilhem@fripost.org>2013-11-29 22:41:56 +0100
committerGuilhem Moulin <guilhem@fripost.org>2015-06-07 02:50:58 +0200
commit89958abf4bc85a4e376cc68d98a721604af1ea77 (patch)
tree19b8930f0291518a0038e37fd605ed156a0bd21f /lib/openldap
parent3d8b0ac104dee68b47d9a4d2ef622e7f1acdd7a4 (diff)
Allow flexible ACLs for SASL's EXTERNAL mechanism.
"username=postfix,cn=peercred,cn=external,cn=auth" is replaced by "gidNumber=106+uidNumber=102,cn=peercred,cn=external,cn=auth" where 102 is postfix's UID and 106 its primary GID (looked up from /etc/passwd).
Diffstat (limited to 'lib/openldap')
-rw-r--r--lib/openldap31
1 files changed, 30 insertions, 1 deletions
diff --git a/lib/openldap b/lib/openldap
index 5a4cef4..983299a 100644
--- a/lib/openldap
+++ b/lib/openldap
@@ -5,65 +5,87 @@
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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
+import re, pwd
# 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'll add
# explicit indices to what we find in the LDIF.
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' )],
}
+# Allow for flexible ACLs for user using SASL's EXTERNAL mechanism.
+# "username=postfix,cn=peercred,cn=external,cn=auth" is replaced by
+# "gidNumber=106+uidNumber=102,cn=peercred,cn=external,cn=auth" where
+# 102 is postfix's UID and 106 its primary GID.
+# (Regular expressions are not allowed.)
+sasl_ext_re = re.compile( r"""(?P<start>\sby\s+dn(?:\.exact)?)=
+ (?P<quote>['\"]?)username=(?P<user>[a-z][-a-z0-9_]*),
+ (?P<end>cn=peercred,cn=external,cn=auth)
+ (?P=quote)\s"""
+ , re.VERBOSE )
+pwd_dict = {}
+
+def acl_sasl_ext(m):
+ u = m.group('user')
+ if u not in pwd_dict.keys():
+ pwd_dict[u] = pwd.getpwnam(u)
+ return '%s="gidNumber=%d+uidNumber=%d,%s" ' % ( m.group('start')
+ , pwd_dict[u].pw_gid
+ , pwd_dict[u].pw_uid
+ , m.group('end')
+ )
+
# 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):
@@ -132,40 +154,45 @@ def flexibleSearch(module, l, dn, entry):
# 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 not in entry.keys():
if a != fst:
# delete all values except for the first attribute,
# which is implicit
diff.append(( ldap.MOD_DELETE, a, None ))
elif a in indexedAttributes:
+ if a == 'olcAccess':
+ # replace "username=...,cn=peercred,cn=external,cn=auth"
+ # by a DN with proper gidNumber and uidNumber
+ entry[a] = map ( partial(re.sub, sasl_ext_re, acl_sasl_ext)
+ , entry[a] )
# add explicit indices in the entry from the LDIF
entry[a] = map( (lambda x: '{%d}%s' % x)
, zip(range(len(entry[a])),entry[a]) )
if v != entry[a]:
diff.append(( ldap.MOD_REPLACE, a, entry[a] ))
elif v != entry[a]:
# for non-indexed attribute, we update values in the
# symmetric difference only
s1 = set(v)
s2 = set(entry[a])
if s1.isdisjoint(s2):
# replace the former values with the new ones
diff.append(( ldap.MOD_REPLACE, a, entry[a] ))
else:
x = list(s1.difference(s2))
if x:
diff.append(( ldap.MOD_DELETE, a, x ))
y = list(s2.difference(s1))
if y:
diff.append(( ldap.MOD_ADD, a, y ))
@@ -293,27 +320,29 @@ def main():
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'])
+ except KeyError, e:
+ module.fail_json(msg=str(e))
module.exit_json(changed=changed)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()