Add per-account storage exceptions for SaaS (#2733)

Allow granting specific accounts more storage via an
account_storage_exceptions table in the saas DB, without
relying on the blanket staff bypass.
This commit is contained in:
Jorge Manrubia
2026-03-19 15:36:36 +01:00
committed by GitHub
parent cf10dbd91e
commit 1c4bf3e1e0
8 changed files with 92 additions and 7 deletions
@@ -0,0 +1,5 @@
class Account::StorageException < SaasRecord
belongs_to :account
validates :bytes_allowed, presence: true, numericality: { greater_than: 0 }
end
+19 -3
View File
@@ -1,14 +1,30 @@
module Account::StorageLimited
extend ActiveSupport::Concern
STORAGE_LIMIT = 1.gigabyte
DEFAULT_STORAGE_LIMIT = 1.gigabyte
NEAR_STORAGE_LIMIT_THRESHOLD = 500.megabytes
included do
has_one :storage_exception
end
def storage_limit
storage_exception&.bytes_allowed || DEFAULT_STORAGE_LIMIT
end
def exceeding_storage_limit?
bytes_used > STORAGE_LIMIT
bytes_used > storage_limit
end
def nearing_storage_limit?
!exceeding_storage_limit? && bytes_used > STORAGE_LIMIT - NEAR_STORAGE_LIMIT_THRESHOLD
!exceeding_storage_limit? && bytes_used > storage_limit - NEAR_STORAGE_LIMIT_THRESHOLD
end
def add_storage_exception(bytes)
if storage_exception
storage_exception.update!(bytes_allowed: bytes)
else
create_storage_exception!(bytes_allowed: bytes)
end
end
end
@@ -3,7 +3,7 @@
<div class="comment__content flex flex-column flex-item-grow full-width">
<div class="comment__body lexxy-content">
<div class="action-text-content">
<strong>This account has used all the included free storage (<%= number_to_human_size(Account::StorageLimited::STORAGE_LIMIT) %>).</strong>
<strong>This account has used all the included free storage (<%= number_to_human_size(Current.account.storage_limit) %>).</strong>
<div><%= link_to "Self-host Fizzy", "https://github.com/basecamp/fizzy", target: "_blank" %> for unlimited storage.</div>
</div>
</div>
@@ -1,6 +1,6 @@
<div class="card-perma__notch card-perma__notch--bottom">
<div class="card-perma__account-limit-message">
<strong>This account has used all the included free storage (<%= number_to_human_size(Account::StorageLimited::STORAGE_LIMIT) %>).</strong>
<strong>This account has used all the included free storage (<%= number_to_human_size(Current.account.storage_limit) %>).</strong>
<div>
<%= link_to "Self-host Fizzy", "https://github.com/basecamp/fizzy", target: "_blank" %> for unlimited storage.
</div>
@@ -1,6 +1,6 @@
<% if Current.account.nearing_storage_limit? && !Current.identity.staff? %>
<div class="txt-small">
This account has used <strong><%= number_to_human_size(Current.account.bytes_used) %></strong> of <strong><%= number_to_human_size(Account::StorageLimited::STORAGE_LIMIT) %></strong> storage.
This account has used <strong><%= number_to_human_size(Current.account.bytes_used) %></strong> of <strong><%= number_to_human_size(Current.account.storage_limit) %></strong> storage.
<div><%= link_to "Self-host Fizzy", "https://github.com/basecamp/fizzy", class: "txt-current txt-underline", target: "_blank" %> for unlimited storage.</div>
</div>
<% end %>
@@ -0,0 +1,10 @@
class CreateAccountStorageExceptions < ActiveRecord::Migration[8.2]
def change
create_table :account_storage_exceptions, id: :uuid do |t|
t.references :account, null: false, type: :uuid, index: { unique: true }
t.bigint :bytes_allowed, null: false
t.timestamps
end
end
end
+9 -1
View File
@@ -10,7 +10,15 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.2].define(version: 2026_03_17_000000) do
ActiveRecord::Schema[8.2].define(version: 2026_03_19_142914) do
create_table "account_storage_exceptions", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.uuid "account_id", null: false
t.bigint "bytes_allowed", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_account_storage_exceptions_on_account_id", unique: true
end
create_table "action_push_native_devices", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "name"
@@ -0,0 +1,46 @@
require "test_helper"
class Account::StorageExceptionTest < ActiveSupport::TestCase
test "storage limit returns default when no exception exists" do
assert_equal Account::StorageLimited::DEFAULT_STORAGE_LIMIT, accounts(:initech).storage_limit
end
test "storage limit returns exception value when one exists" do
account = accounts(:initech)
account.add_storage_exception(5.gigabytes)
assert_equal 5.gigabytes, account.storage_limit
end
test "add storage exception creates a new record" do
account = accounts(:initech)
assert_difference -> { Account::StorageException.count } do
account.add_storage_exception(2.gigabytes)
end
assert_equal 2.gigabytes, account.storage_exception.bytes_allowed
end
test "add storage exception updates existing record" do
account = accounts(:initech)
account.add_storage_exception(2.gigabytes)
assert_no_difference -> { Account::StorageException.count } do
account.add_storage_exception(10.gigabytes)
end
assert_equal 10.gigabytes, account.storage_exception.reload.bytes_allowed
end
test "exceeding storage limit respects exception" do
account = accounts(:initech)
Account.any_instance.stubs(:bytes_used).returns(2.gigabytes)
assert account.exceeding_storage_limit?
account.add_storage_exception(5.gigabytes)
assert_not account.exceeding_storage_limit?
end
end