diff --git a/roles/ldap_server/library/ldap_permissions b/roles/ldap_server/library/ldap_permissions index 9371e6de7a944e152d8b4b0fb69476ddd0c3df91..c21a9781de77735c57d3082bcd92221454b13983 100644 --- a/roles/ldap_server/library/ldap_permissions +++ b/roles/ldap_server/library/ldap_permissions @@ -32,6 +32,22 @@ options: format for specifying this parameter (see examples below). required: true default: "" + server_uri: + description: + - LDAP connection URI specifying what server to connect to. + required: false + default: "ldapi:///" + bind_dn: + description: + - DN for binding to the LDAP server using simple bind. If not set, + EXTERNAL SASL binding method will be used. + required: false + default: "" + bind_password: + description: + - Password for binding to the LDAP server using simple bind. + required: false + default: "" """ EXAMPLES = """ @@ -63,6 +79,31 @@ ldap_permissions: - filter: '(olcDatabase={0}config)' rules: - to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage by * break +# Set-up rules on a remote server. +ldap_permissions: + - filter: '(olcSuffix=dc=example,dc=com)' + rules: + - > + to * + by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth manage + by * break + - > + to attrs=userPassword,shadowLastChange + by self write + by anonymous auth + by dn="cn=admin,dc=example,dc=com" write + by * none + - > + to dn.base="" + by * read + - > + to * + by self write + by dn="cn=admin,dc=example,dc=com" write + by * none + server_uri: ldap://ldap.example.com + bind_dn: cn=admin,dc=example,dc=com + bind_password: somepassword """ # Try to load the Python LDAP module. @@ -76,35 +117,74 @@ else: ldap_found = True -class LDAPPermissions(object): +def get_ldap_connection(uri, bind_dn=None, bind_password=""): """ - This class encapsulates functionality for applying ACL to an LDAP database. + Connects and binds to an LDAP server. + + Arguments: + + uri + LDAP connection URI specifying what server to connect to, including the + protocol. + + bind_dn + Distinguished name to be used for simple bind. If not set, SASL EXTERNAL + mechanism will be used for log-in. Default is None. + + bind_password + Password to be used for simple bind. Needs to be set only if bind_dn is + set as well. Default is "". + + Returns: + + LDAP connection object. """ - def __init__(self, module): - """ - Initialises class instance. Reads parameters from the passed - AnsibleModule instance, and connects to the LDAP server. - """ + connection = ldap.initialize(uri) - self.module = module - self.filter = module.params["filter"] - self.rules = module.params["rules"] - self._connect() + if bind_dn: + connection.simple_bind_s(bind_dn, bind_password) + else: + connection.sasl_interactive_bind_s("", ldap.sasl.external()) - def _connect(self): + return connection + + +class DatabaseFilteringError(Exception): + """ + Exception intended to be thrown in case the filter passed in to module did + not match one and only one entry in the configuration database. + """ + pass + + +class LDAPPermissions(object): + """ + Implements a convenience wrapper for managing permissions in OpenLDAP + server. + """ + + def __init__(self, ldap_filter, rules, connection): """ - Initialises LDAP connection and binds to the LDAP server. + Initialises class instance, setting-up the necessary properties. - Binding is done using the SASL EXTERNAL mechanism. + Arguments: - Returns: + ldap_filter + Filter that should be used under base cn=config to locate the database + that should be modified. - Nothing. + rules + Rules to apply. + + connection + LDAP connection object instance. This connection will be used for + running queries against an LDAP server. """ - self.connection = ldap.initialize("ldapi:///") - self.connection.sasl_interactive_bind_s("", ldap.sasl.external()) + self.ldap_filter = ldap_filter + self.rules = rules + self.connection = connection def _get_database(self): """ @@ -115,11 +195,13 @@ class LDAPPermissions(object): Database entry. Return format is same as for function ldap.search_s. """ - return self.connection.search_s(base="cn=config", scope=ldap.SCOPE_ONELEVEL, filterstr=self.filter) + return self.connection.search_s(base="cn=config", + scope=ldap.SCOPE_ONELEVEL, + filterstr=self.ldap_filter) def _get_modifications(self, database): """ - Returns modification list for updatingn the current ACL with requested + Returns modification list for updating the current ACL with requested ACL. Returns: @@ -136,20 +218,17 @@ class LDAPPermissions(object): requested_rules = [] for n, rule in enumerate(self.rules): rule = "{%d}%s" % (n, rule) - requested_rules.append(rule.rstrip().lstrip().decode("utf-8").encode("utf-8")) + requested_rules.append(rule.rstrip().lstrip().encode("utf-8")) return ldap.modlist.modifyModlist({'olcAccess': current_rules}, {'olcAccess': requested_rules}) - def apply(self): + def update(self): """ - Applies permissions requested via the Ansible module configuration. - - The function also produces the necessary JSON output, and terminates - module execution as appropriate. + Updates permissions for an LDAP database. Returns: - Nothing. + True, if an update was performed, False if no update was necessary. """ # Fetch the database config based on filter and verify and only one was @@ -157,9 +236,9 @@ class LDAPPermissions(object): databases = self._get_database() if databases == []: - self.module.fail_json(msg="No database matched filter: %s" % self.filter) + raise DatabaseFilteringError("No database matched filter: %s" % self.filter) elif len(databases) > 1: - self.module.fail_json(msg="More than one databases matched filter: %s" % self.filter) + raise DatabaseFilteringError("More than one databases matched filter: %s" % self.filter) database = databases[0] @@ -168,13 +247,10 @@ class LDAPPermissions(object): # Apply modifications if necessary. if modify_list == []: - self.module.exit_json(changed=False) + return False else: - try: - self.connection.modify_s(database[0], modify_list) - except ldap.OTHER as e: - self.module.fail_json(msg="Failed to modify permissions. Rule syntax was possibly incorrect. LDAP server responded: %s" % e) - self.module.exit_json(changed=True) + self.connection.modify_s(database[0], modify_list) + return True def main(): @@ -187,15 +263,45 @@ def main(): argument_spec=dict( filter=dict(required=True), rules=dict(required=True), + server_uri=dict(required=False, default="ldapi:///"), + bind_dn=dict(required=False, default=None), + bind_password=dict(required=False) ) ) if not ldap_found: module.fail_json(msg="The Python LDAP module is required") - ldap_rules = LDAPPermissions(module) + try: + connection = get_ldap_connection(module.params["server_uri"], + module.params["bind_dn"], + module.params["bind_password"]) + except ldap.LDAPError as e: + if e.info: + error_message = "%s: %s" % (e.desc, e.info) + else: + error_message = "%s" % e.desc + + module.fail_json(msg=error_message) + + ldap_permissions = LDAPPermissions(module.params["filter"], + module.params["rules"], + connection) + + try: + changed = ldap_permissions.update() + except ldap.LDAPError as e: + if e.info: + error_message = "%s: %s" % (e.desc, e.info) + else: + error_message = "%s" % e.desc + + module.fail_json(msg=error_message) + + except DatabaseFilteringError as e: + module.fail_json(msg=DatabaseFilteringError) - ldap_rules.apply() + module.exit_json(changed=changed) # Import module snippets. from ansible.module_utils.basic import *