Skip to content

Commit d12608f

Browse files
authored
Merge pull request ClickHouse#62669 from slvrtrn/http-interface-role-query-param
Add `role` query parameter to the HTTP interface
2 parents 2f980f6 + d9fd79e commit d12608f

File tree

6 files changed

+225
-12
lines changed

6 files changed

+225
-12
lines changed

base/poco/Net/include/Poco/Net/NameValueCollection.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ namespace Net
7979
/// Returns the value of the first name-value pair with the given name.
8080
/// If no value with the given name has been found, the defaultValue is returned.
8181

82+
const std::vector<std::reference_wrapper<const std::string>> getAll(const std::string & name) const;
83+
/// Returns all values of all name-value pairs with the given name.
84+
///
85+
/// Returns an empty vector if there are no name-value pairs with the given name.
86+
8287
bool has(const std::string & name) const;
8388
/// Returns true if there is at least one name-value pair
8489
/// with the given name.

base/poco/Net/src/NameValueCollection.cpp

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "Poco/Net/NameValueCollection.h"
1616
#include "Poco/Exception.h"
1717
#include <algorithm>
18+
#include <functional>
1819

1920

2021
using Poco::NotFoundException;
@@ -55,7 +56,7 @@ void NameValueCollection::swap(NameValueCollection& nvc)
5556
std::swap(_map, nvc._map);
5657
}
5758

58-
59+
5960
const std::string& NameValueCollection::operator [] (const std::string& name) const
6061
{
6162
ConstIterator it = _map.find(name);
@@ -65,8 +66,8 @@ const std::string& NameValueCollection::operator [] (const std::string& name) co
6566
throw NotFoundException(name);
6667
}
6768

68-
69-
void NameValueCollection::set(const std::string& name, const std::string& value)
69+
70+
void NameValueCollection::set(const std::string& name, const std::string& value)
7071
{
7172
Iterator it = _map.find(name);
7273
if (it != _map.end())
@@ -75,13 +76,13 @@ void NameValueCollection::set(const std::string& name, const std::string& value)
7576
_map.insert(HeaderMap::ValueType(name, value));
7677
}
7778

78-
79+
7980
void NameValueCollection::add(const std::string& name, const std::string& value)
8081
{
8182
_map.insert(HeaderMap::ValueType(name, value));
8283
}
8384

