Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
benscott committed Apr 11, 2017
2 parents 1e9e56f + dd17fd6 commit 70e48ed
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 13 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This plugin provides LDAP authentication for CKAN. Features include:
Requirements
------------

This plugin uses the pythton-ldap module. This available to install via pip:
This plugin uses the python-ldap module. This available to install via pip:

```sh
pip install python-ldap
Expand Down Expand Up @@ -46,16 +46,19 @@ In addition the plugin provides the following optional configuration items:
- `ckanext.ldap.prevent_edits`: If defined and true, this will prevent LDAP users from editing their profile. Note that there is no problem in allowing users to change their details - even their user name can be changed. But you may prefer to keep things centralized in your LDAP server. **Important**: while this prevents the operation from happening, it won't actually remove the 'edit settings' button from the dashboard. You need to do this in your own template;
- `ckanext.ldap.auth.dn`: If your LDAP server requires authentication (eg. Active Directory), this should be the DN to use;
- `ckanext.ldap.auth.password`: If your LDAP server requires authentication, add the password here;
- `ckanext.ldap.auth.method`: This is the method of authentication to use, can be either SIMPLE or SASL;
- `ckanext.ldap.auth.mechanism`: This is the SASL mechanism to use, if auth.method is set to SASL;
- `ckanext.ldap.fullname`: The LDAP attribute to map to the user's full name;
- `ckanext.ldap.about`: The LDAP attribute to map to the user's description;
- `ckanext.ldap.organization.id`: If this is set, users that log in using LDAP will automatically get added to the given organization. **Warning**: Changing this parameter will only affect users that have not yet logged on. It will not modify the organization of users who have already logged on;
- `ckanext.ldap.organization.role`: The role given to users added in the given organization ('admin', 'editor' or 'member'). **Warning**: Changing this parameter will only affect users that have not yet logged on. It will not modify the role of users who have already logged on;
- `ckanext.ldap.search.alt`: An alternative search string for the LDAP filter. If this is present and the search using `ckanext.ldap.search.filter` returns exactly 0 results, then a search using this filter will be performed. If this search returns exactly one result, then it will be accepted. You can use this for example in Active Directory to match against both username and fullname by setting `ckanext.ldap.search.filter` to 'sAMAccountName={login}' and `ckanext.ldap.search.alt` to 'name={login}'
The approach of using two separate filter strings (rather than one with an or statement) ensures that priority will always be given to the unique id match. `ckanext.ldap.search.alt` however can be used to match against more than one field. For example you could match against either the full name or the email address by setting `ckanext.ldap.search.alt` to '(|(name={login})(mail={login}))'.
- `ckanext.ldap.search.alt_msg`: A message that is output to the user when the search on `ckanext.ldap.search.filter` returns 0 results, and the search on `ckanext.ldap.search.alt` returns more than one result. Example: 'Please use your short account name instead'.
- `ckanext.ldap.migrate` : If defined and true this will change an existing CKAN user with the same username to an LDAP user. Otherwise, an exception `UserConflictError`is raised if LDAP-login with an already existing local CKAN username is attempted. This option provides a migration path from local CKAN authentication to LDAP authentication: Rename all users to their LDAP usernames and instruct them to login with their LDAP credentials. Migration then happens transparently.


**Note**: Configuration options wihtout the `ckanext.` prefix are deprecated and will be eventually removed. Please update your settings if you are using them.
**Note**: Configuration options without the `ckanext.` prefix are deprecated and will be eventually removed. Please update your settings if you are using them.


CLI Commands
Expand Down
48 changes: 38 additions & 10 deletions ckanext/ldap/controllers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,30 +137,44 @@ def _get_or_create_ldap_user(ldap_user_dict):
if ldap_user:
# TODO: Update the user detail.
return ldap_user.user.name
user_dict = {}
update=False
# Check whether we have a name conflict (based on the ldap name, without mapping it to allowed chars)
exists = _ckan_user_exists(ldap_user_dict['username'])
if exists['exists'] and not exists['is_ldap']:
raise UserConflictError(_('There is a username conflict. Please inform the site administrator.'))
# If ckanext.ldap.migrate is set, update exsting user_dict.
if not config['ckanext.ldap.migrate']:
raise UserConflictError(_('There is a username conflict. Please inform the site administrator.'))
else:
user_dict = p.toolkit.get_action('user_show')(data_dict = {'id': ldap_user_dict['username']})
update=True

# If a user with the same ckan name already exists but is an LDAP user, this means (given that we didn't
# find it above) that the conflict arises from having mangled another user's LDAP name. There will not
# however be a conflict based on what is entered in the user prompt - so we can go ahead. The current
# user's id will just be mangled to something different.

# Now get a unique user name, and create the CKAN user and the LdapUser entry.
user_name = _get_unique_user_name(ldap_user_dict['username'])
user_dict = {
# Now get a unique user name (if not "migrating"), and create the CKAN user and the LdapUser entry.
user_name = user_dict['name'] if update else _get_unique_user_name(ldap_user_dict['username'])
user_dict.update({
'name': user_name,
'email': ldap_user_dict['email'],
'password': str(uuid.uuid4())
}
})
if 'fullname' in ldap_user_dict:
user_dict['fullname'] = ldap_user_dict['fullname']
if 'about' in ldap_user_dict:
user_dict['about'] = ldap_user_dict['about']
ckan_user = p.toolkit.get_action('user_create')(
context={'ignore_auth': True},
data_dict=user_dict
)
if update:
ckan_user = p.toolkit.get_action('user_update')(
context={'ignore_auth': True},
data_dict=user_dict
)
else:
ckan_user = p.toolkit.get_action('user_create')(
context={'ignore_auth': True},
data_dict=user_dict
)
ldap_user = LdapUser(user_id=ckan_user['id'], ldap_id = ldap_user_dict['username'])
ckan.model.Session.add(ldap_user)
ckan.model.Session.commit()
Expand All @@ -187,13 +201,27 @@ def _find_ldap_user(login):
cnx = ldap.initialize(config['ckanext.ldap.uri'])
if config.get('ckanext.ldap.auth.dn'):
try:
cnx.bind_s(config['ckanext.ldap.auth.dn'], config['ckanext.ldap.auth.password'])
if config['ckanext.ldap.auth.method'] == 'SIMPLE':
cnx.bind_s(config['ckanext.ldap.auth.dn'], config['ckanext.ldap.auth.password'])
elif config['ckanext.ldap.auth.method'] == 'SASL':
if config['ckanext.ldap.auth.mechanism'] == 'DIGEST-MD5':
auth_tokens = ldap.sasl.digest_md5(config['ckanext.ldap.auth.dn'], config['ckanext.ldap.auth.password'])
cnx.sasl_interactive_bind_s("", auth_tokens)
else:
log.error("SASL mechanism not supported: {0}".format(config['ckanext.ldap.auth.mechanism']))
return None
else:
log.error("LDAP authentication method is not supported: {0}".format(config['ckanext.ldap.auth.method']))
return None
except ldap.SERVER_DOWN:
log.error('LDAP server is not reachable')
return None
except ldap.INVALID_CREDENTIALS:
log.error('LDAP server credentials (ckanext.ldap.auth.dn and ckanext.ldap.auth.password) invalid')
return None
except ldap.LDAPError, e:
log.error("Fatal LDAP Error: {0}".format(e))
return None

filter_str = config['ckanext.ldap.search.filter'].format(login=ldap.filter.escape_filter_chars(login))
attributes = [config['ckanext.ldap.username']]
Expand Down
15 changes: 14 additions & 1 deletion ckanext/ldap/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,16 @@ def configure(self, main_config):
'ckanext.ldap.email': {'required': True},
'ckanext.ldap.auth.dn': {},
'ckanext.ldap.auth.password': {'required_if': 'ckanext.ldap.auth.dn'},
'ckanext.ldap.auth.method': {'default': 'SIMPLE', 'validate': _allowed_auth_methods},
'ckanext.ldap.auth.mechanism': {'default': 'DIGEST-MD5', 'validate': _allowed_auth_mechanisms},
'ckanext.ldap.search.alt': {},
'ckanext.ldap.search.alt_msg': {'required_if': 'ckanext.ldap.search.alt'},
'ckanext.ldap.fullname': {},
'ckanext.ldap.organization.id': {},
'ckanext.ldap.organization.role': {'default': 'member', 'validate': _allowed_roles},
'ckanext.ldap.ckan_fallback': {'default': False, 'parse': p.toolkit.asbool},
'ckanext.ldap.prevent_edits': {'default': False, 'parse': p.toolkit.asbool}
'ckanext.ldap.prevent_edits': {'default': False, 'parse': p.toolkit.asbool},
'ckanext.ldap.migrate': {'default': False, 'parse': p.toolkit.asbool}
}
errors = []
for i in schema:
Expand Down Expand Up @@ -145,3 +148,13 @@ def _allowed_roles(v):
"""Raise an exception if the value is not an allowed role"""
if v not in ['member', 'editor', 'admin']:
raise ConfigError('role must be one of "member", "editor" or "admin"')

def _allowed_auth_methods(v):
"""Raise an exception if the value is not an allowed authentication method"""
if v.upper() not in ['SIMPLE', 'SASL']:
raise ConfigError('Only SIMPLE and SASL authentication methods are supported')

def _allowed_auth_mechanisms(v):
"""Raise an exception if the value is not an allowed authentication mechanism"""
if v.upper() not in ['DIGEST-MD5',]: # Only DIGEST-MD5 is supported when the auth method is SASL
raise ConfigError('Only DIGEST-MD5 is supported as an authentication mechanism')

0 comments on commit 70e48ed

Please sign in to comment.