Skip to content

feat(licensing): quota enforcement, client rejection and trial license limits#71

Open
philippfalk wants to merge 6 commits into
mainfrom
falk/license-enforcement
Open

feat(licensing): quota enforcement, client rejection and trial license limits#71
philippfalk wants to merge 6 commits into
mainfrom
falk/license-enforcement

Conversation

@philippfalk
Copy link
Copy Markdown
Member

This PR does three separate things related to licensing:

  1. Slightly reworks the quota enforcement mechanism to detect when the feature is not licensed and to more dynamically react to system state changes (for details see commit message).
  2. Disallows client mounts when the system doesn't have a valid license and the free client mount limit is reached
  3. Allows only a single trial license per system

Tagging @iamjoemccormick for reviewing licensing related semantics and @rustybee42 for reviewing the code flow related changes around quotas and licensing.

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.
@philippfalk philippfalk requested a review from a team as a code owner May 19, 2026 17:28
Comment thread mgmtd/src/bee_msg/common.rs Fixed
Comment thread mgmtd/src/bee_msg/common.rs Fixed
Comment thread mgmtd/src/db/node.rs Outdated
Comment thread mgmtd/src/bee_msg/register_node.rs Outdated
Comment thread mgmtd/src/bee_msg/register_node.rs Outdated
Comment thread mgmtd/src/bee_msg/common.rs Outdated
Comment thread mgmtd/src/bee_msg/common.rs Outdated
Comment thread mgmtd/src/grpc/get_license.rs Outdated
Comment thread mgmtd/src/lib.rs Outdated
Comment thread mgmtd/src/quota.rs Outdated
Comment thread mgmtd/src/quota.rs Outdated
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.
@philippfalk philippfalk requested a review from rustybee42 May 22, 2026 12:25
Comment thread mgmtd/src/bee_msg/common.rs Dismissed
Copy link
Copy Markdown
Collaborator

@rustybee42 rustybee42 left a comment

Choose a reason for hiding this comment

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

Found another couple of tiny and a slightly bigger structural issue

Comment thread mgmtd/src/db/config.rs

// Config entries that should not be changed after initially set. Note that this only controls the
// functions below, the database entries could still be changed by manual query
const IMMUTABLE: &[Config] = &[Config::FsUuid, Config::FsInitDateSecs];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

issue: Should probably add the new field here to prevent it from being set more than once. Even if the application logic prevents that, just for completeness.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point. Done.

Comment thread mgmtd/src/license.rs
d.r#type() == CertType::Trial
&& prev_trial_serial.is_some_and(|s| serial != s)
}) {
library.init_cert_store();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

question: Why is this called again here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

To clear the cert store. The license we loaded might be valid, so we have to actively clear it if we reject applying it.

Comment thread mgmtd/src/lib.rs
Comment on lines +139 to +140
"Initializing licensing library failed. \
Licensed features will be unavailable: {err}"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nitpick: Potentially incorrect message.

Should be rather like "Loading and verifying license failed"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, I thought about that. But it also doesn't really matter to the user. I intended "Initializing library" to mean the entire process of initialization including loading and verifying a license file. But I have changed it. There is another log before on warning level when the license library can not be loaded.

Comment thread mgmtd/src/quota.rs
/// Fetches quota information for all storage targets and updates the quota usage database
pub(crate) async fn fetch_and_update(app: &impl App) -> Result<()> {
if app.verify_licensed_feature(LicensedFeature::Quota).is_err() {
log::info!("Quota is enabled but feature not licensed. Skipping quota collection");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

suggestion: This and the other log below should probably rather be warnings.

This is usually unexpected and the user should probably know about this with the default log level (warn).

Comment thread mgmtd/src/timer.rs
log::debug!("Running quota update");

match update_and_distribute(&app).await {
match fetch_and_update(&app).await {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

suggestion: Replace both with

        if let Err(err) = fetch_and_update(&app).await {
            log::error!("Updating quota failed: {err:#}")
        }
        if let Err(err) = distribute_exceeded(&app).await {
            log::error!("Distributing exceeded quota failed: {err:#}")
        }

fn authenticate_connection(&mut self);
fn addr(&self) -> SocketAddr;
fn msg_id(&self) -> MsgId;
fn msg_compat_feature_flags(&self) -> u8;
Copy link
Copy Markdown
Collaborator

@rustybee42 rustybee42 May 26, 2026

Choose a reason for hiding this comment

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

issue: Instead of adding access to all the header flags separately, we should provide a borrow to the whole header at this point, e.g. replacing msg_id(&self) -> u8 with header(&self) -> &Header that can then be accessed as a whole (including the compat feature flag) instead.

The impl structs already contain the borrowed header which can just be returned, the test impl can just instantiate a default dummy header which it borrows from.

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.

3 participants