Skip to content

Commit 406ddb7

Browse files
committed
pre-refactor tmp commit
1 parent 26dec4f commit 406ddb7

File tree

9 files changed

+97
-70
lines changed

9 files changed

+97
-70
lines changed

docs/en/operations/external-authenticators/tokens.md

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,21 @@ OAuth 2.0 access tokens can be used to authenticate ClickHouse users. This works
1313

1414
Though this authentication method is different from JWT authentication, it works under the same authentication method to maintain better compatibility.
1515

16-
For both of these approaches a definition of `access_token_processors` is mandatory.
16+
For both of these approaches a definition of `token_processors` is mandatory.
1717

1818
## Access Token Processors
1919

20-
To define an access token processor, add `access_token_processors` section to `config.xml`. Example:
20+
To define an access token processor, add `token_processors` section to `config.xml`. Example:
2121
```xml
2222
<clickhouse>
23-
<access_token_processors>
24-
<gogoogle>
25-
<provider>Google</provider>
26-
<email_filter>^[A-Za-z0-9._%+-]+@example\.com$</email_filter>
27-
<cache_lifetime>600</cache_lifetime>
28-
</gogoogle>
23+
<token_processors>
2924
<azuure>
3025
<provider>azure</provider>
26+
<username_claim>claim_name</username_claim>
3127
<client_id>CLIENT_ID</client_id>
3228
<tenant_id>TENANT_ID</tenant_id>
3329
</azuure>
34-
</access_token_processors>
30+
</token_processors>
3531
</clickhouse>
3632
```
3733

@@ -42,11 +38,12 @@ Different providers have different sets of parameters.
4238
**Parameters**
4339

4440
- `provider` -- name of identity provider. Mandatory, case-insensitive. Supported options: "Google", "Azure", "OpenID".
41+
- `username_claim` -- name of claim (field) that will be treated as ClickHouse user name. Optional, default: "sub".
4542
- `cache_lifetime` -- maximum lifetime of cached token (in seconds). Optional, default: 3600.
4643
- `email_filter` -- Regex for validation of user emails. Optional parameter, only for Google IdP.
4744
- `client_id` -- Azure AD (Entra ID) client ID. Optional parameter, used only for Azure IdP.
4845
- `tenant_id` -- Azure AD (Entra ID) tenant ID. Optional parameter, used only for Azure IdP.
49-
- `groups_claim_name` -- Name of claim (field) that contains list of groups user belongs to. This claim will be looked up in the token itself (in case token is a valid JWT, e.g. in Keycloak) or in response from `/userinfo`. Optional parameter.
46+
- `groups_claim` -- Name of claim (field) that contains list of groups user belongs to. This claim will be looked up in the token itself (in case token is a valid JWT, e.g. in Keycloak) or in response from `/userinfo`. Optional parameter.
5047
- `configuration_endpoint` -- URI of `.well-known/openid-configuration`. Optional parameter, useful only for OIDC-compliant providers (e.g. Keycloak).
5148
- `userinfo_endpoint` -- URI of userinfo endpoint. Optional parameter.
5249
- `token_introspection_endpoint` -- URI of token introspection endpoint. Optional parameter.
@@ -67,17 +64,20 @@ Locally defined users can be authenticated with an access token. To allow this,
6764

6865
```xml
6966
<clickhouse>
70-
<!- ... -->
7167
<users>
72-
<!- ... -->
7368
<my_user>
74-
<!- ... -->
69+
<jwt>
70+
<allowed_processors>
71+
<azuure />
72+
</allowed_processors>
7573
</jwt>
7674
</my_user>
7775
</users>
7876
</clickhouse>
7977
```
8078

79+
Inside `jwt` one or more specific access token processors names can be specified -- only those processors will be tried when authenticating. If no processors are specified, _all_ processors will be tried.
80+
8181
At each login attempt, ClickHouse will attempt to validate token and get user info against every defined access token provider.
8282

