RBAC support for policy dark launch#3554
Conversation
There was a problem hiding this comment.
updated to use enum.
There was a problem hiding this comment.
IIRC per the C++ style guide, this should look like: ..., /*is_dark_launch=*/ true)) {
Alternatively, use the enum you added in the protos instead so it's explicit.
There was a problem hiding this comment.
Can we add a test for triggering the dark path?
There was a problem hiding this comment.
added in rbac_filter_test.cc
There was a problem hiding this comment.
use const std::vector& to avoid data copy
There was a problem hiding this comment.
per c++ style guide, the variable name should be is_dark_launch
There was a problem hiding this comment.
updated to use enum.
diemtvu
left a comment
There was a problem hiding this comment.
High level question: can we dark launch from rbac-disable to rbac-enable+rules with this? I guess you can simulate rbac-disable with a dummy rule (i.e action = DENY with no policies), but I wonder if there could be a cleaner way?
|
@diemtvu, another option I could think: |
|
From what I see, I think we can remove this flag ( |
There was a problem hiding this comment.
Probably do this only when there is actually 'darklaunch' policies, otherwise it could be confusing.
There was a problem hiding this comment.
I think probably it's fine to log what user expected to see if they switch the mode ?
There was a problem hiding this comment.
What mode? This means users will see counter for permissive_allowed all the time, even they do not provide any permissive rules. It seems confusing to me. Anyway, it's just my opinion.
There was a problem hiding this comment.
updated to only evaluate/log only when permissive policies is set
|
@quanjielin @diemtvu "engine_disabled_" is used by per-route RBAC config. I think if we leave the RBAC filter spec empty, the RBAC evaluation logic will not kick in. That is basically the RBAC disabled scenario you are thinking about. @rodaine Correct me if I am wrong. |
|
@liminw, agree, just did another commit which includes
Keep engine_disabled_ for per-route config, probably it also could be removed to re-use logic above from RBACPerRoute.RBAC ? |
There was a problem hiding this comment.
This could also be removed?
There was a problem hiding this comment.
as mentioned in above comment, keep this for now because of per-route config; it could be removed if we could decide disable from RBACPerRoute.RBAC field(which this PR uses in other places). I'm not familiar about the RBACPerRoute use case, wait @rodaine to comment whether it's safe to remove disable field from RBACPerRoute proto.
There was a problem hiding this comment.
I mean the if statement.
There was a problem hiding this comment.
oh I misunderstand, done.
55deaa0 to
603d35c
Compare
|
Quick note that we will need docs on this feature in the main filter config docs / arch overview. I think it's definitely worth calling out. I don't think it's necessary to release note it as we just added the main filter. I haven't looked at the code yet, but LMK when the first round of reviews is done and I can take a look. Also note that DCO will need to be fixed. Feel free to keep reviewing and then when you are all satisfied squash/rebase force push to fix DCO and then I will review. Thanks! |
There was a problem hiding this comment.
as config_ is const, this shouldn't be here but in cluster. You can have policies_ and permissive_policies_ as a member. Creating PolicyMatcher is inefficient.
There was a problem hiding this comment.
the if and else is pretty similar, extract to a function.
There was a problem hiding this comment.
seems no extra function needed after moving code around :)
d8b2b02 to
6f312b9
Compare
There was a problem hiding this comment.
What mode? This means users will see counter for permissive_allowed all the time, even they do not provide any permissive rules. It seems confusing to me. Anyway, it's just my opinion.
| const Envoy::Http::HeaderMap& headers) const { | ||
| const Envoy::Http::HeaderMap& headers, | ||
| EnforcementMode mode) const { | ||
| if (engine_disabled_) { |
There was a problem hiding this comment.
I'm still don't understand the purpose of this boolean. Can we just use the logic with the next two if you've just added?
| config.rbac.v2alpha.RBAC rules = 1 [(validate.rules).message.required = true]; | ||
| // Specify the enforcement mode RBAC rules to be applied globally. | ||
| // Disable the filter for enforcement mode if rules isn't set. | ||
| config.rbac.v2alpha.RBAC rules = 1; |
There was a problem hiding this comment.
Before we remove "[(validate.rules).message.required = true]", @rodaine can you please review this part? I think you had it there on purpose.
For RBAC disabled case, we can just leave the entire RBAC filter spec empty. RBAC filter will not be invoked in that case.
There was a problem hiding this comment.
See my other comment on the removal of disabled
There was a problem hiding this comment.
as just mentioned in another comment, this field could be inferred from whether rules is set(same as logic used in other places in this PR), need @rodaine confirm though.
There was a problem hiding this comment.
This was originally added as an explicit indicator that the filter is disabled for the vhost/route. Likewise, making it by-convention (ie, the config.rbac.v2alpha.RBAC being unset in the above config) permits the pathological configuration of RBAC where the global filter has no policies defined at all.
There was a problem hiding this comment.
Can we just keep rules = 1? Otherwise, this would be a breaking change to anyone already consuming them. I understand the desire to clearly indicate that these are the actually enforced rules, but I think that can be accomplished in the comments.
There was a problem hiding this comment.
Can we change the last clause to: ...it is used to test how a new set of policies work before rolling out to production.?
There was a problem hiding this comment.
For clarity, please drop the by the filter enforcement mode phrase from allowed/denied, and change the shadow ones to that would be allowed/denied access by the filter's shadow rules.
There was a problem hiding this comment.
The passed in config cannot be nil (due to the reference in the engine's constructor), and the RBAC proto has validation to prevent it being set with no policies. We're just bypassing that by returning the zero value in the route specific config's constructor. The engine does not need to know about enabled/disabled as that's a function of the filter, though. This should be encoded in the filter's config logic, not the engine impl, IMO.
There was a problem hiding this comment.
can remove the raw string markers: "{}"
There was a problem hiding this comment.
seems {} is needed(tried to remove it then test timeout..)
There was a problem hiding this comment.
sorry i meant replace R"EOF({})EOF" with just "{}"
cfaf034 to
4cc9c0a
Compare
There was a problem hiding this comment.
please change back to 1 (and remove the reserved).
There was a problem hiding this comment.
remove allowed/ (sorry wasn't clearer!)
331fec8 to
1765bc7
Compare
| const std::string& stats_prefix, Stats::Scope& scope); | ||
|
|
||
| RoleBasedAccessControlFilterStats& stats() { return stats_; } | ||
| const envoy::config::filter::http::rbac::v2::RBAC& config() { return config_; } |
|
|
||
| bool isEnabled(EnforcementMode mode) const { | ||
| return mode == EnforcementMode::ENFORCED ? config_.has_rules() : config_.has_shadow_rules(); | ||
| } |
There was a problem hiding this comment.
can this be resolved at construction so we don't need to hold onto the RBAC config?
There was a problem hiding this comment.
use two boolean member variables instead of holding config _? we could do that that but personally I don't feel too much differences (run-time also should be quick ?)
There was a problem hiding this comment.
+1, you can make engine_ and shadow_engien_ an absl::optional
There was a problem hiding this comment.
done, looks much cleaner now, thanks !
|
|
||
| bool RoleBasedAccessControlFilterConfig::isEnabled(const Router::RouteConstSharedPtr route, | ||
| EnforcementMode mode) const { | ||
| if (!route || !route->routeEntry()) { |
There was a problem hiding this comment.
can you extract this part getRouteLocalConfig to another function? It is similar with engine above.
There was a problem hiding this comment.
not necessary now as after getting rid of this function.
|
|
||
| bool isEnabled(EnforcementMode mode) const { | ||
| return mode == EnforcementMode::ENFORCED ? config_.has_rules() : config_.has_shadow_rules(); | ||
| } |
There was a problem hiding this comment.
+1, you can make engine_ and shadow_engien_ an absl::optional
| if (!config_->isEnabled(callbacks_->route(), EnforcementMode::ENFORCED) || | ||
| config_->engine(callbacks_->route(), EnforcementMode::ENFORCED) | ||
| .allowed(*callbacks_->connection(), headers)) { | ||
| config_->stats().allowed_.inc(); |
There was a problem hiding this comment.
Can we do not increase the stats when config_->isEnabled() is false? Otherwise I'm afraid the stats is not very useful as it is increased when rbac is not enabled.
There was a problem hiding this comment.
updated the logic.
| // RBAC filter config. | ||
| // User sees the shadow rules as replacement to the enforced rules. | ||
| // For example, policy A runs in prod in enforced mode, user wants | ||
| // to test policy B; in the config, enforced_rules contains {A.mode=enforced}, |
There was a problem hiding this comment.
encorced_rules -> rules in the comment?
| RoleBasedAccessControlRouteSpecificFilterConfig::RoleBasedAccessControlRouteSpecificFilterConfig( | ||
| const envoy::config::filter::http::rbac::v2::RBACPerRoute& per_route_config) | ||
| : engine_(per_route_config.rbac().rules()), | ||
| shadow_engine_(per_route_config.rbac().shadow_rules()), config_(per_route_config.rbac()) {} |
There was a problem hiding this comment.
I suggest to use pointer (unique_ptr<Filters::Common::RBAC::RoleBasedAccessControlEngine>) for engine / shadow_engine, and create them conditionally in the constructor. Later, you then can simply check if engine_ == nullptr before running it. E.g:
RoleBasedAccessControlRouteSpecificFilterConfig::RoleBasedAccessControlRouteSpecificFilterConfig(const envoy::config::filter::http::rbac::v2::RBACPerRoute& per_route_config) config_(per_route_config.rbac()) {
if (config_.has_rules()) {
engine_.reset(new Filters::Common::RBAC::RoleBasedAccessControlEngineImpl(config_.rules());
}
}
then later in decodeHeader
if (shadow_engine_) {
if (shadow_engine_.allowed(*callbacks_->connection(), headers)) {
config_->stats().shadow_allowed_.inc();
} else {
config_->stats().shadow_denied_.inc();
}
}
if (engine_ == nullptr) {
if (engine_.allowed(*callbacks_->connection(), headers)) {
config_->stats().allowed_.inc();
return Http::FilterHeadersStatus::Continue;
} else {
config_->stats().denied_.inc();
//...
return Http::FilterHeadersStatus::StopIteration;
}
}
// engine_ == nullptr, equivalent to rbac disable. Always accept request.
return Http::FilterHeadersStatus::Continue;
}
There was a problem hiding this comment.
See my comment above, you can use absl::optional instead of a pointer, and check if they are nullopt.
| } | ||
|
|
||
| if (engine.allowed(*callbacks_->connection(), headers)) { | ||
| if (!config_->isEnabled(callbacks_->route(), EnforcementMode::ENFORCED) || |
There was a problem hiding this comment.
I would break this if to separate config_.isEnabled() == false to the different part that just return Continue (without counter) to be consistent. See example in the suggestion above.
lizan
left a comment
There was a problem hiding this comment.
Thanks. PTAL @rodaine @mattklein123
Signed-off-by: Quanjie Lin <quanlin@google.com>
lizan
left a comment
There was a problem hiding this comment.
You didn't have to squash again since DCO was OK.
mattklein123
left a comment
There was a problem hiding this comment.
LGTM, just some small comments. Cool!
| // RBAC filter config. | ||
| // User sees the shadow rules as replacement to the enforced rules. | ||
| // For example, policy A runs in prod in enforced mode, user wants | ||
| // to test policy B; in the config, rules contains {A.mode=enforced}, |
There was a problem hiding this comment.
I think this text is out of data for an old implementation? It doesn't make sense. I think you can just remove and leave to the documentation below to cover.
| // Specify the RBAC rules to be applied globally | ||
| config.rbac.v2alpha.RBAC rules = 1 [(validate.rules).message.required = true]; | ||
| // Specify the enforced RBAC rules to be applied globally. | ||
| // Filter's decision based on enforced rules has impact on user experience. |
There was a problem hiding this comment.
I would replace these two sentences with: "If absent, no enforcing RBAC policy will be applied."
| config.rbac.v2alpha.RBAC rules = 1; | ||
|
|
||
| // Specify the shadow RBAC rules to be applied globally. | ||
| // Filter's decision based on shadow rules doesn't impact on user experience, |
There was a problem hiding this comment.
Replace two sentences with: "Shadow rules are not enforced by the filter (i.e., returning a 403) but will emit stats and logs and can be used for rule testing. If absent, no shadow RBAC policy with be applied."
| // Override the global configuration of the filter with this new config. | ||
| RBAC rbac = 2 [(validate.rules).message.required = true]; | ||
| } | ||
| // Override the global configuration of the filter with this new config. |
There was a problem hiding this comment.
Add: "If absent, the global RBAC policy will be disabled for this route."
| block-list (DENY) set of policies based off properties of the connection (IPs, ports, SSL subject) | ||
| as well as the incoming request's HTTP headers. | ||
| as well as the incoming request's HTTP headers. This filter also supports policy in both enforcement | ||
| and shadow mode, shadow mode won't effect real users, it is used to test a new set of policies work |
There was a problem hiding this comment.
"it is used to test that a..."
| as well as the incoming request's HTTP headers. | ||
| as well as the incoming request's HTTP headers. This filter also supports policy in both enforcement | ||
| and shadow mode, shadow mode won't effect real users, it is used to test a new set of policies work | ||
| before rolling out to prod. |
There was a problem hiding this comment.
s/prod/production here and elsewhere.
| COUNTER(allowed) \ | ||
| COUNTER(denied) | ||
| COUNTER(denied) \ | ||
| COUNTER(shadow_allowed) \ |
| ALL_RBAC_FILTER_STATS(GENERATE_COUNTER_STRUCT) | ||
| }; | ||
|
|
||
| enum class EnforcementMode { ENFORCED, SHADOW }; |
| engine(const Router::RouteConstSharedPtr route, EnforcementMode mode) const; | ||
|
|
||
| private: | ||
| const absl::optional<Filters::Common::RBAC::RoleBasedAccessControlEngineImpl>& |
There was a problem hiding this comment.
Can this derive from RoleBasedAccessControlRouteSpecificFilterConfig? all the code is basically the same.
There was a problem hiding this comment.
Since RoleBasedAccessControlRouteSpecificFilterConfig is derived from RouteSpecificFilterConfig , I may prefer to keep RoleBasedAccessControlFilterConfig as it is.
Signed-off-by: Quanjie Lin <quanlin@google.com>
mattklein123
left a comment
There was a problem hiding this comment.
LGTM. Will defer to @rodaine for any final comments.
#3552
2nd POC for #3552(1st POC: #3548).
1st POC(#3548) adds an enforcement_mode property at policy level, the problem of this approach is it won't be able to test policy deletion/replacement scenarios; also the implementation isn't correct(evaluation uses OR_rule may returns earlier even without applying dark launch policy)
To address issues mentioned above, this PR groups policies into two independent rules(enforcement/darklaunch) in RBAC config.
Signed-off-by: Quanjie Lin quanlin@google.com