linger 0.1.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: 479ee2a71c47fda737a765386fde2dada477256e69c29efe1aa871de5938cdc9
4
+ data.tar.gz: 6a7b9461429e3a1376a382b35df611695b5b60dd728b9eabfb323f319add8b45
5
+ SHA512:
6
+ metadata.gz: eb5f3971ad71e12259399aaa71578f02ce8c7a02b3729ae9823ca3c02975e6bba1ca7034ee91b976c372e8ae522dfdcd381072dcf390cdce65fa8e9e6dd2aaa4
7
+ data.tar.gz: 1d075cb656ffe36736bad93dca30828f70359a0680ee040a2f42a40e396a7361d4441543c47c4d9a594473be75b59a73d8ea16b8626d3e355c30d109ad7c20da
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2022 leastbad
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # Linger
2
+
3
+ Linger takes advantage of the [new](https://github.com/stimulusreflex/stimulus_reflex/pull/588) `throw :forbidden` feature in StimulusReflex 3.5 to provide _unopinionated_ authentication security to Reflexes. If you have two tabs open and sign out in one, you should not be able to run Reflexes in the other tab. If you have sessions active on multiple devices, signing out on one should not impact the sessions on the others. These deceptively hard requirements are addressed by creating a composite key in Redis for every uniquely identified session.
4
+
5
+ Developers can respond to authentication failures by handling Reflexes that arrive in a new `forbidden` state. Forbidden Reflexes are functionally identical to halted Reflexes (eg. `throw :abort`) except that they (conceptually and semantically) represent Reflexes which were not allowed to execute.
6
+
7
+ You are **strongly advised** to consult the [Lingering Presence](https://github.com/leastbad/lingering_presence) reference application for further details, especially in advance of full documentation.
8
+
9
+ ## Requirements and setup
10
+
11
+ - StimulusReflex 3.5
12
+ - Redis server 4.0 and Redis Ruby client 4.2
13
+
14
+ Add the `linger` gem to your `Gemfile`.
15
+
16
+ Linger makes use of the same shared Redis config that Kredis uses. Just make sure that you have a valid `config/redis/shared.yml` and you're all set. You can learn more on the [Kredis](https://github.com/rails/kredis#setting-ssl-options-on-redis-connections) page.
17
+
18
+ ## Usage
19
+
20
+ In your Reflex class, create a `before_reflex` callback:
21
+
22
+ ```ruby
23
+ before_reflex :authenticate_connection!
24
+ ```
25
+
26
+ We recommend placing this in the `app/reflexes/application_reflex.rb` class, but you can manually add it to your Reflex classes on an ad-hoc basis.
27
+
28
+ ```ruby
29
+ class ApplicationReflex < StimulusReflex::Reflex
30
+ before_reflex :authenticate_connection!
31
+ end
32
+ ```
33
+
34
+ You are then responsible for calling `Linger` methods to allow and deny user contexts across all possible cases. You pass in whatever variables are expected, based on your `identified_by` Set that is defined in your `app/channels/application_channel/channel.rb`. The order you provide the identifiers is not important:
35
+
36
+ ```ruby
37
+ # after sign in
38
+ Linger.allow session, current_user
39
+ Linger.deny session
40
+ # after sign out
41
+ Linger.allow session
42
+ Linger.deny session, current_user
43
+ ```
44
+
45
+ Finally, the developer can either terminate Action Cable connections or create a handler for the `reflexForbidden` life-cycle state. You can see that a starting point for customization is presented in `app/javascript/controllers/application_controller.js` where you can see a sample handler for forbidden Reflexes is described. Instead of refreshing the page, you could use a notification library to pop up a toast message, for example.
46
+
47
+ ## Credit and acknowledgements
48
+
49
+ The structure of this project borrows both layout and code extensively from [Kredis](https://github.com/rails/kredis). Thanks to all of the Kredis contributors for such an excellent tool.
@@ -0,0 +1,6 @@
1
+ yaml_path = Rails.root.join("config/redis/shared.yml")
2
+ unless yaml_path.exist?
3
+ say "Adding `config/redis/shared.yml`"
4
+ empty_directory yaml_path.parent.to_s
5
+ copy_file "#{__dir__}/shared.yml", yaml_path
6
+ end
@@ -0,0 +1,15 @@
1
+ production: &production
2
+ url: <%= ENV.fetch("REDIS_URL", "redis://127.0.0.1:6379/0") %>
3
+ timeout: 1
4
+
5
+ development: &development
6
+ url: <%= ENV.fetch("REDIS_URL", "redis://127.0.0.1:6379/0") %>
7
+ timeout: 1
8
+
9
+ # You can also specify host, port, and db instead of url
10
+ # host: <%= ENV.fetch("REDIS_SHARED_HOST", "127.0.0.1") %>
11
+ # port: <%= ENV.fetch("REDIS_SHARED_PORT", "6379") %>
12
+ # db: <%= ENV.fetch("REDIS_SHARED_DB", "11") %>
13
+
14
+ test:
15
+ <<: *development
@@ -0,0 +1,11 @@
1
+ module Linger::Authentication
2
+ extend ActiveSupport::Concern
3
+
4
+ def authenticate_connection!
5
+ identifier_exists = false
6
+ Linger.instrument :meta, message: "Verifing #{connection.connection_identifier}" do
7
+ identifier_exists = Linger.redis.exists?(connection.connection_identifier)
8
+ end
9
+ throw :forbidden unless identifier_exists
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ require "redis"
2
+
3
+ module Linger::Connections
4
+ mattr_accessor :connections, default: {}
5
+ mattr_accessor :configurator
6
+ mattr_accessor :connector, default: ->(config) { Redis.new(config) }
7
+
8
+ def configured_for(name)
9
+ connections[name] ||= Linger.instrument :meta, message: "Connected to #{name}" do
10
+ connector.call configurator.config_for("redis/#{name}")
11
+ end
12
+ end
13
+
14
+ def clear_all
15
+ Linger.instrument :meta, message: "Connections all cleared" do
16
+ connections.each_value do |connection|
17
+ if Linger.namespace
18
+ keys = connection.keys("#{Linger.namespace}:*")
19
+ connection.del keys if keys.any?
20
+ else
21
+ connection.flushdb
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ require "active_support/log_subscriber"
2
+
3
+ class Linger::LogSubscriber < ActiveSupport::LogSubscriber
4
+ def meta(event)
5
+ info formatted_in(MAGENTA, event)
6
+ end
7
+
8
+ private
9
+
10
+ def formatted_in(color, event, type: nil)
11
+ color " Linger #{type} (#{event.duration.round(1)}ms) #{event.payload[:message]}", color, true
12
+ end
13
+ end
14
+
15
+ Linger::LogSubscriber.attach_to :linger
@@ -0,0 +1,13 @@
1
+ module Linger::Namespace
2
+ def namespace=(namespace)
3
+ Thread.current[:linger_namespace] = namespace
4
+ end
5
+
6
+ def namespace
7
+ Thread.current[:linger_namespace]
8
+ end
9
+
10
+ def namespaced_key(key)
11
+ namespace ? "#{namespace}:#{key}" : key
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ class Linger::Railtie < ::Rails::Railtie
2
+ config.linger = ActiveSupport::OrderedOptions.new
3
+
4
+ initializer "linger.testing" do
5
+ ActiveSupport.on_load(:active_support_test_case) do
6
+ parallelize_setup { |worker| Linger.namespace = "test-#{worker}" }
7
+ teardown { Linger.clear_all }
8
+ end
9
+ end
10
+
11
+ initializer "linger.logger" do
12
+ Linger::LogSubscriber.logger = config.linger.logger || Rails.logger
13
+ end
14
+
15
+ initializer "linger.configuration" do
16
+ Linger::Connections.connector = config.linger.connector || ->(config) { Redis.new(config) }
17
+ end
18
+
19
+ initializer "linger.configurator" do
20
+ Linger.configurator = Rails.application
21
+ end
22
+
23
+ initializer "linger.action_cable" do
24
+ ActiveSupport.on_load(:action_cable) do
25
+ StimulusReflex::Reflex.include Linger::Authentication
26
+ end
27
+ end
28
+
29
+ rake_tasks do
30
+ path = File.expand_path("..", __dir__)
31
+ Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module Linger
2
+ VERSION = "0.1.0"
3
+ end
data/lib/linger.rb ADDED
@@ -0,0 +1,56 @@
1
+ require "active_support"
2
+
3
+ require "linger/version"
4
+
5
+ require "linger/connections"
6
+ require "linger/log_subscriber"
7
+ require "linger/namespace"
8
+ require "linger/authentication"
9
+
10
+ require "linger/railtie" if defined?(Rails::Railtie)
11
+
12
+ module Linger
13
+ include Authentication
14
+ include Namespace
15
+ include Connections
16
+ extend self
17
+
18
+ mattr_accessor :logger
19
+
20
+ def redis(config: :shared)
21
+ configured_for(config)
22
+ end
23
+
24
+ def allow(*identifiers, **options)
25
+ key = build_key(identifiers)
26
+ instrument :meta, message: "Allowing #{key}" do
27
+ redis.set key, options[:data] || Time.zone.now
28
+ end
29
+ end
30
+
31
+ def deny(*identifiers)
32
+ key = build_key(identifiers)
33
+ instrument :meta, message: "Denying #{key}" do
34
+ redis.del key
35
+ end
36
+ end
37
+
38
+ def instrument(channel, **options, &block)
39
+ ActiveSupport::Notifications.instrument("#{channel}.linger", **options, &block)
40
+ end
41
+
42
+ private
43
+
44
+ def build_key(identifiers)
45
+ Linger.namespaced_key(identifiers.map do |identifier|
46
+ case identifier
47
+ when ActionDispatch::Request::Session
48
+ identifier.id.to_s
49
+ when Rack::Session::SessionId
50
+ identifier.to_s
51
+ else
52
+ identifier.respond_to?(:to_gid_param) ? identifier.to_gid_param : identifier.to_s
53
+ end
54
+ end.sort.join(":"))
55
+ end
56
+ end
@@ -0,0 +1,6 @@
1
+ namespace :linger do
2
+ desc "Install linger"
3
+ task :install do
4
+ system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{File.expand_path("../../install/install.rb", __dir__)}"
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: linger
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - leastbad
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-06-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 6.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: redis
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '4.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '4.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rails
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 6.0.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 6.0.0
55
+ description:
56
+ email: hello@leastbad.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - MIT-LICENSE
62
+ - README.md
63
+ - lib/install/install.rb
64
+ - lib/install/shared.yml
65
+ - lib/linger.rb
66
+ - lib/linger/authentication.rb
67
+ - lib/linger/connections.rb
68
+ - lib/linger/log_subscriber.rb
69
+ - lib/linger/namespace.rb
70
+ - lib/linger/railtie.rb
71
+ - lib/linger/version.rb
72
+ - lib/tasks/linger/install.rake
73
+ homepage: https://github.com/leastbad/linger
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 2.7.0
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.1.6
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Provide Devise-style authentication helpers for StimulusReflex
96
+ test_files: []