linger 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +49 -0
- data/lib/install/install.rb +6 -0
- data/lib/install/shared.yml +15 -0
- data/lib/linger/authentication.rb +11 -0
- data/lib/linger/connections.rb +26 -0
- data/lib/linger/log_subscriber.rb +15 -0
- data/lib/linger/namespace.rb +13 -0
- data/lib/linger/railtie.rb +33 -0
- data/lib/linger/version.rb +3 -0
- data/lib/linger.rb +56 -0
- data/lib/tasks/linger/install.rake +6 -0
- metadata +96 -0
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,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,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
|
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
|
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: []
|