summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVladislav Vaintroub <wlad@mariadb.com>2020-11-05 23:38:18 +0100
committerVladislav Vaintroub <wlad@mariadb.com>2021-01-27 14:38:00 +0100
commit7ebabea5d35e7bfad5cf846952d3a3db658b09ba (patch)
tree08335ddd2503192808107b4df72cecc0a3d5f2e2
parentc310f4c3813e6c6a3d671cf12534297f6fcf31e3 (diff)
downloadmariadb-git-7ebabea5d35e7bfad5cf846952d3a3db658b09ba.tar.gz
MDEV-23959 GSSAPI plugin - support AD or local group name , and SIDs on Windows
Support membership tests in SSPI with special prefix form CREATE USER u IDENTIFIED WITH gssapi AS "GROUP:<group_name>" or CREATE USER u IDENTIFIED WITH gssapi AS "SID:<sid>" If user is created as one of the above, after successful SSPI handshake, this will happen 1) If "GROUP:" prefix is used, then <group_name> is translated to SID using LookupAccountName() API 2) SSPI user is checked for SID membership with ImpersonateSecurityContext() and CheckMembership() APIs Note, that it <group>/<sid> do not need strictly to refer to an actual group. Identity test is also supported, e.g "GROUP:<users_name>" or "SID:<user_sid>" will work too. Well-known SIDs (in SDDL syntax) appear to be supported such as "SID:WD" will refer to World/Everyone (== "SID:S-1-1-0") or "SID:BA" will refer to Administrators (== "SID:S-1-5-32-544") In UAC environments, for successful checks against Administrators group, elevation(Run As Administrator) might be necessary, since CheckMembership() needs groups to be marked as enabled in the token group list.
-rw-r--r--plugin/auth_gssapi/mysql-test/auth_gssapi/groups.result44
-rw-r--r--plugin/auth_gssapi/mysql-test/auth_gssapi/groups.test73
-rw-r--r--plugin/auth_gssapi/mysql-test/auth_gssapi/suite.pm3
-rw-r--r--plugin/auth_gssapi/server_plugin.cc4
-rw-r--r--plugin/auth_gssapi/sspi_server.cc215
5 files changed, 294 insertions, 45 deletions
diff --git a/plugin/auth_gssapi/mysql-test/auth_gssapi/groups.result b/plugin/auth_gssapi/mysql-test/auth_gssapi/groups.result
new file mode 100644
index 00000000000..10b3eb3e9d6
--- /dev/null
+++ b/plugin/auth_gssapi/mysql-test/auth_gssapi/groups.result
@@ -0,0 +1,44 @@
+INSTALL SONAME 'auth_gssapi';
+Warnings:
+Note 1105 SSPI: using principal name 'localhost', mech 'Negotiate'
+CREATE USER 'nosuchgroup' IDENTIFIED WITH gssapi AS 'GROUP:nosuchgroup';
+connect(localhost,nosuchuser,,test,MASTER_MYPORT,MASTER_MYSOCK);
+connect con1,localhost,nosuchuser,,;
+ERROR 28000: Access denied for user 'nosuchuser'@'localhost' (using password: NO)
+DROP USER nosuchgroup;
+CREATE USER 'nullsid' IDENTIFIED WITH gssapi AS 'SID:S-1-0-0';
+connect(localhost,nullsid,,test,MASTER_MYPORT,MASTER_MYSOCK);
+connect con1,localhost,nullsid,,;
+ERROR 28000: Access denied for user 'nullsid'@'localhost' (using password: NO)
+DROP USER nullsid;
+CREATE USER 'anonymous' IDENTIFIED WITH gssapi AS 'SID:AN';
+connect(localhost,anonymous,,test,MASTER_MYPORT,MASTER_MYSOCK);
+connect con1,localhost,anonymous,,;
+ERROR 28000: Access denied for user 'anonymous'@'localhost' (using password: NO)
+DROP USER anonymous;
+CREATE USER 'group_everyone' IDENTIFIED WITH gssapi AS 'GROUP:Everyone';
+connect con1,localhost,group_everyone,,;
+disconnect con1;
+connection default;
+DROP USER group_everyone;
+CREATE USER 'sid_wd' IDENTIFIED WITH gssapi AS 'SID:WD';
+connect con1,localhost,sid_wd,,;
+disconnect con1;
+connection default;
+DROP USER sid_wd;
+CREATE USER 'S_1_1_0' IDENTIFIED WITH gssapi AS 'SID:S-1-1-0';
+connect con1,localhost,S_1_1_0,,;
+disconnect con1;
+connection default;
+DROP USER S_1_1_0;
+CREATE USER 'me_short' IDENTIFIED WITH gssapi AS 'GROUP:GSSAPI_SHORTNAME';
+connect con1,localhost,me_short,,;
+disconnect con1;
+connection default;
+DROP USER me_short;
+CREATE USER 'me_sid' IDENTIFIED WITH gssapi AS 'SID:MY-SID';
+connect con1,localhost,me_sid,,;
+disconnect con1;
+connection default;
+DROP USER me_sid;
+UNINSTALL SONAME 'auth_gssapi';
diff --git a/plugin/auth_gssapi/mysql-test/auth_gssapi/groups.test b/plugin/auth_gssapi/mysql-test/auth_gssapi/groups.test
new file mode 100644
index 00000000000..1c72ad9cc23
--- /dev/null
+++ b/plugin/auth_gssapi/mysql-test/auth_gssapi/groups.test
@@ -0,0 +1,73 @@
+source include/windows.inc;
+--replace_regex /name '[^']+'/name 'localhost'/
+INSTALL SONAME 'auth_gssapi';
+
+
+# Invalid group name
+CREATE USER 'nosuchgroup' IDENTIFIED WITH gssapi AS 'GROUP:nosuchgroup';
+replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT;
+error ER_ACCESS_DENIED_ERROR;
+connect (con1,localhost,nosuchuser,,);
+DROP USER nosuchgroup;
+
+# Group with no members, NULL SID
+CREATE USER 'nullsid' IDENTIFIED WITH gssapi AS 'SID:S-1-0-0';
+replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT;
+error ER_ACCESS_DENIED_ERROR;
+connect (con1,localhost,nullsid,,);
+DROP USER nullsid;
+
+
+# Anonymous
+CREATE USER 'anonymous' IDENTIFIED WITH gssapi AS 'SID:AN';
+replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT;
+error ER_ACCESS_DENIED_ERROR;
+connect (con1,localhost,anonymous,,);
+DROP USER anonymous;
+
+
+# Positive tests
+
+# Everyone group
+CREATE USER 'group_everyone' IDENTIFIED WITH gssapi AS 'GROUP:Everyone';
+replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT;
+connect (con1,localhost,group_everyone,,);
+disconnect con1;
+connection default;
+DROP USER group_everyone;
+
+# Everyone AS well-known SID name
+CREATE USER 'sid_wd' IDENTIFIED WITH gssapi AS 'SID:WD';
+replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT;
+connect (con1,localhost,sid_wd,,);
+disconnect con1;
+connection default;
+DROP USER sid_wd;
+
+# Everyone AS SID S-1-1-0
+CREATE USER 'S_1_1_0' IDENTIFIED WITH gssapi AS 'SID:S-1-1-0';
+replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT;
+connect (con1,localhost,S_1_1_0,,);
+disconnect con1;
+connection default;
+DROP USER S_1_1_0;
+
+replace_result $GSSAPI_SHORTNAME GSSAPI_SHORTNAME;
+eval CREATE USER 'me_short' IDENTIFIED WITH gssapi AS 'GROUP:$GSSAPI_SHORTNAME';
+replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT;
+connect (con1,localhost,me_short,,);
+disconnect con1;
+connection default;
+DROP USER me_short;
+
+
+replace_result $SID MY-SID;
+eval CREATE USER 'me_sid' IDENTIFIED WITH gssapi AS 'SID:$SID';
+replace_result $MASTER_MYSOCK MASTER_MYSOCK $MASTER_MYPORT MASTER_MYPORT;
+connect (con1,localhost,me_sid,,);
+disconnect con1;
+connection default;
+DROP USER me_sid;
+
+
+UNINSTALL SONAME 'auth_gssapi'; \ No newline at end of file
diff --git a/plugin/auth_gssapi/mysql-test/auth_gssapi/suite.pm b/plugin/auth_gssapi/mysql-test/auth_gssapi/suite.pm
index aa225536a1e..e77ba05cb5c 100644
--- a/plugin/auth_gssapi/mysql-test/auth_gssapi/suite.pm
+++ b/plugin/auth_gssapi/mysql-test/auth_gssapi/suite.pm
@@ -14,6 +14,9 @@ if ($^O eq "MSWin32")
$fullname =~ s/\\/\\\\/; # SQL escaping for backslash
$ENV{'GSSAPI_FULLNAME'} = $fullname;
$ENV{'GSSAPI_SHORTNAME'} = $ENV{'USERNAME'};
+ chomp(my $sid = `powershell -Command "([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.Value"`);
+ $ENV{'SID'} = $sid;
+
}
else
{
diff --git a/plugin/auth_gssapi/server_plugin.cc b/plugin/auth_gssapi/server_plugin.cc
index 4fdad2de4b8..eeca4607ece 100644
--- a/plugin/auth_gssapi/server_plugin.cc
+++ b/plugin/auth_gssapi/server_plugin.cc
@@ -32,11 +32,7 @@
GSSAPI authentication plugin, server side
*/
-#ifdef _WIN32
-typedef unsigned __int64 my_ulonglong;
-#else
typedef unsigned long long my_ulonglong;
-#endif
#include <stdlib.h>
#include <mysqld_error.h>
diff --git a/plugin/auth_gssapi/sspi_server.cc b/plugin/auth_gssapi/sspi_server.cc
index 44aa5051472..4a1958089ef 100644
--- a/plugin/auth_gssapi/sspi_server.cc
+++ b/plugin/auth_gssapi/sspi_server.cc
@@ -31,7 +31,7 @@ POSSIBILITY OF SUCH DAMAGE.
#include "server_plugin.h"
#include <mysql/plugin_auth.h>
#include <mysqld_error.h>
-
+#include <sddl.h>
/* This sends the error to the client */
static void log_error(SECURITY_STATUS err, const char *msg)
@@ -140,32 +140,140 @@ static int get_client_name_from_context(CtxtHandle *ctxt,
}
-int auth_server(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *auth_info)
+/*
+ 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)
{
- int ret;
- SECURITY_STATUS sspi_ret;
- ULONG attribs = 0;
- TimeStamp lifetime;
- CredHandle cred;
- CtxtHandle ctxt;
+ 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;
+ 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;
SecBufferDesc inbuf_desc;
SecBuffer inbuf;
SecBufferDesc outbuf_desc;
SecBuffer outbuf;
void* out= NULL;
- char client_name[MYSQL_USERNAME_LENGTH + 1];
- const char *user= 0;
- int compare_full_name;
- ret= CR_ERROR;
- SecInvalidateHandle(&cred);
- SecInvalidateHandle(&ctxt);
+ SecInvalidateHandle(cred);
+ SecInvalidateHandle(ctxt);
out= malloc(SSPI_MAX_TOKEN_SIZE);
if (!out)
{
log_error(SEC_E_OK, "memory allocation failed");
+ sspi_ret= SEC_E_INSUFFICIENT_MEMORY;
goto cleanup;
}
sspi_ret= AcquireCredentialsHandle(
@@ -176,7 +284,7 @@ int auth_server(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *auth_info)
NULL,
NULL,
NULL,
- &cred,
+ cred,
&lifetime);
if (SEC_ERROR(sspi_ret))
@@ -209,28 +317,15 @@ int auth_server(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *auth_info)
log_error(SEC_E_OK, "communication error(read)");
goto cleanup;
}
- if (!user)
- {
- 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;
- }
- }
inbuf.cbBuffer= len;
outbuf.cbBuffer= SSPI_MAX_TOKEN_SIZE;
sspi_ret= AcceptSecurityContext(
- &cred,
- SecIsValidHandle(&ctxt) ? &ctxt : NULL,
+ cred,
+ SecIsValidHandle(ctxt) ? ctxt : NULL,
&inbuf_desc,
attribs,
SECURITY_NATIVE_DREP,
- &ctxt,
+ ctxt,
&outbuf_desc,
&attribs,
&lifetime);
@@ -256,18 +351,57 @@ int auth_server(MYSQL_PLUGIN_VIO *vio, MYSQL_SERVER_AUTH_INFO *auth_info)
}
} while (sspi_ret == SEC_I_CONTINUE_NEEDED);
- /* Authentication done, now extract and compare user name. */
- ret= get_client_name_from_context(&ctxt, client_name, MYSQL_USERNAME_LENGTH, compare_full_name);
- if (ret != CR_OK)
+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)
goto cleanup;
- /* Always compare case-insensitive on Windows. */
- ret= _stricmp(client_name, user) == 0 ? CR_OK : CR_ERROR;
- if (ret != CR_OK)
+
+ /*
+ Authentication done, now test user name, or group
+ membership.
+ First, find out if matching group was requested.
+ */
+ static struct
{
- my_printf_error(ER_ACCESS_DENIED_ERROR,
- "GSSAPI name mismatch, requested '%s', actual name '%s'",
- 0, user, client_name);
+ 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;
}
cleanup:
@@ -277,7 +411,6 @@ cleanup:
if (SecIsValidHandle(&cred))
FreeCredentialsHandle(&cred);
- free(out);
return ret;
}