587 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			587 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
|  * Copyright (c) 2016 MariaDB Corporation Ab
 | |
|  *
 | |
|  * Use of this software is governed by the Business Source License included
 | |
|  * in the LICENSE.TXT file and at www.mariadb.com/bsl11.
 | |
|  *
 | |
|  * Change Date: 2023-01-01
 | |
|  *
 | |
|  * On the date above, in accordance with the Business Source License, use
 | |
|  * of this software will be governed by version 2 or later of the General
 | |
|  * Public License.
 | |
|  */
 | |
| 
 | |
| /**
 | |
|  * @file cdc_auth.c
 | |
|  *
 | |
|  * CDC Authentication module for handling the checking of clients credentials
 | |
|  * in the CDC protocol.
 | |
|  *
 | |
|  * @verbatim
 | |
|  * Revision History
 | |
|  * Date         Who                     Description
 | |
|  * 11/03/2016   Massimiliano Pinto      Initial version
 | |
|  *
 | |
|  * @endverbatim
 | |
|  */
 | |
| 
 | |
| #define MXS_MODULE_NAME "CDCPlainAuth"
 | |
| 
 | |
| #include <maxscale/authenticator.hh>
 | |
| #include <fcntl.h>
 | |
| #include <sys/stat.h>
 | |
| #include <cdc.hh>
 | |
| #include <maxbase/alloc.h>
 | |
| #include <maxscale/event.hh>
 | |
| #include <maxscale/modulecmd.hh>
 | |
| #include <maxscale/modutil.hh>
 | |
| #include <maxscale/paths.h>
 | |
| #include <maxscale/secrets.h>
 | |
| #include <maxscale/users.h>
 | |
| #include <maxscale/utils.h>
 | |
| 
 | |
| /* Allowed time interval (in seconds) after last update*/
 | |
| #define CDC_USERS_REFRESH_TIME 30
 | |
| /* Max number of load calls within the time interval */
 | |
| #define CDC_USERS_REFRESH_MAX_PER_TIME 4
 | |
| 
 | |
| const char CDC_USERS_FILENAME[] = "cdcusers";
 | |
| 
 | |
| static bool cdc_auth_set_protocol_data(DCB* dcb, GWBUF* buf);
 | |
| static bool cdc_auth_is_client_ssl_capable(DCB* dcb);
 | |
| static int  cdc_auth_authenticate(DCB* dcb);
 | |
| static void cdc_auth_free_client_data(DCB* dcb);
 | |
| 
 | |
| static int cdc_set_service_user(Listener* listener);
 | |
| static int cdc_replace_users(Listener* listener);
 | |
| 
 | |
| static int cdc_auth_check(DCB* dcb,
 | |
|                           CDC_protocol* protocol,
 | |
|                           char* username,
 | |
|                           uint8_t* auth_data,
 | |
|                           unsigned int* flags
 | |
|                           );
 | |
| 
 | |
| static bool cdc_auth_set_client_data(CDC_session* client_data,
 | |
|                                      CDC_protocol* protocol,
 | |
|                                      uint8_t* client_auth_packet,
 | |
|                                      int client_auth_packet_size
 | |
|                                      );
 | |
| 
 | |
| /**
 | |
|  * @brief Add a new CDC user
 | |
|  *
 | |
|  * This function should not be called directly. The module command system will
 | |
|  * call it when necessary.
 | |
|  *
 | |
|  * @param args Arguments for this command
 | |
|  * @return True if user was successfully added
 | |
|  */
 | |
| static bool cdc_add_new_user(const MODULECMD_ARG* args, json_t** output)
 | |
