log_logins 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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