1 /*
2  * Copyright (c) 2015, 2021, Oracle and/or its affiliates.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License, version 2.0,
6  * as published by the Free Software Foundation.
7  *
8  * This program is also distributed with certain software (including
9  * but not limited to OpenSSL) that is licensed under separate terms,
10  * as designated in a particular file or component or in included license
11  * documentation.  The authors of MySQL hereby grant you an additional
12  * permission to link the program and your derivative works with the
13  * separately licensed software that they have included with MySQL.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License, version 2.0, for more details.
19  *
20  * You should have received a copy of the GNU General Public License
21  * along with this program; if not, write to the Free Software
22  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
23  * 02110-1301  USA
24  */
25 
26 #ifndef XPL_USER_VERIFICATION_HELPER_H_
27 #define XPL_USER_VERIFICATION_HELPER_H_
28 
29 #include "ngs_common/connection_type.h"
30 
31 #include "xpl_log.h"
32 #include "sql_data_context.h"
33 #include "sql_user_require.h"
34 #include "query_string_builder.h"
35 
36 
37 namespace xpl {
38 
39   class User_verification_helper {
40   public:
41     typedef Command_delegate::Field_types         Field_types;
42     typedef Sql_data_context::Result_info         Result_info;
43     typedef Buffering_command_delegate::Resultset Resultset;
44 
User_verification_helper(const On_user_password_hash & hash_verification_cb,ngs::IOptions_session_ptr & options_session,const ngs::Connection_type type)45     User_verification_helper(
46         const On_user_password_hash &hash_verification_cb,
47         ngs::IOptions_session_ptr &options_session,
48         const ngs::Connection_type type)
49     : m_hash_verification_cb(hash_verification_cb),
50       m_options_session(options_session),
51       m_type(type) {
52     }
53 
verify_mysql_account(Sql_data_context & sql_data_context,const std::string & user,const std::string & host)54     ngs::Error_code verify_mysql_account(Sql_data_context &sql_data_context, const std::string &user, const std::string &host) {
55       Resultset   r_result_set;
56       Result_info r_info;
57 
58       ngs::PFS_string query = get_sql(user.c_str(), host.c_str());
59       ngs::Error_code error = sql_data_context.execute_sql_and_collect_results(
60           query.c_str(),
61           query.length(),
62           m_fields_type,
63           r_result_set,
64           r_info);
65 
66       if (error) {
67         log_debug("Error %i occurred while executing query: %s", error.error, error.message.c_str());
68         return error;
69       }
70 
71       try {
72         // The query asks for primary key, thus here we should get
73         // only one row
74         if (!r_result_set.empty()) {
75           assert(1 == r_result_set.size());
76           if (this->verify_mysql_account_entry(r_result_set.front()))
77             return ngs::Error_code();
78         }
79       }
80       catch (ngs::Error_code &e)
81       {
82         return e;
83       }
84 
85       return ngs::Error_code(ER_NO_SUCH_USER, "Invalid user or password");
86     }
87 
88   private:
get_sql(const char * user,const char * host)89     ngs::PFS_string get_sql(const char *user, const char *host) const {
90       Query_string_builder qb;
91 
92       // Query for a concrete users primary key (USER,HOST columns) which was chosen by MySQL Server
93       // and verify hash and plugin column.
94       // There are also other informations, like account lock, if password expired and user can be connected, if server is in offline mode and the user is with super priv.
95       // column `is_password_expired` is set to true if
96       //  - password expired
97       // column `disconnect_on_expired_password` is set to true if
98       //  - disconnect_on_expired_password
99       // column `is_offline_mode_and_isnt_super_user` is set true if it
100       //  - offline mode and user has not super priv
101       qb.put("/* xplugin authentication */ SELECT @@require_secure_transport, `authentication_string`,`account_locked`, "
102           "(`password_expired`!='N') as `is_password_expired`, "
103           "@@disconnect_on_expired_password as `disconnect_on_expired_password`, "
104           "@@offline_mode and (`Super_priv`='N') as `is_offline_mode_and_isnt_super_user`,"
105           "`ssl_type`, `ssl_cipher`, `x509_issuer`, `x509_subject` "
106           "FROM mysql.user WHERE ")
107             .quote_string(user).put(" = `user` AND ")
108             .quote_string(host).put(" = `host` ");
109 
110       log_debug("Query user '%s'", qb.get().c_str());
111       return qb.get();
112     }
113 
verify_mysql_account_entry(const Row_data & row)114     bool verify_mysql_account_entry(const Row_data &row) {
115       bool require_secure_transport = false;
116       std::string db_password_hash;
117       bool is_account_not_locked = false;
118       bool is_password_expired = false;
119       bool disconnect_on_expired_password = false;
120       bool is_offline_mode_and_isnt_super_user = false;
121       Sql_user_require required;
122 
123       assert(10 == row.fields.size());
124 
125       if (!get_bool_from_int_value(row, 0, require_secure_transport) ||
126           !get_string_value(row, 1, db_password_hash) ||
127           !get_bool_from_string_value(row, 2, "N", is_account_not_locked) ||
128           !get_bool_from_int_value(row, 3, is_password_expired) ||
129           !get_bool_from_int_value(row, 4, disconnect_on_expired_password) ||
130           !get_bool_from_int_value(row, 5, is_offline_mode_and_isnt_super_user) ||
131           !get_string_value(row, 6, required.ssl_type) ||
132           !get_string_value(row, 7, required.ssl_cipher) ||
133           !get_string_value(row, 8, required.ssl_x509_issuer) ||
134           !get_string_value(row, 9, required.ssl_x509_subject))
135         return false;
136 
137       if (m_hash_verification_cb(db_password_hash)) {
138         // Password check succeeded but...
139 
140         if (!is_account_not_locked)
141           throw ngs::Error_code(ER_ACCOUNT_HAS_BEEN_LOCKED, "Account is locked.");
142 
143         if (is_offline_mode_and_isnt_super_user)
144           throw ngs::Error_code(ER_SERVER_OFFLINE_MODE, "Server works in offline mode.");
145 
146         // password expiration check must come last, because password expiration is not a fatal error
147         // a client that supports expired password state, will be let in... so the user can only
148         // get this error if the auth succeeded
149         if (is_password_expired) {
150           // if the password is expired, it's only a fatal error if disconnect_on_expired_password is enabled
151           // AND the client doesn't support expired passwords (this check is done by the caller of this)
152           // if it's NOT enabled, then the user will be allowed to login in sandbox mode, even if the client
153           // doesn't support expired passwords
154           if (disconnect_on_expired_password)
155             throw ngs::Fatal(ER_MUST_CHANGE_PASSWORD_LOGIN, "Your password has expired. To log in you must change it using a client that supports expired passwords.");
156           else
157             throw ngs::Error(ER_MUST_CHANGE_PASSWORD_LOGIN, "Your password has expired.");
158         }
159 
160         if (require_secure_transport) {
161           if (!ngs::Connection_type_helper::is_secure_type(m_type))
162             throw ngs::Error(ER_SECURE_TRANSPORT_REQUIRED, "Secure transport required. To log in you must use TCP+SSL or UNIX socket connection.");
163         }
164 
165         ngs::Error_code error = required.validate(m_options_session);
166         if (error)
167           throw error;
168 
169         return true;
170       }
171 
172       return false;
173     }
174 
get_string_value(const Row_data & row,const std::size_t index,std::string & value)175     bool get_string_value(const Row_data &row, const std::size_t index, std::string &value) const {
176       Field_value *field = row.fields[index];
177 
178       if (!field)
179         return false;
180 
181       if (MYSQL_TYPE_STRING != m_fields_type[index].type &&
182           MYSQL_TYPE_BLOB != m_fields_type[index].type)
183         return false;
184 
185       value = *field->value.v_string;
186 
187       return true;
188     }
189 
get_bool_from_string_value(const Row_data & row,const std::size_t index,const std::string & match,bool & value)190     bool get_bool_from_string_value(
191         const Row_data &row,
192         const std::size_t index,
193         const std::string &match,
194         bool &value) const {
195       std::string string_value;
196 
197       if (get_string_value(row, index, string_value)) {
198         value = string_value == match;
199 
200         return true;
201       }
202 
203       return false;
204     }
205 
get_bool_from_int_value(const Row_data & row,const std::size_t index,bool & value)206     bool get_bool_from_int_value(const Row_data &row, const std::size_t index, bool &value) const {
207       std::string string_value;
208       Field_value *field = row.fields[index];
209 
210       if (!field)
211         return false;
212 
213       if (MYSQL_TYPE_LONGLONG != m_fields_type[index].type)
214         return false;
215 
216       value = 0 != field->value.v_long;
217 
218       return true;
219     }
220 
221     Field_types                m_fields_type;
222     On_user_password_hash      m_hash_verification_cb;
223     ngs::IOptions_session_ptr &m_options_session;
224     ngs::Connection_type       m_type;
225   };
226 
227 } // namespace xpl
228 
229 #endif // XPL_USER_VERIFICATION_HELPER_H_
230