| {
 | |
|     const char* user = args->argv[1].value.string;
 | |
|     size_t userlen = strlen(user);
 | |
|     const char* password = args->argv[2].value.string;
 | |
|     uint8_t phase1[SHA_DIGEST_LENGTH];
 | |
|     uint8_t phase2[SHA_DIGEST_LENGTH];
 | |
|     SHA1((uint8_t*)password, strlen(password), phase1);
 | |
|     SHA1(phase1, sizeof(phase1), phase2);
 | |
| 
 | |
|     size_t data_size = userlen + 2 + SHA_DIGEST_LENGTH * 2;     // Extra for the : and newline
 | |
|     char final_data[data_size];
 | |
|     strcpy(final_data, user);
 | |
|     strcat(final_data, ":");
 | |
|     gw_bin2hex(final_data + userlen + 1, phase2, sizeof(phase2));
 | |
|     final_data[data_size - 1] = '\n';
 | |
| 
 | |
|     SERVICE* service = args->argv[0].value.service;
 | |
|     char path[PATH_MAX + 1];
 | |
|     snprintf(path, PATH_MAX, "%s/%s/", get_datadir(), service->name());
 | |
|     bool rval = false;
 | |
| 
 | |
|     if (mxs_mkdir_all(path, 0777))
 | |
|     {
 | |
|         strcat(path, CDC_USERS_FILENAME);
 | |
|         int fd = open(path, O_WRONLY | O_APPEND | O_CREAT, 0660);
 | |
| 
 | |
|         if (fd != -1)
 | |
|         {
 | |
|             if (write(fd, final_data, sizeof(final_data)) == static_cast<int>(sizeof(final_data)))
 | |
|             {
 | |
|                 MXS_NOTICE("Added user '%s' to service '%s'", user, service->name());
 | |
|                 rval = true;
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 const char* real_err = mxs_strerror(errno);
 | |
|                 MXS_NOTICE("Failed to write to file '%s': %s", path, real_err);
 | |
|                 modulecmd_set_error("Failed to write to file '%s': %s", path, real_err);
 | |
|             }
 | |
| 
 | |
|             close(fd);
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             const char* real_err = mxs_strerror(errno);
 | |
|             MXS_NOTICE("Failed to open file '%s': %s", path, real_err);
 | |
|             modulecmd_set_error("Failed to open file '%s': %s", path, real_err);
 | |
|         }
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|         modulecmd_set_error("Failed to create directory '%s'. Read the MaxScale "
 | |
|                             "log for more details.",
 | |
|                             path);
 | |
|     }
 | |
| 
 | |
|     return rval;
 | |
| }
 | |
| 
 | |
| extern "C"
 | |
