diff options
author | Vladislav Vaintroub <wlad@mariadb.com> | 2020-11-05 23:38:18 +0100 |
---|---|---|
committer | Vladislav Vaintroub <wlad@mariadb.com> | 2021-01-27 14:38:00 +0100 |
commit | 7ebabea5d35e7bfad5cf846952d3a3db658b09ba (patch) | |
tree | 08335ddd2503192808107b4df72cecc0a3d5f2e2 | |
parent | c310f4c3813e6c6a3d671cf12534297f6fcf31e3 (diff) | |
download | mariadb-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.result | 44 | ||||
-rw-r--r-- | plugin/auth_gssapi/mysql-test/auth_gssapi/groups.test | 73 | ||||
-rw-r--r-- | plugin/auth_gssapi/mysql-test/auth_gssapi/suite.pm | 3 | ||||
-rw-r--r-- | plugin/auth_gssapi/server_plugin.cc | 4 | ||||
-rw-r--r-- | plugin/auth_gssapi/sspi_server.cc | 215 |
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; } |