/* * 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: 2020-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 #include #include #include #include #include #include #include #include #include #include /* 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(SERV_LISTENER *listener); static int cdc_replace_users(SERV_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)) == 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; } /** * 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->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->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->listener) == 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_NOTICE("%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); CHK_PROTOCOL(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 :SHA1()"); } } 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(SERV_LISTENER *listener) { SERVICE *service = listener->service; char *dpwd = NULL; char *newpasswd = NULL; char *service_user = NULL; char *service_passwd = NULL; if (serviceGetUser(service, &service_user, &service_passwd) == 0) { MXS_ERROR("failed to get service user details for service %s", service->name); return 1; } dpwd = decrypt_password(service->credentials.authdata); 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 */ (void)users_add(listener->users, service->credentials.name, 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(SERV_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; spinlock_acquire(&listener->lock); if (i > 0) { /** Successfully loaded at least one user */ oldusers = listener->users; listener->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->users = newusers; } cdc_set_service_user(listener); spinlock_release(&listener->lock); if (oldusers) { users_free(oldusers); } } return rc; }