Skip to content

Build Your Own Portal

The Omnistrate platform is fully API driven and allows you to fully customize the user experience of your SaaS offering. We will walk through a quick example of a DBaaS offering with basic REST APIs which you can integrate within the framework of your choice to design your SaaS portal.

Restish

We will use the excellent Restish to walk through the APIs and design a basic user portal for your SaaS.

Setup

The Omnistrate platform provides 2 separate set of API models for your use:

  • APIs to operate your service
  • Impersonation APIs to operate the portal on behalf of your customers

Let's setup two separate API profiles in Restish for these use-cases.

  1. APIs to operate your service

    restish api configure service-operator https://api.omnistrate.cloud/2022-09-01-00/fleet
    

  2. Impersonation APIs to operate the portal on behalf of your customers

    restish api configure portal https://api.omnistrate.cloud/2022-09-01-00
    
    The default profile is for you as a service owner to onboard (sign-up, validate token, etc.) a customer. The rest of the APIs in this model are operated in the context of the customer.


Next, we will setup the authentication mechanism for each of these APIs. Add your username / password to a JSON file service-signin.json:

{
  "email": "your-email",
  "password": "your-password"
}

Sign-in as the service owner:

restish portal signin-api-signin <service-signin.json

HTTP/2.0 200 OK
Content-Length: 370
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 05:53:31 GMT

{
  jwtToken: "<your-jwt-token>"
}

Now, configure both the APIs with the JWT token:

restish api edit

# Modify the JSON file to use the JWT token
{
  "$schema": "https://rest.sh/schemas/apis.json",
  "portal": {
    "base": "https://api.omnistrate.cloud/2022-09-01-00",
    "profiles": {
      "default": {
        "headers": {
          "Authorization": "Bearer <your-jwt-token>"
        }
      }
    }
  },
  "service-operator": {
    "base": "https://api.omnistrate.cloud/2022-09-01-00/fleet",
    "profiles": {
      "default": {
        "headers": {
          "Authorization": "Bearer <your-jwt-token>"
        }
      }
    }
  }
}

Verify that the APIs are setup correctly by checking the caller identity:

restish portal users-api-describe-user

HTTP/2.0 200 OK
Content-Length: 849
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 06:02:14 GMT

{
  createdAt: "2023-04-13T10:07:33Z"
  email: "[email protected]"
  id: "user-18ZZD6JBrS"
  lastModifiedAt: "2024-10-04T21:30:15Z"
  name: "Omnistrate Demo"
  orgDescription: "OmnistrateDemo"
  orgName: "OmnistrateDemo"
  orgURL: "https://www.omnistrate.com"
  planName: "ENTERPRISE"
  roleType: "root"
}

Onboard your first customer

Now that the APIs are setup, let's onboard your first customer. Omnistrate provides an event loop to manage the lifecycle of your customers. Every customer activity (sign-up, invite, subscription request, etc.) generates an event that you can poll in your app and take appropriate action.

Use the portal APIs to create a new customer. Add the following JSON payload to a file customer-signup.json:

{
  "companyDescription": "The best eCommerce website",
  "companyUrl": "https://www.example.com",
  "email": "[email protected]",
  "legalCompanyName": "eCommerce",
  "name": "John Doe",
  "password": "<password>"
}
restish portal users-api-customer-signup <customer-signup.json

Use the service-operator API to fetch the details of the sign-up event:

restish service-operator events-api-list-events

HTTP/2.0 200 OK
Content-Length: 518
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 06:13:03 GMT

{
  events: [
    {
      eventID: "event-P1AlF9ECW5"
      eventPayload: {
        inviting_user_name: ""
        invoice_id: ""
        is_user_enabled: false
        product_tier_name: ""
        service_name: ""
        token: "<validation-token>"
        user_email: "[email protected]"
        user_id: "user-dcvEakUjz0"
        user_name: ""
        user_organization: ""
      }
      eventType: "CustomerSignUp"
      orgID: "org-4aoZZqPP3T"
      orgName: "Omnistrate"
      orgURL: ""
      priority: ""
      time: "2024-10-07T06:10:38Z"
      userEmail: "[email protected]"
      userID: "user-dcvEakUjz0"
      userName: ""
    }
  ]
}

Your service can send an email at this point to the customer with the validation token to complete the sign-up process. For now, let's explicitly validate the customer with the token in the event payload. Add the following JSON payload to a file customer-validate.json:

{
  "email": "[email protected]",
  "token": "<validation-token>"
}
restish portal signup-api-validate-token <customer-validate.json

