log_logins 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 21a9dcc5a4f82db3ca896fb29adce696076a910519d27f1806e64d7c6b0b0ae1
4
+ data.tar.gz: 8f30718d34d660a686c1635ddcf87952d9dd652b425e2aeea3ef70a0331aa236
5
+ SHA512:
6
+ metadata.gz: 1f97f1daa840b5edb00a4fa8ce744c7608aa0a37485b57e48a537dfbb1aac67042e6ab26173eff2788dc3433aae2e01fa16c1343b4b495b576be3823bc99ac14
7
+ data.tar.gz: 4be1790c473c2a72e6a4ebe84d7c9dac77d5f2a7e4c1293da7e0b8ad0af4e997e5adb0c6c8cd8e5167227aa717abe84046cbd84eaa2c422a34a97df3622571ac
checksums.yaml.gz.sig ADDED
@@ -0,0 +1,2 @@
1
+ �(�u�l)��E&�r�S��F���'�=����Eru��[�k���:0nL��hDi�K��c�r����C����M�T��m�fɁ��P��і�"�)�Ha^]�6�Y�F��]5M���s��Q�<L���.�*�IУ�;Rt�y��V�2�Ne]�T����;��"d�y�����(�>
2
+ '���{�<�l%�-�w[�����~�ԤJ����_��&/��&mW�G��HHQ���SpPƝK�{��Y��._ �tQ��l�UY���
@@ -0,0 +1,26 @@
1
+ class CreateLogLoginsEventsTable < ActiveRecord::Migration[4.2]
2
+
3
+ def change
4
+ create_table :login_events do |t|
5
+ t.string :user_type # User, APIToken
6
+ t.integer :user_id # 141
7
+
8
+ t.string :username
9
+
10
+ t.string :action # Success, Failed, Blocked
11
+ t.string :interface # Web, API, SomeOtherInterface
12
+ t.string :ip # 1.2.3.4
13
+ t.string :user_agent
14
+
15
+ t.datetime :last_attempt_at
16
+
17
+ t.datetime :created_at
18
+
19
+ t.index [:user_type, :user_id], :length => {:user_type => 10}
20
+ t.index :ip, :length => 10
21
+ t.index :interface, :length => 10
22
+ t.index :created_at
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LogLogins
4
+
5
+ def self.config
6
+ @config ||= Config.new
7
+ end
8
+
9
+ def self.configure(&block)
10
+ block.call(config)
11
+ config
12
+ end
13
+
14
+ class Config
15
+
16
+ attr_reader :callbacks
17
+
18
+ def initialize
19
+ @callbacks = {}
20
+ end
21
+
22
+ def on(event, &block)
23
+ @callbacks[event.to_sym] ||= []
24
+ @callbacks[event.to_sym] << block
25
+ end
26
+
27
+ def remove_callback(event, block)
28
+ @callbacks[event.to_sym] && @callbacks[event.to_sym].delete(block)
29
+ end
30
+
31
+ def events_table_name
32
+ @events_table_name || 'login_events'
33
+ end
34
+ attr_writer :events_table_name
35
+
36
+ def block_time
37
+ @block_time || 3600
38
+ end
39
+ attr_writer :block_time
40
+
41
+ def attempts_before_block
42
+ @attempts_before_block || 10
43
+ end
44
+ attr_writer :attempts_before_block
45
+
46
+ def attempts_before_block_on_ip
47
+ @attempts_before_block_on_ip || attempts_before_block
48
+ end
49
+ attr_writer :attempts_before_block_on_ip
50
+
51
+ end
52
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LogLogins
4
+ class Engine < ::Rails::Engine
5
+ engine_name 'log_logins'
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LogLogins
4
+ class Error < StandardError
5
+ end
6
+
7
+ class InvalidLogEventError < Error
8
+ end
9
+
10
+ class LoginBlocked < Error
11
+ attr_accessor :event
12
+
13
+ def initialize(event)
14
+ @event = event
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require 'log_logins/config'
5
+ require 'log_logins/error'
6
+
7
+ module LogLogins
8
+ class Event < ActiveRecord::Base
9
+
10
+ self.table_name = LogLogins.config.events_table_name
11
+
12
+ ACTIONS = ['Success', 'Failed', 'Blocked', 'Unblocked']
13
+
14
+ belongs_to :user, :polymorphic => true
15
+
16
+ validates :action, :inclusion => {:in => ACTIONS}
17
+
18
+ # Events that have failed in the last hour (or whatever the block time might be)
19
+ scope :failed_in_block_time, -> { where(:action => 'Failed').where("created_at > ?", Time.now - LogLogins.config.block_time )}
20
+ scope :success, -> { where(:action => 'Success') }
21
+ scope :success_or_unblock, -> { where(:action => ['Success', 'Unblocked']) }
22
+ scope :failed, -> { where(:action => 'Failed') }
23
+ scope :blocked, -> { where(:action => 'Blocked') }
24
+
25
+ # Is this the first block in a series?
26
+ #
27
+ # @return [Boolean]
28
+ def first_block_in_series?
29
+ !!(self.action == 'Blocked' && (previous.nil? ||previous.action != 'Blocked'))
30
+ end
31
+
32
+ # Return the login event that preceeded this one for the given scope
33
+ #
34
+ # @return [LogLogins::Event, nil]
35
+ def previous
36
+ similar.order(:id => :desc).where("id < ?", self.id).first
37
+ end
38
+
39
+ # Return a scope of similar events
40
+ def similar
41
+ if self.user
42
+ self.class.where(:user => user)
43
+ elsif self.ip
44
+ self.class.where(:user => nil, :ip => ip)
45
+ else
46
+ none
47
+ end
48
+ end
49
+
50
+ # Log a new login event
51
+ #
52
+ # @param action [String] the action
53
+ # @param username [String] the username that was provided with the login attempt
54
+ # @param user [ActiveRecord::Base, nil] the user to login against or a string
55
+ # @option options [String] :ip
56
+ # @option options [String] :scope
57
+ # @option options [String] :user_agent
58
+ # @return [LogLogins::Event]
59
+ def self.log(action, username, user, ip, options = {})
60
+ event = self.new
61
+ event.user = user
62
+ event.action = action
63
+ event.username = username
64
+ event.ip = ip
65
+ event.interface = options[:interface]
66
+ event.user_agent = options[:user_agent]
67
+ if event.save
68
+ event
69
+ else
70
+ raise LogLogins::InvalidLogEventError, event.errors.full_messages.to_sentence
71
+ end
72
+ end
73
+
74
+ # Is the given user currently blocked from logging in?
75
+ #
76
+ # @param user [ActiveRecord::Base]
77
+ # @return [Boolean]
78
+ def self.user_blocked?(user)
79
+ return false unless user.is_a?(ActiveRecord::Base)
80
+ last_success = self.success_or_unblock.order(:id => :desc).select(:id).first.try(:id) || 0
81
+ self.failed_in_block_time.where(:user => user).where("id > ?", last_success).count >= LogLogins.config.attempts_before_block
82
+ end
83
+
84
+ # Is the given IP address currently blocked from logging in?
85
+ #
86
+ # @param ip [String]
87
+ # @return [Boolean]
88
+ def self.ip_blocked?(ip)
89
+ return false if ip.nil?
90
+ last_success = self.success_or_unblock.order(:id => :desc).select(:id).first.try(:id) || 0
91
+ self.failed_in_block_time.where(:user => nil, :ip => ip.to_s).where("id > ?", last_success).count >= LogLogins.config.attempts_before_block_on_ip
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LogLogins
4
+ module User
5
+
6
+ def self.included(base)
7
+ base.has_many :login_events, :class_name => 'LogLogins::Event', :as => :user, :dependent => :delete_all
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LogLogins
4
+ VERSION = '1.0.0'
5
+ end
data/lib/log_logins.rb ADDED
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'log_logins/config'
4
+ require 'log_logins/event'
5
+ require 'log_logins/user'
6
+
7
+ if defined?(Rails)
8
+ require 'log_logins/engine'
9
+ end
10
+
11
+ module LogLogins
12
+
13
+ def self.success(*args)
14
+ event = touch('Success', *args)
15
+ dispatch(:success, event)
16
+ event
17
+ end
18
+
19
+ def self.fail(*args)
20
+ event = touch('Failed', *args)
21
+ dispatch(:fail, event)
22
+ event
23
+ end
24
+
25
+ def self.dispatch(callback_name, event)
26
+ if callbacks = config.callbacks[callback_name.to_sym]
27
+ callbacks.each { |c| c.call(event) }
28
+ end
29
+ end
30
+
31
+ def self.unblock_user(user)
32
+ Event.log('Unblocked', nil, user, nil)
33
+ end
34
+
35
+ def self.unblock_ip(ip)
36
+ Event.log('Unblocked', nil, nil, ip)
37
+ end
38
+
39
+ private
40
+
41
+ def self.blocked!(username, user, ip, options = {})
42
+ event = Event.log('Blocked', username, user, ip, options)
43
+ dispatch(:blocked, event)
44
+ raise LogLogins::LoginBlocked.new(event), "Login has been blocked due to too many failed logins."
45
+ end
46
+
47
+ def self.touch(action, username, user, ip, options = {})
48
+ if Event.user_blocked?(user)
49
+ blocked!(username, user, ip, options)
50
+ end
51
+
52
+ if Event.ip_blocked?(ip)
53
+ blocked!(username, user, ip, options)
54
+ end
55
+
56
+ Event.log(action, username, user, ip, options)
57
+ end
58
+
59
+ end
data.tar.gz.sig ADDED
@@ -0,0 +1 @@
1
+ 2#�^�h�q,O�N�[�L�����hëL��?5)QuAr�A���Dq��76^�搛�g������P&�O�X�v�W���nѦ�R��U��6�d�VȔ4���zW�Dž�x��)�6P#l�˫�
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: log_logins
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Adam Cooke
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIEZDCCAsygAwIBAgIBATANBgkqhkiG9w0BAQsFADA8MQswCQYDVQQDDAJtZTEZ
14
+ MBcGCgmSJomT8ixkARkWCWFkYW1jb29rZTESMBAGCgmSJomT8ixkARkWAmlvMB4X
15
+ DTE4MDMwNTE3MzAwNVoXDTE5MDMwNTE3MzAwNVowPDELMAkGA1UEAwwCbWUxGTAX
16
+ BgoJkiaJk/IsZAEZFglhZGFtY29va2UxEjAQBgoJkiaJk/IsZAEZFgJpbzCCAaIw
17
+ DQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAOH6HpXwjmVYrUQxUHm25mLm9qYK
18
+ WS66Me1IfMUX3ZREZ/GzqiJZdV6itPuaaaKpbcm2A/KjgGSPOi9FZBneZ5KvbIeK
19
+ /GsixL98kxB06q9DZwJbFz7Inklxkd/S0anm+PxtWkQP1TLkMsviRcBPEAqSLON9
20
+ dCKC7+3kibhatdlsbqIQaeEhSoCUipYMi7ZyFHu5Qz+zMwc8JwHvQ4yi8cMa/QZ+
21
+ s1tN4mkp/6vWWj4G4lF3YjFYyt2txJcK5ELDtyBy7a3vbMImPy9pplFx1/M6SNpn
22
+ 7Pck0LqDprRzJXsGjq3CbC0nUaudFjUPr31KwxMYq1u13aQL9YuO3GeQCQ3gvdlJ
23
+ TSd7zoGgLwrMGmXqgd392Psr29yp+WBLcvhFUJnNPDV8nlph/cqmRzoIewP1kdPq
24
+ pEIUIJQdyKJU7gmFlJ1FurarkuT0a2Rgs99WokCoXLxuPmRWQRN1sH2nHL70jgAR
25
+ UuvyXEtyALHoCn3VqBR7ZvpfDblUzfANQDhBgwIDAQABo3EwbzAJBgNVHRMEAjAA
26
+ MAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQUa7gxxSE4SO2Ors4B+y3qANdMpo4wGgYD
27
+ VR0RBBMwEYEPbWVAYWRhbWNvb2tlLmlvMBoGA1UdEgQTMBGBD21lQGFkYW1jb29r
28
+ ZS5pbzANBgkqhkiG9w0BAQsFAAOCAYEAkbz/AJwBsRKwgt2BhWqgr/egf/37IS3s
29
+ utVox7feYutKyFDHXYvCjm64XUJNioG7ipbRwOOGs5bEYfwgkabcAQnxSlkdNjc4
30
+ JIgL/cF4YRg8uJG7DH+LwpydXHqr7RneDiONuiHlEN/1EZZ8tjwXypdwzhQ2/6ot
31
+ YOxdSi/mXdoDoFlIebsLyInUZjqnm7dQ9nTTUNSB+1LoOD8ARNhTIPnKCnxwZd56
32
+ giOxoHuJIOhgi6U2zicZJHv8lUj2Lc3bcirQk5eeOFRPVGQSpLLoqA7dtS7Jy4cv
33
+ 3c5m+HyxSxzlrcVHMAgJYemK0uhVQD9Y6JwHKDroWDH+MPALjlScw8ui1jmNuH31
34
+ n5JOH/07C4gYcwTjJmtoRSov46Z6Gn5cc6NFkQpA185pbRLqEDKzusXvBOQlAOLh
35
+ iyQrH6PJ0xgVJNYx+DLq3eFmo2hYJkw/lVhYAK+MdajtYJbD5VvCIEHO0d5RRgV+
36
+ qnCNZoPPy0UtRmGKZTMZvVJEZiw4g0fY
37
+ -----END CERTIFICATE-----
38
+ date: 2018-03-07 00:00:00.000000000 Z
39
+ dependencies:
40
+ - !ruby/object:Gem::Dependency
41
+ name: activerecord
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ description: A Rails library for logging login attempts and blocking as appropriate.
55
+ email:
56
+ - me@adamcooke.io
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - db/migrate/20180307100300_create_log_logins_events_table.rb
62
+ - lib/log_logins.rb
63
+ - lib/log_logins/config.rb
64
+ - lib/log_logins/engine.rb
65
+ - lib/log_logins/error.rb
66
+ - lib/log_logins/event.rb
67
+ - lib/log_logins/user.rb
68
+ - lib/log_logins/version.rb
69
+ homepage: https://github.com/adamcooke/log_logins
70
+ licenses:
71
+ - MIT
72
+ metadata: {}
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 2.7.4
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: A Rails library for logging login attempts and blocking as appropriate.
93
+ test_files: []
metadata.gz.sig ADDED
Binary file