Skip to content

rbac: add metadata support to rbac filter#3638

Merged
mattklein123 merged 7 commits intoenvoyproxy:masterfrom
yangminzhu:rbac_metadata
Jun 27, 2018
Merged

rbac: add metadata support to rbac filter#3638
mattklein123 merged 7 commits intoenvoyproxy:masterfrom
yangminzhu:rbac_metadata

Conversation

@yangminzhu
Copy link
Contributor

@yangminzhu yangminzhu commented Jun 15, 2018

Signed-off-by: Yangmin Zhu ymzhu@google.com

title: rbac: add metadata support to rbac filter

Description:

  • Added a api/envoy/type/matcher/{metadata.proto, number.proto, string.proto} and namespace Envoy::Matcher for matcher
  • Added the new MetadataMatcher message to support a generic way to match a value in the Metadata
  • Modified the RBAC filter to use the MetadataMatcher and pass the metadata from the dynamicMetadata()

Note: This PR is actually different from my poc and the description in #3624. I changed to not to include a full Metadata in the RBAC policy, instead to use a more flexible way to retrieve a value from it and then do the custom match. The new way is much more simpler and gives us almost the same flexibility.

Risk Level: Low

Testing:
Added and ran unit tests.

Docs Changes:
N/A

Release Notes:
N/A

Fixes #3624

cc @lizan @liminw @diemtvu @qiwzhang @quanjielin

Signed-off-by: Yangmin Zhu <ymzhu@google.com>
@lizan lizan self-assigned this Jun 15, 2018
Copy link
Member

@lizan lizan left a comment

Choose a reason for hiding this comment

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

first pass on API level, this is in good direction


// If specified, value match will be performed based on double value. It's matched if and only
// if the target value is a double value and is equal to this field.
double number_match = 2;
Copy link
Member

Choose a reason for hiding this comment

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

floating number is (almost) impossible to do exact match, consider add a range match like envoy.type.Int64Range

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to use a DoubleMatcher.

// Examples:
//
// * *abc* will only match to string value *abc*.
string exact_match = 3;
Copy link
Member

Choose a reason for hiding this comment

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

Can we reuse StringMatch here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

