Multi-tenancy

Multi-tenancy provides logical isolation between groups of components that share the same broker. Components of different tenants can be connected to the same broker at the same time, but they never see each other’s messages or RPC services.

The feature is compatible with the tenant feature of Rembus.jl.

Concepts

Tenant

A named group of components. The tenant is a property of the component name, not of the connection.

Default tenant

Components whose name has no domain part belong to the default tenant, represented by a single dot (.).

Tenant secret (pin)

A shared secret associated to each tenant by the broker administrator. It is required to register a component of that tenant (see Registration and tenants).

How the tenant is determined

The tenant is the portion of the component name that follows the first dot (see rembus.core.domain()):

Component name

Tenant

mynode

. (default)

foo.org

org

foo.bar.org

bar.org

sensor1.acme.com

acme.com

from rembus.core import domain

domain("foo.org")   # "org"
domain("mynode")    # "."

Isolation rules

Isolation is enforced by the broker.

Pub/Sub

A published message is delivered only to the reactive subscribers belonging to the same tenant as the publisher. Subscribers of other tenants are skipped even if they subscribed to the same topic.

import rembus as rb

sub_org = rb.node("ws://:8338/sub.org")
sub_org.subscribe(lambda data: print("org:", data), topic="mytopic")
sub_org.reactive()

sub_com = rb.node("ws://:8338/sub.com")
sub_com.subscribe(lambda data: print("com:", data), topic="mytopic")
sub_com.reactive()

pub = rb.node("ws://:8338/pub.org")
pub.publish("mytopic", "hello")   # only ``sub.org`` receives it

RPC

An RPC request is routed only to open exposers belonging to the same tenant as the requester. If the topic is exposed only by components of other tenants the request fails exactly as if the method was not exposed.

import rembus as rb

org = rb.node("ws://:8338/exposer.org")
org.expose(lambda x: x + 1, topic="myservice")

cli_org = rb.node("ws://:8338/client.org")
cli_org.rpc("myservice", 1)       # 2

cli_com = rb.node("ws://:8338/client.com")
cli_com.rpc("myservice", 1)       # raises rembus.protocol.RembusError

The tenant filter is applied by every routing policy (first_up, round_robin and less_busy): the policy only chooses among the exposers of the requester’s tenant.

Mesh of brokers

The tenant of a peer broker is derived from its own name with the same rules. Requests and messages forwarded through a peer are therefore subject to the same tenant comparison, as in Rembus.jl.

Note

A broker-side handler (a method or subscriber registered on the broker process itself) is not a tenant component and is not filtered by tenant.

Registration and tenants

Authenticated components are provisioned with rembus.sync.Node.register() (or rembus.register), which generates a key pair and sends the public key to the broker. The pin argument must match the secret configured for the tenant of the component name.

import rembus as rb

rb.register("foo.org", "11223344")   # tenant "org", secret "11223344"
rb.register("bar", "00000000")       # default tenant "."

The broker rejects the registration when:

  • the tenant is not enabled (no secret configured): tenant [org] not enabled;

  • the pin does not match the tenant secret: wrong tenant/pin;

  • the component name is already registered (STS_NAME_ALREADY_TAKEN).

Single-tenant setups need only the default tenant .. A component without a domain can be registered only if the . tenant is configured.

Managing tenants

Tenants and secrets are stored in the tenant table of the broker database, one row per (broker name, tenant):

CREATE TABLE IF NOT EXISTS tenant (
    name   TEXT NOT NULL,   -- broker name
    tenant TEXT NOT NULL,
    secret TEXT NOT NULL
)

The tools/tenant.py script manages them:

# add or update a tenant (default broker name: "broker")
python tools/tenant.py org --secret deadbeef
python tools/tenant.py . --secret deadbeef --broker mybroker

# remove a tenant
python tools/tenant.py org --delete

The same operations are available from Python:

from tools.tenant import add_tenant, remove_tenant

add_tenant("org", "deadbeef", "broker")
remove_tenant("org", "broker")

Adding a tenant that already exists replaces its secret.

Important

The broker loads the tenants once, at startup, when it is started with a listening port. Restart the broker after changing the table. Stop the broker before running the script too, because the database is attached by a single process at a time.

Unlike Rembus.jl, the tenants.json file based configuration is not supported: tenants are read only from the broker database.

Complete example

# 1. configure the tenants (broker stopped)
python tools/tenant.py org --secret orgsecret
python tools/tenant.py com --secret comsecret

# 2. start the broker
brokerd
import rembus as rb

# 3. provision components
rb.register("exposer.org", "orgsecret")
rb.register("client.org", "orgsecret")
rb.register("client.com", "comsecret")

# 4. components of different tenants cannot interact
srv = rb.node("exposer.org")
srv.expose(lambda x: x * 2, topic="double")

rb.node("client.org").rpc("double", 4)   # 8
rb.node("client.com").rpc("double", 4)   # RembusError: method unavailable

Summary of behaviours

Operation

Cross-tenant behaviour

publish

not delivered to subscribers of other tenants

rpc

other tenants’ exposers are never selected (method unavailable)

register

requires the secret of the component’s tenant; tenant must be enabled