From d870b0815a59b2289b4fc3c91105627a9cdbaf15 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Wed, 1 Mar 2023 20:02:58 +0530 Subject: [PATCH] feat: Audit log APIs (#6434) - Adds the appropriate APIs for Audit Logs. ref: #6015 --- Gemfile | 2 + Gemfile.lock | 3 ++ app/models/account.rb | 1 + app/models/automation_rule.rb | 2 + app/models/inbox.rb | 1 + app/models/webhook.rb | 2 + config/initializers/audited.rb | 5 +++ config/routes.rb | 1 + db/migrate/20230202132107_install_audited.rb | 34 ++++++++++++++ db/schema.rb | 22 ++++++++++ .../api/v1/accounts/audit_logs_controller.rb | 15 +++++++ .../app/models/enterprise/audit/account.rb | 7 +++ .../enterprise/audit/automation_rule.rb | 7 +++ .../app/models/enterprise/audit/inbox.rb | 7 +++ .../app/models/enterprise/audit/webhook.rb | 7 +++ enterprise/app/models/enterprise/audit_log.rb | 11 +++++ .../v1/accounts/audit_logs_controller_spec.rb | 44 +++++++++++++++++++ spec/enterprise/models/account_spec.rb | 7 +++ .../enterprise/models/automation_rule_spec.rb | 29 ++++++++++++ spec/enterprise/models/inbox_spec.rb | 25 ++++++++++- spec/enterprise/models/webhook_spec.rb | 30 +++++++++++++ 21 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 config/initializers/audited.rb create mode 100644 db/migrate/20230202132107_install_audited.rb create mode 100644 enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb create mode 100644 enterprise/app/models/enterprise/audit/account.rb create mode 100644 enterprise/app/models/enterprise/audit/automation_rule.rb create mode 100644 enterprise/app/models/enterprise/audit/inbox.rb create mode 100644 enterprise/app/models/enterprise/audit/webhook.rb create mode 100644 enterprise/app/models/enterprise/audit_log.rb create mode 100644 spec/enterprise/controllers/api/v1/accounts/audit_logs_controller_spec.rb create mode 100644 spec/enterprise/models/automation_rule_spec.rb create mode 100644 spec/enterprise/models/webhook_spec.rb diff --git a/Gemfile b/Gemfile index 7aca0710d..a5abc1d90 100644 --- a/Gemfile +++ b/Gemfile @@ -205,6 +205,8 @@ end # worked with microsoft refresh token gem 'omniauth-oauth2' +gem 'audited', '~> 5.2' + # need for google auth gem 'omniauth' gem 'omniauth-google-oauth2' diff --git a/Gemfile.lock b/Gemfile.lock index e64b6b7b3..2664d64b2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -90,6 +90,8 @@ GEM rake (>= 10.4, < 14.0) ast (2.4.2) attr_extras (6.2.5) + audited (5.2.0) + activerecord (>= 5.0, < 7.1) aws-eventstream (1.2.0) aws-partitions (1.605.0) aws-sdk-core (3.131.2) @@ -763,6 +765,7 @@ DEPENDENCIES administrate annotate attr_extras + audited (~> 5.2) aws-sdk-s3 azure-storage-blob barnes diff --git a/app/models/account.rb b/app/models/account.rb index 7c7336642..eadd145bf 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -155,3 +155,4 @@ class Account < ApplicationRecord end Account.prepend_mod_with('Account') +Account.include_mod_with('Audit::Account') diff --git a/app/models/automation_rule.rb b/app/models/automation_rule.rb index 8af2a4aab..36f2b833a 100644 --- a/app/models/automation_rule.rb +++ b/app/models/automation_rule.rb @@ -76,3 +76,5 @@ class AutomationRule < ApplicationRecord errors.add(:conditions, 'Automation conditions should have query operator.') if operators.length > 1 end end + +AutomationRule.include_mod_with('Audit::Inbox') diff --git a/app/models/inbox.rb b/app/models/inbox.rb index c015b7835..0c0204da1 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -158,3 +158,4 @@ class Inbox < ApplicationRecord end Inbox.prepend_mod_with('Inbox') +Inbox.include_mod_with('Audit::Inbox') diff --git a/app/models/webhook.rb b/app/models/webhook.rb index 12d2f0dbf..f43c80548 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -37,3 +37,5 @@ class Webhook < ApplicationRecord errors.add(:subscriptions, I18n.t('errors.webhook.invalid')) if invalid_subscriptions end end + +Webhook.include_mod_with('Audit::Inbox') diff --git a/config/initializers/audited.rb b/config/initializers/audited.rb new file mode 100644 index 000000000..e6041c7d6 --- /dev/null +++ b/config/initializers/audited.rb @@ -0,0 +1,5 @@ +# configuration related audited gem : https://github.com/collectiveidea/audited + +Audited.config do |config| + config.audit_class = 'Enterprise::AuditLog' +end diff --git a/config/routes.rb b/config/routes.rb index d318d6fa8..3f1a4d9bb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -45,6 +45,7 @@ Rails.application.routes.draw do resources :agents, only: [:index, :create, :update, :destroy] resources :agent_bots, only: [:index, :create, :show, :update, :destroy] resources :assignable_agents, only: [:index] + resource :audit_logs, only: [:show] resources :callbacks, only: [] do collection do post :register_facebook_page diff --git a/db/migrate/20230202132107_install_audited.rb b/db/migrate/20230202132107_install_audited.rb new file mode 100644 index 000000000..0bb6da79c --- /dev/null +++ b/db/migrate/20230202132107_install_audited.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class InstallAudited < ActiveRecord::Migration[6.1] + # rubocop:disable RMetrics/MethodLength + def self.up + create_table :audits, :force => true do |t| + t.bigint :auditable_id + t.string :auditable_type + t.bigint :associated_id + t.string :associated_type + t.bigint :user_id + t.string :user_type + t.string :username + t.string :action + t.jsonb :audited_changes + t.integer :version, :integer, :default => 0 + t.string :comment + t.string :remote_address + t.string :request_uuid + t.datetime :created_at + end + # rubocop:enable RMetrics/MethodLength + + add_index :audits, [:auditable_type, :auditable_id, :version], :name => 'auditable_index' + add_index :audits, [:associated_type, :associated_id], :name => 'associated_index' + add_index :audits, [:user_id, :user_type], :name => 'user_index' + add_index :audits, :request_uuid + add_index :audits, :created_at + end + + def self.down + drop_table :audits + end +end diff --git a/db/schema.rb b/db/schema.rb index a9b7217ca..d9a209b95 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -153,6 +153,28 @@ ActiveRecord::Schema.define(version: 2023_02_24_124632) do t.index ["message_id"], name: "index_attachments_on_message_id" end + create_table 'audits', force: :cascade do |t| + t.bigint 'auditable_id' + t.string 'auditable_type' + t.bigint 'associated_id' + t.string 'associated_type' + t.bigint 'user_id' + t.string 'user_type' + t.string 'username' + t.string 'action' + t.jsonb 'audited_changes' + t.integer 'version', default: 0 + t.string 'comment' + t.string 'remote_address' + t.string 'request_uuid' + t.datetime 'created_at' + t.index %w[associated_type associated_id], name: 'associated_index' + t.index %w[auditable_type auditable_id version], name: 'auditable_index' + t.index ['created_at'], name: 'index_audits_on_created_at' + t.index ['request_uuid'], name: 'index_audits_on_request_uuid' + t.index %w[user_id user_type], name: 'user_index' + end + create_table "automation_rules", force: :cascade do |t| t.bigint "account_id", null: false t.string "name", null: false diff --git a/enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb b/enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb new file mode 100644 index 000000000..02db5156b --- /dev/null +++ b/enterprise/app/controllers/api/v1/accounts/audit_logs_controller.rb @@ -0,0 +1,15 @@ +# module Enterprise::Api::V1::Accounts::AuditLogsController < Api::V1::Accounts::BaseController +class Api::V1::Accounts::AuditLogsController < Api::V1::Accounts::BaseController + before_action :check_admin_authorization? + before_action :fetch_audit + + def show + render json: @audit_logs + end + + private + + def fetch_audit + @audit_logs = Current.account.associated_audits + end +end diff --git a/enterprise/app/models/enterprise/audit/account.rb b/enterprise/app/models/enterprise/audit/account.rb new file mode 100644 index 000000000..c9a1f0f13 --- /dev/null +++ b/enterprise/app/models/enterprise/audit/account.rb @@ -0,0 +1,7 @@ +module Enterprise::Audit::Account + extend ActiveSupport::Concern + + included do + has_associated_audits + end +end diff --git a/enterprise/app/models/enterprise/audit/automation_rule.rb b/enterprise/app/models/enterprise/audit/automation_rule.rb new file mode 100644 index 000000000..d40f950bd --- /dev/null +++ b/enterprise/app/models/enterprise/audit/automation_rule.rb @@ -0,0 +1,7 @@ +module Enterprise::Audit::AutomationRule + extend ActiveSupport::Concern + + included do + audited associated_with: :account + end +end diff --git a/enterprise/app/models/enterprise/audit/inbox.rb b/enterprise/app/models/enterprise/audit/inbox.rb new file mode 100644 index 000000000..244e5b60c --- /dev/null +++ b/enterprise/app/models/enterprise/audit/inbox.rb @@ -0,0 +1,7 @@ +module Enterprise::Audit::Inbox + extend ActiveSupport::Concern + + included do + audited associated_with: :account + end +end diff --git a/enterprise/app/models/enterprise/audit/webhook.rb b/enterprise/app/models/enterprise/audit/webhook.rb new file mode 100644 index 000000000..34e0bcc1b --- /dev/null +++ b/enterprise/app/models/enterprise/audit/webhook.rb @@ -0,0 +1,7 @@ +module Enterprise::Audit::Webhook + extend ActiveSupport::Concern + + included do + audited associated_with: :account + end +end diff --git a/enterprise/app/models/enterprise/audit_log.rb b/enterprise/app/models/enterprise/audit_log.rb new file mode 100644 index 000000000..49188110e --- /dev/null +++ b/enterprise/app/models/enterprise/audit_log.rb @@ -0,0 +1,11 @@ +class Enterprise::AuditLog < Audited::Audit + after_save :log_additional_information + + private + + def log_additional_information + # rubocop:disable Rails/SkipsModelValidations + update_columns(username: user&.email) + # rubocop:enable Rails/SkipsModelValidations + end +end diff --git a/spec/enterprise/controllers/api/v1/accounts/audit_logs_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/audit_logs_controller_spec.rb new file mode 100644 index 000000000..3d7b2e0e4 --- /dev/null +++ b/spec/enterprise/controllers/api/v1/accounts/audit_logs_controller_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' + +RSpec.describe 'Enterprise Audit API', type: :request do + let!(:account) { create(:account) } + let!(:inbox) { create(:inbox, account: account) } + let!(:admin) { create(:user, account: account, role: :administrator) } + + describe 'GET /api/v1/accounts/{account.id}/audit_logs' do + context 'when it is an un-authenticated user' do + it 'does not fetch audit logs associated with the account' do + get "/api/v1/accounts/#{account.id}/audit_logs", + as: :json + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated normal user' do + let(:user) { create(:user, account: account) } + + it 'fetches audit logs associated with the account' do + get "/api/v1/accounts/#{account.id}/audit_logs", + headers: user.create_new_auth_token, + as: :json + expect(response).to have_http_status(:unauthorized) + end + end + + # check for response in parse + context 'when it is an authenticated admin user' do + it 'fetches audit logs associated with the account' do + get "/api/v1/accounts/#{account.id}/audit_logs", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + json_response = JSON.parse(response.body) + expect(json_response[0]['auditable_type']).to eql('Inbox') + expect(json_response[0]['action']).to eql('create') + expect(json_response[0]['audited_changes']['name']).to eql(inbox.name) + expect(json_response[0]['associated_id']).to eql(account.id) + end + end + end +end diff --git a/spec/enterprise/models/account_spec.rb b/spec/enterprise/models/account_spec.rb index feb87951b..4c84e3465 100644 --- a/spec/enterprise/models/account_spec.rb +++ b/spec/enterprise/models/account_spec.rb @@ -10,6 +10,13 @@ RSpec.describe Account do let!(:account) { create(:account) } + describe 'audit logs' do + it 'returns audit logs' do + # checking whether associated_audits method is present + expect(account.associated_audits.present?).to be false + end + end + it 'returns max limits from global config when enterprise version' do expect(account.usage_limits).to eq( { diff --git a/spec/enterprise/models/automation_rule_spec.rb b/spec/enterprise/models/automation_rule_spec.rb new file mode 100644 index 000000000..69e7949b7 --- /dev/null +++ b/spec/enterprise/models/automation_rule_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe AutomationRule do + let!(:automation_rule) { create(:automation_rule, name: 'automation rule 1') } + + describe 'audit log' do + context 'when automation rule is created' do + it 'has associated audit log created' do + expect(Audited::Audit.where(auditable_type: 'AutomationRule', action: 'create').count).to eq 1 + end + end + + context 'when automation rule is updated' do + it 'has associated audit log created' do + automation_rule.update(name: 'automation rule 2') + expect(Audited::Audit.where(auditable_type: 'AutomationRule', action: 'update').count).to eq 1 + end + end + + context 'when automation rule is deleted' do + it 'has associated audit log created' do + automation_rule.destroy! + expect(Audited::Audit.where(auditable_type: 'AutomationRule', action: 'destroy').count).to eq 1 + end + end + end +end diff --git a/spec/enterprise/models/inbox_spec.rb b/spec/enterprise/models/inbox_spec.rb index 26dbcef16..259c24b87 100644 --- a/spec/enterprise/models/inbox_spec.rb +++ b/spec/enterprise/models/inbox_spec.rb @@ -3,8 +3,9 @@ require 'rails_helper' RSpec.describe Inbox do + let!(:inbox) { create(:inbox) } + describe 'member_ids_with_assignment_capacity' do - let!(:inbox) { create(:inbox) } let!(:inbox_member_1) { create(:inbox_member, inbox: inbox) } let!(:inbox_member_2) { create(:inbox_member, inbox: inbox) } let!(:inbox_member_3) { create(:inbox_member, inbox: inbox) } @@ -35,4 +36,26 @@ RSpec.describe Inbox do expect(inbox.member_ids_with_assignment_capacity).to eq(inbox.members.ids) end end + + describe 'audit log' do + context 'when inbox is created' do + it 'has associated audit log created' do + expect(Audited::Audit.where(auditable_type: 'Inbox', action: 'create').count).to eq 1 + end + end + + context 'when inbox is updated' do + it 'has associated audit log created' do + inbox.update(auto_assignment_config: { max_assignment_limit: 2 }) + expect(Audited::Audit.where(auditable_type: 'Inbox', action: 'update').count).to eq 1 + end + end + + context 'when inbox is deleted' do + it 'has associated audit log created' do + inbox.destroy! + expect(Audited::Audit.where(auditable_type: 'Inbox', action: 'destroy').count).to eq 1 + end + end + end end diff --git a/spec/enterprise/models/webhook_spec.rb b/spec/enterprise/models/webhook_spec.rb new file mode 100644 index 000000000..ac7d23384 --- /dev/null +++ b/spec/enterprise/models/webhook_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Webhook do + let(:account) { create(:account) } + let!(:webhook) { create(:webhook, account: account) } + + describe 'audit log' do + context 'when webhook is created' do + it 'has associated audit log created' do + expect(Audited::Audit.where(auditable_type: 'Webhook', action: 'create').count).to eq 1 + end + end + + context 'when webhook is updated' do + it 'has associated audit log created' do + webhook.update(url: 'https://example.com') + expect(Audited::Audit.where(auditable_type: 'Webhook', action: 'update').count).to eq 1 + end + end + + context 'when webhook is deleted' do + it 'has associated audit log created' do + webhook.destroy! + expect(Audited::Audit.where(auditable_type: 'Webhook', action: 'destroy').count).to eq 1 + end + end + end +end