Skip to content

Commit

Permalink
Also support Authorization:Bearer header to specify access token (#…
Browse files Browse the repository at this point in the history
…1720)

The access token (for privileged functionality) could so far only be passed as a parameter (that is, either as a field in the data for `application/x-www-form-urlencoded` or as a query parameter). With this change, the access token is also read from the `Authorization` header, provided it exists and the value has the form `Bearer <access token>`. If the access token is specified both via the `Authorization` header and via the `access-token` parameter, they must be the same. Resolves #1691
  • Loading branch information
Qup42 authored Jan 31, 2025
1 parent 49934c0 commit 1f38ba3
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/sparql-conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ jobs:
echo ${{github.event.pull_request.head.sha}} > ./conformance-report/sha
mv ${{ github.workspace}}/qlever-test-suite/results/${{ github.sha }}.json.bz2 conformance-report/${{ github.event.pull_request.head.sha }}.json.bz2
- name: Upload coverage artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: conformance-report
path: conformance-report/
65 changes: 52 additions & 13 deletions src/engine/Server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,37 @@ void Server::run(const string& indexBaseName, bool useText, bool usePatterns,
httpServer.run();
}

std::optional<std::string> Server::extractAccessToken(
const ad_utility::httpUtils::HttpRequest auto& request,
const ad_utility::url_parser::ParamValueMap& params) {
std::optional<std::string> tokenFromAuthorizationHeader;
std::optional<std::string> tokenFromParameter;
if (request.find(http::field::authorization) != request.end()) {
string_view authorization = request[http::field::authorization];
const std::string prefix = "Bearer ";
if (!authorization.starts_with(prefix)) {
throw std::runtime_error(absl::StrCat(
"Authorization header doesn't start with \"", prefix, "\"."));
}
authorization.remove_prefix(prefix.length());
tokenFromAuthorizationHeader = std::string(authorization);
}
if (params.contains("access-token")) {
tokenFromParameter = ad_utility::url_parser::getParameterCheckAtMostOnce(
params, "access-token");
}
// If both are specified, they must be equal. This way there is no hidden
// precedence.
if (tokenFromAuthorizationHeader && tokenFromParameter &&
tokenFromAuthorizationHeader != tokenFromParameter) {
throw std::runtime_error(
"Access token is specified both in the `Authorization` header and by "
"the `access-token` parameter, but they are not the same");
}
return tokenFromAuthorizationHeader ? std::move(tokenFromAuthorizationHeader)
: std::move(tokenFromParameter);
}

