mirror of https://github.com/docusealco/docuseal
parent
32e244f522
commit
05e312bd14
@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Seeds DOCUSEAL_CONFIG_* env overrides into every existing account on boot.
|
||||
# New accounts receive overrides via Account#after_create_commit.
|
||||
#
|
||||
# Runs only when at least one override env var is set and the accounts table is ready.
|
||||
Rails.application.config.after_initialize do
|
||||
next if Rails.env.test?
|
||||
next unless ActiveRecord::Base.connection.data_source_exists?('accounts')
|
||||
next if Account.env_config_overrides.empty?
|
||||
|
||||
Account.find_each(&:apply_env_config_overrides)
|
||||
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid => e
|
||||
Rails.logger.warn("[account_config_env_overrides] skipped: #{e.class}: #{e.message}")
|
||||
end
|
||||
@ -0,0 +1,58 @@
|
||||
# Environment Variable Config Overrides
|
||||
|
||||
Any `account_config` value can be locked via an environment variable using the
|
||||
`DOCUSEAL_CONFIG_<UPCASE_KEY>` pattern. When set, the value takes precedence
|
||||
over the database and the corresponding UI toggle is rendered as disabled with
|
||||
a tooltip "Locked by environment variable".
|
||||
|
||||
## Value parsing rules
|
||||
|
||||
| Raw ENV value | Parsed as |
|
||||
|------------------------------------|---------------------------|
|
||||
| `true`, `1`, `yes`, `on` | boolean `true` |
|
||||
| `false`, `0`, `no`, `off` | boolean `false` |
|
||||
| Valid JSON (object / array / num) | parsed JSON |
|
||||
| Anything else | raw string |
|
||||
|
||||
Comparison is case-insensitive for booleans.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Boolean toggle
|
||||
DOCUSEAL_CONFIG_ALLOW_TYPED_SIGNATURE=true
|
||||
|
||||
# JSON object
|
||||
DOCUSEAL_CONFIG_POLICY_LINKS='{"privacy":"https://example.com/privacy"}'
|
||||
|
||||
# Plain string
|
||||
DOCUSEAL_CONFIG_DOCUMENT_FILENAME_FORMAT="{{template.name}}-{{submitter.name}}"
|
||||
```
|
||||
|
||||
## Supported keys (non-exhaustive)
|
||||
|
||||
All keys declared as constants in `app/models/account_config.rb` are supported.
|
||||
A few common ones:
|
||||
|
||||
| ENV variable | Account config key |
|
||||
|--------------------------------------------------------|----------------------------------|
|
||||
| `DOCUSEAL_CONFIG_ALLOW_TYPED_SIGNATURE` | `allow_typed_signature` |
|
||||
| `DOCUSEAL_CONFIG_ALLOW_TO_DECLINE` | `allow_to_decline` |
|
||||
| `DOCUSEAL_CONFIG_ENFORCE_SIGNING_ORDER` | `enforce_signing_order` |
|
||||
| `DOCUSEAL_CONFIG_FORCE_MFA` | `force_mfa` |
|
||||
| `DOCUSEAL_CONFIG_EMAIL_FOOTER_MESSAGE` | `email_footer_message` |
|
||||
| `DOCUSEAL_CONFIG_SHOW_CONSOLE_LINK` | `show_console_link` |
|
||||
| `DOCUSEAL_CONFIG_SHOW_API_LINK` | `show_api_link` |
|
||||
| `DOCUSEAL_CONFIG_SHOW_TEST_MODE` | `show_test_mode` |
|
||||
|
||||
## How it works
|
||||
|
||||
- `AccountConfig.locked_by_env?(key)` returns `true` when the matching env var
|
||||
is set (non-blank).
|
||||
- `AccountConfig.env_override_cast(key)` parses the value per the rules above.
|
||||
- On `Account` create (`after_create_commit`), all env overrides are upserted
|
||||
into `account_configs`.
|
||||
- On boot (`config/initializers/account_config_env_overrides.rb`), overrides
|
||||
are applied to all existing accounts so the DB stays in sync with the env.
|
||||
- Views should check `AccountConfig.locked_by_env?(key)` to render form fields
|
||||
as disabled with an appropriate tooltip when an override is active.
|
||||
@ -0,0 +1,16 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { loginAsAdmin } from './helpers/auth';
|
||||
|
||||
// Phase 0.1 — Config override pattern (UI reflection only).
|
||||
// The env-var swap itself cannot be exercised via Playwright (needs pod restart),
|
||||
// so we verify the UI renders toggles based on the DB-seeded AccountConfig values.
|
||||
|
||||
test.describe('Config overrides — UI reflection', () => {
|
||||
test('E-Signing settings page loads and renders toggles', async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.goto('/settings/esign');
|
||||
|
||||
// A known account-config backed toggle should be present.
|
||||
await expect(page.locator('form')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,67 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AccountConfig, type: :model do
|
||||
describe '.env_key_for' do
|
||||
it 'builds the DOCUSEAL_CONFIG_<UPCASE_KEY> env variable name' do
|
||||
expect(described_class.env_key_for('allow_typed_signature'))
|
||||
.to eq('DOCUSEAL_CONFIG_ALLOW_TYPED_SIGNATURE')
|
||||
end
|
||||
end
|
||||
|
||||
describe '.locked_by_env?' do
|
||||
it 'returns true when the matching env var is set' do
|
||||
with_env('DOCUSEAL_CONFIG_ALLOW_TYPED_SIGNATURE' => 'true') do
|
||||
expect(described_class.locked_by_env?('allow_typed_signature')).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns false when the matching env var is absent' do
|
||||
with_env('DOCUSEAL_CONFIG_ALLOW_TYPED_SIGNATURE' => nil) do
|
||||
expect(described_class.locked_by_env?('allow_typed_signature')).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns false when the matching env var is blank' do
|
||||
with_env('DOCUSEAL_CONFIG_ALLOW_TYPED_SIGNATURE' => '') do
|
||||
expect(described_class.locked_by_env?('allow_typed_signature')).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.env_override_cast' do
|
||||
it 'casts boolean-ish strings to booleans' do
|
||||
with_env('DOCUSEAL_CONFIG_FORCE_MFA' => 'true') do
|
||||
expect(described_class.env_override_cast('force_mfa')).to be(true)
|
||||
end
|
||||
with_env('DOCUSEAL_CONFIG_FORCE_MFA' => 'false') do
|
||||
expect(described_class.env_override_cast('force_mfa')).to be(false)
|
||||
end
|
||||
with_env('DOCUSEAL_CONFIG_FORCE_MFA' => '1') do
|
||||
expect(described_class.env_override_cast('force_mfa')).to be(true)
|
||||
end
|
||||
with_env('DOCUSEAL_CONFIG_FORCE_MFA' => '0') do
|
||||
expect(described_class.env_override_cast('force_mfa')).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
it 'parses valid JSON' do
|
||||
with_env('DOCUSEAL_CONFIG_POLICY_LINKS' => '{"a":1}') do
|
||||
expect(described_class.env_override_cast('policy_links')).to eq({ 'a' => 1 })
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns the raw string when not boolean or JSON' do
|
||||
with_env('DOCUSEAL_CONFIG_DOCUMENT_FILENAME_FORMAT' => 'hello') do
|
||||
expect(described_class.env_override_cast('document_filename_format')).to eq('hello')
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns nil when the env var is absent' do
|
||||
with_env('DOCUSEAL_CONFIG_FORCE_MFA' => nil) do
|
||||
expect(described_class.env_override_cast('force_mfa')).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Account, type: :model do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe '#apply_env_config_overrides' do
|
||||
it 'upserts env override values into account_configs' do
|
||||
with_env('DOCUSEAL_CONFIG_ALLOW_TYPED_SIGNATURE' => 'true') do
|
||||
account.apply_env_config_overrides
|
||||
row = account.account_configs.find_by(key: 'allow_typed_signature')
|
||||
expect(row).not_to be_nil
|
||||
expect(row.value).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
it 'overwrites existing DB value with env value' do
|
||||
account.account_configs.create!(key: 'allow_typed_signature', value: false)
|
||||
with_env('DOCUSEAL_CONFIG_ALLOW_TYPED_SIGNATURE' => 'true') do
|
||||
account.apply_env_config_overrides
|
||||
expect(account.account_configs.find_by(key: 'allow_typed_signature').value).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#config_value' do
|
||||
it 'returns env value with locked=true when env set' do
|
||||
with_env('DOCUSEAL_CONFIG_ALLOW_TYPED_SIGNATURE' => 'true') do
|
||||
value, locked = account.config_value('allow_typed_signature')
|
||||
expect(value).to be(true)
|
||||
expect(locked).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns DB value with locked=false when env absent' do
|
||||
account.account_configs.create!(key: 'allow_typed_signature', value: true)
|
||||
with_env('DOCUSEAL_CONFIG_ALLOW_TYPED_SIGNATURE' => nil) do
|
||||
value, locked = account.config_value('allow_typed_signature')
|
||||
expect(value).to be(true)
|
||||
expect(locked).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns the default when no env var and no DB row' do
|
||||
value, locked = account.config_value('nonexistent_key', default: :foo)
|
||||
expect(value).to eq(:foo)
|
||||
expect(locked).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ui_visible?' do
|
||||
it 'returns false when DB value is false' do
|
||||
account.account_configs.create!(key: 'show_console_link', value: false)
|
||||
expect(account.ui_visible?('show_console_link')).to be(false)
|
||||
end
|
||||
|
||||
it 'returns true when DB value is true' do
|
||||
account.account_configs.create!(key: 'show_console_link', value: true)
|
||||
expect(account.ui_visible?('show_console_link')).to be(true)
|
||||
end
|
||||
|
||||
it 'returns the default when no row and no env var' do
|
||||
expect(account.ui_visible?('show_console_link', default: true)).to be(true)
|
||||
expect(account.ui_visible?('show_console_link', default: false)).to be(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Tiny helper to set ENV vars for the duration of a block, then restore.
|
||||
module EnvHelpers
|
||||
def with_env(vars)
|
||||
original = {}
|
||||
vars.each do |k, v|
|
||||
original[k] = ENV.fetch(k, nil)
|
||||
if v.nil?
|
||||
ENV.delete(k)
|
||||
else
|
||||
ENV[k] = v
|
||||
end
|
||||
end
|
||||
yield
|
||||
ensure
|
||||
original.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include EnvHelpers
|
||||
end
|
||||
Loading…
Reference in new issue