/\  Semaphor

Security / Multi-Tenancy

Learn how to use multi-tenancy in Semaphor

Overview

Semaphor provides multiple options for securing and segmenting customer data based on tenant isolation. You can implement Connection-Level and Row/Column-Level security policies to control user access within your application.

Connection-Level Security

Connection-level security enables strict data isolation by assigning dedicated databases or data sources to each tenant. With connection-level policies, database connection strings and file paths can be dynamically parameterized based on the requesting tenant. This approach eliminates dashboard duplication and ensures secure data access.

How It Works

When a user authenticates into your app, Semaphor generates AuthToken based on the user's tenancy and profile. This token ensures access only to the authorized data.

For Files (Amazon S3)

In Semaphor, you can visualize .csv or .parquet files stored in Amazon S3. To achieve tenant-based isolation, configure a connection policy that dynamically adjusts the file path based on the tenant. A common pattern is folder-based segmentation:

s3://semaphor-private/store/tenant_a/sales.parquet
s3://semaphor-private/store/tenant_b/sales.parquet
...
s3://semaphor-private/store/tenant_n/sales.parquet

You can create a connection policy using a tenant parameter. You can also have more than one parameter in the connection policy.

CLS

Connection Policy
s3://semaphor-private/store/{{ tenant }}/*.parquet

For Databases

Semaphor supports connections to SQL-compatible databases. A common approach for tenant-based isolation is to have separate databases per tenant:

postgresql+psycopg2://username:password@host:port/tenant_a
postgresql+psycopg2://username:password@host:port/tenant_b
...
postgresql+psycopg2://username:password@host:port/tenant_n

The following connection policy parameterizes the database part of the connection string:

CLS

Connection Policy
postgresql+psycopg2://username:password@host:port/{{ tenant }}

Applying Connection Policies

To apply a connection policy, construct a connection policy object and include it when requesting an authentication token:

const tenant = 'tenant_a'; 
 

const connectionPolicy = {
  name: 'store_sales_primary', 

  params: {

    tenant: tenant, 
  }, 
}; 
 
async function fetchToken() {
  try {
    const response = await fetch(TOKEN_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        dashboardId: DASHBOARD_ID,
        dashboardSecret: DASHBOARD_SECRET,
        cls: tenant ? [connectionPolicy] : [], 
      }),
    });
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
 
    const token = await response.json();
    if (token?.accessToken) {
      setAuthToken(token);
      console.log('Token:', token);
    }
  } catch (error) {
    console.error('There was an error!', error);
  }
}
 
fetchToken();

Security Note

The token generation function must be run on the server side. Do not expose DASHBOARD_SECRET in client-side code in production.


Row/Column-Level Security

Row/Column-Level policies provide logical isolation when all customer data resides in a single database table or set of files. These policies function as hard filters, restricting access to specific rows or columns based on the tenant.

How It Works

When a user authenticates, Semaphor applies the defined security policy to the authentication token. This policy restricts access at the row and column level within the dataset.

Example Policy

RCLS

This Row-Level policy filters data based on a state parameter, ensuring users only see data relevant to them. Additionally, specific columns, such as discount, can be restricted from access.

Row-Level Policy
state IN {{ state }}

Applying Row/Column-Level Policies


const rowPolicy = {

  name: 'state', 

  params: {

    state: ['California', 'Nevada', 'Washington'], 
  }, 
}; 
 
async function fetchToken() {
  try {
    const response = await fetch(TOKEN_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        dashboardId: DASHBOARD_ID,
        dashboardSecret: DASHBOARD_SECRET,
        rcls: rowPolicy, 
      }),
    });
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
 
    const token = await response.json();
    if (token?.accessToken) {
      setAuthToken(token);
      console.log('Token:', token);
    }
  } catch (error) {
    console.error('There was an error!', error);
  }
}
 
fetchToken();

Security Note

The token generation function must be run on the server side. Do not expose DASHBOARD_SECRET in client-side code in production.

Now whenever sales_data table is referenced in the Card SQL, this policy expands into the following SQL expression:

Card SQL
SELECT * FROM sales_data LIMIT 10;
Resolved SQL
SELECT [all columns except `discount`] FROM sales_data
WHERE state IN ('California', 'Nevada', 'Washington') LIMIT 10;

Combining Security Policies

You can use connection-level and row/column-level policies together for enhanced security, ensuring both physical and logical data isolation. This approach is ideal for multi-tenant architectures requiring both dedicated and shared resources.

By implementing these security measures, you can effectively manage data access while maintaining a secure and scalable multi-tenant environment in Semaphor.

On this page