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

Multi tenant support #113

Closed
masterada opened this issue May 7, 2020 · 9 comments
Closed

Multi tenant support #113

masterada opened this issue May 7, 2020 · 9 comments

Comments

@masterada
Copy link
Contributor

Hi!

We have been using centrifuge in production for more than a year now, without any issues, so thanks for the awesome project. A couple weeks ago we started launching multiple websites using our api, and we need to separate them completely. Our api was desigend for this, so it was no issue, but we need to separate centrifuge channels as well.

My first idea was:

  • user subscribes to some:channel
  • in centrifuge, I add support for multiple site, and modify the channel name to some:<somesite>.channel (eg: based on a header, or some values encoded in the jwt token)
  • when messages arrive on the some:<somesite>.channel, i send them out as some:channel

and although it would be best for us, I found no mechanism in centrifuge where a channel name is rewritten, so this approach would be very difficult for me to implement. If you think this is a good idea I would still love for it to happen, but until then I found a different approach.

My second idea:

  • user subscibes to some:<somesite>.channel
  • I validate that the user belongs to the site (eg: based on some values encoded in the jwt token)

I could almost implement this using centrifuge as a library (instead of using centrifugo server). I made a handler for the client.Subscribed event. The only thing I miss is to be able to access the JWT token info in the client.On().Subscribe handler. I made a fork exposing the client.info, with that it works perfectly.

@FZambia
Copy link
Member

FZambia commented May 7, 2020

Hello @masterada ! Glad it works for you :) Will appreciate if you describe your use case in more details so I could understand more on how Centrifuge and Centrifugo are used in the wild.

Do you have a possibility to add prefix to channel name on front-end site? I.e. you have project/site A and project/site B: use different channel names: A.channel and B.channel?

Do you have an option to use separate Centrifugo instances for different projects - this is how it was designed to prevent one project being affected by another project (at my previous work many years before we had issues like this with real sites so not supporting multitenancy was a weighted choice at some point, it also fits very well to microservices design even though I understand that it adds some operational overhead in some real-life situations).

@masterada
Copy link
Contributor Author

Let's discuss this in private, i wrote to you on gitter.

@vladvelici
Copy link

I could almost implement this using centrifuge as a library (instead of using centrifugo server). I made a handler for the client.Subscribed event. The only thing I miss is to be able to access the JWT token info in the client.On().Subscribe handler. I made a fork exposing the client.info, with that it works perfectly.

I'm working on an app with multi-tenancy. We have each channel to include the site in the name. It's not a big issue so no need for channel rewrites. I get extra JWT info by using the context. Just thought this might help you too.

func handler(w http.ResponseWriter, r *http.Request) {
	ctx = context.WithValue(r.Context(), someUniqueKey, dataINeedInTheHandler)
	r = r.WithContext(ctx)
	centrifuge.NewWebsocketHandler(...).ServeHTTP(w, r)
}


node.On().ClientConnected(func(ctx context.Context, client *centrifuge.Client) {
	dataINeedInTheHandler := ctx.Value(someUniqueKey)
        // on subscribe handler...
}

@FZambia
Copy link
Member

FZambia commented May 19, 2020

@vladvelici thanks, your approach looks fine. We discussed this with @masterada on Gitter and come to sth similar, though we tried to solve this based on extra claim inside JWT. Currently it's already possible to use custom JWT implementation with ClientConnecting callback but it does not work fine with client-side JWT refresh workflow - there is no way to do custom JWT parsing but still use client-side refresh workflow because custom OnRefresh callback disables client-side refresh workflow at moment.

I understand that this all can sound a bit complicated without proper example.

Anyway, at moment I see 2 ways to add some sort of multitenancy support:

  1. Provide a way to use client-side refresh workflow with custom OnRefresh callback. In this case OnRefresh should be called with raw token received from client in Refresh command
  2. Add built-in support using new field inside Credentials (and support passing it in JWT I think) - this will require some rewrites on Client-Engine boundary

I suppose we can actually think about adding both, the first seems simple to fix, the second a bit harder but provides more value for Centrifugo users since there is no way to add custom logic on server.

I should emphasize that we are talking about multitenancy for cases where we use different installations of the same app (where client-side uses the same code base and the same channels - just in different application contexts). We don't consider support to work with absolutely different apps using the same server instance.

@masterada
Copy link
Contributor Author

Add built-in support using new field inside Credentials (and support passing it in JWT I think) - this will require some rewrites on Client-Engine boundary

You could add some kind of channel pattern to the JWT (eg: regex) thats evaluated for every subscribe. Apart from solving this issue, it might be useful for other usecases as well.

eg: channel_pattern = ^\w+:mysite\..*$ could subscribe to channels: namespace:mysite.channel-name, namespace:mysite.channel-name#userid, etc.

or you could use it to limit the namspaces: channel_pattern = ^(namespace1|namespace2):.*$ could subscribe to: namspace1:somechannel and namespace2:somechannel, but not to namespace3:somechannel

@FZambia
Copy link
Member

FZambia commented May 20, 2020

Apart from solving this issue, it might be useful for other usecases as well.

Interesting idea, thanks, for now I just thought about some sort of prefix without utilizing regex for validation. Will think about it. Just to make sure - do I understand right, that you have a possibility to modify channel names on client side to insert site name to it? (because in this case you won't be able to use same channel names on client side but in different site context)

@masterada
Copy link
Contributor Author

Yes, I can do that (a little inconvinient but I can live with it :) ).

One thing to note about the prefix, the namespaces are configured globally, meaning a channel name looks like this: namespace:site-id|channel-name (with #userid postfix in some cases). We use | for separation because it wasn't in the reserved list in the centrifuge docs.

@FZambia
Copy link
Member

FZambia commented Jul 3, 2020

@masterada after making multi-tenant POC I decided to go another way and move library to another direction - see #128 and #129 - ready to discuss in case of any feedback.

@masterada
Copy link
Contributor Author

That sounds awesome. I'm closing this issue in favor of #128 .

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

No branches or pull requests

3 participants