HTTP/2.0 200 OK
Content-Length: 0
Content-Security-Policy: default-src 'none'
Date: Mon, 07 Oct 2024 06:16:29 GMT

Your customer is now onboarded and your portal can now impersonate the customer to perform actions on their behalf. To finish the process, let's acknowledge the event:

restish service-operator events-api-acknowledge-event event-P1AlF9ECW5

restish service-operator events-api-list-events
HTTP/2.0 200 OK
Content-Length: 14
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 06:38:01 GMT

{
  events: []
}

Let's sign-in as the customer. Add the following JSON payload to a file customer-signin.json:

{
  "email": "[email protected]",
  "password": "<password>"
}
restish portal users-api-customer-signin <customer-signin.json

Reconfigure the portal API with another profile to impersonate the customer:

restish api edit
...
  "portal": {
    "base": "https://api.omnistrate.cloud/2022-09-01-00",
    "profiles": {
      "customer": {
        "headers": {
          "authorization": "Bearer <customer-jwt-token>"
        }
      },
      "default": {
        "headers": {
          "authorization": "Bearer <your-jwt-token>"
        }
      }
    }
  }
...

Impersonate the customer and fetch the details of the customer:

restish portal users-api-describe-user --rsh-profile customer

HTTP/2.0 200 OK
Content-Length: 420
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 06:32:44 GMT

{
  createdAt: "2024-10-07T06:10:38Z"
  email: "[email protected]"
  id: "user-dcvEakUjz0"
  lastModifiedAt: "2024-10-07T06:16:29Z"
  name: "John Doe"
  orgDescription: "The best eCommerce website"
  orgFavIconURL: ""
  orgId: "org-z0sasy6jke"
  orgLogoURL: ""
  orgName: "eCommerce"
  orgPrivacyPolicy: ""
  orgSupportEmail: ""
  orgTermsOfUse: ""
  orgURL: "https://www.example.com"
  planName: "STARTER_NO_COMMIT"
  roleType: "root"
}

User management for your customers

Omnistrate provides the concept of an organization for your customers to manage their users. Once onboarded, your customers can invite other team members to join their organization on Omnistrate through the portal APIs. This provides a unified view for you and your customers to manage the billing, usage, and access control of the users of your service.

Invite a customer

Let's impersonate the customer we just onboarded and invite another user to join their organization. Add the following JSON payload to a file customer-invite.json:

{
  "email": "[email protected]"
}
restish portal users-api-customer-invite-user <customer-invite.json --rsh-profile customer

HTTP/2.0 200 OK
Content-Length: 0
Content-Security-Policy: default-src 'none'
Date: Mon, 07 Oct 2024 06:39:21 GMT

Use the service-operator API to fetch the details of the invite event:

restish service-operator events-api-list-events

HTTP/2.0 200 OK
Content-Length: 486
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 06:40:45 GMT

{
  events: [
    {
      eventID: "event-CoSqtH5emU"
      eventPayload: {
        inviting_user_name: "John Doe"
        invoice_id: ""
        is_user_enabled: false
        product_tier_name: ""
        service_name: ""
        token: ""
        user_email: "[email protected]"
        user_id: "user-qUClcuYyBW"
        user_name: ""
        user_organization: ""
      }
      eventType: "InviteUser"
      orgID: "org-4aoZZqPP3T"
      orgName: "Omnistrate"
      orgURL: ""
      priority: ""
      time: "2024-10-07T06:40:35Z"
      userEmail: "[email protected]"
      userID: "user-qUClcuYyBW"
      userName: ""
    }
  ]
}

Similar to the onboarding event processing loop, your service can send an email to the invited user to sign-up on your portal.


Let's acknowledge the event:

restish service-operator events-api-acknowledge-event event-CoSqtH5emU

HTTP/2.0 200 OK
Content-Length: 0
Content-Security-Policy: default-src 'none'
Date: Mon, 07 Oct 2024 06:44:18 GMT

Let's manually sign the invited user up. Add the following JSON payload to a file customer-invited-signup.json:

{
  "email": "[email protected]",
  "name": "Jane Smith",
  "password": "<password>"
}
restish portal users-api-customer-signup <customer-invited-signup.json

HTTP/2.0 200 OK
Content-Length: 0
Content-Security-Policy: default-src 'none'
Date: Mon, 07 Oct 2024 06:43:23 GMT

Use the service-operator API to fetch the details of the sign-up event:

restish service-operator events-api-list-events

HTTP/2.0 200 OK
Content-Length: 518
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 06:46:03 GMT

{
  events: [
    {
      eventID: "event-WueVADcmGx"
      eventPayload: {
        inviting_user_name: ""
        invoice_id: ""
        is_user_enabled: false
        product_tier_name: ""
        service_name: ""
        token: "<validation-token>"
        user_email: "[email protected]"
        user_id: "user-qUClcuYyBW"
        user_name: ""
        user_organization: ""
      }
      eventType: "CustomerSignUp"
      orgID: "org-4aoZZqPP3T"
      orgName: "Omnistrate"
      orgURL: ""
      priority: ""
      time: "2024-10-07T06:43:23Z"
      userEmail: "[email protected]"
      userID: "user-qUClcuYyBW"
      userName: ""
    }
  ]
}

Validate the invited user. Add the following JSON payload to a file customer-invited-validate.json:

{
  "email": "[email protected]",
  "token": "<validation-token>"
}
restish portal signup-api-validate-token <customer-invited-validate.json

HTTP/2.0 200 OK
Content-Length: 0
Content-Security-Policy: default-src 'none'
Date: Mon, 07 Oct 2024 06:47:59 GMT

Acknowledge the event:

restish service-operator events-api-acknowledge-event event-WueVADcmGx

List users in an organization

Let's use the customer profile to query a list of users in their organization:

restish portal users-api-describe-users-by-org  --rsh-profile customer

HTTP/2.0 200 OK
Content-Length: 437
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 06:49:08 GMT

{
  Id: "user-dcvEakUjz0"
  orgUsers: [
    {
      email: "[email protected]"
      invitedAt: "2024-10-07T06:10:38Z"
      name: "John Doe"
      roleType: "root"
      userId: "user-dcvEakUjz0"
    }
    {
      email: "[email protected]"
      invitedAt: "2024-10-07T06:40:35Z"
      name: "Jane Smith"
      roleType: "member"
      userId: "user-qUClcuYyBW"
    }
  ]
}

You can see both the customer and the invited user in your customer's organization.


Subscription management for your customers

