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

Credential injection dual filter support #35194

Closed
Closed
3 changes: 3 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,9 @@ new_features:
QUIC stream reset error code will be added to transport failure reason.
This behavior can be reverted by setting the runtime flag ``envoy.reloadable_features.report_stream_reset_error_code``
to ``false``.
- area: filters
change: |
Update ``credential_injector`` filter to support use as an upstream filter.

deprecated:
- area: tracing
Expand Down
5 changes: 5 additions & 0 deletions envoy/server/factory_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ class ServerFactoryContext : public virtual CommonFactoryContext {
*/
virtual Init::Manager& initManager() PURE;

/**
* @return TransportSocketFactoryContext which lifetime is no shorter than the server.
*/
virtual TransportSocketFactoryContext& getTransportSocketFactoryContext() PURE;

/**
* @return DrainManager& the server-wide drain manager.
*/
Expand Down
4 changes: 3 additions & 1 deletion source/common/upstream/cluster_manager_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1045,8 +1045,10 @@ void ClusterManagerImpl::updateClusterCounts() {
}

ThreadLocalCluster* ClusterManagerImpl::getThreadLocalCluster(absl::string_view cluster) {
if (!tls_.get()) {
return nullptr;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

during upstream filter initialization, the filter tries to do a warmup and perform a request. this object is not initialized at that moment. Is there a better solution for this?

ThreadLocalClusterManagerImpl& cluster_manager = *tls_;

auto entry = cluster_manager.thread_local_clusters_.find(cluster);
if (entry != cluster_manager.thread_local_clusters_.end()) {
return entry->second.get();
Expand Down
2 changes: 2 additions & 0 deletions source/extensions/extensions_metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,10 @@ envoy.filters.http.cors:
envoy.filters.http.credential_injector:
categories:
- envoy.filters.http
- envoy.filters.http.upstream
security_posture: unknown
status: alpha
status_upstream: alpha
type_urls:
- envoy.extensions.filters.http.credential_injector.v3.CredentialInjector
envoy.filters.http.csrf:
Expand Down
19 changes: 13 additions & 6 deletions source/extensions/filters/http/credential_injector/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ absl::StatusOr<Envoy::Http::FilterFactoryCb>
CredentialInjectorFilterFactory::createFilterFactoryFromProtoTyped(
const envoy::extensions::filters::http::credential_injector::v3::CredentialInjector&
proto_config,
const std::string& stats_prefix, Server::Configuration::FactoryContext& context) {
const std::string& stats_prefix, DualInfo dual_info,
Server::Configuration::ServerFactoryContext& context) {

// Find the credential injector factory.
auto* config_factory = Envoy::Config::Utility::getFactory<NamedCredentialInjectorConfigFactory>(
Expand All @@ -31,21 +32,27 @@ CredentialInjectorFilterFactory::createFilterFactoryFromProtoTyped(
ProtobufTypes::MessagePtr message = Config::Utility::translateAnyToFactoryConfig(
proto_config.credential().typed_config(), context.messageValidationVisitor(),
*config_factory);

// In upstream filter call, prefix has cluster.cluster_0 without '.', needs to be added
std::string tmp_prefix = dual_info.is_upstream ? stats_prefix + "." : stats_prefix;
CredentialInjectorSharedPtr credential_injector =
config_factory->createCredentialInjectorFromProto(
*message, stats_prefix + "credential_injector.", context);
*message, tmp_prefix + "credential_injector.", context);

FilterConfigSharedPtr config =
std::make_shared<FilterConfig>(std::move(credential_injector), proto_config.overwrite(),
proto_config.allow_request_without_credential(),
stats_prefix + "credential_injector.", context.scope());
FilterConfigSharedPtr config = std::make_shared<FilterConfig>(
std::move(credential_injector), proto_config.overwrite(),
proto_config.allow_request_without_credential(),
dual_info.is_upstream ? "credential_injector." : stats_prefix + "credential_injector.",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is another side effect, it is called twice. without this change we get cluster.cluster_0.cluster.cluster_0credential_injector

dual_info.scope);
return [config](Envoy::Http::FilterChainFactoryCallbacks& callbacks) -> void {
callbacks.addStreamDecoderFilter(std::make_shared<CredentialInjectorFilter>(config));
};
}

REGISTER_FACTORY(CredentialInjectorFilterFactory,
Server::Configuration::NamedHttpFilterConfigFactory);
REGISTER_FACTORY(UpstreamCredentialInjectorFilterFactory,
Server::Configuration::UpstreamHttpFilterConfigFactory);

} // namespace CredentialInjector
} // namespace HttpFilters
Expand Down
10 changes: 6 additions & 4 deletions source/extensions/filters/http/credential_injector/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ namespace HttpFilters {
namespace CredentialInjector {

class CredentialInjectorFilterFactory
: public Common::ExceptionFreeFactoryBase<
: public Common::DualFactoryBase<
envoy::extensions::filters::http::credential_injector::v3::CredentialInjector> {
public:
CredentialInjectorFilterFactory()
: ExceptionFreeFactoryBase("envoy.filters.http.credential_injector") {}
CredentialInjectorFilterFactory() : DualFactoryBase("envoy.filters.http.credential_injector") {}

private:
absl::StatusOr<Http::FilterFactoryCb> createFilterFactoryFromProtoTyped(
const envoy::extensions::filters::http::credential_injector::v3::CredentialInjector& config,
const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override;
const std::string& stats_prefix, DualInfo dual_info,
Server::Configuration::ServerFactoryContext& context) override;
};

using UpstreamCredentialInjectorFilterFactory = CredentialInjectorFilterFactory;

} // namespace CredentialInjector
} // namespace HttpFilters
} // namespace Extensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class NamedCredentialInjectorConfigFactory : public Config::TypedFactory {
virtual CredentialInjectorSharedPtr
createCredentialInjectorFromProto(const Protobuf::Message& config,
const std::string& stats_prefix,
Server::Configuration::FactoryContext& context) PURE;
Server::Configuration::ServerFactoryContext& context) PURE;

std::string category() const override { return "envoy.http.injected_credentials"; }
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@ namespace Common {
template <class ConfigProto>
class CredentialInjectorFactoryBase : public NamedCredentialInjectorConfigFactory {
public:
CredentialInjectorSharedPtr
createCredentialInjectorFromProto(const Protobuf::Message& proto_config,
const std::string& stats_prefix,
Server::Configuration::FactoryContext& context) override {
CredentialInjectorSharedPtr createCredentialInjectorFromProto(
const Protobuf::Message& proto_config, const std::string& stats_prefix,
Server::Configuration::ServerFactoryContext& server_context) override {
return createCredentialInjectorFromProtoTyped(
MessageUtil::downcastAndValidate<const ConfigProto&>(proto_config,
context.messageValidationVisitor()),
stats_prefix, context);
MessageUtil::downcastAndValidate<const ConfigProto&>(
proto_config, server_context.messageValidationVisitor()),
stats_prefix, server_context);
}

ProtobufTypes::MessagePtr createEmptyConfigProto() override {
Expand All @@ -36,7 +35,7 @@ class CredentialInjectorFactoryBase : public NamedCredentialInjectorConfigFactor
private:
virtual CredentialInjectorSharedPtr
createCredentialInjectorFromProtoTyped(const ConfigProto&, const std::string&,
Server::Configuration::FactoryContext&) PURE;
Server::Configuration::ServerFactoryContext&) PURE;

const std::string name_;
};
Expand Down
10 changes: 4 additions & 6 deletions source/extensions/http/injected_credentials/generic/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,16 @@ secretsProvider(const envoy::extensions::transport_sockets::tls::v3::SdsSecretCo
Common::CredentialInjectorSharedPtr
GenericCredentialInjectorFactory::createCredentialInjectorFromProtoTyped(
const Generic& config, const std::string& /*stats_prefix*/,
Server::Configuration::FactoryContext& context) {
Server::Configuration::ServerFactoryContext& server_context) {
const auto& credential_secret = config.credential();
auto& server_context = context.serverFactoryContext();
auto& cluster_manager = server_context.clusterManager();
auto& secret_manager = cluster_manager.clusterManagerFactory().secretManager();
auto& transport_socket_factory = context.getTransportSocketFactoryContext();
auto& transport_socket_factory = server_context.getTransportSocketFactoryContext();
auto secret_provider = secretsProvider(credential_secret, secret_manager,
transport_socket_factory, context.initManager());
transport_socket_factory, server_context.initManager());

auto secret_reader = std::make_shared<const Common::SDSSecretReader>(
std::move(secret_provider), context.serverFactoryContext().threadLocal(),
server_context.api());
std::move(secret_provider), server_context.threadLocal(), server_context.api());
std::string header = config.header();
if (header.empty()) {
header = "Authorization";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ class GenericCredentialInjectorFactory : public Common::CredentialInjectorFactor
: CredentialInjectorFactoryBase("envoy.http.injected_credentials.generic") {}

private:
Common::CredentialInjectorSharedPtr
createCredentialInjectorFromProtoTyped(const Generic& config, const std::string& stats_prefix,
Server::Configuration::FactoryContext& context) override;
Common::CredentialInjectorSharedPtr createCredentialInjectorFromProtoTyped(
const Generic& config, const std::string& stats_prefix,
Server::Configuration::ServerFactoryContext& context) override;
};

DECLARE_FACTORY(GenericCredentialInjectorFactory);
Expand Down
20 changes: 9 additions & 11 deletions source/extensions/http/injected_credentials/oauth2/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ secretsProvider(const envoy::extensions::transport_sockets::tls::v3::SdsSecretCo
Common::CredentialInjectorSharedPtr
OAuth2CredentialInjectorFactory::createCredentialInjectorFromProtoTyped(
const OAuth2& config, const std::string& stats_prefix,
Server::Configuration::FactoryContext& context) {
Server::Configuration::ServerFactoryContext& context) {

switch (config.flow_type_case()) {
case envoy::extensions::http::injected_credentials::oauth2::v3::OAuth2::FlowTypeCase::
Expand All @@ -44,26 +44,24 @@ OAuth2CredentialInjectorFactory::createCredentialInjectorFromProtoTyped(
Common::CredentialInjectorSharedPtr
OAuth2CredentialInjectorFactory::createOauth2ClientCredentialInjector(
const OAuth2& proto_config, const std::string& stats_prefix,
Server::Configuration::FactoryContext& context) {
auto& cluster_manager = context.serverFactoryContext().clusterManager();
Server::Configuration::ServerFactoryContext& server_context) {
auto& cluster_manager = server_context.clusterManager();
auto& secret_manager = cluster_manager.clusterManagerFactory().secretManager();
auto& transport_socket_factory = context.getTransportSocketFactoryContext();
auto& transport_socket_factory = server_context.getTransportSocketFactoryContext();

const auto& client_secret_secret = proto_config.client_credentials().client_secret();

auto client_secret_provider = secretsProvider(client_secret_secret, secret_manager,
transport_socket_factory, context.initManager());
auto client_secret_provider = secretsProvider(
client_secret_secret, secret_manager, transport_socket_factory, server_context.initManager());
if (client_secret_provider == nullptr) {
throw EnvoyException("Invalid oauth2 client secret configuration");
}

auto secret_reader = std::make_shared<const Common::SDSSecretReader>(
std::move(client_secret_provider), context.serverFactoryContext().threadLocal(),
context.serverFactoryContext().api());
std::move(client_secret_provider), server_context.threadLocal(), server_context.api());
auto token_reader = std::make_shared<const TokenProvider>(
secret_reader, context.serverFactoryContext().threadLocal(), cluster_manager, proto_config,
context.serverFactoryContext().mainThreadDispatcher(), stats_prefix,
context.serverFactoryContext().scope());
secret_reader, server_context.threadLocal(), cluster_manager, proto_config,
server_context.mainThreadDispatcher(), stats_prefix, server_context.scope());

return std::make_shared<OAuth2ClientCredentialTokenInjector>(token_reader);
}
Expand Down
8 changes: 4 additions & 4 deletions source/extensions/http/injected_credentials/oauth2/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ class OAuth2CredentialInjectorFactory : public Common::CredentialInjectorFactory
: CredentialInjectorFactoryBase("envoy.http.injected_credentials.oauth2") {}
Common::CredentialInjectorSharedPtr
createOauth2ClientCredentialInjector(const OAuth2& proto_config, const std::string& stats_prefix,
Server::Configuration::FactoryContext& context);
Common::CredentialInjectorSharedPtr
createCredentialInjectorFromProtoTyped(const OAuth2& config, const std::string& stats_prefix,
Server::Configuration::FactoryContext& context) override;
Server::Configuration::ServerFactoryContext& context);
Common::CredentialInjectorSharedPtr createCredentialInjectorFromProtoTyped(
const OAuth2& config, const std::string& stats_prefix,
Server::Configuration::ServerFactoryContext& context) override;
};

DECLARE_FACTORY(OAuth2CredentialInjectorFactory);
Expand Down
1 change: 1 addition & 0 deletions source/server/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ class ServerFactoryContextImpl : public Configuration::ServerFactoryContext,
OverloadManager& overloadManager() override { return server_.overloadManager(); }
OverloadManager& nullOverloadManager() override { return server_.nullOverloadManager(); }
bool healthCheckFailed() const override { return server_.healthCheckFailed(); }
TransportSocketFactoryContext& getTransportSocketFactoryContext() override { return *this; }

// Configuration::TransportSocketFactoryContext
ServerFactoryContext& serverFactoryContext() override { return *this; }
Expand Down
19 changes: 19 additions & 0 deletions test/common/grpc/grpc_client_integration.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ class GrpcClientIntegrationParamTest
ClientType clientType() const override { return std::get<1>(GetParam()); }
};

class UpstreamDownstreamGrpcClientIntegrationParamTest
: public BaseGrpcClientIntegrationParamTest,
public testing::TestWithParam<std::tuple<Network::Address::IpVersion, ClientType, bool>> {
public:
static std::string protocolTestParamsToString(
const ::testing::TestParamInfo<std::tuple<Network::Address::IpVersion, ClientType, bool>>&
p) {
return fmt::format("{}_{}_{}", TestUtility::ipVersionToString(std::get<0>(p.param)),
std::get<1>(p.param) == ClientType::GoogleGrpc ? "GoogleGrpc" : "EnvoyGrpc",
std::get<2>(p.param) ? "DownstreamFilter" : "UpstreamFilter");
}
Network::Address::IpVersion ipVersion() const override { return std::get<0>(GetParam()); }
ClientType clientType() const override { return std::get<1>(GetParam()); }
bool isDownstreamFilter() const { return std::get<2>(GetParam()); }
};

// TODO(kbaichoo): Revert to using GrpcClientIntegrationParamTest for all
// derived classes when deferred processing is enabled by default. It's
// parameterized by deferred processing now to avoid bit rot since the feature
Expand Down Expand Up @@ -168,6 +184,9 @@ class DeltaSotwDeferredClustersIntegrationParamTest
#define GRPC_CLIENT_INTEGRATION_PARAMS \
testing::Combine(testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), \
testing::ValuesIn(TestEnvironment::getsGrpcVersionsForTest()))
#define DOWNSTREAM_UPSTREAM_GRPC_CLIENT_INTEGRATION_PARAMS \
testing::Combine(testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), \
testing::ValuesIn(TestEnvironment::getsGrpcVersionsForTest()), testing::Bool())
#define GRPC_CLIENT_INTEGRATION_DEFERRED_PROCESSING_PARAMS \
testing::Combine(GRPC_CLIENT_INTEGRATION_PARAMS, testing::Bool())
#define DELTA_SOTW_GRPC_CLIENT_INTEGRATION_PARAMS \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace HttpFilters {
namespace CredentialInjector {
namespace {

class CredentialInjectorIntegrationTest : public HttpProtocolIntegrationTest {
class CredentialInjectorIntegrationTest : public UpstreamDownstreamIntegrationTest {
public:
void initializeFilter(const std::string& filter_config) {
TestEnvironment::writeStringToFileForTest("credential.yaml", R"EOF(
Expand All @@ -20,9 +20,15 @@ class CredentialInjectorIntegrationTest : public HttpProtocolIntegrationTest {
secret:
inline_string: "Basic base64EncodedUsernamePassword")EOF",
false);
config_helper_.prependFilter(TestEnvironment::substitute(filter_config));
config_helper_.prependFilter(TestEnvironment::substitute(filter_config),
testing_downstream_filter_);
initialize();
}
std::string getStatName(const std::string& key) {
return (testing_downstream_filter_ ? "http.config_test.credential_injector."
: "cluster.cluster_0.credential_injector.") +
key;
}
};

// CredentialInjector integration tests that should run with all protocols
Expand All @@ -31,8 +37,8 @@ class CredentialInjectorIntegrationTestAllProtocols : public CredentialInjectorI

INSTANTIATE_TEST_SUITE_P(
Protocols, CredentialInjectorIntegrationTestAllProtocols,
testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParamsWithoutHTTP3()),
HttpProtocolIntegrationTest::protocolTestParamsToString);
testing::ValuesIn(UpstreamDownstreamIntegrationTest::getTestParamsWithoutHTTP3()),
UpstreamDownstreamIntegrationTest::testParamsToString);

// Inject credential to a request without credential
TEST_P(CredentialInjectorIntegrationTestAllProtocols, InjectCredential) {
Expand Down Expand Up @@ -74,7 +80,7 @@ name: envoy.filters.http.credential_injector
ASSERT_TRUE(response->complete());
EXPECT_EQ("200", response->headers().getStatusValue());

EXPECT_EQ(1UL, test_server_->counter("http.config_test.credential_injector.injected")->value());
EXPECT_EQ(1UL, test_server_->counter(getStatName("injected"))->value());
}

// Inject credential to a request without credential
Expand Down Expand Up @@ -113,7 +119,7 @@ name: envoy.filters.http.credential_injector
ASSERT_TRUE(response->complete());
EXPECT_EQ("200", response->headers().getStatusValue());

EXPECT_EQ(1UL, test_server_->counter("http.config_test.credential_injector.injected")->value());
EXPECT_EQ(1UL, test_server_->counter(getStatName("injected"))->value());
}

