Skip to content

feat(configurations): tenant-aware configuration overrides - #6205

Merged
delchev merged 7 commits into
masterfrom
feat/tenant-aware-configuration
Jul 8, 2026
Merged

feat(configurations): tenant-aware configuration overrides#6205
delchev merged 7 commits into
masterfrom
feat/tenant-aware-configuration

Conversation

@delchev

@delchev delchev commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

What

Adds tenant-aware configuration: each tenant can override configuration values, resolved per request with precedence RUNTIME -> tenant thread map -> ENVIRONMENT -> DEPLOYMENT -> MODULE.

Design

  • commons-config (bottom layer) stays dependency-free. It gains only a neutral thread-local mechanism (Configuration.set/remove/getThreadConfiguration + the get() precedence tweak) and a new tunable DirigibleConfig.TENANT_CONFIGURATION_ALLOWED_KEYS (default DIRIGIBLE_TENANT_*). No new dependency is added to this module.
  • All tenant/DB logic lives in core-configurations (new data-sources dependency; TenantContext comes from core-base):
    • TenantConfigurationStore — raw SqlFactory CRUD against the tenant-routed default datasource; create-if-absent per-tenant DIRIGIBLE_CONFIGURATIONS table.
    • TenantConfigurationCache — per-tenant, refresh-on-write.
    • TenantConfigurationKeyPolicy — namespaced allow-list plus a hard protected deny-list, so even a wildcard allow-list can never let a tenant shadow the database / repository / security / multi-tenancy keys.
    • TenantConfigurationService — facade over store/cache/policy.
    • TenantConfigurationInitFilter (LOWEST_PRECEDENCE) — runs downstream of the security filter chain, i.e. inside the tenant execution scope, and loads the allow-listed entries into the thread map for the request (cleared in finally). Kept in this module so core-tenants does not have to depend on core-configurations.
    • TenantConfigurationsEndpointGET/PUT/DELETE services/core/configurations/tenant (ADMINISTRATOR/OPERATOR).

UI

A Tenant Configurations view added to view-configurations (CRUD via DialogHub form/confirm dialogs against the REST API), mirroring the existing view-configurations / view-security patterns.

Tests

  • Unit: Configuration precedence (thread map between runtime and env) and TenantConfigurationKeyPolicyTest (allow-list + protected deny-list).
  • Integration: TenantConfigurationIT (HTTP-only, full-app boot) — allow-listed key resolves with precedence and clears; a non-allow-listed key is stored but never injected.
  • Also verified against a running instance: the view HTML/JS are served and the GET/PUT/DELETE roundtrip works over the HTTP boundary.

Notes

  • The cache is per-node (refresh-on-write on the same instance); a multi-node cluster would need an external invalidation signal (documented as out of scope in TenantConfigurationCache).

🤖 Generated with Claude Code

delchev and others added 2 commits July 8, 2026 10:00
Let each tenant override configuration values, resolved per request with
precedence RUNTIME -> tenant thread map -> ENVIRONMENT -> DEPLOYMENT -> MODULE.

commons-config (bottom layer) gains ONLY a neutral thread-local mechanism -
Configuration.set/remove/getThreadConfiguration plus the get() precedence tweak -
and a new DirigibleConfig.TENANT_CONFIGURATION_ALLOWED_KEYS tunable
(default DIRIGIBLE_TENANT_*). No new dependency on that module.

All tenant/DB logic lives in core-configurations (new data-sources dependency):
- TenantConfigurationStore: raw SqlFactory CRUD on the tenant-routed default
  datasource; create-if-absent per-tenant DIRIGIBLE_CONFIGURATIONS table.
- TenantConfigurationCache: per-tenant, refresh-on-write.
- TenantConfigurationKeyPolicy: namespaced allow-list plus a hard protected
  deny-list (a wildcard allow-list can never shadow DB/repo/security/tenant keys).
- TenantConfigurationService: facade over store/cache/policy.
- TenantConfigurationInitFilter (LOWEST_PRECEDENCE): runs inside the tenant scope
  and loads the allow-listed entries into the thread map for the request.
- TenantConfigurationsEndpoint: GET/PUT/DELETE services/core/configurations/tenant
  (ADMINISTRATOR/OPERATOR).

UI: a Tenant Configurations view in view-configurations (CRUD via DialogHub form
dialogs against the REST API).

Tests: Configuration precedence unit tests, TenantConfigurationKeyPolicyTest, and
TenantConfigurationIT (HTTP-only end-to-end).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e-list

Flip the tenant configuration key policy from a deny-list (PROTECTED_PREFIXES)
to a white-list (ALLOWED_PREFIXES): a tenant key is injectable only when it
matches an allowed prefix - default-deny for everything else. For now only the
branding properties (DIRIGIBLE_BRANDING_*) are exposed.

This supersedes the configurable DIRIGIBLE_TENANT_CONFIGURATION_ALLOWED_KEYS
allow-list, which is removed together with its now-unused DirigibleConfig entry.

Tests and the integration test updated for the white-list semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@delchev
delchev force-pushed the feat/tenant-aware-configuration branch from 3f4541c to df176b9 Compare July 8, 2026 07:00
delchev and others added 2 commits July 8, 2026 10:16
Make the tenant key policy an explicit list of full configuration keys (exact
match, no wildcards) - the branding properties for now
(DIRIGIBLE_BRANDING_NAME/SUBTITLE/BRAND/BRAND_URL/FAVICON/THEME/PREFIX/ANALYTICS).
A prefixed-but-unknown key is no longer injectable.

Add GET services/core/configurations/tenant/predefined returning each predefined
key with the current tenant's value (null when unset) - the fixed set the UI renders.

Add a built-in "Tenant Configuration" entry to the Harmonia application shell's
Settings (resources-application): lists the predefined properties with editable
values (empty falls back to the platform default), Save/Reload, and a permission
notice on 403. Rendered locally like Region & Language.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…idable key

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* @return the entries
*/
@GetMapping
public ResponseEntity<List<TenantConfiguration>> findAll() {
* @return the predefined entries
*/
@GetMapping("/predefined")
public ResponseEntity<List<TenantConfiguration>> findPredefined() {
.column(QUOTED_KEY, DataType.VARCHAR, true, false, false, "(255)")
.column(QUOTED_VALUE, DataType.VARCHAR, false, true, false, "(4000)")
.build();
try (PreparedStatement statement = connection.prepareStatement(sql)) {
.build();
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.executeUpdate();
LOGGER.info("Created per-tenant configuration table using sql [{}]", sql);
delchev and others added 3 commits July 8, 2026 12:55
…export/import count

The tenant-configuration store creates DIRIGIBLE_CONFIGURATIONS in every
tenant schema on first request (create-if-absent, by design), so the imported
PUBLIC schema now holds one platform table alongside the five user tables the
test moves. assertTablesCount filters it out - it is infrastructure, not part
of the exported/imported user schema under test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants