<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>article Databricks for Identity Systems - Part 4 (Applications) in Technical Blog</title>
    <link>https://community.databricks.com/t5/technical-blog/databricks-for-identity-systems-part-4-applications/ba-p/123179</link>
    <description>&lt;P&gt;Now that we have our &lt;A href="https://community.databricks.com/t5/technical-blog/databricks-for-identity-systems-part-1/ba-p/116122" target="_self"&gt;Users&lt;/A&gt;, &lt;A href="https://community.databricks.com/t5/technical-blog/databricks-for-identity-systems-part-2-groups-and-group-rules/ba-p/116260" target="_self"&gt;Groups&lt;/A&gt;, and &lt;A href="https://community.databricks.com/t5/technical-blog/databricks-for-identity-systems-part-3-group-membership/ba-p/116421" target="_self"&gt;Group Members&lt;/A&gt; exported to Databricks; it's time us to grab the applications that these users and groups are assigned to. Much like the previous walkthroughs, we will focus on how I created my Notebooks to query the Okta APIs for the data that I am interested in.&lt;/P&gt;
&lt;P&gt;You may have noticed that I target a specific object type per blog entry. This is actually how I have my Notebooks set up. Each object type is it's own Notebook, so that I can schedule them independently or as a number of multi-task jobs. This is important because of 2 things:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;This just happened to be how I created the Notebooks, because I didn't really have a lot of time to think about a design as I was initially learning the platform.&lt;/LI&gt;
&lt;LI&gt;It made things easier (for me) to troubleshoot when something went wrong.&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;And with that...&lt;/P&gt;
&lt;P&gt;&lt;FONT size="5"&gt;Let's begin the operation!&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;As with our previous Notebook, let's&amp;nbsp;make sure we have the modules/libraries installed for this Notebook and restart our kernel if we had to install any of the modules.&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;import importlib.util
import sys

mods = ['nest_asyncio', 'okta']
restart_required = False
for mod in mods:
  spec = importlib.util.find_spec(str(mod))
  if spec is not None:
    print(f'{mod} already installed')
  else:
    %pip install {mod}
    restart_required=True

if restart_required==True:
  dbutils.library.restartPython()&lt;/LI-CODE&gt;
&lt;P&gt;&lt;FONT size="5"&gt;Shhh...It's a Secret!&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;I will continue placing a section on each post to remind you about &lt;A href="https://community.databricks.com/t5/technical-blog/databricks-for-identity-systems-part-1-users/ba-p/116122" target="_self"&gt;secret management&lt;/A&gt;, but will not go into any more detail than already described previously. Just...please don't put your secrets in the Notebooks themselves. Make sure you're using Databricks Secret Management instead.&lt;/P&gt;
&lt;P&gt;&lt;FONT size="5"&gt;Let's Get Cooking!&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;Back to our normal cadence, we'll use the Okta SDK again to to collect all of the Applications and Groups assigned to those applications. We prefer to use group assignment for application access, as it's easier to manage. If you don't do this, you may need to change the script to collect assigned individuals instead.&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;import okta
import nest_asyncio
import asyncio
from okta.client import Client as OktaClient


config = {
    'orgUrl': 'https://my-okta-org.okta.com',
    'token': okta_key
}

okta_client = OktaClient(config)

async def get_app_groups(apps):
    app_data = []
    for app in apps:
        #print(f'checking {app}')
        app_groups, resp, err = await okta_client.list_application_group_assignments(app.id)
        while True:
            for group in app_groups:
                app_data.append({"app_id": app.id, "group_id": group.id})
            if resp.has_next():
                app_groups, err = await resp.next()
            else:
                break
    return app_data

async def list_okta_apps():
    app_list = []
    #attempting to filter for only ACTIVE apps, but that didn't work...if anyone knows of a better way to do this, please comment and let me know!
    apps, resp, err = await okta_client.list_applications(query_params={'status': 'ACTIVE'})
    #write the count to the scren so we know there's some progress while the script runs
    print(f'Got {apps.count}')
    while True:
        for app in apps:
            app_list.append(app)
        if resp.has_next():
            apps, err = await resp.next()
        else:
            break
    return app_list

if __name__ == '__main__':
    nest_asyncio.apply()
    #collect all of the apps
    apps = asyncio.run(list_okta_apps())

    #since the URL filter didn't work, we'll just remove the INACTIVE apps by looping through all apps and removing any that aren't ACTIVE
    #remove the INACTIVE apps
    active_apps = []
    for a in apps:
        if a.status == 'ACTIVE':
            active_apps.append(a)

    #get groups assigned to ACTIVE apps        
    app_groups = asyncio.run(get_app_groups(active_apps))&lt;/LI-CODE&gt;
&lt;P&gt;&lt;FONT size="5"&gt;Add your as_of_date!&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;As previously mentioned, this step is optional, but I like to add today's date to the data. This way, we can always see a snapshot of what the environment looked like on any given day.&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;from datetime import date

new_coll = []
today = date.today()

for one in app_groups:
    # Create a copy of the dictionary to avoid modifying the original
    updated_one = one.copy()
    # Add the new key-value pair
    updated_one['as_of_date'] = str(today)
    # Append the updated dictionary to the list
    new_coll.append(updated_one)
app_groups = new_coll&lt;/LI-CODE&gt;
&lt;P&gt;&lt;FONT size="5"&gt;Define the Schema for App to Group Mapping&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;Another simple schema for app groups. We really only need 2 pieces of information which will tie the Groups table (Part 2 of the series) and the Apps table (below) together.&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;import json
from pyspark.sql import SparkSession
from pyspark.sql.functions import col
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, BooleanType, ArrayType

