-
Notifications
You must be signed in to change notification settings - Fork 227
add api-key parameter #3690
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 api-key parameter #3690
Conversation
src/http_rest_api_handler.cpp
Outdated
} | ||
if (!api_key.empty()) { | ||
if (request_components.headers.count("authorization")) { | ||
if (request_components.headers.at("authorization") != "Bearer " + api_key) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (request_components.headers.at("authorization") != "Bearer " + api_key) { | |
if (request_components.headers.at("authorization") != "Bearer " + apiKey) { |
Is exact match safe here? No risk of some additional whitespaces?
src/http_rest_api_handler.cpp
Outdated
if (!api_key.empty()) { | ||
if (request_components.headers.count("authorization")) { | ||
if (request_components.headers.at("authorization") != "Bearer " + api_key) { | ||
SPDLOG_DEBUG("Unauthorized request - invalid API key {} instead of {}", request_components.headers.at("authorization"), api_key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SPDLOG_DEBUG("Unauthorized request - invalid API key {} instead of {}", request_components.headers.at("authorization"), api_key); | |
SPDLOG_DEBUG("Unauthorized request - invalid API key {} instead of {}", request_components.headers.at("authorization"), apiKey); |
src/server.cpp
Outdated
|
||
std::string Server::getAPIKey() const { | ||
return apiKey; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds API key authentication support for generative endpoints in the HTTP REST API. The implementation introduces an optional authentication mechanism that validates Bearer tokens against a configured API key.
- Adds API key configuration parameter and server-level storage
- Implements Bearer token validation in V3 HTTP endpoints
- Adds unauthorized status code for invalid authentication attempts
Reviewed Changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
File | Description |
---|---|
src/status.hpp | Adds UNAUTHORIZED status code enum |
src/status.cpp | Adds status message mapping for unauthorized requests |
src/server.hpp | Adds API key storage and getter method |
src/server.cpp | Implements API key initialization and getter |
src/http_rest_api_handler.hpp | Updates processV3 signature to accept API key parameter |
src/http_rest_api_handler.cpp | Implements Bearer token validation logic |
src/config.hpp | Adds API key getter method to Config class |
src/config.cpp | Implements API key getter method |
src/cli_parser.cpp | Adds API key command line parameter parsing |
src/capi_frontend/server_settings.hpp | Adds API key field to server settings |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
src/server.hpp
Outdated
Status startModules(ovms::Config& config); | ||
void shutdownModules(); | ||
std::string getAPIKey() const; | ||
std::string setAPIKey(const std::string& newApiKey); |
Copilot
AI
Oct 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The setAPIKey method is declared but never implemented. Either implement this method or remove the declaration if API key modification after initialization is not intended.
std::string setAPIKey(const std::string& newApiKey); | |
std::string setAPIKey(const std::string& newApiKey) { | |
std::unique_lock<std::shared_mutex> lock(modulesMtx); | |
std::string oldApiKey = apiKey; | |
apiKey = newApiKey; | |
return oldApiKey; | |
} |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HttpServerModule has access to ovms::config in HTTPServerModule::start(ovms::Config)
Either extend ovms::createAndStartDrogonHttpServer()
with additional parameter or wrap
apiKey together with restBindAddress,restPort, workersCount in helper struct nested inside server settings (HTTPServerConfig?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bump
src/http_rest_api_handler.cpp
Outdated
// convert headers to lowercase because http headers are case insensitive | ||
for (const auto& [key, value] : request_components.headers) { | ||
std::string lowercaseKey = key; | ||
std::transform(lowercaseKey.begin(), lowercaseKey.end(), lowercaseKey.begin(), | ||
[](unsigned char c) { return std::tolower(c); }); | ||
} |
Copilot
AI
Oct 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loop converts header keys to lowercase but doesn't store the results anywhere. The lowercaseKey variable is not used, so the header lookup will still be case-sensitive. Either store the lowercase headers in a new map or perform case-insensitive lookup directly.
Copilot uses AI. Check for mistakes.
src/http_rest_api_handler.cpp
Outdated
if (request_components.headers.count("authorization")) { | ||
if (request_components.headers.at("authorization") != "Bearer " + api_key) { |
Copilot
AI
Oct 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Header lookup uses exact case 'authorization' but HTTP headers are case-insensitive. This will fail if the client sends 'Authorization' (capitalized). Use case-insensitive header lookup or convert the headers map to lowercase keys.
Copilot uses AI. Check for mistakes.
src/cli_parser.cpp
Outdated
serverSettings.apiKey.erase(serverSettings.apiKey.find_last_not_of(" \n\r\t") + 1); | ||
file.close(); | ||
} else { | ||
throw std::runtime_error("Unable to open API key file: " + api_key_file.string()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't we just cerr here & exit, instead of throw & instant catch?
Co-authored-by: Adrian Tobiszewski <adrian.tobiszewski@intel.com> Co-authored-by: Miłosz Żeglarski <milosz.zeglarski@intel.com>
docs/security_considerations.md
Outdated
|
||
--- | ||
Generative endpoints starting with `/v3`, might be restricted with authorization and API key. It can be set during the server initialization with a parameter `api_key_file` or environment variable `API_KEY`. | ||
The `api_key_file` should contains a path to the file containing the value of API key. The content of the file first line is used. If parameter api_key_file and variable API_KEY are not set, the server will not require any authorization. The client should send the API key inside the `Authorization` header as `Bearer <api_key>`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The `api_key_file` should contains a path to the file containing the value of API key. The content of the file first line is used. If parameter api_key_file and variable API_KEY are not set, the server will not require any authorization. The client should send the API key inside the `Authorization` header as `Bearer <api_key>`. | |
The `api_key_file` should contain a path to the file containing the value of API key. The content of the file first line is used. If parameter api_key_file and variable API_KEY are not set, the server will not require any authorization. The client should send the API key inside the `Authorization` header as `Bearer <api_key>`. |
docs/security_considerations.md
Outdated
OpenVINO Model Server has a set of mechanisms preventing denial of service attacks from the client applications. They include the following: | ||
- setting the number of inference execution streams which can limit the number of parallel inference calls in progress for each model. It can be tuned with `NUM_STREAMS` or `PERFORMANCE_HINT` plugin config. | ||
- setting the maximum number of gRPC threads which is, by default, configured to the number 8 * number_of_cores. It can be changed with the parameter `--grpc_max_threads`. | ||
- setting the maximum number of REST workers which is, be default, configured to the number 4 * number_of_cores. It can be changed with the parameter `--grpc_rest_workers`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is parameter name correct? grpc_rest_workers
src/cli_parser.cpp
Outdated
serverSettings.allowedOrigins = result->operator[]("allowed_origins").as<std::string>(); | ||
serverSettings.allowedMethods = result->operator[]("allowed_methods").as<std::string>(); | ||
serverSettings.allowedHeaders = result->operator[]("allowed_headers").as<std::string>(); | ||
std::filesystem::path api_key_file = result->operator[]("api_key_file").as<std::string>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
std::filesystem::path api_key_file = result->operator[]("api_key_file").as<std::string>(); | |
std::filesystem::path apiKeyFile= result->operator[]("api_key_file").as<std::string>(); |
src/http_rest_api_handler.cpp
Outdated
Status HttpRestApiHandler::checkIfAuthorized(const std::unordered_map<std::string, std::string>& headers, const std::string& apiKey) { | ||
if (!apiKey.empty()) { | ||
auto lowercaseHeaders = toLowerCaseHeaders(headers); | ||
if (lowercaseHeaders.count("authorization")) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use find(...) != end
? It would better explain the intent
src/status.cpp
Outdated
{StatusCode::UNKNOWN_REQUEST_COMPONENTS_TYPE, "Request components type not recognized"}, | ||
{StatusCode::FAILED_TO_PARSE_MULTIPART_CONTENT_TYPE, "Request of multipart type but failed to parse"}, | ||
{StatusCode::FAILED_TO_DEDUCE_MODEL_NAME_FROM_URI, "Failed to deduce model name from all possible ways"}, | ||
{StatusCode::UNAUTHORIZED, "Unauthorized request due to invalid api-key"}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
invalid or missing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{StatusCode::UNAUTHORIZED, "Unauthorized request due to invalid api-key"}, | |
{StatusCode::UNAUTHORIZED, "Unauthorized request due to invalid or missing api-key"}, |
src/status.hpp
Outdated
UNKNOWN_REQUEST_COMPONENTS_TYPE, /*!< Components type not recognized */ | ||
FAILED_TO_PARSE_MULTIPART_CONTENT_TYPE, /*!< Request of multipart type but failed to parse */ | ||
FAILED_TO_DEDUCE_MODEL_NAME_FROM_URI, /*!< Failed to deduce model name from all possible ways */ | ||
UNAUTHORIZED, /*!< Unauthorized request due to invalid api-key*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
invalid or missing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UNAUTHORIZED, /*!< Unauthorized request due to invalid api-key*/ | |
UNAUTHORIZED, /*!< Unauthorized request due to invalid or missing api-key*/ |
auto streamPtr = std::static_pointer_cast<ovms::HttpAsyncWriter>(stream); | ||
std::string response; | ||
auto status = handler->processV3("/v3/completions", comp, response, requestBody, streamPtr, multiPartParser, "1234"); | ||
ASSERT_EQ(status, ovms::StatusCode::MEDIAPIPE_DEFINITION_NAME_MISSING) << status.string(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non-okay status - might be tricky if at some point order of operations is changed and ovms::StatusCode::MEDIAPIPE_DEFINITION_NAME_MISSING gets returned before authorization check. In such case we would not know if authorization works as it will be covered by this status.
src/http_rest_api_handler.cpp
Outdated
|
||
Status HttpRestApiHandler::checkIfAuthorized(const std::unordered_map<std::string, std::string>& headers, const std::string& apiKey) { | ||
if (!apiKey.empty()) { | ||
auto lowercaseHeaders = toLowerCaseHeaders(headers); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we enable authorization for each http request we will create new map of headers for each request. Then we use count, and at so we double search the map.
Maybe sth like this would be more efficient:
bool caseInsensitiveEquals(std::string_view a, std::string_view b) noexcept {
if (a.size() != b.size()) return false;
for (size_t i = 0; i < a.size(); ++i) {
unsigned char ca = static_cast<unsigned char>(a[i]);
unsigned char cb = static_cast<unsigned char>(b[i]);
if (std::tolower(ca) != std::tolower(cb))
return false;
}
return true;
}
const std::string* findCaseInsensitive(
const std::unordered_map<std::string, std::string>& map,
std::string_view key) noexcept
{
auto it = std::find_if(map.begin(), map.end(),
[&](const auto& kv) {
return iequals(kv.first, key);
});
return (it != map.end()) ? &it->second : nullptr;
}
Then we check if there's key in the map (!=nullptr) we compare with expected key
src/http_rest_api_handler.cpp
Outdated
|
||
// convert headers to lowercase because http headers are case insensitive | ||
std::unordered_map<std::string, std::string> lowercaseHeaders; | ||
Status authStatus = checkIfAuthorized(request_components.headers, apiKey); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about v1/v2?
src/test/ovmsconfig_test.cpp
Outdated
} | ||
|
||
TEST(OvmsAPIKeyConfig, positiveAPIKeyEnv) { | ||
SetEnvironmentVar("API_KEY", "ABCD"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SetEnvironmentVar("API_KEY", "ABCD"); | |
EnvGuard envGuard; | |
envGuard.set("API_KEY", "ABCD"); |
src/test/ovmsconfig_test.cpp
Outdated
|
||
ASSERT_EQ(config.getServerSettings().apiKey, "ABCD"); | ||
// Clean up the environment variable | ||
UnSetEnvironmentVar("API_KEY"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UnSetEnvironmentVar("API_KEY"); |
This won't unset if we exit with error in L1938
src/http_rest_api_handler.hpp
Outdated
Status processServerMetadataKFSRequest(const HttpRequestComponents& request_components, std::string& response, const std::string& request_body); | ||
|
||
Status processV3(const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, std::shared_ptr<HttpAsyncWriter> serverReaderWriter, std::shared_ptr<MultiPartParser> multiPartParser); | ||
Status processV3(const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, std::shared_ptr<HttpAsyncWriter> serverReaderWriter, std::shared_ptr<MultiPartParser> multiPartParser, const std::string& apiKey); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not static and we have access to member in processV3. Inside processV3 we could just use
this->apiKey
.
src/test/metrics_flow_test.cpp
Outdated
auto status = handler.processV3("/v3/completions", comps, response, request, streamPtr, multiPartParser, ""); | ||
ASSERT_EQ(status, ovms::StatusCode::OK) << status.string(); | ||
status = handler.processV3("/v3/v1/completions", comps, response, request, streamPtr, multiPartParser); | ||
status = handler.processV3("/v3/v1/completions", comps, response, request, streamPtr, multiPartParser, ""); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we use member of handler in processV3 method instead of passing argument we could just leave this code as on main branch
🛠 Summary
CVS-174477
Enable client authorization using API key passed in authorization header.
API key can be set using --api_key_file parameter or using API_KEY env variable
Key verification is enabled only for generative endpoints /v3
🧪 Checklist
``