Purpose
A solution was required to display a list of all roles and their configurations in a given Identity Security Cloud tenant in a user-friendly web UI. This blog post will demonstrate how to do that by utilizing ISC REST APIs in a Python Dash script. Dash allows developers to create interactive web applications using only Python, without needing to learn JavaScript or other front-end technologies.
Application Features
- Display all roles and their configurations in a table
- List membership criteria and other key attributes
- Sort by column data, such as creation and modification times
- Search for specific text by entering it in the column headers on the second row
- Add exclusion filter to exclude rows with that string if found in the membership column.
Screenshot of initial application launch:
Filtering on columns as well as excluding data from the membership column is shown in this screenshot. The latter is useful if there is a policy that requires the lifecycle state in the membership criteria and you need to validate that all roles are meeting this requirement.
Code Analysis and Detailed Description
The provided code is a Python Dash application designed to fetch, process, and display role data from an ISC tenant. Below we discuss the innovative aspects of the Dash framework, explain the Python libraries that are utilized, and give a detailed breakdown of the code.
Why Dash is Innovative and Beneficial
- Ease of Use: Dash allows developers to create interactive web applications using only Python, without needing to learn JavaScript or other front-end technologies.
- Interactivity: Dash applications are highly interactive, with features like dropdowns, sliders, and buttons that can trigger updates to the UI in real time.
- Data Visualization: Dash integrates seamlessly with Plotly, a powerful graphing library, enabling the creation of rich, interactive visualizations.
- Rapid Prototyping: Dash is ideal for quickly building prototypes or proof-of-concept applications for data analysis and visualization.
- Scalability: Dash applications can be deployed to production environments and scaled to handle large datasets and multiple users.
- Customizability: Dash provides a high degree of customization, allowing developers to create complex layouts and interactions tailored to their specific use case.
- Open Source: Dash is open source and has a large, active community, making it easy to find support and resources.
Required Python Libraries
dash
:
- Dash is the core framework used to build the web application. It provides the structure for creating interactive web apps using Python. Dash abstracts away the complexities of web development, allowing developers to focus on data visualization and interaction logic.
dcc
(Dash Core Components):
- Provides high-level components like inputs, dropdowns, graphs, and tables. Enables the creation of interactive UI elements such as text inputs, buttons, and loading spinners.
html
(Dash HTML Components):
- Provides HTML components like
div
,h1
, andlabel
for structuring the UI. Allows the creation of the layout and structure of the web application.
dash_table
:
- Provides a component for displaying and interacting with tabular data. Renders the fetched role data in a table with features like sorting, filtering, and pagination.
requests
:
- A library for making HTTP requests. Handles API calls to authenticate with SailPoint ISC and fetch role data.
pandas
:
- A data manipulation library. Converts the fetched JSON data into a DataFrame for easier processing and filtering.
logging
:
- A library for logging messages. Logs errors and other important events for debugging and monitoring.
dash.dependencies
:
- Provides decorators for defining callbacks. Links UI components (e.g., buttons, inputs) to Python functions, enabling interactivity.
What the Code Does
- Authentication:
- The user provides the tenant name, Client ID, and Client secret via input fields.
- The application constructs the API enpoint URLs dynamically using the tenant name.
- It authenticates with the SailPoint ISC API using the provided credentials and retrieves an access token.
- Fetching Roles:
- The application fetches role data from the SailPoint ISC API using pagination.
- It processes the fetched data to flatten nested JSON structures and formats specific columns (e.g.,
membership
,identity
,entitlements
).
- Data Filtering:
- The user can filter the membership data by entering a string in the “String excluded from membership” field.
- The application filters out rows where the
membership
column contains the specified string.
- Displaying Data:
- The processed and filtered data is displayed in an interactive table using
dash_table.DataTable
. - The table supports features like sorting, filtering, and pagination.
- Error Handling:
- The application logs errors and displays user-friendly error messages in the UI.
Running the App
- Install (if not already installed):
- VSCode
- Python
- Dash
- Pandas
- Save the Python script below to a file named roles.py.
- Generate a PAT (Personal Access Token) or API credentials with scope
idn:role-unchecked:read
. - Open the roles.py file in VSCode and hit the play button.
- Launching the script runs the Dash server locally.
- The app is accessible at http://127.0.0.1:8050.
- Enter in the tenant name, Client Id and Client Secret. Note the tenant name is the short name for a sandbox or production tenant. If your URL is https://acme-sb.identitynow.com, you would enter “acme-sb”.
- Click the Fetch Roles button
- Roles are displayed in tabular format
- Displayed roles can be sorted by column, filtered, and searched
Python Code
import dash
from dash import dcc, html, dash_table
import requests
import pandas as pd
from dash.dependencies import Input, Output, State
import logging
# Configure logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# Pagination settings
PAGINATION_LIMIT = 60 # Number of roles per page
# Step 1: Generic formatting function
def format_dict_list(data, keys, prefix=""):
"""
Generic function to format a list of dictionaries.
"""
if not data or not isinstance(data, (list, dict)):
return "No data available"
formatted_data = []
if isinstance(data, list):
for item in data:
if isinstance(item, dict):
formatted_item = "\n".join([f"{prefix}{key}: {item.get(key, 'Unknown')}" for key in keys])
formatted_data.append(formatted_item)
elif isinstance(data, dict):
formatted_data.append("\n".join([f"{prefix}{key}: {data.get(key, 'Unknown')}" for key in keys]))
return "\n\n".join(formatted_data)
# Step 2: Format membership column
def format_membership_column(membership_data: dict) -> str:
"""
Format the membership column.
"""
if not membership_data or not isinstance(membership_data, dict):
return "No membership data available"
membership_type = membership_data.get("type", "Unknown")
criteria = parse_criteria(membership_data.get("criteria", {}))
identities = format_dict_list(membership_data.get("identities", []), ["type", "name"])
return f"Type: {membership_type}\nCriteria:\n{criteria}\nIdentities:\n{identities}"
# Step 3: Parse criteria
def parse_criteria(criteria: dict) -> str:
"""
Recursively parse the criteria section.
"""
if not criteria or not isinstance(criteria, dict):
return "No criteria available"
operation = criteria.get("operation", "N/A")
key = criteria.get("key", {})
string_value = criteria.get("stringValue", "")
children = criteria.get("children", [])
key_desc = f"{key.get('type', 'N/A')}.{key.get('property', 'N/A')}" if key and isinstance(key, dict) else "N/A"
result = [f"{operation}: {key_desc} = '{string_value}'"] if string_value else [f"{operation}: {key_desc}"]
if children and isinstance(children, list):
for child in children:
result.append(parse_criteria(child))
return "\n".join(result)
# Step 4: Flatten role data
def flatten_role_data(roles: list) -> list:
"""
Flatten nested JSON data for easier display in a table.
"""
flattened_data = []
for role in roles:
if not isinstance(role, dict):
logging.warning(f"Skipping invalid role: {role}")
continue
flat_role = {}
for key, value in role.items():
if key == "membership":
flat_role[key] = format_membership_column(value)
elif key in {"identity", "owner"}:
flat_role[key] = format_dict_list(value, ["type", "name"])
elif key == "accessProfiles":
flat_role[key] = format_dict_list(value, ["type", "name"])
elif key == "entitlements":
flat_role[key] = format_dict_list(value, ["id", "type", "name"])
elif key == "accessRequestConfig":
flat_role[key] = format_dict_list(
value,
["commentsRequired", "denialCommentsRequired", "reauthorizationRequired"],
prefix="Approval Schemes:\n" if key == "approvalSchemes" else ""
)
elif isinstance(value, (dict, list)) or value is None:
flat_role[key] = str(value)
else:
flat_role[key] = value
flattened_data.append(flat_role)
return flattened_data
# Initialize Dash app
app = dash.Dash(__name__)
app.layout = html.Div([
html.H1("SailPoint ISC Roles - Full Data", style={"textAlign": "center", "color": "#2c3e50"}),
html.Div([
html.Label("Tenant Name:"),
dcc.Input(id="tenant-name-input", type="text", placeholder="Enter tenant name (e.g., example)", style={"marginRight": "10px"}),
html.Label("Client ID:"),
dcc.Input(id="client-id-input", type="text", placeholder="Enter client ID", style={"marginRight": "10px"}),
html.Label("Client Secret:"),
dcc.Input(id="client-secret-input", type="password", placeholder="Enter client secret", style={"marginRight": "30px"}), # Use type="password"
html.Button("Fetch Roles", id="fetch-roles-button", n_clicks=0, style={"marginRight": "180px"}),
dcc.Input(id="exclude-string-input", type="text", placeholder="String excluded from membership", style={"marginRight": "10px", "width": "15%"}),
html.Button("Apply Filter", id="apply-filter-button", n_clicks=0),
], style={"marginBottom": "20px", "textAlign": "center"}),
dcc.Loading(id="loading", children=[html.Div(id="roles-table")], type="circle")
])
# Callback to fetch roles and display in a table
@app.callback(
Output("roles-table", "children"),
[Input("fetch-roles-button", "n_clicks"),
Input("apply-filter-button", "n_clicks")],
[State("tenant-name-input", "value"),
State("client-id-input", "value"),
State("client-secret-input", "value"),
State("exclude-string-input", "value")]
)
def update_roles_table(fetch_clicks, filter_clicks, tenant_name, client_id, client_secret, exclude_string):
if fetch_clicks > 0:
try:
# Construct API URLs dynamically using the tenant name
if not tenant_name or not client_id or not client_secret:
return html.Div("Please provide tenant name, client ID, and client secret.")
BASE_AUTH_URL = f"https://{tenant_name}.api.identitynow.com"
BASE_URL = f"https://{tenant_name}.api.identitynow.com"
TOKEN_URL = f"{BASE_AUTH_URL}/oauth/token"
ROLES_URL = f"{BASE_URL}/beta/roles"
# Authenticate and get access token
auth_data = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
}
response = requests.post(TOKEN_URL, data=auth_data)
response.raise_for_status()
access_token = response.json().get("access_token")
if not access_token:
return html.Div("Failed to authenticate with SailPoint ISC. Check your credentials.")
# Fetch roles
headers = {"Authorization": f"Bearer {access_token}", "Accept": "application/json"}
roles = []
offset = 0
while True:
params = {"offset": offset, "limit": PAGINATION_LIMIT}
response = requests.get(ROLES_URL, headers=headers, params=params)
response.raise_for_status()
data = response.json()
roles.extend(data)
if len(data) < PAGINATION_LIMIT:
break
offset += PAGINATION_LIMIT
# Flatten and filter the role data
flattened_data = flatten_role_data(roles)
df = pd.DataFrame(flattened_data)
if filter_clicks > 0 and exclude_string:
df = df[~df["membership"].str.contains(exclude_string, na=False)]
# Display the filtered data in a table
return dash_table.DataTable(
columns=[{"name": str(i), "id": str(i)} for i in df.columns],
data=df.to_dict("records"),
style_table={"minWidth": "100%", "overflowX": "auto", "height": "800px"},
style_cell={"textAlign": "left", "padding": "5px", "whiteSpace": "normal"},
style_header={"backgroundColor": "#f4f4f4", "fontWeight": "bold", "position": "sticky", "top": 0},
page_size=20,
sort_action="native",
sort_mode="single",
filter_action="native",
)
except Exception as e:
logging.error(f"An error occurred: {e}")
return html.Div(f"An error occurred: {str(e)}")
return html.Div()
# Run the app
if __name__ == "__main__":
app.run_server(debug=True)
Resources
Check out Plotly Dash App Examples for additional ideas to build quick tools or applications to get the data you need.