-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
auth: new api auth implementation #36968
base: main
Are you sure you want to change the base?
Changes from 1 commit
9c77db7
e543fb6
58e74c0
962515f
1126c96
98e2ac2
4d4605f
68232cd
0e2550e
9eea7c8
96dbe4e
3d909d0
d90a0ee
a9a03ee
a0263a2
6d72835
1818af7
b93795c
98ed3ed
4d9973f
750bc3d
ab295f3
b204906
6eb8935
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,50 +28,22 @@ option (xds.annotations.v3.file_status).work_in_progress = true; | |
// authentication_header: "X-API-KEY" | ||
// credentials: | ||
wbpcode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// entries: | ||
// - api_key: 09876abcdefg | ||
// - key: 09876abcdefg | ||
// client_id: user | ||
// key_sources: | ||
// entries: | ||
// - header: "X-API-KEY" | ||
// | ||
// If multiple fields of ``authentication_header``, ``authentication_query``, and ``authentication_cookie`` | ||
// are set. Then filter will try to fetch the API key from the following fields in order: | ||
// | ||
// 1. ``authentication_header`` | ||
// 2. ``authentication_query`` | ||
// 3. ``authentication_cookie`` | ||
// | ||
// If none of the fields are set, then the filter will reject the request. | ||
message ApiKeyAuth { | ||
message Credential { | ||
// The value of the unique API key. | ||
string api_key = 1 [(validate.rules).string = {min_len: 1}]; | ||
|
||
// The unique id or identity that used to identify the client or consumer. | ||
string client_id = 2 [(validate.rules).string = {min_len: 1}]; | ||
} | ||
|
||
message Credentials { | ||
repeated Credential entries = 1; | ||
} | ||
|
||
// Api credentials used to authenticate the clients. | ||
// The credentials that are used to authenticate the clients. | ||
Credentials credentials = 1 [(udpa.annotations.sensitive) = true]; | ||
|
||
// The header name to fetch the key. | ||
// If multiple values are present in the given header, the filter rejects the request. | ||
string authentication_header = 2 | ||
[(validate.rules).string = | ||
{max_len: 1024 well_known_regex: HTTP_HEADER_NAME strict: false ignore_empty: true}]; | ||
|
||
// The query parameter name to fetch the key. | ||
string authentication_query = 3 [(validate.rules).string = {max_len: 1024}]; | ||
|
||
// The cookie name to fetch the key. | ||
string authentication_cookie = 4 | ||
[(validate.rules).string = | ||
{max_len: 1024 well_known_regex: HTTP_HEADER_NAME strict: false ignore_empty: true}]; | ||
// The key sources to fetch the key from the coming request. | ||
KeySources key_sources = 2; | ||
} | ||
|
||
// API key auth configuration of per route or per virtual host or per route configuration. | ||
message ApiKeyAuthPerScope { | ||
message ApiKeyAuthPerRoute { | ||
// Route specific APIKeyAuth configuration. This is optional and could be used to override the | ||
// filter level ApiKeyAuth configuration **partly**. | ||
// | ||
|
@@ -89,3 +61,50 @@ message ApiKeyAuthPerScope { | |
// <config_http_filters_rbac>` instead. | ||
repeated string allowed_clients = 2; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find this field to be somewhat confusing, as there isn't a list of allowed clients in the non-route-override case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are two fields in the No As the comment said, it provide optional simple authorization (Authentication used to find who send the request, authorization used to detect if the client has the permission to access the route. We find lots of users or even developers cannot distinguish the authentication and authorization, they only want simple authentication and limit the route accessing by a simple way, rbac is too complex/heavy for this type users.)
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What should be the behavior if both Also, re:
This is true if only There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They are independent and won't effect each others. The The filter will try to authenticate the client by the |
||
} | ||
|
||
// Single credential entry that contains the API key and the related client id. | ||
message Credential { | ||
// The value of the unique API key. | ||
string key = 1 [(validate.rules).string = {min_len: 1}]; | ||
wbpcode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// The unique id or identity that used to identify the client or consumer. | ||
string client_id = 2 [(validate.rules).string = {min_len: 1}]; | ||
} | ||
|
||
// The credentials that are used to authenticate the clients. | ||
message Credentials { | ||
// The list of credentials. Explicitly setting the Credentials message with zero entries is | ||
// allowed and will result in the filter rejecting all requests. | ||
wbpcode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
repeated Credential entries = 1; | ||
} | ||
|
||
message KeySource { | ||
// The header name to fetch the key. If multiple header values are present, the first one will be | ||
// used. | ||
wbpcode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// | ||
// If set, takes precedence over ``query`` and ``cookie``. | ||
string header = 1 | ||
[(validate.rules).string = | ||
{max_len: 1024 well_known_regex: HTTP_HEADER_NAME strict: false ignore_empty: true}]; | ||
|
||
// The query parameter name to fetch the key. If multiple query values are present, the first one | ||
// will be used. | ||
// | ||
// Only makes sense if ``header`` is not set. If set, takes precedence over ``cookie``. | ||
string query = 2 [(validate.rules).string = {max_len: 1024}]; | ||
|
||
// The cookie name to fetch the key. | ||
// | ||
// Only makes sense if the ``header`` and ``query`` are not set. | ||
string cookie = 3 | ||
[(validate.rules).string = | ||
{max_len: 1024 well_known_regex: HTTP_HEADER_NAME strict: false ignore_empty: true}]; | ||
} | ||
wbpcode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// The key sources to fetch the key from the coming request. | ||
message KeySources { | ||
// The list of key sources. Explicitly setting the KeySources message with zero entries is | ||
// not allowed. The order of the key sources is important and the filter will try to fetch the | ||
// key one by one from the key sources in the order they are defined. | ||
repeated KeySource entries = 1 [(validate.rules).repeated = {min_items: 1}]; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,73 +15,85 @@ namespace HttpFilters { | |
namespace ApiKeyAuth { | ||
|
||
ApiKeyAuthConfig::ApiKeyAuthConfig(const ApiKeyAuthProto& proto_config) | ||
: key_source_(proto_config.authentication_header(), proto_config.authentication_query(), | ||
proto_config.authentication_cookie()) { | ||
: key_sources_(proto_config.key_sources()) { | ||
if (!proto_config.has_credentials()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What should be the behavior if there are no credentials? Is this here just so there's a route override? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there are no credentials but the route has provide one, then the filter still could works. This provide the biggest flexibility to let users to combine the filter config and route config. And even there is no any credentials finally at both filter and route, the filter will handle that case correctly by return a 401 response because we cannot authenticate anyone at that case. |
||
return; | ||
} | ||
|
||
ApiKeyMap api_key_map; | ||
api_key_map.reserve(proto_config.credentials().entries_size()); | ||
Credentials credentials; | ||
credentials.reserve(proto_config.credentials().entries_size()); | ||
|
||
for (const auto& credential : proto_config.credentials().entries()) { | ||
if (api_key_map.contains(credential.api_key())) { | ||
if (credentials.contains(credential.key())) { | ||
throwEnvoyExceptionOrPanic("Duplicate API key."); | ||
wbpcode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
api_key_map[credential.api_key()] = credential.client_id(); | ||
credentials[credential.key()] = credential.client_id(); | ||
} | ||
|
||
api_key_map_ = std::move(api_key_map); | ||
credentials_ = std::move(credentials); | ||
} | ||
|
||
ScopeConfig::ScopeConfig(const ApiKeyAuthPerScopeProto& proto) | ||
RouteConfig::RouteConfig(const ApiKeyAuthPerRouteProto& proto) | ||
: override_config_(proto.override_config()) { | ||
allowed_clients_.insert(proto.allowed_clients().begin(), proto.allowed_clients().end()); | ||
} | ||
|
||
KeySource::KeySource(absl::string_view header, absl::string_view query, absl::string_view cookie) | ||
: header_(header), query_(query), cookie_(cookie) {} | ||
KeySources::Source::Source(absl::string_view header, absl::string_view query, | ||
absl::string_view cookie) { | ||
if (!header.empty()) { | ||
source_ = Http::LowerCaseString(header); | ||
} else if (!query.empty()) { | ||
source_ = std::string(query); | ||
query_source_ = true; | ||
} else { | ||
source_ = std::string(cookie); | ||
} | ||
} | ||
|
||
KeyResult KeySource::getApiKey(const Http::RequestHeaderMap& headers, | ||
std::string& key_buffer) const { | ||
// Try to get the API key from the header first if the header key is not empty. | ||
if (!header_.get().empty()) { | ||
if (const auto auth_header = headers.get(header_); !auth_header.empty()) { | ||
if (auth_header.size() > 1) { | ||
return {{}, true}; | ||
} | ||
KeySources::KeySources(const KeySourcesProto& proto_config) { | ||
key_sources_.reserve(proto_config.entries_size()); | ||
for (const auto& source : proto_config.entries()) { | ||
key_sources_.emplace_back(source.header(), source.query(), source.cookie()); | ||
} | ||
} | ||
|
||
absl::string_view auth_header_view = auth_header[0]->value().getStringView(); | ||
if (absl::StartsWith(auth_header_view, "Bearer ")) { | ||
auth_header_view = auth_header_view.substr(7); | ||
absl::string_view KeySources::Source::getKey(const Http::RequestHeaderMap& headers, | ||
std::string& buffer) const { | ||
if (absl::holds_alternative<Http::LowerCaseString>(source_)) { | ||
if (const auto header = headers.get(absl::get<Http::LowerCaseString>(source_)); | ||
!header.empty()) { | ||
absl::string_view header_view = header[0]->value().getStringView(); | ||
if (absl::StartsWith(header_view, "Bearer ")) { | ||
header_view = header_view.substr(7); | ||
} | ||
return {auth_header_view}; | ||
std::cout << "header_view: " << header_view << std::endl; | ||
wbpcode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return header_view; | ||
} | ||
} | ||
|
||
// If the API key is not found in the header, try to get it from the query parameter | ||
// if the query key is not empty. | ||
if (!query_.empty()) { | ||
auto query_params = | ||
} else if (query_source_) { | ||
auto params = | ||
Http::Utility::QueryParamsMulti::parseAndDecodeQueryString(headers.getPathValue()); | ||
if (auto iter = query_params.data().find(query_); iter != query_params.data().end()) { | ||
if (iter->second.size() > 1) { | ||
return {{}, true}; | ||
} | ||
if (auto iter = params.data().find(absl::get<std::string>(source_)); | ||
iter != params.data().end()) { | ||
if (!iter->second.empty()) { | ||
key_buffer = std::move(iter->second[0]); | ||
return {absl::string_view{key_buffer}}; | ||
buffer = std::move(iter->second[0]); | ||
return buffer; | ||
} | ||
} | ||
} else { | ||
buffer = Http::Utility::parseCookieValue(headers, absl::get<std::string>(source_)); | ||
return buffer; | ||
} | ||
|
||
// If the API key is not found in the header and query parameter, try to get it from the | ||
// cookie if the cookie key is not empty. | ||
if (!cookie_.empty()) { | ||
key_buffer = Http::Utility::parseCookieValue(headers, cookie_); | ||
return {absl::string_view{key_buffer}}; | ||
} | ||
return {}; | ||
} | ||
|
||
absl::string_view KeySources::getKey(const Http::RequestHeaderMap& headers, | ||
std::string& buffer) const { | ||
for (const auto& source : key_sources_) { | ||
if (auto key = source.getKey(headers, buffer); !key.empty()) { | ||
return key; | ||
} | ||
} | ||
return {}; | ||
} | ||
|
||
|
@@ -92,46 +104,43 @@ FilterConfig::FilterConfig(const ApiKeyAuthProto& proto_config, Stats::Scope& sc | |
ApiKeyAuthFilter::ApiKeyAuthFilter(FilterConfigSharedPtr config) : config_(std::move(config)) {} | ||
|
||
Http::FilterHeadersStatus ApiKeyAuthFilter::decodeHeaders(Http::RequestHeaderMap& headers, bool) { | ||
const ScopeConfig* override_config = | ||
Http::Utility::resolveMostSpecificPerFilterConfig<ScopeConfig>(decoder_callbacks_); | ||
const RouteConfig* override_config = | ||
Http::Utility::resolveMostSpecificPerFilterConfig<RouteConfig>(decoder_callbacks_); | ||
|
||
OptRef<const ApiKeyMap> api_key_map = config_->apiKeyMap(); | ||
OptRef<const KeySource> key_source = config_->keySource(); | ||
OptRef<const Credentials> credentials = config_->credentials(); | ||
OptRef<const KeySources> key_sources = config_->keySources(); | ||
|
||
// If there is an override config, then try to override the API key map and key source. | ||
if (override_config != nullptr) { | ||
OptRef<const ApiKeyMap> override_api_key_map = override_config->apiKeyMap(); | ||
if (override_api_key_map.has_value()) { | ||
api_key_map = override_api_key_map; | ||
OptRef<const Credentials> override_credentials = override_config->credentials(); | ||
if (override_credentials.has_value()) { | ||
credentials = override_credentials; | ||
} | ||
|
||
OptRef<const KeySource> override_key_source = override_config->keySource(); | ||
if (override_key_source.has_value()) { | ||
key_source = override_key_source; | ||
OptRef<const KeySources> override_key_sources = override_config->keySources(); | ||
if (override_key_sources.has_value()) { | ||
wbpcode marked this conversation as resolved.
Show resolved
Hide resolved
|
||
key_sources = override_key_sources; | ||
} | ||
} | ||
|
||
if (!key_source.has_value()) { | ||
if (!key_sources.has_value()) { | ||
return onDenied(Http::Code::Unauthorized, "Client authentication failed.", | ||
"missing_key_source"); | ||
"missing_key_sources"); | ||
} | ||
if (!api_key_map.has_value()) { | ||
if (!credentials.has_value()) { | ||
return onDenied(Http::Code::Unauthorized, "Client authentication failed.", | ||
"missing_credentials"); | ||
} | ||
|
||
std::string key_buffer; | ||
const KeyResult key_result = key_source->getApiKey(headers, key_buffer); | ||
absl::string_view key_result = key_sources->getKey(headers, key_buffer); | ||
|
||
if (key_result.multiple_keys_error) { | ||
return onDenied(Http::Code::Unauthorized, "Client authentication failed.", "multiple_api_key"); | ||
} | ||
if (key_result.key_string_view.empty()) { | ||
if (key_result.empty()) { | ||
return onDenied(Http::Code::Unauthorized, "Client authentication failed.", "missing_api_key"); | ||
} | ||
|
||
const auto credential = api_key_map->find(key_result.key_string_view); | ||
if (credential == api_key_map->end()) { | ||
const auto credential = credentials->find(key_result); | ||
if (credential == credentials->end()) { | ||
return onDenied(Http::Code::Unauthorized, "Client authentication failed.", "unkonwn_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.
Where is the
authentication_header
config field defined?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.
sorry, this should be updated.