Skip to content

Commit

Permalink
Allow the application to configure Turbo::StreamChannel’s inheritance
Browse files Browse the repository at this point in the history
  • Loading branch information
Nicklas Ramhöj Holtryd committed Oct 22, 2024
1 parent 52727cb commit 6576ec8
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 3 deletions.
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,59 @@ This gem provides a `turbo_stream_from` helper to create a turbo stream.
<%# Rest of show here %>
```

### Authentication and Authorization

#### Authentication

Stream names generated with `turbo_stream_from` are cryptographically signed meaning that they can't be guessed. However they also don't expire, meaning that you could still take care to authenticate subscriptions to them. You can do so in `ApplicationCable::Connection`:

```rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user

def connect
self.current_user = find_verified_user
end

private
def find_verified_user
if verified_session = Session.find_by(id: cookies.signed[:session_id])
verified_session.user
else
reject_unauthorized_connection
end
end
end
end
```

#### Authorization

In multi-tenancy apps it's especially important to not only rely on authentication as it would allow someone to subscribe to a stream meant
for another tenant. To authorize your subscriptions you need to tell Turbo which channel to inherit from with:

```rb
# config/initializers/turbo.rb
Rails.application.config.to_prepare do
Turbo.base_stream_channel_class = "ApplicationCable::Channel"
end
```

You can now implement your domain specific authorization logic in your `base_stream_channel_class`.

```rb
module ApplicationCable
class Channel < ActionCable::Channel::Base
private

def authorized?
current_user.can_access? streamable
end
end
end
```

### Testing Turbo Stream Broadcasts

Receiving server-generated Turbo Broadcasts requires a connected Web Socket.
Expand Down Expand Up @@ -182,7 +235,7 @@ import "@hotwired/turbo-rails"

You can watch [the video introduction to Hotwire](https://hotwired.dev/#screencast), which focuses extensively on demonstrating Turbo in a Rails demo. Then you should familiarize yourself with [Turbo handbook](https://turbo.hotwired.dev/handbook/introduction) to understand Drive, Frames, and Streams in-depth. Finally, dive into the code documentation by starting with [`Turbo::FramesHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/frames_helper.rb), [`Turbo::StreamsHelper`](https://github.com/hotwired/turbo-rails/blob/main/app/helpers/turbo/streams_helper.rb), [`Turbo::Streams::TagBuilder`](https://github.com/hotwired/turbo-rails/blob/main/app/models/turbo/streams/tag_builder.rb), and [`Turbo::Broadcastable`](https://github.com/hotwired/turbo-rails/blob/main/app/models/concerns/turbo/broadcastable.rb).

Note that in development, the default Action Cable adapter is the single-process `async` adapter. This means that turbo updates are only broadcast within that same process. So you can't start `bin/rails console` and trigger Turbo broadcasts and expect them to show up in a browser connected to a server running in a separate `bin/dev` or `bin/rails server` process. Instead, you should use the web-console when needing to manaually trigger Turbo broadcasts inside the same process. Add "console" to any action or "<%= console %>" in any view to make the web console appear.
Note that in development, the default Action Cable adapter is the single-process `async` adapter. This means that turbo updates are only broadcast within that same process. So you can't start `bin/rails console` and trigger Turbo broadcasts and expect them to show up in a browser connected to a server running in a separate `bin/dev` or `bin/rails server` process. Instead, you should use the web-console when needing to manaually trigger Turbo broadcasts inside the same process. Add "console" to any action or "<%= console %>" in any view to make the web console appear.

### RubyDoc Documentation

Expand Down
36 changes: 34 additions & 2 deletions app/channels/turbo/streams_channel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,47 @@
#
# <%= turbo_stream_from 'room', channel: CustomChannel %>
#
class Turbo::StreamsChannel < ActionCable::Channel::Base
class Turbo::StreamsChannel < Turbo.base_stream_channel_class.constantize
extend Turbo::Streams::Broadcasts, Turbo::Streams::StreamName
include Turbo::Streams::StreamName::ClassMethods

def subscribed
if stream_name = verified_stream_name_from_params
if subscription_allowed?
stream_from stream_name
else
reject
end
end

private
def subscription_allowed?
stream_name && authenticated? && authorized?
end

# Override this method in <tt>Turbo.base_stream_channel_class.constantize</tt>
# to match your <tt>ApplicationCable::Connection</tt> <tt>identified_by</tt> accessor:
#
# def authenticated?
# current_user.present?
# end
def authenticated?
defined?(super) ? super : true
end

# Override this method to match your authorization rules in <tt>Turbo.base_stream_channel_class</tt>:
#
# def authorized?
# current_identity.can_access? streamable
# end
def authorized?
defined?(super) ? super : true
end

def streamable
@streamable ||= GlobalID::Locator.locate(stream_name)
end

def stream_name
@stream_name ||= verified_stream_name_from_params
end
end
1 change: 1 addition & 0 deletions lib/turbo-rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Turbo
extend ActiveSupport::Autoload

mattr_accessor :draw_routes, default: true
mattr_accessor :base_stream_channel_class, default: "ActionCable::Channel::Base"

thread_mattr_accessor :current_request_id

Expand Down

0 comments on commit 6576ec8

Please sign in to comment.