richard-scott opened a new issue, #218:
URL: https://github.com/apache/bifromq/issues/218

   ## Summary
   
   BifroMQ doesn't seem to enforce tenant-level settings. My plugin is being 
called, returning correct values, and successfully casting/returning them. 
BifroMQ accepts the values and doesn't enforce them.
   
   **Critical Finding**: All tested tenant-level settings (6 out of 6) are 
**NOT being enforced**, suggesting a systemic issue in which tenant-level 
settings from the Setting Provider plugin are not enforced by BifroMQ, even 
though the plugin is working correctly.
   
   ## Successfully Created Plugins
   
   For context, I have successfully created and migrated all 5 BifroMQ plugins 
to the Maven archetype structure:
   
   1. ✅ **Auth Provider** 
(`com.bifromq.plugin.authprovider.CustomAuthProvider`) - Fully functional
      - Handles authentication and authorisation
      - Supports both MQTT 3.1.1 and MQTT 5.0
      - Enforces ACL rules correctly
      - **Status**: Working correctly
   
   2. ✅ **Resource Throttler** 
(`com.bifromq.plugin.resourcethrottler.CustomResourceThrottler`) - Fully 
functional
      - Enforces connection limits (max_connections)
      - Enforces rate limits (messages per second)
      - Enforces bandwidth limits (bytes per second)
      - **Status**: Working correctly
   
   3. ⚠️ **Setting Provider** 
(`com.bifromq.plugin.settingprovider.CustomSettingProvider`) - **Plugin works, 
but BifroMQ doesn't enforce limits**
      - Plugin correctly implements `ISettingProvider` interface
      - Plugin returns correct values for all settings
      - The plugin is called by BifroMQ, and values are returned successfully
      - **Status**: Plugin implementation is correct, but BifroMQ does not 
enforce the provided settings (this issue)
   
   4. ✅ **Client Balancer** 
(`com.bifromq.plugin.clientbalancer.CustomClientBalancer`) - Fully functional
      - Implements custom client balancing strategies
      - **Status**: Working correctly
   
   5. ✅ **Event Collector** 
(`com.bifromq.plugin.eventcollector.CustomEventCollector`) - Fully functional
      - Collects runtime events from BifroMQ
      - Tracks active connections per tenant
      - **Status**: Working correctly
   
   **Note**: All plugins follow the Maven archetype structure with `conf/` 
directories, `PluginContext` classes, and proper `pom.xml` configuration. The 
Setting Provider plugin is the only one where we've found that BifroMQ isn't 
enforcing the tenant-level settings it provides.
   
   ## Affected Settings
   
   The following tenant-level settings are **NOT being enforced** (tested and 
confirmed):
   
   1. **MaximumQoS** - Maximum QoS level allowed
   2. **MaxTopicLength** - Maximum topic name length
   3. **MaxUserPayloadBytes** - Maximum user payload size
   4. **RetainEnabled** - Whether retain messages are allowed
   5. **WildcardSubscriptionEnabled** - Whether wildcard subscriptions are 
