4 Commits

Author SHA1 Message Date
Henrik Jess Nielsen
b8eb1d8085 feat(bicep): add all 682 Azure roles from rbaclookup module
All checks were successful
Build and Deploy iLSP / test (push) Successful in 23s
Build and Deploy iLSP / build-and-deploy (push) Successful in 1m32s
Replaces hardcoded 38 roles with complete list extracted from
bicep/lookup/rbaclookup:2.x module.

Changes:
- Add scripts/extract_roles_from_rbaclookup.py to parse rbacLookup.bicep
- Generate ilsp/bicep_lsp/azure_roles.json with 682 role names
- Load roles dynamically in modules.py from JSON file
- Now supports ALL Azure built-in roles for autocomplete

Benefits:
- Complete Azure RBAC coverage (682 vs 38 roles)
- Easy to update when new roles are added to rbaclookup module
- Cleaner code (no giant hardcoded list in modules.py)

Usage to update roles:
  python3 scripts/extract_roles_from_rbaclookup.py /path/to/rbacLookup.bicep

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-19 14:27:00 +02:00
Henrik Jess Nielsen
578f88a0e8 fix(bicep): support multi-line array completion for roles and other enums
All checks were successful
Build and Deploy iLSP / test (push) Successful in 22s
Build and Deploy iLSP / build-and-deploy (push) Successful in 1m35s
Adds support for autocomplete in multi-line array syntax like:

    roles: [
      'KEY_VAULT_  ← cursor triggers completion here
    ]

