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 |
|---|---|
|
|
|
|
|
|
|
|
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
pindoes 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 |
|---|---|
|
not delivered to subscribers of other tenants |
|
other tenants’ exposers are never selected (method unavailable) |
|
requires the secret of the component’s tenant; tenant must be enabled |