feat(licensing): quota enforcement, client rejection and trial license limits#71
feat(licensing): quota enforcement, client rejection and trial license limits#71philippfalk wants to merge 6 commits into
Conversation
Before this commit, the quota retrieval and distribution of exceeded IDs would be disabled on mgmtd startup if the feature wasn't licensed. This lead to a couple of issues with enforcement consistency: * When a license expired while mgmtd was running, enforcement would continue to work but only until mgmtd restarts * When mgmtd restarted, collection and distribution of exceeded quotas would be disabled, leading to other nodes enforcing based on an old state * When other nodes restarted, they would still be able to initially fetch outdated quota states and enforce based on them. This commit fixes all of the above by not allowing intial downloads of quota state if the feature is not licensed and not entirely disabling the quota distribution mechanism. If the quota feature is not licensed nodes quota collection from the nodes will be disabled for efficiency reasons, but nodes will still receive exceeded quota updates to allow for online changes in license state. These updates will not contain any information about exceeded IDs and will simply clear out the state on the nodes so quotas will no longer be enforced.
| /// Counts the number of currently registered distinct clients. | ||
| /// | ||
| /// # Return value | ||
| /// Returns the number of currently registered distinct clients if successful. | ||
| pub(crate) fn count_clients(tx: &Transaction) -> Result<u32> { | ||
| tx.query_row( | ||
| sql!("SELECT COUNT(DISTINCT node_uid) FROM nodes WHERE node_type = ?1"), | ||
| params![NodeType::Client.sql_variant()], | ||
| |row| row.get(0), | ||
| ) | ||
| .map_err(|e| anyhow!(e)) | ||
| } | ||
|
|
There was a problem hiding this comment.
issue: Avoid adding new functions here (= in the "db layer"), just use sql directly at the callsite.
The db abstraction is mostly unnecessary and can be considered deprecated.
There was a problem hiding this comment.
Ok, it's only called from one place anyway. I "inlined" it.
| fail_on_pre_shutdown(app)?; | ||
|
|
||
| let node_id = update_node(self, app).await?; | ||
| let reject = (_req.msg_compat_feature_flags() |
There was a problem hiding this comment.
nitpick: since _req is no longer unsed, it should be renamed to req
There was a problem hiding this comment.
Makes sense. I wonder why the compiler doesn't warn in this direction. Fixed
| use common::update_node; | ||
| use shared::bee_msg::node::*; | ||
|
|
||
| const REGISTERNODEMSG_COMPATFLAG_CLIENT_SUPPORTS_REGREJ: u8 = 1; |
There was a problem hiding this comment.
nitpick: Unnecessary long variable name.\
Even if argued to name it same as in C (which I disagree with), at least the first part (REGISTERNODEMSG_) should be dropped, since the only reason to do that in C is it's missing scope. We don't have this problem here, it's already scoped.
There was a problem hiding this comment.
This was mostly done for symmetry with the client but you are right, in Rust, this is already scoped. I dropped the prefix.
| let registration_disable = app.static_info().user_config.registration_disable; | ||
|
|
||
| let licensed_clients: Option<u32> = match app.get_license_cert_data() { | ||
| Ok(r) => match r.result { |
There was a problem hiding this comment.
issue: match on r.result() which directly returns the VerifyResult
Then no match guards and as i32 below are needed
There was a problem hiding this comment.
Makes a lot of sense. Sorry, I forgot about this pattern again. Fixed.
| }, | ||
| Err(e) => { | ||
| log::error!( | ||
| "Unexpected error during license verification, limiting number of clients to {NUM_CLIENTS}: {e:#}", |
There was a problem hiding this comment.
issue: This will spam the log with errors if the licensing library is not loaded. Not sure if that is intended.
Which also bring me to the question if it shouldn't be made mandatory and deny startup if it isn't there. And then don't do extra error handling here but just fail using ? (because then this would only ever fail in truly unexpected circumstances - the library not being loaded is not really unexpected).
There was a problem hiding this comment.
I think the important part is that we log client rejections, so users know why their clients don't mount. That is done further down in the code, so I downgraded this log to debug level. The calculation now only runs when clients register, because we don't need the information for other node registrations.
| if app | ||
| .get_license_cert_data()? | ||
| .data | ||
| .is_some_and(|d| d.r#type == CertType::Trial.into()) |
There was a problem hiding this comment.
nitpick: Again, use d.r#type() to get the enum instead of converting the enum to i32.
| // Load the licensing library | ||
| let license = if !info.user_config.license_disable { | ||
| // SAFETY: | ||
| // There is no way to verify that the user loaded dynamic library matches the | ||
| // requirements of LicenseVerifier. After all, users can load anything they | ||
| // want. Therefore, this is just not safe to do from the Rust compilers | ||
| // perspective and loading anything with non-matching fp signatures or not | ||
| // behaving as expected will lead to undefined behavior. | ||
| let license = unsafe { LicenseVerifier::with_lib(&info.user_config.license_lib_file) }; |
There was a problem hiding this comment.
issue: Loading the library should stay where it was to avoid having the whole management depend on it being present on the given path.
I intentionally loaded the library outside of here (together with config file parsing and other environment dependent stuff) so the "inners" do not depend on file system paths or external librariers being present and stay kind of "pure". This allows for easy integration testing the whole management using cargo test and just passing the test configuration to start(). It's currently not done, but is definitely planned at some point.
Loading the library from here (instead of passing it in from outside where it can omitted or mocked if needed) removes that independence.
There was a problem hiding this comment.
I moved the library loading back to where it was. The certificate loading and verification is still here, though, because we need access to the database.
There was a problem hiding this comment.
nitpick: This is not relevant anymore (as the distribute function isn't called from inside the tested function anymore), so should be removed.
|
|
||
| super::update_and_distribute(&app).await.unwrap(); | ||
| super::fetch_and_update(&app).await.unwrap(); | ||
| super::distribute_exceeded(&app).await.unwrap(); |
There was a problem hiding this comment.
issue: This is not part of the test, it only tests the fetching and updating.
Thus it should not be called (same below).
There was a problem hiding this comment.
Yep, I overlooked that. I removed the calls from that test.
Drop the somewhat misleading "Internal" prefix for errors during license verification. These errors are usually caused by invalid or non-existing license files, which isn't an "internal" problem and requires user action.
This PR does three separate things related to licensing:
Tagging @iamjoemccormick for reviewing licensing related semantics and @rustybee42 for reviewing the code flow related changes around quotas and licensing.