84-
85+
8586
const std::string& NameValueCollection::get(const std::string& name) const
8687
{
8788
ConstIterator it = _map.find(name);
@@ -101,6 +102,15 @@ const std::string& NameValueCollection::get(const std::string& name, const std::
101102
return defaultValue;
102103
}
103104

105+
const std::vector<std::reference_wrapper<const std::string>> NameValueCollection::getAll(const std::string& name) const
106+
{
107+
std::vector<std::reference_wrapper<const std::string>> values;
108+
for (ConstIterator it = _map.find(name); it != _map.end(); it++)
109+
if (it->first == name)
110+
values.push_back(it->second);
111+
return values;
112+
}
113+
104114

105115
bool NameValueCollection::has(const std::string& name) const
106116
{
@@ -113,19 +123,19 @@ NameValueCollection::ConstIterator NameValueCollection::find(const std::string&
113123
return _map.find(name);
114124
}
115125

116-
126+
117127
NameValueCollection::ConstIterator NameValueCollection::begin() const
118128
{
119129
return _map.begin();
120130
}
121131

122-
132+
123133
NameValueCollection::ConstIterator NameValueCollection::end() const
124134
{
125135
return _map.end();
126136
}
127137

128-
138+
129139
bool NameValueCollection::empty() const
130140
{
131141
return _map.empty();

docs/en/interfaces/http.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,37 @@ $ curl -sS 'http://localhost:8123/?max_result_bytes=4000000&buffer_size=3000000&
325325

326326
Use buffering to avoid situations where a query processing error occurred after the response code and HTTP headers were sent to the client. In this situation, an error message is written at the end of the response body, and on the client-side, the error can only be detected at the parsing stage.
327327

328+
## Setting a role with query parameters {#setting-role-with-query-parameters}
329+
330+
In certain scenarios, it might be required to set the granted role first, before executing the statement itself.
331+
However, it is not possible to send `SET ROLE` and the statement together, as multi-statements are not allowed:
332+
333+
```
334+
curl -sS "http://localhost:8123" --data-binary "SET ROLE my_role;SELECT * FROM my_table;"
335+
```
336+
337+
Which will result in an error:
338+
339+
```
340+
Code: 62. DB::Exception: Syntax error (Multi-statements are not allowed)
341+
```
342+
343+
To overcome this limitation, you could use the `role` query parameter instead:
344+
345+
```
346+
curl -sS "http://localhost:8123?role=my_role" --data-binary "SELECT * FROM my_table;"
347+
```
348+
349+
This will be an equivalent of executing `SET ROLE my_role` before the statement.
350+
351+
Additionally, it is possible to specify multiple `role` query parameters:
352+
353+
```
354+
curl -sS "http://localhost:8123?role=my_role&role=my_other_role" --data-binary "SELECT * FROM my_table;"
355+
```
356+
357+
In this case, `?role=my_role&role=my_other_role` works similarly to executing `SET ROLE my_role, my_other_role` before the statement.
358+
328359
## HTTP response codes caveats {#http_response_codes_caveats}
329360

330361
Because of limitation of HTTP protocol, HTTP 200 response code does not guarantee that a query was successful.

src/Server/HTTPHandler.cpp

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
#include <Access/Authentication.h>
44
#include <Access/Credentials.h>
5+
#include <Access/AccessControl.h>
56
#include <Access/ExternalAuthenticators.h>
7+
#include <Access/Role.h>
8+
#include <Access/User.h>
69
#include <Compression/CompressedReadBuffer.h>
710
#include <Compression/CompressedWriteBuffer.h>
811
#include <Core/ExternalTable.h>
@@ -104,6 +107,7 @@ namespace ErrorCodes
104107
extern const int UNKNOWN_FORMAT;
105108
extern const int UNKNOWN_DATABASE_ENGINE;
106109
extern const int UNKNOWN_TYPE_OF_QUERY;
110+
extern const int UNKNOWN_ROLE;
107111
extern const int NO_ELEMENTS_IN_CONFIG;
108112

109113
extern const int QUERY_IS_TOO_LARGE;
@@ -115,6 +119,7 @@ namespace ErrorCodes
115119
extern const int WRONG_PASSWORD;
116120
extern const int REQUIRED_PASSWORD;
117121
extern const int AUTHENTICATION_FAILED;
122+
extern const int SET_NON_GRANTED_ROLE;
118123

119124
extern const int INVALID_SESSION_TIMEOUT;
120125
extern const int HTTP_LENGTH_REQUIRED;
@@ -140,7 +145,7 @@ bool tryAddHTTPOptionHeadersFromConfig(HTTPServerResponse & response, const Poco
140145
LOG_WARNING(getLogger("processOptionsRequest"), "Empty header was found in config. It will not be processed.");
141146
else
142147
response.add(config.getString("http_options_response." + config_key + ".name", ""),
143-
config.getString("http_options_response." + config_key + ".value", ""));
148+
config.getString("http_options_response." + config_key + ".value", ""));
144149

145150
}
146151
}
@@ -192,7 +197,8 @@ static Poco::Net::HTTPResponse::HTTPStatus exceptionCodeToHTTPStatus(int excepti
192197
}
193198
else if (exception_code == ErrorCodes::UNKNOWN_USER ||
194199
exception_code == ErrorCodes::WRONG_PASSWORD ||
195-
exception_code == ErrorCodes::AUTHENTICATION_FAILED)
200+
exception_code == ErrorCodes::AUTHENTICATION_FAILED ||
201+
exception_code == ErrorCodes::SET_NON_GRANTED_ROLE)
196202
{
197203
return HTTPResponse::HTTP_FORBIDDEN;
198204
}
@@ -235,7 +241,8 @@ static Poco::Net::HTTPResponse::HTTPStatus exceptionCodeToHTTPStatus(int excepti
235241
exception_code == ErrorCodes::UNKNOWN_AGGREGATE_FUNCTION ||
236242
exception_code == ErrorCodes::UNKNOWN_FORMAT ||
237243
exception_code == ErrorCodes::UNKNOWN_DATABASE_ENGINE ||
238-
exception_code == ErrorCodes::UNKNOWN_TYPE_OF_QUERY)
244+
exception_code == ErrorCodes::UNKNOWN_TYPE_OF_QUERY ||
245+
exception_code == ErrorCodes::UNKNOWN_ROLE)
239246
{
240247
return HTTPResponse::HTTP_NOT_FOUND;
241248
}
@@ -704,7 +711,7 @@ void HTTPHandler::processQuery(
704711

705712
std::unique_ptr<ReadBuffer> in;
706713

707-
static const NameSet reserved_param_names{"compress", "decompress", "user", "password", "quota_key", "query_id", "stacktrace",
714+
static const NameSet reserved_param_names{"compress", "decompress", "user", "password", "quota_key", "query_id", "stacktrace", "role",
708715
"buffer_size", "wait_end_of_query", "session_id", "session_timeout", "session_check", "client_protocol_version", "close_session"};
709716

710717
Names reserved_param_suffixes;
@@ -727,6 +734,23 @@ void HTTPHandler::processQuery(
727734
return false;
728735
};
729736

737+
auto roles = params.getAll("role");
738+
if (!roles.empty())
739+
{
740+
const auto & access_control = context->getAccessControl();
741+
const auto & user = context->getUser();
742+
std::vector<UUID> roles_ids(roles.size());
743+
for (size_t i = 0; i < roles.size(); i++)
744+
{
745+
auto role_id = access_control.getID<Role>(roles[i]);
746+
if (user->granted_roles.isGranted(role_id))
747+
roles_ids[i] = role_id;
748+
else
749+
throw Exception(ErrorCodes::SET_NON_GRANTED_ROLE, "Role {} should be granted to set as a current", roles[i].get());
750+
}
751+
context->setCurrentRoles(roles_ids);
752+
}
753+
730754
/// Settings can be overridden in the query.
731755
/// Some parameters (database, default_format, everything used in the code above) do not
732756
/// belong to the Settings class.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
### Shows the default role when there are no role parameters
2+
03096_role_query_param_role_enabled_by_default
3+
### Shows a single role from the query parameters
4+
03096_role_query_param_role1
5+
### Shows multiple roles from the query parameters
6+
03096_role_query_param_role1
7+
03096_role_query_param_role2
8+
### Sets the default role alongside with another granted one
9+
03096_role_query_param_role1
10+
03096_role_query_param_role_enabled_by_default
11+
### Sets a role with special characters in the name
12+
03096_role_query_param_@!\\$
13+
### Sets a role with special characters in the name with another granted role
14+
03096_role_query_param_@!\\$
15+
03096_role_query_param_role1
16+
### Sets a role once when it's present in the query parameters multiple times
17+
03096_role_query_param_role1
18+
### Sets a role when there are other parameters in the query (before the role parameter)
19+
03096_role_query_param_role1
20+
max_result_rows 42
21+
### Sets a role when there are other parameters in the query (after the role parameter)
22+
03096_role_query_param_role1
23+
max_result_rows 42
24+
### Sets multiple roles when there are other parameters in the query
25+
03096_role_query_param_role1
26+
03096_role_query_param_role2
27+
max_result_rows 42
28+
### Cannot set a role that does not exist (single parameter)
29+
Code: 511
30+
UNKNOWN_ROLE
31+
### Cannot set a role that does not exist (multiple parameters)
32+
Code: 511
33+
UNKNOWN_ROLE
34+
### Cannot set a role that is not granted to the user (single parameter)
35+
Code: 512
36+
SET_NON_GRANTED_ROLE
37+
### Cannot set a role that is not granted to the user (multiple parameters)
38+
Code: 512
39+
SET_NON_GRANTED_ROLE
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env bash
2+
# Tags: no-parallel
3+
4+
CUR_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
5+
# shellcheck source=../shell_config.sh
6+
. "$CUR_DIR"/../shell_config.sh
7+
8+
TEST_USER="03096_role_query_param_user"
9+
TEST_USER_AUTH="$TEST_USER:"
10+
11+
TEST_ROLE1="03096_role_query_param_role1"
12+
TEST_ROLE2="03096_role_query_param_role2"
13+
TEST_ROLE_ENABLED_BY_DEFAULT="03096_role_query_param_role_enabled_by_default"
14+
TEST_ROLE_NOT_GRANTED="03096_role_query_param_role_not_granted"
15+
TEST_ROLE_SPECIAL_CHARS="\`03096_role_query_param_@!\\$\`" # = CREATE ROLE `03096_role_query_param_@!\$`
16+
TEST_ROLE_SPECIAL_CHARS_URLENCODED="03096_role_query_param_%40!%5C%24"
17+
18+
CHANGED_SETTING_NAME="max_result_rows"
19+
CHANGED_SETTING_VALUE="42"
20+
21+
SHOW_CURRENT_ROLES_QUERY="SELECT role_name FROM system.current_roles ORDER BY role_name ASC"
22+
SHOW_CHANGED_SETTINGS_QUERY="SELECT name, value FROM system.settings WHERE changed = 1 AND name = '$CHANGED_SETTING_NAME' ORDER BY name ASC"
23+
24+
$CLICKHOUSE_CLIENT -n --query "
25+
DROP USER IF EXISTS $TEST_USER;
26+
DROP ROLE IF EXISTS $TEST_ROLE1;
27+
DROP ROLE IF EXISTS $TEST_ROLE2;
28+
DROP ROLE IF EXISTS $TEST_ROLE_ENABLED_BY_DEFAULT;
29+
DROP ROLE IF EXISTS $TEST_ROLE_NOT_GRANTED;
30+
DROP ROLE IF EXISTS $TEST_ROLE_SPECIAL_CHARS;
31+
CREATE USER $TEST_USER NOT IDENTIFIED;
32+
CREATE ROLE $TEST_ROLE_ENABLED_BY_DEFAULT;
33+
GRANT $TEST_ROLE_ENABLED_BY_DEFAULT TO $TEST_USER;
34+
SET DEFAULT ROLE $TEST_ROLE_ENABLED_BY_DEFAULT TO $TEST_USER;
35+
CREATE ROLE $TEST_ROLE1;
36+
GRANT $TEST_ROLE1 TO $TEST_USER;
37+
CREATE ROLE $TEST_ROLE2;
38+
GRANT $TEST_ROLE2 TO $TEST_USER;
39+
CREATE ROLE $TEST_ROLE_SPECIAL_CHARS;
40+
GRANT $TEST_ROLE_SPECIAL_CHARS TO $TEST_USER;
41+
CREATE ROLE $TEST_ROLE_NOT_GRANTED;
42+
"
43+
44+
echo "### Shows the default role when there are no role parameters"
45+
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
46+
47+
echo "### Shows a single role from the query parameters"
48+
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
49+
50+
echo "### Shows multiple roles from the query parameters"
51+
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1&role=$TEST_ROLE2" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
52+
53+
echo "### Sets the default role alongside with another granted one"
54+
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE_ENABLED_BY_DEFAULT&role=$TEST_ROLE1" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
55+
56+
echo "### Sets a role with special characters in the name"
57+
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE_SPECIAL_CHARS_URLENCODED" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
58+
59+
echo "### Sets a role with special characters in the name with another granted role"
60+
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE_SPECIAL_CHARS_URLENCODED&role=$TEST_ROLE1" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
61+
62+
echo "### Sets a role once when it's present in the query parameters multiple times"
63+
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1&role=$TEST_ROLE1" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
64+
65+
echo "### Sets a role when there are other parameters in the query (before the role parameter)"
66+
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&$CHANGED_SETTING_NAME=$CHANGED_SETTING_VALUE&role=$TEST_ROLE1" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
67+
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&$CHANGED_SETTING_NAME=$CHANGED_SETTING_VALUE&role=$TEST_ROLE1" --data-binary "$SHOW_CHANGED_SETTINGS_QUERY"
68+
69+
echo "### Sets a role when there are other parameters in the query (after the role parameter)"
70+
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1&$CHANGED_SETTING_NAME=$CHANGED_SETTING_VALUE" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
71+
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1&$CHANGED_SETTING_NAME=$CHANGED_SETTING_VALUE" --data-binary "$SHOW_CHANGED_SETTINGS_QUERY"
72+
73+
echo "### Sets multiple roles when there are other parameters in the query"
74+
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1&$CHANGED_SETTING_NAME=$CHANGED_SETTING_VALUE&role=$TEST_ROLE2" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
75+
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1&$CHANGED_SETTING_NAME=$CHANGED_SETTING_VALUE&role=$TEST_ROLE2" --data-binary "$SHOW_CHANGED_SETTINGS_QUERY"
76+
77+
echo "### Cannot set a role that does not exist (single parameter)"
78+
OUT=$($CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=aaaaaaaaaaa" --data-binary "$SHOW_CURRENT_ROLES_QUERY")
79+
echo -ne $OUT | grep -o "Code: 511" || echo "expected code 511, got: $OUT"
80+
echo -ne $OUT | grep -o "UNKNOWN_ROLE" || echo "expected UNKNOWN_ROLE error, got: $OUT"
81+
82+
echo "### Cannot set a role that does not exist (multiple parameters)"
83+
OUT=$($CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1&role=aaaaaaaaaaa" --data-binary "$SHOW_CURRENT_ROLES_QUERY")
84+
echo -ne $OUT | grep -o "Code: 511" || echo "expected code 511, got: $OUT"
85+
echo -ne $OUT | grep -o "UNKNOWN_ROLE" || echo "expected UNKNOWN_ROLE error, got: $OUT"
86+
87+
echo "### Cannot set a role that is not granted to the user (single parameter)"
88+
OUT=$($CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE_NOT_GRANTED" --data-binary "$SHOW_CURRENT_ROLES_QUERY")
89+
echo -ne $OUT | grep -o "Code: 512" || echo "expected code 512, got: $OUT"
90+
echo -ne $OUT | grep -o "SET_NON_GRANTED_ROLE" || echo "expected SET_NON_GRANTED_ROLE error, got: $OUT"
91+
92+
echo "### Cannot set a role that is not granted to the user (multiple parameters)"
93+
OUT=$($CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1&role=$TEST_ROLE_NOT_GRANTED" --data-binary "$SHOW_CURRENT_ROLES_QUERY")
94+
echo -ne $OUT | grep -o "Code: 512" || echo "expected code 512, got: $OUT"
95+
echo -ne $OUT | grep -o "SET_NON_GRANTED_ROLE" || echo "expected SET_NON_GRANTED_ROLE error, got: $OUT"
96+
97+
$CLICKHOUSE_CLIENT -n --query "
98+
DROP USER $TEST_USER;
99+
DROP ROLE $TEST_ROLE1;
100+
DROP ROLE $TEST_ROLE2;
101+
DROP ROLE $TEST_ROLE_ENABLED_BY_DEFAULT;
102+
DROP ROLE $TEST_ROLE_NOT_GRANTED;
103+
DROP ROLE $TEST_ROLE_SPECIAL_CHARS;
104+
"

0 commit comments

Comments
 (0)