/*
 *   This program is is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or (at
 *   your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

/**
 * $Id: 8b4a3eeef191e0560372745f747c4f7c25ddef6a $
 * @file auth_wbclient.c
 * @brief NTLM authentication against the wbclient library
 *
 * @copyright 2015  Matthew Newton
 */

RCSID("$Id: 8b4a3eeef191e0560372745f747c4f7c25ddef6a $")

#include <freeradius-devel/radiusd.h>
#include <freeradius-devel/rad_assert.h>

#include <core/ntstatus.h>

#include "rlm_mschap.h"
#include "mschap.h"
#include "auth_wbclient.h"

#define NT_LENGTH 24

/** Use Winbind to normalise a username
 *
 * @param[in] tctx The talloc context where the result is parented from
 * @param[in] ctx The winbind context
 * @param[in] dom_name The domain of the user
 * @param[in] name The username (without the domain) to be normalised
 * @return The username with the casing according to the Winbind remote server,
 *         or NULL if the username could not be found.
 */
static char *wbclient_normalise_username(TALLOC_CTX *tctx, struct wbcContext *ctx, char const *dom_name, char const *name)
{
	struct wbcDomainSid sid;
	enum wbcSidType name_type;
	wbcErr err;
	char *res_domain = NULL;
	char *res_name = NULL;
	char *res = NULL;

	/* Step 1: Convert a name to a sid */
	err = wbcCtxLookupName(ctx, dom_name, name, &sid, &name_type);
	if (!WBC_ERROR_IS_OK(err))
		return NULL;

	/* Step 2: Convert the sid back to a name */
	err = wbcCtxLookupSid(ctx, &sid, &res_domain, &res_name, &name_type);
	if (!WBC_ERROR_IS_OK(err))
		return NULL;

	MEM(res = talloc_strdup(tctx, res_name));

	wbcFreeMemory(res_domain);
	wbcFreeMemory(res_name);

	return res;
}

/*
 *	Check NTLM authentication direct to winbind via
 *	Samba's libwbclient library
 *
 *	Returns:
 *	 0    success
 *	 -1   auth failure
 *	 -2   failed connecting to AD
 *	 -648 password expired
 */
