Skip to content

Commit

Permalink
Add audit event system
Browse files Browse the repository at this point in the history
  • Loading branch information
Taucher2003 committed Dec 29, 2023
1 parent aa9a461 commit 00245d1
Show file tree
Hide file tree
Showing 28 changed files with 493 additions and 83 deletions.
4 changes: 3 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ GraphQL/ExtractInputType:
Lint/AmbiguousBlockAssociation:
AllowedMethods: [change]


Metrics/AbcSize:
Enabled: false

Expand Down Expand Up @@ -50,6 +49,9 @@ RSpec/ExampleLength:
RSpec/ExpectChange:
EnforcedStyle: block

RSpec/ImplicitSubject:
EnforcedStyle: require_implicit

RSpec/MultipleMemoizedHelpers:
Enabled: false

Expand Down
2 changes: 1 addition & 1 deletion app/graphql/mutations/users/register.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class Register < BaseMutation
argument :username, String, required: true, description: 'Username of the user'

def resolve(username:, email:, password:)
UserCreateService.new(username, email, password).execute.to_mutation_response(success_key: :user)
UserRegisterService.new(username, email, password).execute.to_mutation_response(success_key: :user)
end
end
end
Expand Down
28 changes: 28 additions & 0 deletions app/models/audit_event.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

class AuditEvent < ApplicationRecord
ACTION_TYPES = {
user_registered: 1,
user_logged_in: 2,
}.with_indifferent_access

enum :action_type, ACTION_TYPES, prefix: :action

belongs_to :author, class_name: 'User', inverse_of: :authored_audit_events

validates :entity_id, presence: true
validates :entity_type, presence: true
validates :action_type, presence: true,
inclusion: {
in: ACTION_TYPES.keys.map(&:to_s),
}
validate :validate_details
validates :target_id, presence: true
validates :target_type, presence: true

def validate_details
errors.add(:details, :blank) if details.nil?

errors.add(:details, :invalid) unless details.is_a?(Hash)
end
end
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ class User < ApplicationRecord
validates :lastname, length: { maximum: 50 }

has_many :user_sessions, inverse_of: :user
has_many :authored_audit_events, class_name: 'AuditEvent', inverse_of: :author
end
39 changes: 39 additions & 0 deletions app/services/audit_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module AuditService
module_function

REQUIRED_EVENT_KEYS = %i[author_id entity_id entity_type details target_id target_type].freeze

def audit(type, payload)
Sagittarius::Context.with_context do |context|
payload[:author_id] ||= context[:user][:id]
payload[:ip_address] = context[:ip_address]

if payload.key?(:entity)
entity = payload.delete(:entity)
payload[:entity_id] = entity.id
payload[:entity_type] = entity.class.name
end

if payload.key?(:target)
target = payload.delete(:target)
payload[:target_id] = target.id
payload[:target_type] = target.class.name
end

missing_keys = REQUIRED_EVENT_KEYS.reject do |key|
payload.key?(key)
end

raise InvalidAuditEvent, "Audit Event is missing the #{missing_keys} attributes" unless missing_keys.empty?

AuditEvent.create!(
action_type: type,
**payload
)
end
end

class InvalidAuditEvent < StandardError; end
end
21 changes: 0 additions & 21 deletions app/services/user_create_service.rb

This file was deleted.

27 changes: 19 additions & 8 deletions app/services/user_login_service.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

class UserLoginService
include Sagittarius::Database::Transactional
include Sagittarius::Loggable

attr_reader :args
Expand All @@ -16,14 +17,24 @@ def execute
return ServiceResponse.error(message: 'Invalid login data', payload: :invalid_login_data)
end

user_session = UserSession.create(user: user)
unless user_session.valid?
logger.warn(message: 'Failed to create valid session for user', user_id: user.id, username: user.username)
return ServiceResponse.error(message: 'UserSession is invalid',
payload: user_session.errors)
end
transactional do
user_session = UserSession.create(user: user)
unless user_session.persisted?
logger.warn(message: 'Failed to create valid session for user', user_id: user.id, username: user.username)
return ServiceResponse.error(message: 'UserSession is invalid',
payload: user_session.errors)
end

AuditService.audit(
:user_logged_in,
author_id: user.id,
entity: user,
details: args.slice(:username, :email),
target: user
)

logger.info(message: 'Login to user', user_id: user.id, username: user.username)
ServiceResponse.success(payload: user_session)
logger.info(message: 'Login to user', user_id: user.id, username: user.username)
ServiceResponse.success(payload: user_session)
end
end
end
32 changes: 32 additions & 0 deletions app/services/user_register_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

class UserRegisterService
include Sagittarius::Database::Transactional
include Sagittarius::Loggable

attr_reader :username, :email, :password

def initialize(username, email, password)
@username = username
@email = email
@password = password
end

def execute
transactional do
user = User.create(username: username, email: email, password: password)
return ServiceResponse.error(message: 'User is invalid', payload: user.errors) unless user.persisted?

AuditService.audit(
:user_registered,
author_id: user.id,
entity: user,
details: { username: username, email: email },
target: user
)

