...
...
1 Summary
The goal of this paper is to provide tips and good practices on how to perform a multi-tenant integration with Maestrano, i.e be able to handle multiple Maestrano-powered marketplaces including Maestrano.com.
...
The diagram below provides a general overview of the Maestrano Enterprise Delivery Network:
2 Principles
When doing multi-tenant integrations, three concepts need to be considered and properly scoped: Configuration, Routes and Models.
...
When storing records related to Maestrano and/or Maestrano Enterprise Tenants, your application should be able to keep track of which tenant and which tenant customer account this record is related to. This is usually done by storing a tenant key - and tenant customer account id - on the record or in an identity map table (see section 4).
3 Example of multi-tenant integration (pseudo code)
The example below gives a high level overview of what your code may look like in the context of a multi-tenant integration. If you have already done the integration to maestrano.com the code below will look familiar and essentially intends to show how you can "extend" your current integration to make it multi-tenant.
The code samples show you the general steps involved in getting a basic multi-tenant setup for Single Sign-On, Single Billing and Connec data sharing. Note that for this example, we assume the use of one of Maestrano's SDKs - available in Ruby, Java, PHP and .NET.
3.1 Configuration
The configuration is automatically taken care of by the autoconfigure method.
If your application is environment is set up on more than one marketplaces (or MnoHub), the autoconfigure method will call the https://developer.maestrano.com/api/config/v1/marketplaces endpoint in order to retrieve the different configurations per marketplaces.
Code Block |
---|
language | ruby |
---|
title | App Initializer |
---|
linenumbers | true |
---|
|
# This piece of code should be put in an initializer.
#
Maestrano.autoconfigure({
host: "https://developer.maestrano.com",
api_path: "/api/config/v1",
environment: {
name: "[your environment nid]"
api_key: "[your environment key]"
api_secret: "[your environment secret]"
}
})
# To retrieve the configuration per marketplace you may call
Maestrano.with(marketplace)
# that will contains all the configuration settings for this marketplace
|
...
language | ruby |
---|
title | App Routes |
---|
linenumbers | true |
---|
...
step involves declaring multiple tenant configurations, parameterizing the routes that will be used by our controllers (SSO, Connec etc.) and adding a metadata endpoint to expose our tenant specific configuration.
Code Block |
---|
language | ruby |
---|
title | App Initializer |
---|
linenumbers | true |
---|
|
# This piece of code should be put in an initializer.
#
# With the Maestrano SDKs, you have the ability to add as many
# marketplace configurations as you want. E.g.: you can add a configuration manifest
# for 'maestrano' and another one for 'acme-corp'.
#
# These configuration manifests only need to be added for Enterprise Tenants, which host a dedicated
# Maestrano infrastructure on their private cloud.
#
# Create a configuration manifest for the key "maestrano"
#
Maestrano.with("maestrano").configure({
# Tenant specific configuration
sso_idp: "https://maestrano.com",
account_api_host: "https://maestrano.com",
connec_api_host: "https://api-connec.maestrano.com",
# My app configuration for this tenant - note the use of the tenant key
# in the url.
# This URL will be used by Maestrano.com to send you notifications
app_webhook_path: "https://myapp.com/mno-enterprise/maestrano/connec/receive"
})
#
# Create a configuration manifest for the key "acme-corp"
#
Maestrano.with("acme-corp").configure({
# Tenant specific configuration
sso_idp: "https://saml.acme-corp.com",
account_api_host: "https://accounts.acme-corp.com",
connec_api_host: "https://api-connec.acme-corp.com",
# My app configuration for this tenant - note the use of the tenant key
# in the url.
# This URL will be used by Acme Corp to send you notifications
app_webhook_path: "https://myapp.com/mno-enterprise/acme-corp/connec/receive"
}) |
Code Block |
---|
language | ruby |
---|
title | App Routes |
---|
linenumbers | true |
---|
|
#
# Let's create parameterized routes
#
# The metadata route will be fetched by the enterprise tenants to retrieve your configuration
route "/mno-enterprise/:tenant_key/metadata" to "MetadataController" on action "show"
# The single sign-on routes will be used by enterprise tenants to trigger and complete SSO handshakes
route "/mno-enterprise/:tenant_key/saml/initialize" to "SamlSsoController" on action "initialize"
route "/mno-enterprise/:tenant_key/saml/consume" to "SamlSsoController" on action "consume"
# The Account Webhook routes notify you of groups being removed or users being removed from groups
route "/mno-enterprise/:tenant_key/account/group/:id" to "AccountWebhookController" on action "destroy_group"
route "/mno-enterprise/:tenant_key/account/group/:group_id/user/:id" to "AccountWebhookController" on action "remove_user"
# The Connec!™ webhook route will be used by enterprise tenants to POST data sharing notifications
route "/mno-enterpise/:tenant_key/connec/receive" to "ConnecWebhookController" with action "receive"
|
Code Block |
---|
language | ruby |
---|
title | MetadataController |
---|
linenumbers | true |
---|
|
# The metadata controller exposes my configuration to the requesting tenant
# Thanks to this metadata controller, the tenant will be able to discover my configuration
# and send webhook notifications to the right endpoint.
class MetadataController
# The show action responds to the following route
# GET /mno-enterprise/:tenant_key/metadata
function show
# Because the URL was parameterized, we can retrieve the tenant key
# from the URL parameters
tenant_key = params['tenant_key']
# Next step to make sure we authenticate the tenant. Authentication is
# tenant specific
unless Maestrano.with(tenant_key).authenticate(http_basic['login'],http_basic['password'])
render_json("Unauthorized, code: '401')
end
# Eventually, we render our configuration manifest for this specific tenant
render_json(Maestrano.with(tenant_key).to_metadata)
end
end |
3.2 Single Sign-On
Info |
---|
title | OpenID also available! |
---|
|
The example below assumes you are using our SDK for Single Sign-On which is based on SAML 2.0. Maestrano also has an OpenID provider available. Want to know more? Just checkout our OpenID guide. |
Code Block |
---|
language | ruby |
---|
title | SamlSSOController |
---|
linenumbers | true |
---|
|
#
# This controller handles the Single Sign-On handshake
#
class SamlSsoController
# The 'initialize' controller action responds the following route
# GET /mno-enterprise/:marketplacetenant_key/saml/initialize
#
# The goal of this action is to trigger the Single Sign-On handshake
# between the tenant platform and your application
function initialize
# Retrieve the tenant marketplacekey from the URL parameters
marketplacetenant_key = params['tenant_key']
redirect_to MaestranoSamlRequest.with(marketplacetenant_key).new(params).redirect_url
end
# The 'initialize' controller action responds to the following route
# POST /mno-enterprise/:marketplacetenant_key/saml/consume
function consume
# Retrieve the tenant key from the URL parameters
marketplacetenant_key = params['marketplacetenant_key']
# Process the response
saml_response = Maestrano::Saml::Response.with(marketplacetenant_key).new(params[:SAMLResponse])
# Reject if invalid
unless saml_response.is_valid?
redirect_to "/some/error/path"
end
# Extract information from the response
user_attributes = Maestrano::SSO::BaseUser.new(marketplace, saml_response).to_hash_or_associative_array
group_attributes = Maestrano::SSO::BaseGroup.new(marketplace, saml_response).to_hash_or_associative_array
# Find/create the user and the organization
# The creation or retrieval of records should be scoped to a specific provider (marketplacetenant_key)
user = User.find_or_create_for_maestrano_tenant(user_attributes, marketplacetenant_key)
organization = Organization.find_or_create_for_maestrano_tenant(group_attributes, marketplacetenant_key)
# Add user to the organization if not there already
unless organization.has_member?(user)
organization.add_member(user)
end
# Sign the user in and redirect to application root
# To be customised depending on how you handle user
# sign in and
sign_user_in(user)
redirect_to "/some/post-login/path"
end
end |
3.3 Account Webhooks
Code Block |
---|
language | ruby |
---|
title | WebhookAccountController |
---|
linenumbers | true |
---|
|
#
# This controller handles notification of people leaving a group (remove_user action) or companies
# cancelling their subscription to your service (destroy_group)
class WebhookAccountController
# The 'destroy_group' controller action responds the following route
# DESTROY /mno-enterprise/:marketplacetenant_key/account/group/:id
function destroy_group
# Retrieve the request parameters
marketplace = params[:marketplace]
group_uid = params[:id]group
# Authenticate request as usual
unless Maestrano.with(marketplacetenant_key).authenticate(http_basic['login'],http_basic['password'])
return render json: "Unauthorized, code: '401'
end
# Retrieve the request parameters
tenant_key = params[:tenant_key]
group_uid = params[:id]
# Retrieve the group/company
organization = Organization.find_by(marketplace: marketplace, uid: _by_tenant_and_uid(tenant_key,group_uid)
# Destroy it
organization.destroy
end
# The 'destroy_group' controller action responds the following route
# DESTROY /mno-enterprise/:marketplacetenant_key/account/group/:group_id/user/:id
function remove_user_user
# Authenticate request as usual
unless Maestrano.with(tenant_key).authenticate(http_basic['login'],http_basic['password'])
# Retrieve the request parameters render json: "Unauthorized, code: '401'
marketplaceend
= params[:marketplace]
# Retrieve group_uidthe = params[:group_id]
request parameters
user tenant_uidkey = params[:id]
# Authenticate request as usualtenant_key]
group_uid unless Maestrano.with(marketplace).authenticate(http_basic['login'],http_basic['password'])= params[:group_id]
user_uid return render json: "Unauthorized, code: '401'= params[:id]
end
# Retrieve the group/company as well as the user
organization = Organization.find_by(marketplace: marketplace, uid: find_by_tenant_and_uid(tenant_key,group_uid)
user = User.find_by(marketplace: marketplace, uid: _tenant_and_uid(tenant_key,user_uid)
# Remove the user
organization.remove_user(user)
end
end |
3.4 Billing
The code below shows how to bill an organization which has been tagged with a marketplace keytenant key. All Maestrano SDKs offer the ability to scope REST calls with a marketplace keytenant key. See the documentation of the relevant SDK for more details.
Code Block |
---|
language | ruby |
---|
title | MonthlyBillingJob |
---|
linenumbers | true |
---|
|
class MonthlyBillingJob
# Run the billing job
function run
foreach organization in Organization.all()
if organization.maestrano_marketplacetenant_key != null
# Use Maestrano's billing API
Bill.with(organization.maestrano_tenant_marketplacekey).create(amount: $100, group_id: organization.maestrano_uid)
else
# For your own customers, just charge as usual
organization.charge_credit_card($100)
end
end
end
end |
3.5 Connec Data Sharing
The code sample below shows an example of model automatically forwarding a notification to Connec! upon save, in a multi-tenant way.
Code Block |
---|
language | ruby |
---|
title | InvoiceModel |
---|
linenumbers | true |
---|
|
class InvoiceModel
function save
return false unless this.save_to_db
if this.maestrano_uid
client = MaestranoConnecClient.with(this.maestrano_marketplacetenant_key).new(this.maestrano_group_uid)
client.post('/invoices', this.to_maestrano_json)
end
end
end |
3.6 Connec Webhooks
Code Block |
---|
language | ruby |
---|
title | ConnecWebhookController |
---|
linenumbers | true |
---|
|
# This controller processes any data sharing notifications sent by marketplacestenants via
# Connec!
# E.g.: I receive a new invoice from Connec!™ that was created in another application
class WebhookConnecController
# The 'receive' controller action responds to the following route
# POST /mno-enterprise/acme-corp/connec/receive
function receive
# Retrieve the tenant key from the URL parameters
marketplacetenant_key = params['marketplacetenant_key']
# Authenticate request as usual
unless Maestrano.with(marketplacetenant_key).authenticate(http_basic['login'],http_basic['password'])
render json: "Unauthorized, code: '401'
end
# Finally, process the request for a specific marketplacetenant
MyConnecWrapperClass.process_invoice_updates(params['invoices'], marketplacetenant_key)
end
end |
4 Good practice: handling objects connected to third parties
You may have - or anticipate to have - a couple of integrations with platforms like Maestrano, QuickBooks, Xero, SalesForce etc. which will require you to synchronize objects using their APIs. For each of these objects, you will need to keep track of the foreign ids associated with the objects you've remotely created and/or received ("connected objects").
...