...
- Enterprise Network: these enterprise tenants networks run the Maestrano infrastructure in their private cloud. In order to publish your application to these tenant marketplaces these marketplaces you will need to
- have an agreement with them (which we can facilitate),
- have your app listed on their marketplace (Maestrano Hub),
- link your production environment to their marketplace.
- Express Network: these tenants marketplaces run their own platform using the Marketplace as a Service (MaaS) offering powered by Maestrano's cloud. Being an app provider on maestrano.com makes you automatically available on these marketplaces - meaning you do not need to do anything on your side. Please note that the marketplace owners may choose to restrict their offering to a limited number of apps.
...
When doing multi-marketplace integrations, three concepts need to be considered and properly scoped: Configuration, Routes and Models.
Configuration scoping
Your application should retrieve the automatic configuration from the developer platform configuration endpoint, providing its environments credentials. Your application should then be able to use the right marketplace API configuration (e.g.: which REST API host to use) based on the context of a controller action and/or database model. Multi-marketplace configuration is achieved by associating a configuration manifest preset to a "tenant marketplace key" (unique identifier, eg. 'maestrano-singapore') which will then be used to tag models and to parameterize routes.
...
One way of understanding the context of incoming traffic is to properly scope urls using the tenant "marketplace key" mentioned above. E.g.: Webhooks coming from Maestrano should hit a url like "/maestrano/some-webhook" while webhooks coming from another tenant marketplace should look like "/sometenantsomemarketplace/some-webhook". In a more general manner, multi-tenant marketplace URLs should be parameterized using a tenant key marketplace key parameter: "/:tenant_key#{marketplace}/some-webhook"
Model scoping
or "/some-webhook?marketplace=#{marketplace}"
Model scoping
When storing records related to Maestrano and/or Maestrano Enterprise TenantsMarketplaces, your application should be able to keep track of which tenant marketplace and which tenant customer marketplace customer account this record is related to. This is usually done by storing a tenant key marketplace key - and tenant customer marketplace customer account id - on the record or in an identity map table (see section 4).
3 Example of multi-
...
marketplace 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 marketplace 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-tenantmarketplace.
The code samples show you the general steps involved in getting a basic multi-tenant marketplace 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.
...
The configuration step involves declaring multiple tenant marketplace configurations, parameterizing the routes that will be used by our controllers (SSO, Connec etc.) and adding a metadata endpoint to expose our tenant marketplace specific configuration.
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
# This piece of code should be put in an initializer. # # With the Maestrano SDKs, you have the ability to addfetch as many many marketplace #configurations marketplaceyour configurationsenvironment asis youlisted wanton. E.g.: you can add# a# configurationFetch manifestthe # 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" })automatic configuration # # Content of your ENV variables: # export MNO_DEVPL_HOST=<developer platform host> # export MNO_DEVPL_API_PATH=<developer platform path> # export MNO_DEVPL_ENV_NAME=<your environment nid> # export MNO_DEVPL_ENV_KEY=<your environment key> # export MNO_DEVPL_ENV_SECRET=<your environment secret> # Maestrano.autoConfigure |
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
# # Let's create parameterized routes # # The metadata route will be fetched by the enterprise tenantsmarketplaces to retrieve your configuration route "/mno-enterprise/:tenantmarketplace_key/metadata" to "MetadataController" on action "show" # The single sign-on routes will be used by enterprise tenantsmarketplaces to trigger and complete SSO handshakes route "/mno-enterprise/:tenantmarketplace_key/saml/initialize" to "SamlSsoController" on action "initialize" route "/mno-enterprise/:tenantmarketplace_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/:tenantmarketplace_key/account/group/:id" to "AccountWebhookController" on action "destroy_group" route "/mno-enterprise/:tenantmarketplace_key/account/group/:group_id/user/:id" to "AccountWebhookController" on action "remove_user" # The Connec!™ webhook route will be used by enterprise tenantsmarketplaces to POST data sharing notifications route "/mno-enterpise/:tenantmarketplace_key/connec/receive" to "ConnecWebhookController" with action "receive" |
...
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
# The metadata controller exposes my configuration to the requesting tenantmarketplace # Thanks to this metadata controller, the tenantmarketplace 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/:tenantmarketplace_key/metadata function show # Because the URL was parameterized, we can retrieve the tenantmarketplace key # from the URL parameters tenantmarketplace_key = params['tenantmarketplace_key'] # Next step to make sure we authenticate the tenantmarketplace. Authentication is # tenantmarketplace specific unless Maestrano.with(tenantmarketplace_key).authenticate(http_basic['login'],http_basic['password']) render_json("Unauthorized, code: '401') end # Eventually, we render our configuration manifest for this specific tenantmarketplace render_json(Maestrano.with(tenantmarketplace_key).to_metadata) end end |
...
Info | ||
---|---|---|
| ||
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 | ||||||
---|---|---|---|---|---|---|
| ||||||
# # This controller handles the Single Sign-On handshake # class SamlSsoController # The 'initialize' controller action responds the following route # GET /mno-enterprise/:tenantmarketplace_key/saml/initialize # # The goal of this action is to trigger the Single Sign-On handshake # between the tenantmarketplace platform and your application function initialize # Retrieve the tenantmarketplace key from the URL parameters tenantmarketplace_key = params['tenantmarketplace_key'] redirect_to MaestranoSamlRequest.with(tenantmarketplace_key).new(params).redirect_url end # The 'initializeconsume' controller action responds to the following route # POST /mno-enterprise/:tenantmarketplace_key/saml/consume function consume # Retrieve the tenantmarketplace key from the URL parameters tenantmarketplace_key = params['tenantmarketplace_key'] # Process the response saml_response = Maestrano::Saml::Response.with(tenantmarketplace_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(saml_response).to_hash_or_associative_array group_attributes = Maestrano::SSO::BaseGroup.new(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 (tenantmarketplace_key) user = User.find_or_create_for_maestrano_tenantmarketplace(user_attributes,tenantmarketplace_key) organization = Organization.find_or_create_for_maestrano_tenantmarketplace(group_attributes,tenantmarketplace_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 |
...
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
# # 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/:tenantmarketplace_key/account/group/:id function destroy_group # Authenticate request as usual unless Maestrano.with(tenantmarketplace_key).authenticate(http_basic['login'],http_basic['password']) render json: "Unauthorized, code: '401' end # Retrieve the request parameters tenantmarketplace_key = params[:tenantmarketplace_key] group_uid = params[:id] # Retrieve the group/company organization = Organization.find_by_tenantmarketplace_and_uid(tenantmarketplace_key,group_uid) # Destroy it organization.destroy end # The 'destroy_group' controller action responds the following route # DESTROY /mno-enterprise/:tenantmarketplace_key/account/group/:group_id/user/:id function remove_user # Authenticate request as usual unless Maestrano.with(tenantmarketplace_key).authenticate(http_basic['login'],http_basic['password']) render json: "Unauthorized, code: '401' end # Retrieve the request parameters tenantmarketplace_key = params[:tenantmarketplace_key] group_uid = params[:group_id] user_uid = params[:id] # Retrieve the group/company as well as the user organization = Organization.find_by_tenantmarketplace_and_uid(tenantmarketplace_key,group_uid) user = User.find_by_tenantmarketplace_and_uid(tenantmarketplace_key,user_uid) # Remove the user organization.remove_user(user) end end |
...
The code below shows how to bill an organization which has been tagged with a tenant marketplace key. All Maestrano SDKs offer the ability to scope REST calls with a tenant marketplace key. See the documentation of the relevant SDK for more details.
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
class MonthlyBillingJob # Run the billing job function run foreach organization in Organization.all() if organization.maestrano_tenantmarketplace_key != null # Use Maestrano's billing API Bill.with(organization.maestrano_tenantmarketplace_key).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 |
...
The code sample below shows an example of model automatically forwarding a notification to Connec! upon save, in a multi-tenant marketplace way.
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
class InvoiceModel function save return false unless this.save_to_db if this.maestrano_uid client = MaestranoConnecClient.with(this.maestrano_tenantmarketplace_key).new(this.maestrano_group_uid) client.post('/invoices', this.to_maestrano_json) end end end |
...
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
# This controller processes any data sharing notifications sent by tenantsmarketplaces 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:marketplace/connec/receive function receive # Retrieve the tenantmarketplace key from the URL parameters tenantmarketplace_key = params['tenantmarketplace_key'] # Authenticate request as usual unless Maestrano.with(tenantmarketplace_key).authenticate(http_basic['login'],http_basic['password']) render json: "Unauthorized, code: '401' end # Finally, process the request for a specific tenantmarketplace MyConnecWrapperClass.process_invoice_updates(params['invoices'],tenantmarketplace_key) end end |
4 Good practice: handling objects connected to third parties
...
- Provider: the key identifying the provider for the remote entity (e.g.: Maestrano, Acme Corp, Xero, QuickBooks etc.). For Maestrano Enterprise tenantsmarketplaces, this is the tenant marketplace key.
- Realm: the ID of the customer account (organization/company or user) who owns the remote entity. For Maestrano and Maestrano Enterprise tenantsmarketplaces, this is the group id.
- Entity: the name of the remote entity. You should be able to derive the path of the entity API endpoint from this name.
- RID: the ID of the remote entity.
...