Merge pull request #62669 from slvrtrn/http-interface-role-query-param

Add `role` query parameter to the HTTP interface
This commit is contained in:
Vitaly Baranov 2024-04-17 16:39:49 +00:00 committed by GitHub
commit d12608f366
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 225 additions and 12 deletions

View File

@ -79,6 +79,11 @@ namespace Net
/// Returns the value of the first name-value pair with the given name.
/// If no value with the given name has been found, the defaultValue is returned.
const std::vector<std::reference_wrapper<const std::string>> getAll(const std::string & name) const;
/// Returns all values of all name-value pairs with the given name.
///
/// Returns an empty vector if there are no name-value pairs with the given name.
bool has(const std::string & name) const;
/// Returns true if there is at least one name-value pair
/// with the given name.

View File

@ -15,6 +15,7 @@
#include "Poco/Net/NameValueCollection.h"
#include "Poco/Exception.h"
#include <algorithm>
#include <functional>
using Poco::NotFoundException;
@ -55,7 +56,7 @@ void NameValueCollection::swap(NameValueCollection& nvc)
std::swap(_map, nvc._map);
}
const std::string& NameValueCollection::operator [] (const std::string& name) const
{
ConstIterator it = _map.find(name);
@ -65,8 +66,8 @@ const std::string& NameValueCollection::operator [] (const std::string& name) co
throw NotFoundException(name);
}
void NameValueCollection::set(const std::string& name, const std::string& value)
void NameValueCollection::set(const std::string& name, const std::string& value)
{
Iterator it = _map.find(name);
if (it != _map.end())
@ -75,13 +76,13 @@ void NameValueCollection::set(const std::string& name, const std::string& value)
_map.insert(HeaderMap::ValueType(name, value));
}
void NameValueCollection::add(const std::string& name, const std::string& value)
{
_map.insert(HeaderMap::ValueType(name, value));
}
const std::string& NameValueCollection::get(const std::string& name) const
{
ConstIterator it = _map.find(name);
@ -101,6 +102,15 @@ const std::string& NameValueCollection::get(const std::string& name, const std::
return defaultValue;
}
const std::vector<std::reference_wrapper<const std::string>> NameValueCollection::getAll(const std::string& name) const
{
std::vector<std::reference_wrapper<const std::string>> values;
for (ConstIterator it = _map.find(name); it != _map.end(); it++)
if (it->first == name)
values.push_back(it->second);
return values;
}
bool NameValueCollection::has(const std::string& name) const
{
@ -113,19 +123,19 @@ NameValueCollection::ConstIterator NameValueCollection::find(const std::string&
return _map.find(name);
}
NameValueCollection::ConstIterator NameValueCollection::begin() const
{
return _map.begin();
}
NameValueCollection::ConstIterator NameValueCollection::end() const
{
return _map.end();
}
bool NameValueCollection::empty() const
{
return _map.empty();

View File

@ -325,6 +325,37 @@ $ curl -sS 'http://localhost:8123/?max_result_bytes=4000000&buffer_size=3000000&
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.
## Setting a role with query parameters {#setting-role-with-query-parameters}
In certain scenarios, it might be required to set the granted role first, before executing the statement itself.
However, it is not possible to send `SET ROLE` and the statement together, as multi-statements are not allowed:
```
curl -sS "http://localhost:8123" --data-binary "SET ROLE my_role;SELECT * FROM my_table;"
```
Which will result in an error:
```
Code: 62. DB::Exception: Syntax error (Multi-statements are not allowed)
```
To overcome this limitation, you could use the `role` query parameter instead:
```
curl -sS "http://localhost:8123?role=my_role" --data-binary "SELECT * FROM my_table;"
```
This will be an equivalent of executing `SET ROLE my_role` before the statement.
Additionally, it is possible to specify multiple `role` query parameters:
```
curl -sS "http://localhost:8123?role=my_role&role=my_other_role" --data-binary "SELECT * FROM my_table;"
```
In this case, `?role=my_role&role=my_other_role` works similarly to executing `SET ROLE my_role, my_other_role` before the statement.
## HTTP response codes caveats {#http_response_codes_caveats}
Because of limitation of HTTP protocol, HTTP 200 response code does not guarantee that a query was successful.

View File

@ -2,7 +2,10 @@
#include <Access/Authentication.h>
#include <Access/Credentials.h>
#include <Access/AccessControl.h>
#include <Access/ExternalAuthenticators.h>
#include <Access/Role.h>
#include <Access/User.h>
#include <Compression/CompressedReadBuffer.h>
#include <Compression/CompressedWriteBuffer.h>
#include <Core/ExternalTable.h>
@ -104,6 +107,7 @@ namespace ErrorCodes
extern const int UNKNOWN_FORMAT;
extern const int UNKNOWN_DATABASE_ENGINE;
extern const int UNKNOWN_TYPE_OF_QUERY;
extern const int UNKNOWN_ROLE;
extern const int NO_ELEMENTS_IN_CONFIG;
extern const int QUERY_IS_TOO_LARGE;
@ -115,6 +119,7 @@ namespace ErrorCodes
extern const int WRONG_PASSWORD;
extern const int REQUIRED_PASSWORD;
extern const int AUTHENTICATION_FAILED;
extern const int SET_NON_GRANTED_ROLE;
extern const int INVALID_SESSION_TIMEOUT;
extern const int HTTP_LENGTH_REQUIRED;
@ -140,7 +145,7 @@ bool tryAddHTTPOptionHeadersFromConfig(HTTPServerResponse & response, const Poco
LOG_WARNING(getLogger("processOptionsRequest"), "Empty header was found in config. It will not be processed.");
else
response.add(config.getString("http_options_response." + config_key + ".name", ""),
config.getString("http_options_response." + config_key + ".value", ""));
config.getString("http_options_response." + config_key + ".value", ""));
}
}
@ -192,7 +197,8 @@ static Poco::Net::HTTPResponse::HTTPStatus exceptionCodeToHTTPStatus(int excepti
}
else if (exception_code == ErrorCodes::UNKNOWN_USER ||
exception_code == ErrorCodes::WRONG_PASSWORD ||
exception_code == ErrorCodes::AUTHENTICATION_FAILED)
exception_code == ErrorCodes::AUTHENTICATION_FAILED ||
exception_code == ErrorCodes::SET_NON_GRANTED_ROLE)
{
return HTTPResponse::HTTP_FORBIDDEN;
}
@ -235,7 +241,8 @@ static Poco::Net::HTTPResponse::HTTPStatus exceptionCodeToHTTPStatus(int excepti
exception_code == ErrorCodes::UNKNOWN_AGGREGATE_FUNCTION ||
exception_code == ErrorCodes::UNKNOWN_FORMAT ||
exception_code == ErrorCodes::UNKNOWN_DATABASE_ENGINE ||
exception_code == ErrorCodes::UNKNOWN_TYPE_OF_QUERY)
exception_code == ErrorCodes::UNKNOWN_TYPE_OF_QUERY ||
exception_code == ErrorCodes::UNKNOWN_ROLE)
{
return HTTPResponse::HTTP_NOT_FOUND;
}
@ -704,7 +711,7 @@ void HTTPHandler::processQuery(
std::unique_ptr<ReadBuffer> in;
static const NameSet reserved_param_names{"compress", "decompress", "user", "password", "quota_key", "query_id", "stacktrace",
static const NameSet reserved_param_names{"compress", "decompress", "user", "password", "quota_key", "query_id", "stacktrace", "role",
"buffer_size", "wait_end_of_query", "session_id", "session_timeout", "session_check", "client_protocol_version", "close_session"};
Names reserved_param_suffixes;
@ -727,6 +734,23 @@ void HTTPHandler::processQuery(
return false;
};
auto roles = params.getAll("role");
if (!roles.empty())
{
const auto & access_control = context->getAccessControl();
const auto & user = context->getUser();
std::vector<UUID> roles_ids(roles.size());
for (size_t i = 0; i < roles.size(); i++)
{
auto role_id = access_control.getID<Role>(roles[i]);
if (user->granted_roles.isGranted(role_id))
roles_ids[i] = role_id;
else
throw Exception(ErrorCodes::SET_NON_GRANTED_ROLE, "Role {} should be granted to set as a current", roles[i].get());
}
context->setCurrentRoles(roles_ids);
}
/// Settings can be overridden in the query.
/// Some parameters (database, default_format, everything used in the code above) do not
/// belong to the Settings class.

View File

@ -0,0 +1,39 @@
### Shows the default role when there are no role parameters
03096_role_query_param_role_enabled_by_default
### Shows a single role from the query parameters
03096_role_query_param_role1
### Shows multiple roles from the query parameters
03096_role_query_param_role1
03096_role_query_param_role2
### Sets the default role alongside with another granted one
03096_role_query_param_role1
03096_role_query_param_role_enabled_by_default
### Sets a role with special characters in the name
03096_role_query_param_@!\\$
### Sets a role with special characters in the name with another granted role
03096_role_query_param_@!\\$
03096_role_query_param_role1
### Sets a role once when it's present in the query parameters multiple times
03096_role_query_param_role1
### Sets a role when there are other parameters in the query (before the role parameter)
03096_role_query_param_role1
max_result_rows 42
### Sets a role when there are other parameters in the query (after the role parameter)
03096_role_query_param_role1
max_result_rows 42
### Sets multiple roles when there are other parameters in the query
03096_role_query_param_role1
03096_role_query_param_role2
max_result_rows 42
### Cannot set a role that does not exist (single parameter)
Code: 511
UNKNOWN_ROLE
### Cannot set a role that does not exist (multiple parameters)
Code: 511
UNKNOWN_ROLE
### Cannot set a role that is not granted to the user (single parameter)
Code: 512
SET_NON_GRANTED_ROLE
### Cannot set a role that is not granted to the user (multiple parameters)
Code: 512
SET_NON_GRANTED_ROLE

View File

@ -0,0 +1,104 @@
#!/usr/bin/env bash
# Tags: no-parallel
CUR_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=../shell_config.sh
. "$CUR_DIR"/../shell_config.sh
TEST_USER="03096_role_query_param_user"
TEST_USER_AUTH="$TEST_USER:"
TEST_ROLE1="03096_role_query_param_role1"
TEST_ROLE2="03096_role_query_param_role2"
TEST_ROLE_ENABLED_BY_DEFAULT="03096_role_query_param_role_enabled_by_default"
TEST_ROLE_NOT_GRANTED="03096_role_query_param_role_not_granted"
TEST_ROLE_SPECIAL_CHARS="\`03096_role_query_param_@!\\$\`" # = CREATE ROLE `03096_role_query_param_@!\$`
TEST_ROLE_SPECIAL_CHARS_URLENCODED="03096_role_query_param_%40!%5C%24"
CHANGED_SETTING_NAME="max_result_rows"
CHANGED_SETTING_VALUE="42"
SHOW_CURRENT_ROLES_QUERY="SELECT role_name FROM system.current_roles ORDER BY role_name ASC"
SHOW_CHANGED_SETTINGS_QUERY="SELECT name, value FROM system.settings WHERE changed = 1 AND name = '$CHANGED_SETTING_NAME' ORDER BY name ASC"
$CLICKHOUSE_CLIENT -n --query "
DROP USER IF EXISTS $TEST_USER;
DROP ROLE IF EXISTS $TEST_ROLE1;
DROP ROLE IF EXISTS $TEST_ROLE2;
DROP ROLE IF EXISTS $TEST_ROLE_ENABLED_BY_DEFAULT;
DROP ROLE IF EXISTS $TEST_ROLE_NOT_GRANTED;
DROP ROLE IF EXISTS $TEST_ROLE_SPECIAL_CHARS;
CREATE USER $TEST_USER NOT IDENTIFIED;
CREATE ROLE $TEST_ROLE_ENABLED_BY_DEFAULT;
GRANT $TEST_ROLE_ENABLED_BY_DEFAULT TO $TEST_USER;
SET DEFAULT ROLE $TEST_ROLE_ENABLED_BY_DEFAULT TO $TEST_USER;
CREATE ROLE $TEST_ROLE1;
GRANT $TEST_ROLE1 TO $TEST_USER;
CREATE ROLE $TEST_ROLE2;
GRANT $TEST_ROLE2 TO $TEST_USER;
CREATE ROLE $TEST_ROLE_SPECIAL_CHARS;
GRANT $TEST_ROLE_SPECIAL_CHARS TO $TEST_USER;
CREATE ROLE $TEST_ROLE_NOT_GRANTED;
"
echo "### Shows the default role when there are no role parameters"
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
echo "### Shows a single role from the query parameters"
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
echo "### Shows multiple roles from the query parameters"
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1&role=$TEST_ROLE2" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
echo "### Sets the default role alongside with another granted one"
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE_ENABLED_BY_DEFAULT&role=$TEST_ROLE1" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
echo "### Sets a role with special characters in the name"
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE_SPECIAL_CHARS_URLENCODED" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
echo "### Sets a role with special characters in the name with another granted role"
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE_SPECIAL_CHARS_URLENCODED&role=$TEST_ROLE1" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
echo "### Sets a role once when it's present in the query parameters multiple times"
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1&role=$TEST_ROLE1" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
echo "### Sets a role when there are other parameters in the query (before the role parameter)"
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&$CHANGED_SETTING_NAME=$CHANGED_SETTING_VALUE&role=$TEST_ROLE1" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&$CHANGED_SETTING_NAME=$CHANGED_SETTING_VALUE&role=$TEST_ROLE1" --data-binary "$SHOW_CHANGED_SETTINGS_QUERY"
echo "### Sets a role when there are other parameters in the query (after the role parameter)"
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1&$CHANGED_SETTING_NAME=$CHANGED_SETTING_VALUE" --data-binary "$SHOW_CURRENT_ROLES_QUERY"
$CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1&$CHANGED_SETTING_NAME=$CHANGED_SETTING_VALUE" --data-binary "$SHOW_CHANGED_SETTINGS_QUERY"
echo "### Sets multiple roles when there are other parameters in the query"
$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"
$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"
echo "### Cannot set a role that does not exist (single parameter)"
OUT=$($CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=aaaaaaaaaaa" --data-binary "$SHOW_CURRENT_ROLES_QUERY")
echo -ne $OUT | grep -o "Code: 511" || echo "expected code 511, got: $OUT"
echo -ne $OUT | grep -o "UNKNOWN_ROLE" || echo "expected UNKNOWN_ROLE error, got: $OUT"
echo "### Cannot set a role that does not exist (multiple parameters)"
OUT=$($CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1&role=aaaaaaaaaaa" --data-binary "$SHOW_CURRENT_ROLES_QUERY")
echo -ne $OUT | grep -o "Code: 511" || echo "expected code 511, got: $OUT"
echo -ne $OUT | grep -o "UNKNOWN_ROLE" || echo "expected UNKNOWN_ROLE error, got: $OUT"
echo "### Cannot set a role that is not granted to the user (single parameter)"
OUT=$($CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE_NOT_GRANTED" --data-binary "$SHOW_CURRENT_ROLES_QUERY")
echo -ne $OUT | grep -o "Code: 512" || echo "expected code 512, got: $OUT"
echo -ne $OUT | grep -o "SET_NON_GRANTED_ROLE" || echo "expected SET_NON_GRANTED_ROLE error, got: $OUT"
echo "### Cannot set a role that is not granted to the user (multiple parameters)"
OUT=$($CLICKHOUSE_CURL -u $TEST_USER_AUTH -sS "$CLICKHOUSE_URL&role=$TEST_ROLE1&role=$TEST_ROLE_NOT_GRANTED" --data-binary "$SHOW_CURRENT_ROLES_QUERY")
echo -ne $OUT | grep -o "Code: 512" || echo "expected code 512, got: $OUT"
echo -ne $OUT | grep -o "SET_NON_GRANTED_ROLE" || echo "expected SET_NON_GRANTED_ROLE error, got: $OUT"
$CLICKHOUSE_CLIENT -n --query "
DROP USER $TEST_USER;
DROP ROLE $TEST_ROLE1;
DROP ROLE $TEST_ROLE2;
DROP ROLE $TEST_ROLE_ENABLED_BY_DEFAULT;
DROP ROLE $TEST_ROLE_NOT_GRANTED;
DROP ROLE $TEST_ROLE_SPECIAL_CHARS;
"