logger.info(message: 'Created new user', user_id: user.id, username: user.username)
ServiceResponse.success(payload: user)
end
end
end
7 changes: 0 additions & 7 deletions config/initializers/context.rb

This file was deleted.

9 changes: 9 additions & 0 deletions config/initializers/middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

require_relative '../../lib/sagittarius/context'
require_relative '../../lib/sagittarius/middleware/rack/context'
require_relative '../../lib/sagittarius/middleware/rack/ip_address'

Rails.application.config.middleware.move(1, ActionDispatch::RequestId)
Rails.application.config.middleware.insert(1, Sagittarius::Middleware::Rack::Context)
Rails.application.config.middleware.insert_after(ActionDispatch::RemoteIp, Sagittarius::Middleware::Rack::IpAddress)
18 changes: 18 additions & 0 deletions db/migrate/20231226235332_create_audit_events.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

class CreateAuditEvents < Sagittarius::Database::Migration[1.0]
def change
create_table :audit_events do |t|
t.references :author, null: false, foreign_key: { to_table: :users }
t.integer :entity_id, null: false
t.text :entity_type, null: false
t.integer :action_type, null: false
t.jsonb :details, null: false
t.inet :ip_address
t.integer :target_id, null: false
t.text :target_type, null: false

t.timestamps_with_timezone
end
end
end
1 change: 1 addition & 0 deletions db/schema_migrations/20231226235332
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2a886db15701ff25494a5a3b7aad16f18a4db7831cfaa681d5265093b2d989b7
33 changes: 33 additions & 0 deletions db/structure.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ CREATE TABLE ar_internal_metadata (
updated_at timestamp(6) without time zone NOT NULL
);

CREATE TABLE audit_events (
id bigint NOT NULL,
author_id bigint NOT NULL,
entity_id integer NOT NULL,
entity_type text NOT NULL,
action_type integer NOT NULL,
details jsonb NOT NULL,
ip_address inet,
target_id integer NOT NULL,
target_type text NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL
);

CREATE SEQUENCE audit_events_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;

ALTER SEQUENCE audit_events_id_seq OWNED BY audit_events.id;

CREATE TABLE schema_migrations (
version character varying NOT NULL
);
Expand Down Expand Up @@ -51,13 +74,18 @@ CREATE SEQUENCE users_id_seq

ALTER SEQUENCE users_id_seq OWNED BY users.id;

ALTER TABLE ONLY audit_events ALTER COLUMN id SET DEFAULT nextval('audit_events_id_seq'::regclass);

ALTER TABLE ONLY user_sessions ALTER COLUMN id SET DEFAULT nextval('user_sessions_id_seq'::regclass);

ALTER TABLE ONLY users ALTER COLUMN id SET DEFAULT nextval('users_id_seq'::regclass);

ALTER TABLE ONLY ar_internal_metadata
ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key);

ALTER TABLE ONLY audit_events
ADD CONSTRAINT audit_events_pkey PRIMARY KEY (id);

ALTER TABLE ONLY schema_migrations
ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);

Expand All @@ -67,6 +95,8 @@ ALTER TABLE ONLY user_sessions
ALTER TABLE ONLY users
ADD CONSTRAINT users_pkey PRIMARY KEY (id);

CREATE INDEX index_audit_events_on_author_id ON audit_events USING btree (author_id);

CREATE UNIQUE INDEX index_user_sessions_on_token ON user_sessions USING btree (token);

CREATE INDEX index_user_sessions_on_user_id ON user_sessions USING btree (user_id);
Expand All @@ -77,3 +107,6 @@ CREATE UNIQUE INDEX "index_users_on_LOWER_username" ON users USING btree (lower(

ALTER TABLE ONLY user_sessions
ADD CONSTRAINT fk_rails_9fa262d742 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;

ALTER TABLE ONLY audit_events
ADD CONSTRAINT fk_rails_f64374fc56 FOREIGN KEY (author_id) REFERENCES users(id);
19 changes: 19 additions & 0 deletions lib/sagittarius/database/transactional.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module Sagittarius
module Database
module Transactional
module_function

def transactional
return_value = nil

ActiveRecord::Base.transaction do
return_value = yield
end

return_value
end
end
end
end
33 changes: 0 additions & 33 deletions lib/sagittarius/middleware/rack.rb

This file was deleted.

35 changes: 35 additions & 0 deletions lib/sagittarius/middleware/rack/context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

module Sagittarius
module Middleware
module Rack
class Context
def initialize(app)
@app = app
end

def call(env)
Sagittarius::Context.with_context(
Sagittarius::Context::CORRELATION_ID_KEY => correlation_id(env),
application: 'puma'
) do |context|
status, headers, response = @app.call env
headers['X-Sagittarius-Meta'] = context_to_json context
[status, headers, response]
end
end

def correlation_id(env)
ActionDispatch::Request.new(env).request_id
end

def context_to_json(context)
context
.to_h
.transform_keys { |k| k.delete_prefix("#{Sagittarius::Context::LOG_KEY}.") }
.to_json
end
end
end
end
end
Loading

0 comments on commit 00245d1

Please sign in to comment.