@@ -9,7 +9,7 @@ namespace DB
9
9
10
10
namespace
11
11
{
12
- // / The JSON reply from provider has only a few key-value pairs, so no need for SimdJSON/RapidJSON .
12
+ // / The JSON reply from provider has only a few key-value pairs, so no need for any advanced parsing .
13
13
// / Reduce complexity by using picojson.
14
14
picojson::object parseJSON (const String & json_string) {
15
15
picojson::value jsonValue;
@@ -26,18 +26,20 @@ namespace
26
26
return jsonValue.get <picojson::object>();
27
27
}
28
28
29
- std::string getValueByKey (const picojson::object & jsonObject, const std::string & key) {
29
+ template <typename ValueType = std::string>
30
+ ValueType getValueByKey (const picojson::object & jsonObject, const std::string & key) {
30
31
auto it = jsonObject.find (key); // Find the key in the object
31
- if (it == jsonObject.end ()) {
32
+ if (it == jsonObject.end ())
33
+ {
32
34
throw std::runtime_error (" Key not found: " + key);
33
35
}
34
36
35
- const picojson::value &value = it->second ;
36
- if (!value.is <std::string >()) {
37
- throw std::runtime_error (" Value for key '" + key + " ' is not a string " );
37
+ const picojson::value & value = it->second ;
38
+ if (!value.is <ValueType >()) {
39
+ throw std::runtime_error (" Value for key '" + key + " ' has incorrect type. " );
38
40
}
39
41
40
- return value.get <std::string >();
42
+ return value.get <ValueType >();
41
43
}
42
44
43
45
picojson::object getObjectFromURI (const Poco::URI & uri, const String & token = " " )
@@ -96,9 +98,12 @@ std::unique_ptr<IAccessTokenProcessor> IAccessTokenProcessor::parseTokenProcesso
96
98
String email_regex_str = config.hasProperty (prefix + " .email_filter" ) ? config.getString (
97
99
prefix + " .email_filter" ) : " " ;
98
100
101
+ UInt64 cache_lifetime = config.hasProperty (prefix + " .cache_lifetime" ) ? config.getUInt64 (
102
+ prefix + " .cache_lifetime" ) : 3600 ;
103
+
99
104
if (provider == " google" )
100
105
{
101
- return std::make_unique<GoogleAccessTokenProcessor>(name, email_regex_str);
106
+ return std::make_unique<GoogleAccessTokenProcessor>(name, cache_lifetime, email_regex_str);
102
107
}
103
108
else if (provider == " azure" )
104
109
{
@@ -110,11 +115,9 @@ std::unique_ptr<IAccessTokenProcessor> IAccessTokenProcessor::parseTokenProcesso
110
115
throw Exception (ErrorCodes::INVALID_CONFIG_PARAMETER,
111
116
" Could not parse access token processor {}: tenant_id must be specified" , name);
112
117
113
- String client_id_str = config.getString (prefix + " .client_id" );
114
118
String tenant_id_str = config.getString (prefix + " .tenant_id" );
115
- String client_secret_str = config.hasProperty (prefix + " .client_secret" ) ? config.getString (prefix + " .client_secret" ) : " " ;
116
119
117
- return std::make_unique<AzureAccessTokenProcessor>(name, email_regex_str, client_id_str , tenant_id_str, client_secret_str );
120
+ return std::make_unique<AzureAccessTokenProcessor>(name, cache_lifetime, email_regex_str , tenant_id_str);
118
121
}
119
122
else
120
123
throw Exception (ErrorCodes::INVALID_CONFIG_PARAMETER,
@@ -132,10 +135,11 @@ bool GoogleAccessTokenProcessor::resolveAndValidate(const TokenCredentials & cre
132
135
133
136
auto user_info = getUserInfo (token);
134
137
String user_name = user_info[" sub" ];
138
+ bool has_email = user_info.contains (" email" );
135
139
136
140
if (email_regex.ok ())
137
141
{
138
- if (!user_info. contains ( " email " ) )
142
+ if (!has_email )
139
143
{
140
144
LOG_TRACE (getLogger (" AccessTokenProcessor" ), " {}: Failed to validate {} by e-mail" , name, user_name);
141
145
return false ;
@@ -149,10 +153,54 @@ bool GoogleAccessTokenProcessor::resolveAndValidate(const TokenCredentials & cre
149
153
}
150
154
151
155
}
156
+
152
157
// / Credentials are passed as const everywhere up the flow, so we have to comply,
153
158
// / in this case const_cast looks acceptable.
154
159
const_cast <TokenCredentials &>(credentials).setUserName (user_name);
155
- const_cast <TokenCredentials &>(credentials).setGroups ({});
160
+
161
+ auto token_info = getObjectFromURI (Poco::URI (token_info_uri), token);
162
+ if (token_info.contains (" exp" ))
163
+ const_cast <TokenCredentials &>(credentials).setExpiresAt (std::chrono::system_clock::from_time_t ((getValueByKey<time_t >(token_info, " exp" ))));
164
+
165
+ // / Groups info can only be retrieved if user email is known.
166
+ // / If no email found in user info, we skip this step and there are no external groups for the user.
167
+ if (has_email)
168
+ {
169
+ std::set<String> external_groups_names;
170
+ const Poco::URI get_groups_uri = Poco::URI (" https://cloudidentity.googleapis.com/v1/groups/-/memberships:searchDirectGroups?query=member_key_id==" + user_info[" email" ] + " '" );
171
+
172
+ try
173
+ {
174
+ auto groups_response = getObjectFromURI (get_groups_uri, token);
175
+
176
+ if (!groups_response.contains (" memberships" )) {
177
+ LOG_TRACE (getLogger (" AccessTokenProcessor" ),
178
+ " {}: Failed to get Google groups: invalid content in response from server" , name);
179
+ return true ;
180
+ }
181
+
182
+ picojson::array groups_array = groups_response[" memberships" ].get <picojson::array>();
183
+
184
+ // / TODO: check for invalid JSON, LOG something meaningful
185
+ for (const auto & group: groups_array)
186
+ {
187
+ auto group_data = group.get <picojson::object>();
188
+ String group_name = getValueByKey (group_data[" groupKey" ].get <picojson::object>(), " id" );
189
+ external_groups_names.insert (group_name);
190
+ LOG_TRACE (getLogger (" AccessTokenProcessor" ),
191
+ " {}: User {}: new external group {}" , name, user_name, group_name);
192
+ }
193
+
194
+ const_cast <TokenCredentials &>(credentials).setGroups (external_groups_names);
195
+ }
196
+ catch (const Exception & e)
197
+ {
198
+ // / Could not get groups info. Log it and skip it.
199
+ LOG_TRACE (getLogger (" AccessTokenProcessor" ),
200
+ " {}: Failed to get Google groups, no external roles will be mapped. reason: {}" , name, e.what ());
201
+ return true ;
202
+ }
203
+ }
156
204
157
205
return true ;
158
206
}
@@ -177,8 +225,9 @@ std::unordered_map<String, String> GoogleAccessTokenProcessor::getUserInfo(const
177
225
178
226
bool AzureAccessTokenProcessor::resolveAndValidate (const TokenCredentials & credentials)
179
227
{
180
- // / Token is a JWT in this case, but we cannot directly verify it against Azure AD JWKS. We will not trust any data in this token.
181
- // / e.g. see here: https://stackoverflow.com/questions/60778634/failing-signature-validation-of-jwt-tokens-from-azure-ad
228
+ // / Token is a JWT in this case, but we cannot directly verify it against Azure AD JWKS.
229
+ // / We will not trust user data in this token except for 'exp' value to determine caching duration.
230
+ // / Explanation here: https://stackoverflow.com/questions/60778634/failing-signature-validation-of-jwt-tokens-from-azure-ad
182
231
// / Let Azure validate it: only valid tokens will be accepted.
183
232
// / Use GET https://graph.microsoft.com/oidc/userinfo to verify token and get sub at the same time
184
233
@@ -202,8 +251,49 @@ bool AzureAccessTokenProcessor::resolveAndValidate(const TokenCredentials & cred
202
251
return false ;
203
252
}
204
253
205
- // / TODO: do not store it in credentials.
206
- const_cast <TokenCredentials &>(credentials).setGroups ({});
254
+ try
255
+ {
256
+ const_cast <TokenCredentials &>(credentials).setExpiresAt (jwt::decode (token).get_expires_at ());
257
+ }
258
+ catch (...) {
259
+ LOG_TRACE (getLogger (" AccessTokenProcessor" ),
260
+ " {}: No expiration data found in a valid token, will use default cache lifetime" , name);
261
+ }
262
+
263
+ std::set<String> external_groups_names;
264
+ const Poco::URI get_groups_uri = Poco::URI (" https://graph.microsoft.com/v1.0/me/memberOf" );
265
+
266
+ try
267
+ {
268
+ auto groups_response = getObjectFromURI (get_groups_uri, token);
269
+
270
+ if (!groups_response.contains (" value" )) {
271
+ LOG_TRACE (getLogger (" AccessTokenProcessor" ),
272
+ " {}: Failed to get Azure groups: invalid content in response from server" , name);
273
+ return true ;
274
+ }
275
+
276
+ picojson::array groups_array = groups_response[" value" ].get <picojson::array>();
277
+
278
+ // / TODO: check for invalid JSON
279
+ for (const auto & group: groups_array)
280
+ {
281
+ auto group_data = group.get <picojson::object>();
282
+ String group_name = getValueByKey (group_data, " id" );
283
+ external_groups_names.insert (group_name);
284
+ LOG_TRACE (getLogger (" AccessTokenProcessor" ),
285
+ " {}: User {}: new external group {}" , name, credentials.getUserName (), group_name);
286
+ }
287
+ }
288
+ catch (const Exception & e)
289
+ {
290
+ // / Could not get groups info. Log it and skip it.
291
+ LOG_TRACE (getLogger (" AccessTokenProcessor" ),
292
+ " {}: Failed to get Azure groups, no external roles will be mapped. reason: {}" , name, e.what ());
293
+ return true ;
294
+ }
295
+
296
+ const_cast <TokenCredentials &>(credentials).setGroups (external_groups_names);
207
297
208
298
return true ;
209
299
}
0 commit comments