feat(configurations): tenant-aware configuration overrides - #6205
Merged
Conversation
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
force-pushed
the
feat/tenant-aware-configuration
branch
from
July 8, 2026 07:00
3f4541c to
df176b9
Compare
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); |
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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+ theget()precedence tweak) and a new tunableDirigibleConfig.TENANT_CONFIGURATION_ALLOWED_KEYS(defaultDIRIGIBLE_TENANT_*). No new dependency is added to this module.core-configurations(newdata-sourcesdependency;TenantContextcomes fromcore-base):TenantConfigurationStore— rawSqlFactoryCRUD against the tenant-routed default datasource; create-if-absent per-tenantDIRIGIBLE_CONFIGURATIONStable.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 infinally). Kept in this module socore-tenantsdoes not have to depend oncore-configurations.TenantConfigurationsEndpoint—GET/PUT/DELETE services/core/configurations/tenant(ADMINISTRATOR/OPERATOR).UI
A Tenant Configurations view added to
view-configurations(CRUD viaDialogHubform/confirm dialogs against the REST API), mirroring the existingview-configurations/view-securitypatterns.Tests
Configurationprecedence (thread map between runtime and env) andTenantConfigurationKeyPolicyTest(allow-list + protected deny-list).TenantConfigurationIT(HTTP-only, full-app boot) — allow-listed key resolves with precedence and clears; a non-allow-listed key is stored but never injected.GET/PUT/DELETEroundtrip works over the HTTP boundary.Notes
TenantConfigurationCache).🤖 Generated with Claude Code