Skip to content

Commit 2c38d60

Browse files
jamiestampGDSNewt
andcommitted
Add named-user access limiting
Adds the "Limit access to named publishers" option to the access limiting form. Publishers can now restrict access to a draft to a list of email addresses, with the creating publisher always preserved on the list. `Edition::LimitedAccess` manages the list via the `named_accesses` association with autosave: assigning `access_limited_named_users=` builds and marks-for-destruction records eagerly so the persisted list always matches the input. `EditionRules#access_limit_enforced?` and `Admin::EditionFilter` are extended to enforce membership for the `named_users` mode, and `DraftEditionUpdater` skips its organisation-membership check for it. Co-authored-by: Alex Newton <alex.newton@digital.cabinet-office.gov.uk>
1 parent acd23f1 commit 2c38d60

15 files changed

Lines changed: 295 additions & 22 deletions

File tree

app/controllers/admin/edition_access_limited_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def edition_params
4848
.fetch(:edition, {})
4949
.permit(
5050
:access_limited,
51+
:access_limited_named_users,
5152
:editorial_remark,
5253
{
5354
lead_organisation_ids: [],

app/controllers/admin/editions_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ def permitted_edition_attributes
222222
:scheduled_publication,
223223
:lock_version,
224224
:access_limited,
225+
:access_limited_named_users,
225226
:alternative_format_provider_id,
226227
:opening_at,
227228
:closing_at,

app/models/concerns/edition/limited_access.rb

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
module Edition::LimitedAccess
22
extend ActiveSupport::Concern
33

4+
class Trait < Edition::Traits::Trait
5+
def process_associations_after_save(draft)
6+
@edition.named_accesses.each do |na|
7+
draft.named_accesses.create!(email: na.email)
8+
end
9+
end
10+
end
11+
412
included do
513
enum :access_limited, { disabled: 0, organisations: 1, named_users: 2 }
14+
has_many :named_accesses, dependent: :destroy, inverse_of: :edition, autosave: true
615
after_initialize :set_access_limited
16+
before_save :destroy_named_accesses_unless_named_users
17+
before_save :ensure_creator_in_named_accesses, if: :named_users?
18+
validate :validate_named_users_emails, if: -> { named_users? }
19+
add_trait Trait
720
end
821

922
module ClassMethods
@@ -38,4 +51,60 @@ def set_access_limited
3851
def accessible_to?(user)
3952
user.present? && Whitehall::Authority::Enforcer.new(user, self).can?(:see)
4053
end
54+
55+
def access_limited_named_users=(value)
56+
@access_limited_named_users_input = value
57+
new_emails = parse_named_user_emails(value).map(&:downcase).uniq
58+
59+
existing = active_named_accesses.index_by { |na| na.email.downcase }
60+
61+
existing.each do |email, na|
62+
na.mark_for_destruction unless new_emails.include?(email)
63+
end
64+
65+
new_emails.each do |email|
66+
named_accesses.build(email:) unless existing.key?(email)
67+
end
68+
end
69+
70+
def access_limited_named_users
71+
@access_limited_named_users_input || active_named_accesses.map(&:email).join(", ")
72+
end
73+
74+
private
75+
76+
def active_named_accesses
77+
named_accesses.reject(&:marked_for_destruction?)
78+
end
79+
80+
def parse_named_user_emails(value)
81+
(value || "").split(/[\n,]/).map(&:strip).reject(&:blank?)
82+
end
83+
84+
def validate_named_users_emails
85+
if active_named_accesses.empty?
86+
errors.add(:access_limited_named_users, "must include at least one email address")
87+
end
88+
89+
active_named_accesses.select(&:new_record?).each do |na|
90+
next if URI::MailTo::EMAIL_REGEXP.match?(na.email)
91+
92+
errors.add(:access_limited_named_users, "#{na.email} is not a valid email address")
93+
end
94+
end
95+
96+
def destroy_named_accesses_unless_named_users
97+
return if named_users?
98+
99+
named_accesses.each(&:mark_for_destruction)
100+
end
101+
102+
def ensure_creator_in_named_accesses
103+
return if creator&.email.blank?
104+
105+
email = creator.email.downcase
106+
return if active_named_accesses.any? { |na| na.email.casecmp?(email) }
107+
108+
named_accesses.build(email:)
109+
end
41110
end

app/services/draft_edition_updater.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def verb
2424
private
2525

2626
def should_check_current_user_will_retain_access?
27-
@options[:current_user].present? && edition.access_limited?
27+
@options[:current_user].present? && edition.organisations?
2828
end
2929

3030
def access_limit_excludes_current_user?
Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,41 @@
11
<div class="govuk-!-margin-bottom-6">
2-
<%= render "govuk_publishing_components/components/fieldset", {
3-
legend_text: "Limit access",
4-
heading_level: 3,
5-
heading_size: "m",
6-
} do %>
7-
<%= form.hidden_field :access_limited, value: "disabled" %>
8-
9-
<%= render "govuk_publishing_components/components/checkboxes", {
2+
<% named_users_value = edition.named_users? ? edition.access_limited_named_users.presence : nil %>
3+
<% named_users_value ||= current_user.email %>
4+
<% named_users_error = edition.errors[:access_limited_named_users].first %>
5+
<%= render "govuk_publishing_components/components/radio", {
6+
heading: "Limit access",
107
name: "edition[access_limited]",
118
id: "edition_access_limited",
12-
error_items: errors_for(edition.errors, :access_limited),
139
items: [
1410
{
15-
label: "Limit access to publishers from organisations associated with this document before you publish",
16-
value: "organisations",
17-
checked: edition.access_limited?,
11+
value: :disabled,
12+
text: "No – This document should be available to all publishers",
13+
bold: true,
14+
checked: edition.disabled?,
15+
},
16+
{
17+
value: :organisations,
18+
text: "Limit access to publishers from organisations associated with this document",
19+
bold: true,
20+
checked: edition.organisations?,
21+
},
22+
{
23+
value: :named_users,
24+
text: "Limit access to named publishers",
25+
bold: true,
26+
checked: edition.named_users?,
27+
conditional: (render "govuk_publishing_components/components/textarea", {
28+
label: {
29+
text: "Add publishers who will have access",
30+
bold: true,
31+
},
32+
name: "edition[access_limited_named_users]",
33+
textarea_id: "edition_access_limited_named_users",
34+
error_message: named_users_error,
35+
value: named_users_value,
36+
hint: "Add the emails of the publishers who will have access to this document before publishing. After publishing the document will be available to all publishers in the organisation associated with this document.",
37+
}),
1838
},
1939
],
2040
} %>
21-
<% end %>
2241
</div>

db/schema.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,6 @@
357357

358358
create_table "editions", id: :integer, charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
359359
t.integer "access_limited", default: 0, null: false
360-
t.integer "accessible_by", default: 0, null: false
361360
t.string "additional_related_mainstream_content_title"
362361
t.string "additional_related_mainstream_content_url"
363362
t.boolean "all_nation_applicability", default: true
@@ -625,7 +624,7 @@
625624
t.integer "edition_id"
626625
t.integer "image_data_id"
627626
t.datetime "updated_at", precision: nil
628-
t.string "usage"
627+
t.string "usage", null: false
629628
t.index ["edition_id"], name: "index_images_on_edition_id"
630629
t.index ["image_data_id"], name: "index_images_on_image_data_id"
631630
end

lib/whitehall/authority/rules/edition_rules.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,12 @@ def can_with_a_historic_instance?(action)
9393
end
9494

9595
def access_limit_enforced?
96-
if subject.access_limited?
96+
if subject.organisations?
9797
organisations = subject.organisations
9898
organisations += subject.edition_organisations.map(&:organisation) if subject.respond_to?(:edition_organisations)
9999
organisations.exclude?(actor.organisation)
100+
elsif subject.named_users?
101+
subject.named_accesses.none? { |na| na.email.casecmp?(actor.email) }
100102
else
101103
false
102104
end

test/factories/named_accesses.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FactoryBot.define do
2+
factory :named_access do
3+
association :edition
4+
email { generate(:email) }
5+
end
6+
end

test/functional/admin/edition_access_limited_controller_test.rb

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class Admin::EditionAccessLimitedControllerTest < ActionController::TestCase
2727
get :edit, params: { id: edition }
2828

2929
assert_select "form[action='#{update_access_limited_admin_edition_path(edition.id)}']" do
30-
assert_select "input[name='edition[access_limited]'][type=checkbox][checked=checked]"
30+
assert_select "input[name='edition[access_limited]'][type=radio][value='organisations'][checked=checked]"
3131
assert_select "textarea[name='edition[editorial_remark]']"
3232

3333
(1..4).each do |i|
@@ -147,6 +147,31 @@ class Admin::EditionAccessLimitedControllerTest < ActionController::TestCase
147147
assert edition.reload.access_limited?
148148
end
149149

150+
test "PATCH :update updates named_users access and creates an editorial remark" do
151+
edition = create(
152+
:consultation,
153+
access_limited: :disabled,
154+
)
155+
156+
editorial_remark = "Limiting to named users."
157+
158+
put :update,
159+
params: {
160+
id: edition,
161+
edition: {
162+
access_limited: :named_users,
163+
access_limited_named_users: "named@example.com",
164+
editorial_remark:,
165+
},
166+
}
167+
168+
assert edition.reload.named_users?
169+
assert_includes edition.named_accesses.pluck(:email), "named@example.com"
170+
assert_redirected_to admin_editions_path
171+
assert_equal "Access updated for #{edition.title}", flash[:notice]
172+
assert_equal "Access options updated by GDS Admin: #{editorial_remark}", edition.editorial_remarks.last.body
173+
end
174+
150175
test "PATCH :update doesn't create an editorial remark or re-render with an error when nothing has changed" do
151176
first_organisation = create(:organisation)
152177
second_organisation = create(:organisation)

test/integration/asset_access_options_integration_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class AssetAccessOptionsIntegrationTest < ActionDispatch::IntegrationTest
3434
context "when document is marked as access limited in Whitehall" do
3535
before do
3636
visit edit_admin_edition_path(edition)
37-
check "Limit access"
37+
choose "Limit access to publishers from organisations associated with this document"
3838
click_button "Save"
3939
assert_text "Your document has been saved"
4040
end
@@ -163,7 +163,7 @@ class AssetAccessOptionsIntegrationTest < ActionDispatch::IntegrationTest
163163
context "when document is unmarked as access limited in Whitehall" do
164164
before do
165165
visit edit_admin_edition_path(edition)
166-
uncheck "Limit access"
166+
choose "No – This document should be available to all publishers"
167167
click_button "Save"
168168
assert_text "Your document has been saved"
169169
end

0 commit comments

Comments
 (0)