diff --git a/.gitignore b/.gitignore index 36668c9..1f5d77c 100644 --- a/.gitignore +++ b/.gitignore @@ -716,3 +716,4 @@ britive-broker-0.1.3.jar britive-broker-1.0.0.jar *.jar docker.zip +data_input.yaml diff --git a/python/britive/README.md b/python/britive/README.md index 893e0b6..5ff355c 100644 --- a/python/britive/README.md +++ b/python/britive/README.md @@ -1,9 +1,8 @@ - # README ## Overview -This script automates various tasks using the Britive API, such as processing identity providers, user identities, service identities, applications, tags, and more. It interacts with the Britive platform to create and manage resources using a `.json` file (`britive/data_input.json`) as input. +This script automates various tasks using the Britive API, such as processing identity providers, user identities, service identities, applications, tags, and more. It interacts with the Britive platform to create and manage resources using a `.yaml` file (`britive/data_input.yaml`) as input. ## Prerequisites @@ -16,7 +15,7 @@ Ensure that you have Python 3.x installed on your machine. Install the following Python packages before running the script: ```bash -pip install britive simplejson colorama jmespath python-dotenv +pip install britive pyyaml colorama jmespath python-dotenv ``` ### 3. Environment Configuration @@ -32,29 +31,21 @@ This information is required for the script to connect to the Britive API. ### 4. Input Data -The script reads from a JSON file located at `britive/data_input.json`, which contains the necessary information to create users, applications, tags, and more. Ensure that this file is properly formatted. - -Example structure of `data_input.json`: - -```json -{ - "users": [ - { - "email": "user@example.com", - "firstname": "John", - "lastname": "Doe", - "username": "johndoe", - "idp": "Britive" - } - ], - "tags": [ - { - "name": "Tag1", - "description": "Sample tag" - } - ] - ... -} +The script reads from a YAML file located at `britive/data_input.yaml`, which contains the necessary information to create users, applications, tags, and more. Ensure that this file is properly formatted. + +Example structure of `data_input.yaml`: + +```yaml +users: + - email: user@example.com + firstname: John + lastname: Doe + username: johndoe + idp: Britive + +tags: + - name: Tag1 + description: Sample tag ``` ## Usage @@ -69,38 +60,52 @@ python script.py [options] - `-i`, `--idps`: Process Identity Providers - `-u`, `--users`: Process User Identities -- `-s`, `--services`: Process Service Identities +- `-s`, `--services`: Process Service Identities (reserved) - `-t`, `--tags`: Process Tags - `-a`, `--applications`: Process Applications - `-p`, `--profiles`: Process Profiles for each application - `-n`, `--notification`: Process Notification Mediums - `-r`, `--resourceTypes`: Process creation of Resource Types -- `-b`, `--brokerPool`: Process broker pool. Creates one primary broker pool +- `-b`, `--brokerPool`: Process broker pool (creates one primary broker pool) +- `--dry-run`: Simulate operations without making any changes to the Britive tenant -### Example +### Examples -To process users and tags, run: +Process users and tags: ```bash python script.py --users --tags ``` -### Output +Simulate processing identity providers and applications without making changes: + +```bash +python script.py --idps --applications --dry-run +``` -The script outputs logs to the console, showing the progress for each resource being processed (e.g., users, applications, tags). +Process everything in dry-run mode: + +```bash +python script.py -i -u -t -a -p -n -r -b --dry-run +``` + +## Output + +The script outputs logs to the console, showing the progress for each resource being processed (e.g., users, applications, tags). In `--dry-run` mode, it will print what would be created without making any API calls. ## Script Flow -1. **Environment Setup**: The script loads environment variables from the `.env` file. -2. **Input Parsing**: Reads `britive/data_input.json` for user, application, tag, and other resource data. -3. **Britive API Interaction**: The script uses the Britive API to create and manage the specified resources. -4. **Command-line Argument Handling**: You can choose which resources to process (identity providers, users, tags, etc.) using command-line arguments. -5. **Data Persistence**: Updates made to users, tags, or applications are written back to `data_input.json`. +1. **Environment Setup**: Loads environment variables from the `.env` file. +2. **Input Parsing**: Reads `britive/data_input.yaml` for user, application, tag, and other resource data. +3. **Britive API Interaction**: Uses the Britive API to create and manage the specified resources. +4. **Command-line Argument Handling**: Choose which resources to process using command-line flags. +5. **Dry-Run Simulation**: Use `--dry-run` to preview the operations without executing them. ## Error Handling -If there is an issue with the Britive API or the input data, the script will print warning or error messages using the `colorama` library, providing visual cues with different colors (red for errors, yellow for warnings, blue for info, green for success). - -## License +If there is an issue with the Britive API or the input data, the script will print warning or error messages using the `colorama` library, providing visual cues with different colors: -This project is licensed under the MIT License. +- Red for errors +- Yellow for warnings +- Blue for informational messages +- Green for success indicators diff --git a/python/britive/data_input-template.yaml b/python/britive/data_input-template.yaml index 9ffa275..9935d38 100644 --- a/python/britive/data_input-template.yaml +++ b/python/britive/data_input-template.yaml @@ -1,187 +1,192 @@ apps: -- name: Britive - description: Britive for Britive. Short-lived Admin access - type: Britive - envs: - - name: POC Tenant - description: Britive PoC Tenant - profiles: - - name: Britive Tenant Admin - description: '' - Expiration: '7200000' -- name: AWS - type: AWS - profiles: - - name: Account Admin - description: Administrative privileges on AWS Account - Expiration: '14400000' - - name: S3 Developer Full Access - description: Developer access into S3 - Expiration: '14400000' -- name: AWS Account - type: AWS Standalone - profiles: - - name: Account Admin - description: Administrative privileges on AWS Account - Expiration: '14400000' - - name: S3 Developer Full Access - description: Developer access into S3 - Expiration: '14400000' -- name: Google Workspace - type: Google Workspace - envs: - - name: Sandbox - description: Google Workspace - profiles: - - name: Google Workspace Admin - description: Temporary elevation for admin role - Expiration: '7200000' + - name: Britive + description: Britive for Britive. Short-lived Admin access + type: Britive + envs: + - name: POC Tenant + description: Britive PoC Tenant + profiles: + - name: Britive Tenant Admin + description: '' + Expiration: '7200000' + - name: AWS + type: AWS + profiles: + - name: Account Admin + description: Administrative privileges on AWS Account + Expiration: '14400000' + - name: S3 Developer Full Access + description: Developer access into S3 + Expiration: '14400000' + - name: AWS Account + type: AWS Standalone + profiles: + - name: Account Admin + description: Administrative privileges on AWS Account + Expiration: '14400000' + - name: S3 Developer Full Access + description: Developer access into S3 + Expiration: '14400000' + - name: Google Workspace + type: Google Workspace + envs: + - name: Sandbox + description: Google Workspace + profiles: + - name: Google Workspace Admin + description: Temporary elevation for admin role + Expiration: '7200000' +brokerPools: + - name: LinuxBrokers + description: Brokers running on Linux machines + - name: WindowsBrokers + description: Brokers running on Windows domain-joined machines idps: -- name: Okta - type: SAML - description: Okta Sandbox connection for SSO and SCIM + - name: Okta + type: SAML + description: Okta Sandbox connection for SSO and SCIM notification: -- description: Slack for approval and other Britive notices - name: Slack_TEST - url: https://britive-hq.slack.com/api/ - type: slack + - description: Slack for approval and other Britive notices + name: Slack_TEST + url: https://britive-hq.slack.com/api/ + type: slack resourcesTypes: -- name: ActiveDirectory - description: Active Directory Domains for permission management - permissions: - - name: AddUserToGroup - description: Adds user to group - variables: - - user - - group - checkout: '' - checkin: '' - profiles: - - name: Domain Admin Group - description: '' - Expiration: '7200000' - resources: - - name: Domain01 - description: '' -- name: HelloWorld - description: Hellow World | Sample broker bootstrap and unit test - permissions: - - name: ConfirmUser - description: Returns Username upon checkout - variables: - - username - checkout: echo $username - checkin: echo 'checked-in' - profiles: - - name: Hellow World - description: '' - Expiration: '1200000' - resources: - - name: Server1 - description: '' -- name: LinuxSSH - description: '' - permissions: - - name: '' - description: '' - variables: - - '' - - '' - checkout: '' - checkin: '' - profiles: - - name: SSH Sudo - description: '' - Expiration: '14400000' - - name: SSH NoSudo - description: '' - Expiration: '14400000' - resources: - - name: Server01 - description: '' -- name: WindowsRDP - description: '' - permissions: - - name: '' - description: '' - variables: - - '' - - '' - checkout: '' - checkin: '' - profiles: - - name: Local Admin - description: '' - Expiration: '7200000' - resources: - - name: Server01 - description: '' -- name: AuroraMySQL - description: '' - permissions: - - name: '' - description: '' - variables: - - '' - - '' - checkout: '' - checkin: '' - profiles: - - name: DB Admin - description: '' - Expiration: '7200000' - resources: - - name: DB01 - description: '' -- name: AuroraPostgres - description: '' - permissions: - - name: '' - description: '' - variables: - - '' - - '' - checkout: '' - checkin: '' - profiles: - - name: DB Admin - description: Temporary elevation for Database Admin - Expiration: '7200000' - resources: - - name: DB01 - description: '' + - name: ActiveDirectory + description: Active Directory Domains for permission management + permissions: + - name: AddUserToGroup + description: Adds user to group + variables: + - user + - group + checkout: '' + checkin: '' + profiles: + - name: Domain Admin Group + description: '' + Expiration: '7200000' + resources: + - name: Domain01 + description: '' + - name: HelloWorld + description: Hellow World | Sample broker bootstrap and unit test + permissions: + - name: ConfirmUser + description: Returns Username upon checkout + variables: + - username + checkout: echo $username + checkin: echo 'checked-in' + profiles: + - name: Hellow World + description: '' + Expiration: '1200000' + resources: + - name: Server1 + description: '' + - name: LinuxSSH + description: '' + permissions: + - name: '' + description: '' + variables: + - '' + - '' + checkout: '' + checkin: '' + profiles: + - name: SSH Sudo + description: '' + Expiration: '14400000' + - name: SSH NoSudo + description: '' + Expiration: '14400000' + resources: + - name: Server01 + description: '' + - name: WindowsRDP + description: '' + permissions: + - name: '' + description: '' + variables: + - '' + - '' + checkout: '' + checkin: '' + profiles: + - name: Local Admin + description: '' + Expiration: '7200000' + resources: + - name: Server01 + description: '' + - name: AuroraMySQL + description: '' + permissions: + - name: '' + description: '' + variables: + - '' + - '' + checkout: '' + checkin: '' + profiles: + - name: DB Admin + description: '' + Expiration: '7200000' + resources: + - name: DB01 + description: '' + - name: AuroraPostgres + description: '' + permissions: + - name: '' + description: '' + variables: + - '' + - '' + checkout: '' + checkin: '' + profiles: + - name: DB Admin + description: Temporary elevation for Database Admin + Expiration: '7200000' + resources: + - name: DB01 + description: '' tags: -- description: Admins participating in the user management - name: User Admins -- name: Help Desk Users - description: Users participating in the Demo -- name: Managers - description: Approvers will approve the profiles or checkouts -- name: Tenant Admins - description: Admins managing the Britive Platform + - name: User Admins + description: Admins participating in the user management + - name: Help Desk Users + description: Users participating in the Demo + - name: Managers + description: Approvers will approve the profiles or checkouts + - name: Tenant Admins + description: Admins managing the Britive Platform users: -- firstname: Adam - lastname: Woodard - email: adamwoodard@mailinator.com - username: adamwoodard@mailinator.com - idp: Britive -- firstname: Paul - lastname: Barnett - email: paulbarnett@mailinator.com - username: paulbarnett@mailinator.com - idp: Britive -- firstname: Brianna - lastname: Russell - email: briannarussell@mailinator.com - username: briannarussell@mailinator.com - idp: Britive -- firstname: Alex - lastname: Davis - email: alexdavis@mailinator.com - username: alexdavis@mailinator.com - idp: Britive -- firstname: Christine - lastname: Nichols - email: christinenichols@mailinator.com - username: christinenichols@mailinator.com - idp: Britive + - username: adamwoodard@mailinator.com + firstname: Adam + lastname: Woodard + email: adamwoodard@mailinator.com + idp: Britive + - username: paulbarnett@mailinator.com + firstname: Paul + lastname: Barnett + email: paulbarnett@mailinator.com + idp: Britive + - username: briannarussell@mailinator.com + firstname: Brianna + lastname: Russell + email: briannarussell@mailinator.com + idp: Britive + - username: alexdavis@mailinator.com + firstname: Alex + lastname: Davis + email: alexdavis@mailinator.com + idp: Britive + - username: christinenichols@mailinator.com + firstname: Christine + lastname: Nichols + email: christinenichols@mailinator.com + idp: Britive diff --git a/python/britive/data_input.json b/python/britive/data_input.json deleted file mode 100644 index a31103d..0000000 --- a/python/britive/data_input.json +++ /dev/null @@ -1,288 +0,0 @@ -{ - "apps": [ - { - "name": "Britive", - "description": "Britive for Britive. Short-lived Admin access", - "type": "Britive", - "envs": [ - { - "name": "POC Tenant", - "description": "Britive PoC Tenant" - } - ], - "profiles": [ - { - "name": "Britive Tenant Admin", - "description": "", - "Expiration": "7200000" - } - ] - }, - { - "name": "AWS", - "type": "AWS", - "profiles": [ - { - "name": "Account Admin", - "description": "Administrative privileges on AWS Account", - "Expiration": "14400000" - }, - { - "name": "S3 Developer Full Access", - "description": "Developer access into S3", - "Expiration": "14400000" - } - ] - }, - { - "name": "Google Workspace", - "type": "Google Workspace", - "envs": [ - { - "name": "Sandbox", - "description": "Google Workspace" - } - ], - "profiles": [ - { - "name": "Google Workspace Admin", - "description": "Temporary elevation for admin role", - "Expiration": "7200000" - } - ] - } - ], - "idps": [ - { - "name": "Okta", - "type": "SAML", - "description": "Okta Sandbox connection for SSO and SCIM" - } - ], - "notification": [ - { - "description": "Slack for approval and other Britive notices", - "name": "Slack_TEST", - "url": "https://britive-hq.slack.com/api/", - "type": "slack" - } - ], - "resourcesTypes": [ - { - "name": "ActiveDirectory", - "description": "Active Directory Domains for permission management", - "permissions": [ - { - "name": "AddUserToGroup", - "description": "Adds user to group", - "variables": [ "user", "group" ], - "checkout": "", - "checkin": "" - } - ], - "profiles": [ - { - "name": "Domain Admin Group", - "description": "", - "Expiration": "7200000" - } - ], - "resources": [ - { - "name": "Domain01", - "description": "" - } - ] - }, - { - "name": "HelloWorld", - "description": "Hellow World | Sample broker bootstrap and unit test", - "permissions": [ - { - "name": "ConfirmUser", - "description": "Returns Username upon checkout", - "variables": [ "username" ], - "checkout": "echo $username", - "checkin": "echo 'checked-in'" - } - ], - "profiles": [ - { - "name": "Hellow World", - "description": "", - "Expiration": "1200000" - } - ], - "resources": [ - { - "name": "Server1", - "description": "" - } - ] - }, - { - "name": "LinuxSSH", - "description": "", - "permissions": [ - { - "name": "", - "description": "", - "variables": [ "", "" ], - "checkout": "", - "checkin": "" - } - ], - "profiles": [ - { - "name": "SSH Sudo", - "description": "", - "Expiration": "14400000" - }, - { - "name": "SSH NoSudo", - "description": "", - "Expiration": "14400000" - } - ], - "resources": [ - { - "name": "Server01", - "description": "" - } - ] - }, - { - "name": "WindowsRDP", - "description": "", - "permissions": [ - { - "name": "", - "description": "", - "variables": [ "", "" ], - "checkout": "", - "checkin": "" - } - ], - "profiles": [ - { - "name": "Local Admin", - "description": "", - "Expiration": "7200000" - } - ], - "resources": [ - { - "name": "Server01", - "description": "" - } - ] - }, - { - "name": "AuroraMySQL", - "description": "", - "permissions": [ - { - "name": "", - "description": "", - "variables": [ "", "" ], - "checkout": "", - "checkin": "" - } - ], - "profiles": [ - { - "name": "DB Admin", - "description": "", - "Expiration": "7200000" - } - ], - "resources": [ - { - "name": "DB01", - "description": "" - } - ] - }, - { - "name": "AuroraPostgres", - "description": "", - "permissions": [ - { - "name": "", - "description": "", - "variables": [ "", "" ], - "checkout": "", - "checkin": "" - } - ], - "profiles": [ - { - "name": "DB Admin", - "description": "Temporary elevation for Database Admin", - "Expiration": "7200000" - } - ], - "resources": [ - { - "name": "DB01", - "description": "" - } - ] - } - ], - "tags": [ - { - "description": "Admins participating in the user management", - "name": "User Admins" - }, - { - "name": "Help Desk Users", - "description": "Users participating in the Demo" - }, - { - "name": "Managers", - "description": "Approvers will approve the profiles or checkouts" - }, - { - "name": "Tenant Admins", - "description": "Admins managing the Britive Platform" - } - ], - "users": [ - { - "firstname": "Adam", - "lastname": "Woodard", - "email": "adamwoodard@mailinator.com", - "username": "adamwoodard@mailinator.com", - "idp": "Britive" - }, - { - "firstname": "Paul", - "lastname": "Barnett", - "email": "paulbarnett@mailinator.com", - "username": "paulbarnett@mailinator.com", - "idp": "Britive" - }, - { - "firstname": "Brianna", - "lastname": "Russell", - "email": "briannarussell@mailinator.com", - "username": "briannarussell@mailinator.com", - "idp": "Britive" - }, - { - "firstname": "Alex", - "lastname": "Davis", - "email": "alexdavis@mailinator.com", - "username": "alexdavis@mailinator.com", - "idp": "Britive" - }, - { - "firstname": "Christine", - "lastname": "Nichols", - "email": "christinenichols@mailinator.com", - "username": "christinenichols@mailinator.com", - "idp": "Britive" - } - ] -} \ No newline at end of file diff --git a/python/britive/setup.py b/python/britive/setup.py index 7ffbe33..58d78bf 100755 --- a/python/britive/setup.py +++ b/python/britive/setup.py @@ -1,39 +1,36 @@ #!/usr/bin/env python3 +""" +This script automates Britive tenant setup using a YAML data input file. + +Features: +- Connects to Britive using credentials from a .env file +- Processes various entity types (users, apps, IDPs, tags, notifications, etc.) +- Supports --dry-run mode to preview actions without executing them + +Requirements: +- .env file with BRITIVE_TENANT and BRITIVE_API_TOKEN +- YAML input file (e.g., britive/data_input.yaml) +- Required packages: britive, jmespath, pyyaml, python-dotenv, colorama +""" + import argparse import os import secrets import string - -from dotenv import load_dotenv - -try: - import simplejson as json -except ImportError: - import json - +import yaml import jmespath from colorama import Fore, Style - +from dotenv import load_dotenv from britive.britive import Britive -""" -This code requires a .env file with the information about the Britive tenant to connect to. -Please create a .env file with following key/value pairs: -BRITIVE_TENANT -BRITIVE_API_TOKEN -""" - - -# Color definitions +# Color-coded output for readability caution = f"{Style.BRIGHT}{Fore.RED}" warn = f"{Style.BRIGHT}{Fore.YELLOW}" info = f"{Style.BRIGHT}{Fore.BLUE}" green = f"{Style.BRIGHT}{Fore.GREEN}" -# Load Environment Variables +# Load environment variables from .env file load_dotenv() - -# Validate required environment variables BRITIVE_TENANT = os.getenv("BRITIVE_TENANT") BRITIVE_API_TOKEN = os.getenv("BRITIVE_API_TOKEN") @@ -43,31 +40,27 @@ ) exit(1) -# Load JSON data file -DATA_FILE_INPUT = "britive/data_input.json" - +# Load YAML input data +DATA_FILE_INPUT = "britive/data_input.yaml" try: with open(DATA_FILE_INPUT, "r") as file: - data = json.load(file) + data = yaml.safe_load(file) except FileNotFoundError: - print( - f"{caution}Data file '{DATA_FILE_INPUT}' not found. Ensure it exists.{Style.RESET_ALL}" - ) + print(f"{caution}Data file '{DATA_FILE_INPUT}' not found.{Style.RESET_ALL}") exit(1) -except json.JSONDecodeError: - print( - f"{caution}Error decoding JSON in file '{DATA_FILE_INPUT}'. Check for syntax errors.{Style.RESET_ALL}" - ) +except yaml.YAMLError as e: + print(f"{caution}YAML parsing error in '{DATA_FILE_INPUT}': {e}{Style.RESET_ALL}") exit(1) +# Initialize Britive API session try: br = Britive(tenant=BRITIVE_TENANT, token=BRITIVE_API_TOKEN) - print(f"{info}Connection to: {BRITIVE_TENANT}{Style.RESET_ALL}") + print(f"{info}Connected to Britive tenant: {BRITIVE_TENANT}{Style.RESET_ALL}") except Exception as e: print(f"{caution}Failed to initialize Britive API: {e}{Style.RESET_ALL}") exit(1) -# Get the id for the local (Britive) Identity Provider +# Get ID of local Britive identity provider (used for tagging) try: idp_list = br.identity_management.identity_providers.list() britive_idp = next( @@ -79,10 +72,13 @@ print(f"{caution}Error retrieving Britive IDP: {e}{Style.RESET_ALL}") exit(1) +# Will be set from --dry-run argument +DRY_RUN = False + def main(): - # The argument parser - parser = argparse.ArgumentParser(description="Process some command-line arguments.") + global DRY_RUN + parser = argparse.ArgumentParser(description="Britive tenant bootstrap utility") parser.add_argument( "-i", "--idps", action="store_true", help="Process Identity providers" ) @@ -103,22 +99,23 @@ def main(): help="Process Profiles for each application", ) parser.add_argument( - "-n", "--notification", action="store_true", help="Process Notification Medium" + "-n", "--notification", action="store_true", help="Process Notification Mediums" ) parser.add_argument( - "-r", - "--resourceTypes", - action="store_true", - help="Process creation of Resource Types", + "-r", "--resourceTypes", action="store_true", help="Process Resource Types" ) parser.add_argument( "-b", "--brokerPool", action="store_true", - help="Process creation of a single broker pool", + help="Process creation of Broker Pool", + ) + parser.add_argument( + "--dry-run", action="store_true", help="Simulate operations without API calls" ) args = parser.parse_args() + DRY_RUN = args.dry_run try: if args.idps: @@ -138,277 +135,218 @@ def main(): if args.resourceTypes: process_resource_types() except Exception as e: - print(f"{caution}An error occurred while processing: {e}{Style.RESET_ALL}") + print(f"{caution}An error occurred: {e}{Style.RESET_ALL}") exit(1) - # Save updated data back to JSON - try: - with open(DATA_FILE_INPUT, "w") as f: - json.dump(data, f) - except Exception as e: - print( - f"{caution}Failed to save updates to '{DATA_FILE_INPUT}': {e}{Style.RESET_ALL}" - ) - def process_tags(): - tags = jmespath.search("tags", data) + tags = jmespath.search("tags", data) or [] print(f"{info}Processing {len(tags)} Tags...{Style.RESET_ALL}") for tag in tags: - print(f"{tag['name']}") - tag_response = br.identity_management.tags.create( - name=tag["name"], description=tag["description"], idp=britive_idp - ) - tag["id"] = tag_response["userTagId"] + if DRY_RUN: + print(f"{warn}[Dry-Run] Would create tag: {tag['name']}{Style.RESET_ALL}") + tag["id"] = f"simulated-{tag['name']}-id" + else: + print(f"Tage name: {tag['name']}") + tag_response = br.identity_management.tags.create( + name=tag["name"], description=tag["description"], idp=britive_idp + ) + tag["id"] = tag_response["userTagId"] def process_users(): - try: - users = jmespath.search("users", data) - if not users: - print(f"{caution}No users found in the input data.{Style.RESET_ALL}") - return - print(f"{info}Processing {len(users)} users...{Style.RESET_ALL}") - except Exception as e: - print(f"{caution}Failed to extract users from data: {e}{Style.RESET_ALL}") - return - + users = jmespath.search("users", data) or [] + print(f"{info}Processing {len(users)} Users...{Style.RESET_ALL}") for user in users: try: - print(f"{user['email']} on {user['idp']}") - try: - idp_response = br.identity_management.identity_providers.get_by_name( - identity_provider_name=user["idp"] - ) - user_idp = idp_response["id"] - print(f"{info}User IdP {user['idp']} : {user_idp}{Style.RESET_ALL}") - except Exception as e: - print( - f"{caution}Error fetching IdP for {user['email']}: {e}{Style.RESET_ALL}" - ) - continue - - try: - random_string = "".join( - secrets.choice( - string.ascii_letters + string.digits + string.punctuation - ) - for _ in range(12) - ) - print(f"{warn}{random_string}{Style.RESET_ALL}") - except Exception as e: + idp_response = br.identity_management.identity_providers.get_by_name( + user["idp"] + ) + user_idp = idp_response["id"] + + password = "".join( + secrets.choice(string.ascii_letters + string.digits) for _ in range(12) + ) + + if DRY_RUN: print( - f"{caution}Failed to generate password for {user['email']}: {e}{Style.RESET_ALL}" + f"{warn}[Dry-Run] Would create user: {user['email']}{Style.RESET_ALL}" ) - continue - - try: - user_response = br.identity_management.users.create( + user["id"] = f"simulated-{user['username']}-id" + else: + print(f"{user['email']} on {user['idp']}") + response = br.identity_management.users.create( idp=user_idp, email=user["email"], firstName=user["firstname"], lastName=user["lastname"], username=user["username"], status="active", - password=random_string, - ) - user["id"] = user_response["userId"] - except Exception as e: - print( - f"{caution}Failed to create user {user['email']}: {e}{Style.RESET_ALL}" + password=password, ) - continue - - except KeyError as e: - print(f"{caution}Missing required user field: {e}{Style.RESET_ALL}") + user["id"] = response["userId"] except Exception as e: - print( - f"{caution}Unexpected error processing user {user.get('email', 'unknown')}: {e}{Style.RESET_ALL}" - ) + print(f"{caution}Error creating user {user['email']}: {e}{Style.RESET_ALL}") def process_applications(): - app_catalog = jmespath.search( - "[].{name: name, id: catalogAppId}", - br.application_management.applications.catalog(), - ) - apps = jmespath.search(expression="apps", data=data) - print(f"{info}Processing {len(apps)} applications...{Style.RESET_ALL}") + apps = jmespath.search("apps", data) or [] + catalog = br.application_management.applications.catalog() + catalog_map = {app["name"]: app["catalogAppId"] for app in catalog} + print(f"{info}Processing {len(apps)} Applications...{Style.RESET_ALL}") for app in apps: - catalog_id = [ - item["id"] for item in app_catalog if item["name"] == app["type"] - ][0] - app_response = br.application_management.applications.create( - application_name=app["name"], catalog_id=catalog_id - ) - app["id"] = app_response["appContainerId"] + catalog_id = catalog_map.get(app["type"]) + if DRY_RUN: + print( + f"{warn}[Dry-Run] Would create app: {app['name']} with catalog ID: {catalog_id}{Style.RESET_ALL}" + ) + app["id"] = f"simulated-{app['name']}-id" + else: + response = br.application_management.applications.create( + application_name=app["name"], catalog_id=catalog_id + ) + app["id"] = response["appContainerId"] def process_profiles(): - apps = jmespath.search(expression="apps", data=data) + apps = jmespath.search("apps", data) or [] + print(f"{info}Processing profiles for {len(apps)} applications...{Style.RESET_ALL}") for app in apps: - envs = app["envs"] - print(f"{info}Processing Environments for app: {app['name']} {Style.RESET_ALL}") + envs = app.get("envs", []) for env in envs: - br.application_management.environments.create( - application_id=app["id"], - name=env["name"], - description=env["description"], - ) + if DRY_RUN: + print( + f"{warn}[Dry-Run] Would create environment: {env['name']} for app: {app['name']}{Style.RESET_ALL}" + ) + else: + br.application_management.environments.create( + application_id=app["id"], + name=env["name"], + description=env["description"], + ) - # Process Profiles after environments are created - profiles = app["profiles"] - print(f"{info}Processing profiles for app: {app['name']} {Style.RESET_ALL}") + profiles = app.get("profiles", []) for profile in profiles: - br.application_management.profiles.create( - application_id=app["id"], - name=profile["name"], - status="active", - expirationDuration=profile["Expiration"], - ) + if DRY_RUN: + print( + f"{warn}[Dry-Run] Would create profile: {profile['name']} for app: {app['name']}{Style.RESET_ALL}" + ) + else: + br.application_management.profiles.create( + application_id=app["id"], + name=profile["name"], + status="active", + expirationDuration=profile["Expiration"], + ) def process_notification(): - notifications = jmespath.search(expression="notification", data=data) - print( - f"{green}Processing {len(notifications)} Notification Mediums...{Style.RESET_ALL}" - ) - for note in notifications: - print(note["name"]) - br.global_settings.notification_mediums.create( - name=note["name"], - description=note["description"], - notification_medium_type=note["type"], - url=note["url"], - ) + notes = jmespath.search("notification", data) or [] + print(f"{info}Processing {len(notes)} notification mediums...{Style.RESET_ALL}") + for note in notes: + if DRY_RUN: + print( + f"{warn}[Dry-Run] Would create notification: {note['name']}{Style.RESET_ALL}" + ) + else: + br.global_settings.notification_mediums.create( + name=note["name"], + description=note["description"], + notification_medium_type=note["type"], + url=note["url"], + ) def process_idps(): - idps = jmespath.search(expression="idps", data=data) - print(f"{green}Processing {len(idps)} identity providers...{Style.RESET_ALL}") + idps = jmespath.search("idps", data) or [] + print(f"{info}Processing {len(idps)} Identity Providers...{Style.RESET_ALL}") for idp in idps: - idp_response = br.identity_management.identity_providers.create( - name=idp["name"], description=idp["description"] - ) - idp["id"] = idp_response["id"] - idp["ssoConfig"] = idp_response["ssoConfig"] - if idp["type"].lower() == "azure": - br.identity_management.identity_providers.update( - identity_provider_id=idp_response["id"], sso_provider="Azure" + if DRY_RUN: + print(f"{warn}[Dry-Run] Would create IDP: {idp['name']}{Style.RESET_ALL}") + idp["id"] = f"simulated-{idp['name']}-id" + else: + response = br.identity_management.identity_providers.create( + name=idp["name"], description=idp["description"] ) + idp["id"] = response["id"] def process_broker_pool(): - broker_pool = br.access_broker.pools.create( - name="Primary Pool", description="Broker Pool for Britive Broker" - ) - print(f"{green}Create Broker Pool id: {broker_pool['pool-id']}{Style.RESET_ALL}") + bps = jmespath.search("brokerPools", data) or [] + print(f"{info}Processing {len(bps)} Broker Pools...{Style.RESET_ALL}") + for bp in bps: + if DRY_RUN: + print(f"{warn}[Dry-Run] Would create Broker Pool: {bp['name']}{Style.RESET_ALL}") + bp["id"] = f"simulated-{bp['name']}-id" + else: + response = br.access_broker.pools.create( + name=br["name"], description=br["description"] + ) + bp["id"] = response["pool-id"] + print(f"{green}Created Broker Pool: {bp["id"]}{Style.RESET_ALL}") def process_resource_types(): - try: - rts = jmespath.search(expression="resourcesTypes", data=data) - if not rts: - print(f"{warn}No Resource Types found in the data.{Style.RESET_ALL}") - return - - for rt in rts: - try: - rt_response = br.access_broker.resources.types.create( - name=rt["name"], description=rt.get("description", "") - ) - rt["id"] = rt_response["resourceTypeId"] + rts = jmespath.search("resourcesTypes", data) or [] + print(f"{info}Processing {len(rts)} Resource Types...{Style.RESET_ALL}") + for rt in rts: + if DRY_RUN: + print( + f"{warn}[Dry-Run] Would create resource type: {rt['name']}{Style.RESET_ALL}" + ) + rt["id"] = f"simulated-{rt['name']}-id" + else: + response = br.access_broker.resources.types.create( + name=rt["name"], description=rt.get("description", "") + ) + rt["id"] = response["resourceTypeId"] + + perms = rt.get("permissions", []) + for perm in perms: + if DRY_RUN: print( - f"{green}Created Resource-Type: {rt['name']} with id:{rt['id']} {Style.RESET_ALL}" + f"{warn}[Dry-Run] Would create permission: {perm['name']} under {rt['name']}{Style.RESET_ALL}" ) - except Exception as e: - print( - f"{caution}Error creating Resource-Type {rt['name']}: {e}{Style.RESET_ALL}" + else: + br.access_broker.resources.permissions.create( + name=perm["name"], + resource_type_id=rt["id"], + description=perm["description"], + variables=perm["variables"], + checkout_file=perm["checkout"], + checkin_file=perm["checkin"], ) - exit(1) # Skip to the next resource type - # Process Permissions - try: - perms = jmespath.search(expression="permissions", data=rt) or [] + resources = rt.get("resources", []) + for resource in resources: + if DRY_RUN: print( - f"{info}Creating Permissions {len(perms)} : {perms}{Style.RESET_ALL}" + f"{warn}[Dry-Run] Would create resource: {resource['name']} under {rt['name']}{Style.RESET_ALL}" ) - for perm in perms: - try: - perm_response = br.access_broker.resources.permissions.create( - name=perm["name"], - resource_type_id=rt["id"], - description=perm["description"], - variables=perm["variables"], - checkout_file=perm["checkout"], - checkin_file=perm["checkin"], - ) - perm["id"] = perm_response["permissionId"] - except Exception as e: - print( - f"{caution}Error creating Permission {perm['name']}: {e}{Style.RESET_ALL}" - ) - except Exception as e: - print( - f"{caution}Error processing permissions for {rt['name']}: {e}{Style.RESET_ALL}" + else: + br.access_broker.resources.create( + name=resource["name"], + description=resource["description"], + resource_type_id=rt["id"], ) - # Process Resources - try: - resources = jmespath.search(expression="resources", data=rt) or [] - for resource in resources: - try: - resource_response = br.access_broker.resources.create( - name=resource["name"], - description=resource["description"], - resource_type_id=rt["id"], - ) - resource["id"] = resource_response["resourceId"] - print( - f"{green}Created Resource: {resource['name']} with id: {resource['id']}{Style.RESET_ALL}" - ) - except Exception as e: - print( - f"{caution}Error creating Resource {resource['name']}: {e}{Style.RESET_ALL}" - ) - except Exception as e: + profiles = rt.get("profiles", []) + for profile in profiles: + if DRY_RUN: print( - f"{caution}Error processing resources for {rt['name']}: {e}{Style.RESET_ALL}" + f"{warn}[Dry-Run] Would create profile: {profile['name']} and associate to {rt['name']}{Style.RESET_ALL}" ) - - # Process Profiles - try: - profiles = jmespath.search(expression="profiles", data=rt) or [] - for profile in profiles: - try: - profile_response = br.access_broker.profiles.create( - name=profile["name"], - description=profile["description"], - expiration_duration=profile["Expiration"], - ) - profile["id"] = profile_response["profileId"] - print( - f"{green}Created Profile: {profile['name']} with id: {profile['id']}{Style.RESET_ALL}" - ) - assoc = {"Resource-Type": rt["name"]} - br.access_broker.profiles.add_association( - profile_id=profile["id"], associations=assoc - ) - except Exception as e: - print( - f"{caution}Error creating Profile {profile['name']}: {e}{Style.RESET_ALL}" - ) - except Exception as e: - print( - f"{caution}Error processing profiles for {rt['name']}: {e}{Style.RESET_ALL}" + else: + response = br.access_broker.profiles.create( + name=profile["name"], + description=profile["description"], + expiration_duration=profile["Expiration"], + ) + br.access_broker.profiles.add_association( + profile_id=response["profileId"], + associations={"Resource-Type": rt["name"]}, ) - - except Exception as e: - print( - f"{caution}Unexpected error in process_resource_types: {e}{Style.RESET_ALL}" - ) -# Press the green button in the gutter to run the script. if __name__ == "__main__": main() diff --git a/python/britive/setup_legacy.py b/python/britive/setup_legacy.py new file mode 100755 index 0000000..7ffbe33 --- /dev/null +++ b/python/britive/setup_legacy.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python3 +import argparse +import os +import secrets +import string + +from dotenv import load_dotenv + +try: + import simplejson as json +except ImportError: + import json + +import jmespath +from colorama import Fore, Style + +from britive.britive import Britive + +""" +This code requires a .env file with the information about the Britive tenant to connect to. +Please create a .env file with following key/value pairs: +BRITIVE_TENANT +BRITIVE_API_TOKEN +""" + + +# Color definitions +caution = f"{Style.BRIGHT}{Fore.RED}" +warn = f"{Style.BRIGHT}{Fore.YELLOW}" +info = f"{Style.BRIGHT}{Fore.BLUE}" +green = f"{Style.BRIGHT}{Fore.GREEN}" + +# Load Environment Variables +load_dotenv() + +# Validate required environment variables +BRITIVE_TENANT = os.getenv("BRITIVE_TENANT") +BRITIVE_API_TOKEN = os.getenv("BRITIVE_API_TOKEN") + +if not BRITIVE_TENANT or not BRITIVE_API_TOKEN: + print( + f"{caution}Missing BRITIVE_TENANT or BRITIVE_API_TOKEN in environment variables.{Style.RESET_ALL}" + ) + exit(1) + +# Load JSON data file +DATA_FILE_INPUT = "britive/data_input.json" + +try: + with open(DATA_FILE_INPUT, "r") as file: + data = json.load(file) +except FileNotFoundError: + print( + f"{caution}Data file '{DATA_FILE_INPUT}' not found. Ensure it exists.{Style.RESET_ALL}" + ) + exit(1) +except json.JSONDecodeError: + print( + f"{caution}Error decoding JSON in file '{DATA_FILE_INPUT}'. Check for syntax errors.{Style.RESET_ALL}" + ) + exit(1) + +try: + br = Britive(tenant=BRITIVE_TENANT, token=BRITIVE_API_TOKEN) + print(f"{info}Connection to: {BRITIVE_TENANT}{Style.RESET_ALL}") +except Exception as e: + print(f"{caution}Failed to initialize Britive API: {e}{Style.RESET_ALL}") + exit(1) + +# Get the id for the local (Britive) Identity Provider +try: + idp_list = br.identity_management.identity_providers.list() + britive_idp = next( + (item["id"] for item in idp_list if item["name"] == "Britive"), None + ) + if not britive_idp: + raise ValueError("Britive Identity Provider ID not found.") +except Exception as e: + print(f"{caution}Error retrieving Britive IDP: {e}{Style.RESET_ALL}") + exit(1) + + +def main(): + # The argument parser + parser = argparse.ArgumentParser(description="Process some command-line arguments.") + parser.add_argument( + "-i", "--idps", action="store_true", help="Process Identity providers" + ) + parser.add_argument( + "-u", "--users", action="store_true", help="Process User identities" + ) + parser.add_argument( + "-s", "--services", action="store_true", help="Process Service identities" + ) + parser.add_argument("-t", "--tags", action="store_true", help="Process Tags") + parser.add_argument( + "-a", "--applications", action="store_true", help="Process Applications" + ) + parser.add_argument( + "-p", + "--profiles", + action="store_true", + help="Process Profiles for each application", + ) + parser.add_argument( + "-n", "--notification", action="store_true", help="Process Notification Medium" + ) + parser.add_argument( + "-r", + "--resourceTypes", + action="store_true", + help="Process creation of Resource Types", + ) + parser.add_argument( + "-b", + "--brokerPool", + action="store_true", + help="Process creation of a single broker pool", + ) + + args = parser.parse_args() + + try: + if args.idps: + process_idps() + if args.users: + process_users() + if args.tags: + process_tags() + if args.applications: + process_applications() + if args.profiles: + process_profiles() + if args.notification: + process_notification() + if args.brokerPool: + process_broker_pool() + if args.resourceTypes: + process_resource_types() + except Exception as e: + print(f"{caution}An error occurred while processing: {e}{Style.RESET_ALL}") + exit(1) + + # Save updated data back to JSON + try: + with open(DATA_FILE_INPUT, "w") as f: + json.dump(data, f) + except Exception as e: + print( + f"{caution}Failed to save updates to '{DATA_FILE_INPUT}': {e}{Style.RESET_ALL}" + ) + + +def process_tags(): + tags = jmespath.search("tags", data) + print(f"{info}Processing {len(tags)} Tags...{Style.RESET_ALL}") + for tag in tags: + print(f"{tag['name']}") + tag_response = br.identity_management.tags.create( + name=tag["name"], description=tag["description"], idp=britive_idp + ) + tag["id"] = tag_response["userTagId"] + + +def process_users(): + try: + users = jmespath.search("users", data) + if not users: + print(f"{caution}No users found in the input data.{Style.RESET_ALL}") + return + print(f"{info}Processing {len(users)} users...{Style.RESET_ALL}") + except Exception as e: + print(f"{caution}Failed to extract users from data: {e}{Style.RESET_ALL}") + return + + for user in users: + try: + print(f"{user['email']} on {user['idp']}") + try: + idp_response = br.identity_management.identity_providers.get_by_name( + identity_provider_name=user["idp"] + ) + user_idp = idp_response["id"] + print(f"{info}User IdP {user['idp']} : {user_idp}{Style.RESET_ALL}") + except Exception as e: + print( + f"{caution}Error fetching IdP for {user['email']}: {e}{Style.RESET_ALL}" + ) + continue + + try: + random_string = "".join( + secrets.choice( + string.ascii_letters + string.digits + string.punctuation + ) + for _ in range(12) + ) + print(f"{warn}{random_string}{Style.RESET_ALL}") + except Exception as e: + print( + f"{caution}Failed to generate password for {user['email']}: {e}{Style.RESET_ALL}" + ) + continue + + try: + user_response = br.identity_management.users.create( + idp=user_idp, + email=user["email"], + firstName=user["firstname"], + lastName=user["lastname"], + username=user["username"], + status="active", + password=random_string, + ) + user["id"] = user_response["userId"] + except Exception as e: + print( + f"{caution}Failed to create user {user['email']}: {e}{Style.RESET_ALL}" + ) + continue + + except KeyError as e: + print(f"{caution}Missing required user field: {e}{Style.RESET_ALL}") + except Exception as e: + print( + f"{caution}Unexpected error processing user {user.get('email', 'unknown')}: {e}{Style.RESET_ALL}" + ) + + +def process_applications(): + app_catalog = jmespath.search( + "[].{name: name, id: catalogAppId}", + br.application_management.applications.catalog(), + ) + apps = jmespath.search(expression="apps", data=data) + print(f"{info}Processing {len(apps)} applications...{Style.RESET_ALL}") + for app in apps: + catalog_id = [ + item["id"] for item in app_catalog if item["name"] == app["type"] + ][0] + app_response = br.application_management.applications.create( + application_name=app["name"], catalog_id=catalog_id + ) + app["id"] = app_response["appContainerId"] + + +def process_profiles(): + apps = jmespath.search(expression="apps", data=data) + for app in apps: + envs = app["envs"] + print(f"{info}Processing Environments for app: {app['name']} {Style.RESET_ALL}") + for env in envs: + br.application_management.environments.create( + application_id=app["id"], + name=env["name"], + description=env["description"], + ) + + # Process Profiles after environments are created + profiles = app["profiles"] + print(f"{info}Processing profiles for app: {app['name']} {Style.RESET_ALL}") + for profile in profiles: + br.application_management.profiles.create( + application_id=app["id"], + name=profile["name"], + status="active", + expirationDuration=profile["Expiration"], + ) + + +def process_notification(): + notifications = jmespath.search(expression="notification", data=data) + print( + f"{green}Processing {len(notifications)} Notification Mediums...{Style.RESET_ALL}" + ) + for note in notifications: + print(note["name"]) + br.global_settings.notification_mediums.create( + name=note["name"], + description=note["description"], + notification_medium_type=note["type"], + url=note["url"], + ) + + +def process_idps(): + idps = jmespath.search(expression="idps", data=data) + print(f"{green}Processing {len(idps)} identity providers...{Style.RESET_ALL}") + for idp in idps: + idp_response = br.identity_management.identity_providers.create( + name=idp["name"], description=idp["description"] + ) + idp["id"] = idp_response["id"] + idp["ssoConfig"] = idp_response["ssoConfig"] + if idp["type"].lower() == "azure": + br.identity_management.identity_providers.update( + identity_provider_id=idp_response["id"], sso_provider="Azure" + ) + + +def process_broker_pool(): + broker_pool = br.access_broker.pools.create( + name="Primary Pool", description="Broker Pool for Britive Broker" + ) + print(f"{green}Create Broker Pool id: {broker_pool['pool-id']}{Style.RESET_ALL}") + + +def process_resource_types(): + try: + rts = jmespath.search(expression="resourcesTypes", data=data) + if not rts: + print(f"{warn}No Resource Types found in the data.{Style.RESET_ALL}") + return + + for rt in rts: + try: + rt_response = br.access_broker.resources.types.create( + name=rt["name"], description=rt.get("description", "") + ) + rt["id"] = rt_response["resourceTypeId"] + print( + f"{green}Created Resource-Type: {rt['name']} with id:{rt['id']} {Style.RESET_ALL}" + ) + except Exception as e: + print( + f"{caution}Error creating Resource-Type {rt['name']}: {e}{Style.RESET_ALL}" + ) + exit(1) # Skip to the next resource type + + # Process Permissions + try: + perms = jmespath.search(expression="permissions", data=rt) or [] + print( + f"{info}Creating Permissions {len(perms)} : {perms}{Style.RESET_ALL}" + ) + for perm in perms: + try: + perm_response = br.access_broker.resources.permissions.create( + name=perm["name"], + resource_type_id=rt["id"], + description=perm["description"], + variables=perm["variables"], + checkout_file=perm["checkout"], + checkin_file=perm["checkin"], + ) + perm["id"] = perm_response["permissionId"] + except Exception as e: + print( + f"{caution}Error creating Permission {perm['name']}: {e}{Style.RESET_ALL}" + ) + except Exception as e: + print( + f"{caution}Error processing permissions for {rt['name']}: {e}{Style.RESET_ALL}" + ) + + # Process Resources + try: + resources = jmespath.search(expression="resources", data=rt) or [] + for resource in resources: + try: + resource_response = br.access_broker.resources.create( + name=resource["name"], + description=resource["description"], + resource_type_id=rt["id"], + ) + resource["id"] = resource_response["resourceId"] + print( + f"{green}Created Resource: {resource['name']} with id: {resource['id']}{Style.RESET_ALL}" + ) + except Exception as e: + print( + f"{caution}Error creating Resource {resource['name']}: {e}{Style.RESET_ALL}" + ) + except Exception as e: + print( + f"{caution}Error processing resources for {rt['name']}: {e}{Style.RESET_ALL}" + ) + + # Process Profiles + try: + profiles = jmespath.search(expression="profiles", data=rt) or [] + for profile in profiles: + try: + profile_response = br.access_broker.profiles.create( + name=profile["name"], + description=profile["description"], + expiration_duration=profile["Expiration"], + ) + profile["id"] = profile_response["profileId"] + print( + f"{green}Created Profile: {profile['name']} with id: {profile['id']}{Style.RESET_ALL}" + ) + assoc = {"Resource-Type": rt["name"]} + br.access_broker.profiles.add_association( + profile_id=profile["id"], associations=assoc + ) + except Exception as e: + print( + f"{caution}Error creating Profile {profile['name']}: {e}{Style.RESET_ALL}" + ) + except Exception as e: + print( + f"{caution}Error processing profiles for {rt['name']}: {e}{Style.RESET_ALL}" + ) + + except Exception as e: + print( + f"{caution}Unexpected error in process_resource_types: {e}{Style.RESET_ALL}" + ) + + +# Press the green button in the gutter to run the script. +if __name__ == "__main__": + main() diff --git a/python/requirements.txt b/python/requirements.txt index 9c83101..ccca6d0 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -1,96 +1,8 @@ -adal==1.2.7 -aiohappyeyeballs==2.4.0 -aiohttp==3.10.5 -aiosignal==1.3.1 -anyio==4.5.0 -attrs==24.2.0 -azure-common==1.1.28 -azure-core==1.31.0 -azure-graphrbac==0.61.1 -azure-identity==1.18.0 -azure-mgmt-authorization==4.0.0 -azure-mgmt-core==1.4.0 -azure-mgmt-resource==23.1.1 -azure-mgmt-resourcegraph==8.0.0 -boto3==1.35.23 -botocore==1.35.23 +boto3~=1.38.15 britive>=4.1.3 -cachetools==5.5.0 -certifi==2024.8.30 -cffi==1.17.1 -charset-normalizer==3.4.0 -circuitbreaker==2.0.0 colorama==0.4.6 -cryptography==42.0.8 -Deprecated==1.2.14 -frozenlist==1.4.1 -google-api-core==2.20.0 -google-api-python-client==2.146.0 -google-auth==2.35.0 -google-auth-httplib2==0.2.0 -google-auth-oauthlib==1.2.1 -google-cloud==0.34.0 -google-cloud-iam==2.15.2 -googleapis-common-protos==1.65.0 -grpc-google-iam-v1==0.13.1 -grpcio==1.66.1 -grpcio-status==1.66.1 -h11==0.14.0 -h2==4.1.0 -hpack==4.0.0 -httpcore==1.0.5 -httplib2==0.22.0 -httpx==0.27.2 -hyperframe==6.0.1 -idna==3.10 -importlib_metadata==8.4.0 -isodate==0.6.1 jmespath==1.0.1 -microsoft-kiota-abstractions==1.3.3 -microsoft-kiota-authentication-azure==1.1.0 -microsoft-kiota-http==1.3.3 -microsoft-kiota-serialization-form==0.1.1 -microsoft-kiota-serialization-json==1.3.2 -microsoft-kiota-serialization-multipart==0.1.0 -microsoft-kiota-serialization-text==1.0.0 -msal==1.31.0 -msal-extensions==1.2.0 -msgraph-core==1.1.3 -msgraph-sdk==1.8.0 -msrest==0.7.1 -msrestazure==0.6.4.post1 -multidict==6.1.0 -oauthlib==3.2.2 -oci==2.134.0 -opentelemetry-api==1.27.0 -opentelemetry-sdk==1.27.0 -opentelemetry-semantic-conventions==0.48b0 -pendulum==3.0.0 -portalocker==2.10.1 -proto-plus==1.24.0 -protobuf==5.28.2 -pyasn1==0.6.1 -pyasn1_modules==0.4.1 -pycparser==2.22 -PyJWT==2.9.0 -pyOpenSSL==24.2.1 -pyparsing==3.1.4 -python-dateutil==2.9.0.post0 python-dotenv==1.0.1 -pytz==2024.2 -requests==2.32.3 -requests-oauthlib==2.0.0 -rsa==4.9 -s3transfer==0.10.2 -simplejson==3.19.3 -six==1.16.0 -sniffio==1.3.1 -std-uritemplate==1.0.6 -time-machine==2.15.0 -typing_extensions==4.12.2 -tzdata==2024.1 -uritemplate==4.1.1 -urllib3==2.2.3 -wrapt==1.16.0 -yarl==1.11.1 -zipp==3.20.2 +simplejson~=3.19.2 + +PyYAML~=6.0.2 \ No newline at end of file