Embedding an AWS QuickSight dashboard in a Rails application
By: Steve Olson
Tags: Quicksight, Vue, Amazon Quicksight, Rails, AWS

I recently worked on a project to embed a QuickSight dashboard into a web app, but could not find a complete example of my specific use case: allowing non-authenticated users of our multi-tenant app to one-click view an embedded analytics dashboard, and have the data be scoped to their specific user account. I felt that the documentation and blog posts I found kept getting me nearly to the finish line on several steps, but I really had to bang my head against the wall to get it fully working. Maybe that’s just because I was pretty new to the IAM side of AWS, and totally new to QuickSight, but I found a lot of comments online of people stuck in the same places I was. So I hope this post saves someone that frustration.

When I say non-authenticated, I mean no AWS or 3rd party logins needed, users are only logged into our web application. This article will assume you have already created a QuickSight dashboard, and that you have admin access to QuickSight and the AWS account in general. The basic pieces of this project are:

A published QuickSight dashboard with access granted to all users

A Cognito User/Identity pool setup with the correct authentication flow settings for QuickSight

IAM roles with the needed policies and trust relationships

A back end Rails (or other language/framework) integration to request the dashboard embed url (there are several pieces to this step)

A Front end integration (Vue in the example, but React would be similar) to display the dashboard in an iFrame, and ssl setup for running the app locally (QuickSight requires https for any dashboard embed requests)

Row Level Security implementation to scope the data to the current user of the web application

Share the dashboard

Let’s make sure our dashboard is going to be available to our Cognito users. Go to the dashboard in AWS, click “Share” in the top right, and select the box to “Share with all users in this account”. Remember to make sure you’ve already published this dashboard.

While you’re here copy the dashboard id from the url, it’ll look something like `a056ab24-c7f3–457e-a425–3ddc7c4fxxcc`.

Cognito User Pool and Identity Pool

Now we’ll setup Cognito in AWS. Go to AWS > Services > Cognito and create a user pool, using any name you want (default settings are fine). Once created, copy and save the Pool Id, and then in the sidebar, select App clients, and click on “Add an app client” (again, the default settings are fine here). Once created, copy and save the App client id value as well.

Next step is creating the Identity Pool. This will include some important configurations. Go back to Cognito and create an Identity Pool (or click on Federated Identities from the User Pool page). Each of the drop down sections on this page is important.

Unauthenticated Identities: select “Enable access to unauthenticated identities”. This will allow end users to access an IAM user without logging into AWS.

Authentication flow settings: select “Allow Basic (Classic) Flow”. For my specific use case of end users not signing into AWS, I was only able to make it work with this flow.

Authentication providers: on the Cognito tab, paste in your User Pool Id and App client id.

Create the Identity Pool, and on the next page, click the “Allow” button to create two roles for this Identity Pool (an Auth role and an Unauth role, we’ll only be worried about the Unauth role).

Finalize the IAM role and trust policy

Now we need to give the Unauth role the abilities and access that will be used in our Rails integration to generate the embed url. In AWS, go to IAM > Roles and find the Unauth role that was created (it should have “Cognito” in the name). The role will need the ability to:

Assume a role

Register a QuickSight User

Get a QuickSight dashboard embed url

Delete a QuickSight User (optional)

First, copy the Role ARN from the top of the page when viewing the role. It should start with arn:aws:iam:: . You’ll need this in Rails. Open the policy JSON and edit it to contain the following actions:

{

“Version”: “2012–10–17”,

“Statement”: [

{

“Action”: “quicksight:RegisterUser”,

“Resource”: “*”,

“Effect”: “Allow”

},

{

“Action”: “quicksight:DeleteUser”,

“Resource”: “*”,

“Effect”: “Allow”

},

{

“Action”: “quicksight:GetDashboardEmbedUrl”,

“Resource”: “arn:aws:quicksight:us-west-2:123456789:dashboard/*”,

“Effect”: “Allow”

},

{

“Action”: “sts:AssumeRole”,

“Resource”: “*”,

“Effect”: “Allow”

}

]

}

Note that in the Resource for the “quicksight:GetDashboardEmbedUrl” action, the star can be replaced with a specific dashboard id if you want to restrict access. Assuming you allowed the Identity Pool to create the roles for you, the role should already have a trust relationship setup that looks like:

Request the dashboard embed url from your Rails back end

Now we need to use the AWS sdks in Rails to retrieve the dashboard embed url. This will involve:

Assuming the role we made for the identity pool

Using that role response to setup the configs for a QuickSight client

Using the QuickSight Client to register a Cognito user in QuickSight

Requesting the dashboard embed url with the QuickSight client

Note that this is using the AWS V3 Ruby SDK

I made a QuickSightEmbedder service class that is used in a controller endpoint to provide the dashboard embed url to the front end. The user here is the user signed into our web app, passed to this class from the controller’s current_user . The public get_dashboard_url method looks like:

require 'aws-sdk-quicksight'

require 'aws-sdk-cognitoidentity'

def initialize(user)



end class QuickSightEmbedderdef initialize(user) @user = userend def get_dashboard_url

assume_role_resp = assume_cognito_role

qs_client_configs = set_quicksight_client_configs(assume_role_resp)

qs_client = Aws::QuickSight::Client.new(qs_client_configs)

register_user_in_quicksight(qs_client)

result = get_embed_url(qs_client) {

embed_url: result.embed_url,

status: result.status

}

end

We’ll break down each method called in there.

assume_cognito_role

This method uses the Cognito identity client to get credentials for an idenity pool user (Cognito creates that user on the fly), and then uses an AWS sts client to assume our Cognito idenity role using those credentials (this code is simplified to remove AWS configs and logic for non-local environments). You’ll need your AWS access key id and secret access key for this step to initialize the Aws::STS client. Note that for non-local environments, I use Aws::InstanceProfileCredentials.new to initialize that client. Also note that I hate trying to format code in Medium posts.

def assume_cognito_role

client = Aws::CognitoIdentity::Client.new({ region: 'us-west-2' }) # Note that the client.get_id call here is what creates a new Cognito user. get_id_response = client.get_id({ identity_pool_id: '123abc' }) token_resp = client.get_open_id_token({

identity_id: get_id_response.identity_id

}) configs = {

credentials: Aws::Credentials.new(

'aws_access_key_id',

'aws_secret_access_key'

),

region: 'us-west-2'

}

sts = Aws::STS::Client.new(configs) # You'll need that identity role arn that you copied earlier.

# Note that we use the user's email for the role session.

role_arn: cognito_identity_role_arn,

role_session_name:

web_identity_token: token_resp.token

} assume_role_params = {role_arn: cognito_identity_role_arn,role_session_name: @user .email,web_identity_token: token_resp.token # Now we assume the identity pool role: sts.assume_role_with_web_identity(assume_role_params)

end

set_quicksight_client_configs(assume_role_response)

With that assume role response, we now setup the configs for the QuickSight client. The return value from this method is a hash we’ll use as credentials.

def set_quicksight_client_configs(assume_role_resp)

Aws.config.update({

access_key_id: assume_role_resp.credentials.access_key_id,

secret_access_key: assume_role_resp.credentials.secret_access_key,

session_token: assume_role_resp.credentials.session_token,

region: 'us-west-2'

}) {

credentials: assume_role_resp,

region: 'us-west-2'

}

end

Back in the public method, the QuickSight client is initialized and passed to the next method:

qs_client = Aws::QuickSight::Client.new(qs_client_configs)

register_user_in_quicksight(qs_client)

Now the Cognito user is registered in QuickSight. Notice that we only give them READER access, and that an error is rescued if the user already is registered in QuickSight. Cognito will create a new identity every time the controller endpoint is hit, but QuickSight users persist unless they are manually destroyed, and the user name being a combination of the user email and IAM role name.



begin

qs_client.register_user({

identity_type: "IAM",

email:

user_role: "READER",

iam_arn: cognito_identity_role_arn,

session_name:

aws_account_id: aws_account_id,

namespace: "default"

})

rescue => error

# user already exists in quicksight, swallow error

end

end def register_user_in_quicksight(qs_client)beginqs_client.register_user({identity_type: "IAM",email: @user .email,user_role: "READER",iam_arn: cognito_identity_role_arn,session_name: @user .email,aws_account_id: aws_account_id,namespace: "default"})rescue => error# user already exists in quicksight, swallow errorendend

get_embed_url(qs_client)

And finally it’s time to request the embed url for the dashboard that will be passed back to the controller and to the front end.

def get_embed_url(qs_client)

dashboard_params = {

aws_account_id: aws_account_id,

dashboard_id: aws_quicksight_dashboard_id,

identity_type: "IAM",

session_lifetime_in_minutes: 300

} qs_client.get_dashboard_embed_url(dashboard_params)

end

Hooking up the front end — iFrame and ssl

Once an embed url is available from the server, there’s an npm package for displaying dashboard in an iFrame: amazon-quicksight-embedding-sdk. See that page for instructions on embedding in your front end.

QuickSight requires an https connection to view the dashboard, so we’ll need to setup ssl locally, and whitelist the proper domain in the QuickSight settings.

To generate local certs if needed, run this command in your console:

openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout localhost.key -out localhost.crt

Then for our Vue setup, it was a matter of updating the vue.config.js file. Require the Node File System module:

const fs = require('fs');

Then add an https key to the vueConfig.devServer configuration:

Then, when running the app locally, you can use the url: https://127.0.0.1:3001

We’ll need to whitelist that domain in QuickSight. Go back to the AWS QuickSight console, click on your username at the top right, then select “Manage QuickSight”. Select “Domains and Embedding on the right sidebar. Add https://127.0.0.1:3001 as a trusted domain.

In theory, you should now be able to run your app locally and view the embedded dashboard.

Scope the data with Row Level Security

The last challenge for me was to scope the data in the dashboard to the current user of the web app. AWS offers Row Level Security (RLS), which allows using a spreadsheet or database table to scope the dashboard in a multi-tenant application. Each row in the table whitelists (or blacklists if you prefer) a user for certain entities. In my app, a User has_many: merchants so scoping is done by merchant association. Note that you can only scope by columns that show up in the data tab of your dashboard. My dashboard has a “Merchant” column, which is the merchant name.

The RLS table needs a UserName column, and a column to scope by ( Merchant for this example). The UserName value should be the QuickSight user name for the current user. So to scope my user to a merchant named “McDonalds”, the RLS entry would look like:

Remember here that the user name comes from the Cognito Identity Pool role combined with the email passed in for the role_session_name.

RLS is applied in the “Datasets” section of the QuickSight console. You first need to create the dataset that holds the RLS data, and then apply that dataset as a Row Level Security setting to the dataset that populates your dashboard.

The quick (static) way to apply some RLS restrictions is to upload a spreadsheet as a dataset, but it’s more likely that a dynamic database table will be needed (assuming you have a database available to connect to in AWS). I chose to populate this table at the time the endpoint is hit requesting the dashboard embed url — the permissions record is created or updated to restrict to the user’s current Merchant association. Note that you can whitelist multiple entities by using a comma-delimited string in the Merchant column, like McDonalds,Burger King .

Once you have your RLS dataset created, select the dataset that populates your dashboard, and click on “Row-level security”.

From there you can select your RLS dataset and apply it, selecting “Grant access to data set”. You’ll get an error if your column names aren’t correct.

Another thing to note is that if you’ve hooked up a permissions table that doesn’t have the correct column names, you can apply custom SQL to it.

select some_column as UserName, other_column as Merchant from rls_table

I noticed that once you apply RLS scoping to your dashboard’s dataset, it seems to apply not only when viewing an embedded dashboard, but when viewing the QuickSight console in AWS. So for QuickSight admins, I added RLS rows in the table with all merchant names.

Deleting users from QuickSight

If you added the optional quicksight:DeleteUser action to the role actions, you can clean up QuickSight users when users of your app are removed/deactivated, etc.