allowed
   6. **MaxTopicLevelLength** - Maximum length of each topic level
   
   ### Settings Not Yet Tested
   
   The following settings from the [official 
documentation](https://bifromq.apache.org/docs/plugin/setting_provider/tenantsetting/)
 have not been tested but may also not be enforced:
   
   - **SharedSubscriptionEnabled** - Whether shared subscriptions are allowed
   - **MQTT3Enabled/MQTT4Enabled/MQTT5Enabled** - Protocol version enablement
   - **MaxSessionExpirySeconds** - Maximum session expiry time
   - **SessionInboxSize** - Maximum size of session inbox
   - **MaxTopicLevels** - Maximum number of topic levels
   - **MaxTopicAlias** - Maximum number of topic aliases
   - **MaxSharedGroupMembers** - Maximum members in a shared subscription group
   - **MaxTopicFiltersPerInbox** - Maximum topic filters per inbox
   - **MsgPubPerSec** - Maximum messages published per second per connection
   - **ReceivingMaximum** - Maximum receiving messages per connection
   - **InBoundBandWidth/OutBoundBandWidth** - Maximum bandwidth limits
   - And others...
   
   **Note**: Based on the pattern observed, it appears that **most or all 
tenant-level settings may not be enforced**, even though the plugin is 
correctly providing them.
   
   ## Expected Behaviour
   
   According to [BifroMQ's official 
documentation](https://bifromq.apache.org/docs/plugin/setting_provider/tenantsetting/):
   
   - **MaximumQoS**: "Sets the maximum QoS level. Valid values: 0, 1, 2." - 
Should reject PUBLISH with QoS exceeding limit
   - **MaxTopicLength**: "Maximum total length of a topic." - Should reject 
topics exceeding this length
   - **MaxUserPayloadBytes**: "Maximum user payload size in bytes." - Should 
reject packets exceeding this size
   - **RetainEnabled**: "Enables or disables message retain feature." - Should 
reject PUBLISH with retain flag if disabled
   
   ## Actual Behaviour
   
   The Setting Provider plugin is correctly providing these values, but BifroMQ 
is not enforcing them:
   
   ### Test Results
   
   **Tenant Configuration** (`conf/tenants.json`):
   ```json
   {
     "tenants": {
       "tenant2": {
         "settings": {
           "MaxTopicLength": 128,
           "MaxPacketSizeBytes": 32768,
           "MaxQoS": 1,
           "RetainEnabled": false,
           "WildcardSubscriptionEnabled": false,
           "MaxTopicLevelLength": 10
         }
       }
     }
   }
   ```
   
   **Test Results**:
   - ❌ **MaxTopicLength**: 150-byte topic accepted (limit: 128 bytes) - Should 
be rejected
   - ❌ **MaxPacketSizeBytes**: 35000-byte packet accepted (limit: 32768 bytes) 
- Should be rejected
   - ❌ **MaxQoS**: QoS 2 publish accepted (limit: MaxQoS=1) - Should be rejected
   - ❌ **RetainEnabled**: Retain flag accepted (setting: RetainEnabled=false) - 
Should be rejected
   - ❌ **WildcardSubscriptionEnabled**: Wildcard subscription accepted 
(setting: WildcardSubscriptionEnabled=false) - Should be rejected
   - ❌ **MaxTopicLevelLength**: Topic with 17-byte level accepted (limit: 10 
bytes per level) - Should be rejected
   
   All test cases show `Publish RC: 0 (Success)`, even though they should be 
rejected.
   
   ## Evidence: Plugin is Working Correctly
   
   Enhanced logging shows the plugin is functioning correctly:
   
   ### 1. BifroMQ Calls the Plugin
   ```
   [CustomSettingProvider] provide() CALLED by BifroMQ: setting=MaximumQoS, 
tenant=tenant2
   [CustomSettingProvider] provide() CALLED by BifroMQ: setting=MaxTopicLength, 
tenant=tenant2
   [CustomSettingProvider] provide() CALLED by BifroMQ: 
setting=MaxUserPayloadBytes, tenant=tenant2
   ```
   
   ### 2. Plugin Returns Correct Values
   ```
   [CustomSettingProvider] provide() RETURNING VALUE: setting=MaximumQoS, 
tenant=tenant2, value=1 (type: Integer) - BifroMQ SHOULD USE THIS VALUE
   [CustomSettingProvider] provide() RETURNING VALUE: setting=MaxTopicLength, 
tenant=tenant2, value=128 (type: Integer) - BifroMQ SHOULD USE THIS VALUE
   [CustomSettingProvider] provide() RETURNING VALUE: 
setting=MaxUserPayloadBytes, tenant=tenant2, value=32768 (type: Integer) - 
BifroMQ SHOULD USE THIS VALUE
   ```
   
   ### 3. Plugin Successfully Returns Values
   ```
   [CustomSettingProvider] provide() SUCCESSFULLY CAST AND RETURNED: 
setting=MaximumQoS, tenant=tenant2, result=1
   [CustomSettingProvider] provide() SUCCESSFULLY CAST AND RETURNED: 
setting=MaxTopicLength, tenant=tenant2, result=128
   [CustomSettingProvider] provide() SUCCESSFULLY CAST AND RETURNED: 
setting=MaxUserPayloadBytes, tenant=tenant2, result=32768
   ```
   
   ### 4. But BifroMQ Ignores Them
   Test output shows violations are accepted:
   ```
   [✗] MaxQoS: QoS 2 was accepted (should be rejected)
       Publish RC: 0 (0 = Success, but should be rejected)
       Requested QoS: 2 (MaxQoS limit: 1)
   
   [✗] MaxTopicLevelLength: Topic with long level was accepted (should be 
rejected)
       Publish RC: 0 (0 = Success, but should be rejected)
       Topic: test/verylonglevelname/abc
       Longest Level: 'verylonglevelname' (17 bytes, limit: 10 bytes)
   ```
   
   **Pattern Observed**: All 6 tested settings show identical behaviour - 
plugin provides correct values, but BifroMQ does not enforce them.
   
   ## Configuration
   
   ### BifroMQ Version
   - Version: `4.0.0-incubating-RC3`
   
   ### Setting Provider Plugin
   - Plugin FQN: `com.bifromq.plugin.settingprovider.CustomSettingProvider`
   - Status: ✅ Enabled and loaded
   - Implementation: Correctly implements `ISettingProvider` interface
   
   ### Cache Configuration
   Configured in `docker-compose.yml`:
   ```yaml
   JVM_HEAP_OPTS: ... -Dsetting_provide_init_value=true 
-Dsetting_refresh_seconds=1 -Dsetting_expire_seconds=1 
-Dsetting_tenant_cache_limit=100
   ```
   
   ### Plugin Reload Mechanism
   - Reload interval: 60 seconds or on file modification
   - File path: `/home/bifromq/conf/tenants.json`
   - Status: ✅ Working (settings reloaded successfully)
   
   ## Steps to Reproduce
   
   1. **Configure tenant settings** in `conf/tenants.json`:
      ```json
      {
        "tenants": {
          "tenant2": {
            "settings": {
              "MaxTopicLength": 128,
              "MaxPacketSizeBytes": 32768,
              "MaxQoS": 1,
              "RetainEnabled": false,
              "WildcardSubscriptionEnabled": false,
              "MaxTopicLevelLength": 10
            }
          }
        }
      }
      ```
   
   2. **Start BifroMQ cluster** with Setting Provider plugin enabled
   
   3. **Run test suite**:
      ```bash
      python3 bin/test_tenant_settings.py \
        --host localhost \
        --port 8883 \
        --tenant tenant2 \
        --user user1 \
        --password tenant2_user1_pass123 \
        --protocol 5
      ```
   
   4. **Observe results**: All limit violations are accepted instead of rejected
   
   ## Logs
   
   ### Plugin Logs (showing correct behaviour)
   ```
   [CustomSettingProvider] provide() CALLED by BifroMQ: setting=MaximumQoS, 
tenant=tenant2
   [CustomSettingProvider] provide() RETURNING VALUE: setting=MaximumQoS, 
tenant=tenant2, value=1 (type: Integer) - BifroMQ SHOULD USE THIS VALUE
   [CustomSettingProvider] provide() SUCCESSFULLY CAST AND RETURNED: 
setting=MaximumQoS, tenant=tenant2, result=1
   ```
   
   ### Test Output (showing enforcement failure)
   ```
   [✗] MaxQoS: QoS 2 was accepted (should be rejected)
       Publish RC: 0 (0 = Success, but should be rejected)
       Requested QoS: 2 (MaxQoS limit: 1)
   ```
   
   ## Impact
   
   This issue prevents proper tenant-level resource control and security 
policies:
   
   - **Security**: Cannot enforce QoS restrictions per tenant
   - **Resource Control**: Cannot limit topic length, topic level length, or 
payload size per tenant
   - **Feature Control**: Cannot disable retain messages, wildcard 
subscriptions, or other features per tenant
   - **Multi-tenancy**: Breaks tenant isolation expectations - all tenants 
effectively use default settings
   - **Compliance**: Cannot enforce tenant-specific policies required for 
regulatory compliance
   - **Resource Management**: Cannot prevent resource exhaustion by limiting 
tenant capabilities
   
   **Severity**: **HIGH** - This affects the core functionality of tenant-level 
settings, which is a fundamental feature for multi-tenant deployments.
   
   ## References
   
   - [BifroMQ Setting Provider 
Documentation](https://bifromq.apache.org/docs/plugin/setting_provider/)
   - [Tenant-level Settings 
Reference](https://bifromq.apache.org/docs/plugin/setting_provider/tenantsetting/)
   
   ## Additional Information
   
   ### Plugin Implementation Details
   
   The plugin correctly:
   - ✅ Implements `ISettingProvider` interface
   - ✅ Returns correct values for all settings
   - ✅ Handles type casting correctly
   - ✅ Reloads settings every 60 seconds or on file modification
   - ✅ Logs all calls and return values (enhanced logging added for debugging)
   
   ### Complete Plugin Code
   
   The complete `provide()` method and related helper methods from 
`CustomSettingProvider.java`:
   
   ```java
   /**
    * Provide a setting value for a specific tenant.
    *
    * This method is called by BifroMQ's worker threads to get the current value
    * of a setting for a tenant. The method must be lightweight and 
non-blocking.
    *
    * @param <R> The return type (depends on the Setting type - Integer, 
Boolean, etc.)
    * @param setting The setting to provide a value for (from Setting enum)
    * @param tenantId The tenant identifier
    * @return The setting value (must match the Setting's expected type), or 
null to use the current/default value
    */
   @Override
   public <R> R provide(Setting setting, String tenantId) {
       // Get setting name from the Setting enum
       String settingName = setting.name();
   
       // Log every call to provide() - this proves BifroMQ is calling the 
plugin
       System.err.println(String.format("[CustomSettingProvider] provide() 
CALLED by BifroMQ: setting=%s, tenant=%s",
           settingName, tenantId));
   
       // Get value from configuration (tenant-specific or default)
       Object value = getSettingValue(settingName, tenantId);
   
       // Log the value being returned - this proves we're returning the 
correct value
       if (value != null) {
           System.err.println(String.format("[CustomSettingProvider] provide() 
RETURNING VALUE: setting=%s, tenant=%s, value=%s (type: %s) - BifroMQ SHOULD 
USE THIS VALUE",
               settingName, tenantId, value, value.getClass().getSimpleName()));
           try {
               R result = (R) value;
               System.err.println(String.format("[CustomSettingProvider] 
provide() SUCCESSFULLY CAST AND RETURNED: setting=%s, tenant=%s, result=%s",
                   settingName, tenantId, result));
               return result;
           } catch (ClassCastException e) {
               // Type mismatch - return null to use default
               System.err.println(String.format("[CustomSettingProvider] 
provide() TYPE MISMATCH: setting=%s, tenant=%s, error=%s - returning null",
                   settingName, tenantId, e.getMessage()));
               return null;
           }
       } else {
           // Log when no value is found (only for tenant2 to reduce noise)
           if ("tenant2".equals(tenantId)) {
               System.err.println(String.format("[CustomSettingProvider] 
provide() RETURNING NULL: setting=%s, tenant=%s - BifroMQ will use default",
                   settingName, tenantId));
           }
       }
   
       // Return null to use BifroMQ's default/current value
       return null;
   }
   
   /**
    * Get setting value for a tenant, checking tenant-specific settings first,
    * then defaults, then returning null to use BifroMQ's default.
    */
   private Object getSettingValue(String settingName, String tenantId) {
       loadSettingsIfNeeded();
   
       // Get the JSON field name (may differ from Setting enum name)
       String jsonFieldName = getJsonFieldName(settingName);
   
       // Check tenant-specific settings first (try "Bytes" suffix version)
       Map<String, Object> tenantSettings = tenantSettingsCache.get(tenantId);
       if (tenantSettings != null && tenantSettings.containsKey(jsonFieldName)) 
{
           return tenantSettings.get(jsonFieldName);
       }
   
       // Also check for original field name (backward compatibility)
       if (tenantSettings != null && tenantSettings.containsKey(settingName)) {
           return tenantSettings.get(settingName);
       }
   
       // Check default settings (try "Bytes" suffix version)
       if (defaultSettings.containsKey(jsonFieldName)) {
           return defaultSettings.get(jsonFieldName);
       }
   
       // Also check for original field name in defaults (backward 
compatibility)
       if (defaultSettings.containsKey(settingName)) {
           return defaultSettings.get(settingName);
       }
   
       // Return null to use BifroMQ's default
       return null;
   }
   
   /**
    * Load settings from configuration file.
    * This method is called lazily and caches the results.
    */
   private void loadSettingsIfNeeded() {
       long currentTime = System.currentTimeMillis();
       File settingsFile = new File(SETTINGS_FILE_PATH);
   
       // Check if file exists and if we need to reload
       if (!settingsFile.exists()) {
           return; // No settings file, use defaults
       }
   
       long fileModified = settingsFile.lastModified();
       boolean needsReload = (currentTime - lastReloadTime > 
RELOAD_INTERVAL_MS) ||
                            (fileModified > lastModified);
   
       if (!needsReload) {
           return; // Cache is still valid
       }
   
       try (InputStream is = new FileInputStream(settingsFile)) {
           JsonNode root = objectMapper.readTree(is);
   
           // Load default settings
           if (root.has("defaults") && root.get("defaults").has("settings")) {
               JsonNode defaults = root.get("defaults").get("settings");
               defaultSettings.clear();
               defaults.fields().forEachRemaining(entry -> {
                   String key = entry.getKey();
                   JsonNode value = entry.getValue();
                   if (!value.isNull()) {
                       defaultSettings.put(key, parseValue(value));
                   }
               });
           }
   
           // Load tenant-specific settings
           if (root.has("tenants")) {
               JsonNode tenants = root.get("tenants");
               tenants.fields().forEachRemaining(tenantEntry -> {
                   String tenantId = tenantEntry.getKey();
                   JsonNode tenantConfig = tenantEntry.getValue();
   
                   if (tenantConfig.has("settings")) {
                       JsonNode settings = tenantConfig.get("settings");
                       Map<String, Object> tenantSettings = new 
ConcurrentHashMap<>();
                       settings.fields().forEachRemaining(settingEntry -> {
                           String settingName = settingEntry.getKey();
                           JsonNode value = settingEntry.getValue();
                           if (!value.isNull()) {
                               tenantSettings.put(settingName, 
parseValue(value));
                           }
                       });
                       tenantSettingsCache.put(tenantId, tenantSettings);
                   }
               });
           }
   
           lastModified = fileModified;
           lastReloadTime = currentTime;
           
           // Log successful reload
           System.err.println(String.format("[CustomSettingProvider] Settings 
RELOADED: file=%s, tenants=%d, lastModified=%d, reloadTime=%d",
               SETTINGS_FILE_PATH, tenantSettingsCache.size(), lastModified, 
lastReloadTime));
       } catch (Exception e) {
           // Log error but don't fail - use defaults
           System.err.println("Error loading tenant settings from " + 
SETTINGS_FILE_PATH + ": " + e.getMessage());
       }
   }
   
   /**
    * Map BifroMQ Setting enum names to JSON field names.
    * Some settings have different names in the enum vs JSON:
    * - MaximumQoS (enum) -> MaxQoS (JSON)
    * - MaxUserPayloadBytes (enum) -> MaxPacketSizeBytes (JSON)
    */
   private String getJsonFieldName(String settingName) {
       // Map Setting enum names to JSON field names
       switch (settingName) {
           case "MaximumQoS":
               return "MaxQoS";  // Enum uses MaximumQoS, JSON uses MaxQoS
           case "MaxUserPayloadBytes":
               return "MaxPacketSizeBytes";  // BifroMQ enum uses 
MaxUserPayloadBytes, JSON uses MaxPacketSizeBytes
           case "MaxUserPropertySize":
               return "MaxUserPropertySizeBytes";
           case "MaxWillMessageSize":
               return "MaxWillMessageSizeBytes";
           default:
               // Other settings use the same name
               return settingName;
       }
   }
   
   /**
    * Parse JSON value to appropriate Java type.
    * Returns boxed types (Integer, Boolean, Long) as BifroMQ expects boxed 
types, not primitives.
    */
   private Object parseValue(JsonNode node) {
       if (node.isInt()) {
           return Integer.valueOf(node.asInt());  // Return boxed Integer, not 
primitive int
       } else if (node.isBoolean()) {
           return Boolean.valueOf(node.asBoolean());  // Return boxed Boolean, 
not primitive boolean
       } else if (node.isLong()) {
           return Long.valueOf(node.asLong());  // Return boxed Long, not 
primitive long
       } else if (node.isTextual()) {
           return node.asText();
       }
       return null;
   }
   ```
   
   **Key Implementation Details**:
   - The `provide()` method is called by BifroMQ's worker threads
   - It loads settings from `tenants.json` with 60-second reload interval
   - It checks tenant-specific settings first, then defaults, then returns null 
for BifroMQ defaults
   - It handles type mapping between BifroMQ enum names and JSON field names
   - It returns boxed types (Integer, Boolean, Long) as required by BifroMQ
   - Enhanced logging proves the method is called and returns correct values
   
   ### BifroMQ Behaviour
   
   BifroMQ:
   - ✅ Calls the plugin correctly
   - ✅ Receives correct values from the plugin
   - ❌ Does NOT enforce the provided limits
   - ❌ Accepts operations that violate the limits
   
   ### Possible Root Causes
   
   1. **Cache Issue**: BifroMQ may be using cached default values instead of 
plugin-provided values
   2. **Timing Issue**: Settings may be checked at connection time, but not 
re-checked at publish/subscribe time
   3. **Enforcement Logic Missing**: BifroMQ may not have enforcement logic for 
tenant-level settings from the Setting Provider plugin
   4. **Configuration Issue**: Some JVM parameter or configuration may be 
preventing enforcement
   5. **Plugin Integration Issue**: Settings may be retrieved but not used in 
enforcement code paths
   6. **Default Override**: BifroMQ may be using hardcoded defaults instead of 
plugin-provided values
   
   ### Testing Methodology
   
   All tests follow the same pattern:
   1. Configure a restrictive setting in `tenants.json` for `tenant2`
   2. Verify plugin is called and returns correct value (via enhanced logging)
   3. Attempt an operation that violates the setting
   4. Observe that the operation is accepted instead of rejected
   
   **Test Coverage**: 6 settings tested, 6 settings confirmed NOT enforced 
(100% failure rate).
   
   ## Requested Action
   
   Please investigate why BifroMQ is not enforcing tenant-level settings 
provided by the Setting Provider plugin, even though:
   - The plugin is correctly implemented
   - The plugin is being called by BifroMQ
   - The plugin is returning correct values
   - The values are successfully cast and returned
   
   The [official 
documentation](https://bifromq.apache.org/docs/plugin/setting_provider/tenantsetting/)
 indicates these settings should be enforced, but they are **not being enforced 
in practice**.
   
   ### Specific Questions
   
   1. Are tenant-level settings from the Setting Provider plugin supposed to be 
enforced at publish/subscribe time?
   2. Is there a configuration parameter that enables the enforcement of 
tenant-level settings?
   3. Are there any known issues or limitations with tenant-level settings 
enforcement?
   4. Should these settings be enforced differently (e.g., at connection time 
vs. publish time)?
   
   ### Reproduction
   
   The issue can be reproduced by:
   1. Setting any tenant-level setting in `tenants.json` (e.g., 
`MaxTopicLevelLength: 10`)
   2. Verifying plugin returns the value (check logs)
   3. Attempting an operation that violates the setting
   4. Observing that the operation is accepted
   
   **Expected**: Operation should be rejected  
   **Actual**: Operation is accepted
   
   This suggests a fundamental issue with how BifroMQ uses tenant-level 
settings from the Setting Provider plugin.
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to