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