From 98bdb1275587e60f57c8a53c045ccdd2e87ddf17 Mon Sep 17 00:00:00 2001 From: cameron Date: Mon, 15 Jul 2019 19:02:57 +0800 Subject: [PATCH] add group ldap settings --- .../doctype/ldap_group_mapping/__init__.py | 0 .../ldap_group_mapping.json | 38 ++ .../ldap_group_mapping/ldap_group_mapping.py | 10 + .../doctype/ldap_settings/ldap_settings.json | 605 ++++-------------- .../doctype/ldap_settings/ldap_settings.py | 292 +++++---- .../ldap_settings/test_ldap_settings.py | 10 + frappe/www/login.py | 4 +- 7 files changed, 339 insertions(+), 620 deletions(-) create mode 100644 frappe/integrations/doctype/ldap_group_mapping/__init__.py create mode 100644 frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.json create mode 100644 frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py create mode 100644 frappe/integrations/doctype/ldap_settings/test_ldap_settings.py diff --git a/frappe/integrations/doctype/ldap_group_mapping/__init__.py b/frappe/integrations/doctype/ldap_group_mapping/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.json b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.json new file mode 100644 index 0000000000..92db68e962 --- /dev/null +++ b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.json @@ -0,0 +1,38 @@ +{ + "creation": "2019-05-29 01:24:29.585060", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "ldap_group", + "erpnext_role" + ], + "fields": [ + { + "fieldname": "ldap_group", + "fieldtype": "Data", + "in_list_view": 1, + "label": "LDAP Group", + "reqd": 1 + }, + { + "fieldname": "erpnext_role", + "fieldtype": "Link", + "in_list_view": 1, + "label": "ERPNext Role", + "options": "Role", + "reqd": 1 + } + ], + "istable": 1, + "modified": "2019-07-15 06:46:38.050408", + "modified_by": "Administrator", + "module": "Integrations", + "name": "LDAP Group Mapping", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py new file mode 100644 index 0000000000..f9f2adeed0 --- /dev/null +++ b/frappe/integrations/doctype/ldap_group_mapping/ldap_group_mapping.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class LDAPGroupMapping(Document): + pass diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.json b/frappe/integrations/doctype/ldap_settings/ldap_settings.json index aa43b2e9d0..5d30a873fb 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.json +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.json @@ -1,594 +1,215 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, "creation": "2016-09-22 04:16:48.829658", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "System", "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enabled", + "ldap_server_url", + "column_break_4", + "base_dn", + "password", + "section_break_5", + "organizational_unit", + "default_role", + "ldap_search_string", + "ldap_email_field", + "ldap_username_field", + "column_break_11", + "ldap_first_name_field", + "ldap_middle_name_field", + "ldap_last_name_field", + "ldap_phone_field", + "ldap_mobile_field", + "ldap_security", + "ssl_tls_mode", + "require_trusted_certificate", + "column_break_17", + "local_private_key_file", + "local_server_certificate_file", + "local_ca_certs_file", + "ldap_group_mappings_section", + "ldap_group_field", + "ldap_groups" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "enabled", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Enabled", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Enabled" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "ldap_server_url", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "LDAP Server Url", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "organizational_unit", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Organizational Unit", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "column_break_4", + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "base_dn", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Base Distinguished Name (DN)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "password", "fieldtype": "Password", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Password for Base DN", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "section_break_5", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "LDAP User Creation and Mapping" + }, + { + "fieldname": "organizational_unit", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Organizational Unit for Users", + "reqd": 1 + }, + { + "fieldname": "default_role", + "fieldtype": "Link", + "label": "Default Role on Creation", + "options": "Role", + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "ldap_search_string", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "LDAP Search String", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "ldap_first_name_field", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "LDAP First Name Field", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "ldap_email_field", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "LDAP Email Field", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "ldap_username_field", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "LDAP Username Field", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "ldap_first_name_field", + "fieldtype": "Data", + "label": "LDAP First Name Field", + "reqd": 1 + }, + { + "fieldname": "ldap_middle_name_field", + "fieldtype": "Data", + "label": "LDAP Middle Name Field" + }, + { + "fieldname": "ldap_last_name_field", + "fieldtype": "Data", + "label": "LDAP Last Name Field" + }, + { + "fieldname": "ldap_phone_field", + "fieldtype": "Data", + "label": "LDAP Phone Field" + }, + { + "fieldname": "ldap_mobile_field", + "fieldtype": "Data", + "label": "LDAP Mobile Field" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "ldap_security", "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "LDAP Security", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "LDAP Security" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Off", - "description": "", - "fetch_if_empty": 0, "fieldname": "ssl_tls_mode", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "SSL/TLS Mode", - "length": 0, - "no_copy": 0, - "options": "Off\nStartTLS", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Off\nStartTLS" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "No", - "fetch_if_empty": 0, "fieldname": "require_trusted_certificate", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Require Trusted Certificate", - "length": 0, - "no_copy": 0, "options": "No\nYes", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 + }, + { + "fieldname": "column_break_17", + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "local_private_key_file", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Path to private Key File", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Path to private Key File" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "local_server_certificate_file", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Path to Server Certificate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Path to Server Certificate" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "local_ca_certs_file", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Path to CA Certs File", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Path to CA Certs File" + }, + { + "fieldname": "ldap_group_mappings_section", + "fieldtype": "Section Break", + "label": "LDAP Group Mappings" + }, + { + "fieldname": "ldap_group_field", + "fieldtype": "Data", + "label": "LDAP Group Field" + }, + { + "fieldname": "ldap_groups", + "fieldtype": "Table", + "label": "LDAP Group Mappings", + "options": "LDAP Group Mapping" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, "in_create": 1, - "is_submittable": 0, "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2019-04-29 10:56:42.322696", + "modified": "2019-07-15 06:48:16.562109", "modified_by": "Administrator", "module": "Integrations", "name": "LDAP Settings", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, - "report": 0, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, "read_only": 1, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py index 5fa54e9628..8cc2489b72 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py @@ -15,155 +15,195 @@ class LDAPSettings(Document): if not self.flags.ignore_mandatory: if self.ldap_search_string and self.ldap_search_string.endswith("={0}"): - connect_to_ldap(server_url=self.ldap_server_url, - base_dn=self.base_dn, - password=self.get_password(raise_exception=False), - ssl_tls_mode=self.ssl_tls_mode, - trusted_cert=self.require_trusted_certificate, - private_key_file=self.local_private_key_file, - server_cert_file=self.local_server_certificate_file, - ca_certs_file=self.local_ca_certs_file - ) + self.connect_to_ldap(base_dn=self.base_dn, password=self.get_password(raise_exception=False)) else: frappe.throw(_("LDAP Search String needs to end with a placeholder, eg sAMAccountName={0}")) + def connect_to_ldap(self, base_dn, password): + try: + import ldap3 + import ssl -def get_ldap_client_settings(): - #return the settings to be used on the client side. - result = { - "enabled": False - } - settings = frappe.get_doc("LDAP Settings") + if self.require_trusted_certificate == 'Yes': + tls_configuration = ldap3.Tls(validate=ssl.CERT_REQUIRED, version=ssl.PROTOCOL_TLSv1) + else: + tls_configuration = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1) - if settings and settings.enabled: - result["enabled"] = True - result["method"] = "frappe.integrations.doctype.ldap_settings.ldap_settings.login" - return result + if self.local_private_key_file: + tls_configuration.private_key_file = self.local_private_key_file + if self.local_server_certificate_file: + tls_configuration.certificate_file = self.local_server_certificate_file + if self.local_ca_certs_file: + tls_configuration.ca_certs_file = self.local_ca_certs_file + server = ldap3.Server(host=self.ldap_server_url, tls=tls_configuration) + bind_type = ldap3.AUTO_BIND_TLS_BEFORE_BIND if self.ssl_tls_mode == "StartTLS" else True -def connect_to_ldap(server_url, - base_dn, - password, - ssl_tls_mode, - trusted_cert, - private_key_file, - server_cert_file, - ca_certs_file): - try: - import ldap3 - import ssl + conn = ldap3.Connection( + server=server, + user=base_dn, + password=password, + auto_bind=bind_type, + read_only=True, + raise_exceptions=True) - if trusted_cert == 'Yes': - tls_configuration = ldap3.Tls(validate=ssl.CERT_REQUIRED, - version=ssl.PROTOCOL_TLSv1) + return conn + + except ImportError: + msg = _("Please Install the ldap3 library via pip to use ldap functionality.") + frappe.throw(msg, title=_("LDAP Not Installed")) + except ldap3.core.exceptions.LDAPInvalidCredentialsResult: + frappe.throw(_("Invalid username or password")) + except Exception as ex: + frappe.throw(_(str(ex))) + + @staticmethod + def get_ldap_client_settings(): + # return the settings to be used on the client side. + result = { + "enabled": False + } + ldap = frappe.get_doc("LDAP Settings") + if ldap.enabled: + result["enabled"] = True + result["method"] = "frappe.integrations.doctype.ldap_settings.ldap_settings.login" + return result + + @classmethod + def update_user_fields(cls, user, user_data): + + updatable_data = {key: value for key, value in user_data.items() if key != 'email'} + + for key, value in updatable_data.items(): + setattr(user, key, value) + user.save(ignore_permissions=True) + + def sync_roles(self, user, additional_groups=None): + + current_roles = set([d.role for d in user.get("roles")]) + + needed_roles = set() + needed_roles.add(self.default_role) + + lower_groups = [g.lower() for g in additional_groups or []] + + all_mapped_roles = {r.erpnext_role for r in self.ldap_groups} + matched_roles = {r.erpnext_role for r in self.ldap_groups if r.ldap_group.lower() in lower_groups} + unmatched_roles = all_mapped_roles.difference(matched_roles) + needed_roles.update(matched_roles) + roles_to_remove = current_roles.intersection(unmatched_roles) + + if not needed_roles.issubset(current_roles): + missing_roles = needed_roles.difference(current_roles) + user.add_roles(*missing_roles) + + user.remove_roles(*roles_to_remove) + + def create_or_update_user(self, user_data, groups=None): + user = None + if frappe.db.exists("User", user_data['email']): + user = frappe.get_doc("User", user_data['email']) + LDAPSettings.update_user_fields(user=user, user_data=user_data) else: - tls_configuration = ldap3.Tls(validate=ssl.CERT_NONE, - version=ssl.PROTOCOL_TLSv1) + doc = user_data + doc.update({ + "doctype": "User", + "send_welcome_email": 0, + "language": "", + "user_type": "System User", + # "roles": [{ + # "role": self.default_role + # }] + }) + user = frappe.get_doc(doc) + user.insert(ignore_permissions=True) + # always add default role. + user.add_roles(self.default_role) + if self.ldap_group_field: + self.sync_roles(user, groups) + return user - if private_key_file: - tls_configuration.private_key_file = private_key_file - if server_cert_file: - tls_configuration.certificate_file = server_cert_file - if ca_certs_file: - tls_configuration.ca_certs_file = ca_certs_file + def get_ldap_attributes(self): + ldap_attributes = [self.ldap_email_field, self.ldap_username_field, self.ldap_first_name_field] - server = ldap3.Server(host=server_url, - tls=tls_configuration) - bind_type = ldap3.AUTO_BIND_TLS_BEFORE_BIND if ssl_tls_mode == "StartTLS" else True + if self.ldap_group_field: + ldap_attributes.append(self.ldap_group_field) - conn = ldap3.Connection(server=server, - user=base_dn, - password=password, - auto_bind=bind_type, - read_only=True, - raise_exceptions=True) + if self.ldap_middle_name_field: + ldap_attributes.append(self.ldap_middle_name_field) - return conn + if self.ldap_last_name_field: + ldap_attributes.append(self.ldap_last_name_field) - except ImportError: - msg = _("Please Install the ldap3 library via pip to use ldap functionality.") - frappe.throw(msg, title=_("LDAP Not Installed")) - except ldap3.core.exceptions.LDAPInvalidCredentialsResult: - frappe.throw(_("Invalid Credentials")) - except Exception as ex: - frappe.throw(_(str(ex))) + if self.ldap_phone_field: + ldap_attributes.append(self.ldap_phone_field) + + if self.ldap_mobile_field: + ldap_attributes.append(self.ldap_mobile_field) + + return ldap_attributes + + def authenticate(self, username, password): + + if not self.enabled: + frappe.throw(_("LDAP is not enabled.")) + + user_filter = self.ldap_search_string.format(username) + ldap_attributes = self.get_ldap_attributes() + + conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False)) + + conn.search( + search_base=self.organizational_unit, + search_filter="({0})".format(user_filter), + attributes=ldap_attributes) + + if len(conn.entries) == 1 and conn.entries[0]: + user = conn.entries[0] + # only try and connect as the user, once we have their fqdn entry. + self.connect_to_ldap(base_dn=user.entry_dn, password=password) + + groups = None + if self.ldap_group_field: + groups = getattr(user, self.ldap_group_field).values + return self.create_or_update_user(self.convert_ldap_entry_to_dict(user), groups=groups) + else: + frappe.throw(_("Invalid username or password")) + + def convert_ldap_entry_to_dict(self, user_entry): + data = { + 'username': user_entry[self.ldap_username_field].value, + 'email': user_entry[self.ldap_email_field].value, + 'first_name': user_entry[self.ldap_first_name_field].value + } + + # optional fields + + if self.ldap_middle_name_field: + data['middle_name'] = user_entry[self.ldap_middle_name_field].value + + if self.ldap_last_name_field: + data['last_name'] = user_entry[self.ldap_last_name_field].value + + if self.ldap_phone_field: + data['phone'] = user_entry[self.ldap_phone_field].value + + if self.ldap_mobile_field: + data['mobile_no'] = user_entry[self.ldap_mobile_field].value + + return data @frappe.whitelist(allow_guest=True) def login(): # LDAP LOGIN LOGIC args = frappe.form_dict - user = authenticate_ldap_user(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd)) + ldap = frappe.get_doc("LDAP Settings") + + user = ldap.authenticate(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd)) frappe.local.login_manager.user = user.name frappe.local.login_manager.post_login() # because of a GET request! frappe.db.commit() - - -def authenticate_ldap_user(user=None, - password=None): - - params = {} - settings = frappe.get_doc("LDAP Settings") - if settings and settings.enabled: - conn = connect_to_ldap(server_url=settings.ldap_server_url, - base_dn=settings.base_dn, - password=settings.get_password(raise_exception=False), - ssl_tls_mode=settings.ssl_tls_mode, - trusted_cert=settings.require_trusted_certificate, - private_key_file=settings.local_private_key_file, - server_cert_file=settings.local_server_certificate_file, - ca_certs_file=settings.local_ca_certs_file) - - user_filter = settings.ldap_search_string.format(user) - conn.search(search_base=settings.organizational_unit, - search_filter="({0})".format(user_filter), - attributes=[settings.ldap_email_field, - settings.ldap_username_field, - settings.ldap_first_name_field]) - - if len(conn.entries) > 0 and conn.entries[0]: - user = conn.entries[0] - params["email"] = str(user[settings.ldap_email_field]) - params["username"] = str(user[settings.ldap_username_field]) - params["first_name"] = str(user[settings.ldap_first_name_field]) - connect_to_ldap(server_url=settings.ldap_server_url, - base_dn=user.entry_dn, - password=frappe.as_unicode(password), - ssl_tls_mode=settings.ssl_tls_mode, - trusted_cert=settings.require_trusted_certificate, - private_key_file=settings.local_private_key_file, - server_cert_file=settings.local_server_certificate_file, - ca_certs_file=settings.local_ca_certs_file - ) - return create_user(params) - else: - frappe.throw(_("Not a valid LDAP user")) - else: - frappe.throw(_("LDAP is not enabled.")) - - -def create_user(params): - if frappe.db.exists("User", params["email"]): - user = frappe.get_doc("User", params["email"]) - user.first_name = params["first_name"] - user.username = params["username"] - user.save(ignore_permissions=True) - return user - - else: - params.update({ - "doctype": "User", - "send_welcome_email": 0, - "language": "", - "user_type": "System User", - "roles": [{ - "role": _("Customer") - }] - }) - - user = frappe.get_doc(params).insert(ignore_permissions=True) - - return user diff --git a/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py new file mode 100644 index 0000000000..e6cf4eef3a --- /dev/null +++ b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLDAPSettings(unittest.TestCase): + pass diff --git a/frappe/www/login.py b/frappe/www/login.py index c21cea2d7f..6745ddea56 100644 --- a/frappe/www/login.py +++ b/frappe/www/login.py @@ -8,7 +8,7 @@ from frappe.utils.oauth import get_oauth2_authorize_url, get_oauth_keys, login_v import json from frappe import _ from frappe.auth import LoginManager -from frappe.integrations.doctype.ldap_settings.ldap_settings import get_ldap_client_settings +from frappe.integrations.doctype.ldap_settings.ldap_settings import LDAPSettings from frappe.utils.password import get_decrypted_password from frappe.utils.html_utils import get_icon_html @@ -38,7 +38,7 @@ def get_context(context): "icon": icon }) context["social_login"] = True - ldap_settings = get_ldap_client_settings() + ldap_settings = LDAPSettings.get_ldap_client_settings() context["ldap_settings"] = ldap_settings login_name_placeholder = [_("Email address")]