// Inject credential to a request with credential, overwrite is false
Expand Down Expand Up @@ -154,8 +160,7 @@ name: envoy.filters.http.credential_injector
ASSERT_TRUE(response->complete());
EXPECT_EQ("200", response->headers().getStatusValue());

EXPECT_EQ(1UL,
test_server_->counter("http.config_test.credential_injector.already_exists")->value());
EXPECT_EQ(1UL, test_server_->counter(getStatName("already_exists"))->value());
}

// Inject credential to a request with credential, overwrite is true
Expand Down Expand Up @@ -196,7 +201,7 @@ name: envoy.filters.http.credential_injector
ASSERT_TRUE(response->complete());
EXPECT_EQ("200", response->headers().getStatusValue());

EXPECT_EQ(1UL, test_server_->counter("http.config_test.credential_injector.injected")->value());
EXPECT_EQ(1UL, test_server_->counter(getStatName("injected"))->value());
}

} // namespace
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ TEST(Config, OAuth2FlowTypeUnset) {
envoy::extensions::http::injected_credentials::oauth2::v3::OAuth2 proto_config;
proto_config.mutable_token_fetch_retry_interval()->set_seconds(1);
OAuth2CredentialInjectorFactory factory;
NiceMock<Server::Configuration::MockFactoryContext> context;
NiceMock<Server::Configuration::MockServerFactoryContext> context;

EXPECT_THROW_WITH_REGEX(
factory.createCredentialInjectorFromProtoTyped(proto_config, "stats", context),
Expand All @@ -38,7 +38,7 @@ TEST(Config, NullClientSecret) {
envoy::extensions::http::injected_credentials::oauth2::v3::OAuth2 proto_config;
TestUtility::loadFromYaml(yaml_string, proto_config);
OAuth2CredentialInjectorFactory factory;
NiceMock<Server::Configuration::MockFactoryContext> context;
NiceMock<Server::Configuration::MockServerFactoryContext> context;

EXPECT_THROW_WITH_REGEX(
factory.createOauth2ClientCredentialInjector(proto_config, "stats", context), EnvoyException,
Expand Down
Loading