unabridged/motion

[Question] Preventing unauthorized access to a resource's stream

mrrooijen opened this issue · 2 comments

Normally with ActionCable, as I understand it, if you want to prevent unauthorized access to a stream when someone attempts to subscribe to it, you simply check if the user has access to that particular resource, and if so, explicitly call stream_for so that they can stream it.

For example:

class ChatMessageChannel < ApplicationCable::Channel
  def subscribed
    if chat = current_user.chats.find_by(id: params[:id])
      # current_user has access to the chat,
      # therefore we allow it to stream its data.
      stream_for chat
    else
      # current_user doesn't have access to the chat,
      # so we don't call `stream_for chat`, thus
      # preventing the user from accessing the stream.
    end
  end
end

I'd like to know whether the following accomplishes the same thing in Motion:

class ChatComponent < ApplicationComponent
  include Motion::Component

  def initialize(id:, current_user:)
    @messages = []

    if chat = current_user.chats.find_by(id: id)
      stream_for chat, :new_message
    end
  end

  def new_message(msg)
    @messages << msg
  end
end

With this in place, is there any way that a user who doesn't have access to the chat resource can gain access to the stream regardless? I assume not, but perhaps I'm missing something. Any insight is much appreciated.

Really appreciate this library, thanks for making it open source!

@mrrooijen You have exactly the right idea! 🎉

Just like in ActionCable, only code on the server can call stream_from/stream_for and you are free to guard that with whatever application logic that you would like.

However, Motion takes this a step further by allowing you to to choose exactly what about each message is available to the client. Since only the rendered markup is exposed, anything that you do not explicitly choose to display will not be visible to the client. In fact, if you choose to ignore a message completely (by not changing the component's state at all in response to it), it isn't even possible for the client to observe that a broadcast was received:

def new_message(message) 
  return if message.hidden_from?(current_user)

  @messages << message
end

Unrelated: You probably do not need to pass in the current_user explicitly in this context.

Brilliant. Thanks for clarifying!

Unrelated: You probably do not need to pass in the current_user explicitly in this context.

Yeah, you probably don't even have to perform checks at this stage in many cases. The fact that you're rendering the component likely means that the current_user has access to the resources in the first place.