-
Notifications
You must be signed in to change notification settings - Fork 305
poc: enforcing allowed features #7917
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
base: dev
Are you sure you want to change the base?
Conversation
✅ Docs preview readyThe preview is ready to be viewed. View the preview File Changes 0 new, 1 changed, 0 removed
Build ID: 55aa5335f525e79ff37f1aef URL: https://www.apollographql.com/docs/deploy-preview/55aa5335f525e79ff37f1aef |
@DMallare, please consider creating a changeset entry in |
8e1bcde
to
1a7bdb9
Compare
match feature { | ||
"apq" => AllowedFeature::APQ, | ||
"authentication" => AllowedFeature::Authentication, | ||
other => AllowedFeature::Undefined(other.into()), |
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.
Note: there is a TODO to finish this
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 are we doing with Other
here? Is the String
necessary for metrics? Or in other words, what's the expectation for features that router doesn't know about and how many of those do we expect from a typical response?
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.
I saw the String
primarily for use with logging so that, for example, if a customer tried to use a new feature with an older version of the router and didn’t see it take effect, the log message could guide them to the appropriate action - updating their router.
newer jwt - older router
If the router encounters a feature without its own variant in AllowedFeature
it will map to Other
, the router will ignore it and log at the warn level that it does not recognize it and the customer should consider updating their version of the router.
older jwt - newer router
If the jwt allowed_features
's claim does not contain the feature they want to use with their router then that feature will show up in the license report as a restricted feature and the router either won't start up or reload.
The metrics piece & how many we expect are good call outs:
Do we want any metrics around this?
My first thought is no - this variant will be created when a customer is using a feature unsupported by their version of the router
On the other hand, if we want to know what features customers are trying to use, albeit unsuccessfully, this might be something we want to record and keep on our radar to ensure we are communicating correctly to customers what min version of the router they need to use said feature(s).
How many Other
's do we expect to see?
- This is dependent on how many new features we plan to introduce and how well we document the min version of the router they need (older router - newer jwt above)
- We may also see this if a customer has an outdated jwt (older jwt - newer router above)
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 a customer tried to use a new feature with an older version of the router and didn’t see it take effect, the log message could guide them to the appropriate action - updating their router.
Are they manually opting in to allowed_features
in GraphOS UI? If not, it might be a bit confusing reading a message saying "new_feature
you're not using nor have you opted into is not available on your version of the router". We will also not be able to add a message in the router saying in which version this new feature is available, unless we literally backport to every version of the router after this change lands. So that information, that is minimum router version, will then have to live somewhere in GraphOS and make its way to the router along with the actual feature name. If they are opting into allowed_features
, then this makes a bit more sense.
If the jwt allowed_features's claim does not contain the feature they want to use with their router then that feature will show up in the license report as a restricted feature and the router either won't start up or reload.
I did not yet see the logic for this in the code here. Right now it seems there is only a warn
, and a plugin does not load (which is a nice experience - you can still use the router - but I am not sure if that's the intention). Or is that more for configuration
based entitlements? Speaking of, while I am usually a proponent of separating PRs into smallest possible units of work, it might be easier to bring this PR and #7939 together, especially when you're testing. You are going to want to test the intersection of what happens when there isn't any config for a licensed feature (i.e. default config) but the plugin still shouldn't load because there isn't a license etc.
On the other hand, if we want to know what features customers are trying to use, albeit unsuccessfully, this might be something we want to record and keep on our radar to ensure we are communicating correctly to customers what min version of the router they need to use said feature(s).
That sounds like we should have a metric recording the Other
and the current router version. This could form a helpful UI in GraphOS letting users know they should update their router and would also just be helpful information for us to know.
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.
Are they manually opting in to allowed_features in GraphOS UI?
We may have this eventually but for now we don't the ability to do th is in GraphOS UI. I am thinking we can add the warning to the jwt.
I did not yet see the logic for this in the code here
My apologies, as you had mentioned it is in #7939. I brought the contents of #7939 to this PR to make it easier to understand everything as a whole
That sounds like we should have a metric recording the Other and the current router version.
This sounds very interesting! I will think about how we can add this and what it would look like in terms of UI for our users
/// Allowed features for a License, representing what's available to a particular pricing tier | ||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Hash)] | ||
pub enum AllowedFeature { | ||
/// Automated persistent queries |
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.
Note: there is a TODO to finish the documentation for these
1a7bdb9
to
8bb665b
Compare
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.
I had a few small comments, but overall I think the direction is good. We will definitely need a bunch of tests, especially integration tests, for this though.
apollo-router/src/router_factory.rs
Outdated
if let Some(allowed_features) = $license.get_allowed_features() { | ||
if allowed_features.contains(&AllowedFeature::from($name)) { | ||
add_plugin!(name.to_string(), factory, plugin_config, full_config); | ||
} else { | ||
tracing::warn!( | ||
"{name} is a restricted feature that requires a license" | ||
); | ||
} |
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.
When testing, we should check the behaviour for Other
(Undefined
) feature. It might be strange to send a warn for a feature that very clearly doesn't exist in the router.
match feature { | ||
"apq" => AllowedFeature::APQ, | ||
"authentication" => AllowedFeature::Authentication, | ||
other => AllowedFeature::Undefined(other.into()), |
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 are we doing with Other
here? Is the String
necessary for metrics? Or in other words, what's the expectation for features that router doesn't know about and how many of those do we expect from a typical response?
/// Allowed features for a License, representing what's available to a particular pricing tier | ||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Hash)] | ||
pub enum AllowedFeature { |
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.
Parts of telemetry are a license-only feature (custom instruments). How is this going to be handled by the the allowlist?
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.
Thanks for calling this out, I was not aware of this. One idea is to add another item to this enum. I will explore this more to understand what parts of telemetry are license-only
@DMallare @aaronArinder I'll let you all continue working on this for the time being, but give me a shout once this is out of draft and ready for review again. |
/// Set of allowed features. These may not exist in a License; if not, all features are enabled | ||
pub(crate) allowed_features: Option<Vec<AllowedFeature>>, |
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.
probs worth explaining why all features are enabled if there's no allowedFeatures claim (ie, middle versus end state of this work)
if let Some(allowed_features) = $license.get_allowed_features() { | ||
if allowed_features.contains(&AllowedFeature::from($name)) { | ||
add_plugin!(name.to_string(), factory, plugin_config, full_config); | ||
} else { | ||
tracing::warn!( | ||
"{name} plugin is not registered, {name} is a restricted feature that requires a license" | ||
); | ||
} |
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 will come out in testing, but the allowed features are probably named differently than their plugins (some get prefixed with a apollo.
for example); so, we'll want to make sure there's some mapping between the two that's testable
// If the license has no allowed_features claim, we're using a pricing plan | ||
// that should have the plugin enabled regardless | ||
} else { | ||
add_plugin!(name.to_string(), factory, plugin_config, full_config); |
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.
we'll also want to spend some time (probably through tests) in figuring out which plugins are potentially added in other ways; the telemetry plugin gets some special handling, but is that the only one?
macro_rules! add_mandatory_apollo_plugin { | ||
($name: literal) => { | ||
($name: literal, $license: expr) => { | ||
add_apollo_plugin!( | ||
$name, | ||
Some( | ||
apollo_plugins_config | ||
.remove($name) | ||
.unwrap_or(Value::Object(Map::new())) | ||
) | ||
), | ||
$license | ||
); | ||
}; | ||
} |
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.
I'm starting to worry that I led you down the wrong path with this approach; mandatory plugins should probably be mandatory without the risk of not being added if they're not in the allowed features list. Part of the trouble is that the macros are fairly coupled. Not sure how you want to tackle it, but the main requirement is that mandatory plugins always be added and the main problem with our current approach is that all plugins are gated on being in the allowed features list but certain plugins aren't going to be in that list (like csrf
)
Checklist
Complete the checklist (and note appropriate exceptions) before the PR is marked ready-for-review.
Exceptions
Note any exceptions here
Notes
Footnotes
It may be appropriate to bring upcoming changes to the attention of other (impacted) groups. Please endeavour to do this before seeking PR approval. The mechanism for doing this will vary considerably, so use your judgement as to how and when to do this. ↩
Configuration is an important part of many changes. Where applicable please try to document configuration examples. ↩
A lot of (if not most) features benefit from built-in observability and
debug
-level logs. Please read this guidance on metrics best-practices. ↩Tick whichever testing boxes are applicable. If you are adding Manual Tests, please document the manual testing (extensively) in the Exceptions. ↩