Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add role query parameter to the HTTP interface #62669

Merged
merged 13 commits into from
Apr 17, 2024
Merged
31 changes: 31 additions & 0 deletions docs/en/interfaces/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
35 changes: 31 additions & 4 deletions src/Server/HTTPHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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;
Expand All @@ -115,6 +119,7 @@ namespace ErrorCodes
extern const int WRONG_PASSWORD;
extern const int REQUIRED_PASSWORD;
extern const int AUTHENTICATION_FAILED;
extern const int ACCESS_DENIED;

extern const int INVALID_SESSION_TIMEOUT;
extern const int HTTP_LENGTH_REQUIRED;
Expand All @@ -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", ""));

}
}
Expand Down Expand Up @@ -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::ACCESS_DENIED)
{
return HTTPResponse::HTTP_FORBIDDEN;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -727,6 +734,26 @@ void HTTPHandler::processQuery(
return false;
};

auto role_params_it = params.find("role");
if (role_params_it != params.end())
{
std::vector<UUID> roles_ids;
const auto & access_control = context->getAccessControl();
const auto & user = context->getUser();
for (; role_params_it != params.end(); role_params_it++)
{
if (role_params_it->first == "role")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this algorithm extracting multiple values of a parameter to the class of params.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vitlibar, this is coming from Poco, namely, Poco::ListMap, if I am not mistaken; is it OK to add it there? I assume it is since it is included in the CH source code and not as a contrib module.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a new NameValueCollection::getAll method to extract all the parameters. It might be useful in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I thought about modifying DB::HTMLForm actually, but probably modifying NameValueCollection is also ok.

{
auto role_id = access_control.getID<Role>(role_params_it->second);
if (user->granted_roles.isGranted(role_id))
roles_ids.push_back(role_id);
else
throw Exception(ErrorCodes::ACCESS_DENIED, "Role {} is not granted to the current user", role_params_it->second);
}
}
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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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 is not granted to the user (single parameter)
Code: 497
ACCESS_DENIED
### Cannot set a role that is not granted to the user (multiple parameters)
Code: 497
ACCESS_DENIED
### 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
102 changes: 102 additions & 0 deletions tests/queries/0_stateless/03096_http_interface_role_query_param.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env bash

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 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: 497" || echo "expected code 497, got: $OUT"
echo -ne $OUT | grep -o "ACCESS_DENIED" || echo "expected ACCESS_DENIED 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: 497" || echo "expected code 497, got: $OUT"
echo -ne $OUT | grep -o "ACCESS_DENIED" || echo "expected ACCESS_DENIED error, got: $OUT"

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"

$CLICKHOUSE_CLIENT -n --query "
DROP USER $TEST_USER;
DROP ROLE $TEST_ROLE1;
DROP ROLE $TEST_ROLE_ENABLED_BY_DEFAULT;
DROP ROLE $TEST_ROLE_NOT_GRANTED;
DROP ROLE $TEST_ROLE_SPECIAL_CHARS;
"