Now that your customers are onboarded, we need to provide a way for them to subscribe to your service plans. On the Omnistrate portal, you can define multiple services and multiple service plans for each service. A Service Plan allows you to define the tenancy (multi-tenant / dedicated), pricing, deployment model (in your cloud account or in your customer's cloud account), and the capabilities of the service. This step assumes you have already defined atleast 1 service on Omnistrate and have a Production environment ready for this service. If not, head over to https://omnistrate.cloud/services and create a new service and release a production environment for it.


List available services

As your customer, you can get a list of all services they can subscribe to using the Service offering APIs. Let's use the customer profile to query a list of services available for subscription:

restish portal service-offering-api-list-service-offering --rsh-profile customer

HTTP/2.0 200 OK
Content-Length: 1739
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 06:59:57 GMT

{
  serviceIds: ["s-JA6hp8BqRL"]
  services: [
    {
      createdAt: "2024-10-07T06:59:06Z"
      isDeprecated: false
      offerings: [
        {
          AutoApproveSubscription: true
          awsRegions: ["us-east-2"]
          cloudProviders: ["aws", "gcp"]
          gcpRegions: ["us-central1"]
          productTierDescription: "Postgres SaaS hosted in Omnistrate account"
          productTierDocumentation: "Provide guides and informational resources for this plan"
          productTierID: "pt-48SQR0HcCe"
          productTierName: "Postgres SaaS"
          productTierPlanDescription: "Postgres SaaS hosted in Omnistrate account"
          productTierPricing: {
            value: "Specify the pricing model and structure for this plan"
          }
          productTierSupport: "Detail the support options available for this plan"
          productTierType: "OMNISTRATE_DEDICATED_TENANCY"
          productTierURLKey: "postgres-saas-postgres-saas-omnistrate-hosted-model-omnistrate-dedicated-tenancy"
          productTierVersion: "1.0"
          resourceParameters: [
            {
              isAutoscalingEnabled: false
              isBackupEnabled: false
              isDeprecated: false
              name: "Cluster"
              resourceId: "r-M7LY75veyE"
              urlKey: "cluster"
            }
          ]
          serviceAPIID: "sa-XwalJyGTbo"
          serviceAPIVersion: "v1"
          serviceEnvironmentID: "se-7esFfwjU0p"
          serviceEnvironmentName: "Production"
          serviceEnvironmentType: "PROD"
          serviceEnvironmentURLKey: "production"
          serviceEnvironmentVisibility: "PUBLIC"
          serviceLogoURL: ""
          serviceModelID: "sm-oqvlwYhRZ9"
          serviceModelName: "Postgres SaaS OMNISTRATE_HOSTED"
          serviceModelStatus: "READY"
          serviceModelType: "OMNISTRATE_HOSTED"
          serviceModelURLKey: "postgres-saas-omnistrate-hosted"
        }
      ]
      serviceDescription: "Postgres SaaS"
      serviceId: "s-JA6hp8BqRL"
      serviceName: "Postgres SaaS"
      serviceOrgId: "org-4aoZZqPP3T"
      serviceProviderId: "sp-JGcqr1hPxX"
      serviceProviderName: "Omnistrate"
      serviceURLKey: "postgres-saas"
    }
  ]
}

In this example, we have a Postgres SaaS service available for subscription and ready-to-use in AWS and GCP.


Subscribe to a service

Let's subscribe to this service as the customer. Add the following JSON payload to a file customer-subscribe.json using information from the listing above:

{
  "productTierId": "pt-48SQR0HcCe",
  "serviceId": "s-JA6hp8BqRL"
}
restish portal subscription-api-create-subscription <customer-subscribe.json --rsh-profile customer

HTTP/2.0 200 OK
Content-Length: 18
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 07:03:51 GMT

Let's fetch the details of the subscription from your customer's POV:

restish portal subscription-api-list-subscriptions --rsh-profile customer

HTTP/2.0 200 OK
Content-Length: 517
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 07:13:40 GMT

{
  ids: ["sub-ImCvoojwpm"]
  subscriptions: [
    {
      accountConfigIdentityId: "org-z0sasy6jke"
      cloudProviderNames: ["aws", "gcp"]
      createdAt: "2024-10-07T07:13:19Z"
      defaultSubscription: false
      id: "sub-ImCvoojwpm"
      productTierId: "pt-48SQR0HcCe"
      productTierName: "Postgres SaaS"
      roleType: "root"
      rootUserId: "user-dcvEakUjz0"
      serviceId: "s-JA6hp8BqRL"
      serviceLogoURL: ""
      serviceName: "Postgres SaaS"
      serviceOrgId: "org-4aoZZqPP3T"
      serviceOrgName: "Omnistrate"
      status: "ACTIVE"
      subscriptionOwnerName: "John Doe"
    }
  ]
}

Deploying a cluster of Postgres

Now that your customer has subscribed to the Postgres SaaS service, let's deploy a cluster of Postgres for them. The resource-instance APIs provide a way to deploy a cluster of Postgres for your customer using the subscription ID. The resource instance APIs are designed to be asynchronous and return a deployment ID that you can poll to get the status of the task. The API requires the following parameters that can be obtained from the service offering listing above:

restish resource-instance-api-create-resource-instance serviceproviderid servicekey serviceapiversion serviceenvironmentkey servicemodelkey producttierkey resourcekey <input.json

For the Postgres SaaS service, the command would look like:

restish portal resource-instance-api-create-resource-instance --subscription-id sub-ImCvoojwpm sp-JGcqr1hPxX postgres-saas v1 production postgres-saas-omnistrate-hosted postgres-saas-postgres-saas-omnistrate-hosted-model-omnistrate-dedicated-tenancy cluster <customer-instance.json --rsh-profile customer

HTTP/2.0 202 Accepted
Content-Length: 28
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 07:24:09 GMT

{
  id: "instance-5i4m1cwrp"
}

The customer-instance.json file should contain the configuration for the Postgres cluster. Eg:

{
  "region": "us-east-2",
  "cloud_provider": "aws",
  "requestParams": {
    "dbName": "abc",
    "instanceType": "t4g.small",
    "pgadminEmailAddress": "[email protected]",
    "postgresqlPassword": "<password>",
    "postgresqlUsername": "<username>"
  }
}


Let's fetch the details of the deployment using the deployment / resource-instance ID:

restish portal resource-instance-api-describe-resource-instance --subscription-id sub-ImCvoojwpm sp-JGcqr1hPxX postgres-saas v1 production postgres-saas-omnistrate-hosted postgres-saas-postgres-saas-omnistrate-hosted-model-omnistrate-dedicated-tenancy cluster instance-5i4m1cwrp --rsh-profile customer

HTTP/2.0 200 OK
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 07:28:20 GMT

{
  active: true
  cloud_provider: "aws"
  createdByUserId: "user-dcvEakUjz0"
  createdByUserName: "John Doe"
  created_at: "2024-10-07T07:24:08Z"
  detailedNetworkTopology: {
    r-DbJa2uX0L6: {
      allowedIPRanges: ["0.0.0.0/0"]
      clusterEndpoint: "pgadmin.instance-5i4m1cwrp.hc-pelsk80ph.us-east-2.aws.f2e0a955bb84.cloud"
      clusterPorts: [80, 443]
      customDNSEndpoint: {
        enabled: false
      }
      hasCompute: true
      main: false
      networkingType: "PUBLIC"
      nodes: [
        {
          availabilityZone: "us-east-2a"
          detailedHealth: {
            ConnectivityStatus: "HEALTHY"
            DiskHealth: "HEALTHY"
            LoadHealth: "POD_NORMAL"
            NodeHealth: "HEALTHY"
            ProcessHealth: "HEALTHY"
            ProcessLiveness: "HEALTHY"
          }
          endpoint: "pgadmin-0.instance-5i4m1cwrp.hc-pelsk80ph.us-east-2.aws.f2e0a955bb84.cloud"
          healthStatus: "HEALTHY"
          id: "pgadmin-0"
          ports: [80]
          status: "RUNNING"
          storageSize: 30
        }
      ]
      privateNetworkCIDR: "172.0.0.0/16"
      publiclyAccessible: true
      resourceInstanceMetadata: null
      resourceKey: "pGAdmin"
      resourceName: "PGAdmin"
    }
    r-M7LY75veyE: {
      allowedIPRanges: []
      clusterEndpoint: ""
      customDNSEndpoint: {
        enabled: false
      }
      hasCompute: false
      main: true
      networkingType: ""
      privateNetworkCIDR: "172.0.0.0/16"
      publiclyAccessible: true
      resourceInstanceMetadata: null
      resourceKey: "cluster"
      resourceName: "Cluster"
    }
    r-WyuLUOnWVy: {
      allowedIPRanges: ["0.0.0.0/0"]
      clusterEndpoint: "r-wyuluonwvy.instance-5i4m1cwrp.hc-pelsk80ph.us-east-2.aws.f2e0a955bb84.cloud"
      clusterPorts: [5432]
      customDNSEndpoint: {
        enabled: false
      }
      hasCompute: true
      main: false
      networkingType: "PUBLIC"
      nodes: [
        {
          availabilityZone: "us-east-2a"
          detailedHealth: {
            ConnectivityStatus: "HEALTHY"
            DiskHealth: "HEALTHY"
            LoadHealth: "POD_IDLE"
            NodeHealth: "HEALTHY"
            ProcessHealth: "HEALTHY"
            ProcessLiveness: "HEALTHY"
          }
          endpoint: "writer-0.instance-5i4m1cwrp.hc-pelsk80ph.us-east-2.aws.f2e0a955bb84.cloud"
          healthStatus: "HEALTHY"
          id: "writer-0"
          ports: [5432]
          status: "RUNNING"
          storageSize: 30
        }
      ]
      privateNetworkCIDR: "172.0.0.0/16"
      publiclyAccessible: true
      resourceInstanceMetadata: null
      resourceKey: "writer"
      resourceName: "Writer"
    }
    r-aRgU5ukzZV: {
      allowedIPRanges: ["0.0.0.0/0"]
      clusterEndpoint: "r-argu5ukzzv.instance-5i4m1cwrp.hc-pelsk80ph.us-east-2.aws.f2e0a955bb84.cloud"
      clusterPorts: [5432]
      customDNSEndpoint: {
        enabled: false
      }
      hasCompute: true
      main: false
      networkingType: "PUBLIC"
      nodes: [
        {
          availabilityZone: "us-east-2a"
          detailedHealth: {
            ConnectivityStatus: "HEALTHY"
            DiskHealth: "HEALTHY"
            LoadHealth: "POD_IDLE"
            NodeHealth: "HEALTHY"
            ProcessHealth: "HEALTHY"
            ProcessLiveness: "HEALTHY"
          }
          endpoint: "reader-0.instance-5i4m1cwrp.hc-pelsk80ph.us-east-2.aws.f2e0a955bb84.cloud"
          healthStatus: "HEALTHY"
          id: "reader-0"
          ports: [5432]
          status: "RUNNING"
          storageSize: 30
        }
      ]
      privateNetworkCIDR: "172.0.0.0/16"
      publiclyAccessible: true
      resourceInstanceMetadata: null
      resourceKey: "reader"
      resourceName: "Reader"
    }
    r-obsrvpt48sqr0hcce: {
      allowedIPRanges: ["0.0.0.0/0"]
      clusterEndpoint: "streamer.hc-pelsk80ph.us-east-2.aws.f2e0a955bb84.cloud"
      clusterPorts: [8080]
      customDNSEndpoint: {
        enabled: false
      }
      hasCompute: false
      main: false
      networkingType: "PUBLIC"
      privateNetworkCIDR: "172.0.0.0/16"
      publiclyAccessible: true
      resourceInstanceMetadata: null
      resourceKey: "omnistrateobserv"
      resourceName: "Omnistrate Observability"
    }
  }
  highAvailability: false
  id: "instance-5i4m1cwrp"
  last_modified_at: "2024-10-07T07:28:02Z"
  network_type: "PUBLIC"
  productTierFeatures: {
    LOGS: {
      enabled: true
      featureName: "LOGS"
      label: ["ONLINE"]
      scope: "CUSTOMER"
    }
    METRICS: {
      enabled: true
      featureName: "METRICS"
      label: ["ONLINE"]
      scope: "CUSTOMER"
    }
  }
  region: "us-east-2"
  result_params: {
    dbName: "abc"
    instanceType: "t4g.small"
    pgadminEmailAddress: "[email protected]"
    postgresqlUsername: "homepage"
  }
  status: "RUNNING"
  subscriptionId: "sub-ImCvoojwpm"
}

This provides a detailed view of the Postgres cluster deployed for your customer including the network topology, health status, and the endpoints for the cluster. You can customize the experience for your customer by providing them with the necessary information to connect to the cluster.


Customer RBAC

Omnistrate allows more fine-grained RBAC within the context of a customer organization. A customer can invite other users within (or outside) their org to share a subscription with them. This provides an easy way for your customers to pay for your service while providing easy access to their team members. In this example, let's add the user we invited earlier to share the subscription with the customer that owns it.

Setup the invited user

First, let's login as the invited user. Add the following JSON payload to a file customer-invited-signin.json:

{
  "email": "[email protected]",
  "password": "<password>"
}
restish portal users-api-customer-signin <customer-invited-signin.json

HTTP/2.0 200 OK
Content-Length: 370
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 07:33:17 GMT

{
  jwtToken: "<user1-jwt-token>"
}

Add the invited user to the Restish profile:

restish api edit

# Modify the JSON file to use the JWT token
{
  "$schema": "https://rest.sh/schemas/apis.json",
  "portal": {
    "base": "https://api.omnistrate.cloud/2022-09-01-00",
    "profiles": {
      "customer": {
        "headers": {
          "authorization": "Bearer <customer-jwt-token>"
        }
      },
      "default": {
        "headers": {
          "authorization": "Bearer <your-jwt-token>"
        }
      },
      "user1": {
        "headers": {
          "authorization": "Bearer <user1-jwt-token>"
        }
      }
    }
  }
}

Let's verify the new user's identity:

restish portal users-api-describe-user --rsh-profile user1

HTTP/2.0 200 OK
Content-Length: 424
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 07:34:52 GMT

{
  createdAt: "2024-10-07T06:40:35Z"
  email: "[email protected]"
  id: "user-qUClcuYyBW"
  lastModifiedAt: "2024-10-07T06:47:59Z"
  name: "Jane Smith"
  orgDescription: "The best eCommerce website"
  orgFavIconURL: ""
  orgId: "org-z0sasy6jke"
  orgLogoURL: ""
  orgName: "eCommerce"
  orgPrivacyPolicy: ""
  orgSupportEmail: ""
  orgTermsOfUse: ""
  orgURL: "https://www.example.com"
  planName: "STARTER_NO_COMMIT"
  roleType: "member"
}

Share the subscription with the invited user

Let's attempt to describe the Postgres cluster as the invited user:

restish portal resource-instance-api-describe-resource-instance --subscription-id sub-ImCvoojwpm sp-JGcqr1hPxX postgres-saas v1 production postgres-saas-omnistrate-hosted postgres-saas-postgres-saas-omnistrate-hosted-model-omnistrate-dedicated-tenancy cluster instance-5i4m1cwrp --rsh-profile user1

HTTP/2.0 403 Forbidden
Content-Length: 157
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 07:36:13 GMT

{
  fault: false
  id: ""
  message: "unauthorized: not authorized to perform DESCRIBE action on instance"
  name: "forbidden"
  temporary: false
  timeout: false
}

As expected, the invited user does not have the necessary permissions to describe the Postgres cluster.


Let's add the user to the subscription sub-ImCvoojwpm with read-only rights. Add the following JSON payload to a file customer-invite-user.json:

{
  "email": "[email protected]",
  "roleType": "reader"
}
restish portal consumption-user-api-invite-user sub-ImCvoojwpm <subscription-invite.json --rsh-profile customer

HTTP/2.0 200 OK
Content-Length: 0
Content-Security-Policy: default-src 'none'
Date: Mon, 07 Oct 2024 07:40:42 GMT

Now let's try to describe the Postgres cluster as the invited user:

restish portal resource-instance-api-describe-resource-instance --subscription-id sub-ImCvoojwpm sp-JGcqr1hPxX postgres-saas v1 production postgres-saas-omnistrate-hosted postgres-saas-postgres-saas-omnistrate-hosted-model-omnistrate-dedicated-tenancy cluster instance-5i4m1cwrp --rsh-profile user1

HTTP/2.0 200 OK
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 07:41:42 GMT

{
  active: true
  cloud_provider: "aws"
  createdByUserId: "user-dcvEakUjz0"
  createdByUserName: "John Doe"
  created_at: "2024-10-07T07:24:08Z"
  detailedNetworkTopology: {
    r-DbJa2uX0L6: {
      allowedIPRanges: ["0.0.0.0/0"]
      clusterEndpoint: "pgadmin.instance-5i4m1cwrp.hc-pelsk80ph.us-east-2.aws.f2e0a955bb84.cloud"
      clusterPorts: [80, 443]
      customDNSEndpoint: {
        enabled: false
      }
      hasCompute: true
      main: false
      networkingType: "PUBLIC"
      nodes: [
        {
          availabilityZone: "us-east-2a"
          detailedHealth: {
            ConnectivityStatus: "HEALTHY"
            DiskHealth: "HEALTHY"
            LoadHealth: "POD_IDLE"
            NodeHealth: "HEALTHY"
            ProcessHealth: "HEALTHY"
            ProcessLiveness: "HEALTHY"
          }
          endpoint: "pgadmin-0.instance-5i4m1cwrp.hc-pelsk80ph.us-east-2.aws.f2e0a955bb84.cloud"
          healthStatus: "HEALTHY"
          id: "pgadmin-0"
          ports: [80]
          status: "RUNNING"
          storageSize: 30
        }
      ]
      privateNetworkCIDR: "172.0.0.0/16"
      publiclyAccessible: true
      resourceInstanceMetadata: null
      resourceKey: "pGAdmin"
      resourceName: "PGAdmin"
    }
    r-M7LY75veyE: {
      allowedIPRanges: []
      clusterEndpoint: ""
      customDNSEndpoint: {
        enabled: false
      }
      hasCompute: false
      main: true
      networkingType: ""
      privateNetworkCIDR: "172.0.0.0/16"
      publiclyAccessible: true
      resourceInstanceMetadata: null
      resourceKey: "cluster"
      resourceName: "Cluster"
    }
    r-WyuLUOnWVy: {
      allowedIPRanges: ["0.0.0.0/0"]
      clusterEndpoint: "r-wyuluonwvy.instance-5i4m1cwrp.hc-pelsk80ph.us-east-2.aws.f2e0a955bb84.cloud"
      clusterPorts: [5432]
      customDNSEndpoint: {
        enabled: false
      }
      hasCompute: true
      main: false
      networkingType: "PUBLIC"
      nodes: [
        {
          availabilityZone: "us-east-2a"
          detailedHealth: {
            ConnectivityStatus: "HEALTHY"
            DiskHealth: "HEALTHY"
            LoadHealth: "POD_IDLE"
            NodeHealth: "HEALTHY"
            ProcessHealth: "HEALTHY"
            ProcessLiveness: "HEALTHY"
          }
          endpoint: "writer-0.instance-5i4m1cwrp.hc-pelsk80ph.us-east-2.aws.f2e0a955bb84.cloud"
          healthStatus: "HEALTHY"
          id: "writer-0"
          ports: [5432]
          status: "RUNNING"
          storageSize: 30
        }
      ]
      privateNetworkCIDR: "172.0.0.0/16"
      publiclyAccessible: true
      resourceInstanceMetadata: null
      resourceKey: "writer"
      resourceName: "Writer"
    }
    r-aRgU5ukzZV: {
      allowedIPRanges: ["0.0.0.0/0"]
      clusterEndpoint: "r-argu5ukzzv.instance-5i4m1cwrp.hc-pelsk80ph.us-east-2.aws.f2e0a955bb84.cloud"
      clusterPorts: [5432]
      customDNSEndpoint: {
        enabled: false
      }
      hasCompute: true
      main: false
      networkingType: "PUBLIC"
      nodes: [
        {
          availabilityZone: "us-east-2a"
          detailedHealth: {
            ConnectivityStatus: "HEALTHY"
            DiskHealth: "HEALTHY"
            LoadHealth: "POD_IDLE"
            NodeHealth: "HEALTHY"
            ProcessHealth: "HEALTHY"
            ProcessLiveness: "HEALTHY"
          }
          endpoint: "reader-0.instance-5i4m1cwrp.hc-pelsk80ph.us-east-2.aws.f2e0a955bb84.cloud"
          healthStatus: "HEALTHY"
          id: "reader-0"
          ports: [5432]
          status: "RUNNING"
          storageSize: 30
        }
      ]
      privateNetworkCIDR: "172.0.0.0/16"
      publiclyAccessible: true
      resourceInstanceMetadata: null
      resourceKey: "reader"
      resourceName: "Reader"
    }
    r-obsrvpt48sqr0hcce: {
      allowedIPRanges: ["0.0.0.0/0"]
      clusterEndpoint: "streamer.hc-pelsk80ph.us-east-2.aws.f2e0a955bb84.cloud"
      clusterPorts: [8080]
      customDNSEndpoint: {
        enabled: false
      }
      hasCompute: false
      main: false
      networkingType: "PUBLIC"
      privateNetworkCIDR: "172.0.0.0/16"
      publiclyAccessible: true
      resourceInstanceMetadata: null
      resourceKey: "omnistrateobserv"
      resourceName: "Omnistrate Observability"
    }
  }
  highAvailability: false
  id: "instance-5i4m1cwrp"
  last_modified_at: "2024-10-07T07:28:02Z"
  network_type: "PUBLIC"
  productTierFeatures: {
    LOGS: {
      enabled: true
      featureName: "LOGS"
      label: ["ONLINE"]
      scope: "CUSTOMER"
    }
    METRICS: {
      enabled: true
      featureName: "METRICS"
      label: ["ONLINE"]
      scope: "CUSTOMER"
    }
  }
  region: "us-east-2"
  result_params: {
    dbName: "abc"
    instanceType: "t4g.small"
    pgadminEmailAddress: "[email protected]"
    postgresqlUsername: "homepage"
  }
  status: "RUNNING"
  subscriptionId: "sub-ImCvoojwpm"
}

Let's try to delete the Postgres cluster as the invited user:

restish portal resource-instance-api-delete-resource-instance --subscription-id sub-ImCvoojwpm sp-JGcqr1hPxX postgres-saas v1 production postgres-saas-omnistrate-hosted postgres-saas-postgres-saas-omnistrate-hosted-model-omnistrate-dedicated-tenancy cluster instance-5i4m1cwrp --rsh-profile user1

HTTP/2.0 403 Forbidden
Content-Length: 155
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 07:43:10 GMT

{
  fault: false
  id: ""
  message: "unauthorized: not authorized to perform DELETE action on instance"
  name: "forbidden"
  temporary: false
  timeout: false
}

As expected, the invited user does not have the necessary permissions to delete the Postgres cluster.


Revoke access to the subscription

To complete the RBAC demonstration, let's revoke the user's access to the subscription:

restish portal consumption-user-api-revoke-user-role sub-ImCvoojwpm <subscription-invite.json --rsh-profile customer

HTTP/2.0 200 OK
Content-Length: 0
Content-Security-Policy: default-src 'none'
Date: Mon, 07 Oct 2024 07:44:22 GMT
restish portal resource-instance-api-describe-resource-instance --subscription-id sub-ImCvoojwpm sp-JGcqr1hPxX postgres-saas v1 production postgres-saas-omnistrate-hosted postgres-saas-postgres-saas-omnistrate-hosted-model-omnistrate-dedicated-tenancy cluster instance-5i4m1cwrp --rsh-profile user1

HTTP/2.0 403 Forbidden
Content-Length: 157
Content-Security-Policy: default-src 'none'
Content-Type: application/json
Date: Mon, 07 Oct 2024 07:44:44 GMT

{
  fault: false
  id: ""
  message: "unauthorized: not authorized to perform DESCRIBE action on instance"
  name: "forbidden"
  temporary: false
  timeout: false
}