8383
When SQL-driven [Access Control and Account Management](/docs/en/guides/sre/user-management/index.md#access-control) is enabled, users that are authenticated with tokens can also be created using the [CREATE USER](/docs/en/sql-reference/statements/create/user.md#create-user-statement) statement.
@@ -94,7 +94,7 @@ If there is no suitable user pre-defined in ClickHouse, authentication is still
9494
To allow this, add `token` section to the `users_directories` section of the `config.xml` file.
9595

9696
At each login attempt, ClickHouse tries to find the user definition locally and authenticate it as usual.
97-
If the user is not defined, ClickHouse will treat user as externally defined, and will try to validate the token and get user information from the specified processor.
97+
If the user is not defined, ClickHouse will treat the user as externally defined and will try to validate the token and get user information from the specified processor.
9898
If validated successfully, the user will be considered existing and authenticated. The user will be assigned roles from the list specified in the `roles` section.
9999
All this implies that the SQL-driven [Access Control and Account Management](/docs/en/guides/sre/user-management/index.md#access-control) is enabled and roles are created using the [CREATE ROLE](/docs/en/sql-reference/statements/create/role.md#create-role-statement) statement.
100100

@@ -105,15 +105,21 @@ All this implies that the SQL-driven [Access Control and Account Management](/do
105105
<user_directories>
106106
<token>
107107
<processor>processor_name</processor>
108-
<roles>
108+
<common_roles>
109109
<token_test_role_1 />
110-
</roles>
110+
</common_roles>
111+
<roles_filter></roles_filter>
111112
</token>
112113
</user_directories>
113114
</clickhouse>
114115
```
115116

117+
:::note
118+
For now, no more than one `token` section can be defined inside `user_directories`. This _may_ change in future.
119+
:::
120+
116121
**Parameters**
117122

118-
- `processor` — Name of one of processors defined in `access_token_processors` config section described above. This parameter is mandatory and cannot be empty.
119-
- `roles` — Section with a list of locally defined roles that will be assigned to each user retrieved from the IdP.
123+
- `processor` — Name of one of processors defined in `token_processors` config section described above. This parameter is mandatory and cannot be empty.
124+
- `common_roles` — Section with a list of locally defined roles that will be assigned to each user retrieved from the IdP. Optional.
125+
- `roles_filter` — Regex string for groups filtering. Only groups matching this regex will be mapped to roles. Optional.

src/Access/AccessControl.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ namespace ErrorCodes
4242
extern const int REQUIRED_PASSWORD;
4343
extern const int CANNOT_COMPILE_REGEXP;
4444
extern const int BAD_ARGUMENTS;
45+
extern const int INVALID_CONFIG_PARAMETER;
4546
}
4647

4748
namespace
@@ -440,6 +441,8 @@ void AccessControl::addStoragesFromUserDirectoriesConfig(
440441
Strings keys_in_user_directories;
441442
config.keys(key, keys_in_user_directories);
442443

444+
bool has_token_storage = false;
445+
443446
for (const String & key_in_user_directories : keys_in_user_directories)
444447
{
445448
String prefix = key + "." + key_in_user_directories;
@@ -490,7 +493,11 @@ void AccessControl::addStoragesFromUserDirectoriesConfig(
490493
}
491494
else if (type == TokenAccessStorage::STORAGE_TYPE)
492495
{
496+
if (has_token_storage)
497+
throw Exception(ErrorCodes::INVALID_CONFIG_PARAMETER, "Only one `token` section can be defined.");
498+
493499
addTokenStorage(name, config, prefix);
500+
has_token_storage = true;
494501
}
495502
else
496503
throw Exception(ErrorCodes::UNKNOWN_ELEMENT_IN_CONFIG, "Unknown storage type '{}' at {} in config", type, prefix);

src/Access/AccessTokenProcessor.cpp

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,9 @@ std::unique_ptr<IAccessTokenProcessor> IAccessTokenProcessor::parseTokenProcesso
9797
{
9898
String provider = Poco::toLower(config.getString(prefix + ".provider"));
9999

100-
String email_regex_str = config.hasProperty(prefix + ".email_filter") ? config.getString(
101-
prefix + ".email_filter") : "";
102-
103-
UInt64 cache_lifetime = config.hasProperty(prefix + ".cache_lifetime") ? config.getUInt64(
104-
prefix + ".cache_lifetime") : 3600;
100+
String email_regex_str = config.getString(prefix + ".email_filter", "");
101+
UInt64 cache_lifetime = config.getUInt64(prefix + ".cache_lifetime", 3600);
102+
String username_claim = config.getString(prefix + ".username_claim", "sub");
105103

106104
if (provider == "google")
107105
{
@@ -134,7 +132,7 @@ std::unique_ptr<IAccessTokenProcessor> IAccessTokenProcessor::parseTokenProcesso
134132
cache_lifetime,
135133
email_regex_str,
136134
config.getString(prefix + ".configuration_endpoint"),
137-
config.getString(prefix + ".groups_claim_name", ""));
135+
config.getString(prefix + ".groups_claim", ""));
138136
}
139137
else if (!is_auto && is_manual)
140138
{
@@ -144,7 +142,7 @@ std::unique_ptr<IAccessTokenProcessor> IAccessTokenProcessor::parseTokenProcesso
144142
config.getString(prefix + ".userinfo_endpoint"),
145143
config.getString(prefix + ".token_introspection_endpoint"),
146144
config.getString(prefix + ".jwks_uri"),
147-
config.getString(prefix + ".groups_claim_name", ""));
145+
config.getString(prefix + ".groups_claim", ""));
148146
}
149147

150148
throw Exception(ErrorCodes::INVALID_CONFIG_PARAMETER, "Could not parse access token processor {}: "
@@ -165,7 +163,7 @@ bool GoogleAccessTokenProcessor::resolveAndValidate(const TokenCredentials & cre
165163
const String & token = credentials.getToken();
166164

167165
auto user_info = getUserInfo(token);
168-
String user_name = user_info["sub"];
166+
String user_name = user_info[username_claim];
169167
bool has_email = user_info.contains("email");
170168

171169
if (email_regex.ok())
@@ -249,7 +247,7 @@ std::unordered_map<String, String> GoogleAccessTokenProcessor::getUserInfo(const
249247
try
250248
{
251249
user_info_map["email"] = getValueByKey(user_info_json, "email");
252-
user_info_map["sub"] = getValueByKey(user_info_json, "sub");
250+
user_info_map[username_claim] = getValueByKey(user_info_json, username_claim);
253251
return user_info_map;
254252
}
255253
catch (std::runtime_error & e)
@@ -265,7 +263,7 @@ bool AzureAccessTokenProcessor::resolveAndValidate(const TokenCredentials & cred
265263
/// We will not trust user data in this token except for 'exp' value to determine caching duration.
266264
/// Explanation here: https://stackoverflow.com/questions/60778634/failing-signature-validation-of-jwt-tokens-from-azure-ad
267265
/// Let Azure validate it: only valid tokens will be accepted.
268-
/// Use GET https://graph.microsoft.com/oidc/userinfo to verify token and get sub at the same time
266+
/// Use GET https://graph.microsoft.com/oidc/userinfo to verify token and get user info at the same time
269267

270268
const String & token = credentials.getToken();
271269

@@ -344,7 +342,7 @@ bool AzureAccessTokenProcessor::resolveAndValidate(const TokenCredentials & cred
344342
String AzureAccessTokenProcessor::validateTokenAndGetUsername(const String & token) const
345343
{
346344
picojson::object user_info_json = getObjectFromURI(user_info_uri, token);
347-
return getValueByKey(user_info_json, "sub");
345+
return getValueByKey(user_info_json, username_claim);
348346
}
349347

350348

@@ -354,9 +352,9 @@ OpenIDAccessTokenProcessor::OpenIDAccessTokenProcessor(const String & name_,
354352
const String & userinfo_endpoint_,
355353
const String & token_introspection_endpoint_,
356354
const String & jwks_uri_,
357-
const String & groups_claim_name_)
355+
const String & groups_claim_)
358356
: IAccessTokenProcessor(name_, cache_invalidation_interval_, email_regex_str),
359-
userinfo_endpoint(userinfo_endpoint_), token_introspection_endpoint(token_introspection_endpoint_), groups_claim_name(groups_claim_name_)
357+
userinfo_endpoint(userinfo_endpoint_), token_introspection_endpoint(token_introspection_endpoint_), groups_claim(groups_claim_)
360358
{
361359
if (!jwks_uri_.empty())
362360
{
@@ -369,7 +367,7 @@ OpenIDAccessTokenProcessor::OpenIDAccessTokenProcessor(const String & name_,
369367
const UInt64 cache_invalidation_interval_,
370368
const String & email_regex_str,
371369
const String & openid_config_endpoint_,
372-
const String & groups_claim_name_)
370+
const String & groups_claim_)
373371
: IAccessTokenProcessor(name_, cache_invalidation_interval_, email_regex_str)
374372
{
375373
const picojson::object openid_config = getObjectFromURI(Poco::URI(openid_config_endpoint_));
@@ -383,7 +381,7 @@ OpenIDAccessTokenProcessor::OpenIDAccessTokenProcessor(const String & name_,
383381
getValueByKey(openid_config, "userinfo_endpoint"),
384382
getValueByKey(openid_config, "introspection_endpoint"),
385383
openid_config.contains("jwks_uri") ? getValueByKey(openid_config, "jwks_uri") : "",
386-
groups_claim_name_);
384+
groups_claim_);
387385
}
388386

389387
bool OpenIDAccessTokenProcessor::resolveAndValidate(const TokenCredentials & credentials)
@@ -415,7 +413,7 @@ bool OpenIDAccessTokenProcessor::resolveAndValidate(const TokenCredentials & cre
415413
try
416414
{
417415
user_info_json = getObjectFromURI(userinfo_endpoint, token);
418-
username = getValueByKey(user_info_json, "sub");
416+
username = getValueByKey(user_info_json, username_claim);
419417
}
420418
catch (...)
421419
{
@@ -440,9 +438,9 @@ bool OpenIDAccessTokenProcessor::resolveAndValidate(const TokenCredentials & cre
440438

441439
/// For now, list of groups is expected in a claim with specified name either in token itself or in userinfo response (Keycloak works this way)
442440
/// TODO: add support for custom endpoints for retrieving groups. Keycloak lists groups in /userinfo and token itself, which is not always the case.
443-
if (!groups_claim_name.empty() && user_info_json.contains(groups_claim_name))
441+
if (!groups_claim.empty() && user_info_json.contains(groups_claim))
444442
{
445-
if (!user_info_json[groups_claim_name].is<picojson::array>())
443+
if (!user_info_json[groups_claim].is<picojson::array>())
446444
{
447445
LOG_TRACE(getLogger("AccessTokenProcessor"),
448446
"{}: Failed to extract groups: invalid content in user data", name);
@@ -451,7 +449,7 @@ bool OpenIDAccessTokenProcessor::resolveAndValidate(const TokenCredentials & cre
451449

452450
std::set<String> external_groups_names;
453451

454-
picojson::array groups_array = user_info_json[groups_claim_name].get<picojson::array>();
452+
picojson::array groups_array = user_info_json[groups_claim].get<picojson::array>();
455453
for (const auto & group: groups_array)
456454
{
457455
if (group.is<std::string>())

src/Access/AccessTokenProcessor.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class IAccessTokenProcessor
5555

5656
protected:
5757
const String name;
58+
const String username_claim = "sub";
5859
const UInt64 cache_invalidation_interval;
5960
re2::RE2 email_regex;
6061

@@ -107,7 +108,7 @@ class OpenIDAccessTokenProcessor : public IAccessTokenProcessor
107108
const UInt64 cache_invalidation_interval_,
108109
const String & email_regex_str,
109110
const String & openid_config_endpoint_,
110-
const String & groups_claim_name_);
111+
const String & groups_claim_);
111112

112113
/// Specify endpoints manually
113114
OpenIDAccessTokenProcessor(const String & name_,
@@ -116,7 +117,7 @@ class OpenIDAccessTokenProcessor : public IAccessTokenProcessor
116117
const String & userinfo_endpoint_,
117118
const String & token_introspection_endpoint_,
118119
const String & jwks_uri_,
119-
const String & groups_claim_name_);
120+
const String & groups_claim_);
120121

121122
bool resolveAndValidate(const TokenCredentials & credentials) override;
122123
private:
@@ -127,7 +128,7 @@ class OpenIDAccessTokenProcessor : public IAccessTokenProcessor
127128
std::optional<JWKSValidator> jwt_validator = std::nullopt;
128129

129130
/// groups are expected under /userinfo endpoint under specified name
130-
const String groups_claim_name;
131+
const String groups_claim;
131132
};
132133

133134
}

src/Access/AuthenticationData.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ class AuthenticationData
8282
const String & getJWTClaims() const { return jwt_claims; }
8383
void setJWTClaims(const String & jwt_claims_) { jwt_claims = jwt_claims_; }
8484

85+
const String & getTokenProcessorName() const { return token_processor_name; }
86+
void setTokenProcessorName(const String & token_processor_name_) { token_processor_name = token_processor_name_; }
87+
8588
friend bool operator ==(const AuthenticationData & lhs, const AuthenticationData & rhs);
8689
friend bool operator !=(const AuthenticationData & lhs, const AuthenticationData & rhs) { return !(lhs == rhs); }
8790

@@ -121,6 +124,7 @@ class AuthenticationData
121124
HTTPAuthenticationScheme http_auth_scheme = HTTPAuthenticationScheme::BASIC;
122125
time_t valid_until = 0;
123126
String jwt_claims;
127+
String token_processor_name;
124128
};
125129

126130
}

0 commit comments

Comments
 (0)