| {
 | |
| /**
 | |
|  * The module entry point routine. It is this routine that
 | |
|  * must populate the structure that is referred to as the
 | |
|  * "module object", this is a structure with the set of
 | |
|  * external entry points for this module.
 | |
|  *
 | |
|  * @return The module object
 | |
|  */
 | |
| MXS_MODULE* MXS_CREATE_MODULE()
 | |
| {
 | |
|     static modulecmd_arg_type_t args[] =
 | |
|     {
 | |
|         {MODULECMD_ARG_SERVICE, "Service where the user is added"},
 | |
|         {MODULECMD_ARG_STRING,  "User to add"                    },
 | |
|         {MODULECMD_ARG_STRING,  "Password of the user"           }
 | |
|     };
 | |
| 
 | |
|     modulecmd_register_command("cdc",
 | |
|                                "add_user",
 | |
|                                MODULECMD_TYPE_ACTIVE,
 | |
|                                cdc_add_new_user,
 | |
|                                3,
 | |
|                                args,
 | |
|                                "Add a new CDC user");
 | |
| 
 | |
|     static MXS_AUTHENTICATOR MyObject =
 | |
|     {
 | |
|         NULL,                               /* No initialize entry point */
 | |
|         NULL,                               /* No create entry point */
 | |
|         cdc_auth_set_protocol_data,         /* Extract data into structure   */
 | |
|         cdc_auth_is_client_ssl_capable,     /* Check if client supports SSL  */
 | |
|         cdc_auth_authenticate,              /* Authenticate user credentials */
 | |
|         cdc_auth_free_client_data,          /* Free the client data held in DCB */
 | |
|         NULL,                               /* No destroy entry point */
 | |
|         cdc_replace_users,                  /* Load CDC users */
 | |
|         users_default_diagnostic,           /* Default diagnostic */
 | |
|         users_default_diagnostic_json,      /* Default diagnostic */
 | |
|         NULL                                /* No user reauthentication */
 | |
|     };
 | |
| 
 | |
|     static MXS_MODULE info =
 | |
|     {
 | |
|         MXS_MODULE_API_AUTHENTICATOR,
 | |
|         MXS_MODULE_GA,
 | |
|         MXS_AUTHENTICATOR_VERSION,
 | |
|         "The CDC client to MaxScale authenticator implementation",
 | |
|         "V1.1.0",
 | |
|         MXS_NO_MODULE_CAPABILITIES,
 | |
|         &MyObject,
 | |
|         NULL,       /* Process init. */
 | |
|         NULL,       /* Process finish. */
 | |
|         NULL,       /* Thread init. */
 | |
|         NULL,       /* Thread finish. */
 | |
|         {
 | |
|             {MXS_END_MODULE_PARAMS}
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     return &info;
 | |
| }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @brief Function to easily call authentication check.
 | |
|  *
 | |
|  * @param dcb Request handler DCB connected to the client
 | |
|  * @param protocol  The protocol structure for the connection
 | |
|  * @param username  String containing username
 | |
|  * @param auth_data  The encrypted password for authentication
 | |
|  * @return Authentication status
 | |
|  * @note Authentication status codes are defined in cdc.h
 | |
|  */
 | |
| static int cdc_auth_check(DCB* dcb,
 | |
|                           CDC_protocol* protocol,
 | |
|                           char* username,
 | |
|                           uint8_t* auth_data,
 | |
|                           unsigned int* flags)
 | |
| {
 | |
|     int rval = CDC_STATE_AUTH_FAILED;
 | |
| 
 | |
|     if (dcb->session->listener->users())
 | |
|     {
 | |
|         /* compute SHA1 of auth_data */
 | |
|         uint8_t sha1_step1[SHA_DIGEST_LENGTH] = "";
 | |
|         char hex_step1[2 * SHA_DIGEST_LENGTH + 1] = "";
 | |
| 
 | |
|         gw_sha1_str(auth_data, SHA_DIGEST_LENGTH, sha1_step1);
 | |
|         gw_bin2hex(hex_step1, sha1_step1, SHA_DIGEST_LENGTH);
 | |
| 
 | |
|         if (users_auth(dcb->session->listener->users(), username, hex_step1))
 | |
|         {
 | |
|             rval = CDC_STATE_AUTH_OK;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return rval;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @brief Authenticates a CDC user who is a client to MaxScale.
 | |
|  *
 | |
|  * @param dcb Request handler DCB connected to the client
 | |
|  * @return Authentication status
 | |
|  * @note Authentication status codes are defined in cdc.h
 | |
|  */
 | |
| static int cdc_auth_authenticate(DCB* dcb)
 | |
| {
 | |
|     CDC_protocol* protocol = DCB_PROTOCOL(dcb, CDC_protocol);
 | |
|     CDC_session* client_data = (CDC_session*)dcb->data;
 | |
|     int auth_ret;
 | |
| 
 | |
|     if (0 == strlen(client_data->user))
 | |
|     {
 | |
|         auth_ret = CDC_STATE_AUTH_ERR;
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|         MXS_DEBUG("Receiving connection from '%s'",
 | |
|                   client_data->user);
 | |
| 
 | |
|         auth_ret =
 | |
|             cdc_auth_check(dcb, protocol, client_data->user, client_data->auth_data, client_data->flags);
 | |
| 
 | |
|         /* On failed authentication try to reload users and authenticate again */
 | |
|         if (CDC_STATE_AUTH_OK != auth_ret
 | |
|             && cdc_replace_users(dcb->session->listener.get()) == MXS_AUTH_LOADUSERS_OK)
 | |
|         {
 | |
|             auth_ret = cdc_auth_check(dcb,
 | |
|                                       protocol,
 | |
|                                       client_data->user,
 | |
|                                       client_data->auth_data,
 | |
|                                       client_data->flags);
 | |
|         }
 | |
| 
 | |
|         /* on successful authentication, set user into dcb field */
 | |
|         if (CDC_STATE_AUTH_OK == auth_ret)
 | |
|         {
 | |
|             dcb->user = MXS_STRDUP_A(client_data->user);
 | |
|         }
 | |
|         else if (dcb->service->log_auth_warnings)
 | |
|         {
 | |
|             MXS_LOG_EVENT(maxscale::event::AUTHENTICATION_FAILURE,
 | |
|                           "%s: login attempt for user '%s', authentication failed.",
 | |
|                           dcb->service->name(),
 | |
|                           client_data->user);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return auth_ret;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @brief Transfer data from the authentication request to the DCB.
 | |
|  *
 | |
|  * The request handler DCB has a field called data that contains protocol
 | |
|  * specific information. This function examines a buffer containing CDC
 | |
|  * authentication data and puts it into a structure that is referred to
 | |
|  * by the DCB. If the information in the buffer is invalid, then a failure
 | |
|  * code is returned. A call to cdc_auth_set_client_data does the
 | |
|  * detailed work.
 | |
|  *
 | |
|  * @param dcb Request handler DCB connected to the client
 | |
|  * @param buffer Pointer to pointer to buffer containing data from client
 | |
|  * @return True on success, false on error
 | |
|  */
 | |
| static bool cdc_auth_set_protocol_data(DCB* dcb, GWBUF* buf)
 | |
| {
 | |
|     uint8_t* client_auth_packet = GWBUF_DATA(buf);
 | |
|     CDC_protocol* protocol = NULL;
 | |
|     CDC_session* client_data = NULL;
 | |
|     int client_auth_packet_size = 0;
 | |
| 
 | |
|     protocol = DCB_PROTOCOL(dcb, CDC_protocol);
 | |
|     if (dcb->data == NULL)
 | |
|     {
 | |
|         if (NULL == (client_data = (CDC_session*)MXS_CALLOC(1, sizeof(CDC_session))))
 | |
|         {
 | |
|             return false;
 | |
|         }
 | |
|         dcb->data = client_data;
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|         client_data = (CDC_session*)dcb->data;
 | |
|     }
 | |
| 
 | |
|     client_auth_packet_size = gwbuf_length(buf);
 | |
| 
 | |
|     return cdc_auth_set_client_data(client_data,
 | |
|                                     protocol,
 | |
|                                     client_auth_packet,
 | |
|                                     client_auth_packet_size);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @brief Transfer detailed data from the authentication request to the DCB.
 | |
|  *
 | |
|  * The caller has created the data structure pointed to by the DCB, and this
 | |
|  * function fills in the details. If problems are found with the data, the
 | |
|  * return code indicates failure.
 | |
|  *
 | |
|  * @param client_data The data structure for the DCB
 | |
|  * @param protocol The protocol structure for this connection
 | |
|  * @param client_auth_packet The data from the buffer received from client
 | |
|  * @param client_auth_packet size An integer giving the size of the data
 | |
|  * @return True on success, false on error
 | |
|  */
 | |
| static bool cdc_auth_set_client_data(CDC_session* client_data,
 | |
|                                      CDC_protocol* protocol,
 | |
|                                      uint8_t* client_auth_packet,
 | |
|                                      int client_auth_packet_size)
 | |
| {
 | |
|     if (client_auth_packet_size % 2 != 0)
 | |
|     {
 | |
|         /** gw_hex2bin expects an even number of bytes */
 | |
|         client_auth_packet_size--;
 | |
|     }
 | |
| 
 | |
|     bool rval = false;
 | |
|     int decoded_size = client_auth_packet_size / 2;
 | |
|     char decoded_buffer[decoded_size + 1];      // Extra for terminating null
 | |
| 
 | |
|     /* decode input data */
 | |
|     if (client_auth_packet_size <= CDC_USER_MAXLEN)
 | |
|     {
 | |
|         gw_hex2bin((uint8_t*)decoded_buffer,
 | |
|                    (const char*)client_auth_packet,
 | |
|                    client_auth_packet_size);
 | |
|         decoded_buffer[decoded_size] = '\0';
 | |
|         char* tmp_ptr = strchr(decoded_buffer, ':');
 | |
| 
 | |
|         if (tmp_ptr)
 | |
|         {
 | |
|             size_t user_len = tmp_ptr - decoded_buffer;
 | |
|             *tmp_ptr++ = '\0';
 | |
|             size_t auth_len = decoded_size - (tmp_ptr - decoded_buffer);
 | |
| 
 | |
|             if (user_len <= CDC_USER_MAXLEN && auth_len == SHA_DIGEST_LENGTH)
 | |
|             {
 | |
|                 strcpy(client_data->user, decoded_buffer);
 | |
|                 memcpy(client_data->auth_data, tmp_ptr, auth_len);
 | |
|                 rval = true;
 | |
|             }
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             MXS_ERROR("Authentication failed, the decoded client authentication "
 | |
|                       "packet is malformed. Expected <username>:SHA1(<password>)");
 | |
|         }
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|         MXS_ERROR("Authentication failed, client authentication packet length "
 | |
|                   "exceeds the maximum allowed length of %d bytes.",
 | |
|                   CDC_USER_MAXLEN);
 | |
|     }
 | |
| 
 | |
|     return rval;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @brief Determine whether the client is SSL capable
 | |
|  *
 | |
|  * The authentication request from the client will indicate whether the client
 | |
|  * is expecting to make an SSL connection. The information has been extracted
 | |
|  * in the previous functions.
 | |
|  *
 | |
|  * @param dcb Request handler DCB connected to the client
 | |
|  * @return Boolean indicating whether client is SSL capable
 | |
|  */
 | |
| static bool cdc_auth_is_client_ssl_capable(DCB* dcb)
 | |
| {
 | |
|     return false;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @brief Free the client data pointed to by the passed DCB.
 | |
|  *
 | |
|  * Currently all that is required is to free the storage pointed to by
 | |
|  * dcb->data.  But this is intended to be implemented as part of the
 | |
|  * authentication API at which time this code will be moved into the
 | |
|  * CDC authenticator.  If the data structure were to become more complex
 | |
|  * the mechanism would still work and be the responsibility of the authenticator.
 | |
|  * The DCB should not know authenticator implementation details.
 | |
|  *
 | |
|  * @param dcb Request handler DCB connected to the client
 | |
|  */
 | |
| static void cdc_auth_free_client_data(DCB* dcb)
 | |
| {
 | |
|     MXS_FREE(dcb->data);
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * Add the service user to CDC dbusers (service->users)
 | |
|  * via cdc_users_alloc
 | |
|  *
 | |
|  * @param service   The current service
 | |
|  * @return      0 on success, 1 on failure
 | |
|  */
 | |
| static int cdc_set_service_user(Listener* listener)
 | |
| {
 | |
|     SERVICE* service = listener->service();
 | |
|     char* dpwd = NULL;
 | |
|     char* newpasswd = NULL;
 | |
|     const char* service_user = NULL;
 | |
|     const char* service_passwd = NULL;
 | |
| 
 | |
|     serviceGetUser(service, &service_user, &service_passwd);
 | |
|     dpwd = decrypt_password(service_passwd);
 | |
| 
 | |
|     if (!dpwd)
 | |
|     {
 | |
|         MXS_ERROR("decrypt password failed for service user %s, service %s",
 | |
|                   service_user,
 | |
|                   service->name());
 | |
| 
 | |
|         return 1;
 | |
|     }
 | |
| 
 | |
|     newpasswd = create_hex_sha1_sha1_passwd(dpwd);
 | |
| 
 | |
|     if (!newpasswd)
 | |
|     {
 | |
|         MXS_ERROR("create hex_sha1_sha1_password failed for service user %s",
 | |
|                   service_user);
 | |
| 
 | |
|         MXS_FREE(dpwd);
 | |
|         return 1;
 | |
|     }
 | |
| 
 | |
|     /* add service user */
 | |
|     const char* user;
 | |
|     const char* password;
 | |
|     serviceGetUser(service, &user, &password);
 | |
|     users_add(listener->users(), user, newpasswd, USER_ACCOUNT_ADMIN);
 | |
| 
 | |
|     MXS_FREE(newpasswd);
 | |
|     MXS_FREE(dpwd);
 | |
| 
 | |
|     return 0;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Load the AVRO users
 | |
|  *
 | |
|  * @param service    Current service
 | |
|  * @param usersfile  File with users
 | |
|  * @return -1 on error or users loaded (including 0)
 | |
|  */
 | |
| 
 | |
| static int cdc_read_users(USERS* users, char* usersfile)
 | |
| {
 | |
|     FILE* fp;
 | |
|     int loaded = 0;
 | |
|     char* avro_user;
 | |
|     char* user_passwd;
 | |
|     /* user maxlen ':' password hash  '\n' '\0' */
 | |
|     char read_buffer[CDC_USER_MAXLEN + 1 + SHA_DIGEST_LENGTH + 1 + 1];
 | |
| 
 | |
|     int max_line_size = sizeof(read_buffer) - 1;
 | |
| 
 | |
|     if ((fp = fopen(usersfile, "r")) == NULL)
 | |
|     {
 | |
|         return -1;
 | |
|     }
 | |
| 
 | |
|     while (!feof(fp))
 | |
|     {
 | |
|         if (fgets(read_buffer, max_line_size, fp) != NULL)
 | |
|         {
 | |
|             char* tmp_ptr = read_buffer;
 | |
| 
 | |
|             if ((tmp_ptr = strchr(read_buffer, ':')) != NULL)
 | |
|             {
 | |
|                 *tmp_ptr++ = '\0';
 | |
|                 avro_user = read_buffer;
 | |
|                 user_passwd = tmp_ptr;
 | |
|                 if ((tmp_ptr = strchr(user_passwd, '\n')) != NULL)
 | |
|                 {
 | |
|                     *tmp_ptr = '\0';
 | |
|                 }
 | |
| 
 | |
|                 /* add user */
 | |
|                 users_add(users, avro_user, user_passwd, USER_ACCOUNT_ADMIN);
 | |
| 
 | |
|                 loaded++;
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     fclose(fp);
 | |
| 
 | |
|     return loaded;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * @brief Replace the user/passwd in the servicei users tbale from a db file
 | |
|  *
 | |
|  * @param service The current service
 | |
|  */
 | |
| int cdc_replace_users(Listener* listener)
 | |
| {
 | |
|     int rc = MXS_AUTH_LOADUSERS_ERROR;
 | |
|     USERS* newusers = users_alloc();
 | |
| 
 | |
|     if (newusers)
 | |
|     {
 | |
|         char path[PATH_MAX + 1];
 | |
|         snprintf(path,
 | |
|                  PATH_MAX,
 | |
|                  "%s/%s/%s",
 | |
|                  get_datadir(),
 | |
|                  listener->service()->name(),
 | |
|                  CDC_USERS_FILENAME);
 | |
| 
 | |
|         int i = cdc_read_users(newusers, path);
 | |
|         USERS* oldusers = NULL;
 | |
| 
 | |
|         if (i > 0)
 | |
|         {
 | |
|             /** Successfully loaded at least one user */
 | |
|             oldusers = listener->users();
 | |
|             listener->set_users(newusers);
 | |
|             rc = MXS_AUTH_LOADUSERS_OK;
 | |
|         }
 | |
|         else if (listener->users())
 | |
|         {
 | |
|             /** Failed to load users, use the old users table */
 | |
|             users_free(newusers);
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             /** No existing users, use the new empty users table */
 | |
|             listener->set_users(newusers);
 | |
|         }
 | |
| 
 | |
|         cdc_set_service_user(listener);
 | |
| 
 | |
|         if (oldusers)
 | |
|         {
 | |
|             users_free(oldusers);
 | |
|         }
 | |
|     }
 | |
|     return rc;
 | |
| }
 | 
