2016-01-18 19:30:46 +01:00
|
|
|
/* Copyright (c) 2015, Shuang Qiu, Robbie Harwood,
|
2016-01-14 13:31:08 +01:00
|
|
|
Vladislav Vaintroub & MariaDB Corporation
|
|
|
|
|
|
|
|
All rights reserved.
|
|
|
|
|
|
|
|
Redistribution and use in source and binary forms, with or without
|
|
|
|
modification, are permitted provided that the following conditions are met:
|
|
|
|
|
|
|
|
1. Redistributions of source code must retain the above copyright notice,
|
|
|
|
this list of conditions and the following disclaimer.
|
|
|
|
|
|
|
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
|
|
this list of conditions and the following disclaimer in the documentation
|
|
|
|
and/or other materials provided with the distribution.
|
|
|
|
|
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
|
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
|
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
|
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
|
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
|
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
|
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
|
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
|
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
|
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
|
|
POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "sspi.h"
|
|
|
|
#include "common.h"
|
|
|
|
#include "server_plugin.h"
|
|
|
|
#include <mysql/plugin_auth.h>
|
|
|
|
#include <mysqld_error.h>
|
2020-11-05 23:38:18 +01:00
|
|
|
#include <sddl.h>
|
2016-01-14 13:31:08 +01:00
|
|
|
|
|
|
|
/* This sends the error to the client */
|
|
|
|
static void log_error(SECURITY_STATUS err, const char *msg)
|
|
|
|
{
|
|
|
|
if (err)
|
|
|
|
{
|
|
|
|
char buf[1024];
|
|
|
|
sspi_errmsg(err, buf, sizeof(buf));
|
2018-10-16 09:19:03 +01:00
|
|
|
my_printf_error(ER_UNKNOWN_ERROR, "SSPI server error 0x%x - %s - %s", 0, err, msg, buf);
|
2016-01-14 13:31:08 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2017-08-11 13:58:32 +02:00
|
|
|
my_printf_error(ER_UNKNOWN_ERROR, "SSPI server error %s", 0, msg);
|
2016-01-14 13:31:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
static char INVALID_KERBEROS_PRINCIPAL[] = "localhost";
|
|
|
|
|
|
|
|
static char *get_default_principal_name()
|
|
|
|
{
|
|
|
|
static char default_principal[PRINCIPAL_NAME_MAX +1];
|
|
|
|
ULONG size= sizeof(default_principal);
|
|
|
|
|
|
|
|
if (GetUserNameEx(NameUserPrincipal,default_principal,&size))
|
|
|
|
return default_principal;
|
|
|
|
|
|
|
|
size= sizeof(default_principal);
|
|
|
|
if (GetUserNameEx(NameServicePrincipal,default_principal,&size))
|
|
|
|
return default_principal;
|
|
|
|
|
|
|
|
char domain[PRINCIPAL_NAME_MAX+1];
|
|
|
|
char host[PRINCIPAL_NAME_MAX+1];
|
|
|
|
size= sizeof(domain);
|
|
|
|
if (GetComputerNameEx(ComputerNameDnsDomain,domain,&size) && size > 0)
|
|
|
|
{
|
|
|
|
size= sizeof(host);
|
|
|
|
if (GetComputerNameEx(ComputerNameDnsHostname,host,&size))
|
|
|
|
{
|
|
|
|
_snprintf(default_principal,sizeof(default_principal),"%s$@%s",host, domain);
|
|
|
|
return default_principal;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/* Unable to retrieve useful name, return something */
|
|
|
|
return INVALID_KERBEROS_PRINCIPAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Extract client name from SSPI context */
|
|
|
|
static int get_client_name_from_context(CtxtHandle *ctxt,
|
|
|
|
char *name,
|
|
|
|
size_t name_len,
|
|
|
|
int use_full_name)
|
|
|
|
{
|
|
|
|
SecPkgContext_NativeNames native_names;
|
|
|
|
SECURITY_STATUS sspi_ret;
|
|
|
|
char *p;
|
|
|
|
|
|
|
|
sspi_ret= QueryContextAttributes(ctxt, SECPKG_ATTR_NATIVE_NAMES, &native_names);
|
|
|
|
if (sspi_ret == SEC_E_OK)
|
|
|
|
{
|
|
|
|
/* Extract user from Kerberos principal name user@realm */
|
|
|
|
if(!use_full_name)
|
|
|
|
{
|
|
|
|
p = strrchr(native_names.sClientName,'@');
|
|
|
|
if(p)
|
|
|
|
*p = 0;
|
|
|
|
}
|
|
|
|
strncpy(name, native_names.sClientName, name_len);
|
2018-10-15 22:11:14 +01:00
|
|
|
|
|
|
|
if (native_names.sClientName)
|
|
|
|
FreeContextBuffer(native_names.sClientName);
|
|
|
|
if (native_names.sServerName)
|
|
|
|
FreeContextBuffer(native_names.sServerName);
|
|
|
|
|
2016-01-14 13:31:08 +01:00
|
|
|
return CR_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
sspi_ret= ImpersonateSecurityContext(ctxt);
|
|
|
|
if (sspi_ret == SEC_E_OK)
|
|
|
|
{
|
2018-02-06 12:55:58 +00:00
|
|
|
ULONG len= (ULONG)name_len;
|
2016-01-14 13:31:08 +01:00
|
|
|
if (!GetUserNameEx(NameSamCompatible, name, &len))
|
|
|
|
{
|
|
|
|
log_error(GetLastError(), "GetUserNameEx");
|
|
|
|
RevertSecurityContext(ctxt);
|
|
|
|
return CR_ERROR;
|
|
|
|
}
|
|
|
|
RevertSecurityContext(ctxt);
|
|
|
|
|
|
|
|
/* Extract user from Windows name realm\user */
|
|
|
|
if (!use_full_name)
|
|
|
|
{
|
|
|
|
p = strrchr(name, '\\');
|
|
|
|
if (p)
|
|
|
|
{
|
|
|
|
p++;
|
|
|
|
memmove(name, p, name + len + 1 - p);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return CR_OK;
|
|
|
|
}
|
|
|
|
|
|
|
|
log_error(sspi_ret, "ImpersonateSecurityContext");
|
|
|
|
return CR_ERROR;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-11-05 23:38:18 +01:00
|
|
|
/*
|
|
|
|
Check if username from SSPI context matches the name requested
|
|
|
|
in MYSQL_SERVER_AUTH_INFO
|
|
|
|
|
|
|
|
There are 2 ways to specify SSPI username
|
|
|
|
username, of auth_string.
|
|
|
|
|
|
|
|
if auth_string is used, we compare full name (i.e , with user+domain)
|
|
|
|
if not, we match just the user name.
|
|
|
|
*/
|
|
|
|
static bool check_username_match(CtxtHandle *ctxt,
|
|
|
|
MYSQL_SERVER_AUTH_INFO *auth_info)
|
2016-01-14 13:31:08 +01:00
|
|
|
{
|
2020-11-05 23:38:18 +01:00
|
|
|
char client_name[MYSQL_USERNAME_LENGTH + 1];
|
|
|
|
const char *user= 0;
|
|
|
|
int compare_full_name;;
|
|
|
|
if (auth_info->auth_string_length > 0)
|
|
|
|
{
|
|
|
|
compare_full_name= 1;
|
|
|
|
user= auth_info->auth_string;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
compare_full_name= 0;
|
|
|
|
user= auth_info->user_name;
|
|
|
|
}
|
|
|
|
if (get_client_name_from_context(ctxt, client_name, MYSQL_USERNAME_LENGTH,
|
|
|
|
compare_full_name) != CR_OK)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Always compare case-insensitive on Windows. */
|
|
|
|
if (_stricmp(client_name, user))
|
|
|
|
{
|
|
|
|
my_printf_error(ER_ACCESS_DENIED_ERROR,
|
|
|
|
"GSSAPI name mismatch, requested '%s', actual name '%s'", 0, user,
|
|
|
|
client_name);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
Checks the security token extracted from SSPI context
|
|
|
|
for membership in specfied group.
|
|
|
|
|
|
|
|
@param ctxt - SSPI context
|
|
|
|
@param group_name - group name to check membership against
|
|
|
|
NOTE: this can also be a user's name
|
|
|
|
|
|
|
|
@param use_sid - whether name is SID
|
|
|
|
@last_error - will be set, if the function returns false, and
|
|
|
|
some of the API's have failed.
|
|
|
|
@failing_api - name of the API that has failed(for error logging)
|
|
|
|
*/
|
|
|
|
static bool check_group_match(CtxtHandle *ctxt, const char *name,
|
|
|
|
bool name_is_sid)
|
|
|
|
{
|
|
|
|
BOOL is_member= FALSE;
|
|
|
|
bool is_impersonating= false;
|
|
|
|
bool free_sid= false;
|
|
|
|
PSID sid= 0;
|
|
|
|
|
|
|
|
|
|
|
|
#define FAIL(msg) \
|
|
|
|
do \
|
|
|
|
{ \
|
|
|
|
log_error(GetLastError(), msg); \
|
|
|
|
goto cleanup; \
|
|
|
|
} while (0)
|
|
|
|
|
|
|
|
/* Get the group SID.*/
|
|
|
|
if (name_is_sid)
|
|
|
|
{
|
|
|
|
if (!ConvertStringSidToSidA(name, &sid))
|
|
|
|
FAIL("ConvertStringSidToSid");
|
|
|
|
free_sid= true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
/* Get the SID of the specified group via LookupAccountName().*/
|
|
|
|
char sid_buf[SECURITY_MAX_SID_SIZE];
|
|
|
|
char domain[256];
|
|
|
|
DWORD sid_size= sizeof(sid_buf);
|
|
|
|
DWORD domain_size= sizeof(domain);
|
|
|
|
|
|
|
|
SID_NAME_USE sid_name_use;
|
|
|
|
sid= (PSID) sid_buf;
|
|
|
|
|
|
|
|
if (!LookupAccountName(0, name, sid, &sid_size, domain,
|
|
|
|
&domain_size, &sid_name_use))
|
|
|
|
{
|
|
|
|
FAIL("LookupAccountName");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Impersonate, to check group membership */
|
|
|
|
if (ImpersonateSecurityContext(ctxt))
|
|
|
|
FAIL("ImpersonateSecurityContext");
|
|
|
|
is_impersonating= true;
|
2016-01-14 13:31:08 +01:00
|
|
|
|
2020-11-05 23:38:18 +01:00
|
|
|
if (!CheckTokenMembership(GetCurrentThreadToken(), sid, &is_member))
|
|
|
|
FAIL("CheckTokenMembership");
|
|
|
|
|
|
|
|
cleanup:
|
|
|
|
if (is_impersonating)
|
|
|
|
RevertSecurityContext(ctxt);
|
|
|
|
if (free_sid)
|
|
|
|
LocalFree(sid);
|
|
|
|
return is_member;
|
|
|
|
}
|
|
|
|
|
|
|
|
static SECURITY_STATUS sspi_get_context(MYSQL_PLUGIN_VIO *vio,
|
|
|
|
CtxtHandle *ctxt, CredHandle *cred)
|
|
|
|
{
|
|
|
|
SECURITY_STATUS sspi_ret= SEC_E_OK;
|
|
|
|
ULONG attribs= 0;
|
|
|
|
TimeStamp lifetime;
|
2016-01-14 13:31:08 +01:00
|
|
|
SecBufferDesc inbuf_desc;
|
|
|
|
SecBuffer inbuf;
|
|
|
|
SecBufferDesc outbuf_desc;
|
|
|
|
SecBuffer outbuf;
|
|
|
|
void* out= NULL;
|
|
|
|
|
2020-11-05 23:38:18 +01:00
|
|
|
SecInvalidateHandle(cred);
|
|
|
|
SecInvalidateHandle(ctxt);
|
2016-01-14 13:31:08 +01:00
|
|
|
|
|
|
|
out= malloc(SSPI_MAX_TOKEN_SIZE);
|
|
|
|
if (!out)
|
|
|
|
{
|
|
|
|
log_error(SEC_E_OK, "memory allocation failed");
|
2020-11-05 23:38:18 +01:00
|
|
|
sspi_ret= SEC_E_INSUFFICIENT_MEMORY;
|
2016-01-14 13:31:08 +01:00
|
|
|
goto cleanup;
|
|
|
|
}
|
|
|
|
sspi_ret= AcquireCredentialsHandle(
|
|
|
|
srv_principal_name,
|
2018-02-07 01:40:16 +00:00
|
|
|
(LPSTR)srv_mech_name,
|
2016-01-14 13:31:08 +01:00
|
|
|
SECPKG_CRED_INBOUND,
|
|
|
|
NULL,
|
|
|
|
NULL,
|
|
|
|
NULL,
|
|
|
|
NULL,
|
2020-11-05 23:38:18 +01:00
|
|
|
cred,
|
2016-01-14 13:31:08 +01:00
|
|
|
&lifetime);
|
|
|
|
|
|
|
|
if (SEC_ERROR(sspi_ret))
|
|
|
|
{
|
|
|
|
log_error(sspi_ret, "AcquireCredentialsHandle failed");
|
|
|
|
goto cleanup;
|
|
|
|
}
|
|
|
|
|
|
|
|
inbuf.cbBuffer= 0;
|
|
|
|
inbuf.BufferType= SECBUFFER_TOKEN;
|
|
|
|
inbuf.pvBuffer= NULL;
|
|
|
|
inbuf_desc.ulVersion= SECBUFFER_VERSION;
|
|
|
|
inbuf_desc.cBuffers= 1;
|
|
|
|
inbuf_desc.pBuffers= &inbuf;
|
|
|
|
|
|
|
|
outbuf.BufferType= SECBUFFER_TOKEN;
|
|
|
|
outbuf.cbBuffer= SSPI_MAX_TOKEN_SIZE;
|
|
|
|
outbuf.pvBuffer= out;
|
|
|
|
|
|
|
|
outbuf_desc.ulVersion= SECBUFFER_VERSION;
|
|
|
|
outbuf_desc.cBuffers= 1;
|
|
|
|
outbuf_desc.pBuffers= &outbuf;
|
|
|
|
|
|
|
|
do
|
|
|
|
{
|
|
|
|
/* Read SSPI blob from client. */
|
|
|
|
int len= vio->read_packet(vio, (unsigned char **)&inbuf.pvBuffer);
|
|
|
|
if (len < 0)
|
|
|
|
{
|
|
|
|
log_error(SEC_E_OK, "communication error(read)");
|
|
|
|
goto cleanup;
|
|
|
|
}
|
|
|
|
inbuf.cbBuffer= len;
|
|
|
|
outbuf.cbBuffer= SSPI_MAX_TOKEN_SIZE;
|
|
|
|
sspi_ret= AcceptSecurityContext(
|
2020-11-05 23:38:18 +01:00
|
|
|
cred,
|
|
|
|
SecIsValidHandle(ctxt) ? ctxt : NULL,
|
2016-01-14 13:31:08 +01:00
|
|
|
&inbuf_desc,
|
|
|
|
attribs,
|
|
|
|
SECURITY_NATIVE_DREP,
|
2020-11-05 23:38:18 +01:00
|
|
|
ctxt,
|
2016-01-14 13:31:08 +01:00
|
|
|
&outbuf_desc,
|
|
|
|
&attribs,
|
|
|
|
&lifetime);
|
|
|
|
|
|
|
|
if (SEC_ERROR(sspi_ret))
|
|
|
|
{
|
|
|
|
log_error(sspi_ret, "AcceptSecurityContext");
|
|
|
|
goto cleanup;
|
|
|
|
}
|
|
|
|
if (sspi_ret != SEC_E_OK && sspi_ret != SEC_I_CONTINUE_NEEDED)
|
|
|
|
{
|
|
|
|
log_error(sspi_ret, "AcceptSecurityContext unexpected return value");
|
|
|
|
goto cleanup;
|
|
|
|
}
|
|
|
|
if (outbuf.cbBuffer)
|
|
|
|
{
|
|
|
|
/* Send generated blob to client. */
|
|
|
|
if (vio->write_packet(vio, (unsigned char *)outbuf.pvBuffer, outbuf.cbBuffer))
|
|
|
|
{
|
|
|
|
log_error(SEC_E_OK, "communicaton error(write)");
|
|
|
|
goto cleanup;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} while (sspi_ret == SEC_I_CONTINUE_NEEDED);
|
|
|
|
|
2020-11-05 23:38:18 +01:00
|
|
|
cleanup:
|
|
|
|
free(out);
|
|
|
|
return sspi_ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int auth_server(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *auth_info)
|
|
|
|
{
|
|
|
|
int ret= CR_ERROR;
|
|
|
|
const char* group = 0;
|
|
|
|
bool use_sid = 0;
|
|
|
|
|
|
|
|
CtxtHandle ctxt;
|
|
|
|
CredHandle cred;
|
|
|
|
if (sspi_get_context(vio, &ctxt, &cred) != SEC_E_OK)
|
2016-01-14 13:31:08 +01:00
|
|
|
goto cleanup;
|
|
|
|
|
2020-11-05 23:38:18 +01:00
|
|
|
|
|
|
|
/*
|
|
|
|
Authentication done, now test user name, or group
|
|
|
|
membership.
|
|
|
|
First, find out if matching group was requested.
|
|
|
|
*/
|
|
|
|
static struct
|
2016-01-14 13:31:08 +01:00
|
|
|
{
|
2020-11-05 23:38:18 +01:00
|
|
|
const char *str;
|
|
|
|
size_t len;
|
|
|
|
bool sid;
|
|
|
|
} prefixes[]= {{"GROUP:", sizeof("GROUP:") - 1, false},
|
|
|
|
{"SID:", sizeof("SID:") - 1, true}};
|
|
|
|
group= 0;
|
|
|
|
for (auto &prefix : prefixes)
|
|
|
|
{
|
|
|
|
if (auth_info->auth_string_length >= prefix.len &&
|
|
|
|
!strncmp(auth_info->auth_string, prefix.str, prefix.len))
|
|
|
|
{
|
|
|
|
group= auth_info->auth_string + prefix.len;
|
|
|
|
use_sid= prefix.sid;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (group)
|
|
|
|
{
|
|
|
|
/* Test group membership.*/
|
|
|
|
ret= check_group_match(&ctxt, group, use_sid) ? CR_OK : CR_ERROR;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
/* Compare username. */
|
|
|
|
ret= check_username_match(&ctxt, auth_info) ? CR_OK : CR_ERROR;
|
2016-01-14 13:31:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
cleanup:
|
|
|
|
if (SecIsValidHandle(&ctxt))
|
|
|
|
DeleteSecurityContext(&ctxt);
|
|
|
|
|
|
|
|
if (SecIsValidHandle(&cred))
|
|
|
|
FreeCredentialsHandle(&cred);
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
int plugin_init()
|
|
|
|
{
|
|
|
|
CredHandle cred;
|
|
|
|
SECURITY_STATUS ret;
|
|
|
|
|
|
|
|
/*
|
|
|
|
Use negotiate by default, which accepts raw kerberos
|
|
|
|
and also NTLM.
|
|
|
|
*/
|
|
|
|
if (srv_mech == PLUGIN_MECH_DEFAULT)
|
|
|
|
srv_mech= PLUGIN_MECH_SPNEGO;
|
|
|
|
|
|
|
|
if(srv_mech == PLUGIN_MECH_KERBEROS)
|
|
|
|
srv_mech_name= "Kerberos";
|
|
|
|
else if(srv_mech == PLUGIN_MECH_SPNEGO )
|
|
|
|
srv_mech_name= "Negotiate";
|
|
|
|
|
|
|
|
if(!srv_principal_name[0])
|
|
|
|
{
|
|
|
|
srv_principal_name= get_default_principal_name();
|
|
|
|
}
|
2018-02-01 12:23:34 +00:00
|
|
|
my_printf_error(ER_UNKNOWN_ERROR, "SSPI: using principal name '%s', mech '%s'",
|
2017-04-18 16:37:57 +00:00
|
|
|
ME_ERROR_LOG | ME_NOTE, srv_principal_name, srv_mech_name);
|
2016-01-14 13:31:08 +01:00
|
|
|
|
|
|
|
ret = AcquireCredentialsHandle(
|
|
|
|
srv_principal_name,
|
2018-02-07 01:40:16 +00:00
|
|
|
(LPSTR)srv_mech_name,
|
2016-01-14 13:31:08 +01:00
|
|
|
SECPKG_CRED_INBOUND,
|
|
|
|
NULL,
|
|
|
|
NULL,
|
|
|
|
NULL,
|
|
|
|
NULL,
|
|
|
|
&cred,
|
|
|
|
NULL);
|
|
|
|
if (SEC_ERROR(ret))
|
|
|
|
{
|
|
|
|
log_error(ret, "AcquireCredentialsHandle");
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
FreeCredentialsHandle(&cred);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
int plugin_deinit()
|
|
|
|
{
|
|
|
|
return 0;
|
|
|
|
}
|