// _____________________________________________________________________________
ad_utility::url_parser::ParsedRequest Server::parseHttpRequest(
const ad_utility::httpUtils::HttpRequest auto& request) {
Expand All @@ -177,7 +208,8 @@ ad_utility::url_parser::ParsedRequest Server::parseHttpRequest(
// This is a concatenation of the URL path and the query strings.
auto parsedUrl = ad_utility::url_parser::parseRequestTarget(request.target());
ad_utility::url_parser::ParsedRequest parsedRequest{
std::move(parsedUrl.path_), std::move(parsedUrl.parameters_), None{}};
std::move(parsedUrl.path_), std::nullopt,
std::move(parsedUrl.parameters_), None{}};

// Some valid requests (e.g. QLever's custom commands like retrieving index
// statistics) don't have a query. So an empty operation is not necessarily an
Expand Down Expand Up @@ -208,10 +240,15 @@ ad_utility::url_parser::ParsedRequest Server::parseHttpRequest(
addToDatasetClausesIfOperationIs(ti<Update>, "using-graph-uri", false);
addToDatasetClausesIfOperationIs(ti<Update>, "using-named-graph-uri", true);
};
auto extractAccessTokenFromRequest = [&parsedRequest, &request]() {
parsedRequest.accessToken_ =
extractAccessToken(request, parsedRequest.parameters_);
};

if (request.method() == http::verb::get) {
setOperationIfSpecifiedInParams(ti<Query>, "query");
addDatasetClauses();
extractAccessTokenFromRequest();

if (parsedRequest.parameters_.contains("update")) {
throw std::runtime_error("SPARQL Update is not allowed as GET request.");
Expand Down Expand Up @@ -283,16 +320,22 @@ ad_utility::url_parser::ParsedRequest Server::parseHttpRequest(
setOperationIfSpecifiedInParams(ti<Query>, "query");
setOperationIfSpecifiedInParams(ti<Update>, "update");
addDatasetClauses();
// We parse the access token from the url-encoded parameters in the body.
// The URL parameters must be empty for URL-encoded POST (see above).
extractAccessTokenFromRequest();

return parsedRequest;
}
if (contentType.starts_with(contentTypeSparqlQuery)) {
parsedRequest.operation_ = Query{request.body(), {}};
addDatasetClauses();
extractAccessTokenFromRequest();
return parsedRequest;
}
if (contentType.starts_with(contentTypeSparqlUpdate)) {
parsedRequest.operation_ = Update{request.body(), {}};
addDatasetClauses();
extractAccessTokenFromRequest();
return parsedRequest;
}
throw std::runtime_error(absl::StrCat(
Expand Down Expand Up @@ -369,15 +412,13 @@ Awaitable<void> Server::process(
// Check the access token. If an access token is provided and the check fails,
// throw an exception and do not process any part of the query (even if the
// processing had been allowed without access token).
bool accessTokenOk =
checkAccessToken(checkParameter("access-token", std::nullopt));
bool accessTokenOk = checkAccessToken(parsedHttpRequest.accessToken_);
auto requireValidAccessToken = [&accessTokenOk](
const std::string& actionName) {
if (!accessTokenOk) {
throw std::runtime_error(absl::StrCat(
actionName,
" requires a valid access token. No valid access token is present.",
"Processing of request aborted."));
" requires a valid access token but no access token was provided"));
}
};

Expand Down Expand Up @@ -1181,17 +1222,15 @@ bool Server::checkAccessToken(
if (!accessToken) {
return false;
}
auto accessTokenProvidedMsg = absl::StrCat(
"Access token \"access-token=", accessToken.value(), "\" provided");
auto requestIgnoredMsg = ", request is ignored";
const auto accessTokenProvidedMsg = "Access token was provided";
if (accessToken_.empty()) {
throw std::runtime_error(absl::StrCat(
accessTokenProvidedMsg,
" but server was started without --access-token", requestIgnoredMsg));
throw std::runtime_error(
absl::StrCat(accessTokenProvidedMsg,
" but server was started without --access-token"));
} else if (!ad_utility::constantTimeEquals(accessToken.value(),
accessToken_)) {
throw std::runtime_error(absl::StrCat(
accessTokenProvidedMsg, " but not correct", requestIgnoredMsg));
throw std::runtime_error(
absl::StrCat(accessTokenProvidedMsg, " but it was invalid"));
} else {
LOG(DEBUG) << accessTokenProvidedMsg << " and correct" << std::endl;
return true;
Expand Down
7 changes: 7 additions & 0 deletions src/engine/Server.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class Server {
FRIEND_TEST(ServerTest, parseHttpRequest);
FRIEND_TEST(ServerTest, getQueryId);
FRIEND_TEST(ServerTest, createMessageSender);
FRIEND_TEST(ServerTest, extractAccessToken);

public:
explicit Server(unsigned short port, size_t numThreads,
Expand Down Expand Up @@ -115,6 +116,12 @@ class Server {
static ad_utility::url_parser::ParsedRequest parseHttpRequest(
const ad_utility::httpUtils::HttpRequest auto& request);

/// Extract the Access token for that request from the `Authorization` header
/// or the URL query parameters.
static std::optional<std::string> extractAccessToken(
const ad_utility::httpUtils::HttpRequest auto& request,
const ad_utility::url_parser::ParamValueMap& params);

/// Handle a single HTTP request. Check whether a file request or a query was
/// sent, and dispatch to functions handling these cases. This function
/// requires the constraints for the `HttpHandler` in `HttpServer.h`.
Expand Down
2 changes: 2 additions & 0 deletions src/util/http/UrlParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@ struct None {

// Representation of parsed HTTP request.
// - `path_` is the URL path
// - `accessToken_` is the access token for that request
// - `parameters_` is a hashmap of the parameters
// - `operation_` the operation that should be performed
struct ParsedRequest {
std::string path_;
std::optional<std::string> accessToken_;
ParamValueMap parameters_;
std::variant<sparqlOperation::Query, sparqlOperation::Update,
sparqlOperation::None>
Expand Down
Loading

0 comments on commit 1f38ba3

Please sign in to comment.