# Create a SparkSession
spark = SparkSession.builder.appName("OktaAppGroups").getOrCreate()

# Define the schema
schema = StructType([
  StructField("app_id", StringType(), True),
  StructField("group_id", StringType(), True),
  StructField("as_of_date", StringType(), True)
])

df = spark.createDataFrame(app_groups, schema)
df_formatted = df.select("app_id", "group_id", "as_of_date")&lt;/LI-CODE&gt;
&lt;P&gt;&lt;FONT size="5"&gt;&amp;nbsp;Write to the Table&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;Since there isn't really much transformation needed here, we'll just write to one table. However, to keep naming consistent, we'll still use the &lt;EM&gt;_formatted&lt;/EM&gt; table name.&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;df_formatted.write.option("mergeSchema", "true").saveAsTable("users.jack_zaldivar.okta_prod_app_groups_formatted", mode="append") &lt;/LI-CODE&gt;
&lt;P&gt;&lt;FONT size="5"&gt;Two for One&lt;/FONT&gt;&lt;/P&gt;
&lt;P&gt;Since we had to get the Application list in order to get the App Groups assigned to those applications, we should take advantage of that! We have the App list in memory as &lt;STRONG&gt;active_apps&lt;/STRONG&gt;. Let's add our &lt;STRONG&gt;as_of_date&lt;/STRONG&gt; to that collection next. While running this next cell, I also ran into some issues where an app may not have a defined &lt;STRONG&gt;accessPolicy&lt;/STRONG&gt;. Because of this, I just added a little check for each app and created a new key in the dictionary for to handle &lt;EM&gt;null&lt;/EM&gt; &lt;STRONG&gt;accessPolicies&lt;/STRONG&gt;.&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;from datetime import date

new_coll = []
today = date.today()

for one in apps:
    # Create a copy of the dictionary to avoid modifying the original
    updated_one = one.__dict__.copy()
    # Add the new key-value pair
    if 'accessPolicy' in one.links:
        #get attached policy by splitting the href and taking the last element
        tmp_pol = str(one.links['accessPolicy']['href'])
        split_pol = str(tmp_pol).split('/')
        updated_one['access_policy'] = split_pol[len(split_pol)-1]
    else:
        updated_one['access_policy'] = ''
    updated_one['as_of_date'] = str(today)
    # Append the updated dictionary to the list
    new_coll.append(updated_one)
apps = new_coll&lt;/LI-CODE&gt;
&lt;P&gt;&lt;FONT size="5"&gt;Define our App Schema&lt;/FONT&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;import json
from pyspark.sql import SparkSession
from pyspark.sql.functions import col
from pyspark.sql.types import StructType, StructField, StringType, IntegerType, BooleanType, ArrayType

# Create a SparkSession
spark = SparkSession.builder.appName("OktaGroups").getOrCreate()

# Define the schema
schema = StructType([
  StructField("id", StringType(), True),
  StructField("name", StringType(), True),
  StructField("label", StringType(), True),
  StructField("status", StringType(), True),
  StructField("last_updated", StringType(), True),
  StructField("created", StringType(), True),
  StructField("accessibility", StructType([
    StructField("self_service", StringType(), True),
    StructField("error_redirect_url", StringType(), True),
    StructField("login_redirect_url", StringType(), True),
  ]), True),
  StructField("visibility", StructType([
    StructField("auto_submit_toolbar", StringType(), True),
    StructField("hide", StructType([
      StructField("i_os", BooleanType(), True),
      StructField("web", BooleanType(), True),
    ])),
  ])),
  StructField("features", ArrayType(StringType()), True),
  StructField("sign_on_mode", StringType(), True),
  StructField("access_policy", StringType(), True),
  StructField("as_of_date", StringType(), True)
])

df_apps = spark.createDataFrame(apps, schema)
df_apps_formatted = df_apps.select("id", "name", "label", "status", "last_updated", "created", "accessibility.*", "visibility.auto_submit_toolbar", "visibility.hide.*", "features", "sign_on_mode", "access_policy", "as_of_date")&lt;/LI-CODE&gt;
&lt;P&gt;&lt;FONT size="5"&gt;Write to the Apps Table&lt;/FONT&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;df_apps.write.option("mergeSchema", "true").saveAsTable("users.jack_zaldivar.okta_prod_apps", mode="append") 
df_apps_formatted.write.option("mergeSchema", "true").saveAsTable("users.jack_zaldivar.okta_prod_apps_formatted", mode="append") &lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;</description>
    <pubDate>Wed, 09 Jul 2025 16:19:20 GMT</pubDate>
    <dc:creator>jack_zaldivar</dc:creator>
    <dc:date>2025-07-09T16:19:20Z</dc:date>
    <item>
      <title>Databricks for Identity Systems - Part 4 (Applications)</title>
      <link>https://community.databricks.com/t5/technical-blog/databricks-for-identity-systems-part-4-applications/ba-p/123179</link>
      <description>&lt;P&gt;Continue building your identity dataset!&lt;/P&gt;</description>
      <pubDate>Wed, 09 Jul 2025 16:19:20 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/databricks-for-identity-systems-part-4-applications/ba-p/123179</guid>
      <dc:creator>jack_zaldivar</dc:creator>
      <dc:date>2025-07-09T16:19:20Z</dc:date>
    </item>
  </channel>
</rss>