// Specifies how to match a value. Only have effect on primitive value.
// For non-primitive value, it's always not matched.
oneof match_specifier {
option (validate.required) = true;
Copy link
Member

Choose a reason for hiding this comment

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

instead of listing all combination of type and its matcher here, would it be clearer to make a Matcher type (with oneof) for each type and have a oneof here?

i.e.:

message NullMatch {
}

message NumberMatch {
  oneof match {
    Int64Range range_match = 1;
    int64 exact_match = 2;
  }
}

message ValueMatch {
  oneof match {
    NullMatch null = 1;
    NumberMatch number = 2;
    StringMatch string = 3;
    ...
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that makes sense for string, number or other non primitive type, for bool I just kept it there as it's much simpler to use and also easy to compare in the code.

//
// An example use of MetadataMatcher is specifying additional metadata in envoy.filters.http.rbac to
// enforce access control based on dynamic metadata in a request.
message MetadataMatcher {
Copy link
Member

Choose a reason for hiding this comment

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

I have a general comment here about something that's been bothering me that I think we should get on top of now before it gets even worse.

We have a ton of different matching constructs in the API that at the end of the day are all basically the same. This includes and/or logic, headers, matching on various other things such as response flags, health check, etc.

I suggest we make a new namespace/folder called "matchers" and start moving all of the different matchers into that namespace. Also, I just looked, and is StringMatch used anywhere? I don't think so, so can we delete it as part of this change?

Thoughts? cc @htuch

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can start this change in this PR, (Initially only for MetadataMatcher and then move other Matchers to it in follow up PR).
As for StringMatch, I think it makes sense to make it a standalone Matcher under that new directory/namespace, at least it could be used by this MetadataMatcher.

Should I start it right in this PR? Thanks!
cc @lizan

Copy link
Member

Choose a reason for hiding this comment

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

+1 I would like to see more standardization across the entire API of these match patterns.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a new directory api/envoy/type/matchers and namespace Envoy::Matchers for the matchers. Currently it doesn't include and/or logic, headers, health check, etc. I prefer to do those in a different PR with a clear description of use case and goal.
Thanks!

Signed-off-by: Yangmin Zhu <ymzhu@google.com>
Signed-off-by: Yangmin Zhu <ymzhu@google.com>
@@ -0,0 +1,59 @@
syntax = "proto3";

package envoy.type.matchers;
Copy link
Member

Choose a reason for hiding this comment

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

envoy.type.matcher (no -s)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

string filter = 1 [(validate.rules).string.min_bytes = 1];

// Required. The multi-key path to retrieve the Value from the Struct.
repeated string path = 2 [(validate.rules).repeated .min_items = 1];
Copy link
Member

Choose a reason for hiding this comment

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

it is unclear to me how this works for array, notes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added comment, it's actually not supported and will just result a not match.

// If specified, value match will be performed based on whether the path is referring to a
// valid primitive value in the metadata. If the path is referring to a non-primitive value,
// the result is always not matched.
bool present_match = 7;
Copy link
Member

Choose a reason for hiding this comment

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

nit: 5

oneof match_pattern {
option (validate.required) = true;

// If specified, it's matched if and only if the target value is a NullValue and this field is
Copy link
Member

Choose a reason for hiding this comment

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

what is the use case of setting null_match to false? (same for below)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not too much real use case, I make it a bool to simplify the Value design and also feel it's easier to set a bool field for the user.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Signed-off-by: Yangmin Zhu <ymzhu@google.com>
@@ -0,0 +1,60 @@
syntax = "proto3";

package envoy.type.matcher;
Copy link
Member

Choose a reason for hiding this comment

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

You'll need to rename directories etc..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

oneof match_pattern {
option (validate.required) = true;

// If specified, it's matched if and only if the target value is a NullValue and this field is
Copy link
Member

Choose a reason for hiding this comment

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

string filter = 1 [(validate.rules).string.min_bytes = 1];

// Required. The multi-key path to retrieve the Value from the Struct.
// Note: Array is not supported and will result a not match discarding any specified values.
Copy link
Member

Choose a reason for hiding this comment

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

Still questionable how this will be extended to support array. Will it make more sense that each segment be oneof (key, index)? Needs more thoughts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to a message with a oneof field. We only need string key for now and the current metadataValue() also only supports string keys.
It should be easy to extend to include index, I prefer to do that when we really need to use a index or at least in a different PR.

Copy link
Member

Choose a reason for hiding this comment

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

yeah that's no problem, metadataValue take Metadata as input, so it could contain array anyway. make this PathSegment will allow us to extend this to support array without breaking backward compatibility.

Signed-off-by: Yangmin Zhu <ymzhu@google.com>
lizan
lizan previously approved these changes Jun 21, 2018
Copy link
Member

@lizan lizan left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

@yangminzhu
Copy link
Contributor Author

@mattklein123 @htuch PTAL, Thank you!

@mattklein123 mattklein123 self-assigned this Jun 22, 2018
@mattklein123
Copy link
Member

Sorry for the delay I will get to this tomorrow.

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Thanks for working on this! Some comments to get started.

// A port number that describes the destination port connecting to.
uint32 destination_port = 6 [(validate.rules).uint32.lte = 65535];

// A metadata that describes additional information about the action.
Copy link
Member

Choose a reason for hiding this comment

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

nit: s/A metadata/Metadata. Same below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

// :ref:`Metadata <envoy_api_msg_core.Metadata>`. It uses `filter` and `path` to retrieve the value
// from the Metadata and then check if it's matched to one of the specified values.
//
// An example use of MetadataMatcher is specifying additional metadata in envoy.filters.http.rbac to
Copy link
Member

Choose a reason for hiding this comment

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

nit: ref link to rbac message.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

// enforce access control based on dynamic metadata in a request.
message MetadataMatcher {
// Specifies the segment in a path to retrieve value from Metadata.
// Note: Array is not supported for now and will result a not match directly.
Copy link
Member

Choose a reason for hiding this comment

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

What does this mean exactly? Clarify?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

}
}

// Specifies the value to match. Only primitive value is supported. For non-primitive value, the
Copy link
Member

Choose a reason for hiding this comment

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

"are supported", "For non-primitive values,"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

oneof match_pattern {
option (validate.required) = true;

// If specified, it's matched if and only if the target value is a NullValue.
Copy link
Member

Choose a reason for hiding this comment

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

s/it's matched/a match occurs (same in all cases below)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

// Required. The filter name to retrieve the Struct from the Metadata.
string filter = 1 [(validate.rules).string.min_bytes = 1];

// Required. The path to retrieve the Value from the Struct.
Copy link
Member

Choose a reason for hiding this comment

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

I don't really understand what a path segment is in this context. Can you add a full formed config example at the top of this file and explain it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

bool DoubleMatcher::match(double value) const {
switch (matcher_.match_pattern_case()) {
case envoy::type::matcher::DoubleMatcher::kRange:
return matcher_.range().start() <= value && value < matcher_.range().end();
Copy link
Member

Choose a reason for hiding this comment

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

The docs don't specify whether the ranges are inclusive or exclusive, and it seems a bit odd to to inclusive at start but not at end? Any reason for this? Either way please clearly document.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did this to follow the current Int64Range, The document mentioned this in the definition of DoubleRange, also added it to DoubleMatcher.

case envoy::type::matcher::DoubleMatcher::kExact:
return matcher_.exact() == value;
default:
return false;
Copy link
Member

Choose a reason for hiding this comment

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

This is checked by schema, switch to NOT_REACHED. Same elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


private:
const envoy::type::matcher::StringMatcher matcher_;
std::regex regex_;
Copy link
Member

Choose a reason for hiding this comment

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

Why isn't this part of StringMatcher?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

StringMatcher proto has this but only as a simple string field, here I copy the parsed regex to a local variable so that it could be used directly later.


// Required. A set of values to match. The MetadataMatcher is matched if at least one value is
// matched, in other words, it's matched with OR semantics.
repeated Value values = 3 [(validate.rules).repeated .min_items = 1];
Copy link
Member

Choose a reason for hiding this comment

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

From looking at the code, IMO it's kind of strange that you force a matcher to be all one top of match. Why can't people just use multiple metadata matchers and make each match singular? Then all the normal and/or/etc. logic applies? I'm not sure we really need repeated in here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A repeated values make it easier to use when a user want to share the same common information in a metadata matcher, like the filter and path field and I think this is what we need in most cases.

For more advanced and/or/etc. logic, I feel it's better to make it out of the MetadataMatcher, just like the existing RBAC matcher behavior: each specific matcher do one simple thing to match a single value, an outer and/or wrapper matcher is used to support complex and/or logic.

Copy link
Member

Choose a reason for hiding this comment

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

In Envoy we don't optimize for human use but instead assume machine generation. I don't think there is any need to have multiple ways of doing things, so I think we can remove the repeated here unless there is something that cannot be done otherwise? Thank you!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see, thanks for the explanation, removed repeated here.

Signed-off-by: Yangmin Zhu <ymzhu@google.com>
string filter = 1 [(validate.rules).string.min_bytes = 1];

// The path to retrieve the Value from the Struct.
repeated PathSegment path = 2 [(validate.rules).repeated .min_items = 1];
Copy link
Contributor

Choose a reason for hiding this comment

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

Shall we use fieldmask? You can write paths as "a.b.c".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fieldmask have some disadvantages, from my investigations, it cannot support either list or map which means we cannot use it directly here, unless we implement it by ourselves in Envoy.

Second, the semantics would be much more complicated, it won't be a.b.c, you have to list all field names in the path, so it will be something like fields.a.struct_value.fields.b.struct_value.fields.c.string_value.

Last, Envoy already have a well implemented metadataValue() method which satisfy all our needs here, so I think no need to use the fieldmask anymore.

Signed-off-by: Yangmin Zhu <ymzhu@google.com>
@yangminzhu
Copy link
Contributor Author

@mattklein123
I had removed the repeated in values, PTAL, thank you!

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Nice!

@mattklein123 mattklein123 merged commit 9e7a3ab into envoyproxy:master Jun 27, 2018
@yangminzhu yangminzhu deleted the rbac_metadata branch June 27, 2018 21:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants