From 0b0de69870996b6768a6910c62fbec4011284aa9 Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 29 Jul 2026 19:04:43 -0400 Subject: [PATCH 1/2] Gate taggings People/Organizations sections by index permission The taggings index is public, but People and Organizations both have admin-only index actions (index? => admin?). Non-admins saw those sections and a "View all" link that leads to an action they can't reach. Show each tagged section only when the viewer is allowed to index that model, so the browse page never surfaces results it can't let you open. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/controllers/taggings_controller.rb | 2 +- spec/requests/taggings_spec.rb | 49 ++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/app/controllers/taggings_controller.rb b/app/controllers/taggings_controller.rb index 60b4523b2c..653c5b08e4 100644 --- a/app/controllers/taggings_controller.rb +++ b/app/controllers/taggings_controller.rb @@ -26,7 +26,7 @@ def index category_names_all: @category_names_all, pages: pages, number_of_items_per_page: number_of_items_per_page - ) + ).select { |type, _items| allowed_to?(:index?, Tag::TAGGABLE_META.fetch(type)[:klass]) } @sectors = authorized_scope(Sector.all, as: :taggable).order(:name) @categories = authorized_scope(Category.all, as: :taggable) diff --git a/spec/requests/taggings_spec.rb b/spec/requests/taggings_spec.rb index 36af448060..711dfd0b2a 100644 --- a/spec/requests/taggings_spec.rb +++ b/spec/requests/taggings_spec.rb @@ -113,4 +113,53 @@ expect(response.body).to include("Art for Healing") end end + + describe "People and Organizations sections gated by index permission" do + let!(:active_status) { create(:organization_status, name: "Active") } + let!(:organization) do + create(:organization, name: "Healing Org", organization_status: active_status) + end + let!(:person) { create(:person, profile_is_searchable: true) } + + before do + # An active affiliation makes the org active (published) and the person + # visible via with_active_affiliations. + create(:affiliation, person: person, organization: organization) + create(:sectorable_item, sector: sector_1, sectorable: organization) + create(:sectorable_item, sector: sector_1, sectorable: person) + end + + context "as a regular signed-in user (cannot index people or organizations)" do + it "hides the People and Organizations sections but keeps allowed sections" do + get taggings_path(sector_names_all: sector_1.name) + + expect(response.body).not_to include("View all People results") + expect(response.body).not_to include("View all Organizations results") + expect(response.body).to include("Art for Healing") + end + end + + context "as a guest (cannot index people or organizations)" do + it "hides the People and Organizations sections" do + sign_out user + get taggings_path(sector_names_all: sector_1.name) + + expect(response.body).not_to include("View all People results") + expect(response.body).not_to include("View all Organizations results") + end + end + + context "as an admin (can index people and organizations)" do + let!(:admin) { create(:user, :admin) } + + before { sign_in admin } + + it "shows the People and Organizations sections" do + get taggings_path(sector_names_all: sector_1.name) + + expect(response.body).to include("View all People results") + expect(response.body).to include("View all Organizations results") + end + end + end end From 92c4f59b9f53d98fba716dd34d87240134a13602 Mon Sep 17 00:00:00 2001 From: maebeale Date: Wed, 29 Jul 2026 21:13:56 -0400 Subject: [PATCH 2/2] Add behavioral coverage for OrganizationPolicy non-admin scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The org scope spec only asserted SQL-string equality with Organization.published; it never created records. This is the "next gate" for when index? changes from admin? to user — non-admins will start seeing these results, so lock in the data-level behavior: active-affiliation orgs (and Active-by-status orgs) are included, inactive-only orgs are excluded. Mirrors the person scope coverage. Co-Authored-By: Claude Opus 4.8 (1M context) --- spec/policies/organization_policy_spec.rb | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/spec/policies/organization_policy_spec.rb b/spec/policies/organization_policy_spec.rb index a66e8d94aa..0bcaa20858 100644 --- a/spec/policies/organization_policy_spec.rb +++ b/spec/policies/organization_policy_spec.rb @@ -81,6 +81,27 @@ def policy_for(record: nil, user:) scope = policy.apply_scope(Organization.all, type: :active_record_relation) expect(scope.to_sql).to eq(Organization.published.to_sql) end + + it "includes organizations with an active affiliation and excludes those without one" do + regular = create(:user) + with_active_affiliation = create(:organization) + create(:affiliation, organization: with_active_affiliation, inactive: false, end_date: nil) + + # An expired affiliation isn't active, and the factory status is never + # "Active", so this org matches neither branch of `published`/`active`. + without_active_affiliation = create(:organization) + create(:affiliation, organization: without_active_affiliation, end_date: 1.day.ago) + + # The scope is broader than "has an active affiliation": an org flagged + # "Active" by status alone still qualifies, even with no affiliations. + active_by_status = create(:organization, organization_status: create(:organization_status, name: "Active")) + + policy = described_class.new(Organization, user: regular) + scope = policy.apply_scope(Organization.all, type: :active_record_relation) + + expect(scope).to include(with_active_affiliation, active_by_status) + expect(scope).not_to include(without_active_affiliation) + end end end end