panel-redesign: the 2.0 schema from zero - one baseline, no 1.x chain
Stage 1, step 1. BREAKING: a 1.x database is not migrated.
Schema
- internal/store/migrations/ holds one file, 0001_init.sql, equal to the
2.0 schema; 0002..0010 are deleted (they stay in history at 1.9.5). No
admin table created and dropped, no ALTERs.
- users.email replaces users.dmarc_report_email: the address is the
user's own. The default DMARC report address is a user's setting
(dmarc_default_mode hosted/account/custom/none, dmarc_default_address);
the instance setting that mirrored the global user's field is gone.
- domains.dmarc_rua_user_id: a domain follows a named user's default, or
has an address of its own in dmarc_rua ('' = none). ON DELETE SET NULL
leaves it at "no reports", never at someone else's default.
- user_inbound_domains, users.all_domains, users.all_inbound_domains: in
the schema for the inbound delegation of the next step.
- users.role is 'global' / 'domain' (was 'domain_admin');
rate_limits.allowed_ips, unused since 1.9.0, is dropped.
Opening a database
- The baseline sets PRAGMA application_id ("SP20"). store.Open refuses a
file that has a schema version without it (ErrForeignSchema) and one
newer than the build. A 1.x file at user_version 10 would otherwise read
as already migrated. The refused file is left untouched.
Code
- Store.DomainDMARCRua is the one place a domain's report address is
resolved; dnscheck.ResolveDMARCRua and Store.GlobalDMARCReportEmail are
gone. dmarc.AllowedRecipients, the domain page, the dashboard and the
DNS re-check go through it.
- The old templates keep working on the new model until their pages are
restyled: the Settings form's address field is the account e-mail and
switches the user's default between it and none; a domain's "Same as
Settings" makes it follow the user who chose it; a new or imported
domain follows its creator.
- Domain export/import: a domain that follows a user exports no address.
Tests: the baseline pinned table by table (TestBaselineSchema), refusal of a
1.x and of a newer database, constraints and cascades of the new columns and
table, resolution in every default mode, the deleted-user case, and the three
bridged handlers.
docs/schema-migrations.md is rewritten for the baseline; roadmap 11/27,
schema-squash marked built.
go test ./... (Windows)
ok for every package except two that fail the same way before this commit:
internal/domain TestWriteLoadPrivateKeyRoundtrip (file mode on NTFS)
internal/logtail TestFollowTailsAndRotates (rename of an open file)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,6 @@ const (
|
||||
type RateLimit struct {
|
||||
Scope string
|
||||
RefID int64
|
||||
AllowedIPs []string // legacy column; unused for new rows
|
||||
MaxMessages int
|
||||
WindowSeconds int
|
||||
Mode string // manual | auto
|
||||
@@ -63,7 +62,7 @@ func (r RateLimit) Active() bool {
|
||||
// its id, for the panel's edit form. ok is false when none is configured.
|
||||
func (s *Store) GetRateLimit(scope string, refID int64) (RateLimit, bool, error) {
|
||||
row := s.db.QueryRow(
|
||||
`SELECT allowed_ips, max_messages, window_seconds, mode, auto_multiplier, auto_updated_at
|
||||
`SELECT max_messages, window_seconds, mode, auto_multiplier, auto_updated_at
|
||||
FROM rate_limits WHERE scope = ? AND ref_id = ?`, scope, refID)
|
||||
rl, err := scanRateLimit(row)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@@ -93,16 +92,15 @@ func (s *Store) SetRateLimit(rl RateLimit) error {
|
||||
autoUpdated = rl.AutoUpdatedAt.UTC().Format(time.RFC3339)
|
||||
}
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO rate_limits (scope, ref_id, allowed_ips, max_messages, window_seconds, mode, auto_multiplier, auto_updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`INSERT INTO rate_limits (scope, ref_id, max_messages, window_seconds, mode, auto_multiplier, auto_updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(scope, ref_id) DO UPDATE SET
|
||||
allowed_ips = excluded.allowed_ips,
|
||||
max_messages = excluded.max_messages,
|
||||
window_seconds = excluded.window_seconds,
|
||||
mode = excluded.mode,
|
||||
auto_multiplier = excluded.auto_multiplier,
|
||||
auto_updated_at = excluded.auto_updated_at`,
|
||||
rl.Scope, rl.RefID, strings.Join(rl.AllowedIPs, ","), rl.MaxMessages, rl.WindowSeconds,
|
||||
rl.Scope, rl.RefID, rl.MaxMessages, rl.WindowSeconds,
|
||||
mode, autoMult, autoUpdated,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -144,11 +142,11 @@ func (s *Store) RateLimit(scope, ref string) (RateLimit, bool, error) {
|
||||
var query string
|
||||
switch scope {
|
||||
case RateLimitScopeDomain:
|
||||
query = `SELECT rl.allowed_ips, rl.max_messages, rl.window_seconds, rl.mode, rl.auto_multiplier, rl.auto_updated_at
|
||||
query = `SELECT rl.max_messages, rl.window_seconds, rl.mode, rl.auto_multiplier, rl.auto_updated_at
|
||||
FROM rate_limits rl JOIN domains d ON d.id = rl.ref_id
|
||||
WHERE rl.scope = 'domain' AND d.name = ?`
|
||||
case RateLimitScopeApp:
|
||||
query = `SELECT rl.allowed_ips, rl.max_messages, rl.window_seconds, rl.mode, rl.auto_multiplier, rl.auto_updated_at
|
||||
query = `SELECT rl.max_messages, rl.window_seconds, rl.mode, rl.auto_multiplier, rl.auto_updated_at
|
||||
FROM rate_limits rl JOIN applications a ON a.id = rl.ref_id
|
||||
WHERE rl.scope = 'application' AND a.login = ?`
|
||||
default:
|
||||
@@ -196,23 +194,21 @@ func (s *Store) CountMessages(scope, ref string, since time.Time) (int64, error)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// scanRateLimit reads the three stored columns, tolerating NULL numeric columns
|
||||
// scanRateLimit reads the stored columns, tolerating NULL numeric columns
|
||||
// by leaving the corresponding field zero, which makes the limit inert via
|
||||
// Active() until max and window are both set.
|
||||
func scanRateLimit(r scanRow) (RateLimit, error) {
|
||||
var (
|
||||
ips sql.NullString
|
||||
maxMsgs sql.NullInt64
|
||||
windowSecs sql.NullInt64
|
||||
mode sql.NullString
|
||||
autoMult sql.NullFloat64
|
||||
autoUpdated sql.NullString
|
||||
)
|
||||
if err := r.Scan(&ips, &maxMsgs, &windowSecs, &mode, &autoMult, &autoUpdated); err != nil {
|
||||
if err := r.Scan(&maxMsgs, &windowSecs, &mode, &autoMult, &autoUpdated); err != nil {
|
||||
return RateLimit{}, err
|
||||
}
|
||||
rl := RateLimit{
|
||||
AllowedIPs: splitIPs(ips.String),
|
||||
MaxMessages: int(maxMsgs.Int64),
|
||||
WindowSeconds: int(windowSecs.Int64),
|
||||
Mode: mode.String,
|
||||
|
||||
Reference in New Issue
Block a user