diff --git a/app/controllers/concerns/api/articles_controller.rb b/app/controllers/concerns/api/articles_controller.rb index 4120256c3..34320eaad 100644 --- a/app/controllers/concerns/api/articles_controller.rb +++ b/app/controllers/concerns/api/articles_controller.rb @@ -109,6 +109,9 @@ module Api authorize @article, :revoke_publication? if Articles::Unpublish.call(@user, @article) + payload = { action: "api_article_unpublish", article_id: @article.id } + Audit::Logger.log(:admin_api, @user, payload) + render head: :ok else render json: { message: @article.errors.full_messages }, status: :unprocessable_entity diff --git a/app/controllers/concerns/api/users_controller.rb b/app/controllers/concerns/api/users_controller.rb index 284d1c735..918526dd5 100644 --- a/app/controllers/concerns/api/users_controller.rb +++ b/app/controllers/concerns/api/users_controller.rb @@ -32,6 +32,10 @@ module Api Moderator::ManageActivityAndRoles.handle_user_roles(admin: @user, user: target_user, user_params: suspend_params) + + payload = { action: "api_user_suspend", target_user_id: target_user.id } + Audit::Logger.log(:admin_api, @user, payload) + render head: :ok rescue StandardError render json: { @@ -43,7 +47,17 @@ module Api def unpublish authorize(@user, :unpublish_all_articles?) + target_articles = Article.published.where(user_id: params[:id].to_i) + Moderator::UnpublishAllArticlesWorker.perform_async(params[:id].to_i) + + payload = { + action: "api_user_unpublish", + target_user_id: params[:id].to_i, + target_article_ids: target_articles.ids + } + Audit::Logger.log(:admin_api, @user, payload) + render head: :ok end end diff --git a/app/models/audit_log.rb b/app/models/audit_log.rb index 9a2f33662..86514c3d1 100644 --- a/app/models/audit_log.rb +++ b/app/models/audit_log.rb @@ -4,4 +4,5 @@ class AuditLog < ApplicationRecord validates :data, presence: true MODERATOR_AUDIT_LOG_CATEGORY = "moderator.audit.log".freeze + ADMIN_API_AUDIT_LOG_CATEGORY = "admin_api.audit.log".freeze end diff --git a/config/initializers/audit_events.rb b/config/initializers/audit_events.rb index 574e0bf88..252cc3af0 100644 --- a/config/initializers/audit_events.rb +++ b/config/initializers/audit_events.rb @@ -7,5 +7,5 @@ # Audit::Subscribe.listen :internal, :quest_user Rails.application.reloader.to_prepare do - Audit::Subscribe.listen(:moderator, :internal) unless Rails.env.test? + Audit::Subscribe.listen(:moderator, :internal, :admin_api) unless Rails.env.test? end diff --git a/spec/requests/api/v1/articles_spec.rb b/spec/requests/api/v1/articles_spec.rb index d3fe980d2..4fbaa87bb 100644 --- a/spec/requests/api/v1/articles_spec.rb +++ b/spec/requests/api/v1/articles_spec.rb @@ -6,6 +6,7 @@ RSpec.describe "Api::V1::Articles", type: :request do let(:article) { create(:article, featured: true, tags: "discuss") } let(:new_article) { create(:article) } let(:api_secret) { create(:api_secret) } + let(:listener) { :admin_api } let(:v1_headers) do { "content-type" => "application/json", @@ -1184,11 +1185,13 @@ RSpec.describe "Api::V1::Articles", type: :request do end end - describe "PUT /api/articles/:id/unpublish" do + describe "PUT /api/articles/:id/unpublish", :aggregate_failures do let(:user) { api_secret.user } let!(:published_article) { create(:article, published: true) } let(:path) { api_article_unpublish_path(published_article.id) } + before { Audit::Subscribe.listen listener } + context "when unauthorized" do it "fails with no api key" do put path, headers: { "content-type" => "application/json", "Accept" => "application/vnd.forem.api-v1+json" } @@ -1228,6 +1231,16 @@ RSpec.describe "Api::V1::Articles", type: :request do expect(response).to have_http_status(:ok) expect(published_article.reload.published).to be false end + + it "creates an audit log of the action taken" do + put path, headers: v1_headers + + log = AuditLog.last + expect(log.category).to eq(AuditLog::ADMIN_API_AUDIT_LOG_CATEGORY) + expect(log.data["action"]).to eq("api_article_unpublish") + expect(log.data["article_id"]).to eq(published_article.id) + expect(log.user_id).to eq(user.id) + end end end end diff --git a/spec/requests/api/v1/users_spec.rb b/spec/requests/api/v1/users_spec.rb index e89da77ef..b6d7482b2 100644 --- a/spec/requests/api/v1/users_spec.rb +++ b/spec/requests/api/v1/users_spec.rb @@ -3,6 +3,7 @@ require "rails_helper" RSpec.describe "Api::V0::Users", type: :request do let(:api_secret) { create(:api_secret) } let(:v1_headers) { { "api-key" => api_secret.secret, "Accept" => "application/vnd.forem.api-v1+json" } } + let(:listener) { :admin_api } describe "GET /api/users/:id" do before { allow(FeatureFlag).to receive(:enabled?).with(:api_v1).and_return(true) } @@ -143,11 +144,14 @@ RSpec.describe "Api::V0::Users", type: :request do end end - describe "PUT /api/users/:id/suspend" do + describe "PUT /api/users/:id/suspend", :aggregate_failures do let(:target_user) { create(:user) } let(:payload) { { note: "Violated CoC despite multiple warnings" } } - before { allow(FeatureFlag).to receive(:enabled?).with(:api_v1).and_return(true) } + before do + allow(FeatureFlag).to receive(:enabled?).with(:api_v1).and_return(true) + Audit::Subscribe.listen listener + end context "when unauthenticated" do it "returns unauthorized" do @@ -178,9 +182,9 @@ RSpec.describe "Api::V0::Users", type: :request do end context "when request is authenticated" do - it "is successful in suspending a user", :aggregate_failures do - api_secret.user.add_role(:super_admin) + before { api_secret.user.add_role(:super_admin) } + it "is successful in suspending a user", :aggregate_failures do expect do put api_user_suspend_path(id: target_user.id), params: payload, @@ -191,13 +195,29 @@ RSpec.describe "Api::V0::Users", type: :request do expect(Note.last.content).to eq(payload[:note]) end.to change(Note, :count).by(1) end + + it "creates an audit log of the action taken" do + put api_user_suspend_path(id: target_user.id), + params: payload, + headers: v1_headers + + log = AuditLog.last + expect(log.category).to eq(AuditLog::ADMIN_API_AUDIT_LOG_CATEGORY) + expect(log.data["action"]).to eq("api_user_suspend") + expect(log.data["target_user_id"]).to eq(target_user.id) + expect(log.user_id).to eq(api_secret.user.id) + end end end - describe "PUT /api/users/:id/unpublish" do + describe "PUT /api/users/:id/unpublish", :aggregate_failures do let(:target_user) { create(:user) } + let!(:target_articles) { create_list(:article, 3, user_id: target_user.id) } - before { allow(FeatureFlag).to receive(:enabled?).with(:api_v1).and_return(true) } + before do + allow(FeatureFlag).to receive(:enabled?).with(:api_v1).and_return(true) + Audit::Subscribe.listen listener + end context "when unauthenticated" do it "returns unauthorized" do @@ -225,15 +245,29 @@ RSpec.describe "Api::V0::Users", type: :request do end context "when request is authenticated" do - it "is successful in unpublishing a user's comments and articles", :aggregate_failures do + before do allow(Moderator::UnpublishAllArticlesWorker).to receive(:perform_async) api_secret.user.add_role(:super_admin) + end + + it "is successful in unpublishing a user's comments and articles", :aggregate_failures do put api_user_unpublish_path(id: target_user.id), headers: v1_headers expect(response).to have_http_status(:ok) expect(Moderator::UnpublishAllArticlesWorker).to have_received(:perform_async).with(target_user.id).once end + + it "creates an audit log of the action taken" do + put api_user_unpublish_path(id: target_user.id), + headers: v1_headers + + log = AuditLog.last + expect(log.category).to eq(AuditLog::ADMIN_API_AUDIT_LOG_CATEGORY) + expect(log.data["action"]).to eq("api_user_unpublish") + expect(log.data["target_article_ids"]).to match_array(target_articles.map(&:id)) + expect(log.user_id).to eq(api_secret.user.id) + end end end end