Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feedback appreciated] Expose message bus to client #343

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

antoniusostermann
Copy link

Hey folks,

on the last recent days, I have been working on exposing volt's message bus to the client. With this pull request, this should be possible, but before merging, I'd love to have some feedback. In addition, I still need to write all specs for it, just wanted to check first that I've done everything right. There are some comments introduced with 'todo:', which reference to questions or tasks you could pick up in discussion.

How does it work?

The interface of the message bus on the client is the same as on the server. Volt.current_app.message_bus is connected to MessageBusClientProxy on the client, which supports the Eventable module and calls MessageBusTasks for publish/subscribe. Of course, these methods return Promises on the client, but unless you do not explicitly want to wait until sth has finished, you don't really need to handle them. Because of proxying, you can even call .remove the same way you would do on the server: Volt.current_app.message_bus.on('some-event') { |message| ... }.remove.
If you subscribe / publish to a channel, MessageBusTasks takes care of redirecting the subscription to the real MessageBus and pipelines everything up and down the websocket. It automatically matches local client listeners to message bus listeners using uuids.

Authorization layer

information extracted from app/volt/tasks/message_bus_tasks.rb
Generally you have the power to publish or subscribe to any channel, even to volt internals, if you want to.
Nevertheless, all channels are protected by an authorization layer, so publishing and subscribing
from client is only possible if the specified user is allowed to. Per default, channel names starting
with 'public:' are usable for everyone. If you want to restrict some channels / use the authorization
layer, have a look at /server/message_bus/client_authorizer, where everything you need to know is
explained very well.
Volt uses channels starting with 'volt:' for internal stuff, so be aware of publishing / subscribing
to these channels (although you could do!).

Define custom authorization rules

information extracted from lib/volt/server/message_bus/client_authorizer.rb
lib/volt/server/message_bus/client_authorizer.rb is used in message_bus_tasks to check if publishing or subscribing to a given channel is allowed or not. With the help of this class, you can easily add your own authorization layer, which is applied to certain channels or modes. To do this, just create an instance of this class, for example in an initializer:
Volt::MessageBus::ClientAuthorizer.new(:mode, 'channel1', 'channel2', ...) (see #initialize for param info)

Next, you can add rules to the authorizer which are evaluated on subscribing / publishing to a channel of the MessageBus from a client. To do this, call allow_if with a block:

 authorizer.allow_if do |task_context|
   # ...
   break true
 end

All authorization computations are executed in a task context (MessageBusTask). You get an instance of this task as a parameter, if you need it. To allow an action, your block has to return true, if it returns anything else subscribing/publishing is not allowed.

You can add multiple blocks/allow_ifs to one authorizer, all of these blocks have to evaluate to true to proceed. You can also add multiple authorizers to a channel, all associated authorizers have to evaluate to true to proceed.

The authorization class supports method chaining, so you can do:
Volt::MessageBus::ClientAuthorizer.new(:publish, 'my-channel').allow_if{|t| ... }.allow_if{...}

If there is no authorizer found for a request, e. g. if you have only defined an authorizer for :publish, not for :subscribe, the action is denied.

Using namespaces

information extracted from lib/volt/server/message_bus/client_authorizer.rb
ClientAuthorizer also supports the use of namespaces. If your channel contains a ':', the first part of the channel name is interpreted as the namespace. On an authorization request, this class then looks for a namespace defining rule, too. In addition to channel rule (if any), the namespace rule is evaluated and has to return true, too.

With the help of this, you can create a general authorizer for a class of channels (maybe for a volt component?) and specific, additional authorizers for some of the channels in this namespace. Or alternativly, you could restrict access to all channels with a namespace like 'chat:', and afterwards use channels with the prefix 'chat:' dynamically, without the need of adding additional authorizers.

To define an authorizer, which should apply to a namespace, use 'namespace:*' as channel name, for example:

 Volt::MessageBus::ClientAuthorizer.new(:subscribe, 'chat:*').make_public!
 Volt::MessageBus::ClientAuthorizer.new(:publish, 'chat:*').allow_if{.. check authentication? ..}

Now you can use channels like 'chat:messages', 'chat:signals', etc which would all include the above two rules.

Per default, there is already a 'public' namespace (see end of lib/volt/server/message_bus/client_authorizer.rb), which enables everyone to
publish/subscribe to all channels in this namespace. If you want to disable this behaviour, use:
Volt::MessageBus::ClientAuthorizer.new(:publish_and_subscribe, 'public:*').make_private!

@antoniusostermann
Copy link
Author

Currently, if you publish something on the message_bus (Server), the event is only triggered on remote volt instances. So if you do this in console:

Volt.current_app.message_bus.on('my-event') do 
  puts 'my-event fired'
end

Volt.current_app.message_bus.publish('my-event', 'my-message')

nothing will happen in your console session.

I think this is not intuitive for pub/sub systems and should be changed. By exposing the message bus to the client, this pull request also tries to establish a single global pub/sub system in volt. It would be possible to fire events on the client and react on all volt instances, If you fire an event on the client, you would expect not only remote servers to answer on it but also the directly connected one. Instead of https://github.com/voltrb/volt/pull/343/files#diff-a32cda92fb3f7cd2aa3c92dd64ae5d45R25, imho we should generally fire an event on all instances, not only on remote ones.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant