rebus-org / Rebus.AzureServiceBus

:bus: Azure Service Bus transport for Rebus
https://mookid.dk/category/rebus
Other
33 stars 20 forks source link

Two-way communication with queue SAS's, not namespace #50

Closed eeskildsen closed 4 years ago

eeskildsen commented 4 years ago

I'm working on a project that needs 2-way communication...1 server, n clients. Clients should be isolated...unable to see one another's messages or even know of one another's existence. The server sends specific messages to specific clients.

During testing, I started with:

Something like:

Configure
    .With(new BuiltinHandlerActivator())
    .Transport(t => t
        .UseAzureServiceBus("Endpoint=sb://<MyServiceBus>.servicebus.windows.net/;SharedAccessKeyName=<MyServerKeyName>;SharedAccessKey=<MyServerKey>", "<MyServerQueue>")
        .AutomaticallyRenewPeekLock()
        .DoNotCreateQueues()
    )
    .Routing(r => r.TypeBased().MapAssemblyOf<TestMessage>("<MyClientQueue>"))
    .Options(o => o.EnableSynchronousRequestReply())
    .Start();

This worked well; the namespace SAS had rights to both queues.

However, in reality there will be many clients. (I think) I need 1 server queue and 1 client queue for each client to keep them isolated...(right?).

At first I tried (on the server):

Configure
    .With(new BuiltinHandlerActivator())
    .Transport(t => t
        .UseAzureServiceBus("Endpoint=sb://<MyServiceBus>.servicebus.windows.net/;SharedAccessKeyName=<MyServerKeyName>;SharedAccessKey=<MyServerKey>", "<MyServerQueue>")
        .AutomaticallyRenewPeekLock()
        .DoNotCreateQueues()
    )
    .Routing(r => r.TypeBased().MapAssemblyOf<TestMessage>("Endpoint=sb://<MyServiceBus>.servicebus.windows.net/;SharedAccessKeyName=<MyClientKeyName>;SharedAccessKey=<MyClientKey>;EntityPath=<MyClientQueue>"))
    .Options(o => o.EnableSynchronousRequestReply())
    .Start();

But that didn't work. It seems MapAssemblyOf expects a queue name.

I thought about using two bus instances...1 for the server queue, 1 for the client queue. But I'm not sure how that would work with synchronous request/reply.

Is this scenario supported?

mookid8000 commented 4 years ago

Is this scenario supported?

Good question 🙂

Rebus does not work that well with "clients that come and go". It's not that it's impossible, it's just that dynamically keeping track of specific clients is not something Rebus can help you with.

It could, if by "clients" you meant "subscribers", but that implies that the server does not care about each individual subscriber.

I don't know which kind of system you're building here, but have you considered looking at SignalR?


Btw. if you want to do the thing you seem to be doing with Rebus, you'd need to keep track of queue names manually somehow. You can then manually route messages to their destination queues like this:

await bus.Advanced.Routing.Send(destinationQueue, new SomeMessage());
eeskildsen commented 4 years ago

Thanks for the reply. I probably obscured my question with the talk about n clients. It's not so much that they come and go. What I'm really asking is, Is it possible to use queue-level authentication on routes, instead of namespace-level authentication?

I think I need that for two-way communication (e.g., Rebus.Async) if I want to isolate clients.

Example

  1. I have a server and three customers:
    • My server
    • Customer A
    • Customer B
    • Customer C
  2. The server sends a request to Customer A for some data. It uses the Customer A Client Queue.
  3. Customer A replies, using the Customer A Server Queue.

Customers B and C shouldn't have access to Customer A's queues.

Issue

I don't think I can do the above in Rebus yet...can I? The reason is, routes piggyback off the main Service Bus connection string.

For example, I can't do this:

.Transport(t => t
    .UseAzureServiceBus("<ServerQueueConnectionString>", "<ServerQueue>")
)
.Routing(r => r.TypeBased().MapAssemblyOf<TestMessage>("<ClientQueueConnectionString>"))

MapAssemblyOf expects a queue name, not a connection string.

So...if you want to use 2 queues...you have to use a namespace SAS key. That's a security risk in my scenario, because all customers can now access one another's queues.

Alternatively, you could create a namespace for each customer. But then you quickly run out of namespaces since Azure limits you to 100 per subscription.


Am I right that this can't be done in Rebus yet? Would it make sense to add it? I could work on a PR if you want.

If this is way out scope, that's okay....I can look at other options. Just wanted to check first.

mookid8000 commented 4 years ago

Ah, now I think I understand what you're trying to do.... although I am not entirely sure I understand why configuring a bus instance with multiple connection strings can improve security in your case, because no matter what you do, it will be up to your code to send a message to the right queue.

Am I right that this can't be done in Rebus yet?

Yes. Rebus is pretty much built around the assumption that a bus instance can send to any queue, although it's possible in many cases to limit that by using features built into whichever platform you're running on. E.g. with MSMQ, the user account under which your code is running can simply be denied access to certain queues, etc.

With Azure Service Bus, I actually think it would be possible to use a managed identity and give it access to specific queues/topics, so maybe that could be used in your scenario?

Would it make sense to add it? I could work on a PR if you want.

Not sure, actually. 🙂

eeskildsen commented 4 years ago

I am not entirely sure I understand why configuring a bus instance with multiple connection strings can improve security

Well...our client software runs on premise. So we need to limit what access the connection string grants.

For example, Customer A installs our software. The software receives requests on one queue and sends responses on another using Rebus.Async.

If we set them up with 1 connection string for both queues, that connection string has to contain a namespace SAS ... because you can't create a single queue SAS that grants access to multiple individual queues. A queue SAS can only grant access to the 1 queue you create it on. The other option is a namespace SAS, which grants access to all queues in that namespace.

So...if we can only give the customer 1 connection string, we have to give them a namespace SAS. Hence we'd need 1 namespace per customer, for a maximum of 100 customers.

On the other hand, say we can use 1 connection string for the transport and another for the mapping. Now we can create 2 queue SAS's: 1 for the receiving queue, 1 for the replying queue. So there's no need to grant access to the entire namespace. A namespace can contain up to 10,000 queues, so this would support thousands of customers.

With Azure Service Bus, I actually think it would be possible to use a managed identity and give it access to specific queues/topics, so maybe that could be used in your scenario?

Yes ... but only for software that runs inside Azure, like on an Azure VM. In the scenario above, customers could install their client software anywhere ... behind corporate firewalls, on AWS, etc. So we can't make their installations managed identities.

eeskildsen commented 4 years ago

Looking at ASB's authentication methods...

  1. SAS
  2. Azure Active Directory
    1. With managed identities
    2. From an application

...I think "from an application" could work. It can use OAuth client credentials grant flow. Rebus.AzureServiceBus would need to accept an ITokenProvider and pass it into MessageSender's constructor.

I'll probably look at implementing that locally this week if that sounds good....Let me know if you have any thoughts/suggestions. I may be missing something obvious here....

mookid8000 commented 4 years ago

Ok, sorry – I understand what you're trying to do now 🙂

I am not entirely sure that I want to include the ability to use multiple connection strings in Rebus.AzureServiceBus, because I am afraid that the code is going to be more complex.

Don't let that discourage you though! If your experiments show that you can solve this in an elegant way, I will definitely consider accepting a PR.

eeskildsen commented 4 years ago

Makes sense....Thanks for puzzling through this with me. I'll play with it a bit and see what happens.

A single connection string could be fine if we could pass in an ITokenProvider. With ITokenProvider, the caller could use a security principal instead of a SAS. Security principals can be granted rights to multiple queues without full namespace rights.

I'll get back to you once I've experimented with it some.

mookid8000 commented 4 years ago

A single connection string could be fine if we could pass in an ITokenProvider. With ITokenProvider, the caller could use a security principal instead of a SAS. Security principals can be granted rights to multiple queues without full namespace rights.

That sounds to me like the prettiest solution. 🙂