Previously only worked on same line as opening bracket:
    roles: ['KEY_VAULT_  ← only this worked

Changes:
- Walk backwards up to 10 lines to find array opening (e.g. "roles: [")
- Detect if cursor is inside array based on indentation and quotes
- Stop lookback if closing bracket found (not in array anymore)
- Add test case for nested multi-line array completion
- Improve lsp_bridge.py error handling with traceback logging
- Add lsp_bridge_debug.sh wrapper for easier IntelliJ debugging
- Update EDITOR_SETUP.md with correct IntelliJ LSP4IJ config

Fixes autocomplete for deeply nested structures like:
    assignments: [{ roles: ['APP_CONFIGURATION_...'] }]

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-19 13:51:17 +02:00
Henrik Jess Nielsen
d4cb9ae8fb Roles
All checks were successful
Build and Deploy iLSP / test (push) Successful in 21s
Build and Deploy iLSP / build-and-deploy (push) Successful in 48s
2026-05-19 11:46:54 +02:00
Henrik Jess Nielsen
aa37c259ad Roles
All checks were successful
Build and Deploy iLSP / test (push) Successful in 23s
Build and Deploy iLSP / build-and-deploy (push) Successful in 3m13s
2026-05-19 10:28:22 +02:00
12 changed files with 992 additions and 450 deletions

View File

@@ -12,10 +12,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends curl unzip ca-c
COPY scripts/download_bicep_ls.sh /scripts/ COPY scripts/download_bicep_ls.sh /scripts/
RUN chmod +x /scripts/download_bicep_ls.sh && BICEP_VERSION=${BICEP_VERSION} /scripts/download_bicep_ls.sh RUN chmod +x /scripts/download_bicep_ls.sh && BICEP_VERSION=${BICEP_VERSION} /scripts/download_bicep_ls.sh
# Download Azure DevOps pipeline schema for YAML task completions
RUN curl -f -o /azdo-pipeline-schema.json \
"https://raw.githubusercontent.com/microsoft/azure-pipelines-vscode/main/service-schema.json"
# ── Stage 2: Python wheel build ─────────────────────────────────────────────── # ── Stage 2: Python wheel build ───────────────────────────────────────────────
FROM python:3.12-slim AS builder FROM python:3.12-slim AS builder
@@ -45,7 +41,6 @@ RUN apt-get update \
# Copy Bicep Language Server (baked in at build time — no volume needed) # Copy Bicep Language Server (baked in at build time — no volume needed)
COPY --from=bicep-downloader /opt/bicep-langserver /opt/bicep-langserver COPY --from=bicep-downloader /opt/bicep-langserver /opt/bicep-langserver
COPY --from=bicep-downloader /azdo-pipeline-schema.json /azdo-pipeline-schema.json
# Install Python package and dependencies # Install Python package and dependencies
COPY --from=builder /dist/*.whl /tmp/ COPY --from=builder /dist/*.whl /tmp/

View File

@@ -14,17 +14,49 @@ supports LSP WebSocket transport.
## IntelliJ IDEA setup (LSP4IJ) ## IntelliJ IDEA setup (LSP4IJ)
Install the [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin, then: Install the [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin, then add language servers:
### Bicep
1. **Settings → Languages & Frameworks → LSP → Language Servers → +** 1. **Settings → Languages & Frameworks → LSP → Language Servers → +**
- Name: `iLSP Bicep` - Name: `iLSP Bicep`
- Server type: `WebSocket` - Server type: `Command` (not WebSocket — see note below)
- URL: `wss://ilsp.i80.dk/bicep` - Command: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh wss://ilsp.i80.dk/bicep`
- Arguments: (leave blank)
- File pattern: `*.bicep;*.bicepparam`
2. **Add a file-type mapping** under the new server entry: ### YAML (Azure DevOps + GitHub Actions)
- File type: `Bicep` (or pattern `*.bicep`)
3. **Disable Azure Toolkit Bicep completions** — this is critical if you have the 2. **Settings → Languages & Frameworks → LSP → Language Servers → +**
- Name: `iLSP YAML`
- Server type: `Command`
- Command: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh wss://ilsp.i80.dk/yaml`
- Arguments: (leave blank)
- File pattern: `*.yaml;*.yml;azure-pipelines.yml`
### Python (optional)
3. **Settings → Languages & Frameworks → LSP → Language Servers → +**
- Name: `iLSP Python`
- Server type: `Command`
- Command: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh wss://ilsp.i80.dk/python`
- Arguments: (leave blank)
- File pattern: `*.py`
> **Note**: LSP4IJ's WebSocket mode doesn't handle LSP Content-Length framing correctly,
> causing "starting..." to hang or "Stream closed" errors. Use `lsp_bridge_debug.sh` wrapper
> which calls `lsp_bridge.py` and logs errors to `/tmp/lsp_bridge_debug.log` for debugging.
### Troubleshooting
If the language server doesn't start, check the debug log:
```bash
tail -f /tmp/lsp_bridge_debug.log
```
### Disable conflicting plugins
**Disable Azure Toolkit Bicep completions** — this is critical if you have the
Azure Toolkit plugin installed. Without this step, IDEA merges completions from Azure Toolkit plugin installed. Without this step, IDEA merges completions from
both LSP4IJ and Azure Toolkit, resulting in noisy suggestions (`resource`, both LSP4IJ and Azure Toolkit, resulting in noisy suggestions (`resource`,
`projectName`, Bicep schema types, etc.) appearing alongside iLSP results. `projectName`, Bicep schema types, etc.) appearing alongside iLSP results.

View File

@@ -0,0 +1,684 @@
[
"ACCESS_REVIEW_OPERATOR_SERVICE_ROLE",
"ACRDELETE",
"ACRIMAGESIGNER",
"ACRPULL",
"ACRPUSH",
"ACRQUARANTINEREADER",
"ACRQUARANTINEWRITER",
"ADVISOR_RECOMMENDATIONS_CONTRIBUTOR_ASSESSMENTS_AND_REVIEWS",
"ADVISOR_REVIEWS_CONTRIBUTOR",
"ADVISOR_REVIEWS_READER",
"AGENTLESS_SCANNING_FOR_SERVERLESS_SCANNER_SERVICE_ROLE",
"AGFOOD_PLATFORM_DATASET_ADMIN",
"AGFOOD_PLATFORM_SENSOR_PARTNER_CONTRIBUTOR",
"AGFOOD_PLATFORM_SERVICE_ADMIN",
"AGFOOD_PLATFORM_SERVICE_CONTRIBUTOR",
"AGFOOD_PLATFORM_SERVICE_READER",
"ANYBUILD_BUILDER",
"API_MANAGEMENT_DEVELOPER_PORTAL_CONTENT_EDITOR",
"API_MANAGEMENT_SERVICE_CONTRIBUTOR",
"API_MANAGEMENT_SERVICE_OPERATOR_ROLE",
"API_MANAGEMENT_SERVICE_READER_ROLE",
"API_MANAGEMENT_SERVICE_WORKSPACE_API_DEVELOPER",
"API_MANAGEMENT_SERVICE_WORKSPACE_API_PRODUCT_MANAGER",
"API_MANAGEMENT_WORKSPACE_API_DEVELOPER",
"API_MANAGEMENT_WORKSPACE_API_PRODUCT_MANAGER",
"API_MANAGEMENT_WORKSPACE_CONTRIBUTOR",
"API_MANAGEMENT_WORKSPACE_READER",
"APPGW_FOR_CONTAINERS_CONFIGURATION_MANAGER",
"APPLICATION_GROUP_CONTRIBUTOR",
"APPLICATION_INSIGHTS_COMPONENT_CONTRIBUTOR",
"APPLICATION_INSIGHTS_SNAPSHOT_DEBUGGER",
"APP_COMPLIANCE_AUTOMATION_ADMINISTRATOR",
"APP_COMPLIANCE_AUTOMATION_READER",
"APP_CONFIGURATION_CONTRIBUTOR",
"APP_CONFIGURATION_DATA_OWNER",
"APP_CONFIGURATION_DATA_READER",
"APP_CONFIGURATION_DATA_SAS_USER",
"APP_CONFIGURATION_READER",
"APP_SERVICE_ENVIRONMENT_CONTRIBUTOR",
"ARC_GATEWAY_MANAGER",
"ATTESTATION_CONTRIBUTOR",
"ATTESTATION_READER",
"AUTOMATION_CONTRIBUTOR",
"AUTOMATION_JOB_OPERATOR",
"AUTOMATION_OPERATOR",
"AUTOMATION_RUNBOOK_OPERATOR",
"AUTONOMOUS_DEVELOPMENT_PLATFORM_DATA_CONTRIBUTOR_PREVIEW",
"AUTONOMOUS_DEVELOPMENT_PLATFORM_DATA_OWNER_PREVIEW",
"AUTONOMOUS_DEVELOPMENT_PLATFORM_DATA_READER_PREVIEW",
"AUTO_ACTIONS_CONTRIBUTOR",
"AVERE_CONTRIBUTOR",
"AVERE_OPERATOR",
"AVS_ON_FLEET_VIS_ROLE",
"AVS_ORCHESTRATOR_ROLE",
"AZUREML_COMPUTE_OPERATOR",
"AZUREML_DATA_SCIENTIST",
"AZUREML_METRICS_WRITER_PREVIEW",
"AZUREML_REGISTRY_USER",
"AZURE_AI_ACCOUNT_OWNER",
"AZURE_AI_ADMINISTRATOR",
"AZURE_AI_DEVELOPER",
"AZURE_AI_ENTERPRISE_NETWORK_CONNECTION_APPROVER",
"AZURE_AI_INFERENCE_DEPLOYMENT_OPERATOR",
"AZURE_AI_PROJECT_MANAGER",
"AZURE_AI_SAFETY_EVALUATOR",
"AZURE_AI_USER",
"AZURE_API_CENTER_COMPLIANCE_MANAGER",
"AZURE_API_CENTER_CREDENTIAL_ACCESS_READER",
"AZURE_API_CENTER_DATA_READER",
"AZURE_API_CENTER_SERVICE_CONTRIBUTOR",
"AZURE_API_CENTER_SERVICE_READER",
"AZURE_ARC_ENABLED_KUBERNETES_CLUSTER_USER_ROLE",
"AZURE_ARC_KUBERNETES_ADMIN",
"AZURE_ARC_KUBERNETES_CLUSTER_ADMIN",
"AZURE_ARC_KUBERNETES_VIEWER",
"AZURE_ARC_KUBERNETES_WRITER",
"AZURE_ARC_SCVMM_ADMINISTRATOR_ROLE",
"AZURE_ARC_SCVMM_PRIVATE_CLOUDS_ONBOARDING",
"AZURE_ARC_SCVMM_PRIVATE_CLOUD_USER",
"AZURE_ARC_SCVMM_VM_CONTRIBUTOR",
"AZURE_ARC_VMWARE_ADMINISTRATOR_ROLE",
"AZURE_ARC_VMWARE_PRIVATE_CLOUDS_ONBOARDING",
"AZURE_ARC_VMWARE_PRIVATE_CLOUD_USER",
"AZURE_ARC_VMWARE_VM_CONTRIBUTOR",
"AZURE_AUTOMANAGE_CONTRIBUTOR",
"AZURE_BACKUP_SNAPSHOT_CONTRIBUTOR",
"AZURE_BATCH_ACCOUNT_CONTRIBUTOR",
"AZURE_BATCH_ACCOUNT_READER",
"AZURE_BATCH_DATA_CONTRIBUTOR",
"AZURE_BATCH_JOB_SUBMITTER",
"AZURE_BATCH_SERVICE_ORCHESTRATION_ROLE",
"AZURE_BOT_SERVICE_CONTRIBUTOR_ROLE",
"AZURE_CENTER_FOR_SAP_SOLUTIONS_ADMINISTRATOR",
"AZURE_CENTER_FOR_SAP_SOLUTIONS_MANAGEMENT_ROLE",
"AZURE_CENTER_FOR_SAP_SOLUTIONS_READER",
"AZURE_CENTER_FOR_SAP_SOLUTIONS_SERVICE_ROLE",
"AZURE_CENTER_FOR_SAP_SOLUTIONS_SERVICE_ROLE_FOR_MANAGEMENT",
"AZURE_CONNECTED_MACHINE_ONBOARDING",
"AZURE_CONNECTED_MACHINE_RESOURCE_ADMINISTRATOR",
"AZURE_CONNECTED_MACHINE_RESOURCE_MANAGER",
"AZURE_CONNECTED_SQL_SERVER_ONBOARDING",
"AZURE_CONTAINERAPPS_SESSION_EXECUTOR",
"AZURE_CONTAINER_INSTANCES_CONTRIBUTOR_ROLE",
"AZURE_CONTAINER_REGISTRY_SECURE_SUPPLY_CHAIN_OPERATOR_SERVICE_ROLE",
"AZURE_CONTAINER_STORAGE_CONTRIBUTOR",
"AZURE_CONTAINER_STORAGE_OPERATOR",
"AZURE_CONTAINER_STORAGE_OWNER",
"AZURE_CUSTOMER_LOCKBOX_APPROVER_FOR_SUBSCRIPTION",
"AZURE_DEPLOYMENT_STACK_CONTRIBUTOR",
"AZURE_DEPLOYMENT_STACK_OWNER",
"AZURE_DEVICE_ONBOARDING_DISCOVERY_CONTRIBUTOR",
"AZURE_DEVICE_UPDATE_AGENT",
"AZURE_DIGITAL_TWINS_DATA_OWNER",
"AZURE_DIGITAL_TWINS_DATA_READER",
"AZURE_EDGE_HARDWARE_CENTER_ADMINISTRATOR",
"AZURE_EDGE_ON_SITE_DEPLOYMENT_ENGINEER",
"AZURE_EVENT_HUBS_DATA_OWNER",
"AZURE_EVENT_HUBS_DATA_RECEIVER",
"AZURE_EVENT_HUBS_DATA_SENDER",
"AZURE_EXTENSION_FOR_SQL_SERVER_DEPLOYMENT",
"AZURE_FILE_SYNC_ADMINISTRATOR",
"AZURE_FILE_SYNC_READER",
"AZURE_FRONT_DOOR_DOMAIN_CONTRIBUTOR",
"AZURE_FRONT_DOOR_DOMAIN_READER",
"AZURE_FRONT_DOOR_PROFILE_READER",
"AZURE_FRONT_DOOR_SECRET_CONTRIBUTOR",
"AZURE_FRONT_DOOR_SECRET_READER",
"AZURE_HYBRID_DATABASE_ADMINISTRATOR_READ_ONLY_SERVICE_ROLE",
"AZURE_IMPACT_INSIGHT_READER",
"AZURE_KUBERNETES_FLEET_MANAGER_CONTRIBUTOR_ROLE",
"AZURE_KUBERNETES_FLEET_MANAGER_HUB_AGENT_ROLE",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_ADMIN",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_CLUSTER_ADMIN",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_CLUSTER_READER",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_CLUSTER_WRITER",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_READER",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_WRITER",
"AZURE_KUBERNETES_SERVICE_AGENT_POOL_MANAGER_ROLE",
"AZURE_KUBERNETES_SERVICE_ARC_CLUSTER_ADMIN_ROLE",
"AZURE_KUBERNETES_SERVICE_ARC_CLUSTER_USER_ROLE",
"AZURE_KUBERNETES_SERVICE_ARC_CONTRIBUTOR_ROLE",
"AZURE_KUBERNETES_SERVICE_CLUSTER_ADMIN_ROLE",
"AZURE_KUBERNETES_SERVICE_CLUSTER_MONITORING_USER",
"AZURE_KUBERNETES_SERVICE_CLUSTER_USER_ROLE",
"AZURE_KUBERNETES_SERVICE_CONTRIBUTOR_ROLE",
"AZURE_KUBERNETES_SERVICE_HYBRID_CLUSTER_ADMIN_ROLE",
"AZURE_KUBERNETES_SERVICE_HYBRID_CLUSTER_USER_ROLE",
"AZURE_KUBERNETES_SERVICE_HYBRID_CONTRIBUTOR_ROLE",
"AZURE_KUBERNETES_SERVICE_POLICY_ADD_ON_DEPLOYMENT",
"AZURE_KUBERNETES_SERVICE_RBAC_ADMIN",
"AZURE_KUBERNETES_SERVICE_RBAC_CLUSTER_ADMIN",
"AZURE_KUBERNETES_SERVICE_RBAC_READER",
"AZURE_KUBERNETES_SERVICE_RBAC_WRITER",
"AZURE_MACHINE_LEARNING_WORKSPACE_CONNECTION_SECRETS_READER",
"AZURE_MANAGED_GRAFANA_WORKSPACE_CONTRIBUTOR",
"AZURE_MAPS_CONTRIBUTOR",
"AZURE_MAPS_DATA_CONTRIBUTOR",
"AZURE_MAPS_DATA_READER",
"AZURE_MAPS_DATA_READ_AND_BATCH_ROLE",
"AZURE_MAPS_SEARCH_AND_RENDER_DATA_READER",
"AZURE_MESSAGING_CATALOG_DATA_OWNER",
"AZURE_MESSAGING_CONNECTORS_OWNER",
"AZURE_MONITOR_DASHBOARDS_WITH_GRAFANA_CONTRIBUTOR",
"AZURE_PROGRAMMABLE_CONNECTIVITY_GATEWAY_DATAPLANE_USER",
"AZURE_PROGRAMMABLE_CONNECTIVITY_GATEWAY_USER",
"AZURE_RED_HAT_OPENSHIFT_CLOUD_CONTROLLER_MANAGER",
"AZURE_RED_HAT_OPENSHIFT_CLUSTER_INGRESS_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_DISK_STORAGE_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_FEDERATED_CREDENTIAL",
"AZURE_RED_HAT_OPENSHIFT_FILE_STORAGE_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_HOSTED_CONTROL_PLANES_CLUSTER_API_PROVIDER",
"AZURE_RED_HAT_OPENSHIFT_HOSTED_CONTROL_PLANES_CONTROL_PLANE_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_HOSTED_CONTROL_PLANES_SERVICE_MANAGED_IDENTITY",
"AZURE_RED_HAT_OPENSHIFT_IMAGE_REGISTRY_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_MACHINE_API_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_NETWORK_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_SERVICE_OPERATOR",
"AZURE_RELAY_LISTENER",
"AZURE_RELAY_OWNER",
"AZURE_RELAY_SENDER",
"AZURE_RESOURCE_BRIDGE_DEPLOYMENT_ROLE",
"AZURE_RESOURCE_NOTIFICATIONS_SYSTEM_TOPICS_SUBSCRIBER",
"AZURE_SERVICE_BUS_DATA_OWNER",
"AZURE_SERVICE_BUS_DATA_RECEIVER",
"AZURE_SERVICE_BUS_DATA_SENDER",
"AZURE_SPHERE_CONTRIBUTOR",
"AZURE_SPHERE_OWNER",
"AZURE_SPHERE_PUBLISHER",
"AZURE_SPHERE_READER",
"AZURE_SPRING_APPS_APPLICATION_CONFIGURATION_SERVICE_CONFIG_FILE_PATTERN_READER_ROLE",
"AZURE_SPRING_APPS_APPLICATION_CONFIGURATION_SERVICE_LOG_READER_ROLE",
"AZURE_SPRING_APPS_CONNECT_ROLE",
"AZURE_SPRING_APPS_JOB_EXECUTION_INSTANCE_LIST_ROLE",
"AZURE_SPRING_APPS_JOB_LOG_READER_ROLE",
"AZURE_SPRING_APPS_MANAGED_COMPONENTS_LOG_READER_ROLE",
"AZURE_SPRING_APPS_REMOTE_DEBUGGING_ROLE",
"AZURE_SPRING_APPS_SPRING_CLOUD_CONFIG_SERVER_LOG_READER_ROLE",
"AZURE_SPRING_APPS_SPRING_CLOUD_GATEWAY_LOG_READER_ROLE",
"AZURE_SPRING_CLOUD_CONFIG_SERVER_CONTRIBUTOR",
"AZURE_SPRING_CLOUD_CONFIG_SERVER_READER",
"AZURE_SPRING_CLOUD_DATA_READER",
"AZURE_SPRING_CLOUD_SERVICE_REGISTRY_CONTRIBUTOR",
"AZURE_SPRING_CLOUD_SERVICE_REGISTRY_READER",
"AZURE_STACK_HCI_ADMINISTRATOR",
"AZURE_STACK_HCI_CONNECTED_INFRAVMS",
"AZURE_STACK_HCI_DEVICE_MANAGEMENT_ROLE",
"AZURE_STACK_HCI_EDGE_MACHINE_CONTRIBUTOR_ROLE",
"AZURE_STACK_HCI_VM_CONTRIBUTOR",
"AZURE_STACK_HCI_VM_READER",
"AZURE_STACK_REGISTRATION_OWNER",
"AZURE_USAGE_BILLING_DATA_SENDER",
"AZURE_VM_MANAGED_IDENTITIES_RESTORE_CONTRIBUTOR",
"BACKUP_CONTRIBUTOR",
"BACKUP_MUA_ADMIN",
"BACKUP_MUA_OPERATOR",
"BACKUP_OPERATOR",
"BACKUP_READER",
"BAYER_AG_POWERED_SERVICES_CROP_ID_SOLUTION_USER_ROLE",
"BAYER_AG_POWERED_SERVICES_CWUM_SOLUTION",
"BAYER_AG_POWERED_SERVICES_FIELD_IMAGERY_SOLUTION_SERVICE_ROLE",
"BAYER_AG_POWERED_SERVICES_GDU_SOLUTION",
"BAYER_AG_POWERED_SERVICES_HISTORICAL_WEATHER_DATA_SOLUTION_USER_ROLE",
"BAYER_AG_POWERED_SERVICES_IMAGERY_SOLUTION",
"BAYER_AG_POWERED_SERVICES_SMART_BOUNDARY_SOLUTION_USER_ROLE",
"BILLING_READER",
"BIZTALK_CONTRIBUTOR",
"BLOCKCHAIN_MEMBER_NODE_ACCESS_PREVIEW",
"BLUEPRINT_CONTRIBUTOR",
"BLUEPRINT_OPERATOR",
"CARBON_OPTIMIZATION_READER",
"CDN_ENDPOINT_CONTRIBUTOR",
"CDN_ENDPOINT_READER",
"CDN_PROFILE_CONTRIBUTOR",
"CDN_PROFILE_READER",
"CHAOS_STUDIO_EXPERIMENT_CONTRIBUTOR",
"CHAOS_STUDIO_OPERATOR",
"CHAOS_STUDIO_READER",
"CHAOS_STUDIO_TARGET_CONTRIBUTOR",
"CLASSIC_NETWORK_CONTRIBUTOR",
"CLASSIC_STORAGE_ACCOUNT_CONTRIBUTOR",
"CLASSIC_STORAGE_ACCOUNT_KEY_OPERATOR_SERVICE_ROLE",
"CLASSIC_VIRTUAL_MACHINE_CONTRIBUTOR",
"CLEARDB_MYSQL_DB_CONTRIBUTOR",
"CLOUDTEST_CONTRIBUTOR_ROLE",
"COGNITIVE_SEARCH_SERVERLESS_DATA_CONTRIBUTOR_DEPRECATED",
"COGNITIVE_SEARCH_SERVERLESS_DATA_READER_DEPRECATED",
"COGNITIVE_SERVICES_CONTRIBUTOR",
"COGNITIVE_SERVICES_CUSTOM_VISION_CONTRIBUTOR",
"COGNITIVE_SERVICES_CUSTOM_VISION_DEPLOYMENT",
"COGNITIVE_SERVICES_CUSTOM_VISION_LABELER",
"COGNITIVE_SERVICES_CUSTOM_VISION_READER",
"COGNITIVE_SERVICES_CUSTOM_VISION_TRAINER",
"COGNITIVE_SERVICES_DATA_CONTRIBUTOR_PREVIEW",
"COGNITIVE_SERVICES_DATA_READER",
"COGNITIVE_SERVICES_FACE_CONTRIBUTOR",
"COGNITIVE_SERVICES_FACE_RECOGNIZER",
"COGNITIVE_SERVICES_IMMERSIVE_READER_USER",
"COGNITIVE_SERVICES_LANGUAGE_OWNER",
"COGNITIVE_SERVICES_LANGUAGE_READER",
"COGNITIVE_SERVICES_LANGUAGE_WRITER",
"COGNITIVE_SERVICES_LUIS_OWNER",
"COGNITIVE_SERVICES_LUIS_READER",
"COGNITIVE_SERVICES_LUIS_WRITER",
"COGNITIVE_SERVICES_METRICS_ADVISOR_ADMINISTRATOR",
"COGNITIVE_SERVICES_METRICS_ADVISOR_USER",
"COGNITIVE_SERVICES_OPENAI_CONTRIBUTOR",
"COGNITIVE_SERVICES_OPENAI_USER",
"COGNITIVE_SERVICES_QNA_MAKER_EDITOR",
"COGNITIVE_SERVICES_QNA_MAKER_READER",
"COGNITIVE_SERVICES_SPEECH_CONTRIBUTOR",
"COGNITIVE_SERVICES_SPEECH_USER",
"COGNITIVE_SERVICES_USAGES_READER",
"COGNITIVE_SERVICES_USER",
"COLLABORATIVE_DATA_CONTRIBUTOR",
"COLLABORATIVE_RUNTIME_OPERATOR",
"COMMUNICATION_AND_EMAIL_SERVICE_OWNER",
"COMMUNITY_CONTRIBUTOR_ROLE",
"COMMUNITY_OWNER_ROLE",
"COMMUNITY_READER_ROLE",
"COMPUTE_DIAGNOSTICS_ROLE",
"COMPUTE_FLEET_CONTRIBUTOR",
"COMPUTE_GALLERY_ARTIFACTS_PUBLISHER",
"COMPUTE_GALLERY_IMAGE_READER",
"COMPUTE_GALLERY_SHARING_ADMIN",
"COMPUTE_RECOMMENDATIONS_ROLE",
"CONNECTED_CLUSTER_MANAGED_IDENTITY_CHECKACCESS_READER",
"CONNECTOR_READER",
"CONTAINERAPP_READER",
"CONTAINER_APPS_CONNECTEDENVIRONMENTS_CONTRIBUTOR",
"CONTAINER_APPS_CONNECTEDENVIRONMENTS_READER",
"CONTAINER_APPS_CONTRIBUTOR",
"CONTAINER_APPS_JOBS_CONTRIBUTOR",
"CONTAINER_APPS_JOBS_OPERATOR",
"CONTAINER_APPS_JOBS_READER",
"CONTAINER_APPS_MANAGEDENVIRONMENTS_CONTRIBUTOR",
"CONTAINER_APPS_MANAGEDENVIRONMENTS_READER",
"CONTAINER_APPS_OPERATOR",
"CONTAINER_APPS_SESSIONPOOLS_CONTRIBUTOR",
"CONTAINER_APPS_SESSIONPOOLS_READER",
"CONTAINER_INSTANCES_CONTRIBUTOR",
"CONTAINER_REGISTRY_CACHE_RULE_ADMINISTRATOR",
"CONTAINER_REGISTRY_CACHE_RULE_READER",
"CONTAINER_REGISTRY_CONFIGURATION_READER_AND_DATA_ACCESS_CONFIGURATION_READER",
"CONTAINER_REGISTRY_CONTRIBUTOR_AND_DATA_ACCESS_CONFIGURATION_ADMINISTRATOR",
"CONTAINER_REGISTRY_CREDENTIAL_SET_ADMINISTRATOR",
"CONTAINER_REGISTRY_CREDENTIAL_SET_READER",
"CONTAINER_REGISTRY_DATA_IMPORTER_AND_DATA_READER",
"CONTAINER_REGISTRY_REPOSITORY_CATALOG_LISTER",
"CONTAINER_REGISTRY_REPOSITORY_CONTRIBUTOR",
"CONTAINER_REGISTRY_REPOSITORY_READER",
"CONTAINER_REGISTRY_REPOSITORY_WRITER",
"CONTAINER_REGISTRY_TASKS_CONTRIBUTOR",
"CONTAINER_REGISTRY_TRANSFER_PIPELINE_CONTRIBUTOR",
"CONTRIBUTOR",
"COSMOSBACKUPOPERATOR",
"COSMOSRESTOREOPERATOR",
"COSMOS_DB_ACCOUNT_READER_ROLE",
"COSMOS_DB_OPERATOR",
"COST_MANAGEMENT_CONTRIBUTOR",
"COST_MANAGEMENT_READER",
"CROSSCONNECTIONMANAGER",
"CROSSCONNECTIONREADER",
"CUSTOM_AUTOMATION_ROLE_ROOT",
"DATA_BOUNDARY_TENANT_ADMINISTRATOR",
"DATA_BOX_CONTRIBUTOR",
"DATA_BOX_READER",
"DATA_FACTORY_CONTRIBUTOR",
"DATA_LABELING_LABELER",
"DATA_LAKE_ANALYTICS_DEVELOPER",
"DATA_OPERATOR_FOR_MANAGED_DISKS",
"DATA_PURGER",
"DEDICATED_HOST_CONTRIBUTOR_ROLE",
"DEFENDER_CSPM_STORAGE_DATA_SCANNER",
"DEFENDER_CSPM_STORAGE_SCANNER_OPERATOR",
"DEFENDER_FOR_STORAGE_DATA_SCANNER",
"DEFENDER_FOR_STORAGE_SCANNER_OPERATOR",
"DEFENDER_KUBERNETES_AGENT_OPERATOR",
"DEID_BATCH_DATA_OWNER",
"DEID_BATCH_DATA_READER",
"DEID_DATA_OWNER",
"DEID_REALTIME_DATA_USER",
"DEPLOYMENT_ENVIRONMENTS_READER",
"DEPLOYMENT_ENVIRONMENTS_USER",
"DESKTOP_VIRTUALIZATION_APPLICATION_GROUP_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_APPLICATION_GROUP_READER",
"DESKTOP_VIRTUALIZATION_APP_ATTACH_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_HOST_POOL_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_HOST_POOL_READER",
"DESKTOP_VIRTUALIZATION_POWER_ON_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_POWER_ON_OFF_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_READER",
"DESKTOP_VIRTUALIZATION_SESSION_HOST_OPERATOR",
"DESKTOP_VIRTUALIZATION_USER",
"DESKTOP_VIRTUALIZATION_USER_SESSION_OPERATOR",
"DESKTOP_VIRTUALIZATION_VIRTUAL_MACHINE_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_WORKSPACE_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_WORKSPACE_READER",
"DEVCENTER_DEV_BOX_USER",
"DEVCENTER_PROJECT_ADMIN",
"DEVICE_PROVISIONING_SERVICE_DATA_CONTRIBUTOR",
"DEVICE_PROVISIONING_SERVICE_DATA_READER",
"DEVICE_UPDATE_ADMINISTRATOR",
"DEVICE_UPDATE_CONTENT_ADMINISTRATOR",
"DEVICE_UPDATE_CONTENT_READER",
"DEVICE_UPDATE_DEPLOYMENTS_ADMINISTRATOR",
"DEVICE_UPDATE_DEPLOYMENTS_READER",
"DEVICE_UPDATE_READER",
"DEVTEST_LABS_USER",
"DICOM_DATA_OWNER",
"DICOM_DATA_READER",
"DISK_BACKUP_READER",
"DISK_ENCRYPTION_SET_OPERATOR_FOR_MANAGED_DISKS",
"DISK_POOL_OPERATOR",
"DISK_RESTORE_OPERATOR",
"DISK_SNAPSHOT_CONTRIBUTOR",
"DNS_RESOLVER_CONTRIBUTOR",
"DNS_ZONE_CONTRIBUTOR",
"DOCUMENTDB_ACCOUNT_CONTRIBUTOR",
"DOMAIN_SERVICES_CONTRIBUTOR",
"DOMAIN_SERVICES_READER",
"DURABLE_TASK_DATA_CONTRIBUTOR",
"DURABLE_TASK_DATA_READER",
"DURABLE_TASK_WORKER",
"EDGE_MANAGEMENT_COPILOT_USER",
"ELASTIC_SAN_NETWORK_ADMIN",
"ELASTIC_SAN_OWNER",
"ELASTIC_SAN_READER",
"ELASTIC_SAN_SNAPSHOT_EXPORTER",
"ELASTIC_SAN_VOLUME_GROUP_OWNER",
"ELASTIC_SAN_VOLUME_IMPORTER",
"ENCLAVE_APPROVER_ROLE",
"ENCLAVE_CONTRIBUTOR_ROLE",
"ENCLAVE_OWNER_ROLE",
"ENCLAVE_READER_ROLE",
"EVENTGRID_CONTRIBUTOR",
"EVENTGRID_DATA_CONTRIBUTOR",
"EVENTGRID_DATA_RECEIVER",
"EVENTGRID_DATA_SENDER",
"EVENTGRID_EVENTSUBSCRIPTION_CONTRIBUTOR",
"EVENTGRID_EVENTSUBSCRIPTION_READER",
"EVENTGRID_TOPICSPACES_PUBLISHER",
"EVENTGRID_TOPICSPACES_SUBSCRIBER",
"EXPERIMENTATION_ADMINISTRATOR",
"EXPERIMENTATION_CONTRIBUTOR",
"EXPERIMENTATION_METRIC_CONTRIBUTOR",
"EXPERIMENTATION_READER",
"FHIR_DATA_CONTRIBUTOR",
"FHIR_DATA_CONVERTER",
"FHIR_DATA_EXPORTER",
"FHIR_DATA_IMPORTER",
"FHIR_DATA_READER",
"FHIR_DATA_WRITER",
"FHIR_SMART_USER",
"FIRMWARE_ANALYSIS_ADMIN",
"FLUX_CONFIGURATIONS_CONTRIBUTOR",
"GEOCATALOG_ADMINISTRATOR",
"GEOCATALOG_READER",
"GITOPS_LZ_LIST_ACTIONS",
"GRAFANA_ADMIN",
"GRAFANA_EDITOR",
"GRAFANA_LIMITED_VIEWER",
"GRAFANA_VIEWER",
"GRAPH_OWNER",
"GROUPQUOTA_READER",
"GROUPQUOTA_REQUEST_OPERATOR",
"GUEST_CONFIGURATION_RESOURCE_CONTRIBUTOR",
"HDINSIGHT_CLUSTER_ADMIN",
"HDINSIGHT_CLUSTER_OPERATOR",
"HDINSIGHT_DOMAIN_SERVICES_CONTRIBUTOR",
"HDINSIGHT_ON_AKS_CLUSTER_ADMIN",
"HDINSIGHT_ON_AKS_CLUSTER_OPERATOR",
"HDINSIGHT_ON_AKS_CLUSTER_POOL_ADMIN",
"HEALTHCARE_AGENT_ADMIN",
"HEALTHCARE_AGENT_EDITOR",
"HEALTHCARE_AGENT_READER",
"HEALTH_SAFEGUARDS_DATA_USER",
"HIERARCHY_SETTINGS_ADMINISTRATOR",
"HYBRIDCOMPUTE_MACHINE_LISTACCESSDETAILS_ACTION_IN_BUILT_ROLE",
"HYBRID_SERVER_ONBOARDING",
"HYBRID_SERVER_RESOURCE_ADMINISTRATOR",
"IMPACT_READER",
"IMPACT_REPORTER",
"INTEGRATION_SERVICE_ENVIRONMENT_CONTRIBUTOR",
"INTEGRATION_SERVICE_ENVIRONMENT_DEVELOPER",
"INTELLIGENT_SYSTEMS_ACCOUNT_CONTRIBUTOR",
"IOT_HUB_DATA_CONTRIBUTOR",
"IOT_HUB_DATA_READER",
"IOT_HUB_REGISTRY_CONTRIBUTOR",
"IOT_HUB_TWIN_CONTRIBUTOR",
"IPAM_POOL_USER",
"ISSUE_CONTRIBUTOR",
"KEY_VAULT_ADMINISTRATOR",
"KEY_VAULT_CERTIFICATES_OFFICER",
"KEY_VAULT_CERTIFICATE_USER",
"KEY_VAULT_CONTRIBUTOR",
"KEY_VAULT_CRYPTO_OFFICER",
"KEY_VAULT_CRYPTO_SERVICE_ENCRYPTION_USER",
"KEY_VAULT_CRYPTO_SERVICE_RELEASE_USER",
"KEY_VAULT_CRYPTO_USER",
"KEY_VAULT_DATA_ACCESS_ADMINISTRATOR",
"KEY_VAULT_PURGE_OPERATOR",
"KEY_VAULT_READER",
"KEY_VAULT_SECRETS_OFFICER",
"KEY_VAULT_SECRETS_USER",
"KNOWLEDGE_CONSUMER",
"KOSTNER_DFS_BASE_ROLE",
"KUBERNETESRUNTIME_LOAD_BALANCER_CONTRIBUTOR_ROLE",
"KUBERNETES_AGENTLESS_OPERATOR",
"KUBERNETES_AGENT_OPERATOR",
"KUBERNETES_AGENT_SUBSCRIPTION_LEVEL_OPERATOR",
"KUBERNETES_CLUSTER_AZURE_ARC_ONBOARDING",
"KUBERNETES_EXTENSION_CONTRIBUTOR",
"KUBERNETES_NAMESPACE_USER",
"KUBERNETES_RUNTIME_STORAGE_CLASS_CONTRIBUTOR_ROLE",
"LAB_ASSISTANT",
"LAB_CONTRIBUTOR",
"LAB_CREATOR",
"LAB_OPERATOR",
"LAB_SERVICES_CONTRIBUTOR",
"LAB_SERVICES_READER",
"LANDING_ZONE_ACCOUNT_OWNER",
"LANDING_ZONE_ACCOUNT_READER",
"LANDING_ZONE_MANAGEMENT_OWNER",
"LANDING_ZONE_MANAGEMENT_READER",
"LOAD_TEST_CONTRIBUTOR",
"LOAD_TEST_OWNER",
"LOAD_TEST_READER",
"LOCALNGFIREWALLADMINISTRATOR_ROLE",
"LOCALRULESTACKSADMINISTRATOR_ROLE",
"LOCKS_CONTRIBUTOR",
"LOGIC_APPS_STANDARD_CONTRIBUTOR_PREVIEW",
"LOGIC_APPS_STANDARD_DEVELOPER_PREVIEW",
"LOGIC_APPS_STANDARD_OPERATOR_PREVIEW",
"LOGIC_APPS_STANDARD_READER_PREVIEW",
"LOGIC_APP_CONTRIBUTOR",
"LOGIC_APP_OPERATOR",
"LOG_ANALYTICS_CONTRIBUTOR",
"LOG_ANALYTICS_READER",
"MANAGED_APPLICATION_CONTRIBUTOR_ROLE",
"MANAGED_APPLICATION_OPERATOR_ROLE",
"MANAGED_APPLICATION_PUBLISHER_OPERATOR",
"MANAGED_HSM_CONTRIBUTOR",
"MANAGED_IDENTITY_CONTRIBUTOR",
"MANAGED_IDENTITY_FEDERATED_IDENTITY_CREDENTIAL_CONTRIBUTOR",
"MANAGED_IDENTITY_OPERATOR",
"MANAGED_SERVICES_REGISTRATION_ASSIGNMENT_DELETE_ROLE",
"MANAGEMENT_GROUP_CONTRIBUTOR",
"MANAGEMENT_GROUP_READER",
"MEDIA_SERVICES_ACCOUNT_ADMINISTRATOR",
"MEDIA_SERVICES_LIVE_EVENTS_ADMINISTRATOR",
"MEDIA_SERVICES_MEDIA_OPERATOR",
"MEDIA_SERVICES_POLICY_ADMINISTRATOR",
"MEDIA_SERVICES_STREAMING_ENDPOINTS_ADMINISTRATOR",
"MICROSOFT_EDGE_WINFIELDS_FEDERATED_SUBSCRIPTION_READ_ACCESS_ROLE",
"MICROSOFT_KUBERNETES_CONNECTED_CLUSTER_ROLE",
"MICROSOFT_POWERBI_TENANT_OPERATIONS_ROLE",
"MICROSOFT_SENTINEL_AUTOMATION_CONTRIBUTOR",
"MICROSOFT_SENTINEL_BUSINESS_APPLICATIONS_AGENT_OPERATOR",
"MICROSOFT_SENTINEL_CONTRIBUTOR",
"MICROSOFT_SENTINEL_PLAYBOOK_OPERATOR",
"MICROSOFT_SENTINEL_READER",
"MICROSOFT_SENTINEL_RESPONDER",
"MONITORING_CONTRIBUTOR",
"MONITORING_DATA_READER",
"MONITORING_METRICS_PUBLISHER",
"MONITORING_READER",
"MYSQL_BACKUP_AND_EXPORT_OPERATOR",
"NETWORK_CONTRIBUTOR",
"NEW_RELIC_APM_ACCOUNT_CONTRIBUTOR",
"NEXUS_NETWORK_FABRIC_SERVICE_READER",
"NEXUS_NETWORK_FABRIC_SERVICE_WRITER",
"OBJECT_ANCHORS_ACCOUNT_OWNER",
"OBJECT_ANCHORS_ACCOUNT_READER",
"OBJECT_UNDERSTANDING_ACCOUNT_OWNER",
"OBJECT_UNDERSTANDING_ACCOUNT_READER",
"ONLINE_EXPERIMENTATION_CONTRIBUTOR",
"ONLINE_EXPERIMENTATION_DATA_OWNER",
"ONLINE_EXPERIMENTATION_DATA_READER",
"ONLINE_EXPERIMENTATION_READER",
"OPENCOST_RATE_CARD_READER",
"OPERATOR_NEXUS_COMPUTE_CONTRIBUTOR_ROLE_PREVIEW",
"OPERATOR_NEXUS_KEY_VAULT_WRITER_SERVICE_ROLE_PREVIEW",
"OPERATOR_NEXUS_OWNER_PREVIEW",
"ORACLE_DATABASE_AUTONOMOUS_DATABASE_ADMINISTRATOR",
"ORACLE_DATABASE_EXADATA_INFRASTRUCTURE_ADMINISTRATOR_BUILT_IN_ROLE",
"ORACLE_DATABASE_EXASCALE_STORAGE_VAULT_ADMINISTRATOR",
"ORACLE_DATABASE_EXASCALE_VMCLUSTER_ADMINISTRATOR",
"ORACLE_DATABASE_OWNER_BUILT_IN_ROLE",
"ORACLE_DATABASE_READER_BUILT_IN_ROLE",
"ORACLE_DATABASE_VMCLUSTER_ADMINISTRATOR_BUILT_IN_ROLE",
"ORACLE_SUBSCRIPTIONS_MANAGER_BUILT_IN_ROLE",
"OWNER",
"PLAYFAB_CONTRIBUTOR",
"PLAYFAB_READER",
"POLICY_INSIGHTS_DATA_WRITER_PREVIEW",
"PORTAL_DASHBOARD_WRITER_SERVICE_ROLE",
"POSTGRESQL_FLEXIBLE_SERVER_LONG_TERM_RETENTION_BACKUP_ROLE",
"POWER_PLATFORM_ACCOUNT_CONTRIBUTOR",
"POWER_PLATFORM_ENTERPRISE_POLICY_CONTRIBUTOR",
"PRIVATE_DNS_ZONE_CONTRIBUTOR",
"PROCUREMENT_CONTRIBUTOR",
"PROJECT_BABYLON_DATA_CURATOR",
"PROJECT_BABYLON_DATA_READER",
"PROJECT_BABYLON_DATA_SOURCE_ADMINISTRATOR",
"PROVIDERHUB_CONTRIBUTOR",
"PROVIDERHUB_READER",
"QUANTUM_WORKSPACE_DATA_CONTRIBUTOR",
"QUOTA_REQUEST_OPERATOR",
"READER",
"READER_AND_DATA_ACCESS",
"REDIS_CACHE_CONTRIBUTOR",
"REMOTE_RENDERING_ADMINISTRATOR",
"REMOTE_RENDERING_CLIENT",
"RESERVATION_PURCHASER",
"RESOURCE_POLICY_CONTRIBUTOR",
"ROLE_BASED_ACCESS_CONTROL_ADMINISTRATOR",
"SAAS_HUB_CONTRIBUTOR",
"SAVINGS_PLAN_PURCHASER",
"SCHEDULED_EVENTS_CONTRIBUTOR",
"SCHEDULED_PATCHING_CONTRIBUTOR",
"SCHEDULER_JOB_COLLECTIONS_CONTRIBUTOR",
"SCHEMA_REGISTRY_CONTRIBUTOR",
"SCHEMA_REGISTRY_READER",
"SEARCH_INDEX_DATA_CONTRIBUTOR",
"SEARCH_INDEX_DATA_READER",
"SEARCH_PARAMETER_MANAGER",
"SEARCH_SERVICE_CONTRIBUTOR",
"SECRETS_STORE_EXTENSION_OWNER",
"SECURE_SCORE_READER",
"SECURITY_ADMIN",
"SECURITY_ASSESSMENT_CONTRIBUTOR",
"SECURITY_DETONATION_CHAMBER_PUBLISHER",
"SECURITY_DETONATION_CHAMBER_READER",
"SECURITY_DETONATION_CHAMBER_SUBMISSION_MANAGER",
"SECURITY_DETONATION_CHAMBER_SUBMITTER",
"SECURITY_MANAGER_LEGACY",
"SECURITY_READER",
"SERVICES_HUB_OPERATOR",
"SERVICE_CONNECTOR_CONTRIBUTOR",
"SERVICE_FABRIC_CLUSTER_CONTRIBUTOR",
"SERVICE_FABRIC_MANAGED_CLUSTER_CONTRIBUTOR",
"SERVICE_GROUP_READER",
"SIGNALR_ACCESSKEY_READER",
"SIGNALR_APP_SERVER",
"SIGNALR_REST_API_OWNER",
"SIGNALR_REST_API_READER",
"SIGNALR_SERVICE_OWNER",
"SIGNALR_WEB_PUBSUB_CONTRIBUTOR",
"SITE_RECOVERY_CONTRIBUTOR",
"SITE_RECOVERY_OPERATOR",
"SITE_RECOVERY_READER",
"SPATIALMAPSACCOUNTS_ACCOUNT_OWNER",
"SPATIAL_ANCHORS_ACCOUNT_CONTRIBUTOR",
"SPATIAL_ANCHORS_ACCOUNT_OWNER",
"SPATIAL_ANCHORS_ACCOUNT_READER",
"SQLDB_MIGRATION_ROLE",
"SQLMI_MIGRATION_ROLE",
"SQLVM_MIGRATION_ROLE",
"SQL_DB_CONTRIBUTOR",
"SQL_MANAGED_INSTANCE_CONTRIBUTOR",
"SQL_SECURITY_MANAGER",
"SQL_SERVER_CONTRIBUTOR",
"SSH_PUBLICKEYS_CONTRIBUTOR_ROLE",
"SSH_PUBLICKEYS_READER_ROLE",
"STANDBY_CONTAINER_GROUP_POOL_CONTRIBUTOR",
"STORAGE_ACCOUNT_BACKUP_CONTRIBUTOR",
"STORAGE_ACCOUNT_CONTRIBUTOR",
"STORAGE_ACCOUNT_ENCRYPTION_SCOPE_CONTRIBUTOR_ROLE",
"STORAGE_ACCOUNT_KEY_OPERATOR_SERVICE_ROLE",
"STORAGE_BLOB_DATA_CONTRIBUTOR",
"STORAGE_BLOB_DATA_OWNER",
"STORAGE_BLOB_DATA_READER",
"STORAGE_BLOB_DELEGATOR",
"STORAGE_FILE_DATA_PRIVILEGED_CONTRIBUTOR",
"STORAGE_FILE_DATA_PRIVILEGED_READER",
"STORAGE_FILE_DATA_SMB_SHARE_CONTRIBUTOR",
"STORAGE_FILE_DATA_SMB_SHARE_ELEVATED_CONTRIBUTOR",
"STORAGE_FILE_DATA_SMB_SHARE_READER",
"STORAGE_QUEUE_DATA_CONTRIBUTOR",
"STORAGE_QUEUE_DATA_MESSAGE_PROCESSOR",
"STORAGE_QUEUE_DATA_MESSAGE_SENDER",
"STORAGE_QUEUE_DATA_READER",
"STORAGE_TABLE_DATA_CONTRIBUTOR",
"STORAGE_TABLE_DATA_READER",
"STREAM_ANALYTICS_CONTRIBUTOR",
"STREAM_ANALYTICS_QUERY_TESTER",
"STREAM_ANALYTICS_READER",
"SUBSCRIPTION_CREATOR",
"SUBSCRIPTION_CREATOR_OLD",
"SUPPORT_REQUEST_CONTRIBUTOR",
"TAG_CONTRIBUTOR",
"TAG_READER",
"TEMPLATE_SPEC_CONTRIBUTOR",
"TEMPLATE_SPEC_READER",
"TEST_BASE_READER",
"TOOLCHAINORCHESTRATOR_ADMIN_ROLE",
"TOOLCHAINORCHESTRATOR_VIEWER_ROLE",
"TRAFFIC_MANAGER_CONTRIBUTOR",
"TRANSPARENCY_LOGS_OWNER",
"TRUSTED_SIGNING_CERTIFICATE_PROFILE_SIGNER",
"TRUSTED_SIGNING_IDENTITY_VERIFIER",
"USAGE_BILLING_CONTRIBUTOR",
"USER_ACCESS_ADMINISTRATOR",
"VIDEO_INDEXER_RESTRICTED_VIEWER",
"VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN",
"VIRTUAL_MACHINE_CONTRIBUTOR",
"VIRTUAL_MACHINE_DATA_ACCESS_ADMINISTRATOR_PREVIEW",
"VIRTUAL_MACHINE_LOCAL_USER_LOGIN",
"VIRTUAL_MACHINE_USER_LOGIN",
"VM_RESTORE_OPERATOR",
"VM_SCANNER_OPERATOR",
"WEBSITE_CONTRIBUTOR",
"WEB_PLAN_CONTRIBUTOR",
"WEB_PUBSUB_SERVICE_OWNER",
"WEB_PUBSUB_SERVICE_READER",
"WINDOWS_ADMIN_CENTER_ADMINISTRATOR_LOGIN",
"WORKBOOK_CONTRIBUTOR",
"WORKBOOK_READER",
"WORKLOADBUILDER_MIGRATION_AGENT_ROLE",
"WORKLOAD_ORCHESTRATION_IT_ADMIN",
"WORKLOAD_ORCHESTRATION_SOLUTION_EXTERNAL_VALIDATOR"
]

View File

@@ -18,8 +18,20 @@ from typing import Any
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Known Azure enum values not always captured in the catalog schema # Known Azure enum values not always captured in the catalog schema
def _load_azure_roles() -> list[str]:
"""Load Azure roles from azure_roles.json (generated from bicep/lookup/rbaclookup)."""
roles_file = pathlib.Path(__file__).parent / "azure_roles.json"
if roles_file.exists():
try:
return json.loads(roles_file.read_text())
except Exception as e:
logger.warning("Failed to load azure_roles.json: %s", e)
return []
_KNOWN_ENUMS: dict[str, list[str]] = { _KNOWN_ENUMS: dict[str, list[str]] = {
"principalType": ["User", "Group", "ServicePrincipal", "Device", "ForeignGroup"], "principalType": ["User", "Group", "ServicePrincipal", "Device", "ForeignGroup"],
"roles": _load_azure_roles(),
"roleDefinitionIds": _load_azure_roles(), # alias for roles
} }
# Catalog is baked into the image root at /bicep_modules_catalog.json # Catalog is baked into the image root at /bicep_modules_catalog.json
@@ -36,34 +48,6 @@ _IAC_SOURCE_PATHS = [
pathlib.Path(__file__).parent.parent.parent / "iac_source_catalog.json", # dev pathlib.Path(__file__).parent.parent.parent / "iac_source_catalog.json", # dev
] ]
# Principals catalog — known object IDs for array params (e.g. additionalAccess)
# Format: {"params": {"additionalAccess": [{"id": "<guid>", "label": "...", "description": "..."}]}}
_PRINCIPALS_CATALOG_PATHS = [
pathlib.Path("/data/principals_catalog.json"), # volume-mount (freshest)
pathlib.Path("/principals_catalog.json"), # baked into image (fallback)
pathlib.Path(__file__).parent.parent.parent / "principals_catalog.json", # dev
]
def _load_principals_catalog() -> dict[str, list[dict[str, Any]]]:
"""Load principals catalog for array param completions.
Returns dict keyed by param name (e.g. 'additionalAccess') → list of
{id, label, description} entries.
"""
for path in _PRINCIPALS_CATALOG_PATHS:
if path.exists():
try:
data = json.loads(path.read_text())
params = data.get("params", {})
count = sum(len(v) for v in params.values())
logger.info("Principals catalog loaded from %s: %d entries", path, count)
return params
except Exception:
logger.exception("Failed to parse principals catalog at %s", path)
logger.debug("No principals_catalog.json found — array param completions unavailable")
return {}
def _load_iac_source_catalog() -> dict[str, dict[str, Any]]: def _load_iac_source_catalog() -> dict[str, dict[str, Any]]:
"""Load IAC source catalog for enriched param descriptions. """Load IAC source catalog for enriched param descriptions.
@@ -122,14 +106,12 @@ class BicepModuleCatalog:
_modules: list[dict[str, Any]] = [] _modules: list[dict[str, Any]] = []
_iac: dict[str, dict[str, Any]] = {} # module name → IAC source info _iac: dict[str, dict[str, Any]] = {} # module name → IAC source info
_principals: dict[str, list[dict[str, Any]]] = {} # param name → [{id, label, description}]
@classmethod @classmethod
def load(cls) -> None: def load(cls) -> None:
"""Load all catalogs from disk. Call once at startup.""" """Load both catalogs from disk. Call once at startup."""
cls._modules = _load_catalog() cls._modules = _load_catalog()
cls._iac = _load_iac_source_catalog() cls._iac = _load_iac_source_catalog()
cls._principals = _load_principals_catalog()
@classmethod @classmethod
def _iac_param_map(cls, module_name: str) -> dict[str, dict[str, Any]]: def _iac_param_map(cls, module_name: str) -> dict[str, dict[str, Any]]:
@@ -276,44 +258,6 @@ class BicepModuleCatalog:
}) })
return items return items
@classmethod
def param_array_item_completion_items(
cls,
module_name: str,
version: str,
param_name: str,
has_open_quote: bool = False,
) -> list[dict[str, Any]]:
"""Completions for items inside an array param (e.g. additionalAccess objectIds).
Looks up known entries from the principals catalog keyed by param name.
"""
entries = cls._principals.get(param_name, [])
if not entries:
return []
items = []
for i, entry in enumerate(entries):
val = entry["id"]
label = entry.get("label", val)
description = entry.get("description", "")
insert = f"{val}'" if has_open_quote else f"'{val}'"
doc = f"**{label}**\n\n`{val}`"
if description:
doc += f"\n\n{description}"
items.append({
"label": label,
"kind": 12, # Value
"detail": val, # GUID shown as detail
"insertText": insert,
"sortText": f"0_lru_arr_{i:03d}_{label}",
"documentation": {
"kind": "markdown",
"value": doc,
},
})
return items
@classmethod @classmethod
def param_completion_items(cls, module_name: str, version: str) -> list[dict[str, Any]]: def param_completion_items(cls, module_name: str, version: str) -> list[dict[str, Any]]:
"""Param completions for a specific module+version combination.""" """Param completions for a specific module+version combination."""
@@ -336,6 +280,56 @@ class BicepModuleCatalog:
iac_params = cls._iac_param_map(module_name) iac_params = cls._iac_param_map(module_name)
items = [] items = []
# Build snippet for full params block (shown first)
snippet_params = []
tabstop = 1
for param_name, param_info in ver_params.items():
iac = iac_params.get(param_name, {})
required = iac.get("required", False)
ptype = param_info.get("type", "any")
allowed = param_info.get("allowed", [])
# Include required params + first few optional params in snippet
if required or len(snippet_params) < 5:
if allowed:
# Enum: use placeholder with first allowed value
placeholder = f"'{allowed[0]}'"
elif ptype == "bool":
placeholder = "true"
elif ptype == "int":
placeholder = "0"
elif ptype == "array":
placeholder = "[]"
elif ptype == "object":
placeholder = "{{}}"
else:
placeholder = "''"
snippet_params.append(f" {param_name}: ${{{tabstop}:{placeholder}}}")
tabstop += 1
if snippet_params:
snippet_text = "\n" + "\n".join(snippet_params) + "\n"
required_count = sum(1 for p, i in ver_params.items()
if iac_params.get(p, {}).get("required", False))
items.append({
"label": "⚡ Fill params block",
"kind": 15, # Snippet
"detail": f"{len(snippet_params)} params ({required_count} required)",
"insertText": snippet_text,
"insertTextFormat": 2, # Snippet
"sortText": "0_lru_snippet_000",
"documentation": {
"kind": "markdown",
"value": (
f"**Fill params block**\n\n"
f"Inserts {len(snippet_params)} params for `{module_name}`.\n"
f"Use Tab to navigate between fields."
),
},
})
# Individual param completions
for param_name, param_info in ver_params.items(): for param_name, param_info in ver_params.items():
ptype = param_info.get("type", "any") ptype = param_info.get("type", "any")
allowed = param_info.get("allowed", []) allowed = param_info.get("allowed", [])

View File

@@ -130,19 +130,6 @@ class _ProxySession:
mod_name = last_mod.group(1) mod_name = last_mod.group(1)
mod_ver = last_mod.group(2) mod_ver = last_mod.group(2)
# Array item context: cursor inside [...] for an array param.
# Must be checked BEFORE value_m, since array lines also match
# the value pattern (e.g. `additionalAccess: ['`).
array_m = re.search(r"^\s*(\w+):\s*\[", current)
if array_m and current.count("[") > current.count("]"):
return {
"type": "param_array_item",
"module": mod_name,
"version": mod_ver,
"param": array_m.group(1),
"has_open_quote": bool(re.search(r"'[^']*$", current)),
}
# Check if cursor is after 'paramname: ' on the current line # Check if cursor is after 'paramname: ' on the current line
# (value context — inject enum/allowed values) # (value context — inject enum/allowed values)
value_m = re.search(r"^\s*(\w+):\s*('?)([^'{}]*)$", current) value_m = re.search(r"^\s*(\w+):\s*('?)([^'{}]*)$", current)
@@ -155,6 +142,43 @@ class _ProxySession:
"has_open_quote": bool(value_m.group(2)), "has_open_quote": bool(value_m.group(2)),
} }
# Check if cursor is inside an array value for a param
# e.g. "roles: ['KEY_VAULT_" or "roles: [ '"
array_m = re.search(r"^\s*(\w+):\s*\[[^\]]*?('?)([^',\]]*)$", current)
if array_m and array_m.group(1) not in {"params", "name", "module", "resource"}:
return {
"type": "param_value",
"module": mod_name,
"version": mod_ver,
"param": array_m.group(1),
"has_open_quote": bool(array_m.group(2)),
}
# Check if cursor is inside a multi-line array element
# e.g. " 'APP_CONFIG" on line after "roles: ["
# Walk backwards to find the array opening
for lookback_idx in range(line_idx - 1, max(0, line_idx - 10), -1):
prev_line = lines[lookback_idx]
# Found array opening like "roles: [" or " roles: ["
array_open_m = re.match(r"^\s*(\w+):\s*\[$", prev_line.rstrip())
if array_open_m:
param_name = array_open_m.group(1)
if param_name not in {"params", "name", "module", "resource"}:
# Check if current line is inside the array (has quote or is indented)
if re.match(r"^\s+('?)([^',\]]*)\s*$", current):
has_quote = bool(re.match(r"^\s+'", current))
return {
"type": "param_value",
"module": mod_name,
"version": mod_ver,
"param": param_name,
"has_open_quote": has_quote,
}
break
# Stop if we hit a closing bracket (we're outside the array)
if "]" in prev_line:
break
return { return {
"type": "param", "type": "param",
"module": mod_name, "module": mod_name,
@@ -210,18 +234,11 @@ def _inject_completions(msg: dict[str, Any], context: dict | None = None) -> byt
context["param"], context["param"],
context.get("has_open_quote", False), context.get("has_open_quote", False),
) )
elif ctx_type == "param_array_item":
lru_items = BicepModuleCatalog.param_array_item_completion_items(
context["module"],
context["version"],
context["param"],
context.get("has_open_quote", False),
)
else: else:
# Default: module name completions # Default: module name completions
lru_items = BicepModuleCatalog.as_completion_items() lru_items = BicepModuleCatalog.as_completion_items()
if ctx_type in ("version", "param", "param_value", "param_array_item"): if ctx_type in ("version", "param", "param_value"):
# Always replace LS completions for private-registry contexts — the # Always replace LS completions for private-registry contexts — the
# Bicep LS doesn't know about our ACR, so anything it returns is noise. # Bicep LS doesn't know about our ACR, so anything it returns is noise.
# Even if lru_items is empty (no enum values for a param), suppress LS. # Even if lru_items is empty (no enum values for a param), suppress LS.

View File

@@ -1,11 +0,0 @@
{
"params": {
"additionalAccess": [
{
"id": "c88bf29d-b13a-4153-9738-8995085a451e",
"label": "LRIADMPRO-IaC-Bicep",
"description": "IaC Bicep pipeline service principal"
}
]
}
}

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""
Extract all Azure role names from rbacLookup.bicep and generate Python code
for _KNOWN_ENUMS in modules.py.
Usage:
python3 scripts/extract_roles_from_rbaclookup.py /path/to/rbacLookup.bicep
"""
import re
import sys
from pathlib import Path
def extract_role_names(bicep_file: Path) -> list[str]:
"""Extract all role names from the rbacLookup.bicep var roles = {...} block."""
content = bicep_file.read_text()
# Find the "var roles = {" block
roles_match = re.search(r'@export\(\)\s*var\s+roles\s*=\s*\{(.+?)\n\}', content, re.DOTALL)
if not roles_match:
raise ValueError("Could not find 'var roles = {' block in Bicep file")
roles_block = roles_match.group(1)
# Extract all role names (keys before the colon)
# Pattern: " ROLE_NAME: 'guid'"
role_names = re.findall(r'^\s+([A-Z_]+):', roles_block, re.MULTILINE)
return sorted(role_names)
def generate_python_code(role_names: list[str]) -> str:
"""Generate Python code for _KNOWN_ENUMS["roles"]."""
lines = ['_KNOWN_ENUMS = {']
lines.append(' "roles": [')
for role in role_names:
lines.append(f' "{role}",')
lines.append(' ],')
lines.append(' "roleDefinitionIds": [ # alias for roles')
for role in role_names:
lines.append(f' "{role}",')
lines.append(' ],')
lines.append('}')
return '\n'.join(lines)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: extract_roles_from_rbaclookup.py /path/to/rbacLookup.bicep", file=sys.stderr)
sys.exit(1)
bicep_path = Path(sys.argv[1])
if not bicep_path.exists():
print(f"Error: File not found: {bicep_path}", file=sys.stderr)
sys.exit(1)
try:
roles = extract_role_names(bicep_path)
print(f"# Extracted {len(roles)} roles from {bicep_path.name}")
print(f"# Generated: {Path(__file__).name}\n")
print(generate_python_code(roles))
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)

View File

@@ -17,6 +17,7 @@ frames straight to stdout.
import asyncio import asyncio
import ssl import ssl
import sys import sys
import traceback
import websockets import websockets
@@ -71,8 +72,9 @@ async def main(uri: str) -> None:
if msg is None: if msg is None:
break break
await ws.send(msg) await ws.send(msg)
except Exception: except Exception as e:
pass print(f"stdin_to_ws error: {e}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
async def ws_to_stdout() -> None: async def ws_to_stdout() -> None:
try: try:
@@ -80,8 +82,9 @@ async def main(uri: str) -> None:
data = frame if isinstance(frame, bytes) else frame.encode() data = frame if isinstance(frame, bytes) else frame.encode()
sys.stdout.buffer.write(data) sys.stdout.buffer.write(data)
sys.stdout.buffer.flush() sys.stdout.buffer.flush()
except Exception: except Exception as e:
pass print(f"ws_to_stdout error: {e}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
await asyncio.gather(stdin_to_ws(), ws_to_stdout()) await asyncio.gather(stdin_to_ws(), ws_to_stdout())
@@ -90,4 +93,9 @@ if __name__ == "__main__":
if len(sys.argv) < 2: if len(sys.argv) < 2:
print("Usage: lsp_bridge.py <wss://...>", file=sys.stderr) print("Usage: lsp_bridge.py <wss://...>", file=sys.stderr)
sys.exit(1) sys.exit(1)
asyncio.run(main(sys.argv[1])) try:
asyncio.run(main(sys.argv[1]))
except Exception as e:
print(f"Fatal error: {e}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
sys.exit(1)

12
scripts/lsp_bridge_debug.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
# Wrapper for lsp_bridge.py that logs to a file for debugging IntelliJ issues
LOG_FILE="/tmp/lsp_bridge_debug.log"
echo "=== LSP Bridge started at $(date) ===" >> "$LOG_FILE"
echo "Args: $@" >> "$LOG_FILE"
echo "PWD: $(pwd)" >> "$LOG_FILE"
echo "Python: $(which python3)" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
exec /opt/homebrew/bin/python3 /Users/lrihni/Projects/iLSP/scripts/lsp_bridge.py "$@" 2>> "$LOG_FILE"

View File

@@ -31,7 +31,6 @@ BICEP_CATALOG="$DEVOPS_MCP_REPO/bicep_modules_catalog.json"
IAC_CATALOG="$DEVOPS_MCP_REPO/iac_source_catalog.json" IAC_CATALOG="$DEVOPS_MCP_REPO/iac_source_catalog.json"
ILSP_REPO="$(cd "$(dirname "$0")/.." && pwd)" ILSP_REPO="$(cd "$(dirname "$0")/.." && pwd)"
TMPL_CATALOG="$ILSP_REPO/pipeline_templates_catalog.json" TMPL_CATALOG="$ILSP_REPO/pipeline_templates_catalog.json"
PRINCIPALS_CATALOG="$ILSP_REPO/principals_catalog.json"
echo "── iLSP catalog push ──────────────────────────────" echo "── iLSP catalog push ──────────────────────────────"
@@ -51,13 +50,6 @@ if [[ ! -f "$TMPL_CATALOG" ]]; then
fi fi
echo "$(basename "$TMPL_CATALOG") ($(du -sh "$TMPL_CATALOG" | cut -f1))" echo "$(basename "$TMPL_CATALOG") ($(du -sh "$TMPL_CATALOG" | cut -f1))"
if [[ ! -f "$PRINCIPALS_CATALOG" ]]; then
echo " ✗ Not found: $PRINCIPALS_CATALOG"
echo " Run: python3 $ILSP_REPO/scripts/sync_principals_catalog.py"
exit 1
fi
echo "$(basename "$PRINCIPALS_CATALOG") ($(du -sh "$PRINCIPALS_CATALOG" | cut -f1))"
# Copy iac_source_catalog.json to iLSP repo root so it gets baked into the Docker image # Copy iac_source_catalog.json to iLSP repo root so it gets baked into the Docker image
echo "" echo ""
echo " → Copying iac_source_catalog.json to iLSP repo root (for Docker bake) …" echo " → Copying iac_source_catalog.json to iLSP repo root (for Docker bake) …"
@@ -67,7 +59,7 @@ echo " ✓ Copied to $ILSP_REPO/iac_source_catalog.json"
echo "" echo ""
echo " → Copying to $AUTOBOX:$REMOTE_DIR/ …" echo " → Copying to $AUTOBOX:$REMOTE_DIR/ …"
ssh "$AUTOBOX" "mkdir -p $REMOTE_DIR" ssh "$AUTOBOX" "mkdir -p $REMOTE_DIR"
scp "$BICEP_CATALOG" "$IAC_CATALOG" "$TMPL_CATALOG" "$PRINCIPALS_CATALOG" "$AUTOBOX:$REMOTE_DIR/" scp "$BICEP_CATALOG" "$IAC_CATALOG" "$TMPL_CATALOG" "$AUTOBOX:$REMOTE_DIR/"
echo " ✓ Upload done" echo " ✓ Upload done"
if [[ "$NO_RELOAD" == "true" ]]; then if [[ "$NO_RELOAD" == "true" ]]; then
@@ -86,5 +78,5 @@ else
fi fi
echo "" echo ""
echo " Done. Bicep + YAML pipeline template + principals completions updated." echo " Done. Bicep + YAML pipeline template completions updated."
echo " Note: iac_source_catalog.json was copied to repo root — commit + push to bake into next Docker image." echo " Note: iac_source_catalog.json was copied to repo root — commit + push to bake into next Docker image."

View File

@@ -1,214 +0,0 @@
#!/usr/bin/env python3
"""
sync_principals_catalog.py — Build principals_catalog.json by scanning .bicep files
for array param values (GUIDs) and their inline comments.
Scans configured IaC repo directories for patterns like:
additionalAccess: ['c88bf29d-...'] // LRIADMPRO-IaC-Bicep
additionalAccess: [
'c88bf29d-...' // LRIADMPRO-IaC-Bicep
'another-guid' // Another-SP
]
Usage:
python3 scripts/sync_principals_catalog.py
python3 scripts/sync_principals_catalog.py --paths ~/IdeaProjects/Bitbucket/IaC
python3 scripts/sync_principals_catalog.py --dry-run
python3 scripts/sync_principals_catalog.py --output /path/to/principals_catalog.json
"""
import argparse
import json
import logging
import pathlib
import re
import sys
from datetime import datetime, timezone
from typing import Any
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
log = logging.getLogger(__name__)
_REPO_ROOT = pathlib.Path(__file__).parent.parent
_DEFAULT_OUTPUT = _REPO_ROOT / "principals_catalog.json"
# Default paths to scan — adjust to match your IaC repo locations
_DEFAULT_SCAN_PATHS = [
"~/IdeaProjects/Bitbucket/IaC",
"~/IdeaProjects/Bitbucket/LRU",
]
# Matches a UUID/GUID
_GUID_RE = re.compile(
r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
)
# Matches a single quoted GUID optionally followed by an inline comment:
# 'c88bf29d-...' // Some label text
# 'c88bf29d-...' // or with hash comments
_ITEM_RE = re.compile(
r"'(" + _GUID_RE.pattern + r")'\s*(?://+\s*(.+?)\s*)?$",
re.IGNORECASE,
)
# Matches the opening of an array param assignment:
# additionalAccess: [ or additionalAccess: ['guid'
_ARRAY_OPEN_RE = re.compile(r"^\s*(\w+)\s*:\s*\[")
def _extract_label(comment: str | None) -> str | None:
"""Clean up an inline comment to use as a display label."""
if not comment:
return None
# Strip trailing punctuation and whitespace
return comment.strip().rstrip(".,;")
def scan_file(path: pathlib.Path) -> dict[str, list[dict[str, Any]]]:
"""Scan a single .bicep file and return {param_name: [{id, label, source}]}."""
try:
text = path.read_text(encoding="utf-8")
except Exception as exc:
log.debug("Cannot read %s: %s", path, exc)
return {}
lines = text.splitlines()
results: dict[str, list[dict[str, Any]]] = {}
i = 0
while i < len(lines):
line = lines[i]
array_m = _ARRAY_OPEN_RE.match(line)
if not array_m:
i += 1
continue
param_name = array_m.group(1)
# Collect all characters on this and subsequent lines until array closes
collected = line[array_m.end() - 1:] # from '[' onwards
j = i + 1
# If the array doesn't close on the same line, keep accumulating
while collected.count("[") > collected.count("]") and j < len(lines):
collected += "\n" + lines[j]
j += 1
# Extract all GUID items from the collected block
for item_line in collected.splitlines():
m = _ITEM_RE.search(item_line)
if not m:
continue
guid = m.group(1).lower()
label = _extract_label(m.group(2))
entry: dict[str, Any] = {
"id": guid,
"label": label or guid,
"source": str(path),
}
if label:
entry["description"] = f"From {path.name}"
results.setdefault(param_name, [])
results[param_name].append(entry)
i = j
return results
def scan_paths(paths: list[pathlib.Path]) -> dict[str, list[dict[str, Any]]]:
"""Scan all .bicep files under the given paths, deduplicating GUIDs per param."""
# param_name → {guid → entry} (dict for dedup)
merged: dict[str, dict[str, dict[str, Any]]] = {}
files_scanned = 0
for base in paths:
if not base.exists():
log.warning("Path not found, skipping: %s", base)
continue
for bicep_file in sorted(base.rglob("*.bicep")):
file_results = scan_file(bicep_file)
files_scanned += 1
for param, entries in file_results.items():
bucket = merged.setdefault(param, {})
for entry in entries:
guid = entry["id"]
if guid not in bucket:
bucket[guid] = entry
else:
# Keep the entry with the most informative label
existing = bucket[guid]
if entry.get("label") and entry["label"] != guid:
if existing.get("label") == guid or not existing.get("label"):
bucket[guid] = entry
log.info("Scanned %d .bicep files across %d path(s)", files_scanned, len(paths))
# Flatten back to lists, sorted by label
return {
param: sorted(entries.values(), key=lambda e: e.get("label", e["id"]).lower())
for param, entries in sorted(merged.items())
}
def build_catalog(scan_paths_list: list[pathlib.Path]) -> dict[str, Any]:
params = scan_paths(scan_paths_list)
total = sum(len(v) for v in params.values())
log.info("Found %d unique entries across %d param(s)", total, len(params))
return {
"synced_at": datetime.now(timezone.utc).isoformat(),
"entry_count": total,
"params": params,
}
def main() -> None:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--paths",
nargs="+",
default=_DEFAULT_SCAN_PATHS,
metavar="PATH",
help="Directories to scan for .bicep files (default: %(default)s)",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print findings without writing the catalog",
)
parser.add_argument(
"--output",
default=str(_DEFAULT_OUTPUT),
help="Output JSON file (default: %(default)s)",
)
args = parser.parse_args()
resolved = [pathlib.Path(p).expanduser().resolve() for p in args.paths]
catalog = build_catalog(resolved)
if args.dry_run:
print(f"\n── Principals catalog (dry-run) ──────────────────────")
if not catalog["params"]:
print(" No GUID values found in .bicep files.")
for param, entries in catalog["params"].items():
print(f"\n param: {param} ({len(entries)} entries)")
for e in entries:
print(f" {e['label']:<40} {e['id']}")
print(f"\n Total: {catalog['entry_count']} entries")
return
out = pathlib.Path(args.output)
# Strip internal 'source' field — not needed at runtime
for entries in catalog["params"].values():
for e in entries:
e.pop("source", None)
out.write_text(json.dumps(catalog, indent=2, ensure_ascii=False), encoding="utf-8")
log.info("Written: %s (%d entries)", out, catalog["entry_count"])
if __name__ == "__main__":
main()

View File

@@ -18,7 +18,6 @@ def test_frame_produces_correct_header():
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def reset_modules(): def reset_modules():
BicepModuleCatalog._modules = [] BicepModuleCatalog._modules = []
BicepModuleCatalog._principals = {}
yield yield
@@ -174,8 +173,12 @@ def test_param_completions_injected_on_param_context():
assert "roleDefinitionIds" in labels assert "roleDefinitionIds" in labels
assert "principalId" in labels assert "principalId" in labels
assert "principalType" in labels assert "principalType" in labels
assert items[0]["sortText"].startswith("0_lru_param_") # First item is now the snippet completion
assert items[0]["kind"] == 5 # Field assert items[0]["label"] == "⚡ Fill params block"
assert items[0]["kind"] == 15 # Snippet
# Second item should be a regular param
assert items[1]["sortText"].startswith("0_lru_param_")
assert items[1]["kind"] == 5 # Field
def test_param_completion_items_have_insert_text(): def test_param_completion_items_have_insert_text():
@@ -183,7 +186,12 @@ def test_param_completion_items_have_insert_text():
"roleassignments", versions=["1.1.x"], schema=_ROLEASSIGNMENTS_SCHEMA "roleassignments", versions=["1.1.x"], schema=_ROLEASSIGNMENTS_SCHEMA
)] )]
items = BicepModuleCatalog.param_completion_items("roleassignments", "1.1.x") items = BicepModuleCatalog.param_completion_items("roleassignments", "1.1.x")
for item in items: # First item should be the snippet completion
assert items[0]["label"] == "⚡ Fill params block"
assert items[0]["insertTextFormat"] == 2
assert "principalId" in items[0]["insertText"]
# Individual param items should have ": " suffix
for item in items[1:]:
assert item["insertText"].endswith(": ") assert item["insertText"].endswith(": ")
@@ -303,6 +311,48 @@ def test_detect_param_value_context_open_quote():
assert ctx["has_open_quote"] is True assert ctx["has_open_quote"] is True
def test_detect_param_value_context_in_array():
"""Cursor inside array value → param_value context."""
lines = [
"module myMod 'br/modules:roleassignments:1.1.x' = {",
" params: {",
" roles: ['KEY_VAULT_", # ← cursor inside array element
" }",
"}",
]
session = _make_session_with_doc(URI, lines)
# character = len(" roles: ['KEY_VAULT_") = 23
ctx = session._detect_context(URI, {"line": 2, "character": 23})
assert ctx["type"] == "param_value"
assert ctx["module"] == "roleassignments"
assert ctx["param"] == "roles"
assert ctx["has_open_quote"] is True
def test_detect_param_value_context_in_multiline_array():
"""Cursor in multi-line array element → param_value context."""
lines = [
"module myMod 'br/modules:roleassignments:2.0.x' = {",
" params: {",
" assignments: [",
" {",
" roles: [",
" 'APP_CONFIGURATION_", # ← cursor on separate line
" ]",
" }",
" ]",
" }",
"}",
]
session = _make_session_with_doc(URI, lines)
# character = len(" 'APP_CONFIGURATION_") = 27
ctx = session._detect_context(URI, {"line": 5, "character": 27})
assert ctx["type"] == "param_value"
assert ctx["module"] == "roleassignments"
assert ctx["param"] == "roles"
assert ctx["has_open_quote"] is True
def test_param_value_items_from_catalog_allowed(): def test_param_value_items_from_catalog_allowed():
"""environmentType completions come from catalog 'allowed' field.""" """environmentType completions come from catalog 'allowed' field."""
BicepModuleCatalog._modules = [_make_module( BicepModuleCatalog._modules = [_make_module(
@@ -354,6 +404,24 @@ def test_param_value_items_known_enum_fallback():
assert "User" in labels assert "User" in labels
def test_param_value_items_roles_enum():
"""roles parameter uses _KNOWN_ENUMS for Azure role completions."""
BicepModuleCatalog._modules = [_make_module(
"roleassignments",
versions=["1.1.x"],
schema={"1.1.x": {"parameters": {
"roles": {"type": "array"}, # no 'allowed' in catalog
}}},
)]
items = BicepModuleCatalog.param_value_completion_items(
"roleassignments", "1.1.x", "roles"
)
labels = [i["label"] for i in items]
assert "KEY_VAULT_SECRETS_USER" in labels
assert "STORAGE_BLOB_DATA_READER" in labels
assert "CONTRIBUTOR" in labels
def test_param_value_items_empty_for_free_string(): def test_param_value_items_empty_for_free_string():
"""A plain string param with no allowed values returns no completions.""" """A plain string param with no allowed values returns no completions."""
BicepModuleCatalog._modules = [_make_module( BicepModuleCatalog._modules = [_make_module(
@@ -387,111 +455,6 @@ def test_param_value_injected_in_completion_response():
assert labels == ["DEV", "TEST", "PROD"] assert labels == ["DEV", "TEST", "PROD"]
# ── Array item completion tests ────────────────────────────────────────────────
def test_detect_param_array_item_context():
"""Cursor inside [...] for an array param → param_array_item context."""
lines = [
"module keyVault 'br/modules:modules/keyvault:2.1.x' = {",
" params: {",
" additionalAccess: ['", # ← cursor after opening quote inside array
" }",
"}",
]
session = _make_session_with_doc(URI, lines)
# character 24 = after the `'` (4 spaces + 16 "additionalAccess" + 2 ": " + 1 "[" + 1 "'" = 24)
ctx = session._detect_context(URI, {"line": 2, "character": 24})
assert ctx["type"] == "param_array_item"
assert ctx["module"] == "modules/keyvault"
assert ctx["param"] == "additionalAccess"
assert ctx["has_open_quote"] is True
def test_detect_param_array_item_context_no_quote():
"""Cursor at start of empty array → param_array_item, no open quote."""
lines = [
"module keyVault 'br/modules:modules/keyvault:2.1.x' = {",
" params: {",
" additionalAccess: [", # ← cursor after [
" }",
"}",
]
session = _make_session_with_doc(URI, lines)
ctx = session._detect_context(URI, {"line": 2, "character": 23})
assert ctx["type"] == "param_array_item"
assert ctx["has_open_quote"] is False
def test_detect_param_array_item_context_after_existing_item():
"""Cursor after an existing item inside array → still param_array_item."""
lines = [
"module keyVault 'br/modules:modules/keyvault:2.1.x' = {",
" params: {",
" additionalAccess: ['c88bf29d-b13a-4153-9738-8995085a451e', '",
" }",
"}",
]
session = _make_session_with_doc(URI, lines)
char = len(" additionalAccess: ['c88bf29d-b13a-4153-9738-8995085a451e', '")
ctx = session._detect_context(URI, {"line": 2, "character": char})
assert ctx["type"] == "param_array_item"
assert ctx["has_open_quote"] is True
def test_param_array_item_completion_items():
"""param_array_item_completion_items returns entries from principals catalog."""
BicepModuleCatalog._principals = {
"additionalAccess": [
{"id": "aaaa-bbbb", "label": "My-SP", "description": "Test SP"},
]
}
items = BicepModuleCatalog.param_array_item_completion_items(
"modules/keyvault", "2.1.x", "additionalAccess"
)
assert len(items) == 1
assert items[0]["label"] == "My-SP"
assert items[0]["detail"] == "aaaa-bbbb"
assert items[0]["insertText"] == "'aaaa-bbbb'"
def test_param_array_item_completion_items_open_quote():
BicepModuleCatalog._principals = {
"additionalAccess": [{"id": "aaaa-bbbb", "label": "My-SP"}]
}
items = BicepModuleCatalog.param_array_item_completion_items(
"modules/keyvault", "2.1.x", "additionalAccess", has_open_quote=True
)
assert items[0]["insertText"] == "aaaa-bbbb'"
def test_param_array_item_empty_when_no_catalog_entry():
BicepModuleCatalog._principals = {}
items = BicepModuleCatalog.param_array_item_completion_items(
"modules/keyvault", "2.1.x", "additionalAccess"
)
assert items == []
def test_param_array_item_injected_in_completion_response():
"""Full pipeline: param_array_item context injects principal completions."""
BicepModuleCatalog._principals = {
"additionalAccess": [{"id": "aaaa-1111", "label": "IaC-SP", "description": "Pipeline SP"}]
}
msg = _completion_response([{"label": "noise", "sortText": "z"}])
ctx = {
"type": "param_array_item",
"module": "modules/keyvault",
"version": "2.1.x",
"param": "additionalAccess",
"has_open_quote": False,
}
out = json.loads(_inject_completions(msg, ctx))
items = out["result"]["items"]
assert len(items) == 1
assert items[0]["label"] == "IaC-SP"
assert items[0]["detail"] == "aaaa-1111"
def test_detect_unknown_context_outside_module(): def test_detect_unknown_context_outside_module():
lines = ["var x = 'hello'"] lines = ["var x = 'hello'"]
session = _make_session_with_doc(URI, lines) session = _make_session_with_doc(URI, lines)