From: Radoslaw Korzeniewski Date: Tue, 13 Oct 2020 10:04:57 +0000 (+0200) Subject: Add BConsole Authentication Plugin framework and a LDAP Plugin. X-Git-Tag: Release-11.3.2~924 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d0afa77f1bd52445f923d87e887a8198df6e4c69;p=thirdparty%2Fbacula.git Add BConsole Authentication Plugin framework and a LDAP Plugin. --- diff --git a/bacula/Makefile.in b/bacula/Makefile.in index 10fb7e7f9..3772c4a93 100755 --- a/bacula/Makefile.in +++ b/bacula/Makefile.in @@ -25,7 +25,7 @@ fd_subdirs = src scripts src/lib src/findlib src/filed \ @READLINE_SRC@ @BAT_DIR@ src/console @FD_PLUGIN_DIR@ # Non-client-only directores -subdirs = src/cats @DIRD_DIR@ @STORED_DIR@ src/tools +subdirs = src/cats @DIRD_DIR@ @STORED_DIR@ src/tools @DIR_PLUGIN_DIR@ all_subdirs = ${fd_subdirs} ${@ALL_DIRS@} manpages diff --git a/bacula/src/console/authenticate.c b/bacula/src/console/authenticate.c index 9331cd2de..4157a2a63 100644 --- a/bacula/src/console/authenticate.c +++ b/bacula/src/console/authenticate.c @@ -39,11 +39,14 @@ * Community * prior to 06Aug13 no version * 100 14Feb17 - added comm line compression + * + * Starting from version 9.7 both Hellos become the same: + * 200 1/10/2020 - added `auth interactive` functionality */ #ifndef COMMUNITY #define UA_VERSION 1 /* Enterprise */ #else -#define UA_VERSION 100 /* Community */ +#define UA_VERSION 200 /* Community */ #endif void senditf(const char *fmt, ...); @@ -69,9 +72,9 @@ public: virtual bool CheckTLSRequirement(); int authenticate_director(DIRRES *director, CONRES *cons); + bool ClientAuthenticate(CONRES *cons, const char *password); }; - int authenticate_director(BSOCK *dir, DIRRES *director, CONRES *cons) { ConsoleAuthenticate auth(dir); @@ -97,6 +100,117 @@ bool ConsoleAuthenticate::CheckTLSRequirement() return true; } +bool ConsoleAuthenticate::ClientAuthenticate(CONRES *cons, const char *password) +{ + BSOCK *dir = bsock; + bool legacy = true; // by default we should execute legacy authentication + POOL_MEM msg(PM_MESSAGE); + + // at first handle optional startTLS + if (!ClientEarlyTLS()) { + return false; + } + + // auth interactive is handled in named console only + if (cons){ + // if starttls does not left our auth interactive command get it from director + if (!check_early_tls){ + if (dir->recv() <= 0) { + bmicrosleep(5, 0); // original cram_md5_respond() wait for 5s here + return false; + } + } + + // temporary buffer + pm_strcpy(msg, dir->msg); + + // the check_early_tls=false means legacy authentication should do standard recv() else it can get command already in the bsock->msg + check_early_tls = true; + + if (strncmp(msg.c_str(), UA_AUTH_INTERACTIVE, strlen(UA_AUTH_INTERACTIVE)) == 0){ + // do challenge/response chatting + check_early_tls = false; // "tell" cram_md5_respond to do a recv() + legacy = false; // we do the authentication + + // first display a welcome string if available + char *welcome = msg.c_str() + strlen(UA_AUTH_INTERACTIVE) + 1; + if (strlen(welcome) > 2){ + sendit("> "); + sendit(welcome); + } + + for (;;){ + // loop over operations + if (dir->recv() <= 0) { + bmicrosleep(5, 0); // original cram_md5_respond() wait for 5s here + return false; + } + + pm_strcpy(msg, NULL); + pm_strcpy(msg, dir->msg + 1); + strip_trailing_junk(msg.c_str()); + + char *data = msg.c_str(); + POOL_MEM buf(PM_MESSAGE); + char *passwd; + char *input; + + // the command is encoded as a first char of the message + switch (dir->msg[0]){ + case UA_AUTH_INTERACTIVE_PLAIN: + sendit(data); + input = fgets(buf.c_str(), buf.size(), stdin); + if (!input){ + Dmsg0(1, "Error reading user input!\n"); + return false; + } + + // now we should return it to director + strip_trailing_junk(buf.c_str()); + dir->fsend("%s", buf.c_str()); + break; + + case UA_AUTH_INTERACTIVE_HIDDEN: +#if defined(HAVE_WIN32) + sendit(data); + if (win32_cgets(buf.c_str(), buf.size()) == NULL) { + buf[0] = 0; + return 0; + } else { + return strlen(buf); + } +#else + passwd = getpass(data); + bstrncpy(buf.c_str(), passwd, buf.size()); +#endif + // now we should get a hidden response at `buf` class, return it to director + dir->fsend("%s", buf.c_str()); + break; + + case UA_AUTH_INTERACTIVE_MESSAGE: + sendit(data); + sendit("\n"); + break; + + case UA_AUTH_INTERACTIVE_FINISH: + return true; + + default: + bmicrosleep(5, 0); // original cram_md5_respond() wait for 5s here + return false; + } + } + } + } + + /* standard md5 handle */ + if (legacy && !ClientCramMD5AuthenticateBase(password)) { + return false; + } + + return true; +} + /* * Authenticate Director */ @@ -106,6 +220,7 @@ int ConsoleAuthenticate::authenticate_director(DIRRES *director, CONRES *cons) int dir_version = 0; char bashed_name[MAX_NAME_LENGTH]; bool skip_msg = false; + /* * Send my name to the Director then do authentication */ @@ -128,7 +243,7 @@ int ConsoleAuthenticate::authenticate_director(DIRRES *director, CONRES *cons) dir->fsend(hello, bashed_name, UA_VERSION, tlspsk_local_need); - if (!ClientCramMD5Authenticate(password)) { + if (!ClientAuthenticate(cons, password)){ if (dir->is_timed_out()) { sendit(_("The Director is busy or the MaximumConsoleConnections limit is reached.\n")); skip_msg = true; diff --git a/bacula/src/dird/Makefile.in b/bacula/src/dird/Makefile.in index 815318e61..db4060272 100644 --- a/bacula/src/dird/Makefile.in +++ b/bacula/src/dird/Makefile.in @@ -34,7 +34,8 @@ dummy: # SVRSRCS = dird.c admin.c authenticate.c \ autoprune.c backup.c bsr.c \ - catreq.c dir_plugins.c dird_conf.c expand.c \ + catreq.c dir_plugins.c dir_authplugin.c \ + dird_conf.c expand.c \ fd_cmds.c getmsg.c inc_conf.c job.c \ jobq.c mac.c mac_sql.c \ mountreq.c msgchan.c next_vol.c newvol.c \ diff --git a/bacula/src/dird/authenticate.c b/bacula/src/dird/authenticate.c index c3b2ee147..e40fdf49b 100644 --- a/bacula/src/dird/authenticate.c +++ b/bacula/src/dird/authenticate.c @@ -29,6 +29,7 @@ #include "bacula.h" #include "dird.h" +#include "dir_authplugin.h" static const int dbglvl = 50; @@ -235,6 +236,7 @@ public: } int authenticate_user_agent(); + bool authenticate_with_plugin(CONRES * cons); }; int authenticate_user_agent(UAContext *uac) @@ -254,6 +256,7 @@ int UAAuthenticate::authenticate_user_agent() int tlspsk_remote = 0; bool fdcallsdir=false; CLIENT *cli=NULL; + bool legacy_auth = true; if (ua->msglen < 16 || ua->msglen >= MAX_NAME_LENGTH + 15) { Qmsg3(NULL, M_SECURITY, 0, _("UA Hello from %s:%s is invalid. Len=%d\n"), ua->who(), @@ -323,13 +326,23 @@ int UAAuthenticate::authenticate_user_agent() } DecodeRemoteTLSPSKNeed(tlspsk_remote); - if (!ServerCramMD5Authenticate(password)) { - goto auth_done; - } - - if (cons) { + // if user console + if (cons){ uac->cons = cons; /* save console resource pointer */ bstrncpy(uac->name, uac->cons->hdr.name, sizeof(uac->name)); + + // require authentication and user agent is on proper version + if (cons->authenticationplugin){ + legacy_auth = false; + Dmsg1(dbglvl, "authenticate with Plugin=%s\n", cons->authenticationplugin); + if (ua_version < UA_VERSION_PLUGINAUTH || !authenticate_with_plugin(cons)){ + goto auth_done; + } + } + } + + if (legacy_auth && !ServerCramMD5Authenticate(password)) { + goto auth_done; } this->auth_success = HandleTLS(); @@ -359,3 +372,65 @@ auth_done: } return 1; } + +/** + * @brief This function perform user authentication procedure augmented with Auth Plugin API. + * All bconsole network chatting and interaction is forwarded to dir_authplugins module + * and we just provide a required frameworka dn resources, i.e. jcr or bsock to ua. + * + * @param cons a Console resource required for auth plugin registration + * @return true when authentication process finish with success and we can proceed next operations + * @return false when some error raised or authentication was unsuccessful + */ +bool UAAuthenticate::authenticate_with_plugin(CONRES * cons) +{ + /* + * The user authentication procedure with Auth Plugins API follow like this: + * 1. get bDirAuthenticationRegister data which defines what and how we should proceed with authentication + * 2. if auth operation is not bDirAuthenticationOperationPluginAll (which points that you have to ask plugin for every auth operation you execute) + * then iterate over returned data + * 3. the iteration goes like this: + * a. send question to console or simple message do display if operation is bDirAuthenticationOperationMessage + * b. receive response from console + * c. forward response to plugin with plugin event + * 4. when all interactions were handled without problem then do authenticate with plugin + * 5. if plugin return authentication OK? return true, Not? return false + */ + + bDirAuthenticationRegister *authData; + + authData = (bDirAuthenticationRegister*) dir_authplugin_getauthenticationData(uac->jcr, cons->authenticationplugin); + if (authData == NULL) + { + return false; + } + + // do tls before real auth + if (!ServerEarlyTLS()) + { + return false; + } + + // send auth plugin start packet and optional welcome string to console + Dmsg1(dbglvl, "send: auth interactive %s\n", NPRT(authData->welcome)); + if (!bsock->fsend("auth interactive %s\n", NPRTB(authData->welcome))) { + Dmsg1(dbglvl, "Send interactive start comm error. ERR=%s\n", bsock->bstrerror()); + return false; + } + + // now a console should know how to deal with it, so iterate over auth data + const bDirAuthenticationData *data = authData->data; + + for (uint i = 0; i < authData->num; i++){ + Dmsg1(dbglvl, "bDirAuthenticationData step %d\n", i); + if (dir_authplugin_do_interaction(uac->jcr, bsock, authData->name, (void *)&data[i]) != bRC_OK){ + return false; + } + } + + if (dir_authplugin_authenticate(uac->jcr, bsock, authData->name) != bRC_OK){ + return false; + } + + return true; +} \ No newline at end of file diff --git a/bacula/src/dird/dir_authplugin.c b/bacula/src/dird/dir_authplugin.c new file mode 100644 index 000000000..105c52514 --- /dev/null +++ b/bacula/src/dird/dir_authplugin.c @@ -0,0 +1,338 @@ +/* + Bacula(R) - The Network Backup Solution + + Copyright (C) 2000-2020 Kern Sibbald + + The original author of Bacula is Kern Sibbald, with contributions + from many others, a complete list can be found in the file AUTHORS. + + You may use this file and others of this release according to the + license defined in the LICENSE file, which includes the Affero General + Public License, v3.0 ("AGPLv3") and some additional permissions and + terms pursuant to its AGPLv3 Section 7. + + This notice must be preserved when any source code is + conveyed and/or propagated. + + Bacula(R) is a registered trademark of Kern Sibbald. +*/ +/* + * Bacula authentication and authorization plugin framework. + * + * Author: Radosław Korzeniewski, MMXX + * radoslaw@korzeniewski.net, radekk@inteos.pl + * Inteos Sp. z o.o. http://www.inteos.pl/ + */ + +#include "bacula.h" +#include "dird.h" +#include "dir_authplugin.h" + +const int dbglvl = 500; + +// borrow a constant from main dir_plugins.c +extern const char *plugin_type; + +/** + * @brief Prepares a consistent plugin name based on a differently formatted input strings. + * First it checks for a plugin file name which ends with '-dir.so' defined in dir_plugins.c + * Next it checks for plugin name colon ':' delimiter in plugin parameter string. + * Finally it just copies the whole string as a plugin name. + * + * @param plugin a plugin name source string + * @return char* a prepared plugin name as a newly allocated string which caller has to free + */ +static char * prepare_authplugin_name(const char * plugin) +{ + char *buf = bstrdup(plugin); + char *p; + + p = strstr(buf, plugin_type); + + if (!p){ + p = strchr(buf, ':'); + } + + if (p){ + // prepare plugin name + *p = '\0'; + } + + return buf; +} + +/** + * @brief Checks if selected `Plugin` is defined in Authentication Plugin parameter from `param`. + * It is checking a plugin filename with console resource parameter string. + * + * @param plugin the `Plugin` class to check + * @param param the Authentication Plugin parameter from console resource + * @return true when it is an authentication plugin which matches request + * @return false when it is something other + */ +static bool is_authentication_plugin(Plugin *plugin, const char *param) +{ + char *pluginfile = prepare_authplugin_name(plugin->file); + char *pluginname = prepare_authplugin_name(param); + bool rc = 0; + + if (pluginfile){ + if (pluginname){ + Dmsg2(dbglvl, "plugin: %s reqested: %s\n", pluginfile, pluginname); + if (strncmp(pluginfile, pluginname, strlen(pluginname)) == 0 && + dirplug_func(plugin)->getPluginAuthenticationData){ + rc = true; + } + free(pluginname); + } + free(pluginfile); + } + + return rc; +} + +/** + * @brief Generate a director auth plugin (BPAM) event. + * The event is directed to a single plugin only as it could include sensitive authentication + * data which should not be propagated to all. It is a main reason why it is a different + * function and we do not use `generate_plugin_event()` from dir_plugins.c + * It currently handle authentication plugins only, but it could change in the future. + * + * @param jcr job control record + * @param pluginname a name of the plugin which should handle the event + * @param eventType an event to generate + * @param value a generic value passed to plugin which handles an event + * @return bRC it could return a following values: + * - bRC_Max when no plugins loaded + * - bRC_Cancel - when job is canceled + * - bRC_OK - on success, propagated directly from plugin handling function + * - any other on error, propagated directly from plugin handling function + */ +static bRC dir_authplugin_generate_plugin_event(JCR *jcr, const char * pluginname, bDirEventsType eventType, void *value) +{ + bpContext *plugin_ctx; + bDirEvent event; + Plugin *plugin; + int i = 0; + bRC rc = bRC_OK; + + if (!b_plugin_list || !jcr || !jcr->plugin_ctx_list) { + return bRC_Max; // Return if no plugins loaded. + } + + if (jcr->is_job_canceled()) { + return bRC_Cancel; + } + + bpContext *plugin_ctx_list = (bpContext *)jcr->plugin_ctx_list; + event.eventType = eventType; + + foreach_alist_index(i, plugin, b_plugin_list) { + plugin_ctx = &plugin_ctx_list[i]; + if (!is_authentication_plugin(plugin, pluginname)) { + continue; + } + if (dirplug_func(plugin)->handlePluginEvent){ + rc = dirplug_func(plugin)->handlePluginEvent(plugin_ctx, &event, value); + } + break; + } + + return rc; +} + +/** + * @brief Return Authentication Data struct from selected plugin. + * The Plugin should use Authentication Plugin parameter string from `param` variable + * to prepare approaching authentication operations (events). + * The returned struct (`bDirAuthenticationRegister`) will be used by Director to + * perform required authentication operations. + * + * @param jcr job control record + * @param param a Console resource Authentication Plugin parameter string + * @return void* a `NULL` is returned on any error and a pointer to bDirAuthenticationRegister + * struct (casted to void*) on success + */ +void * dir_authplugin_getauthenticationData(JCR *jcr, const char * param) +{ + bpContext *plugin_ctx; + Plugin *plugin; + int i = 0; + bDirAuthenticationRegister *data = NULL; + + if (!b_plugin_list || !jcr || !jcr->plugin_ctx_list) { + return NULL; /* Return if no plugins loaded */ + } + + if (jcr->is_job_canceled()) { + return NULL; + } + + bpContext *plugin_ctx_list = (bpContext *)jcr->plugin_ctx_list; + + foreach_alist_index(i, plugin, b_plugin_list) { + plugin_ctx = &plugin_ctx_list[i]; + if (!is_authentication_plugin(plugin, param)) { + continue; + } + + if (dirplug_func(plugin)->getPluginAuthenticationData(plugin_ctx, param, (void**)&data) != bRC_OK){ + // getting authdata operations failed + return NULL; + } + + // check if data returned are valid + if (data->name == NULL || + data->data == NULL || + data->num == 0){ + Dmsg1(1, "Invalid bDirAuthenticationRegister data for %s\n", plugin->file); + return NULL; + } + + break; + } + + return (void*)data; +} + +/** + * @brief A main function to sending a message to bconsole using a valid `bsock`. + * + * @param bsock the `BSOCK` object for Director <-> bconsole communication + * @param type the message type to send + * @param message a message string to sent + * @return bRC bRC_OK on success, bRC_Error on any error + */ +static bRC dir_authplugin_display_message(BSOCK *bsock, const char type, const char *message) +{ + // display message + if (!bsock->fsend("%c%s\n", type, message)) { + Dmsg1(dbglvl, "Send auth message comm error. ERR=%s\n", bsock->bstrerror()); + return bRC_Error; + } + return bRC_OK; +} + +/** + * @brief Handles a response (data receive) from bconsole using a valid `bsock`. + * + * @param jcr job control record + * @param bsock the `BSOCK` object for Director <-> bconsole communication + * @param pluginname a name of the plugin which should receive an resoinse with plugin event + * @param seqdata the `seqdata` variable for current authentication operation (basically from bDirAuthenticationData struct) + * @return bRC bRC_OK on success, bRC_Error on any error + */ +static bRC dir_authplugin_handle_response(JCR *jcr, BSOCK *bsock, const char *pluginname, const uint32_t seqdata) +{ + bDirAuthValue value; + + // get response + if (bsock->wait_data(180) <= 0 || bsock->recv() <= 0) { + Dmsg1(dbglvl, "Receive auth response comm error. ERR=%s\n", bsock->bstrerror()); + bmicrosleep(5, 0); + return bRC_Error; + } + + // forward response to plugin + value.seqdata = seqdata; + value.response = bsock->msg; + return dir_authplugin_generate_plugin_event(jcr, pluginname, bDirEventAuthenticationResponse, (void*)&value); +} + +/** + * @brief Perform a final BPAM authenticate with a Plugin. + * + * @param jcr job control record + * @param bsock the `BSOCK` object for Director <-> bconsole communication + * @param pluginname a name of the plugin which should handle authentication + * @return bRC bRC_OK on success - authentication OK, bRC_Error on any error + */ +bRC dir_authplugin_authenticate(JCR *jcr, BSOCK *bsock, const char *pluginname) +{ + // sanity check + if (!jcr || !bsock || !pluginname){ + Dmsg3(1, "Invalid input parameters: %p %p %p %p\n", jcr, bsock, pluginname); + return bRC_Error; + } + + if (dir_authplugin_display_message(bsock, UA_AUTH_INTERACTIVE_FINISH, "") != bRC_OK){ + return bRC_Error; + } + + return dir_authplugin_generate_plugin_event(jcr, pluginname, bDirEventAuthenticate, NULL); +} + +/** + * @brief Perform BPAM authentication interaction with a user/bconsole nased on authentication data operation + * + * @param jcr job control record + * @param bsock the `BSOCK` object for Director <-> bconsole communication + * @param pluginname a name of the plugin which should handle authentication interaction + * @param data a pointer to the bDirAuthenticationData single authentication operation + * @param pluginall if set to true then we are called from a recursive bDirAuthenticationOperationPluginAll + * @return bRC bRC_OK on success - operation OK, bRC_Error on any error + */ +bRC dir_authplugin_do_interaction(JCR *jcr, BSOCK *bsock, const char *pluginname, void *data, bool pluginall) +{ + bDirAuthenticationData *authData = (bDirAuthenticationData *)data; + bDirAuthValue value; + + // sanity check + if (!jcr || !bsock || !pluginname || !data){ + Dmsg4(1, "Invalid input parameters: %p %p %p %p\n", jcr, bsock, pluginname, data); + return bRC_Error; + } + + switch (authData->operation){ + case bDirAuthenticationOperationPlugin: + // ask plugin about operation to execute + value.seqdata = authData->seqdata; + if (dir_authplugin_generate_plugin_event(jcr, pluginname, bDirEventAuthenticationQuestion, (void *)&value) != bRC_OK){ + // error + Dmsg1(dbglvl, "Error getting question from plugin: %s\n", pluginname); + return bRC_Error; + } + return dir_authplugin_do_interaction(jcr, bsock, pluginname, (void*)value.authdata, pluginall); + + case bDirAuthenticationOperationPluginAll: + // we will handle all interaction if no bDirAuthenticationOperationPluginAll was generated before + if (!pluginall){ + while (dir_authplugin_generate_plugin_event(jcr, pluginname, bDirEventAuthenticationQuestion, (void *)&value) == bRC_OK){ + // do interaction with operation from plugin and mark it is during pluginall operation + if (dir_authplugin_do_interaction(jcr, bsock, pluginname, (void*)value.authdata, true) != bRC_OK){ + return bRC_Error; + } + } + } else { + // we do not support bDirAuthenticationOperationPluginAll during bDirAuthenticationOperationPluginAll operation + return bRC_Error; + } + break; + + case bDirAuthenticationOperationMessage: + // display message + return dir_authplugin_display_message(bsock, UA_AUTH_INTERACTIVE_MESSAGE, authData->question); + + case bDirAuthenticationOperationPlain: + // display message + if (dir_authplugin_display_message(bsock, UA_AUTH_INTERACTIVE_PLAIN, authData->question) != bRC_OK){ + return bRC_Error; + } + return dir_authplugin_handle_response(jcr, bsock, pluginname, authData->seqdata); + + case bDirAuthenticationOperationHidden: + // display message + if (dir_authplugin_display_message(bsock, UA_AUTH_INTERACTIVE_HIDDEN, authData->question) != bRC_OK){ + return bRC_Error; + } + return dir_authplugin_handle_response(jcr, bsock, pluginname, authData->seqdata); + + case bDirAuthenticationOperationAuthenticate: + return bRC_OK; + + default: + return bRC_Error; + } + + return bRC_OK; +} diff --git a/bacula/src/dird/dir_authplugin.h b/bacula/src/dird/dir_authplugin.h new file mode 100644 index 000000000..8380cd8cc --- /dev/null +++ b/bacula/src/dird/dir_authplugin.h @@ -0,0 +1,111 @@ +/* + Bacula(R) - The Network Backup Solution + + Copyright (C) 2000-2020 Kern Sibbald + + The original author of Bacula is Kern Sibbald, with contributions + from many others, a complete list can be found in the file AUTHORS. + + You may use this file and others of this release according to the + license defined in the LICENSE file, which includes the Affero General + Public License, v3.0 ("AGPLv3") and some additional permissions and + terms pursuant to its AGPLv3 Section 7. + + This notice must be preserved when any source code is + conveyed and/or propagated. + + Bacula(R) is a registered trademark of Kern Sibbald. +*/ +/* + * Bacula authentication and authorization plugin framework. + * + * Author: Radosław Korzeniewski, MMXX + * radoslaw@korzeniewski.net, radekk@inteos.pl + * Inteos Sp. z o.o. http://www.inteos.pl/ + */ + +#ifndef __DIR_AUTHPLUGINS_H +#define __DIR_AUTHPLUGINS_H + +#ifndef _BACULA_H +#ifdef __cplusplus +/* Workaround for SGI IRIX 6.5 */ +#define _LANGUAGE_C_PLUS_PLUS 1 +#endif +#define _REENTRANT 1 +#define _THREAD_SAFE 1 +#define _POSIX_PTHREAD_SEMANTICS 1 +#define _FILE_OFFSET_BITS 64 +#define _LARGEFILE_SOURCE 1 +#define _LARGE_FILES 1 +#endif + +#include +#ifndef __CONFIG_H +#define __CONFIG_H +#include "config.h" +#endif +#include "bc_types.h" +#include "lib/plugins.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/**************************************************************************** + * * + * Bacula definitions * + * * + ****************************************************************************/ + +/** + * @brief This is a list of supported auth plugin framework operations. + */ +typedef enum { + bDirAuthenticationOperationPlugin, + bDirAuthenticationOperationPluginAll, + bDirAuthenticationOperationMessage, + bDirAuthenticationOperationPlain, + bDirAuthenticationOperationLogin = bDirAuthenticationOperationPlain, + bDirAuthenticationOperationHidden, + bDirAuthenticationOperationPassword = bDirAuthenticationOperationHidden, + bDirAuthenticationOperationAuthenticate, +} bDirAuthenticationOperation; + +/** + * @brief + */ +typedef struct s_bDirAuthenticationData { + const bDirAuthenticationOperation operation; + const char * question; + const uint32_t seqdata; +} bDirAuthenticationData; + +typedef struct s_bDirAuthenticationRegister { + const char * name; + const char * welcome; + const uint32_t num; + const bDirAuthenticationData *data; + const int32_t nsTTL; +} bDirAuthenticationRegister; + +typedef struct s_bDirAuthenticationValue { + union { + void * rawdata; // future extension + const bDirAuthenticationData * authdata; // a pointer to single bDirAuthenticationData as a dynamic operation + const char *response; + }; + uint32_t seqdata; +} bDirAuthValue; + +typedef struct s_bDirAuthorizationACLRegister { + const char * name; + // *TBD* - Incomplete + const int32_t nsTTL; +} bDirAuthorizationACLRegister; + +#ifdef __cplusplus +} +#endif + +#endif /* __DIR_AUTHPLUGINS_H */ diff --git a/bacula/src/dird/dir_plugins.c b/bacula/src/dird/dir_plugins.c index 9832eec06..5fee8e769 100644 --- a/bacula/src/dird/dir_plugins.c +++ b/bacula/src/dird/dir_plugins.c @@ -461,6 +461,10 @@ static bRC baculaRegisterEvents(bpContext *ctx, ...) va_start(args, ctx); while ((event = va_arg(args, uint32_t))) { Dmsg1(dbglvl, "dir-Plugin wants event=%u\n", event); + switch(event){ + default: + Dmsg1(dbglvl, "event=%u not registered\n", event); + } } va_end(args); return bRC_OK; diff --git a/bacula/src/dird/dir_plugins.h b/bacula/src/dird/dir_plugins.h index 0638a6036..6dc9b2039 100644 --- a/bacula/src/dird/dir_plugins.h +++ b/bacula/src/dird/dir_plugins.h @@ -94,18 +94,21 @@ typedef enum { bwDirVarJobLevel = 4 } bwDirVariable; - typedef enum { - bDirEventJobStart = 1, - bDirEventJobEnd = 2, - bDirEventJobInit = 3, - bDirEventJobRun = 4, - bDirEventVolumePurged = 5, - bDirEventNewVolume = 6, - bDirEventNeedVolume = 7, - bDirEventVolumeFull = 8, - bDirEventRecyle = 9, - bDirEventGetScratch = 10 + bDirEventJobStart = 1, + bDirEventJobEnd = 2, + bDirEventJobInit = 3, + bDirEventJobRun = 4, + bDirEventVolumePurged = 5, + bDirEventNewVolume = 6, + bDirEventNeedVolume = 7, + bDirEventVolumeFull = 8, + bDirEventRecyle = 9, + bDirEventGetScratch = 10, + bDirEventAuthenticationQuestion = 1000, // *value is a bDirAuthValue struct allocated by Dir + // to get return value from + bDirEventAuthenticationResponse = 1001, // *value is a char* to user response + bDirEventAuthenticate = 1002, // return bRC_OK when authenticate is successful } bDirEventsType; typedef struct s_bDirEvent { @@ -165,7 +168,8 @@ typedef struct s_dirpluginInfo { const char *plugin_description; } pDirInfo; -typedef struct s_dirpluginFuncs { +typedef struct s_dirpluginFuncs +{ uint32_t size; uint32_t version; bRC (*newPlugin)(bpContext *ctx); @@ -173,6 +177,8 @@ typedef struct s_dirpluginFuncs { bRC (*getPluginValue)(bpContext *ctx, pDirVariable var, void *value); bRC (*setPluginValue)(bpContext *ctx, pDirVariable var, void *value); bRC (*handlePluginEvent)(bpContext *ctx, bDirEvent *event, void *value); + bRC (*getPluginAuthenticationData)(bpContext *ctx, const char *param, void **data); + bRC (*getPluginAuthorizationData)(bpContext *ctx, const char *param, void **data); } pDirFuncs; #define dirplug_func(plugin) ((pDirFuncs *)(plugin->pfuncs)) diff --git a/bacula/src/dird/dird.c b/bacula/src/dird/dird.c index 5e37b6030..08e06a8a2 100644 --- a/bacula/src/dird/dird.c +++ b/bacula/src/dird/dird.c @@ -130,7 +130,7 @@ static BDB *dir_get_db(JCR *jcr) return db; } -static bool dir_sql_log(JCR *jcr, JobId_t jobid, utime_t mtime, char *msg) +static bool dir_sql_log(JCR *jcr, JobId_t jobid, utime_t mtime, char *msg) { bool ret; BDB *db = dir_get_db(jcr); @@ -144,7 +144,7 @@ static bool dir_sql_log(JCR *jcr, JobId_t jobid, utime_t mtime, char *msg) db_close_database(jcr, db); /* It was open just for us */ } return ret; -} +} static bool dir_sql_event(JCR *jcr, utime_t mtime, char *line) { @@ -167,8 +167,8 @@ bail_out: if (!jcr || !jcr->db) { db_close_database(jcr, db); /* It was open just for us */ } - return ret; -} + return ret; +} static void usage() { @@ -1153,7 +1153,7 @@ static bool check_resources() /* Make sure the job doesn't use the Scratch Pool to start with */ const char *name; if (!check_pool(job->JobType, job->JobLevel, - job->pool, job->next_pool, &name)) { + job->pool, job->next_pool, &name)) { Jmsg(NULL, M_FATAL, 0, _("%s \"Scratch\" not valid in Job \"%s\".\n"), name, job->name()); @@ -1319,7 +1319,7 @@ static bool check_resources() case COLLECTOR_BACKEND_CSV: /* a CSV backend require a file parameter */ if (!collect->file){ - Jmsg(NULL, M_FATAL, 0, _("File parameter required in Collector CSV resource \"%s\".\n"), + Jmsg(NULL, M_FATAL, 0, _("File parameter required in Collector CSV resource \"%s\".\n"), collect->hdr.name); OK = false; } @@ -1327,7 +1327,7 @@ static bool check_resources() case COLLECTOR_BACKEND_Graphite: /* we require a host parameter at least */ if (!collect->host){ - Jmsg(NULL, M_FATAL, 0, _("Host parameter required in Collector Graphite resource \"%s\".\n"), + Jmsg(NULL, M_FATAL, 0, _("Host parameter required in Collector Graphite resource \"%s\".\n"), collect->hdr.name); OK = false; } @@ -1345,7 +1345,7 @@ static bool check_resources() } -/* Take note of all jobs to be canceled when we start the director +/* Take note of all jobs to be canceled when we start the director * We don't send events directly here because the events might also * be sent to the Catalog. Better to return a list of message. */ @@ -1383,13 +1383,13 @@ static bool check_catalog(cat_op mode) * Make sure we can open catalog, otherwise print a warning * message because the server is probably not running. */ - db = db_init_database(NULL, catalog->db_driver, catalog->db_name, + db = db_init_database(NULL, catalog->db_driver, catalog->db_name, catalog->db_user, catalog->db_password, catalog->db_address, catalog->db_port, catalog->db_socket, catalog->db_ssl_mode, catalog->db_ssl_key, catalog->db_ssl_cert, catalog->db_ssl_ca, - catalog->db_ssl_capath, catalog->db_ssl_cipher, + catalog->db_ssl_capath, catalog->db_ssl_cipher, true /* mult_db_connections */, /* Avoid issue with counters during a reload */ catalog->disable_batch_insert); diff --git a/bacula/src/dird/dird_conf.c b/bacula/src/dird/dird_conf.c index 98a2be3fd..aa002ac77 100644 --- a/bacula/src/dird/dird_conf.c +++ b/bacula/src/dird/dird_conf.c @@ -438,6 +438,8 @@ static RES_ITEM con_items[] = { {"TlsKey", store_dir, ITEM(res_con.tls_keyfile), 0, 0, 0}, {"TlsDhFile", store_dir, ITEM(res_con.tls_dhfile), 0, 0, 0}, {"TlsAllowedCn", store_alist_str, ITEM(res_con.tls_allowed_cns), 0, 0, 0}, + {"AuthenticationPlugin", store_str, ITEM(res_con.authenticationplugin), 0, 0, 0}, + {"AuthorizationPlugin", store_str, ITEM(res_con.authorizationplugin), 0, 0, 0}, {NULL, NULL, {0}, 0, 0, 0} }; @@ -941,6 +943,12 @@ void dump_resource(int type, RES *ares, void sendit(void *sock, const char *fmt, } sendit(sock, _("Console: name=%s SSL=%d PSK=%d\n"), res->res_con.hdr.name, res->res_con.tls_enable, res->res_con.tls_psk_enable); + if (res->res_con.authenticationplugin){ + sendit(sock, _(" Authentication=%s\n"), res->res_con.authenticationplugin); + } + if (res->res_con.authorizationplugin){ + sendit(sock, _(" (!) Authorization=%s\n"), res->res_con.authorizationplugin); + } for (int acl=0; aclres_con.ACL_lists[acl]==NULL) { continue; @@ -1317,7 +1325,7 @@ void dump_resource(int type, RES *ares, void sendit(void *sock, const char *fmt, int i; RUN *run = res->res_sch.run; char buf[1000], num[30]; - sendit(sock, _("Schedule: Name=%s Enabled=%d\n"), + sendit(sock, _("Schedule: Name=%s Enabled=%d\n"), res->res_sch.hdr.name, res->res_sch.is_enabled()); if (!run) { break; @@ -1659,6 +1667,12 @@ void free_resource(RES *rres, int type) res->res_con.ACL_lists[i] = NULL; } } + if (res->res_con.authenticationplugin) { + free(res->res_con.authenticationplugin); + } + if (res->res_con.authorizationplugin) { + free(res->res_con.authorizationplugin); + } break; case R_CLIENT: if (res->res_client.client_address) { @@ -2045,7 +2059,7 @@ bool save_resource(CONFIG *config, int type, RES_ITEM *items, int pass) res->res_store.shared_storage = res_all.res_store.shared_storage; res->res_store.autochanger = res_all.res_store.autochanger; /* The resource name is Autochanger instead of Storage - * so we force the Autochanger attributes + * so we force the Autochanger attributes */ if (strcasecmp(resources[rindex].name, "autochanger") == 0) { /* The Autochanger resource might be already defined */ diff --git a/bacula/src/dird/dird_conf.h b/bacula/src/dird/dird_conf.h index 8d0700c74..dbb4e1180 100644 --- a/bacula/src/dird/dird_conf.h +++ b/bacula/src/dird/dird_conf.h @@ -198,21 +198,23 @@ enum { class CONRES { public: RES hdr; - char *password; /* UA server password */ - alist *ACL_lists[Num_ACL]; /* pointers to ACLs */ - char *tls_ca_certfile; /* TLS CA Certificate File */ - char *tls_ca_certdir; /* TLS CA Certificate Directory */ - char *tls_certfile; /* TLS Server Certificate File */ - char *tls_keyfile; /* TLS Server Key File */ - char *tls_dhfile; /* TLS Diffie-Hellman Parameters */ - alist *tls_allowed_cns; /* TLS Allowed Clients */ - TLS_CONTEXT *tls_ctx; /* Shared TLS Context */ - TLS_CONTEXT *psk_ctx; /* Shared TLS-PSK Context */ - bool tls_authenticate; /* Authenticated with TLS */ - bool tls_enable; /* Enable TLS */ - bool tls_psk_enable; /* Enable TLS-PSK */ - bool tls_require; /* Require TLS */ - bool tls_verify_peer; /* TLS Verify Client Certificate */ + char *password; /* UA server password */ + alist *ACL_lists[Num_ACL]; /* pointers to ACLs */ + char *tls_ca_certfile; /* TLS CA Certificate File */ + char *tls_ca_certdir; /* TLS CA Certificate Directory */ + char *tls_certfile; /* TLS Server Certificate File */ + char *tls_keyfile; /* TLS Server Key File */ + char *tls_dhfile; /* TLS Diffie-Hellman Parameters */ + alist *tls_allowed_cns; /* TLS Allowed Clients */ + TLS_CONTEXT *tls_ctx; /* Shared TLS Context */ + TLS_CONTEXT *psk_ctx; /* Shared TLS-PSK Context */ + bool tls_authenticate; /* Authenticated with TLS */ + bool tls_enable; /* Enable TLS */ + bool tls_psk_enable; /* Enable TLS-PSK */ + bool tls_require; /* Require TLS */ + bool tls_verify_peer; /* TLS Verify Client Certificate */ + char *authenticationplugin; /* Authentication Plugin (user/password) */ + char *authorizationplugin; /* Authorization Plugin (permissions) */ /* Methods */ char *name() const; diff --git a/bacula/src/dird/job.c b/bacula/src/dird/job.c index e67a32091..0e9519a39 100644 --- a/bacula/src/dird/job.c +++ b/bacula/src/dird/job.c @@ -121,7 +121,7 @@ bool setup_job(JCR *jcr) jcr->catalog->db_socket, jcr->catalog->db_ssl_mode, jcr->catalog->db_ssl_key, jcr->catalog->db_ssl_cert, - jcr->catalog->db_ssl_ca, jcr->catalog->db_ssl_capath, + jcr->catalog->db_ssl_ca, jcr->catalog->db_ssl_capath, jcr->catalog->db_ssl_cipher, jcr->catalog->mult_db_connections, jcr->catalog->disable_batch_insert); @@ -291,7 +291,7 @@ static bool setup_resume_job(JCR *jcr, JOB_DBR *jr) jcr->catalog->db_socket, jcr->catalog->db_ssl_mode, jcr->catalog->db_ssl_key, jcr->catalog->db_ssl_cert, - jcr->catalog->db_ssl_ca, jcr->catalog->db_ssl_capath, + jcr->catalog->db_ssl_ca, jcr->catalog->db_ssl_capath, jcr->catalog->db_ssl_cipher, jcr->catalog->mult_db_connections, jcr->catalog->disable_batch_insert); @@ -713,7 +713,7 @@ cancel_job(UAContext *ua, JCR *jcr, int wait, bool cancel) "%s jobid=%s job=%s", cancel?"cancel":"stop", edit_uint64(jcr->JobId, ed1), jcr->Job); - + if (!cancel) { /* stop the job */ if (!jcr->can_be_stopped()) { ua->error_msg(_("Cannot stop JobId %s, Job %s is not a regular Backup Job\n"), diff --git a/bacula/src/dird/protos.h b/bacula/src/dird/protos.h index ea175fcd2..49189b4d8 100644 --- a/bacula/src/dird/protos.h +++ b/bacula/src/dird/protos.h @@ -390,3 +390,10 @@ bool catreq_get_pool_info(JCR *jcr, BSOCK *bs); # define mark_access_denied(a) #endif +/* dedup_util.c */ +bool is_dedup_ref(DEV_RECORD *rec, bool lazy); + +/* dir_authplugins.c */ +void *dir_authplugin_getauthenticationData(JCR *jcr, const char *param); +bRC dir_authplugin_do_interaction(JCR *jcr, BSOCK *bsock, const char *pluginname, void *data, bool pluginall = false); +bRC dir_authplugin_authenticate(JCR *jcr, BSOCK *bsock, const char *pluginname); diff --git a/bacula/src/dird/ua_server.c b/bacula/src/dird/ua_server.c index 7d892115a..17e18488b 100644 --- a/bacula/src/dird/ua_server.c +++ b/bacula/src/dird/ua_server.c @@ -130,6 +130,9 @@ static void *handle_UA_client_request(void *arg) jcr = new_control_jcr("-Console-", JT_CONSOLE); + // create plugins for authentication/authorization handling + new_plugins(jcr); + ua = new_ua_context(jcr); ua->UA_sock = user; set_jcr_in_tsd(INVALID_JCR); diff --git a/bacula/src/jcr.h b/bacula/src/jcr.h index 2a4bff0c4..768a49f2f 100644 --- a/bacula/src/jcr.h +++ b/bacula/src/jcr.h @@ -422,7 +422,6 @@ public: bool dummy_jobmedia; /* Dummy JobMedia written */ #endif /* DIRECTOR_DAEMON */ - #ifdef FILE_DAEMON /* File Daemon specific part of JCR */ BSOCK *sd_calls_client_bsock; /* Socket used by SDCallsClient feature */ diff --git a/bacula/src/lib/authenticatebase.cc b/bacula/src/lib/authenticatebase.cc index a876ac555..7bd13200c 100644 --- a/bacula/src/lib/authenticatebase.cc +++ b/bacula/src/lib/authenticatebase.cc @@ -67,7 +67,7 @@ AuthenticateBase::~AuthenticateBase() } /* - * + * * LOCAL REMOTE * {TLS,PSK} {TLS,PSK} * -------------------------------------------------- @@ -161,7 +161,7 @@ AuthenticateBase::~AuthenticateBase() * {REQUIRED,REQUIRED} {REQUIRED,OK} => t * {REQUIRED,REQUIRED} {REQUIRED,NONE} => t * {REQUIRED,REQUIRED} {REQUIRED,REQUIRED} => t - * + * */ /* OK @@ -198,7 +198,7 @@ int AuthenticateBase::TestTLSRequirement() * {NONE,NONE} {NONE,REQUIRED} => F -- Remote * {NONE,NONE} {REQUIRED,OK} => F -- Remote * {NONE,NONE} {REQUIRED,NONE} => F -- Remote - * {NONE,NONE} {REQUIRED,REQUIRED} => F + * {NONE,NONE} {REQUIRED,REQUIRED} => F */ if (tls_local_need == BNET_TLS_NONE && psk_local_need == BNET_TLS_NONE && @@ -387,12 +387,18 @@ bool AuthenticateBase::ClientEarlyTLS() /* DIR is calling, DIR is the client */ bool AuthenticateBase::ClientCramMD5Authenticate(const char *password) { - int compatible = true; - if (!ClientEarlyTLS()) { return false; } + return ClientCramMD5AuthenticateBase(password); +} + +/* the *Base version works without startTLS feature */ +bool AuthenticateBase::ClientCramMD5AuthenticateBase(const char *password) +{ + int compatible = true; + if (((local_class == dcSD && remote_class == dcSD) || (local_class == dcFD && remote_class == dcSD))) { if (jcr && job_canceled(jcr)) { @@ -460,7 +466,7 @@ bool AuthenticateBase::ServerEarlyTLS() /* If both can speak TLS or PSK then send the "starttls" even if the * local requirement is not full filled. The client need to * know the server requirements too. Both will terminate the connection - * in HandleTLS if the requirement are not full filled + * in HandleTLS if the requirement are not full filled */ if (!bsock->fsend("starttls tlspsk=%d\n", tlspsk_local_need)) { // TODO tweak the error message diff --git a/bacula/src/lib/authenticatebase.h b/bacula/src/lib/authenticatebase.h index c8d4fee00..123829f93 100644 --- a/bacula/src/lib/authenticatebase.h +++ b/bacula/src/lib/authenticatebase.h @@ -29,6 +29,14 @@ enum { TLS_REQ_ERR_REMOTE }; +#define UA_VERSION_PLUGINAUTH 200 +#define UA_AUTH_INTERACTIVE "auth interactive" +#define UA_AUTH_INTERACTIVE_PLAIN 'P' +#define UA_AUTH_INTERACTIVE_HIDDEN 'H' +#define UA_AUTH_INTERACTIVE_MESSAGE 'M' +#define UA_AUTH_INTERACTIVE_FINISH 'F' +#define UA_AUTH_INTERACTIVE_RESPONSE 'R' + class AuthenticateBase { static const char *dc_short_name[6]; @@ -87,6 +95,7 @@ public: bool ServerEarlyTLS(); bool ClientCramMD5Authenticate(const char *password); + bool ClientCramMD5AuthenticateBase(const char *password); bool ServerCramMD5Authenticate(const char *password); bool HandleTLS(); diff --git a/bacula/src/plugins/dir/Makefile.in b/bacula/src/plugins/dir/Makefile.in index c20b20476..ab7c58639 100644 --- a/bacula/src/plugins/dir/Makefile.in +++ b/bacula/src/plugins/dir/Makefile.in @@ -13,12 +13,15 @@ DIRDIR=../../dird SRCDIR=../.. LIBDIR=../../lib +topdir = @BUILD_DIR@ +thisdir = src/plugins/dir + .SUFFIXES: .c .o .lo .c.lo: $(LIBTOOL_COMPILE) $(CXX) $(DEFS) $(DEBUG) $(CPPFLAGS) $(CFLAGS) -I${SRCDIR} -I${DIRDIR} -DTEST_PROGRAM -c $< -all: example-plugin-dir.la +all: dirpluglib.lo example-plugin-dir.lo: example-plugin-dir.c ${DIRDIR}/dir_plugins.h $(LIBTOOL_COMPILE) $(CXX) $(DEFS) $(DEBUG) $(CFLAGS) -I../.. -I${DIRDIR} -c example-plugin-dir.c @@ -26,10 +29,20 @@ example-plugin-dir.lo: example-plugin-dir.c ${DIRDIR}/dir_plugins.h example-plugin-dir.la: Makefile example-plugin-dir.lo $(LIBTOOL_LINK) $(CXX) $(LDFLAGS) -shared example-plugin-dir.lo -o $@ -rpath $(plugindir) -module -export-dynamic -avoid-version +test-authentication-api-dir.lo: test-authentication-api-dir.c ${DIRDIR}/dir_plugins.h + $(LIBTOOL_COMPILE) $(CXX) $(DEFS) $(DEBUG) $(CFLAGS) -I../.. -I${DIRDIR} -c test-authentication-api-dir.c + +test-authentication-api-dir.la: Makefile test-authentication-api-dir.lo dirpluglib.lo + @echo "Linking $(@:.la=.so) ..." + $(NO_ECHO)$(LIBTOOL_LINK) --silent $(CXX) $(LDFLAGS) -shared $^ -o $@ -rpath $(plugindir) -module -export-dynamic -avoid-version + install: all $(MKDIR) $(DESTDIR)$(plugindir) - $(LIBTOOL_INSTALL) $(INSTALL_PROGRAM) example-plugin-dir.la $(DESTDIR)$(plugindir) - $(RMF) $(DESTDIR)$(plugindir)/example-plugin-dir.la + +# $(LIBTOOL_INSTALL) $(INSTALL_PROGRAM) example-plugin-dir.la $(DESTDIR)$(plugindir) +# $(LIBTOOL_INSTALL) $(INSTALL_PROGRAM) test-authentication-api-dir.la $(DESTDIR)$(plugindir) +# $(RMF) $(DESTDIR)$(plugindir)/example-plugin-dir.la +# $(RMF) $(DESTDIR)$(plugindir)/test-authentication-api-dir.la libtool-clean: find . -name '*.lo' -print | xargs $(LIBTOOL_CLEAN) $(RMF) @@ -45,6 +58,10 @@ distclean: clean libtool-uninstall: $(LIBTOOL_UNINSTALL) $(RMF) $(DESTDIR)$(plugindir)/example-plugin-dir.la +Makefile: Makefile.in $(topdir)/config.status + cd $(topdir) \ + && CONFIG_FILES=$(thisdir)/$@ CONFIG_HEADERS= $(SHELL) ./config.status + uninstall: @LIBTOOL_UNINSTALL_TARGET@ depend: diff --git a/bacula/src/plugins/dir/dirpluglib.c b/bacula/src/plugins/dir/dirpluglib.c new file mode 100644 index 000000000..813274474 --- /dev/null +++ b/bacula/src/plugins/dir/dirpluglib.c @@ -0,0 +1,232 @@ +/* + Bacula(R) - The Network Backup Solution + + Copyright (C) 2000-2020 Kern Sibbald + + The original author of Bacula is Kern Sibbald, with contributions + from many others, a complete list can be found in the file AUTHORS. + + You may use this file and others of this release according to the + license defined in the LICENSE file, which includes the Affero General + Public License, v3.0 ("AGPLv3") and some additional permissions and + terms pursuant to its AGPLv3 Section 7. + + This notice must be preserved when any source code is + conveyed and/or propagated. + + Bacula(R) is a registered trademark of Kern Sibbald. +*/ +/* + * Common definitions and utility functions for Inteos plugins. + * Functions defines a common framework used in our utilities and plugins. + * This a Director Plugins flavor. + * + * Author: Radosław Korzeniewski, MMXIX + * radoslaw@korzeniewski.net, radekk@inteos.pl + * Inteos Sp. z o.o. http://www.inteos.pl/ + */ + +#include "dirpluglib.h" + +/* Pointers to Bacula functions used in plugins */ +extern bDirFuncs *bfuncs; +extern bDirInfo *binfo; + +/* Events that are passed to plugin +typedef enum { + bDirEventJobStart = 1, + bDirEventJobEnd = 2, + bDirEventJobInit = 3, + bDirEventJobRun = 4, + bDirEventVolumePurged = 5, + bDirEventNewVolume = 6, + bDirEventNeedVolume = 7, + bDirEventVolumeFull = 8, + bDirEventRecyle = 9, + bDirEventGetScratch = 10, + bDirEventAuthenticationParam = 1000, // *value is a char* to console resource param value + bDirEventAuthorizationACLParam = 1001, // *value is a char* to console resource param value + bDirEventAuthenticationQuestion = 1002, // *value is a bDirAuthValue struct allocated by Dir + // to get return value from + bDirEventAuthenticationResponse = 1003, // *value is a char* to user response + bDirEventAuthenticate = 1004, // return bRC_OK when authenticate is successful +} bDirEventsType; +*/ + +const char *eventtype2str(bDirEvent *event){ + switch (event->eventType){ + case bDirEventJobStart: + return "bDirEventJobStart"; + case bDirEventJobEnd: + return "bDirEventJobEnd"; + case bDirEventJobInit: + return "bDirEventJobInit"; + case bDirEventJobRun: + return "bDirEventJobRun"; + case bDirEventVolumePurged: + return "bDirEventVolumePurged"; + case bDirEventNewVolume: + return "bDirEventNewVolume"; + case bDirEventNeedVolume: + return "bDirEventNeedVolume"; + case bDirEventVolumeFull: + return "bDirEventVolumeFull"; + case bDirEventRecyle: + return "bDirEventRecyle"; + case bDirEventGetScratch: + return "bDirEventGetScratch"; + case bDirEventAuthenticationQuestion: + return "bDirEventAuthenticationQuestion"; + case bDirEventAuthenticationResponse: + return "bDirEventAuthenticationResponse"; + case bDirEventAuthenticate: + return "bDirEventAuthenticate"; + default: + return "Unknown"; + } +} + +/* + * Return the real size of the disk based on the size suffix. + * + * in: + * disksize - the numeric value of the disk size to compute + * suff - the suffix for a disksize value + * out: + * uint64_t - the size of the disk computed with suffix + */ +uint64_t pluglib_size_suffix(int disksize, char suff) +{ + uint64_t size; + + switch (suff){ + case 'G': + size = (uint64_t)disksize * 1024 * 1048576; + break; + case 'M': + size = (uint64_t)disksize * 1048576; + break; + case 'T': + size = (uint64_t)disksize * 1048576 * 1048576; + break; + case 'K': + case 'k': + size = (uint64_t)disksize * 1024; + break; + default: + size = disksize; + } + return size; +} + +/* + * Return the real size of the disk based on the size suffix. + * This version uses a floating point numbers (double) for computation. + * + * in: + * disksize - the numeric value of the disk size to compute + * suff - the suffix for a disksize value + * out: + * uint64_t - the size of the disk computed with suffix + */ +uint64_t pluglib_size_suffix(double disksize, char suff) +{ + uint64_t size; + + switch (suff){ + case 'G': + size = disksize * 1024.0 * 1048576.0; + break; + case 'M': + size = disksize * 1048576.0; + break; + case 'T': + size = disksize * 1048576.0 * 1048576.0; + break; + case 'K': + case 'k': + size = disksize * 1024.0; + break; + default: + size = disksize; + } + return size; +} + +/* + * Creates a path hierarchy on local FS. + * It is used for local restore mode to create a required directory. + * The functionality is similar to 'mkdir -p'. + * + * TODO: make a support for relative path + * TODO: check if we can use findlib/makepath implementation instead + * + * in: + * bpContext - for Bacula debug and jobinfo messages + * path - a full path to create, does not check if the path is relative, + * could fail in this case + * out: + * bRC_OK - path creation was successful + * bRC_Error - on any error + */ +bRC pluglib_mkpath(bpContext* ctx, char* path, bool isfatal) +{ +#ifdef PLUGINPREFIX +#define _OLDPREFIX PLUGINPREFIX +#endif +#define PLUGINPREFIX "pluglibmkpath:" + struct stat statp; + POOL_MEM dir(PM_FNAME); + char *p, *q; + + if (!path){ + return bRC_Error; + } + if (stat(path, &statp) == 0){ + if (S_ISDIR(statp.st_mode)){ + return bRC_OK; + } else { + DMSG(ctx, DERROR, "Path %s is not directory\n", path); + JMSG(ctx, isfatal ? M_FATAL : M_ERROR, "Path %s is not directory\n", path); + return bRC_Error; + } + } + DMSG(ctx, DDEBUG, "mkpath verify dir: %s\n", path); + pm_strcpy(dir, path); + p = dir.addr() + 1; + while (*p && (q = strchr(p, (int)PathSeparator)) != NULL){ + *q = 0; + DMSG(ctx, DDEBUG, "mkpath scanning(1): %s\n", dir.c_str()); + if (stat(dir.c_str(), &statp) == 0){ + *q = PathSeparator; + p = q + 1; + continue; + } + DMSG0(ctx, DDEBUG, "mkpath will create dir(1).\n"); + if (mkdir(dir.c_str(), 0750) < 0){ + /* error */ + berrno be; + DMSG2(ctx, DERROR, "Cannot create directory %s Err=%s\n", dir.c_str(), be.bstrerror()); + JMSG2(ctx, isfatal ? M_FATAL : M_ERROR, "Cannot create directory %s Err=%s\n", dir.c_str(), be.bstrerror()); + return bRC_Error; + } + *q = PathSeparator; + p = q + 1; + } + DMSG0(ctx, DDEBUG, "mkpath will create dir(2).\n"); + if (mkdir(path, 0750) < 0){ + /* error */ + berrno be; + DMSG2(ctx, DERROR, "Cannot create directory %s Err=%s\n", path, be.bstrerror()); + JMSG2(ctx, isfatal ? M_FATAL : M_ERROR, "Cannot create directory %s Err=%s\n", path, be.bstrerror()); + return bRC_Error; + } + DMSG0(ctx, DDEBUG, "mkpath finish.\n"); +#ifdef _OLDPREFIX +#define PLUGINPREFIX _OLDPREFIX +#undef _OLDPREFIX +#else +#undef PLUGINPREFIX +#endif + return bRC_OK; +} diff --git a/bacula/src/plugins/dir/dirpluglib.h b/bacula/src/plugins/dir/dirpluglib.h new file mode 100644 index 000000000..41da06c4f --- /dev/null +++ b/bacula/src/plugins/dir/dirpluglib.h @@ -0,0 +1,128 @@ +/* + Bacula(R) - The Network Backup Solution + + Copyright (C) 2000-2020 Kern Sibbald + + The original author of Bacula is Kern Sibbald, with contributions + from many others, a complete list can be found in the file AUTHORS. + + You may use this file and others of this release according to the + license defined in the LICENSE file, which includes the Affero General + Public License, v3.0 ("AGPLv3") and some additional permissions and + terms pursuant to its AGPLv3 Section 7. + + This notice must be preserved when any source code is + conveyed and/or propagated. + + Bacula(R) is a registered trademark of Kern Sibbald. +*/ +/* + * Common definitions and utility functions for Inteos plugins. + * Functions defines a common framework used in our utilities and plugins. + * + * Author: Radosław Korzeniewski, MMXX + * radoslaw@korzeniewski.net, radekk@inteos.pl + * Inteos Sp. z o.o. http://www.inteos.pl/ + */ + +#ifndef _PLUGLIB_H_ +#define _PLUGLIB_H_ + +#include +#include +#include + +#include "bacula.h" +#include "dir_plugins.h" + +/* definitions */ +/* size of different string or query buffers */ +#define BUFLEN 4096 +#define BIGBUFLEN 65536 + +/* debug and messages functions */ +#define JMSG0(ctx,type,msg) \ + if (ctx) bfuncs->JobMessage ( ctx, __FILE__, __LINE__, type, 0, PLUGINPREFIX " " msg ); + +#define JMSG1 JMSG +#define JMSG(ctx,type,msg,var) \ + if (ctx) bfuncs->JobMessage ( ctx, __FILE__, __LINE__, type, 0, PLUGINPREFIX " " msg, var ); + +#define JMSG2(ctx,type,msg,var1,var2) \ + if (ctx) bfuncs->JobMessage ( ctx, __FILE__, __LINE__, type, 0, PLUGINPREFIX " " msg, var1, var2 ); + +#define JMSG3(ctx,type,msg,var1,var2,var3) \ + if (ctx) bfuncs->JobMessage ( ctx, __FILE__, __LINE__, type, 0, PLUGINPREFIX " " msg, var1, var2, var3 ); + +#define JMSG4(ctx,type,msg,var1,var2,var3,var4) \ + if (ctx) bfuncs->JobMessage ( ctx, __FILE__, __LINE__, type, 0, PLUGINPREFIX " " msg, var1, var2, var3, var4 ); + +#define DMSG0(ctx,level,msg) \ + if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg ); + +#define DMSG1 DMSG +#define DMSG(ctx,level,msg,var) \ + if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg, var ); + +#define DMSG2(ctx,level,msg,var1,var2) \ + if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg, var1, var2 ); + +#define DMSG3(ctx,level,msg,var1,var2,var3) \ + if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg, var1, var2, var3 ); + +#define DMSG4(ctx,level,msg,var1,var2,var3,var4) \ + if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg, var1, var2, var3, var4 ); + +#define DMSG6(ctx,level,msg,var1,var2,var3,var4,var5,var6) \ + if (ctx) bfuncs->DebugMessage ( ctx, __FILE__, __LINE__, level, PLUGINPREFIX " " msg, var1, var2, var3, var4, var5, var6 ); + +/* fixed debug level definitions */ +#define D1 1 /* debug for every error */ +#define DERROR D1 +#define D2 10 /* debug only important stuff */ +#define DINFO D2 +#define D3 200 /* debug for information only */ +#define DDEBUG D3 +#define D4 800 /* debug for detailed information only */ +#define DVDEBUG D4 + +#define getBaculaVar(bvar,val) bfuncs->getBaculaValue(ctx, bvar, val); + +/* used for sanity check in plugin functions */ +#define ASSERT_CTX \ + if (!ctx || !ctx->pContext || !bfuncs) \ + { \ + return bRC_Error; \ + } + +/* defines for handleEvent */ +#define DMSG_EVENT_STR(event,value) DMSG2(ctx, DINFO, "%s value=%s\n", eventtype2str(event), NPRT((char *)value)); +#define DMSG_EVENT_CHAR(event,value) DMSG2(ctx, DINFO, "%s value='%c'\n", eventtype2str(event), (char)value); +#define DMSG_EVENT_LONG(event,value) DMSG2(ctx, DINFO, "%s value=%ld\n", eventtype2str(event), (intptr_t)value); +#define DMSG_EVENT_PTR(event,value) DMSG2(ctx, DINFO, "%s value=%p\n", eventtype2str(event), value); + +const char *eventtype2str(bDirEvent *event); +uint64_t pluglib_size_suffix(int disksize, char suff); +uint64_t pluglib_size_suffix(double disksize, char suff); +bRC pluglib_mkpath(bpContext* ctx, char* path, bool isfatal); + +/* + * Checks if plugin command points to our Plugin + * + * in: + * command - the plugin command used for backup/restore + * out: + * True - if it is our plugin command + * False - the other plugin command + */ +inline bool isourplugincommand(const char *pluginprefix, const char *command) +{ + /* check if it is our Plugin command */ + if (strncmp(pluginprefix, command, strlen(pluginprefix)) == 0){ + /* it is not our plugin prefix */ + return true; + } + return false; +} + +#endif /* _PLUGLIB_H_ */ diff --git a/bacula/src/plugins/dir/ldap/src/.gitignore b/bacula/src/plugins/dir/ldap/src/.gitignore new file mode 100644 index 000000000..1513d9ad7 --- /dev/null +++ b/bacula/src/plugins/dir/ldap/src/.gitignore @@ -0,0 +1,2 @@ +.libs +ldaptest \ No newline at end of file diff --git a/bacula/src/plugins/dir/ldap/src/Makefile.in b/bacula/src/plugins/dir/ldap/src/Makefile.in new file mode 100644 index 000000000..fd62206df --- /dev/null +++ b/bacula/src/plugins/dir/ldap/src/Makefile.in @@ -0,0 +1,90 @@ +# +# Simple Makefile for building test FD plugins for Bacula +# +# Copyright (C) 2000-2015 by Kern Sibbald +# License: BSD 2-Clause; see file LICENSE-FOSS +# +# +@MCOMMON@ + +# No optimization for now for easy debugging + +DIRDIR=../../../../dird +SRCDIR=../../../.. +LIBDIR=../../../../lib +DIRPLUGDIR=../.. + +topdir = @BUILD_DIR@ +working_dir=@working_dir@ +thisdir = src/plugins/dir/ldap/src + +LDAP_LIBS=@LDAP_LIBS@ +LDAP_LDFLAGS=@LDAP_LDFLAGS@ +LDAP_INC=@LDAP_INC@ +BPAM_LDAP_TARGET = @BPAM_LDAP_TARGET@ +BPAM_LDAP_TARGET_INSTALL = @BPAM_LDAP_TARGET_INSTALL@ + +LDAPSRC = ldap-dir.c +LDAPTESTSRC = ldaptest.c +LDAPOBJ = $(LDAPSRC:.c=.lo) +LDAPTESTOBJ = $(LDAPTESTSRC:.c=.lo) + +.SUFFIXES: .c .lo + +# ldap-dir.la ldaptest +all: $(BPAM_LDAP_TARGET) + +.c.lo: + @echo "Compiling $< ..." + $(NO_ECHO)$(LIBTOOL_COMPILE) $(CXX) $(DEFS) $(DEBUG) $(CPPFLAGS) -I${SRCDIR} -I${DIRDIR} -I${DIRPLUGDIR} $(LDAP_INC) -c $< + +%.lo: %.c %.h Makefile + @echo "Compiling $< ..." + $(NO_ECHO)$(LIBTOOL_COMPILE) $(CXX) $(DEFS) $(DEBUG) $(CPPFLAGS) $(CFLAGS) -I${SRCDIR} -I${DIRDIR} -I${DIRPLUGDIR} $(LDAP_INC) -c $(@:.lo=.c) + +ldap-dir.la: Makefile $(LDAPOBJ) $(COMMONOBJ) $(DIRPLUGDIR)/dirpluglib.lo + @echo "Linking $(@:.la=.so) ..." + $(NO_ECHO)$(LIBTOOL_LINK) --silent $(CXX) $(LDFLAGS) $(LDAP_LDFLAGS) $(LDAP_LIBS) -shared $^ -o $@ -rpath $(plugindir) -module -export-dynamic -avoid-version + +$(DIRPLUGDIR)/dirpluglib.lo: + $(MAKE) -C $(DIRPLUGDIR) dirpluglib.lo + +ldaptest: $(LDAPTESTOBJ) Makefile + @echo "Building ldaptest ..." + $(NO_ECHO)$(LIBTOOL_LINK) --silent $(CXX) $(LDFLAGS) $(LDAP_LDFLAGS) $(LDAP_LIBS) ldaptest.lo -o $@ + +install: $(BPAM_LDAP_TARGET_INSTALL) + +install-ldap: ldap-dir.la + @echo "Installing plugin ... $(^:.la=.so)" + $(MKDIR) $(DESTDIR)$(plugindir) + $(LIBTOOL_INSTALL) $(INSTALL_PROGRAM) ldap-dir.la $(DESTDIR)$(plugindir) + $(NO_ECHO)$(RMF) $(DESTDIR)$(plugindir)/ldap-dir.la + +install-ldaptest: ldaptest + @echo "Installing ldaptest ..." + $(LIBTOOL_INSTALL) $(INSTALL_PROGRAM) ldaptest $(DESTDIR)$(sbindir) + +Makefile: Makefile.in $(topdir)/config.status + cd $(topdir) \ + && CONFIG_FILES=$(thisdir)/$@ CONFIG_HEADERS= $(SHELL) ./config.status + +libtool-clean: + @find . -name '*.lo' -print | xargs $(LIBTOOL_CLEAN) $(RMF) + @$(RMF) *.la + @$(RMF) -r .libs _libs + +clean: libtool-clean + @rm -f main ldaptest *.so *.o + +distclean: clean + @rm -f Makefile *.la *.lo + @rm -rf .libs + +libtool-uninstall: + $(LIBTOOL_UNINSTALL) $(RMF) $(DESTDIR)$(plugindir)/ldap-dir.so + $(LIBTOOL_UNINSTALL) $(RMF) $(DESTDIR)$(plugindir)/msad-fd.so + +uninstall: @LIBTOOL_UNINSTALL_TARGET@ + +depend: \ No newline at end of file diff --git a/bacula/src/plugins/dir/ldap/src/ldap-dir.c b/bacula/src/plugins/dir/ldap/src/ldap-dir.c new file mode 100644 index 000000000..68b457071 --- /dev/null +++ b/bacula/src/plugins/dir/ldap/src/ldap-dir.c @@ -0,0 +1,595 @@ +/* + Bacula(R) - The Network Backup Solution + + Copyright (C) 2000-2020 Kern Sibbald + + The original author of Bacula is Kern Sibbald, with contributions + from many others, a complete list can be found in the file AUTHORS. + + You may use this file and others of this release according to the + license defined in the LICENSE file, which includes the Affero General + Public License, v3.0 ("AGPLv3") and some additional permissions and + terms pursuant to its AGPLv3 Section 7. + + This notice must be preserved when any source code is + conveyed and/or propagated. + + Bacula(R) is a registered trademark of Kern Sibbald. +*/ +/* + * Bacula Pluggable Authentication Modules - LDAP Plugin + * + * Author: Radosław Korzeniewski, radoslaw@korzeniewski.net, Inteos Sp. z o.o. + */ + +#include "ldap-dir.h" + +/* + * This is a BPAM (Bacula Pluggable Authentication Modules) which authenticate users + * found in any LDAP Directory Server. It support OpenLDAP and Microsoft Active Directory + * among others standard LDAPs. + */ + + +// a list of authentication operations we expect and handle before final authenticate +static bDirAuthenticationData bpamldapquestions[] = +{ + // operation; question; seqdata; + {bDirAuthenticationOperationLogin, "Username:", 0}, + {bDirAuthenticationOperationPassword, "Password:", 1}, +}; + +#define LDAP_WELCOME "LDAP Authentication" + +// plugin authentication registration struct +static bDirAuthenticationRegister bpamldapregister = +{ +#if __cplusplus > 201103L + .name = PLUGIN_NAME, + .welcome = LDAP_WELCOME, // our plugin name which should correspond to plugin filename + .num = 2, // it shows the number of defined auth operations + .data = bpamldapquestions, // our auth operations list + .nsTTL = 0, // future usage +#else + PLUGIN_NAME, LDAP_WELCOME, 2, bpamldapquestions, 0 +#endif +}; + +#ifdef __cplusplus +extern "C" { +#endif + +/* Forward referenced functions */ +static bRC newPlugin(bpContext *ctx); +static bRC freePlugin(bpContext *ctx); +static bRC handlePluginEvent(bpContext *ctx, bDirEvent *event, void *value); +static bRC getAuthenticationData(bpContext *ctx, const char *param, void **data); +static bRC getAuthorizationData(bpContext *ctx, const char *param, void **data); + +/* Pointers to Bacula functions */ +bDirFuncs *bfuncs = NULL; +bDirInfo *binfo = NULL; + +static pDirInfo pluginInfo = { + sizeof(pluginInfo), + DIR_PLUGIN_INTERFACE_VERSION, + DIR_PLUGIN_MAGIC, + PLUGIN_LICENSE, + PLUGIN_AUTHOR, + PLUGIN_DATE, + PLUGIN_VERSION, + PLUGIN_DESCRIPTION +}; + +static pDirFuncs pluginFuncs = { + sizeof(pluginFuncs), + DIR_PLUGIN_INTERFACE_VERSION, + + /* Entry points into plugin */ + newPlugin, /* new plugin instance */ + freePlugin, /* free plugin instance */ + NULL, // we do not provide this callback + NULL, // we do not provide this callback + handlePluginEvent, + getAuthenticationData, + getAuthorizationData, +}; + +bRC loadPlugin(bDirInfo *lbinfo, bDirFuncs *lbfuncs, pDirInfo **pinfo, pDirFuncs **pfuncs) +{ + bfuncs = lbfuncs; /* set Bacula funct pointers */ + binfo = lbinfo; + Dmsg2(DINFO, "Loaded: size=%d version=%d\n", bfuncs->size, bfuncs->version); + + *pinfo = &pluginInfo; /* return pointer to our info */ + *pfuncs = &pluginFuncs; /* return pointer to our functions */ + + return bRC_OK; +} + +bRC unloadPlugin() +{ + Dmsg0(DINFO, "plugin: Unloaded\n"); + return bRC_OK; +} + +static bRC newPlugin(bpContext *ctx) +{ + BPAMLDAP *self = New (BPAMLDAP(ctx)); + DMSG0(ctx, DINFO, "newPlugin\n"); + ctx->pContext = (void*)self; + + return bRC_OK; +} + +static bRC freePlugin(bpContext *ctx) +{ + DMSG0(ctx, DINFO, "freePlugin\n"); + if (!ctx){ + return bRC_Error; + } + + BPAMLDAP *self = (BPAMLDAP*)ctx->pContext; + + if (!self){ + return bRC_Error; + } + delete self; + + return bRC_OK; +} + +static bRC handlePluginEvent(bpContext *ctx, bDirEvent *event, void *value) +{ + DMSG(ctx, D1, "handlePluginEvent (%i)\n", event->eventType); + BPAMLDAP *self = (BPAMLDAP*)ctx->pContext; + return self->handlePluginEvent(event, value); +} + +static bRC getAuthenticationData(bpContext *ctx, const char *param, void **data) +{ + DMSG(ctx, D1, "getAuthenticationData (%s)\n", param); + BPAMLDAP *self = (BPAMLDAP*)ctx->pContext; + return self->getAuthenticationData(param, data); +} + +static bRC getAuthorizationData(bpContext *ctx, const char *param, void **data) +{ + DMSG(ctx, D1, "getAuthorizationData (%s)\n", param); + BPAMLDAP *self = (BPAMLDAP*)ctx->pContext; + return self->getAuthorizationData(param, data); +} + +#ifdef __cplusplus +} +#endif + +/** + * @brief display common connection error message and diagnose + * + * @param ret the return code from ldap_* functions to analize and display + */ +void BPAMLDAP::ldapserverconnectionerror(int ret) +{ + char *errmsg = NULL; + + ldap_get_option(ld, LDAP_OPT_DIAGNOSTIC_MESSAGE, (void*)&errmsg); + DMSG1(ctx, D1, "LDAP Server connection error: %s\n", ldap_err2string(ret)); + if (errmsg){ + DMSG1(ctx, D1, "diagnose error: %s\n", errmsg); + } + ldap_memfree(errmsg); +} + +/** + * @brief connect to LDAP service + * + * @return bRC on success bRC_OK, on error bRC_Error + */ +bRC BPAMLDAP::ldapconnect() +{ + const int desired_version = LDAP_VERSION3; + POOL_MEM tmp(PM_FNAME); + int ret; + struct berval cred; + struct berval *msgidp = NULL; + bool starttls = false; + bool starttlsforce = false; + + // first initialize ldap connection context `LDAP` +#ifdef __sun__ + // In Solaris our `url` parameter is a host name only and not a real URL. + // It should be notice in documentation + if ((ld = ldap_init(url.c_str(), LDAP_PORT)) == NULL) { + return bRC_Error; + } +#else + if (ldap_initialize(&ld, url.c_str()) != LDAP_SUCCESS) { + return bRC_Error; + } +#endif + + // required a desired LDAP protocol version +#ifdef __sun__ + if ((ret = ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &desired_version)) != LDAP_SUCCESS) +#else + if ((ret = ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &desired_version)) != LDAP_OPT_SUCCESS) +#endif + { + ldapserverconnectionerror(ret); + return bRC_Error; + } + + // disable referals return in queries - we do not support it +#ifdef __sun__ + if ((ret = ldap_set_option(ld, LDAP_OPT_REFERRALS, LDAP_OPT_OFF)) != LDAP_SUCCESS) +#else + if ((ret = ldap_set_option(ld, LDAP_OPT_REFERRALS, LDAP_OPT_OFF)) != LDAP_OPT_SUCCESS) +#endif + { + ldapserverconnectionerror(ret); + return bRC_Error; + } + + // handle starttls if requested in configuration +#ifdef HAVE_LDAP_START_TLS + // + // Start TLS support + // + // To enable StartTLS for LDAP connection you have to setup 'starttls' or 'starttlsforce' plugin options + // where 'starttlsforce' will fail connection if server do not support it. + // + if (starttls || starttlsforce){ + DMSG0(ctx, D2, "executing ldap_start_tls_s\n"); + if ((ret = ldap_start_tls_s(ld, NULL, NULL)) != LDAP_SUCCESS) + { + ldapserverconnectionerror(ret); + if (starttlsforce) + { + DMSG0(ctx, D1, "STARTTLSFORCE set, cannot continue!\n"); + return bRC_Error; + } + } + } +#endif /* HAVE_LDAP_START_TLS */ + + DMSG2(ctx, D1, "credentials to connect: binddn='%s' pass='%s'\n", binddn.c_str(), bindpass.c_str()); + + cred.bv_val = bindpass.c_str(); + cred.bv_len = strlen(cred.bv_val); + + if ((ret = ldap_sasl_bind_s(ld, binddn.c_str(), LDAP_SASL_SIMPLE, &cred, NULL, NULL, &msgidp)) != LDAP_SUCCESS) + { + ldapserverconnectionerror(ret); + if (strcasestr(url.c_str(), "ldaps://") != NULL || starttls || starttlsforce){ + DMSG0(ctx, M_INFO, "Using TLS/SSL for LDAP service require CA certificate configuration on the backup server.\n"); + DMSG0(ctx, M_INFO, "If it is not configured properly a connection over TLS/SSL will fail.\n"); + } + return bRC_Error; + } + + DMSG0(ctx, DDEBUG, "LDAP connection successful\n"); + return bRC_OK; +} + +/** + * @brief disconnect from LDAP service + * + * @return bRC on success bRC_OK, on error bRC_Error + */ +bRC BPAMLDAP::ldapdisconnect() +{ + int rc; + + rc = ldap_unbind_ext(ld, NULL, NULL); + if (rc != LDAP_SUCCESS) { + return bRC_Error; + } + + return bRC_OK; +} + +/** + * @brief check if supplied dn exist in directory + * + * @param dn checked dn + * @return true dn exist in directory + * @return false dn does not exist + */ +bool BPAMLDAP::ldapsearchonedn() +{ + int rc; + bool ret = false; + int type; + const char *attrs[] = { "cn", NULL }; + LDAPMessage *msg = NULL; + const char *dn; + + DMSG2(ctx, D3, "ldapsearchonedn for: %s and filter: %s\n", basedn.c_str(), filter.c_str()); + + rc = ldap_search_ext_s(ld, basedn.c_str(), LDAP_SCOPE_SUB, filter.c_str(), (char **)attrs, + 0, NULL, NULL, NULL, LDAP_NO_LIMIT, &msg); + + switch (rc){ + case LDAP_NO_SUCH_OBJECT: /* object not found */ + case LDAP_REFERRAL: /* for LDAP we reached an out of scope query */ + DMSG0(ctx, DDEBUG, "no such object or referral found\n"); + break; + case LDAP_SUCCESS: + type = ldap_msgtype(msg); + DMSG(ctx, D3, "ldapsearchonedn resulting msgtype: %i\n", type); + if (type == LDAP_RES_SEARCH_ENTRY){ + ret = true; + dn = ldap_get_dn(ld, msg); + if (!dn){ + DMSG0(ctx, DERROR, "ldapsearchonedn cannot get entry DN!\n"); + ret = false; + } else { + DMSG1(ctx, DDEBUG, "ldapsearchonedn get DN: %s\n", dn); + pm_strcpy(userdn, dn); + } + } + break; + default: + /* we reach some error */ + DMSG2(ctx, D1, "ldapsearchonedn search error: %s for: %s\n", ldap_err2string(rc), basedn.c_str()); + break; + } + + ldap_msgfree(msg); + + return ret; +} + +/** + * @brief This method perform a variable substitute in filter expression. + * Substituted variables are: + * * %p - for user password response + * * %u - for username response + * Any single `%` character without next `p` or `u` will be simple rewritten without change. + * If you want to rewrite '%p' or '%u' without substituting it with predefined variables then + * you have to write it as: '%%p' or '%%u' respectively. + */ +void BPAMLDAP::substitute_filter_parameters() +{ + if (strlen(filter.c_str()) > 0){ + POOL_MEM tmp(PM_MESSAGE); + char *p; + char *s; + const char *c = NULL; + char *q = s = filter.c_str(); + tmp.c_str()[0] = '\0'; + while ((p = strchr(q, '%')) != NULL) + { + switch (p[1]){ + case 'u': + *p = '\0'; + c = username.c_str(); + p++; + break; + + case 'p': + *p = '\0'; + c = password.c_str(); + p++; + break; + + case '%': + p[1] = '\0'; + p++; + c = ""; + break; + default: + c = NULL; + break; + } + q = p + 1; + if (c){ + pm_strcat(tmp, s); + pm_strcat(tmp, c); + s = q; + } + } + if (s){ + pm_strcat(tmp, s); + } + pm_strcpy(filter, tmp.c_str()); + DMSG1(ctx, DINFO, "filter after substitute: %s\n", filter.c_str()); + } +} + +/** + * @brief Perform a standard LDAP authentication using authentication parameters and user credentials. + * The The LDAP authentication procedure is similar to the one implemented by Apache mod_authnz_ldap + * (check: https://httpd.apache.org/docs/2.4/mod/mod_authnz_ldap.html) which is known as "search/bind". + * In this case Plugin first connects to LDAP Service to search for authenticated user definition + * (using binddn and bindpass) as it requires a proper user DN to do the second part of authentication. + * If found then it will bind to LDAP on behalf of the user with user credentials from bconsole + * interactive query/response session. + * + * @return bRC bRC_OK on successful authentication, bRC_Error when not + */ +bRC BPAMLDAP::do_ldap_authenticate() +{ + // prepare search filter based on response variables + substitute_filter_parameters(); + + // connect using bind credentials + // TODO: we should handle first search without credentials + if (ldapconnect() != bRC_OK){ + return bRC_Error; + } + + // check if user query rerurn required object + if (!ldapsearchonedn()){ + return bRC_Error; + } + + // its all with bind credentials + if (ldapdisconnect() != bRC_OK){ + return bRC_Error; + } + + // now we should connect to LDAP using user credentials + pm_strcpy(binddn, userdn.c_str()); + pm_strcpy(bindpass, password.c_str()); + // login as user + if (ldapconnect() != bRC_OK){ + return bRC_Error; + } + // disconnect + if (ldapdisconnect() != bRC_OK){ + return bRC_Error; + } + + // now we should report login success! + return bRC_OK; +} + +/** + * @brief Hndles plugin events defined in BPAM API. + * + * @param event the event to handle + * @param value a pointer to opional value for selected event + * @return bRC bRC_OK on success, bRC_Error on any error + */ +bRC BPAMLDAP::handlePluginEvent(bDirEvent *event, void *value) +{ + bDirAuthValue *pvalue; + + switch (event->eventType) { + case bDirEventAuthenticationQuestion: + break; + + case bDirEventAuthenticationResponse: + pvalue = (bDirAuthValue *)value; + DMSG_EVENT_STR(event, pvalue->response); + switch (pvalue->seqdata){ + case 0: // username + pm_strcpy(username, pvalue->response); + break; + case 1: // password + pm_strcpy(password, pvalue->response); + break; + default: + return bRC_Error; + } + break; + + case bDirEventAuthenticate: + DMSG_EVENT_PTR(event, value); + return do_ldap_authenticate(); + + default: + break; + } + + return bRC_OK; +} + +/** + * @brief Handles Plugin parameters defined in "Authentication Plugin = ..." Console resource parameter. + * + * @param param a string from "Authentication Plugin = ..." Console resource parameter + * @return bRC bRC_OK when all parameters are valid and parsed, bRC_Error on any error + */ +bRC BPAMLDAP::parse_plugin_params(const char *param) +{ + cmd_parser parser; + + if (!param){ + return bRC_Error; + } + + // and parse command + if (parser.parse_cmd(param) != bRC_OK) { + DMSG0(ctx, DERROR, "Unable to parse Plugin parameters.\n"); + return bRC_Error; + } + + // the first (zero) parameter is a plugin name + if (!bstrcmp(parser.argk[0], PLUGIN_NAME)){ + return bRC_Error; + } + + // iterate over next parameters + for (int i = 1; i < parser.argc; i++) { + if (bstrcmp(parser.argk[i], "url")){ + pm_strcpy(url, parser.argv[i]); + DMSG1(ctx, DDEBUG, "parsed url: %s\n", url.c_str()); + continue; + } else + if (bstrcmp(parser.argk[i], "binddn")){ + pm_strcpy(binddn, parser.argv[i]); + DMSG1(ctx, DDEBUG, "parsed binddn: %s\n", binddn.c_str()); + continue; + } else + if (bstrcmp(parser.argk[i], "bindpass")) { + pm_strcpy(bindpass, parser.argv[i]); + DMSG1(ctx, DDEBUG, "parsed bindpass: %s\n", bindpass.c_str()); + continue; + } else + if (bstrcmp(parser.argk[i], "query")) { + POOL_MEM tmp(PM_MESSAGE); + pm_strcpy(tmp, parser.argv[i]); + char *d = strchr(tmp.c_str(), '/'); + if (!d){ + DMSG1(ctx, DERROR, "Cannot find basedn delimiter in query=%s\n", tmp.c_str()); + return bRC_Error; + } + // separate basedn and filter + *d = '\0'; + pm_strcpy(basedn, tmp.c_str()); + pm_strcpy(filter, d + 1); + DMSG2(ctx, DDEBUG, "parsed query - basedn:%s filter:%s \n", basedn.c_str(), filter.c_str()); + continue; + } else + if (bstrcmp(parser.argk[i], "starttls")) { + starttls = true; + DMSG0(ctx, DDEBUG, "parsed starttls\n"); + continue; + } else + if (bstrcmp(parser.argk[i], "starttlsforce")) { + starttlsforce = true; + DMSG0(ctx, DDEBUG, "parsed starttlsforce\n"); + continue; + } else { + DMSG1(ctx, DERROR, "unknown parameter: %s\n", parser.argk[i]); + return bRC_Error; + } + } + + return bRC_OK; +} + +/** + * @brief Register BPAM LDAP Plugin authentication operations. + * + * @param param a string from "Authentication Plugin = ..." Console resource parameter + * @param data a double pointer used for return value + * @return bRC bRC_OK on success, bRC_Error on any error + */ +bRC BPAMLDAP::getAuthenticationData(const char *param, void **data) +{ + bDirAuthenticationRegister **padata = (bDirAuthenticationRegister **)data; + + DMSG1(ctx, DINFO, "registering with: %s\n", NPRT(param)); + if (parse_plugin_params(param) != bRC_OK){ + return bRC_Error; + } + *padata = &bpamldapregister; + + return bRC_OK; +} + +/** + * @brief Unimplemented + * + * @param param Unimplemented + * @param data Unimplemented + * @return bRC Unimplemented + */ +bRC BPAMLDAP::getAuthorizationData(const char *param, void **data) +{ + return bRC_OK; +} diff --git a/bacula/src/plugins/dir/ldap/src/ldap-dir.h b/bacula/src/plugins/dir/ldap/src/ldap-dir.h new file mode 100644 index 000000000..f6c0f41a3 --- /dev/null +++ b/bacula/src/plugins/dir/ldap/src/ldap-dir.h @@ -0,0 +1,120 @@ +/* + Bacula(R) - The Network Backup Solution + + Copyright (C) 2000-2020 Kern Sibbald + + The original author of Bacula is Kern Sibbald, with contributions + from many others, a complete list can be found in the file AUTHORS. + + You may use this file and others of this release according to the + license defined in the LICENSE file, which includes the Affero General + Public License, v3.0 ("AGPLv3") and some additional permissions and + terms pursuant to its AGPLv3 Section 7. + + This notice must be preserved when any source code is + conveyed and/or propagated. + + Bacula(R) is a registered trademark of Kern Sibbald. +*/ +/* + * This is a Bacula plugin for making a backup and restore of individual LDAP objects. + * Author: Radosław Korzeniewski, radekk@inteos.pl, Inteos Sp. z o.o. + */ + +#ifndef _LDAP_DIR_H_ +#define _LDAP_DIR_H_ + +#include "bacula.h" +#include "dir_plugins.h" +#include "dir_authplugin.h" +// #include "lib/ini.h" +#include "lib/cmd_parser.h" + +/* it is an LDAP authentication plugin, so we need a libldap library */ +#ifdef __WIN32__ +#include +#else +#include +#endif + +#include +#include +#include + +#include "dirpluglib.h" +#include "ldap-util.h" + +/* + * libbac uses its own sscanf implementation which is not compatible with + * libc implementation, unfortunately. usage of bsscanf require format string rewriting. + */ +#ifdef sscanf +#undef sscanf +#endif + +/* Plugin compile time variables */ +#define PLUGIN_LICENSE "Bacula AGPLv3" +#define PLUGIN_AUTHOR "Inteos Sp. z o.o." +#define PLUGIN_DATE "September 2020" +#define PLUGIN_VERSION "0.1.0" +#define PLUGIN_DESCRIPTION "BPAM LDAP plugin (c) Inteos" +#define PLUGIN_NAME "ldap" + +class BPAMLDAP : public SMARTALLOC +{ +private: + POOL_MEM url; // This is our LDAP server url + POOL_MEM binddn; // This is a binddn (user) to make queries at LDAP server + POOL_MEM bindpass; // This is a binddn (user) password + POOL_MEM basedn; // This is a LDAP basedn query starting point + POOL_MEM filter; // This is a LDAP query string for selecting required username + bool starttls; + bool starttlsforce; + + LDAP *ld; // This is out connection to LDAP server + POOL_MEM userdn; + + POOL_MEM username; // here we will save user response to our queries + POOL_MEM password; // here we will save user response to our queries + + bpContext *ctx; + + bRC ldapconnect(); + bRC ldapdisconnect(); + bool ldapsearchonedn(); + void ldapserverconnectionerror(int ret); + + bRC parse_plugin_params(const char *param); + bRC do_ldap_authenticate(); + void substitute_filter_parameters(); + +public: +#if __cplusplus > 201103L + BPAMLDAP() = delete; + ~BPAMLDAP() = default; +#else + BPAMLDAP() {}; + ~BPAMLDAP() {}; +#endif + BPAMLDAP(bpContext *pctx) : + url(PM_FNAME), + binddn(PM_FNAME), + bindpass(PM_NAME), + basedn(PM_FNAME), + filter(PM_FNAME), + starttls(false), + starttlsforce(false), + ld(NULL), + userdn(PM_NAME), + username(PM_NAME), + password(PM_NAME), + ctx(pctx) + {}; + + + bRC handlePluginEvent(bDirEvent *event, void *value); + bRC getAuthenticationData(const char *param, void **data); + bRC getAuthorizationData(const char *param, void **data); +}; + +#endif /* _LDAP_FD_H_ */ diff --git a/bacula/src/plugins/dir/ldap/src/ldap-util.c b/bacula/src/plugins/dir/ldap/src/ldap-util.c new file mode 100644 index 000000000..f5ffcbad6 --- /dev/null +++ b/bacula/src/plugins/dir/ldap/src/ldap-util.c @@ -0,0 +1,229 @@ +/* + Bacula(R) - The Network Backup Solution + + Copyright (C) 2000-2020 Kern Sibbald + + The original author of Bacula is Kern Sibbald, with contributions + from many others, a complete list can be found in the file AUTHORS. + + You may use this file and others of this release according to the + license defined in the LICENSE file, which includes the Affero General + Public License, v3.0 ("AGPLv3") and some additional permissions and + terms pursuant to its AGPLv3 Section 7. + + This notice must be preserved when any source code is + conveyed and/or propagated. + + Bacula(R) is a registered trademark of Kern Sibbald. +*/ +/* + * Common definitions and utility functions for Inteos plugins. + * Functions defines a common framework used in our utilities and plugins. + * LDAP plugin specific functions. + * + * Author: Radosław Korzeniewski, radoslaw@korzeniewski.net, Inteos Sp. z o.o. + */ + +#include "ldap-util.h" +#include "dirpluglib.h" + +/* + * libbac uses its own sscanf implementation which is not compatible with + * libc implementation, unfortunately. usage of bsscanf require format string rewriting. + */ +#ifdef sscanf +#undef sscanf +#endif + +/* Pointers to Bacula functions used in plugins */ +extern bDirFuncs *bfuncs; +extern bDirInfo *binfo; + +/* + * display common connection error message and diagnose + */ +void ldapserverconnectionerror(bpContext * ctx, LDAP *ldapconn, int ret) +{ + char *errmsg = NULL; + + ldap_get_option(ldapconn, LDAP_OPT_DIAGNOSTIC_MESSAGE, (void*)&errmsg); + DMSG1(ctx, D1, "LDAP Server connection error: %s\n", ldap_err2string(ret)); + if (errmsg){ + DMSG1(ctx, D1, "diagnose error: %s\n", errmsg); + } + JMSG1(ctx, M_WARNING, "LDAP Server connection error: %s\n", ldap_err2string(ret)); + if (errmsg){ + JMSG1(ctx, M_WARNING, "diagnose error: %s\n", errmsg); + } + ldap_memfree(errmsg); +} + +/* + * connect to LDAP service + * + * in: + * paramlist - list of parameters from config file + * required params: LDAPURI, BINDDN, BINDPASS, BINDMODE + * out: + * on success: LDAP * - working LDAP connection + * on error: NULL + */ +LDAP *ldapconnect(bpContext *ctx, ldap_paramlist *paramlist) +{ + LDAP *ldapconn = NULL; + const char *ldapuri; + const int desired_version = LDAP_VERSION3; + const char *binddn; + char *bindpass; + POOLMEM *tmp = NULL; + int ret; + struct berval cred; + struct berval *msgidp = NULL; + bool starttls = false; + bool starttlsforce = false; + + ldapuri = paramlist->ldapuri; + +#ifdef __sun__ +/* TODO: change ldapuri to ldaphost + ldapport */ + if ((ldapconn = ldap_init(ldapuri, LDAP_PORT)) == NULL) { + return NULL; + } +#else + if (ldap_initialize(&ldapconn, ldapuri) != LDAP_SUCCESS) { + return NULL; + } +#endif + +#ifdef __sun__ + if ((ret = ldap_set_option(ldapconn, LDAP_OPT_PROTOCOL_VERSION, &desired_version)) != LDAP_SUCCESS) +#else + if ((ret = ldap_set_option(ldapconn, LDAP_OPT_PROTOCOL_VERSION, &desired_version)) != LDAP_OPT_SUCCESS) +#endif + { + ldapserverconnectionerror(ctx, ldapconn, ret); + return NULL; + } +#ifdef __sun__ + if ((ret = ldap_set_option(ldapconn, LDAP_OPT_REFERRALS, LDAP_OPT_OFF)) != LDAP_SUCCESS) +#else + if ((ret = ldap_set_option(ldapconn, LDAP_OPT_REFERRALS, LDAP_OPT_OFF)) != LDAP_OPT_SUCCESS) +#endif + { + ldapserverconnectionerror(ctx, ldapconn, ret); + return NULL; + } + +#ifdef HAVE_LDAP_START_TLS + /* + * Start TLS support + * + * To enable StartTLS for LDAP connection you have to setup 'starttls' + * or 'starttlsforce' plugin options + * where 'starttlsforce' will fail connection if server do not support it. + */ + starttls = paramlist->starttls; + starttlsforce = paramlist->starttlsforce; + + if (starttls || starttlsforce){ + DMSG0(ctx, D2, "executing ldap_start_tls_s\n"); + if ((ret = ldap_start_tls_s(ldapconn, NULL, NULL)) != LDAP_SUCCESS) + { + ldapserverconnectionerror(ctx, ldapconn, ret); + if (starttlsforce) + { + DMSG0(ctx, D1, "STARTTLSFORCE set, cannot continue!\n"); + JMSG0(ctx, M_ERROR, "STARTTLSFORCE set, cannot continue!\n"); + return NULL; + } + } + } +#endif /* HAVE_LDAP_START_TLS */ + + binddn = paramlist->binddn; + + /* The password might be obfuscated */ + bindpass = paramlist->bindpass; + + DMSG2(ctx, D1, "credentials to connect: binddn='%s' pass='%s'\n", binddn, bindpass); + + cred.bv_val = bindpass; + cred.bv_len = strlen(cred.bv_val); + + if ((ret = ldap_sasl_bind_s(ldapconn, binddn, LDAP_SASL_SIMPLE, &cred, NULL, NULL, &msgidp)) != LDAP_SUCCESS) + { + free_and_null_pool_memory(tmp); + ldapserverconnectionerror(ctx, ldapconn, ret); + if (strcasestr(ldapuri, "ldaps://") != NULL || starttls || starttlsforce){ + JMSG0(ctx, M_INFO, "Using TLS/SSL for LDAP service require CA certificate configuration on the backup server.\n"); + JMSG0(ctx, M_INFO, "If it is not configured properly to do a job over TLS/SSL it will fail.\n"); + } + return NULL; + } + + free_and_null_pool_memory(tmp); + return ldapconn; +} + +/* + * disconnect from LDAP service + * + * in: + * ld - ldap connection handle to unbind + * out: + * 0 - on success + * 1 - on error + */ +int ldapdisconnect(bpContext * ctx, LDAP * ld) +{ + int rc; + + rc = ldap_unbind_ext(ld, NULL, NULL); + if (rc != LDAP_SUCCESS) { + return 1; + } + + return 0; +} + +/* + * check if supplied dn exist in directory + * + * in: + * ld - ldap connection + * dn - checked dn + * out: + * True - dn exist in directory + * False - dn does not exist + */ +bool ldapcheckonedn(bpContext * ctx, LDAP * ld, char *dn) +{ + int rc; + bool ret = false; + int type; + LDAPMessage *msg = NULL; + + DMSG(ctx, D3, "ldapcheckonedn for: %s\n", dn); + + rc = ldap_search_ext_s(ld, dn, LDAP_SCOPE_BASE, NULL, NULL, + 0, NULL, NULL, NULL, LDAP_NO_LIMIT, &msg); + + switch (rc){ + case LDAP_NO_SUCH_OBJECT: /* object not found */ + case LDAP_REFERRAL: /* for LDAP we reached an out of scope query */ + break; + case LDAP_SUCCESS: + type = ldap_msgtype(msg); + DMSG(ctx, D3, "ldapcheckonedn: resulting msgtype: %i\n", type); + ret = type == LDAP_RES_SEARCH_ENTRY; + break; + default: + /* we reach some error */ + JMSG2(ctx, M_ERROR, "ldapcheckonedn: search error: %s for: %s\n", ldap_err2string(rc),dn); + DMSG2(ctx, D1, "ldapcheckonedn: search error: %s for: %s\n", ldap_err2string(rc),dn); + } + + ldap_msgfree(msg); + + return ret; +} diff --git a/bacula/src/plugins/dir/ldap/src/ldap-util.h b/bacula/src/plugins/dir/ldap/src/ldap-util.h new file mode 100644 index 000000000..2e8775bb2 --- /dev/null +++ b/bacula/src/plugins/dir/ldap/src/ldap-util.h @@ -0,0 +1,56 @@ +/* + Bacula(R) - The Network Backup Solution + + Copyright (C) 2000-2020 Kern Sibbald + + The original author of Bacula is Kern Sibbald, with contributions + from many others, a complete list can be found in the file AUTHORS. + + You may use this file and others of this release according to the + license defined in the LICENSE file, which includes the Affero General + Public License, v3.0 ("AGPLv3") and some additional permissions and + terms pursuant to its AGPLv3 Section 7. + + This notice must be preserved when any source code is + conveyed and/or propagated. + + Bacula(R) is a registered trademark of Kern Sibbald. +*/ +/* + * Common definitions and utility functions for Inteos plugins. + * Functions defines a common framework used in our utilities and plugins. + * LDAP plugin specific functions. + * + * Author: Radosław Korzeniewski, radoslaw@korzeniewski.net, Inteos Sp. z o.o. + */ + +#ifndef _LDAP_UTIL_H_ +#define _LDAP_UTIL_H_ + +#include "bacula.h" +#include "dir_plugins.h" + +#ifdef __WIN32__ +#include +#else +#include +#endif + +/* Plugin compile time variables */ +#define PLUGINPREFIX "ldap:" +#define PLUGINNAME "LDAP" + +typedef struct s_ldap_paramlist { + const char *ldapuri; + const char *binddn; + char *bindpass; + bool starttls; + bool starttlsforce; +} ldap_paramlist; + +/* util definitions */ +LDAP * ldapconnect(bpContext * ctx, ldap_paramlist * paramlist); +int ldapdisconnect(bpContext * ctx, LDAP * ld); +bool ldapcheckonedn(bpContext * ctx, LDAP * ld, char *dn); + +#endif /* _LDAP_UTIL_H_ */ diff --git a/bacula/src/plugins/dir/ldap/src/ldaptest.c b/bacula/src/plugins/dir/ldap/src/ldaptest.c new file mode 100644 index 000000000..1f7c7f442 --- /dev/null +++ b/bacula/src/plugins/dir/ldap/src/ldaptest.c @@ -0,0 +1,206 @@ +/* + Bacula(R) - The Network Backup Solution + + Copyright (C) 2000-2020 Kern Sibbald + + The original author of Bacula is Kern Sibbald, with contributions + from many others, a complete list can be found in the file AUTHORS. + + You may use this file and others of this release according to the + license defined in the LICENSE file, which includes the Affero General + Public License, v3.0 ("AGPLv3") and some additional permissions and + terms pursuant to its AGPLv3 Section 7. + + This notice must be preserved when any source code is + conveyed and/or propagated. + + Bacula(R) is a registered trademark of Kern Sibbald. +*/ + +#include +#include +#include +#include "config.h" + +/* + gcc/g++ -Wall ldaptest.c -o ldaptest -lldap + ./ldaptest "" "" "" +*/ + +const char *extensionStartTLS_OID = "1.3.6.1.4.1.1466.20037"; +const char *extensionTLSattrib = "supportedExtension"; +const char *extensionSASLattrib = "supportedSASLMechanisms"; + +/* + * connect to LDAP service + */ +LDAP *ldapconnect(char *ldapuri, char *binddn, char *bindpass, bool starttls) +{ + LDAP *ldapconn = NULL; + int desired_version = LDAP_VERSION3; + int ret; + char *errmsg = NULL; + int debug = 0xffff; + struct berval cred; + struct berval *msgidp = NULL; + + if ((ret = ldap_initialize(&ldapconn, ldapuri)) != LDAP_SUCCESS) + { + ldap_get_option(ldapconn, LDAP_OPT_DIAGNOSTIC_MESSAGE, (void*)&errmsg); + printf("LDAP initialize error %s\n", ldap_err2string(ret)); + printf("diagnose error: %s\n", errmsg); + ldap_memfree(errmsg); + return NULL; + } + if ((ret = ldap_set_option(ldapconn, LDAP_OPT_PROTOCOL_VERSION, &desired_version)) != LDAP_OPT_SUCCESS) + { + ldap_get_option(ldapconn, LDAP_OPT_DIAGNOSTIC_MESSAGE, (void*)&errmsg); + printf("LDAP set option error %s\n", ldap_err2string(ret)); + printf("diagnose error: %s\n", errmsg); + ldap_memfree(errmsg); + return NULL; + } + if ((ret = ldap_set_option(ldapconn, LDAP_OPT_REFERRALS, LDAP_OPT_OFF)) != LDAP_OPT_SUCCESS) + { + ldap_get_option(ldapconn, LDAP_OPT_DIAGNOSTIC_MESSAGE, (void*)&errmsg); + printf("LDAP set option error %s\n", ldap_err2string(ret)); + printf("diagnose error: %s\n", errmsg); + ldap_memfree(errmsg); + return NULL; + } + +#ifdef HAVE_LDAP_START_TLS + if (starttls){ + if ((ret = ldap_start_tls_s(ldapconn, NULL, NULL)) != LDAP_SUCCESS) + { + ldap_get_option(ldapconn, LDAP_OPT_DIAGNOSTIC_MESSAGE, (void*)&errmsg); + printf("LDAP start TLS error %s\n", ldap_err2string(ret)); + printf("diagnose error: %s\n", errmsg); + ldap_memfree(errmsg); + return NULL; + } + } +#endif + + if( ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, &debug) != LDAP_OPT_SUCCESS) + { + ldap_get_option(ldapconn, LDAP_OPT_DIAGNOSTIC_MESSAGE, (void*)&errmsg); + printf("LDAP could not set LDAP_OPT_DEBUG_LEVEL %d\n", debug); + printf("diagnose error: %s\n", errmsg); + ldap_memfree(errmsg); + } + + cred.bv_val = bindpass; + cred.bv_len = strlen(cred.bv_val); + + if ((ret = ldap_sasl_bind_s(ldapconn, binddn, LDAP_SASL_SIMPLE, &cred, NULL, NULL, &msgidp) != LDAP_SUCCESS)) + { + ldap_get_option(ldapconn, LDAP_OPT_DIAGNOSTIC_MESSAGE, (void*)&errmsg); + printf("LDAP Server bind error: %s\n", ldap_err2string(ret)); + printf("diagnose error: %s\n", errmsg); + ldap_memfree(errmsg); + return NULL; + } + printf("LDAP Server connection OK\n"); + return ldapconn; +} + +/* + * disconnect from LDAP service + * + * in: + * ld - ldap connection handle to unbind + * out: + * 0 - on success + * 1 - on error + */ +int ldapdisconnect(LDAP * ld) +{ + + int rc; + + rc = ldap_unbind_ext(ld, NULL, NULL); + if (rc != LDAP_SUCCESS) { + return 1; + } + + return 0; +} + +int main(int argc, char **argv) +{ + LDAP *ld; + /* all and extended attributes required */ + const char *attrs[] = { "+", "*", extensionTLSattrib, extensionSASLattrib, NULL }; + const char *filter = "(objectClass=*)"; + LDAPMessage *msg = NULL; + LDAPMessage *entry; + BerElement *berdata; + struct berval **values; + char *attr; + int rc, i; + bool have_start_tls = false; + bool test_start_tls = false; + + if (argc < 4) { + printf("Usage: %s ", argv[0]); +#ifdef HAVE_LDAP_START_TLS + printf ("[starttls]"); +#endif + printf ("\n"); + exit(1); + } + +#ifdef HAVE_LDAP_START_TLS + if (argc > 4 && strcmp(argv[4], "starttls") == 0) { + printf ("> Will test with start tls option.\n"); + test_start_tls = true; + } +#endif + + ld = ldapconnect(argv[1], argv[2], argv[3], test_start_tls); + if (ld) { + i = 0; + ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, &i); + rc = ldap_search_ext_s(ld, "", LDAP_SCOPE_BASE, filter, (char **)attrs, + 0, NULL, NULL, NULL, LDAP_NO_LIMIT, &msg); + + switch (rc){ + case LDAP_SUCCESS: + entry = ldap_first_entry(ld, msg); + if (entry != NULL) { + printf("\nRootDSE:\n"); + for (attr = ldap_first_attribute(ld, entry, &berdata); + attr; + attr = ldap_next_attribute(ld, entry, berdata)){ + if ((values = ldap_get_values_len(ld, entry, attr)) != NULL) { + for (i = 0; values[i] != NULL; i++) { + printf ("\t %s: %s\n", attr, values[i]->bv_val); + if (strcmp(values[i]->bv_val, extensionStartTLS_OID) == 0 && strcmp(attr, extensionTLSattrib) == 0) + { + have_start_tls = true; + } + } + } + ldap_value_free_len(values); + ldap_memfree(attr); + } + } + default: + ldap_msgfree(msg); + } + + ldapdisconnect(ld); + printf ("\nTest successful!\n"); +#ifdef HAVE_LDAP_START_TLS + if (have_start_tls && !test_start_tls) + { + printf ("\nIt seems your LDAP server supports Start TLS Extension, so you could use 'starttls'\n"); + printf ("parameter to verify if you can use it in your configuration.\n\n"); + } +#endif + } else { + printf("\nTest failed. Please check the log above!\n"); + } + exit(0); +} diff --git a/bacula/src/plugins/dir/test-authentication-api-dir.c b/bacula/src/plugins/dir/test-authentication-api-dir.c new file mode 100644 index 000000000..1deac0b81 --- /dev/null +++ b/bacula/src/plugins/dir/test-authentication-api-dir.c @@ -0,0 +1,288 @@ +/* + Bacula(R) - The Network Backup Solution + + Copyright (C) 2000-2020 Kern Sibbald + + The original author of Bacula is Kern Sibbald, with contributions + from many others, a complete list can be found in the file AUTHORS. + + You may use this file and others of this release according to the + license defined in the LICENSE file, which includes the Affero General + Public License, v3.0 ("AGPLv3") and some additional permissions and + terms pursuant to its AGPLv3 Section 7. + + This notice must be preserved when any source code is + conveyed and/or propagated. + + Bacula(R) is a registered trademark of Kern Sibbald. +*/ +/* + * Sample Plugin program + * + * Kern Sibbald, October 2007 + */ +#include "bacula.h" +#include "dir_plugins.h" +#include "dir_authplugin.h" +#include "dirpluglib.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define PLUGIN_LICENSE "AGPLv3" +#define PLUGIN_AUTHOR "Radosław Korzeniewski" +#define PLUGIN_DATE "September 2020" +#define PLUGIN_VERSION "1" +#define PLUGIN_DESCRIPTION "Test Director Auth API Plugin" + +/* Forward referenced functions */ +static bRC newPlugin(bpContext *ctx); +static bRC freePlugin(bpContext *ctx); +static bRC getPluginValue(bpContext *ctx, pDirVariable var, void *value); +static bRC setPluginValue(bpContext *ctx, pDirVariable var, void *value); +static bRC handlePluginEvent(bpContext *ctx, bDirEvent *event, void *value); +static bRC getAuthenticationData(bpContext *ctx, const char *param, void **data); +static bRC getAuthorizationData(bpContext *ctx, const char *param, void **data); + +/* Plugin compile time variables */ +#define PLUGINPREFIX "authapi:" +#define PLUGIN_NAME "test-authentication-api" + +/* Pointers to Bacula functions */ +bDirFuncs *bfuncs = NULL; +bDirInfo *binfo = NULL; + +static pDirInfo pluginInfo = { + sizeof(pluginInfo), + DIR_PLUGIN_INTERFACE_VERSION, + DIR_PLUGIN_MAGIC, + PLUGIN_LICENSE, + PLUGIN_AUTHOR, + PLUGIN_DATE, + PLUGIN_VERSION, + PLUGIN_DESCRIPTION +}; + +static pDirFuncs pluginFuncs = { + sizeof(pluginFuncs), + DIR_PLUGIN_INTERFACE_VERSION, + + /* Entry points into plugin */ + newPlugin, /* new plugin instance */ + freePlugin, /* free plugin instance */ + getPluginValue, + setPluginValue, + handlePluginEvent, + getAuthenticationData, + getAuthorizationData, +}; + +static bDirAuthenticationData testquestions0[] = +{ + // operation; question; data; + {bDirAuthenticationOperationLogin, "Username:", 0}, + {bDirAuthenticationOperationPassword, "Password:", 1}, +}; + +static bDirAuthenticationRegister testregister0 = +{ + .name = PLUGIN_NAME, + .welcome = "This is a test authplugin API Plugin. Use root/root to login.", + .num = 2, + .data = testquestions0, + .nsTTL = 0, +}; + +static bDirAuthenticationData testquestions1[] = +{ + // operation; question; data; + {bDirAuthenticationOperationLogin, "Username:", 0}, + {bDirAuthenticationOperationPlugin, NULL, 1}, + {bDirAuthenticationOperationPlain, "Response:", 2}, +}; + +static bDirAuthenticationData testquestions1_msg = +{ + bDirAuthenticationOperationMessage, NULL, 0 +}; + +static bDirAuthenticationRegister testregister1 = +{ + .name = PLUGIN_NAME, + .welcome = "This is a test authplugin API Plugin. Use bacula username to login.", + .num = 3, + .data = testquestions1, + .nsTTL = 0, +}; + +struct test_api : public SMARTALLOC { + POOL_MEM username{PM_NAME}; + POOL_MEM password{PM_NAME}; + POOL_MEM challenge{PM_NAME}; + POOL_MEM challenge_str{PM_NAME}; + int mode{0}; +}; + +bRC loadPlugin(bDirInfo *lbinfo, bDirFuncs *lbfuncs, pDirInfo **pinfo, pDirFuncs **pfuncs) +{ + bfuncs = lbfuncs; /* set Bacula funct pointers */ + binfo = lbinfo; + Dmsg2(DINFO, "Loaded: size=%d version=%d\n", bfuncs->size, bfuncs->version); + + *pinfo = &pluginInfo; /* return pointer to our info */ + *pfuncs = &pluginFuncs; /* return pointer to our functions */ + + return bRC_OK; +} + +bRC unloadPlugin() +{ + Dmsg0(DINFO, "plugin: Unloaded\n"); + return bRC_OK; +} + +static bRC newPlugin(bpContext *ctx) +{ + test_api *self = New (test_api); + DMSG0(ctx, DINFO, "newPlugin\n"); + ctx->pContext = self; + return bRC_OK; +} + +static bRC freePlugin(bpContext *ctx) +{ + DMSG0(ctx, DINFO, "freePlugin\n"); + if (ctx->pContext){ + test_api *self = (test_api *)ctx->pContext; + delete self; + } + return bRC_OK; +} + +static bRC getPluginValue(bpContext *ctx, pDirVariable var, void *value) +{ + DMSG1(ctx, DINFO, "plugin: getPluginValue var=%d\n", var); + return bRC_OK; +} + +static bRC setPluginValue(bpContext *ctx, pDirVariable var, void *value) +{ + DMSG1(ctx, DINFO, "plugin: setPluginValue var=%d\n", var); + return bRC_OK; +} + +static bRC handlePluginEvent(bpContext *ctx, bDirEvent *event, void *value) +{ + bDirAuthValue *pvalue; + test_api *self = (test_api *)ctx->pContext; + int r; + + switch (event->eventType) { + case bDirEventAuthenticationQuestion: + pvalue = (bDirAuthValue *)value; + switch (self->mode) + { + case 1: + r = rand(); + Mmsg(self->challenge, "%d", r); + Mmsg(self->challenge_str, "You should use %s as a response.", self->challenge.c_str()); + testquestions1_msg.question = self->challenge_str.c_str(); + pvalue->authdata = &testquestions1_msg; + break; + default: + break; + } + break; + + case bDirEventAuthenticationResponse: + pvalue = (bDirAuthValue *)value; + DMSG_EVENT_STR(event, pvalue->response); + switch (self->mode) + { + case 0: + switch (pvalue->seqdata) + { + case 0: + pm_strcpy(self->username, pvalue->response); + break; + case 1: + pm_strcpy(self->password, pvalue->response); + break; + default: + break; + } + case 1: + switch (pvalue->seqdata) + { + case 0: + pm_strcpy(self->username, pvalue->response); + break; + case 2: + pm_strcpy(self->password, pvalue->response); + break; + default: + break; + } + } + break; + + case bDirEventAuthenticate: + DMSG_EVENT_PTR(event, value); + switch (self->mode) + { + case 0: + if (!bstrcmp(self->username.c_str(), "root") || !bstrcmp(self->password.c_str(), "root")) + { + return bRC_Error; + } + break; + case 1: + if (!bstrcmp(self->username.c_str(), "bacula") || !bstrcmp(self->password.c_str(), self->challenge.c_str())) + { + return bRC_Error; + } + break; + default: + return bRC_Error; + } + break; + + default: + break; + } + return bRC_OK; +} + +static bRC getAuthenticationData(bpContext *ctx, const char *param, void **data) +{ + test_api *self = (test_api *)ctx->pContext; + bDirAuthenticationRegister **padata = (bDirAuthenticationRegister **)data; + + self->mode = 0; // this is a default + + DMSG1(ctx, DINFO, "registering with: %s\n", NPRT(param)); + sscanf(param, PLUGIN_NAME ":%d", &self->mode); + switch (self->mode){ + case 1: + DMSG0(ctx, DINFO, "testregister1\n"); + *padata = &testregister1; + break; + default: + DMSG0(ctx, DINFO, "testregister0\n"); + *padata = &testregister0; + break; + } + + return bRC_OK; +} + +static bRC getAuthorizationData(bpContext *ctx, const char *param, void **data) +{ + + return bRC_OK; +} + +#ifdef __cplusplus +} +#endif diff --git a/bacula/src/plugins/fd/pluglib.c b/bacula/src/plugins/fd/pluglib.c index 36612f31b..5f756b10b 100644 --- a/bacula/src/plugins/fd/pluglib.c +++ b/bacula/src/plugins/fd/pluglib.c @@ -27,6 +27,10 @@ #include "pluglib.h" +/* Pointers to Bacula functions used in plugins */ +extern bFuncs *bfuncs; +extern bInfo *binfo; + /* Events that are passed to plugin typedef enum { bEventJobStart = 1, diff --git a/bacula/src/plugins/fd/pluglib.h b/bacula/src/plugins/fd/pluglib.h index 63acffb2a..d7badec8e 100644 --- a/bacula/src/plugins/fd/pluglib.h +++ b/bacula/src/plugins/fd/pluglib.h @@ -35,10 +35,6 @@ #include "bacula.h" #include "fd_plugins.h" -/* Pointers to Bacula functions used in plugins */ -extern bFuncs *bfuncs; -extern bInfo *binfo; - /* definitions */ /* size of different string or query buffers */ #define BUFLEN 4096