Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add audit event system #14

Merged
merged 1 commit into from
Dec 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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