int do_auth_wbclient(rlm_mschap_t *inst, REQUEST *request,
		     uint8_t const *challenge, uint8_t const *response,
		     uint8_t nthashhash[NT_DIGEST_LENGTH])
{
	int rcode = -1;
	struct wbcContext *wb_ctx = NULL;
	struct wbcAuthUserParams authparams;
	wbcErr err;
	int len;
	struct wbcAuthUserInfo *info = NULL;
	struct wbcAuthErrorInfo *error = NULL;
	char user_name_buf[500];
	char domain_name_buf[500];
	uint8_t resp[NT_LENGTH];

	/*
	 * Clear the auth parameters - this is important, as
	 * there are options that will cause wbcAuthenticateUserEx
	 * to bomb out if not zero.
	 */
	memset(&authparams, 0, sizeof(authparams));

	/*
	 * wb_username must be set for this function to be called
	 */
	rad_assert(inst->wb_username);

	/*
	 * Get the username and domain from the configuration
	 */
	len = tmpl_expand(&authparams.account_name, user_name_buf, sizeof(user_name_buf),
			  request, inst->wb_username, NULL, NULL);
	if (len < 0) {
		REDEBUG2("Unable to expand winbind_username");
		goto done;
	}

	if (inst->wb_domain) {
		len = tmpl_expand(&authparams.domain_name, domain_name_buf, sizeof(domain_name_buf),
				  request, inst->wb_domain, NULL, NULL);
		if (len < 0) {
			REDEBUG2("Unable to expand winbind_domain");
			goto done;
		}
	} else {
		RWDEBUG2("No domain specified; authentication may fail because of this");
	}


	/*
	 * Build the wbcAuthUserParams structure with what we know
	 */
	authparams.level = WBC_AUTH_USER_LEVEL_RESPONSE;
	authparams.password.response.nt_length = NT_LENGTH;

	memcpy(resp, response, NT_LENGTH);
	authparams.password.response.nt_data = resp;

	memcpy(authparams.password.response.challenge, challenge,
	       sizeof(authparams.password.response.challenge));

	authparams.parameter_control |= WBC_MSV1_0_ALLOW_MSVCHAPV2 |
					WBC_MSV1_0_ALLOW_WORKSTATION_TRUST_ACCOUNT |
					WBC_MSV1_0_ALLOW_SERVER_TRUST_ACCOUNT;


	/*
	 * Send auth request across to winbind
	 */
	wb_ctx = fr_connection_get(inst->wb_pool);
	if (wb_ctx == NULL) {
		RERROR("Unable to get winbind connection from pool");
		goto done;
	}

	RDEBUG2("sending authentication request user='%s' domain='%s'", authparams.account_name,
									authparams.domain_name);

	err = wbcCtxAuthenticateUserEx(wb_ctx, &authparams, &info, &error);

	if (err == WBC_ERR_AUTH_ERROR && inst->wb_retry_with_normalised_username) {
		VALUE_PAIR *vp_response, *vp_challenge;
		char *normalised_username = wbclient_normalise_username(request, wb_ctx, authparams.domain_name, authparams.account_name);
		if (normalised_username) {
			RDEBUG2("Starting retry, normalised username %s to %s", authparams.account_name, normalised_username);
			if (strcmp(authparams.account_name, normalised_username) != 0) {
				authparams.account_name = normalised_username;

				/* Set PW_MS_CHAP_USER_NAME */
				if (!fr_pair_make(request->packet, &request->packet->vps, "MS-CHAP-User-Name", normalised_username, T_OP_SET)) {
					RERROR("Failed creating MS-CHAP-User-Name");
					goto normalised_username_retry_failure;
				}

				RDEBUG2("retrying authentication request user='%s' domain='%s'", authparams.account_name,
												authparams.domain_name);

				/* Recalculate hash */
				if (!(vp_challenge = fr_pair_find_by_num(request->packet->vps, PW_MSCHAP_CHALLENGE, VENDORPEC_MICROSOFT, TAG_ANY))) {
					RERROR("Unable to get MS-CHAP-Challenge");
					goto normalised_username_retry_failure;
				}
				if (!(vp_response = fr_pair_find_by_num(request->packet->vps, PW_MSCHAP2_RESPONSE, VENDORPEC_MICROSOFT, TAG_ANY))) {
					RERROR("Unable to get MS-CHAP2-Response");
					goto normalised_username_retry_failure;
				}
				mschap_challenge_hash(vp_response->vp_octets + 2,
									vp_challenge->vp_octets,
									normalised_username,
									authparams.password.response.challenge);

				err = wbcCtxAuthenticateUserEx(wb_ctx, &authparams, &info, &error);
			}
normalised_username_retry_failure:
			talloc_free(normalised_username);
		}
	}

	fr_connection_release(inst->wb_pool, wb_ctx);

	/*
	 * Try and give some useful feedback on what happened. There are only
	 * a few errors that can actually be returned from wbcCtxAuthenticateUserEx.
	 */
	switch (err) {
	case WBC_ERR_SUCCESS:
		rcode = 0;
		RDEBUG2("Authenticated successfully");
		/* Grab the nthashhash from the result */
		memcpy(nthashhash, info->user_session_key, NT_DIGEST_LENGTH);
		break;
	case WBC_ERR_WINBIND_NOT_AVAILABLE:
		rcode = -2;
		RERROR("Unable to contact winbind!");
		RDEBUG2("Check that winbind is running and that FreeRADIUS has");
		RDEBUG2("permission to connect to the winbind privileged socket.");
		break;
	case WBC_ERR_DOMAIN_NOT_FOUND:
		REDEBUG2("Domain not found");
		break;
	case WBC_ERR_AUTH_ERROR:
		if (!error) {
			REDEBUG2("Authentication failed");
			break;
		}

		/*
		 * The password needs to be changed, so set rcode appropriately.
		 */
		if (error->nt_status == NT_STATUS_PASSWORD_EXPIRED ||
		    error->nt_status == NT_STATUS_PASSWORD_MUST_CHANGE) {
			rcode = -648;
		}

		/*
		 * Return the NT_STATUS human readable error string, if there is one.
		 */
		if (error->display_string) {
			REDEBUG2("%s [0x%X]", error->display_string, error->nt_status);
		} else {
			REDEBUG2("Authentication failed [0x%X]", error->nt_status);
		}
		break;
	default:
		/*
		 * Only errors left are 
		 *   WBC_ERR_INVALID_PARAM
		 *   WBC_ERR_NO_MEMORY
		 * neither of which are particularly likely.
		 */
		rcode = -2;
		if (error && error->display_string) {
			REDEBUG2("libwbclient error: wbcErr %d (%s)", err, error->display_string);
		} else {
			REDEBUG2("libwbclient error: wbcErr %d", err);
		}
		break;
	}


done:
	if (info) wbcFreeMemory(info);
	if (error) wbcFreeMemory(error);

	return rcode;
}

