From f9a9e1de49de4e01809b1fd14ae9ff5fe0c93410 Mon Sep 17 00:00:00 2001 From: Dmitry Degtyarev Date: Tue, 12 Jan 2021 14:55:47 +0400 Subject: [PATCH] merge adldap into adinterface lots of adldap f-ns were just wrapping calls to ldap lib also adldap code was held back by lack of string/list class --- .gear/admc.spec | 1 - src/CMakeLists.txt | 1 - src/adldap/CMakeLists.txt | 29 -- src/adldap/active_directory.c | 549 --------------------------------- src/adldap/active_directory.h | 108 ------- src/adldap/test/CMakeLists.txt | 5 - src/adldap/test/test_adldap | Bin 102384 -> 0 bytes src/adldap/test/test_adldap.c | 46 --- src/admc/CMakeLists.txt | 5 +- src/admc/ad_interface.cpp | 414 +++++++++++++++++++++---- src/admc/ad_interface.h | 6 +- 11 files changed, 364 insertions(+), 800 deletions(-) delete mode 100644 src/adldap/CMakeLists.txt delete mode 100644 src/adldap/active_directory.c delete mode 100644 src/adldap/active_directory.h delete mode 100644 src/adldap/test/CMakeLists.txt delete mode 100755 src/adldap/test/test_adldap delete mode 100644 src/adldap/test/test_adldap.c diff --git a/.gear/admc.spec b/.gear/admc.spec index a67faf78..f0c2c449 100644 --- a/.gear/admc.spec +++ b/.gear/admc.spec @@ -62,7 +62,6 @@ cd BUILD %files %doc README.md %_bindir/admc -%_libdir/libadldap.so %files gpgui %_bindir/gpgui diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 768ff86f..bfc36b7c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,5 +6,4 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) add_compile_options(-Wall -Wshadow -Werror=switch) add_subdirectory(admc) -add_subdirectory(adldap) add_subdirectory(gpgui) diff --git a/src/adldap/CMakeLists.txt b/src/adldap/CMakeLists.txt deleted file mode 100644 index 4fe39ed8..00000000 --- a/src/adldap/CMakeLists.txt +++ /dev/null @@ -1,29 +0,0 @@ -option(ENABLE_TESTS "Enable unit tests for libadldap" ON) - -find_package(Ldap REQUIRED) -find_package(Resolv REQUIRED) - -add_library(adldap SHARED - active_directory.c -) - -target_include_directories(adldap - PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} -) - -target_link_libraries(adldap - PUBLIC - Ldap::Ldap - Resolv::Resolv -) - -install(TARGETS adldap) - -if(ENABLE_TESTS) - find_package(CMocka CONFIG REQUIRED) - include(AddCMockaTest) - include(AddMockedTest) - enable_testing() - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/test) -endif() diff --git a/src/adldap/active_directory.c b/src/adldap/active_directory.c deleted file mode 100644 index 772f4720..00000000 --- a/src/adldap/active_directory.c +++ /dev/null @@ -1,549 +0,0 @@ -/** - * Copyright (c) by: Mike Dawson mike _at_ no spam gp2x.org - * Copyright (C) 2020 BaseALT Ltd. - * - * This file may be used subject to the terms and conditions of the - * GNU Library General Public License Version 2, or any later version - * at your option, as published by the Free Software Foundation. - * 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 Library General Public License for more details. - * -**/ - -#include "active_directory.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// NOTE: LDAP library char* inputs are non-const in the API but are -// actually const so we opt to discard const qualifiers rather -// than allocate copies - -#ifdef __GNUC__ -# define UNUSED(x) x __attribute__((unused)) -#else -# define UNUSED(x) x -#endif - -#define MAX_DN_LENGTH 1024 -#define MAX_PASSWORD_LENGTH 255 - -typedef struct sasl_defaults_gssapi { - char *mech; - char *realm; - char *authcid; - char *passwd; - char *authzid; -} sasl_defaults_gssapi; - -int sasl_interact_gssapi(LDAP *ld, unsigned flags, void *indefaults, void *in); -int query_server_for_hosts(const char *dname, char ***hosts_out); - -int ad_get_ldap_result(LDAP *ld) { - int result; - ldap_get_option(ld, LDAP_OPT_RESULT_CODE, &result); - - return result; -} - -int ad_get_domain_hosts(const char *domain, const char *site, char ***hosts_out) { - int result = AD_SUCCESS; - - char **hosts = NULL; - char **site_hosts = NULL; - char **default_hosts = NULL; - - // TODO: confirm site query is formatted properly, currently getting no answer back (might be working as intended, since tested on domain without sites?) - - // Query site hosts - if (site != NULL && strlen(site) > 0) { - char dname[1000]; - snprintf(dname, sizeof(dname), "_ldap._tcp.%s._sites.%s", site, domain); - - int query_result = query_server_for_hosts(dname, &site_hosts); - if (query_result != AD_SUCCESS) { - result = AD_ERROR; - - goto end; - } - } - - const size_t site_hosts_size = ad_array_size(site_hosts); - - // Query default hosts - char dname_default[1000]; - snprintf(dname_default, sizeof(dname_default), "_ldap._tcp.%s", domain); - - int query_result = query_server_for_hosts(dname_default, &default_hosts); - if (query_result != AD_SUCCESS) { - result = AD_ERROR; - - goto end; - } - - const size_t default_hosts_size = ad_array_size(default_hosts); - - // Combine site and default hosts - const int hosts_max_size = site_hosts_size + default_hosts_size + 1; - hosts = calloc(hosts_max_size, sizeof(char *)); - size_t hosts_current_i = 0; - - // Load all site hosts first - for (int i = 0; i < site_hosts_size; i++) { - char *site_host = site_hosts[i]; - hosts[hosts_current_i] = strdup(site_host); - hosts_current_i++; - } - - // Add default hosts that aren't already in list - for (int i = 0; i < default_hosts_size; i++) { - char *default_host = default_hosts[i]; - - bool already_in_list = false; - for (int j = 0; j < hosts_current_i; j++) { - char *other_host = hosts[j]; - - if (strcmp(default_host, other_host) == 0) { - already_in_list = true; - break; - } - } - - if (!already_in_list) { - hosts[hosts_current_i] = strdup(default_host); - hosts_current_i++; - } - } - - end: - { - ad_array_free(site_hosts); - ad_array_free(default_hosts); - - if (result == AD_SUCCESS) { - *hosts_out = hosts; - } else { - *hosts_out = NULL; - ad_array_free(hosts); - } - - return result; - } -} - -int ad_connect(const char* uri, LDAP **ld_out) { - int result = AD_SUCCESS; - - LDAP *ld = NULL; - - const int result_init = ldap_initialize(&ld, uri); - if (result_init != LDAP_SUCCESS) { - result = AD_LDAP_ERROR; - - goto end; - } - - // Set version - const int version = LDAP_VERSION3; - const int result_set_version = ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &version); - if (result_set_version != LDAP_OPT_SUCCESS) { - result = AD_LDAP_ERROR; - - goto end; - } - - // Disable referrals - const int result_referral =ldap_set_option(ld, LDAP_OPT_REFERRALS, LDAP_OPT_OFF); - if (result_referral != LDAP_OPT_SUCCESS) { - result = AD_LDAP_ERROR; - - goto end; - } - - // Set maxssf - const char* sasl_secprops = "maxssf=56"; - const int result_secprops = ldap_set_option(ld, LDAP_OPT_X_SASL_SECPROPS, sasl_secprops); - if (result_secprops != LDAP_SUCCESS) { - result = AD_LDAP_ERROR; - - goto end; - } - - ldap_set_option(ld, LDAP_OPT_X_SASL_NOCANON, LDAP_OPT_ON); - - // TODO: add option to turn off - ldap_set_option(ld, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER); - - // Setup sasl_defaults_gssapi - struct sasl_defaults_gssapi defaults; - defaults.mech = "GSSAPI"; - ldap_get_option(ld, LDAP_OPT_X_SASL_REALM, &defaults.realm); - ldap_get_option(ld, LDAP_OPT_X_SASL_AUTHCID, &defaults.authcid); - ldap_get_option(ld, LDAP_OPT_X_SASL_AUTHZID, &defaults.authzid); - defaults.passwd = NULL; - - // Perform bind operation - unsigned sasl_flags = LDAP_SASL_QUIET; - const int result_sasl = ldap_sasl_interactive_bind_s(ld, NULL,defaults.mech, NULL, NULL, sasl_flags, sasl_interact_gssapi, &defaults); - - ldap_memfree(defaults.realm); - ldap_memfree(defaults.authcid); - ldap_memfree(defaults.authzid); - if (result_sasl != LDAP_SUCCESS) { - result = AD_LDAP_ERROR; - - goto end; - } - - // NOTE: not using this for now but might need later - // The Man says: this function is used when an application needs to bind to another server in order to follow a referral or search continuation reference - // ldap_set_rebind_proc(ld, sasl_rebind_gssapi, NULL); - - end: - { - if (result == AD_SUCCESS) { - *ld_out = ld; - } else { - ldap_memfree(ld); - *ld_out = NULL; - } - - return result; - } -} - -size_t ad_array_size(char **array) { - if (array == NULL) { - return 0; - } else { - size_t count = 0; - - for (int i = 0; array[i] != NULL; i++) { - count++; - } - - return count; - } -} - -void ad_array_free(char **array) { - if (array != NULL) { - for (int i = 0; array[i] != NULL; i++) { - free(array[i]); - } - - free(array); - } -} - -int ad_add(LDAP *ld, const char *dn, const char **objectClass) { - LDAPMod attr; - attr.mod_op = LDAP_MOD_ADD; - attr.mod_type = "objectClass"; - attr.mod_values = (char **)objectClass; - - LDAPMod *attrs[] = {&attr, NULL}; - - const int result_add = ldap_add_ext_s(ld, dn, attrs, NULL, NULL); - if (result_add != LDAP_SUCCESS) { - return AD_LDAP_ERROR; - } else { - return AD_SUCCESS; - } -} - -int ad_delete(LDAP *ld, const char *dn) { - int result = AD_SUCCESS; - - const int result_delete = ldap_delete_ext_s(ld, dn, NULL, NULL); - if (result_delete != LDAP_SUCCESS) { - result = AD_LDAP_ERROR; - } - - return result; -} - -int ad_attribute_add_value(LDAP *ld, const char *dn, const char *attribute, const char *data, int data_length) { - int result = AD_SUCCESS; - - char *data_copy = strdup(data); - - struct berval ber_data; - ber_data.bv_val = data_copy; - ber_data.bv_len = data_length; - - struct berval *values[] = {&ber_data, NULL}; - - LDAPMod attr; - attr.mod_op = LDAP_MOD_ADD | LDAP_MOD_BVALUES; - attr.mod_type = (char *)attribute; - attr.mod_bvalues = values; - - LDAPMod *attrs[] = {&attr, NULL}; - - const int result_modify = ldap_modify_ext_s(ld, dn, attrs, NULL, NULL); - - if (result_modify != LDAP_SUCCESS) { - result = AD_LDAP_ERROR; - - goto end; - } - - end: - { - free(data_copy); - - return result; - } -} - -int ad_attribute_delete_value(LDAP *ld, const char *dn, const char *attribute, const char *data, const int data_length) { - int result = AD_SUCCESS; - - char *data_copy = (char *) malloc(data_length); - memcpy(data_copy, data, data_length); - - struct berval ber_data; - ber_data.bv_val = data_copy; - ber_data.bv_len = data_length; - - LDAPMod attr; - struct berval *values[] = {&ber_data, NULL}; - attr.mod_op = LDAP_MOD_DELETE | LDAP_MOD_BVALUES; - attr.mod_type = (char *)attribute; - attr.mod_bvalues = values; - - LDAPMod *attrs[] = {&attr, NULL}; - - const int result_modify = ldap_modify_ext_s(ld, dn, attrs, NULL, NULL); - if (result_modify != LDAP_SUCCESS) { - result = AD_LDAP_ERROR; - } - - free(data_copy); - - return result; -} - -int ad_rename(LDAP *ld, const char *dn, const char *new_rdn) { - int result = AD_SUCCESS; - - const int result_rename = ldap_rename_s(ld, dn, new_rdn, NULL, 1, NULL, NULL); - if (result_rename != LDAP_SUCCESS) { - result = AD_LDAP_ERROR; - } - - return result; -} - -int ad_move(LDAP *ld, const char *current_dn, const char *new_container) { - int result = AD_SUCCESS; - - char *rdn = strdup(current_dn); - - char *comma_ptr = strchr(rdn, ','); - if (comma_ptr == NULL) { - // Failed to extract RDN from DN - result = AD_INVALID_DN; - - goto end; - } - *comma_ptr = '\0'; - - const int result_rename = ldap_rename_s(ld, current_dn, rdn, new_container, 1, NULL, NULL); - if (result_rename != LDAP_SUCCESS) { - result = AD_LDAP_ERROR; - - goto end; - } - - end: - { - free(rdn); - - return result; - } -} - -/** - * Callback for ldap_sasl_interactive_bind_s - */ -int sasl_interact_gssapi(LDAP *ld, unsigned flags, void *indefaults, void *in) { - sasl_defaults_gssapi *defaults = indefaults; - sasl_interact_t *interact = (sasl_interact_t*)in; - - if (ld == NULL) { - return LDAP_PARAM_ERROR; - } - - while (interact->id != SASL_CB_LIST_END) { - const char *dflt = interact->defresult; - - switch (interact->id) { - case SASL_CB_GETREALM: - if (defaults) - dflt = defaults->realm; - break; - case SASL_CB_AUTHNAME: - if (defaults) - dflt = defaults->authcid; - break; - case SASL_CB_PASS: - if (defaults) - dflt = defaults->passwd; - break; - case SASL_CB_USER: - if (defaults) - dflt = defaults->authzid; - break; - case SASL_CB_NOECHOPROMPT: - break; - case SASL_CB_ECHOPROMPT: - break; - } - - if (dflt && !*dflt) { - dflt = NULL; - } - - /* input must be empty */ - interact->result = (dflt && *dflt) ? dflt : ""; - interact->len = strlen(interact->result); - interact++; - } - - return LDAP_SUCCESS; -} - -/** - * Perform a query for dname and output hosts - * dname is a combination of protocols (ldap, tcp), domain and site - * NOTE: this is rewritten from - * https://github.com/paleg/libadclient/blob/master/adclient.cpp - * which itself is copied from - * https://www.ccnx.org/releases/latest/doc/ccode/html/ccndc-srv_8c_source.html - * Another example of similar procedure: - * https://www.gnu.org/software/shishi/coverage/shishi/lib/resolv.c.gcov.html - */ -int query_server_for_hosts(const char *dname, char ***hosts_out) { - union dns_msg { - HEADER header; - unsigned char buf[NS_MAXMSG]; - } msg; - - int result = AD_SUCCESS; - - char **hosts = NULL; - - const int msg_len = res_search(dname, ns_c_in, ns_t_srv, msg.buf, sizeof(msg.buf)); - - if (msg_len < 0 || msg_len < sizeof(HEADER)) { - result = AD_ERROR; - - goto end; - } - - const int packet_count = ntohs(msg.header.qdcount); - const int answer_count = ntohs(msg.header.ancount); - - unsigned char *curr = msg.buf + sizeof(msg.header); - const unsigned char *eom = msg.buf + msg_len; - - // Skip over packet records - for (int i = packet_count; i > 0 && curr < eom; i--) { - const int packet_len = dn_skipname(curr, eom); - - if (packet_len < 0) { - result = AD_ERROR; - - goto end; - } - - curr = curr + packet_len + QFIXEDSZ; - } - - // Init hosts list - const size_t hosts_size = answer_count + 1; - hosts = calloc(hosts_size, sizeof(char *)); - - // Process answers by collecting hosts into list - size_t hosts_current_i = 0; - for (int i = 0; i < answer_count; i++) { - // Get server - char server[NS_MAXDNAME]; - const int server_len = dn_expand(msg.buf, eom, curr, server, sizeof(server)); - if (server_len < 0) { - result = AD_ERROR; - - goto end; - } - curr = curr + server_len; - - int record_type; - int UNUSED(record_class); - int UNUSED(ttl); - int record_len; - GETSHORT(record_type, curr); - GETSHORT(record_class, curr); - GETLONG(ttl, curr); - GETSHORT(record_len, curr); - - unsigned char *record_end = curr + record_len; - if (record_end > eom) { - result = AD_ERROR; - - goto end; - } - - // Skip non-server records - if (record_type != ns_t_srv) { - curr = record_end; - - continue; - } - - int UNUSED(priority); - int UNUSED(weight); - int UNUSED(port); - GETSHORT(priority, curr); - GETSHORT(weight, curr); - GETSHORT(port, curr); - // TODO: need to save port field? maybe to incorporate into uri - - // Get host - char host[NS_MAXDNAME]; - const int host_len = dn_expand(msg.buf, eom, curr, host, sizeof(host)); - if (host_len < 0) { - result = AD_ERROR; - - goto end; - } - - hosts[hosts_current_i] = strdup(host); - hosts_current_i++; - - curr = record_end; - } - - end: - { - if (result == AD_SUCCESS) { - *hosts_out = hosts; - } else { - *hosts_out = NULL; - ad_array_free(hosts); - } - - return result; - } -} diff --git a/src/adldap/active_directory.h b/src/adldap/active_directory.h deleted file mode 100644 index 4b21b840..00000000 --- a/src/adldap/active_directory.h +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright (c) by: Mike Dawson mike _at_ no spam gp2x.org - * - * This file may be used subject to the terms and conditions of the - * GNU Library General Public License Version 2, or any later version - * at your option, as published by the Free Software Foundation. - * 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 Library General Public License for more details. - * -**/ - -#include -#include - -#ifndef ACTIVE_DIRECTORY_H -#define ACTIVE_DIRECTORY_H 1 - -// Result codes -#define AD_SUCCESS 0 -#define AD_ERROR 1 -#define AD_LDAP_ERROR 2 -#define AD_INVALID_DN 3 - -#if defined(__cplusplus) -extern "C" { -#endif /* __cplusplus */ - -/** - * Return a result code from last LDAP operation - * When a library function returns AD_LDAP_ERROR, use this to get - * the ldap error code - */ -int ad_get_ldap_result(LDAP *ld); - -/** - * Output a list of hosts that exist for given domain and site - * list is NULL terminated - * list should be freed by the caller using ad_array_free() - * Returns AD_SUCCESS, AD_ERROR - */ -int ad_get_domain_hosts(const char *domain, const char *site, char ***hosts_out); - -/** - * Connect and authenticate to Active Directory server - * If connected succesfully saves connection handle into ds - * Returns AD_SUCCESS, AD_LDAP_ERROR - */ -int ad_connect(const char* uri, LDAP **ld_out); - -/** - * Calculate size of null-terminated array by iterating through it - */ -size_t ad_array_size(char **array); - -/** - * Free a null-terminated array that was returned by one of - * the functions in this library - * If array is NULL, nothing is done - */ -void ad_array_free(char **array); - -/** - * Adds an object with given DN and objectClass - * objectClass is a NULL terminated array of objectClass values - * All ancestors of object must already exist - * Returns AD_SUCCESS, AD_LDAP_ERROR - */ -int ad_add(LDAP *ld, const char *dn, const char **objectClass); - -/** - * Delete object - * Returns AD_SUCCESS, AD_LDAP_ERROR - */ -int ad_delete(LDAP *ld, const char *dn); - -/** - * Adds a value to given attribute - * This function works only on multi-valued attributes - * Returns AD_SUCCESS, AD_LDAP_ERROR - */ -int ad_attribute_add_value(LDAP *ld, const char *dn, const char *attribute, const char *data, int data_length); - -/** - * Remove (attribute, value) mapping from object - * If given value is NULL, remove all values of this attributes - * Returns AD_SUCCESS, AD_LDAP_ERROR - */ -int ad_attribute_delete_value(LDAP *ld, const char *dn, const char *attribute, const char *data, const int data_length); - -/** - * Rename object - * Returns AD_SUCCESS, AD_LDAP_ERROR - */ -int ad_rename(LDAP *ld, const char *dn, const char *new_rdn); - -/** - * Move object - * Returns AD_SUCCESS, AD_LDAP_ERROR - */ -int ad_move(LDAP *ld, const char *current_dn, const char *new_container); - -#if defined(__cplusplus) -} -#endif /* __cplusplus */ - -#endif /* ACTIVE_DIRECTORY_H */ diff --git a/src/adldap/test/CMakeLists.txt b/src/adldap/test/CMakeLists.txt deleted file mode 100644 index e7aadf93..00000000 --- a/src/adldap/test/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -add_mocked_test(adldap - SOURCES test_adldap.c - COMPILE_OPTIONS -g3 -I${CMAKE_CURRENT_SOURCE_DIR}/.. - LINK_LIBRARIES adldap) - diff --git a/src/adldap/test/test_adldap b/src/adldap/test/test_adldap deleted file mode 100755 index 649e3b1a6806a28836dbeb2818c7e29acdc9f6c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102384 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 diff --git a/src/adldap/test/test_adldap.c b/src/adldap/test/test_adldap.c deleted file mode 100644 index 795955ea..00000000 --- a/src/adldap/test/test_adldap.c +++ /dev/null @@ -1,46 +0,0 @@ -/* - * ADMC - AD Management Center - * - * Copyright (C) 2020 BaseALT Ltd. - * - * 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 . - */ -#include -#include -#include -#include -#include - -#include - -#include "active_directory.h" - -/* This function is not present in header files */ -size_t ad_array_size(char **array); - -static void test_ad_array_size(void **state) { - char *test_array[] = {"1", "2", "3", NULL}; - - size_t size = ad_array_size((char **)test_array); - - assert_true(size == 3); -} - -int main(void) { - const struct CMUnitTest tests[] = { - cmocka_unit_test(test_ad_array_size) - }; - - return cmocka_run_group_tests(tests, NULL, NULL); -} diff --git a/src/admc/CMakeLists.txt b/src/admc/CMakeLists.txt index 04cb3a79..8a2d917b 100644 --- a/src/admc/CMakeLists.txt +++ b/src/admc/CMakeLists.txt @@ -7,6 +7,8 @@ find_package(Qt5 REQUIRED find_package(Uuid REQUIRED) find_package(Smbclient REQUIRED) find_package(Krb5 REQUIRED) +find_package(Ldap REQUIRED) +find_package(Resolv REQUIRED) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) @@ -111,10 +113,11 @@ target_include_directories(admc target_link_libraries(admc Qt5::Core Qt5::Widgets - adldap Uuid::Uuid Smbclient::Smbclient Krb5::Krb5 + Ldap::Ldap + Resolv::Resolv ) install(TARGETS admc) diff --git a/src/admc/ad_interface.cpp b/src/admc/ad_interface.cpp index cd234ce5..f3bc827a 100644 --- a/src/admc/ad_interface.cpp +++ b/src/admc/ad_interface.cpp @@ -18,7 +18,7 @@ */ #include "ad_interface.h" -#include "active_directory.h" + #include "ad_utils.h" #include "ad_config.h" #include "ad_object.h" @@ -28,15 +28,44 @@ #include #include +#include #include #include #include +#include +#include +#include +#include #include #include #include +// NOTE: LDAP library char* inputs are non-const in the API +// but are const for practical purposes so we use forced +// casts (const char *) -> (char *) + +#ifdef __GNUC__ +# define UNUSED(x) x __attribute__((unused)) +#else +# define UNUSED(x) x +#endif + +#define MAX_DN_LENGTH 1024 +#define MAX_PASSWORD_LENGTH 255 + +typedef struct sasl_defaults_gssapi { + char *mech; + char *realm; + char *authcid; + char *passwd; + char *authzid; +} sasl_defaults_gssapi; + QList get_domain_hosts(const QString &domain, const QString &site); +QList query_server_for_hosts(const char *dname); +bool ad_connect(const char* uri, LDAP **ld_out); +int sasl_interact_gssapi(LDAP *ld, unsigned flags, void *indefaults, void *in); AdInterface *AdInterface::instance() { static AdInterface ad_interface; @@ -106,9 +135,9 @@ bool AdInterface::connect() { m_configuration_dn = "CN=Configuration," + m_domain_head; m_schema_dn = "CN=Schema," + m_configuration_dn; - const int result = ad_connect(cstr(uri), &ld); + const bool success = ad_connect(cstr(uri), &ld); - if (result == AD_SUCCESS) { + if (success) { m_config = new AdConfig(this); // TODO: can this context expire, for example from a disconnect? @@ -118,7 +147,7 @@ bool AdInterface::connect() { smbc_setOptionFallbackAfterKerberos(smbc, true); if (!smbc_init_context(smbc)) { smbc_free_context(smbc, 0); - printf("Could not initialize smbc context\n"); + qDebug() << "Could not initialize smbc context"; } smbc_set_context(smbc); @@ -334,7 +363,12 @@ QHash AdInterface::search(const QString &filter, const QList< } }; - ad_array_free(attrs); + if (attrs != NULL) { + for (int i = 0; attrs[i] != NULL; i++) { + free(attrs[i]); + } + free(attrs); + } ber_bvfree(prev_cookie); @@ -352,10 +386,11 @@ AdObject AdInterface::search_object(const QString &dn, const QList &att } bool AdInterface::attribute_replace_values(const QString &dn, const QString &attribute, const QList &values, const DoStatusMsg do_msg) { - int result = AD_SUCCESS; - const AdObject object = search_object(dn, {attribute}); const QList old_values = object.get_values(attribute); + const QString name = dn_get_name(dn); + const QString values_display = attribute_display_values(attribute, values); + const QString old_values_display = attribute_display_values(attribute, old_values); // Do nothing if both new and old values are empty if (old_values.isEmpty() && values.isEmpty()) { @@ -383,16 +418,9 @@ bool AdInterface::attribute_replace_values(const QString &dn, const QString &att LDAPMod *attrs[] = {&attr, NULL}; - const int result_modify = ldap_modify_ext_s(ld, cstr(dn), attrs, NULL, NULL); - if (result_modify != LDAP_SUCCESS) { - result = AD_LDAP_ERROR; - } + const int result = ldap_modify_ext_s(ld, cstr(dn), attrs, NULL, NULL); - const QString name = dn_get_name(dn); - const QString values_display = attribute_display_values(attribute, values); - const QString old_values_display = attribute_display_values(attribute, old_values); - - if (result == AD_SUCCESS) { + if (result == LDAP_SUCCESS) { success_status_message(QString(tr("Changed attribute \"%1\" of object \"%2\" from \"%3\" to \"%4\"")).arg(attribute, name, old_values_display, values_display), do_msg); emit object_changed(dn); @@ -421,14 +449,29 @@ bool AdInterface::attribute_replace_value(const QString &dn, const QString &attr } bool AdInterface::attribute_add_value(const QString &dn, const QString &attribute, const QByteArray &value, const DoStatusMsg do_msg) { - const int result = ad_attribute_add_value(ld, cstr(dn), cstr(attribute), value.constData(), value.size()); + char *data_copy = (char *) malloc(value.size()); + memcpy(data_copy, value.constData(), value.size()); + + struct berval ber_data; + ber_data.bv_val = data_copy; + ber_data.bv_len = value.size(); + + struct berval *values[] = {&ber_data, NULL}; + + LDAPMod attr; + attr.mod_op = LDAP_MOD_ADD | LDAP_MOD_BVALUES; + attr.mod_type = (char *)cstr(attribute); + attr.mod_bvalues = values; + + LDAPMod *attrs[] = {&attr, NULL}; + + const int result = ldap_modify_ext_s(ld, cstr(dn), attrs, NULL, NULL); + free(data_copy); const QString name = dn_get_name(dn); - const QString new_display_value = attribute_display_value(attribute, value); - ; - if (result == AD_SUCCESS) { + if (result == LDAP_SUCCESS) { const QString context = QString(tr("Added value \"%1\" for attribute \"%2\" of object \"%3\"")).arg(new_display_value, attribute, name); success_status_message(context, do_msg); @@ -446,13 +489,28 @@ bool AdInterface::attribute_add_value(const QString &dn, const QString &attribut } bool AdInterface::attribute_delete_value(const QString &dn, const QString &attribute, const QByteArray &value, const DoStatusMsg do_msg) { - const int result = ad_attribute_delete_value(ld, cstr(dn), cstr(attribute), value.constData(), value.size()); - const QString name = dn_get_name(dn); - const QString value_display = attribute_display_value(attribute, value); - if (result == AD_SUCCESS) { + char *data_copy = (char *) malloc(value.size()); + memcpy(data_copy, value.constData(), value.size()); + + struct berval ber_data; + ber_data.bv_val = data_copy; + ber_data.bv_len = value.size(); + + LDAPMod attr; + struct berval *values[] = {&ber_data, NULL}; + attr.mod_op = LDAP_MOD_DELETE | LDAP_MOD_BVALUES; + attr.mod_type = (char *)cstr(attribute); + attr.mod_bvalues = values; + + LDAPMod *attrs[] = {&attr, NULL}; + + const int result = ldap_modify_ext_s(ld, cstr(dn), attrs, NULL, NULL); + free(data_copy); + + if (result == LDAP_SUCCESS) { const QString context = QString(tr("Deleted value \"%1\" for attribute \"%2\" of object \"%3\"")).arg(value_display, attribute, name); success_status_message(context, do_msg); @@ -492,9 +550,16 @@ bool AdInterface::attribute_replace_datetime(const QString &dn, const QString &a bool AdInterface::object_add(const QString &dn, const QString &object_class) { const char *classes[2] = {cstr(object_class), NULL}; - const int result = ad_add(ld, cstr(dn), classes); + LDAPMod attr; + attr.mod_op = LDAP_MOD_ADD; + attr.mod_type = (char *) "objectClass"; + attr.mod_values = (char **) classes; - if (result == AD_SUCCESS) { + LDAPMod *attrs[] = {&attr, NULL}; + + const int result = ldap_add_ext_s(ld, cstr(dn), attrs, NULL, NULL); + + if (result == LDAP_SUCCESS) { success_status_message(QString(tr("Created object \"%1\"")).arg(dn)); emit object_added(dn); @@ -510,11 +575,11 @@ bool AdInterface::object_add(const QString &dn, const QString &object_class) { } bool AdInterface::object_delete(const QString &dn) { - int result = ad_delete(ld, cstr(dn)); - const QString name = dn_get_name(dn); - if (result == AD_SUCCESS) { + const int result = ldap_delete_ext_s(ld, cstr(dn), NULL, NULL); + + if (result == LDAP_SUCCESS) { success_status_message(QString(tr("Deleted object \"%1\"")).arg(name)); emit object_deleted(dn); @@ -530,18 +595,14 @@ bool AdInterface::object_delete(const QString &dn) { } bool AdInterface::object_move(const QString &dn, const QString &new_container) { - QList dn_split = dn.split(','); - QString new_dn = dn_split[0] + "," + new_container; - - const int result = ad_move(ld, cstr(dn), cstr(new_container)); - - // TODO: drag and drop handles checking move compatibility but need - // to do this here as well for CLI? - + const QString rdn = dn.split(',')[0]; + const QString new_dn = rdn + "," + new_container; const QString object_name = dn_get_name(dn); const QString container_name = dn_get_name(new_container); - if (result == AD_SUCCESS) { + const int result = ldap_rename_s(ld, cstr(dn), cstr(rdn), cstr(new_container), 1, NULL, NULL); + + if (result == LDAP_SUCCESS) { success_status_message(QString(tr("Moved object \"%1\" to \"%2\"")).arg(object_name, container_name)); emit object_deleted(dn); @@ -560,12 +621,11 @@ bool AdInterface::object_move(const QString &dn, const QString &new_container) { bool AdInterface::object_rename(const QString &dn, const QString &new_name) { const QString new_dn = dn_rename(dn, new_name); const QString new_rdn = new_dn.split(",")[0]; - - int result = ad_rename(ld, cstr(dn), cstr(new_rdn)); - const QString old_name = dn_get_name(dn); - if (result == AD_SUCCESS) { + const int result = ldap_rename_s(ld, cstr(dn), cstr(new_rdn), NULL, 1, NULL, NULL); + + if (result == LDAP_SUCCESS) { success_status_message(QString(tr("Renamed object \"%1\" to \"%2\"")).arg(old_name, new_name)); emit object_deleted(dn); @@ -739,7 +799,7 @@ bool AdInterface::user_set_pass(const QString &dn, const QString &password) { const QString error = [this]() { - const int ldap_result = ad_get_ldap_result(ld); + const int ldap_result = get_ldap_result(); if (ldap_result == LDAP_CONSTRAINT_VIOLATION) { return tr("Password doesn't match rules"); } else { @@ -1101,7 +1161,7 @@ void AdInterface::success_status_message(const QString &msg, const DoStatusMsg d return; } - STATUS()->message(msg, StatusType_Success); + Status::instance()->message(msg, StatusType_Success); } void AdInterface::error_status_message(const QString &context, const QString &error, const DoStatusMsg do_msg) { @@ -1114,11 +1174,11 @@ void AdInterface::error_status_message(const QString &context, const QString &er msg += QString(tr(". Error: \"%1\"")).arg(error);; } - STATUS()->message(msg, StatusType_Error); + Status::instance()->message(msg, StatusType_Error); } QString AdInterface::default_error() const { - const int ldap_result = ad_get_ldap_result(ld); + const int ldap_result = get_ldap_result(); switch (ldap_result) { case LDAP_NO_SUCH_OBJECT: return tr("No such object"); case LDAP_CONSTRAINT_VIOLATION: return tr("Constraint violation"); @@ -1132,25 +1192,263 @@ QString AdInterface::default_error() const { } } +int AdInterface::get_ldap_result() const { + int result; + ldap_get_option(ld, LDAP_OPT_RESULT_CODE, &result); + + return result; +} + AdInterface *AD() { return AdInterface::instance(); } QList get_domain_hosts(const QString &domain, const QString &site) { - char **hosts_raw = NULL; - int hosts_result = ad_get_domain_hosts(cstr(domain), cstr(site), &hosts_raw); + QList hosts; - if (hosts_result == AD_SUCCESS) { - auto hosts = QList(); + // TODO: confirm site query is formatted properly, currently getting no answer back (might be working as intended, since tested on domain without sites?) - for (int i = 0; hosts_raw[i] != NULL; i++) { - auto host = QString(hosts_raw[i]); - hosts.append(host); - } - ad_array_free(hosts_raw); + // Query site hosts + if (!site.isEmpty()) { + char dname[1000]; + snprintf(dname, sizeof(dname), "_ldap._tcp.%s._sites.%s", cstr(site), cstr(domain)); - return hosts; - } else { - return QList(); + const QList site_hosts = query_server_for_hosts(dname); + hosts.append(site_hosts); } + + // Query default hosts + char dname_default[1000]; + snprintf(dname_default, sizeof(dname_default), "_ldap._tcp.%s", cstr(domain)); + + const QList default_hosts = query_server_for_hosts(dname_default); + hosts.append(default_hosts); + + hosts.removeDuplicates(); + + return hosts; +} + +/** + * Perform a query for dname and output hosts + * dname is a combination of protocols (ldap, tcp), domain and site + * NOTE: this is rewritten from + * https://github.com/paleg/libadclient/blob/master/adclient.cpp + * which itself is copied from + * https://www.ccnx.org/releases/latest/doc/ccode/html/ccndc-srv_8c_source.html + * Another example of similar procedure: + * https://www.gnu.org/software/shishi/coverage/shishi/lib/resolv.c.gcov.html + */ +QList query_server_for_hosts(const char *dname) { + union dns_msg { + HEADER header; + unsigned char buf[NS_MAXMSG]; + } msg; + + auto error = + []() { + return QList(); + }; + + const long unsigned msg_len = res_search(dname, ns_c_in, ns_t_srv, msg.buf, sizeof(msg.buf)); + + const bool message_error = (msg_len < 0 || msg_len < sizeof(HEADER)); + if (message_error) { + error(); + } + + const int packet_count = ntohs(msg.header.qdcount); + const int answer_count = ntohs(msg.header.ancount); + + unsigned char *curr = msg.buf + sizeof(msg.header); + const unsigned char *eom = msg.buf + msg_len; + + // Skip over packet records + for (int i = packet_count; i > 0 && curr < eom; i--) { + const int packet_len = dn_skipname(curr, eom); + + const bool packet_error = (packet_len < 0); + if (packet_error) { + error(); + } + + curr = curr + packet_len + QFIXEDSZ; + } + + QList hosts; + + // Process answers by collecting hosts into list + for (int i = 0; i < answer_count; i++) { + // Get server + char server[NS_MAXDNAME]; + const int server_len = dn_expand(msg.buf, eom, curr, server, sizeof(server)); + + const bool server_error = (server_len < 0); + if (server_error) { + error(); + } + + curr = curr + server_len; + + int record_type; + int UNUSED(record_class); + int UNUSED(ttl); + int record_len; + GETSHORT(record_type, curr); + GETSHORT(record_class, curr); + GETLONG(ttl, curr); + GETSHORT(record_len, curr); + + unsigned char *record_end = curr + record_len; + if (record_end > eom) { + error(); + } + + // Skip non-server records + if (record_type != ns_t_srv) { + curr = record_end; + + continue; + } + + int UNUSED(priority); + int UNUSED(weight); + int UNUSED(port); + GETSHORT(priority, curr); + GETSHORT(weight, curr); + GETSHORT(port, curr); + // TODO: need to save port field? maybe to incorporate into uri + + // Get host + char host[NS_MAXDNAME]; + const int host_len = dn_expand(msg.buf, eom, curr, host, sizeof(host)); + const bool host_error = (host_len < 0); + if (host_error) { + error(); + } + + hosts.append(QString(host)); + + curr = record_end; + } + + return hosts; +} + +bool ad_connect(const char* uri, LDAP **ld_out) { + int result; + LDAP *ld = NULL; + + result = ldap_initialize(&ld, uri); + if (result != LDAP_SUCCESS) { + ldap_memfree(ld); + return false; + } + + // Set version + const int version = LDAP_VERSION3; + result = ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &version); + if (result != LDAP_OPT_SUCCESS) { + ldap_memfree(ld); + return false; + } + + // Disable referrals + result =ldap_set_option(ld, LDAP_OPT_REFERRALS, LDAP_OPT_OFF); + if (result != LDAP_OPT_SUCCESS) { + ldap_memfree(ld); + return false; + } + + // Set maxssf + const char* sasl_secprops = "maxssf=56"; + result = ldap_set_option(ld, LDAP_OPT_X_SASL_SECPROPS, sasl_secprops); + if (result != LDAP_SUCCESS) { + ldap_memfree(ld); + return false; + } + + ldap_set_option(ld, LDAP_OPT_X_SASL_NOCANON, LDAP_OPT_ON); + + // TODO: add option to turn off + ldap_set_option(ld, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER); + + // Setup sasl_defaults_gssapi + struct sasl_defaults_gssapi defaults; + defaults.mech = (char *)"GSSAPI"; + ldap_get_option(ld, LDAP_OPT_X_SASL_REALM, &defaults.realm); + ldap_get_option(ld, LDAP_OPT_X_SASL_AUTHCID, &defaults.authcid); + ldap_get_option(ld, LDAP_OPT_X_SASL_AUTHZID, &defaults.authzid); + defaults.passwd = NULL; + + // Perform bind operation + unsigned sasl_flags = LDAP_SASL_QUIET; + result = ldap_sasl_interactive_bind_s(ld, NULL,defaults.mech, NULL, NULL, sasl_flags, sasl_interact_gssapi, &defaults); + ldap_memfree(defaults.realm); + ldap_memfree(defaults.authcid); + ldap_memfree(defaults.authzid); + if (result != LDAP_SUCCESS) { + ldap_memfree(ld); + return false; + } + + // NOTE: not using this for now but might need later + // The Man says: this function is used when an application needs to bind to another server in order to follow a referral or search continuation reference + // ldap_set_rebind_proc(ld, sasl_rebind_gssapi, NULL); + + if (ld_out != NULL) { + *ld_out = ld; + } + + return true; +} + +/** + * Callback for ldap_sasl_interactive_bind_s + */ +int sasl_interact_gssapi(LDAP *ld, unsigned flags, void *indefaults, void *in) { + sasl_defaults_gssapi *defaults = (sasl_defaults_gssapi *) indefaults; + sasl_interact_t *interact = (sasl_interact_t*)in; + + if (ld == NULL) { + return LDAP_PARAM_ERROR; + } + + while (interact->id != SASL_CB_LIST_END) { + const char *dflt = interact->defresult; + + switch (interact->id) { + case SASL_CB_GETREALM: + if (defaults) + dflt = defaults->realm; + break; + case SASL_CB_AUTHNAME: + if (defaults) + dflt = defaults->authcid; + break; + case SASL_CB_PASS: + if (defaults) + dflt = defaults->passwd; + break; + case SASL_CB_USER: + if (defaults) + dflt = defaults->authzid; + break; + case SASL_CB_NOECHOPROMPT: + break; + case SASL_CB_ECHOPROMPT: + break; + } + + if (dflt && !*dflt) { + dflt = NULL; + } + + /* input must be empty */ + interact->result = (dflt && *dflt) ? dflt : ""; + interact->len = strlen((const char *) interact->result); + interact++; + } + + return LDAP_SUCCESS; } diff --git a/src/admc/ad_interface.h b/src/admc/ad_interface.h index 367a665d..e6507565 100644 --- a/src/admc/ad_interface.h +++ b/src/admc/ad_interface.h @@ -29,8 +29,9 @@ #include /** - * Interface to AD server that provides a way to search for - * objects and modify them. + * Interface to AD server. Provides a way to search and + * modify objects . Success and error messages resulting + * from operations are sent to Status. */ enum SearchScope { @@ -133,6 +134,7 @@ private: void success_status_message(const QString &msg, const DoStatusMsg do_msg = DoStatusMsg_Yes); void error_status_message(const QString &context, const QString &error, const DoStatusMsg do_msg = DoStatusMsg_Yes); QString default_error() const; + int get_ldap_result() const; AdInterface(const AdInterface&) = delete; AdInterface& operator=(const AdInterface&) = delete;