Skip to content

Commit

Permalink
Add read-only mode to DB server (#1102)
Browse files Browse the repository at this point in the history
setting the `API_READ_ONLY` environment variable blocks API operations that create/update/delete versions, annotations, etc. Since we ware now working towards shutting down the entire system, this is helpful so we can work on creating archives without worrying about the database changing from underneath us. See edgi-govdata-archiving/web-monitoring#170.
  • Loading branch information
Mr0grog authored Jul 17, 2023
1 parent 53c3bc1 commit 3632762
Show file tree
Hide file tree
Showing 15 changed files with 142 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,7 @@ ALLOW_VERSIONS_IN_PAGE_RESPONSES='false'
# Set to 'true' to require versions to have valid mime/content/media type fields
# in order to be automatically analyzed after importing.
# ANALYSIS_REQUIRE_MEDIA_TYPE='false'

# Put the API into read-only mode, where imports, annotations, and other changes
# to data are blocked.
# API_READ_ONLY='true'
2 changes: 2 additions & 0 deletions app/controllers/api/v0/annotations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ def show
end

def create
raise Api::ReadOnlyError if Rails.configuration.read_only

begin
data = JSON.parse(request.body.read)
rescue JSON::ParserError
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/api/v0/imports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ def show
end

def create
raise Api::ReadOnlyError if Rails.configuration.read_only

update_behavior = params[:update] || :skip
unless Import.update_behaviors.key?(update_behavior)
raise Api::InputError, "'#{update_behavior}' is not a valid update behavior. Use one of: #{Import.update_behaviors.join(', ')}"
Expand Down
6 changes: 6 additions & 0 deletions app/controllers/api/v0/maintainers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ def show
end

def create
raise Api::ReadOnlyError if Rails.configuration.read_only

data = JSON.parse(request.body.read)
if data['uuid'].nil? && data['name'].nil?
raise Api::InputError, 'You must specify either a `uuid` or `name` for the maintainer to add.'
Expand Down Expand Up @@ -64,13 +66,17 @@ def create
end

def update
raise Api::ReadOnlyError if Rails.configuration.read_only

@maintainer = (page ? page.maintainers : Maintainer).find(params[:id])
data = JSON.parse(request.body.read).slice('name', 'parent_id')
@maintainer.update!(data)
show
end

def destroy
raise Api::ReadOnlyError if Rails.configuration.read_only

# NOTE: this assumes you can only get here in the context of a page
page.remove_maintainer(Maintainer.find(params[:id]))
redirect_to(api_v0_page_maintainers_url(page))
Expand Down
6 changes: 6 additions & 0 deletions app/controllers/api/v0/tags_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ def show
end

def create
raise Api::ReadOnlyError if Rails.configuration.read_only

data = JSON.parse(request.body.read)
if data['uuid'].nil? && data['name'].nil?
raise Api::InputError, 'You must specify either a `uuid` or `name` for the tag to add.'
Expand All @@ -43,13 +45,17 @@ def create
end

def update
raise Api::ReadOnlyError if Rails.configuration.read_only

@tag = (page ? page.tags : Tag).find(params[:id])
data = JSON.parse(request.body.read)
@tag.update(name: data['name'])
show
end

def destroy
raise Api::ReadOnlyError if Rails.configuration.read_only

# NOTE: this assumes you can only get here in the context of a page
page.untag(Tag.find(params[:id]))
redirect_to(api_v0_page_tags_url(page))
Expand Down
6 changes: 6 additions & 0 deletions app/controllers/api/v0/urls_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ def show
end

def create
raise Api::ReadOnlyError if Rails.configuration.read_only

@page_url = page.urls.create!(url_params)
show
rescue ActiveRecord::RecordNotUnique
raise Api::ResourceExistsError, 'This page already has the given URL and timeframe'
end

def update
raise Api::ReadOnlyError if Rails.configuration.read_only

updates = url_params
if updates.key?(:url)
raise Api::UnprocessableError, 'You cannot change a URL\'s `url`'
Expand All @@ -40,6 +44,8 @@ def update
end

def destroy
raise Api::ReadOnlyError if Rails.configuration.read_only

@page_url ||= page.urls.find(params[:id])
# You cannot delete the canonical URL.
if @page_url.url == page.url
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/api/v0/versions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ def raw
end

def create
raise Api::ReadOnlyError if Rails.configuration.read_only

authorize(:api, :import?)

# TODO: unify this with import code in ImportVersionsJob#import_record
Expand Down
4 changes: 4 additions & 0 deletions config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,9 @@ class Application < Rails::Application
config.allow_public_view = ActiveModel::Type::Boolean.new.cast(
ENV.fetch('ALLOW_PUBLIC_VIEW', 'true')
).present?

config.read_only = ActiveModel::Type::Boolean.new.cast(
ENV.fetch('API_READ_ONLY', 'true')
).present?
end
end
10 changes: 10 additions & 0 deletions lib/api/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,14 @@ def initialize(url, hash)
super("Response body for '#{url}' did not match expected hash (#{hash})")
end
end

class ReadOnlyError < ApiError
def status_code
423
end

def initialize(message = 'This API is read-only; you cannot add or update data.')
super(message)
end
end
end
16 changes: 16 additions & 0 deletions test/controllers/api/v0/annotations_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ class Api::V0::AnnotationsControllerTest < ActionDispatch::IntegrationTest
assert_equal annotation, body['data']['annotation']
end

test 'cannot annotate in read-only mode' do
page = pages(:home_page)
annotation = { 'test_key' => 'test_value' }

with_rails_configuration(:read_only, true) do
sign_in users(:alice)
post(
api_v0_page_change_annotations_path(page, "..#{page.versions[0].uuid}"),
as: :json,
params: annotation
)

assert_response :locked
end
end

test 'posting a new annotation updates previous annotations by the same user' do
page = pages(:home_page)
annotation1 = { 'test_key' => 'test_value' }
Expand Down
43 changes: 43 additions & 0 deletions test/controllers/api/v0/imports_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,49 @@ def teardown
assert_equal(import_data[1][:page_url], versions[0].url)
end

test 'cannot import in read-only mode' do
import_data = [
{
page_url: 'http://testsite.com/',
title: 'Example Page',
page_maintainers: ['The Federal Example Agency'],
page_tags: ['Example Site'],
capture_time: '2017-05-01T12:33:01Z',
body_url: 'https://test-bucket.s3.amazonaws.com/example-v1',
body_hash: 'f366e89639758cd7f75d21e5026c04fb1022853844ff471865004b3274059686',
source_type: 'some_source',
source_metadata: { test_meta: 'data' }
},
{
page_url: 'http://testsite.com/',
title: 'Example Page',
page_maintainers: ['The Federal Example Agency'],
page_tags: ['Test', 'Home Page'],
capture_time: '2017-05-02T12:33:01Z',
body_url: 'https://test-bucket.s3.amazonaws.com/example-v2',
body_hash: 'f366e89639758cd7f75d21e5026c04fb1022853844ff471865004b3274059687',
source_type: 'some_source',
source_metadata: { test_meta: 'data' }
}
]

start_version_count = Version.count

with_rails_configuration(:read_only, true) do
sign_in users(:alice)
perform_enqueued_jobs do
post(
api_v0_imports_path,
headers: { 'Content-Type': 'application/x-json-stream' },
params: import_data.map(&:to_json).join("\n")
)
end

assert_response :locked
assert_equal Version.count, start_version_count
end
end

test 'does not add or modify a version if it already exists' do
page_versions_count = pages(:home_page).versions.count
original_data = versions(:page1_v1).as_json
Expand Down
12 changes: 12 additions & 0 deletions test/controllers/api/v0/maintainers_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@ class Api::V0::MaintainersControllerTest < ActionDispatch::IntegrationTest
assert_response(:forbidden)
end

test 'cannot add a maintainer in read-only mode' do
with_rails_configuration(:read_only, true) do
sign_in users(:alice)
post(
api_v0_maintainers_path,
as: :json,
params: { name: 'EPA' }
)
assert_response(:locked)
end
end

test 'can add a maintainer to a page' do
sign_in users(:alice)
post(
Expand Down
12 changes: 12 additions & 0 deletions test/controllers/api/v0/tags_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ class Api::V0::TagsControllerTest < ActionDispatch::IntegrationTest
assert_response(:forbidden)
end

test 'cannot add a tag in read-only mode' do
with_rails_configuration(:read_only, true) do
sign_in users(:alice)
post(
api_v0_page_tags_path(pages(:home_page)),
as: :json,
params: { name: 'Page of wonderment' }
)
assert_response(:locked)
end
end

test 'can add a tag to a page' do
sign_in users(:alice)
post(
Expand Down
12 changes: 12 additions & 0 deletions test/controllers/api/v0/urls_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ class Api::V0::UrlsControllerTest < ActionDispatch::IntegrationTest
assert_response(:forbidden)
end

test 'cannot create new urls in read-only mode' do
with_rails_configuration(:read_only, true) do
sign_in users(:alice)
post(
api_v0_page_urls_path(pages(:home_page)),
as: :json,
params: { page_url: { url: 'https://example.gov/new_url' } }
)
assert_response :locked
end
end

test 'can create new urls' do
sign_in users(:alice)
post(
Expand Down
5 changes: 5 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all

def setup
# Assume writability. Individual tests can opt-in to read-only.
Rails.configuration.read_only = false
end

# Add more helper methods to be used by all tests here...
def assert_ordered(list, reverse: false, name: 'Items')
sorted = list.sort
Expand Down

0 comments on commit 3632762

Please sign in to comment.