Infowarning | ||
---|---|---|
| ||
Section 5 on serialization/deserialization is coming soon. |
| |
This section of our wiki is deprecated. You landed here because we developed new better tools to integrate with Maestrano and our Partners. We now have a brand new Developer Platform which simplifies a lot your integration on all our Partner Marketplaces (both to manage marketing listing and to manage your technical integration). If your application is already integrated with Maestrano, do not panic! Your integration still works perfectly. To simplify your journey with us and our partner's marketplaces, we will onboard you soon on the developer platform. If you want to know more, just send us an email (developers@maestrano.com), nothing to be afraid of |
...
Table of Contents | ||
---|---|---|
|
...
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)
...
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
# 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 | ||||||
---|---|---|---|---|---|---|
| ||||||
# # 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 | ||||||
---|---|---|---|---|---|---|
| ||||||
# 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 | ||
---|---|---|
| ||
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/:tenant_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 key from the URL parameters tenant_key = params['tenant_key'] redirect_to MaestranoSamlRequest.with(tenant_key).new(params).redirect_url end # The 'initialize' controller action responds to the following route # POST /mno-enterprise/:tenant_key/saml/consume function consume # Retrieve the tenant key from the URL parameters tenant_key = params['tenant_key'] # Process the response saml_response = Maestrano::Saml::Response.with(tenant_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 (tenant_key) user = User.find_or_create_for_maestrano_tenant(user_attributes,tenant_key) organization = Organization.find_or_create_for_maestrano_tenant(group_attributes,tenant_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 | ||||||
---|---|---|---|---|---|---|
| ||||||
# # 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/:tenant_key/account/group/:id function destroy_group # Authenticate request as usual unless Maestrano.with(tenant_key).authenticate(http_basic['login'],http_basic['password']) 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_tenant_and_uid(tenant_key,group_uid) # Destroy it organization.destroy end # The 'destroy_group' controller action responds the following route # DESTROY /mno-enterprise/:tenant_key/account/group/:group_id/user/:id function remove_user # Authenticate request as usual unless Maestrano.with(tenant_key).authenticate(http_basic['login'],http_basic['password']) render json: "Unauthorized, code: '401' end # Retrieve the request parameters tenant_key = params[:tenant_key] group_uid = params[:group_id] user_uid = params[:id] # Retrieve the group/company as well as the user organization = Organization.find_by_tenant_and_uid(tenant_key,group_uid) user = User.find_by_tenant_and_uid(tenant_key,user_uid) # Remove the user organization.remove_user(user) end end |
...
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
class InvoiceModel function save return false unless this.save_to_db if this.maestrano_uid client = MaestranoConnecClient.with(this.maestrano_tenant_key).new(this.maestrano_group_uid) client.post('/invoices', this.to_maestrano_json) end end end |
3.6 Connec Webhooks
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
# This controller processes any data sharing notifications sent by tenants 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 tenant_key = params['tenant_key'] # Authenticate request as usual unless Maestrano.with(tenant_key).authenticate(http_basic['login'],http_basic['password']) render json: "Unauthorized, code: '401' end # Finally, process the request for a specific tenant MyConnecWrapperClass.process_invoice_updates(params['invoices'],tenant_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 tenants, this is the tenant key.
- Realm: the ID of the customer account (organization/company or user) who owns the remote entity. For Maestrano and Maestrano Enterprise tenants, 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.
...
The diagram below summarizes the concept:
The ID map makes it easy to write reusable code for connected objects. The pseudo code below shows how one could right a module, composing trait, interface or abstract class handling all the logic.
...
Info | ||
---|---|---|
| ||
The example above shows one way of handling connected objects and does not pretend to be a silver bullet. Your application may have different integration requirements. Do not hesitate to change, extend or simply adopt other patterns based on what you need to do! |
...