From 514e0cfa602e8878ac31c3d039c4512d619b7639 Mon Sep 17 00:00:00 2001 From: Garima Garg Date: Thu, 6 Aug 2026 16:50:17 -0700 Subject: [PATCH] Update Standard Logic App templates and scripts (logic-app-changes) Updates the Standard Logic App workflow templates and supporting documentation from the approved `logic-app-changes` export. - Refreshes Initialization, Orchestrator, SCIMTests, and UserTests workflows - Updates Orchestrator parameters - Updates Standard Logic App setup and onboarding documentation - Bumps StandardLogicApp VERSION from 7.0 to 8.0 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Initialization_Workflow.json | 4 +- .../Orchestrator_Parameters.json | 91 ++-- .../Orchestrator_Workflow.json | 17 +- .../StandardLogicApp/SCIMTests_Workflow.json | 504 +++++++++++++++--- .../SetupLogicApp-Standard-Agent.md | 136 +++-- .../StandardLogicApp/UserTests_Workflow.json | 2 +- .../StandardLogicApp/VERSION | 2 +- .../StandardLogicApp/scim-onboarding.agent.md | 308 +---------- 8 files changed, 611 insertions(+), 453 deletions(-) diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Initialization_Workflow.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Initialization_Workflow.json index e4661de9..5735f313 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Initialization_Workflow.json +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Initialization_Workflow.json @@ -952,7 +952,7 @@ "type": "Select", "inputs": { "from": "@body('Compose_SCIM_User_Extension_URNs')", - "select": "@join(take(split(item()?['name'], ':'), sub(length(split(item()?['name'], ':')), 1)), ':')" + "select": "@substring(item()?['name'], 0, lastIndexOf(item()?['name'], ':'))" } }, "Compose_SCIM_User_Schemas": { @@ -1025,7 +1025,7 @@ ] }, "type": "Compose", - "inputs": "@if(or(or(or(or(equals(items('ForEach_Build_SCIM_Body')?['name'], 'id'), equals(toLower(coalesce(items('ForEach_Build_SCIM_Body')?['type'], 'string')), 'reference')), equals(items('ForEach_Build_SCIM_Body')?['name'], 'externalId')), or(equals(items('ForEach_Build_SCIM_Body')?['name'], 'userName'), equals(items('ForEach_Build_SCIM_Body')?['name'], 'active'))), equals(items('ForEach_Build_SCIM_Body')?['name'], 'roles')), variables('scimDirectUserCreateBody'), if(startsWith(items('ForEach_Build_SCIM_Body')?['name'], 'urn:'), union(variables('scimDirectUserCreateBody'), json(concat('{\"', join(take(split(items('ForEach_Build_SCIM_Body')?['name'], ':'), sub(length(split(items('ForEach_Build_SCIM_Body')?['name'], ':')), 1)), ':'), '\":', string(setProperty(coalesce(variables('scimDirectUserCreateBody')?[join(take(split(items('ForEach_Build_SCIM_Body')?['name'], ':'), sub(length(split(items('ForEach_Build_SCIM_Body')?['name'], ':')), 1)), ':')], json('{}')), last(split(items('ForEach_Build_SCIM_Body')?['name'], ':')), outputs('Compose_SCIM_Source_Value'))), '}'))), if(contains(items('ForEach_Build_SCIM_Body')?['name'], '['), setProperty(variables('scimDirectUserCreateBody'), first(split(items('ForEach_Build_SCIM_Body')?['name'], '[')), setProperty(coalesce(variables('scimDirectUserCreateBody')?[first(split(items('ForEach_Build_SCIM_Body')?['name'], '['))], json('{}')), first(skip(split(items('ForEach_Build_SCIM_Body')?['name'], '\"'), 1)), setProperty(coalesce(variables('scimDirectUserCreateBody')?[first(split(items('ForEach_Build_SCIM_Body')?['name'], '['))]?[first(skip(split(items('ForEach_Build_SCIM_Body')?['name'], '\"'), 1))], json(concat('{\"type\":\"', first(skip(split(items('ForEach_Build_SCIM_Body')?['name'], '\"'), 1)), '\",\"primary\":', if(not(contains(string(coalesce(variables('scimDirectUserCreateBody')?[first(split(items('ForEach_Build_SCIM_Body')?['name'], '['))], json('{}'))), '\"primary\":true')), 'true', 'false'), '}'))), last(split(items('ForEach_Build_SCIM_Body')?['name'], '.')), outputs('Compose_SCIM_Source_Value')))), if(contains(items('ForEach_Build_SCIM_Body')?['name'], '.'), setProperty(variables('scimDirectUserCreateBody'), first(split(items('ForEach_Build_SCIM_Body')?['name'], '.')), setProperty(coalesce(variables('scimDirectUserCreateBody')?[first(split(items('ForEach_Build_SCIM_Body')?['name'], '.'))], json('{}')), last(split(items('ForEach_Build_SCIM_Body')?['name'], '.')), outputs('Compose_SCIM_Source_Value'))), setProperty(variables('scimDirectUserCreateBody'), items('ForEach_Build_SCIM_Body')?['name'], outputs('Compose_SCIM_Source_Value'))))))" + "inputs": "@if(or(or(or(or(equals(items('ForEach_Build_SCIM_Body')?['name'], 'id'), equals(toLower(coalesce(items('ForEach_Build_SCIM_Body')?['type'], 'string')), 'reference')), equals(items('ForEach_Build_SCIM_Body')?['name'], 'externalId')), or(equals(items('ForEach_Build_SCIM_Body')?['name'], 'userName'), equals(items('ForEach_Build_SCIM_Body')?['name'], 'active'))), equals(items('ForEach_Build_SCIM_Body')?['name'], 'roles')), variables('scimDirectUserCreateBody'), if(startsWith(items('ForEach_Build_SCIM_Body')?['name'], 'urn:'), union(variables('scimDirectUserCreateBody'), json(concat('{\"', substring(items('ForEach_Build_SCIM_Body')?['name'], 0, lastIndexOf(items('ForEach_Build_SCIM_Body')?['name'], ':')), '\":', string(setProperty(coalesce(variables('scimDirectUserCreateBody')?[substring(items('ForEach_Build_SCIM_Body')?['name'], 0, lastIndexOf(items('ForEach_Build_SCIM_Body')?['name'], ':'))], json('{}')), last(split(items('ForEach_Build_SCIM_Body')?['name'], ':')), outputs('Compose_SCIM_Source_Value'))), '}'))), if(contains(items('ForEach_Build_SCIM_Body')?['name'], '['), setProperty(variables('scimDirectUserCreateBody'), first(split(items('ForEach_Build_SCIM_Body')?['name'], '[')), setProperty(coalesce(variables('scimDirectUserCreateBody')?[first(split(items('ForEach_Build_SCIM_Body')?['name'], '['))], json('{}')), first(skip(split(items('ForEach_Build_SCIM_Body')?['name'], '\"'), 1)), setProperty(coalesce(variables('scimDirectUserCreateBody')?[first(split(items('ForEach_Build_SCIM_Body')?['name'], '['))]?[first(skip(split(items('ForEach_Build_SCIM_Body')?['name'], '\"'), 1))], json(concat('{\"type\":\"', first(skip(split(items('ForEach_Build_SCIM_Body')?['name'], '\"'), 1)), '\",\"primary\":', if(not(contains(string(coalesce(variables('scimDirectUserCreateBody')?[first(split(items('ForEach_Build_SCIM_Body')?['name'], '['))], json('{}'))), '\"primary\":true')), 'true', 'false'), '}'))), last(split(items('ForEach_Build_SCIM_Body')?['name'], '.')), outputs('Compose_SCIM_Source_Value')))), if(contains(items('ForEach_Build_SCIM_Body')?['name'], '.'), setProperty(variables('scimDirectUserCreateBody'), first(split(items('ForEach_Build_SCIM_Body')?['name'], '.')), setProperty(coalesce(variables('scimDirectUserCreateBody')?[first(split(items('ForEach_Build_SCIM_Body')?['name'], '.'))], json('{}')), last(split(items('ForEach_Build_SCIM_Body')?['name'], '.')), outputs('Compose_SCIM_Source_Value'))), setProperty(variables('scimDirectUserCreateBody'), items('ForEach_Build_SCIM_Body')?['name'], outputs('Compose_SCIM_Source_Value'))))))" }, "Set_SCIM_Body_Property": { "runAfter": { diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json index 97556e9a..bd852a36 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json @@ -22,6 +22,7 @@ "SCIM_User_Create_Test", "SCIM_Group_Create_Test", "SCIM_User_Update_Test", + "SCIM_Update_Manager_Test", "SCIM_Group_Update_Test", "SCIM_User_Pagination_Test", "SCIM_Group_Pagination_Test", @@ -38,7 +39,7 @@ "description": "Version of the Logic App for tracking and deployment purposes" }, "type": "String", - "value": "4.0" + "value": "8.0" }, "attributeTransforms": { "metadata": { @@ -226,7 +227,7 @@ }, "scimEndpoint": { "type": "String", - "value": "https://scimserver2.yellowsmoke-af7a3fff.eastus.azurecontainerapps.io/scim/endpoints/8f3c4c4a-7b5d-4e91-9a62-2d7f1b8e5c73" + "value": "https://scimserver2.yellowsmoke-af7a3fff.eastus.azurecontainerapps.io/scim/endpoints/f4c95b63-db4c-4f0a-91b0-78fdf88007be" }, "scimOAuthScope": { "metadata": { @@ -243,61 +244,61 @@ "value": "https://scimserver2.yellowsmoke-af7a3fff.eastus.azurecontainerapps.io/scim/oauth/token" }, "federatedEntraTenantId": { - "metadata": { - "description": "Entra tenant ID used to construct the token endpoint URL for federated identity token acquisition." + "metadata": { + "description": "Entra tenant ID used to construct the token endpoint URL for federated identity token acquisition." + }, + "type": "String", + "value": "changeme-tenant-id" }, - "type": "String", - "value": "changeme-tenant-id" - }, - "federatedApplicationId": { - "metadata": { - "description": "OAuth2 Application ID for the Entra app registration used in federated identity token acquisition." + "federatedApplicationId": { + "metadata": { + "description": "OAuth2 Application ID for the Entra app registration used in federated identity token acquisition." + }, + "type": "String", + "value": "changeme-client-id" }, - "type": "String", - "value": "changeme-client-id" - }, - "federatedApplicationClientSecret": { - "metadata": { - "description": "OAuth2 Client Secret for the Entra app registration used in federated identity token acquisition." + "federatedApplicationClientSecret": { + "metadata": { + "description": "OAuth2 Client Secret for the Entra app registration used in federated identity token acquisition." + }, + "type": "String", + "value": "changeme-client-secret" }, - "type": "String", - "value": "changeme-client-secret" - }, - "federatedTokenEndpoint": { - "metadata": { - "description": "ISV token endpoint URL for federated identity token exchange." + "federatedTokenEndpoint": { + "metadata": { + "description": "ISV token endpoint URL for federated identity token exchange." + }, + "type": "String", + "value": "changeme-federated-endpoint" }, - "type": "String", - "value": "changeme-federated-endpoint" - }, - "federatedClientId": { - "metadata": { - "description": "ISV client ID used in federated identity token exchange." + "federatedClientId": { + "metadata": { + "description": "ISV client ID used in federated identity token exchange." + }, + "type": "String", + "value": "changeme-federated-app-id" }, - "type": "String", - "value": "changeme-federated-app-id" - }, - "federatedBaseAddress": { - "metadata": { - "description": "ISV base address sent in the federated identity token exchange request." + "federatedBaseAddress": { + "metadata": { + "description": "ISV base address sent in the federated identity token exchange request." + }, + "type": "String", + "value": "changeme-base-address" }, - "type": "String", - "value": "changeme-base-address" - }, - "federatedAudience": { - "metadata": { - "description": "Audience value for federated token exchange." + "federatedAudience": { + "metadata": { + "description": "Audience value for federated token exchange." + }, + "type": "String", + "value": "changeme-audience" }, - "type": "String", - "value": "changeme-audience" - }, "servicePrincipalId": { "type": "String", - "value": "ed1a4e07-8807-4362-8b9b-c8edc2b51747" + "value": "3197053d-46d9-4a22-b843-6a9d98ec267e" }, "testUserDomain": { "type": "String", - "value": "proviamla01.onmicrosoft.com" + "value": "proviamtest07.onmicrosoft.com" }, "scimTargetUserValues": { "type": "Array", diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Workflow.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Workflow.json index d7745e7b..bd8a91d8 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Workflow.json +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Workflow.json @@ -462,7 +462,8 @@ "validateCredentialsTestEnabled": "@or(equals(parameters('EnabledTests'), 'All'), equals(parameters('EnabledTests'), 'SCIMTests'), equals(parameters('EnabledTests'), 'Validate_Credentials_Test'))", "federatedIdentityTestEnabled": "@or(equals(parameters('EnabledTests'), 'All'), equals(parameters('EnabledTests'), 'SCIMTests'), equals(parameters('EnabledTests'), 'Federated_Identity_Test'))", "scimUserPaginationTestEnabled": "@or(equals(parameters('EnabledTests'), 'All'), equals(parameters('EnabledTests'), 'SCIMTests'), equals(parameters('EnabledTests'), 'SCIM_User_Pagination_Test'))", - "scimGroupPaginationTestEnabled": "@or(equals(parameters('EnabledTests'), 'All'), equals(parameters('EnabledTests'), 'SCIMTests'), equals(parameters('EnabledTests'), 'SCIM_Group_Pagination_Test'))" + "scimGroupPaginationTestEnabled": "@or(equals(parameters('EnabledTests'), 'All'), equals(parameters('EnabledTests'), 'SCIMTests'), equals(parameters('EnabledTests'), 'SCIM_Group_Pagination_Test'))", + "scimUpdateManagerTestEnabled": "@and(contains(string(coalesce(body('Call_Initialization_Workflow')?['targetDirectoryUserAttributes'], json('[]'))), 'manager'), or(equals(parameters('EnabledTests'), 'All'), equals(parameters('EnabledTests'), 'SCIMTests'), equals(parameters('EnabledTests'), 'SCIM_Update_Manager_Test')))" }, "testResults": { "createUserTestResult": "@coalesce(body('Get_UserTests_Output_Content')?['CreateUserTestOutputs']?['overallResult'], 'SKIPPED')", @@ -488,7 +489,8 @@ "validateCredentialsTestResult": "@coalesce(body('Get_SCIMTests_Output_Content')?['ValidateCredentialsTestOutputs']?['overallResult'], 'SKIPPED')", "federatedIdentityTestResult": "@coalesce(body('Get_SCIMTests_Output_Content')?['FederatedIdentityTestOutputs']?['overallResult'], 'SKIPPED')", "scimUserPaginationTestResult": "@coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserPaginationTestOutputs']?['overallResult'], 'SKIPPED')", - "scimGroupPaginationTestResult": "@coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupPaginationTestOutputs']?['overallResult'], 'SKIPPED')" + "scimGroupPaginationTestResult": "@coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupPaginationTestOutputs']?['overallResult'], 'SKIPPED')", + "scimUpdateManagerTestResult": "@coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUpdateManagerTestOutputs']?['overallResult'], 'SKIPPED')" }, "childWorkflowStatuses": { "initializationStatus": "@coalesce(actions('Call_Initialization_Workflow')?['status'], 'Unknown')", @@ -502,7 +504,7 @@ "GroupTests_Workflow": "@outputs('Compose_GroupTests_RunLink')", "SCIMTests_Workflow": "@outputs('Compose_SCIMTests_RunLink')" }, - "overallLogicAppResult": "@if(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(equals(coalesce(body('Get_UserTests_Output_Content')?['CreateUserTestOutputs']?['overallResult'],'SKIPPED'),'FAILED'),equals(coalesce(body('Get_UserTests_Output_Content')?['UpdateUserTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_UserTests_Output_Content')?['DisableUserTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_UserTests_Output_Content')?['ManagerTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),or(equals(coalesce(body('Get_UserTests_Output_Content')?['RestoreUserTestOutputs']?['overallResult'],'SKIPPED'),'FAILED'),equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMNullUpdateTestOutputs']?['overallResult'],'SKIPPED'),'FAILED'))),equals(coalesce(body('Get_SCIMTests_Output_Content')?['ValidateCredentialsTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_SCIMTests_Output_Content')?['FederatedIdentityTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_GroupTests_Output_Content')?['CreateGroupTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_GroupTests_Output_Content')?['UpdateGroupTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_GroupTests_Output_Content')?['AddMemberTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_GroupTests_Output_Content')?['RemoveMemberTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_SCIMTests_Output_Content')?['SchemaDiscoverabilityTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserCreateTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupCreateTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserUpdateTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupUpdateTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserPaginationTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_UserTests_Output_Content')?['PODUserTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_GroupTests_Output_Content')?['PODGroupTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(actions('Call_Initialization_Workflow')?['status'],'Failed')),equals(coalesce(body('Poll_UserTests_Status')?['properties']?['status'],''),'Failed')),equals(coalesce(body('Poll_GroupTests_Status')?['properties']?['status'],''),'Failed')),and(not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['ValidateCredentialsTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['FederatedIdentityTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')))),not(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false))),'Failed',if(equals(coalesce(body('Poll_SCIMTests_Status')?['properties']?['status'],''),'Failed'),'Failed','Succeeded'))", + "overallLogicAppResult": "@if(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(or(equals(coalesce(body('Get_UserTests_Output_Content')?['CreateUserTestOutputs']?['overallResult'],'SKIPPED'),'FAILED'),equals(coalesce(body('Get_UserTests_Output_Content')?['UpdateUserTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_UserTests_Output_Content')?['DisableUserTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_UserTests_Output_Content')?['ManagerTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),or(equals(coalesce(body('Get_UserTests_Output_Content')?['RestoreUserTestOutputs']?['overallResult'],'SKIPPED'),'FAILED'),equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMNullUpdateTestOutputs']?['overallResult'],'SKIPPED'),'FAILED'))),equals(coalesce(body('Get_SCIMTests_Output_Content')?['ValidateCredentialsTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_SCIMTests_Output_Content')?['FederatedIdentityTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_GroupTests_Output_Content')?['CreateGroupTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_GroupTests_Output_Content')?['UpdateGroupTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_GroupTests_Output_Content')?['AddMemberTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_GroupTests_Output_Content')?['RemoveMemberTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_SCIMTests_Output_Content')?['SchemaDiscoverabilityTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserCreateTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupCreateTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserUpdateTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupUpdateTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserPaginationTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_UserTests_Output_Content')?['PODUserTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(coalesce(body('Get_GroupTests_Output_Content')?['PODGroupTestOutputs']?['overallResult'],'SKIPPED'),'FAILED')),equals(actions('Call_Initialization_Workflow')?['status'],'Failed')),equals(coalesce(body('Poll_UserTests_Status')?['properties']?['status'],''),'Failed')),equals(coalesce(body('Poll_GroupTests_Status')?['properties']?['status'],''),'Failed')),and(not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['ValidateCredentialsTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['FederatedIdentityTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')))),not(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false))),and(contains(string(coalesce(body('Call_Initialization_Workflow')?['targetDirectoryUserAttributes'],json('[]'))),'manager'),equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUpdateManagerTestOutputs']?['overallResult'],'SKIPPED'),'FAILED'))),'Failed',if(equals(coalesce(body('Poll_SCIMTests_Status')?['properties']?['status'],''),'Failed'),'Failed','Succeeded'))", "optionalTestsResult": "@if(or(or(or(equals(coalesce(body('Get_UserTests_Output_Content')?['DeleteUserTestOutputs']?['overallResult'], 'SKIPPED'), 'FAILED'), equals(coalesce(body('Get_GroupTests_Output_Content')?['DeleteGroupTestOutputs']?['overallResult'], 'SKIPPED'), 'FAILED')), equals(coalesce(body('Get_GroupTests_Output_Content')?['RestoreGroupTestOutputs']?['overallResult'], 'SKIPPED'), 'FAILED')), equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupPaginationTestOutputs']?['overallResult'], 'SKIPPED'), 'FAILED')), 'WARNING', 'PASSED')", "authenticationModelResult": "@if(or(equals(coalesce(body('Get_SCIMTests_Output_Content')?['ValidateCredentialsTestOutputs']?['overallResult'], 'SKIPPED'), 'PASSED'), equals(coalesce(body('Get_SCIMTests_Output_Content')?['FederatedIdentityTestOutputs']?['overallResult'], 'SKIPPED'), 'PASSED')), 'PASSED', 'FAILED')" } @@ -723,6 +725,15 @@ "runLink": "@if(equals(coalesce(body('Get_GroupTests_Output_Content')?['PODGroupTestOutputs']?['overallResult'], 'SKIPPED'), 'SKIPPED'), '', outputs('Compose_GroupTests_RunLink'))", "message": "@if(equals(coalesce(body('Get_GroupTests_Output_Content')?['PODGroupTestOutputs']?['overallResult'], 'SKIPPED'), 'SKIPPED'), '', 'Click the runLink and search for the action Compose_Final_Results for more info.')" }, + { + "testName": "SCIM_Update_Manager_Test", + "testCategory": "mandatory", + "testResult": "@if(not(contains(string(coalesce(body('Call_Initialization_Workflow')?['targetDirectoryUserAttributes'], json('[]'))), 'manager')), 'SKIPPED - manager attribute not in target directory', coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUpdateManagerTestOutputs']?['result'], 'SKIPPED'))", + "provisioningErrorDetails": "@if(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUpdateManagerTestOutputs']?['overallResult'], 'SKIPPED'), 'FAILED'), body('Get_SCIMTests_Output_Content')?['SCIMUpdateManagerTestOutputs']?['errorDetails'], '')", + "recommendationUrl": "@if(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUpdateManagerTestOutputs']?['overallResult'], 'SKIPPED'), 'FAILED'), 'https://learn.microsoft.com/en-us/azure/active-directory/app-provisioning/known-issues', '')", + "runLink": "@if(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUpdateManagerTestOutputs']?['overallResult'], 'SKIPPED'), 'SKIPPED'), '', outputs('Compose_SCIMTests_RunLink'))", + "message": "@if(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUpdateManagerTestOutputs']?['overallResult'], 'SKIPPED'), 'SKIPPED'), '', 'Click the runLink and search for the action Compose_Final_Results for more info.')" + }, { "testName": "Restore_Group_Test", "testCategory": "optional", diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_Workflow.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_Workflow.json index 756988dd..c4c9fbbd 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_Workflow.json +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_Workflow.json @@ -42,12 +42,34 @@ } }, "actions": { - "Response_Immediate": { + "Initialize_SCIM_Test_Variables_3": { "runAfter": { "Initialize_SCIM_Test_Variables_2": [ "Succeeded" ] }, + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "SCIMUpdateManagerTestOutputs", + "type": "Object", + "value": { + "overallResult": "SKIPPED", + "result": "SKIPPED", + "summary": null, + "errorDetails": null + } + } + ] + } + }, + "Response_Immediate": { + "runAfter": { + "Initialize_SCIM_Test_Variables_3": [ + "Succeeded" + ] + }, "type": "Response", "kind": "Http", "inputs": { @@ -331,6 +353,24 @@ } } }, + "Set_SchemaTestOutputs_On_Failure": { + "runAfter": { + "SCIM_Get_Schemas": ["Failed", "TimedOut"] + }, + "type": "SetVariable", + "inputs": { + "name": "SchemaDiscoverabilityTestOutputs", + "value": { + "overallResult": "FAILED", + "result": "@concat('FAILED - Action: SCIM_Get_Schemas - /Schemas endpoint returned HTTP ', string(coalesce(outputs('SCIM_Get_Schemas')?['statusCode'], 0)))", + "summary": null, + "detailedResults": [], + "errorDetails": { + "summary": "@concat('/Schemas endpoint not accessible. HTTP ', string(coalesce(outputs('SCIM_Get_Schemas')?['statusCode'], 0)), '. Response: ', substring(string(coalesce(body('SCIM_Get_Schemas'), json('{}'))), 0, min(500, length(string(coalesce(body('SCIM_Get_Schemas'), json('{}')))))))" + } + } + } + }, "Filter_User_Schema": { "runAfter": { "SCIM_Get_Schemas": [ @@ -343,7 +383,7 @@ "where": "@equals(item()?['id'], 'urn:ietf:params:scim:schemas:core:2.0:User')" } }, - "Filter_EnterpriseUser_Schema": { + "Filter_All_User_Extension_Schemas": { "runAfter": { "SCIM_Get_Schemas": [ "Succeeded" @@ -352,7 +392,7 @@ "type": "Query", "inputs": { "from": "@coalesce(body('SCIM_Get_Schemas')?['Resources'], json('[]'))", - "where": "@equals(item()?['id'], 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User')" + "where": "@and(not(equals(item()?['id'], 'urn:ietf:params:scim:schemas:core:2.0:User')), not(equals(item()?['id'], 'urn:ietf:params:scim:schemas:core:2.0:Group')))" } }, "Filter_Group_Schema": { @@ -931,7 +971,7 @@ }, "Compose_User_Lookup_String": { "runAfter": { - "Flatten_Enterprise_User_Attrs_Loop": [ + "ForEach_User_Extension_Schema": [ "Succeeded" ] }, @@ -1011,7 +1051,7 @@ "schemasEndpointStatusCode": "@outputs('SCIM_Get_Schemas')['statusCode']", "discoveredSchemasCount": "@length(coalesce(body('SCIM_Get_Schemas')?['Resources'], json('[]')))", "userSchemaFound": "@greater(length(body('Filter_User_Schema')), 0)", - "enterpriseUserSchemaFound": "@greater(length(body('Filter_EnterpriseUser_Schema')), 0)", + "extensionSchemasCount": "@length(body('Filter_All_User_Extension_Schemas'))", "groupSchemaFound": "@greater(length(body('Filter_Group_Schema')), 0)", "flattenedUserSchemaAttrs": "@variables('flattenedUserAttrNames')", "flattenedGroupSchemaAttrs": "@variables('flattenedGroupAttrNames')", @@ -1046,9 +1086,9 @@ } } }, - "Flatten_Enterprise_User_Attrs_Loop": { + "ForEach_User_Extension_Schema": { "runAfter": { - "Filter_EnterpriseUser_Schema": [ + "Filter_All_User_Extension_Schemas": [ "Succeeded" ], "Flatten_User_Attrs_Loop": [ @@ -1056,40 +1096,23 @@ ] }, "type": "Foreach", - "foreach": "@coalesce(first(body('Filter_EnterpriseUser_Schema'))?['attributes'], json('[]'))", + "foreach": "@body('Filter_All_User_Extension_Schemas')", "actions": { - "Check_Enterprise_Attr_Type": { - "type": "If", - "expression": { - "and": [ - { - "not": { - "equals": [ - "@items('Flatten_Enterprise_User_Attrs_Loop')?['type']", - "complex" - ] - } - } - ] - }, + "ForEach_Extension_Schema_Attr": { + "type": "Foreach", + "foreach": "@coalesce(items('ForEach_User_Extension_Schema')?['attributes'], json('[]'))", "actions": { - "Append_Enterprise_Simple_Attr": { + "Append_Extension_Attr": { "type": "AppendToArrayVariable", "inputs": { "name": "flattenedUserAttrNames", - "value": "@concat(first(body('Filter_EnterpriseUser_Schema'))?['id'], ':', items('Flatten_Enterprise_User_Attrs_Loop')?['name'])" + "value": "@concat(items('ForEach_User_Extension_Schema')?['id'], ':', items('ForEach_Extension_Schema_Attr')?['name'])" } } }, - "else": { - "actions": { - "Append_Enterprise_Complex_Attr": { - "type": "AppendToArrayVariable", - "inputs": { - "name": "flattenedUserAttrNames", - "value": "@concat(first(body('Filter_EnterpriseUser_Schema'))?['id'], ':', items('Flatten_Enterprise_User_Attrs_Loop')?['name'])" - } - } + "runtimeConfiguration": { + "concurrency": { + "repetitions": 1 } } } @@ -1147,13 +1170,10 @@ "inputs": { "name": "scimNullCreateBody", "value": { - "schemas": [ - "urn:ietf:params:scim:schemas:core:2.0:User", - "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" - ], + "schemas": "@coalesce(triggerBody()?['initializationData']?['scimUserBody']?['schemas'], createArray('urn:ietf:params:scim:schemas:core:2.0:User'))", "userName": "@{concat(outputs('Generate_SCIM_Null_Test_UserName'), '@', coalesce(last(split(coalesce(triggerBody()?['initializationData']?['userBody']?['userPrincipalName'], 'x@test.com'), '@')), 'test.com'))}", "active": true, - "externalId": "@outputs('Generate_SCIM_Null_Test_UserName')" + "externalId": "@{if(contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryUserAttributes'], json('[]'))), 'externalId'), guid(), '')}" } } }, @@ -1171,7 +1191,7 @@ ] }, "type": "Compose", - "inputs": "@if(or(or(equals(items('ForEach_Build_Null_Create_Body')?['source'], 'manager'), equals(items('ForEach_Build_Null_Create_Body')?['target'], 'userName')), or(equals(items('ForEach_Build_Null_Create_Body')?['target'], 'active'), equals(items('ForEach_Build_Null_Create_Body')?['target'], 'externalId')), equals(items('ForEach_Build_Null_Create_Body')?['target'], 'roles')), variables('scimNullCreateBody'), if(startsWith(items('ForEach_Build_Null_Create_Body')?['target'], 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:'), union(variables('scimNullCreateBody'), json(concat('{\"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\":', string(setProperty(coalesce(variables('scimNullCreateBody')?['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'], json('{}')), last(split(items('ForEach_Build_Null_Create_Body')?['target'], ':')), outputs('Compose_Null_Create_Source_Value'))), '}'))), if(contains(items('ForEach_Build_Null_Create_Body')?['target'], '['), setProperty(variables('scimNullCreateBody'), first(split(items('ForEach_Build_Null_Create_Body')?['target'], '[')), setProperty(coalesce(variables('scimNullCreateBody')?[first(split(items('ForEach_Build_Null_Create_Body')?['target'], '['))], json('{}')), first(skip(split(items('ForEach_Build_Null_Create_Body')?['target'], '\"'), 1)), setProperty(coalesce(variables('scimNullCreateBody')?[first(split(items('ForEach_Build_Null_Create_Body')?['target'], '['))]?[first(skip(split(items('ForEach_Build_Null_Create_Body')?['target'], '\"'), 1))], json(concat('{\"type\":\"', first(skip(split(items('ForEach_Build_Null_Create_Body')?['target'], '\"'), 1)), '\",\"primary\":', if(not(contains(string(coalesce(variables('scimNullCreateBody')?[first(split(items('ForEach_Build_Null_Create_Body')?['target'], '['))], json('{}'))), '\"primary\":true')), 'true', 'false'), '}'))), last(split(items('ForEach_Build_Null_Create_Body')?['target'], '.')), outputs('Compose_Null_Create_Source_Value')))), if(contains(items('ForEach_Build_Null_Create_Body')?['target'], '.'), setProperty(variables('scimNullCreateBody'), first(split(items('ForEach_Build_Null_Create_Body')?['target'], '.')), setProperty(coalesce(variables('scimNullCreateBody')?[first(split(items('ForEach_Build_Null_Create_Body')?['target'], '.'))], json('{}')), last(split(items('ForEach_Build_Null_Create_Body')?['target'], '.')), outputs('Compose_Null_Create_Source_Value'))), setProperty(variables('scimNullCreateBody'), items('ForEach_Build_Null_Create_Body')?['target'], outputs('Compose_Null_Create_Source_Value'))))))" + "inputs": "@if(or(or(equals(items('ForEach_Build_Null_Create_Body')?['source'], 'manager'), equals(items('ForEach_Build_Null_Create_Body')?['target'], 'userName')), or(equals(items('ForEach_Build_Null_Create_Body')?['target'], 'active'), equals(items('ForEach_Build_Null_Create_Body')?['target'], 'externalId')), equals(items('ForEach_Build_Null_Create_Body')?['target'], 'roles')), variables('scimNullCreateBody'), if(startsWith(items('ForEach_Build_Null_Create_Body')?['target'], 'urn:'), union(variables('scimNullCreateBody'), json(concat('{\"', substring(items('ForEach_Build_Null_Create_Body')?['target'], 0, lastIndexOf(items('ForEach_Build_Null_Create_Body')?['target'], ':')), '\":', string(setProperty(coalesce(variables('scimNullCreateBody')?[substring(items('ForEach_Build_Null_Create_Body')?['target'], 0, lastIndexOf(items('ForEach_Build_Null_Create_Body')?['target'], ':'))], json('{}')), last(split(items('ForEach_Build_Null_Create_Body')?['target'], ':')), outputs('Compose_Null_Create_Source_Value'))), '}'))), if(contains(items('ForEach_Build_Null_Create_Body')?['target'], '['), setProperty(variables('scimNullCreateBody'), first(split(items('ForEach_Build_Null_Create_Body')?['target'], '[')), setProperty(coalesce(variables('scimNullCreateBody')?[first(split(items('ForEach_Build_Null_Create_Body')?['target'], '['))], json('{}')), first(skip(split(items('ForEach_Build_Null_Create_Body')?['target'], '\"'), 1)), setProperty(coalesce(variables('scimNullCreateBody')?[first(split(items('ForEach_Build_Null_Create_Body')?['target'], '['))]?[first(skip(split(items('ForEach_Build_Null_Create_Body')?['target'], '\"'), 1))], json(concat('{\"type\":\"', first(skip(split(items('ForEach_Build_Null_Create_Body')?['target'], '\"'), 1)), '\",\"primary\":', if(not(contains(string(coalesce(variables('scimNullCreateBody')?[first(split(items('ForEach_Build_Null_Create_Body')?['target'], '['))], json('{}'))), '\"primary\":true')), 'true', 'false'), '}'))), last(split(items('ForEach_Build_Null_Create_Body')?['target'], '.')), outputs('Compose_Null_Create_Source_Value')))), if(contains(items('ForEach_Build_Null_Create_Body')?['target'], '.'), setProperty(variables('scimNullCreateBody'), first(split(items('ForEach_Build_Null_Create_Body')?['target'], '.')), setProperty(coalesce(variables('scimNullCreateBody')?[first(split(items('ForEach_Build_Null_Create_Body')?['target'], '.'))], json('{}')), last(split(items('ForEach_Build_Null_Create_Body')?['target'], '.')), outputs('Compose_Null_Create_Source_Value'))), setProperty(variables('scimNullCreateBody'), items('ForEach_Build_Null_Create_Body')?['target'], outputs('Compose_Null_Create_Source_Value'))))))" }, "Set_scimNullCreateBody": { "runAfter": { @@ -1320,7 +1340,7 @@ "type": "Query", "inputs": { "from": "@coalesce(triggerBody()?['initializationData']?['sourceToTargetLookup'], json('[]'))", - "where": "@and(equals(coalesce(item()?['matchingPrecedence'], 0), 0), not(contains(toLower(coalesce(item()?['targetSchema']?['type'], '')), 'reference')), not(equals(coalesce(item()?['target'], ''), 'userName')), not(equals(coalesce(item()?['target'], ''), 'active')), not(equals(coalesce(item()?['target'], ''), 'externalId')), not(equals(coalesce(item()?['target'], ''), 'roles')), if(startsWith(coalesce(item()?['target'], ''), 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:'), not(contains(string(body('Query_Required_Enterprise_Attrs')), concat('\"name\":\"', last(split(item()?['target'], ':')), '\"'))), not(contains(string(body('Query_Required_Core_Attrs')), concat('\"name\":\"', if(contains(coalesce(item()?['target'], ''), '['), first(split(item()?['target'], '[')), if(contains(coalesce(item()?['target'], ''), '.'), first(split(item()?['target'], '.')), coalesce(item()?['target'], ''))), '\"')))))" + "where": "@and(equals(coalesce(item()?['matchingPrecedence'], 0), 0), not(contains(toLower(coalesce(item()?['targetSchema']?['type'], '')), 'reference')), not(equals(coalesce(item()?['target'], ''), 'userName')), not(equals(coalesce(item()?['target'], ''), 'active')), not(equals(coalesce(item()?['target'], ''), 'externalId')), not(equals(coalesce(item()?['target'], ''), 'roles')), if(startsWith(coalesce(item()?['target'], ''), 'urn:'), not(contains(string(body('Query_Required_Enterprise_Attrs')), concat('\"name\":\"', last(split(item()?['target'], ':')), '\"'))), not(contains(string(body('Query_Required_Core_Attrs')), concat('\"name\":\"', if(contains(coalesce(item()?['target'], ''), '['), first(split(item()?['target'], '[')), if(contains(coalesce(item()?['target'], ''), '.'), first(split(item()?['target'], '.')), coalesce(item()?['target'], ''))), '\"')))))" } }, "ForEach_Build_Null_Patch_Operations": { @@ -1476,7 +1496,7 @@ ] }, "type": "Compose", - "inputs": "@if(contains(items('ForEach_Verify_Null_Attributes')?['target'], '[type eq'), if(contains(items('ForEach_Verify_Null_Attributes')?['target'], '].'), coalesce(first(body('NullVerify_Filter_Array_By_Type'))?[last(split(items('ForEach_Verify_Null_Attributes')?['target'], '.'))], ''), coalesce(first(body('NullVerify_Filter_Array_By_Type'))?['value'], '')), if(and(contains(items('ForEach_Verify_Null_Attributes')?['target'], '.'), not(contains(items('ForEach_Verify_Null_Attributes')?['target'], ':'))), coalesce(body('SCIM_Get_User_After_Null_Update')?[first(split(items('ForEach_Verify_Null_Attributes')?['target'], '.'))]?[last(split(items('ForEach_Verify_Null_Attributes')?['target'], '.'))], ''), if(contains(items('ForEach_Verify_Null_Attributes')?['target'], 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:'), coalesce(body('SCIM_Get_User_After_Null_Update')?['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']?[last(split(items('ForEach_Verify_Null_Attributes')?['target'], ':'))], ''), coalesce(body('SCIM_Get_User_After_Null_Update')?[items('ForEach_Verify_Null_Attributes')?['target']], ''))))" + "inputs": "@if(contains(items('ForEach_Verify_Null_Attributes')?['target'], '[type eq'), if(contains(items('ForEach_Verify_Null_Attributes')?['target'], '].'), coalesce(first(body('NullVerify_Filter_Array_By_Type'))?[last(split(items('ForEach_Verify_Null_Attributes')?['target'], '.'))], ''), coalesce(first(body('NullVerify_Filter_Array_By_Type'))?['value'], '')), if(and(contains(items('ForEach_Verify_Null_Attributes')?['target'], '.'), not(contains(items('ForEach_Verify_Null_Attributes')?['target'], ':'))), coalesce(body('SCIM_Get_User_After_Null_Update')?[first(split(items('ForEach_Verify_Null_Attributes')?['target'], '.'))]?[last(split(items('ForEach_Verify_Null_Attributes')?['target'], '.'))], ''), if(startsWith(items('ForEach_Verify_Null_Attributes')?['target'], 'urn:'), coalesce(body('SCIM_Get_User_After_Null_Update')?[substring(items('ForEach_Verify_Null_Attributes')?['target'], 0, lastIndexOf(items('ForEach_Verify_Null_Attributes')?['target'], ':'))]?[last(split(items('ForEach_Verify_Null_Attributes')?['target'], ':'))], ''), coalesce(body('SCIM_Get_User_After_Null_Update')?[items('ForEach_Verify_Null_Attributes')?['target']], ''))))" }, "Compose_NullVerify_IsNullOrEmpty": { "runAfter": { @@ -2130,7 +2150,7 @@ ] }, "type": "Compose", - "inputs": "@setProperty(coalesce(triggerBody()?['initializationData']?['scimUserBody'], json('{}')), 'userName', outputs('Generate_SCIM_Direct_UserName'))" + "inputs": "@setProperty(if(contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryUserAttributes'], json('[]'))), 'externalId'), setProperty(coalesce(triggerBody()?['initializationData']?['scimUserBody'], json('{}')), 'externalId', guid()), coalesce(triggerBody()?['initializationData']?['scimUserBody'], json('{}'))), 'userName', outputs('Generate_SCIM_Direct_UserName'))" } }, "runAfter": {}, @@ -2290,7 +2310,7 @@ ] }, "type": "Compose", - "inputs": "@setProperty(setProperty(coalesce(triggerBody()?['initializationData']?['scimGroupBody'], json('{}')), 'displayName', outputs('Generate_SCIM_Direct_GroupName')), 'externalId', substring(guid(),0,8))" + "inputs": "@setProperty(if(contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryGroupAttributes'], json('[]'))), 'externalId'), setProperty(coalesce(triggerBody()?['initializationData']?['scimGroupBody'], json('{}')), 'externalId', guid()), coalesce(triggerBody()?['initializationData']?['scimGroupBody'], json('{}'))), 'displayName', outputs('Generate_SCIM_Direct_GroupName'))" } }, "runAfter": {}, @@ -2373,7 +2393,7 @@ "type": "Query", "inputs": { "from": "@coalesce(triggerBody()?['initializationData']?['targetDirectoryUserAttributes'], json('[]'))", - "where": "@and(and(and(and(equals(coalesce(item()?['multivalued'], item()?['multiValued'], false), false), or(equals(toLower(coalesce(item()?['mutability'], 'readwrite')), 'readwrite'), equals(toLower(coalesce(item()?['mutability'], 'readwrite')), 'writeonly'))), not(contains(toLower(coalesce(item()?['type'], 'string')), 'reference'))), not(equals(toLower(coalesce(item()?['type'], 'string')), 'boolean'))), and(and(and(not(equals(coalesce(item()?['name'], ''), 'id')), not(equals(coalesce(item()?['name'], ''), 'schemas'))), not(equals(coalesce(item()?['name'], ''), 'meta'))), and(not(equals(coalesce(item()?['name'], ''), 'active')), not(contains(createArray('groups','roles'), coalesce(item()?['name'], ''))))))" + "where": "@and(and(and(equals(coalesce(item()?['multivalued'], item()?['multiValued'], false), false), or(equals(toLower(coalesce(item()?['mutability'], 'readwrite')), 'readwrite'), equals(toLower(coalesce(item()?['mutability'], 'readwrite')), 'writeonly'))), not(contains(toLower(coalesce(item()?['type'], 'string')), 'reference'))), and(and(and(not(equals(coalesce(item()?['name'], ''), 'id')), not(equals(coalesce(item()?['name'], ''), 'schemas'))), not(equals(coalesce(item()?['name'], ''), 'meta'))), and(not(equals(coalesce(item()?['name'], ''), 'active')), not(contains(createArray('groups','roles'), coalesce(item()?['name'], ''))))))" } }, "Filter_Update_Typed_Attributes": { @@ -2385,7 +2405,7 @@ "type": "Query", "inputs": { "from": "@body('Filter_Update_Eligible_Attributes')", - "where": "@contains(json('[\"string\",\"integer\",\"datetime\"]'), toLower(coalesce(item()?['type'], 'string')))" + "where": "@contains(json('[\"string\",\"integer\",\"datetime\",\"boolean\"]'), toLower(coalesce(item()?['type'], 'string')))" } }, "Select_Update_Patch_Ops": { @@ -2400,7 +2420,7 @@ "select": { "op": "replace", "path": "@coalesce(item()?['name'], '')", - "value": "@if(and(greater(length(coalesce(triggerBody()?['scimTargetUserValues'], json('[]'))), 1), contains(triggerBody()?['scimTargetUserValues'][1], coalesce(item()?['name'], ''))), triggerBody()?['scimTargetUserValues'][1][coalesce(item()?['name'], '')], if(equals(toLower(coalesce(item()?['type'], 'string')), 'integer'), rand(1, 99999), if(equals(toLower(coalesce(item()?['type'], 'string')), 'datetime'), utcNow(), concat('upd-', substring(guid(),0,8)))))" + "value": "@if(and(greater(length(coalesce(triggerBody()?['scimTargetUserValues'], json('[]'))), 1), contains(triggerBody()?['scimTargetUserValues'][1], coalesce(item()?['name'], ''))), triggerBody()?['scimTargetUserValues'][1][coalesce(item()?['name'], '')], if(equals(toLower(coalesce(item()?['type'], 'string')), 'integer'), rand(1, 99999), if(equals(toLower(coalesce(item()?['type'], 'string')), 'boolean'), true, if(equals(toLower(coalesce(item()?['type'], 'string')), 'datetime'), utcNow(), concat('upd-', substring(guid(),0,8))))))" } } }, @@ -2413,7 +2433,7 @@ "type": "Select", "inputs": { "from": "@body('Select_Update_Patch_Ops')", - "select": "@concat('\"', replace(item()?['path'], '\"', '\\\"'), '\":', if(or(equals(item()?['value'], null), equals(string(item()?['value']), '')), 'null', if(isInt(item()?['value']), string(item()?['value']), concat('\"', replace(string(item()?['value']), '\"', '\\\"'), '\"'))))" + "select": "@concat('\"', replace(item()?['path'], '\"', '\\\"'), '\":', if(or(equals(item()?['value'], null), equals(string(item()?['value']), '')), 'null', if(or(isInt(item()?['value']), equals(item()?['value'], true), equals(item()?['value'], false)), string(item()?['value']), concat('\"', replace(string(item()?['value']), '\"', '\\\"'), '\"'))))" } }, "Filter_Update_MultiValued_Ops": { @@ -2449,7 +2469,7 @@ "type": "Select", "inputs": { "from": "@body('Filter_Update_Batch_Ops')", - "select": "@concat('\"', replace(item()?['path'], '\"', '\\\"'), '\":', if(or(equals(item()?['value'], null), equals(string(item()?['value']), '')), 'null', if(isInt(item()?['value']), string(item()?['value']), concat('\"', replace(string(item()?['value']), '\"', '\\\"'), '\"'))))" + "select": "@concat('\"', replace(item()?['path'], '\"', '\\\"'), '\":', if(or(equals(item()?['value'], null), equals(string(item()?['value']), '')), 'null', if(or(isInt(item()?['value']), equals(item()?['value'], true), equals(item()?['value'], false)), string(item()?['value']), concat('\"', replace(string(item()?['value']), '\"', '\\\"'), '\"'))))" } }, "Compose_Update_Patch_Body": { @@ -2659,7 +2679,7 @@ ] }, "type": "Compose", - "inputs": "@setProperty(coalesce(triggerBody()?['initializationData']?['scimUserBody'], json('{}')), 'userName', outputs('Generate_SCIM_Update_UserName'))" + "inputs": "@setProperty(if(contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryUserAttributes'], json('[]'))), 'externalId'), setProperty(coalesce(triggerBody()?['initializationData']?['scimUserBody'], json('{}')), 'externalId', guid()), coalesce(triggerBody()?['initializationData']?['scimUserBody'], json('{}'))), 'userName', outputs('Generate_SCIM_Update_UserName'))" } }, "runAfter": {}, @@ -2824,7 +2844,7 @@ ] }, "type": "Compose", - "inputs": "@setProperty(setProperty(coalesce(triggerBody()?['initializationData']?['scimGroupBody'], json('{}')), 'displayName', outputs('Generate_SCIM_GroupUpdate_GroupName')), 'externalId', substring(guid(),0,8))" + "inputs": "@setProperty(if(contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryGroupAttributes'], json('[]'))), 'externalId'), setProperty(coalesce(triggerBody()?['initializationData']?['scimGroupBody'], json('{}')), 'externalId', guid()), coalesce(triggerBody()?['initializationData']?['scimGroupBody'], json('{}'))), 'displayName', outputs('Generate_SCIM_GroupUpdate_GroupName'))" }, "Filter_GroupUpdate_Eligible": { "runAfter": { @@ -2835,7 +2855,7 @@ "type": "Query", "inputs": { "from": "@coalesce(triggerBody()?['initializationData']?['targetDirectoryGroupAttributes'], json('[]'))", - "where": "@and(and(and(equals(coalesce(item()?['multivalued'], item()?['multiValued'], false), false), or(equals(toLower(coalesce(item()?['mutability'], 'readwrite')), 'readwrite'), equals(toLower(coalesce(item()?['mutability'], 'readwrite')), 'writeonly'))), not(contains(toLower(coalesce(item()?['type'], 'string')), 'reference'))), and(not(equals(coalesce(item()?['name'], ''), 'id')), not(equals(toLower(coalesce(item()?['type'], 'string')), 'boolean'))))" + "where": "@and(and(and(equals(coalesce(item()?['multivalued'], item()?['multiValued'], false), false), or(equals(toLower(coalesce(item()?['mutability'], 'readwrite')), 'readwrite'), equals(toLower(coalesce(item()?['mutability'], 'readwrite')), 'writeonly'))), not(contains(toLower(coalesce(item()?['type'], 'string')), 'reference'))), and(not(equals(coalesce(item()?['name'], ''), 'id')), not(contains(createArray('members','schemas','meta'), coalesce(item()?['name'], '')))))" } }, "Select_GroupUpdate_Patch_Ops": { @@ -2850,7 +2870,7 @@ "select": { "op": "replace", "path": "@coalesce(item()?['name'], '')", - "value": "@concat('upd-', substring(guid(),0,8))" + "value": "@if(equals(toLower(coalesce(item()?['type'], 'string')), 'integer'), rand(1, 99999), if(equals(toLower(coalesce(item()?['type'], 'string')), 'boolean'), true, if(equals(toLower(coalesce(item()?['type'], 'string')), 'datetime'), utcNow(), concat('upd-', substring(guid(),0,8)))))" } } }, @@ -3186,7 +3206,7 @@ }, "type": "If" }, - "Federated_Identity_Test": { + "Federated_Identity_Test": { "runAfter": {}, "actions": { "Acquire_Entra_Token": { @@ -3202,7 +3222,7 @@ }, "Check_Entra_Token_Acquired": { "runAfter": { - "Acquire_Entra_Token": ["Succeeded", "Failed", "TimedOut"] + "Acquire_Entra_Token": [ "Succeeded", "Failed", "TimedOut" ] }, "type": "If", "expression": { @@ -3687,10 +3707,10 @@ "expression": { "or": [ { - "equals": ["@triggerBody()?['EnabledTests']", "All"] + "equals": [ "@triggerBody()?['EnabledTests']", "All" ] }, { - "equals": ["@triggerBody()?['EnabledTests']", "SCIMTests"] + "equals": [ "@triggerBody()?['EnabledTests']", "SCIMTests" ] }, { "equals": [ @@ -3761,7 +3781,7 @@ "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, - "body": "@setProperty(setProperty(triggerBody()?['initializationData']?['scimUserBody'], 'userName', concat('paguser', substring(guid(),0,8), '@', coalesce(parameters('testUserDomain'), 'test.com'))), 'displayName', concat('PagUser ', substring(guid(),0,8)))" + "body": "@setProperty(setProperty(if(contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryUserAttributes'], json('[]'))), 'externalId'), setProperty(triggerBody()?['initializationData']?['scimUserBody'], 'externalId', guid()), triggerBody()?['initializationData']?['scimUserBody']), 'userName', concat('paguser', substring(guid(),0,8), '@', coalesce(parameters('testUserDomain'), 'test.com'))), 'displayName', concat('PagUser ', substring(guid(),0,8)))" } }, "Append_User_Pagination_UserId": { @@ -4057,7 +4077,7 @@ "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, - "body": "@setProperty(setProperty(coalesce(triggerBody()?['initializationData']?['scimGroupBody'], json('{}')), 'displayName', concat('PagGroup ', substring(guid(),0,8))), 'externalId', substring(guid(),0,8))" + "body": "@setProperty(if(contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryGroupAttributes'], json('[]'))), 'externalId'), setProperty(coalesce(triggerBody()?['initializationData']?['scimGroupBody'], json('{}')), 'externalId', guid()), coalesce(triggerBody()?['initializationData']?['scimGroupBody'], json('{}'))), 'displayName', concat('PagGroup ', substring(guid(),0,8)))" } }, "Append_Group_Pagination_GroupId": { @@ -4295,6 +4315,363 @@ ] }, "type": "If" + }, + "SCIM_Update_Manager_Test": { + "runAfter": {}, + "actions": { + "SCIM_Manager_Test_Actions": { + "actions": { + "Generate_Manager_TestUser_Name": { + "type": "Compose", + "inputs": "@concat('scmgr-user-', substring(guid(),0,8), '@', coalesce(last(split(coalesce(triggerBody()?['initializationData']?['userBody']?['userPrincipalName'], 'x@test.com'), '@')), 'test.com'))" + }, + "Compose_Manager_TestUser_Body": { + "runAfter": { + "Generate_Manager_TestUser_Name": ["Succeeded"] + }, + "type": "Compose", + "inputs": "@setProperty(if(contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryUserAttributes'], json('[]'))), 'externalId'), setProperty(coalesce(triggerBody()?['initializationData']?['scimUserBody'], json('{}')), 'externalId', guid()), coalesce(triggerBody()?['initializationData']?['scimUserBody'], json('{}'))), 'userName', outputs('Generate_Manager_TestUser_Name'))" + }, + "SCIM_Manager_Create_TestUser": { + "runAfter": { + "Compose_Manager_TestUser_Body": ["Succeeded"] + }, + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users')}", + "method": "POST", + "headers": { + "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Content-Type": "@{parameters('scimContentType')}", + "Accept": "@{parameters('scimContentType')}" + }, + "body": "@outputs('Compose_Manager_TestUser_Body')" + } + }, + "Generate_Manager_ManagerUser_Name": { + "runAfter": { + "SCIM_Manager_Create_TestUser": ["Succeeded"] + }, + "type": "Compose", + "inputs": "@concat('scmgr-mgr-', substring(guid(),0,8), '@', coalesce(last(split(coalesce(triggerBody()?['initializationData']?['userBody']?['userPrincipalName'], 'x@test.com'), '@')), 'test.com'))" + }, + "Compose_Manager_ManagerUser_Body": { + "runAfter": { + "Generate_Manager_ManagerUser_Name": ["Succeeded"] + }, + "type": "Compose", + "inputs": "@setProperty(if(contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryUserAttributes'], json('[]'))), 'externalId'), setProperty(coalesce(triggerBody()?['initializationData']?['scimUserBody'], json('{}')), 'externalId', guid()), coalesce(triggerBody()?['initializationData']?['scimUserBody'], json('{}'))), 'userName', outputs('Generate_Manager_ManagerUser_Name'))" + }, + "SCIM_Manager_Create_ManagerUser": { + "runAfter": { + "Compose_Manager_ManagerUser_Body": ["Succeeded"] + }, + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users')}", + "method": "POST", + "headers": { + "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Content-Type": "@{parameters('scimContentType')}", + "Accept": "@{parameters('scimContentType')}" + }, + "body": "@outputs('Compose_Manager_ManagerUser_Body')" + } + }, + "Compose_Manager_Set_Patch_Body": { + "runAfter": { + "SCIM_Manager_Create_ManagerUser": [ "Succeeded" ] + }, + "type": "Compose", + "inputs": { + "schemas": [ + "urn:ietf:params:scim:api:messages:2.0:PatchOp" + ], + "Operations": [ + { + "op": "Add", + "path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager", + "value": "@body('SCIM_Manager_Create_ManagerUser')?['id']" + } + ] + } + }, + "SCIM_Manager_Patch_Set_Manager": { + "runAfter": { + "Compose_Manager_Set_Patch_Body": [ "Succeeded" ] + }, + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', body('SCIM_Manager_Create_TestUser')?['id'])}", + "method": "PATCH", + "headers": { + "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Content-Type": "@{parameters('scimContentType')}", + "Accept": "@{parameters('scimContentType')}" + }, + "body": "@outputs('Compose_Manager_Set_Patch_Body')" + } + }, + "SCIM_Manager_Get_After_Set": { + "runAfter": { + "SCIM_Manager_Patch_Set_Manager": [ "Succeeded" ] + }, + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', body('SCIM_Manager_Create_TestUser')?['id'])}", + "method": "GET", + "headers": { + "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Accept": "@{parameters('scimContentType')}" + } + } + }, + "Generate_Manager_SecondManager_Name": { + "runAfter": { + "SCIM_Manager_Get_After_Set": ["Succeeded"] + }, + "type": "Compose", + "inputs": "@concat('scmgr-mgr2-', substring(guid(),0,8), '@', coalesce(last(split(coalesce(triggerBody()?['initializationData']?['userBody']?['userPrincipalName'], 'x@test.com'), '@')), 'test.com'))" + }, + "Compose_Manager_SecondManager_Body": { + "runAfter": { + "Generate_Manager_SecondManager_Name": ["Succeeded"] + }, + "type": "Compose", + "inputs": "@setProperty(if(contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryUserAttributes'], json('[]'))), 'externalId'), setProperty(coalesce(triggerBody()?['initializationData']?['scimUserBody'], json('{}')), 'externalId', guid()), coalesce(triggerBody()?['initializationData']?['scimUserBody'], json('{}'))), 'userName', outputs('Generate_Manager_SecondManager_Name'))" + }, + "SCIM_Manager_Create_SecondManager": { + "runAfter": { + "Compose_Manager_SecondManager_Body": ["Succeeded"] + }, + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users')}", + "method": "POST", + "headers": { + "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Content-Type": "@{parameters('scimContentType')}", + "Accept": "@{parameters('scimContentType')}" + }, + "body": "@outputs('Compose_Manager_SecondManager_Body')" + } + }, + "Compose_Manager_Change_Patch_Body": { + "runAfter": { + "SCIM_Manager_Create_SecondManager": ["Succeeded"] + }, + "type": "Compose", + "inputs": { + "schemas": [ + "urn:ietf:params:scim:api:messages:2.0:PatchOp" + ], + "Operations": [ + { + "op": "replace", + "path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager", + "value": "@body('SCIM_Manager_Create_SecondManager')?['id']" + } + ] + } + }, + "SCIM_Manager_Patch_Change_Manager": { + "runAfter": { + "Compose_Manager_Change_Patch_Body": ["Succeeded"] + }, + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', body('SCIM_Manager_Create_TestUser')?['id'])}", + "method": "PATCH", + "headers": { + "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Content-Type": "@{parameters('scimContentType')}", + "Accept": "@{parameters('scimContentType')}" + }, + "body": "@outputs('Compose_Manager_Change_Patch_Body')" + } + }, + "SCIM_Manager_Get_After_Change": { + "runAfter": { + "SCIM_Manager_Patch_Change_Manager": ["Succeeded"] + }, + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', body('SCIM_Manager_Create_TestUser')?['id'])}", + "method": "GET", + "headers": { + "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Accept": "@{parameters('scimContentType')}" + } + } + }, + "Compose_Manager_Remove_Patch_Body": { + "runAfter": { + "SCIM_Manager_Get_After_Change": [ "Succeeded" ] + }, + "type": "Compose", + "inputs": { + "schemas": [ + "urn:ietf:params:scim:api:messages:2.0:PatchOp" + ], + "Operations": [ + { + "op": "replace", + "path": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:manager", + "value": "" + } + ] + } + }, + "SCIM_Manager_Patch_Remove_Manager": { + "runAfter": { + "Compose_Manager_Remove_Patch_Body": [ "Succeeded" ] + }, + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', body('SCIM_Manager_Create_TestUser')?['id'])}", + "method": "PATCH", + "headers": { + "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Content-Type": "@{parameters('scimContentType')}", + "Accept": "@{parameters('scimContentType')}" + }, + "body": "@outputs('Compose_Manager_Remove_Patch_Body')" + } + }, + "SCIM_Manager_Get_After_Remove": { + "runAfter": { + "SCIM_Manager_Patch_Remove_Manager": [ "Succeeded" ] + }, + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', body('SCIM_Manager_Create_TestUser')?['id'])}", + "method": "GET", + "headers": { + "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Accept": "@{parameters('scimContentType')}" + } + } + }, + "Compose_SCIM_Manager_Test_Summary": { + "runAfter": { + "SCIM_Manager_Get_After_Remove": [ "Succeeded", "Failed" ] + }, + "type": "Compose", + "inputs": { + "createTestUserStatus": "@coalesce(outputs('SCIM_Manager_Create_TestUser')?['statusCode'], 0)", + "createManagerUserStatus": "@coalesce(outputs('SCIM_Manager_Create_ManagerUser')?['statusCode'], 0)", + "createSecondManagerStatus": "@coalesce(outputs('SCIM_Manager_Create_SecondManager')?['statusCode'], 0)", + "setManagerPatchStatus": "@coalesce(outputs('SCIM_Manager_Patch_Set_Manager')?['statusCode'], 0)", + "setManagerVerifyResult": "@if(or(equals(coalesce(outputs('SCIM_Manager_Patch_Set_Manager')?['statusCode'], 0), 200), equals(coalesce(outputs('SCIM_Manager_Patch_Set_Manager')?['statusCode'], 0), 204)), if(equals(string(coalesce(body('SCIM_Manager_Get_After_Set')?['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']?['manager']?['value'], body('SCIM_Manager_Get_After_Set')?['manager']?['value'], '')), string(body('SCIM_Manager_Create_ManagerUser')?['id'])), 'PASSED', 'FAILED_VERIFY'), 'FAILED_PATCH')", + "changeManagerPatchStatus": "@coalesce(outputs('SCIM_Manager_Patch_Change_Manager')?['statusCode'], 0)", + "changeManagerVerifyResult": "@if(or(equals(coalesce(outputs('SCIM_Manager_Patch_Change_Manager')?['statusCode'], 0), 200), equals(coalesce(outputs('SCIM_Manager_Patch_Change_Manager')?['statusCode'], 0), 204)), if(equals(string(coalesce(body('SCIM_Manager_Get_After_Change')?['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']?['manager']?['value'], body('SCIM_Manager_Get_After_Change')?['manager']?['value'], '')), string(body('SCIM_Manager_Create_SecondManager')?['id'])), 'PASSED', 'FAILED_VERIFY'), 'FAILED_PATCH')", + "removeManagerPatchStatus": "@coalesce(outputs('SCIM_Manager_Patch_Remove_Manager')?['statusCode'], 0)", + "removeManagerVerifyResult": "@if(or(equals(coalesce(outputs('SCIM_Manager_Patch_Remove_Manager')?['statusCode'], 0), 200), equals(coalesce(outputs('SCIM_Manager_Patch_Remove_Manager')?['statusCode'], 0), 204)), if(or(or(equals(coalesce(body('SCIM_Manager_Get_After_Remove')?['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']?['manager']?['value'], body('SCIM_Manager_Get_After_Remove')?['manager']?['value'], 'NOT_NULL'), ''), equals(coalesce(body('SCIM_Manager_Get_After_Remove')?['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']?['manager']?['value'], body('SCIM_Manager_Get_After_Remove')?['manager']?['value'], 'NOT_NULL'), null)), equals(coalesce(body('SCIM_Manager_Get_After_Remove')?['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']?['manager'], body('SCIM_Manager_Get_After_Remove')?['manager'], json('null')), null)), 'PASSED', 'FAILED_VERIFY'), 'FAILED_PATCH')", + "overallResult": "@if(and(and(or(equals(coalesce(outputs('SCIM_Manager_Patch_Set_Manager')?['statusCode'], 0), 200), equals(coalesce(outputs('SCIM_Manager_Patch_Set_Manager')?['statusCode'], 0), 204)), equals(string(coalesce(body('SCIM_Manager_Get_After_Set')?['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']?['manager']?['value'], body('SCIM_Manager_Get_After_Set')?['manager']?['value'], '')), string(body('SCIM_Manager_Create_ManagerUser')?['id']))), and(and(or(equals(coalesce(outputs('SCIM_Manager_Patch_Change_Manager')?['statusCode'], 0), 200), equals(coalesce(outputs('SCIM_Manager_Patch_Change_Manager')?['statusCode'], 0), 204)), equals(string(coalesce(body('SCIM_Manager_Get_After_Change')?['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']?['manager']?['value'], body('SCIM_Manager_Get_After_Change')?['manager']?['value'], '')), string(body('SCIM_Manager_Create_SecondManager')?['id']))), and(or(equals(coalesce(outputs('SCIM_Manager_Patch_Remove_Manager')?['statusCode'], 0), 200), equals(coalesce(outputs('SCIM_Manager_Patch_Remove_Manager')?['statusCode'], 0), 204)), or(or(equals(coalesce(body('SCIM_Manager_Get_After_Remove')?['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']?['manager']?['value'], body('SCIM_Manager_Get_After_Remove')?['manager']?['value'], 'NOT_NULL'), ''), equals(coalesce(body('SCIM_Manager_Get_After_Remove')?['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']?['manager']?['value'], body('SCIM_Manager_Get_After_Remove')?['manager']?['value'], 'NOT_NULL'), null)), equals(coalesce(body('SCIM_Manager_Get_After_Remove')?['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']?['manager'], body('SCIM_Manager_Get_After_Remove')?['manager'], json('null')), null))))), 'PASSED', 'FAILED')" + } + } + }, + "type": "Scope" + }, + "Set_SCIMUpdateManagerTestOutputs": { + "runAfter": { + "SCIM_Manager_Test_Actions": [ "Succeeded", "Failed", "TimedOut" ] + }, + "type": "SetVariable", + "inputs": { + "name": "SCIMUpdateManagerTestOutputs", + "value": { + "overallResult": "@if(equals(coalesce(outputs('Compose_SCIM_Manager_Test_Summary')?['overallResult'], 'FAILED'), 'PASSED'), 'PASSED', 'FAILED')", + "result": "@if(equals(coalesce(outputs('Compose_SCIM_Manager_Test_Summary')?['overallResult'], 'FAILED'), 'PASSED'), 'success', concat('FAILED - Action: ', if(not(or(equals(coalesce(outputs('SCIM_Manager_Patch_Set_Manager')?['statusCode'], 0), 200), equals(coalesce(outputs('SCIM_Manager_Patch_Set_Manager')?['statusCode'], 0), 204))), 'SCIM_Manager_Patch_Set_Manager', if(not(equals(coalesce(outputs('Compose_SCIM_Manager_Test_Summary')?['setManagerVerifyResult'], ''), 'PASSED')), 'SCIM_Manager_Get_After_Set', if(not(or(equals(coalesce(outputs('SCIM_Manager_Patch_Remove_Manager')?['statusCode'], 0), 200), equals(coalesce(outputs('SCIM_Manager_Patch_Remove_Manager')?['statusCode'], 0), 204))), 'SCIM_Manager_Patch_Remove_Manager', 'SCIM_Manager_Get_After_Remove'))), ' - ', if(not(or(equals(coalesce(outputs('SCIM_Manager_Patch_Set_Manager')?['statusCode'], 0), 200), equals(coalesce(outputs('SCIM_Manager_Patch_Set_Manager')?['statusCode'], 0), 204))), concat('Set manager PATCH returned HTTP ', string(coalesce(outputs('SCIM_Manager_Patch_Set_Manager')?['statusCode'], 0))), if(not(equals(coalesce(outputs('Compose_SCIM_Manager_Test_Summary')?['setManagerVerifyResult'], ''), 'PASSED')), 'Manager value not set correctly after PATCH', if(not(or(equals(coalesce(outputs('SCIM_Manager_Patch_Remove_Manager')?['statusCode'], 0), 200), equals(coalesce(outputs('SCIM_Manager_Patch_Remove_Manager')?['statusCode'], 0), 204))), concat('Remove manager PATCH returned HTTP ', string(coalesce(outputs('SCIM_Manager_Patch_Remove_Manager')?['statusCode'], 0))), 'Manager value not removed after null PATCH')))))", + "summary": "@outputs('Compose_SCIM_Manager_Test_Summary')", + "errorDetails": "@if(equals(coalesce(outputs('Compose_SCIM_Manager_Test_Summary')?['overallResult'], 'FAILED'), 'PASSED'), null, string(outputs('Compose_SCIM_Manager_Test_Summary')))" + } + } + }, + "SCIM_Manager_Test_Cleanup": { + "runAfter": { + "Set_SCIMUpdateManagerTestOutputs": [ "Succeeded", "Failed", "Skipped" ] + }, + "type": "Scope", + "actions": { + "SCIM_Manager_Delete_TestUser": { + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', coalesce(body('SCIM_Manager_Create_TestUser')?['id'], 'UNKNOWN'))}", + "method": "DELETE", + "headers": { + "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Accept": "@{parameters('scimContentType')}" + } + } + }, + "SCIM_Manager_Delete_ManagerUser": { + "runAfter": { + "SCIM_Manager_Delete_TestUser": [ "Succeeded", "Failed", "Skipped" ] + }, + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', coalesce(body('SCIM_Manager_Create_ManagerUser')?['id'], 'UNKNOWN'))}", + "method": "DELETE", + "headers": { + "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Accept": "@{parameters('scimContentType')}" + } + } + }, + "SCIM_Manager_Delete_SecondManager": { + "runAfter": { + "SCIM_Manager_Delete_ManagerUser": [ "Succeeded", "Failed", "Skipped" ] + }, + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', coalesce(body('SCIM_Manager_Create_SecondManager')?['id'], 'UNKNOWN'))}", + "method": "DELETE", + "headers": { + "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Accept": "@{parameters('scimContentType')}" + } + } + } + } + } + }, + "else": { + "actions": {} + }, + "expression": { + "and": [ + { + "equals": [ + "@contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryUserAttributes'], json('[]'))), 'manager')", + true + ] + }, + { + "or": [ + { + "equals": [ + "@triggerBody()?['EnabledTests']", + "All" + ] + }, + { + "equals": [ + "@triggerBody()?['EnabledTests']", + "SCIMTests" + ] + }, + { + "equals": [ + "@triggerBody()?['EnabledTests']", + "SCIM_Update_Manager_Test" + ] + } + ] + } + ] + }, + "type": "If" } }, "runAfter": { @@ -4306,7 +4683,7 @@ }, "Compose_Final_Results": { "runAfter": { - "SCIMTests_Scope": ["Succeeded", "Failed", "Skipped", "TimedOut"] + "SCIMTests_Scope": [ "Succeeded", "Failed", "Skipped", "TimedOut" ] }, "type": "Compose", "inputs": { @@ -4318,11 +4695,12 @@ "SCIMGroupCreateTestOutputs": "@variables('SCIMGroupCreateTestOutputs')", "SCIMUserUpdateTestOutputs": "@variables('SCIMUserUpdateTestOutputs')", "SCIMGroupUpdateTestOutputs": "@variables('SCIMGroupUpdateTestOutputs')", - "testSuiteName": "SCIMTests", - "status": "@if(or(or(or(equals(variables('SchemaDiscoverabilityTestOutputs')['overallResult'], 'FAILED'), equals(variables('SCIMNullUpdateTestOutputs')['overallResult'], 'FAILED')), or(or(equals(variables('ValidateCredentialsTestOutputs')['overallResult'], 'FAILED'), equals(variables('FederatedIdentityTestOutputs')['overallResult'], 'FAILED')), equals(variables('SCIMUserCreateTestOutputs')['overallResult'], 'FAILED'))), or(or(equals(variables('SCIMGroupCreateTestOutputs')['overallResult'], 'FAILED'), equals(variables('SCIMUserUpdateTestOutputs')['overallResult'], 'FAILED')), equals(variables('SCIMGroupUpdateTestOutputs')['overallResult'], 'FAILED'))), 'Failed', 'Passed')", - "workflowRunId": "@workflow().run.name", "SCIMUserPaginationTestOutputs": "@variables('SCIMUserPaginationTestOutputs')", - "SCIMGroupPaginationTestOutputs": "@variables('SCIMGroupPaginationTestOutputs')" + "SCIMGroupPaginationTestOutputs": "@variables('SCIMGroupPaginationTestOutputs')", + "SCIMUpdateManagerTestOutputs": "@variables('SCIMUpdateManagerTestOutputs')", + "testSuiteName": "SCIMTests", + "status": "@if(or(or(or(equals(variables('SchemaDiscoverabilityTestOutputs')['overallResult'], 'FAILED'), equals(variables('SCIMNullUpdateTestOutputs')['overallResult'], 'FAILED')), or(or(equals(variables('ValidateCredentialsTestOutputs')['overallResult'], 'FAILED'), equals(variables('FederatedIdentityTestOutputs')['overallResult'], 'FAILED')), equals(variables('SCIMUserCreateTestOutputs')['overallResult'], 'FAILED'))), or(or(or(equals(variables('SCIMGroupCreateTestOutputs')['overallResult'], 'FAILED'), equals(variables('SCIMUserUpdateTestOutputs')['overallResult'], 'FAILED')), equals(variables('SCIMGroupUpdateTestOutputs')['overallResult'], 'FAILED')), and(contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryUserAttributes'], json('[]'))), 'manager'), equals(variables('SCIMUpdateManagerTestOutputs')['overallResult'], 'FAILED')))), 'Failed', 'Passed')", + "workflowRunId": "@workflow().run.name" } } }, diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md index cd1cddb4..7270737a 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md @@ -933,16 +933,22 @@ will be skipped                     "GroupTests", +                    "SCIMTests", +                     "Create_User_Test",                     "Update_User_Test",                     "Delete_User_Test", -                    "User_Disable_Test", +                    "Disable_User_Test",                     "User_Update_Manager_Test", +                    "Restore_User_Test", + +                    "POD_User_Test", +                     "Create_Group_Test",                     "Update_Group_Test", @@ -953,49 +959,58 @@ will be skipped                     "Group_Update_Remove_Member_Test", -“Schema_Discoverability_Test”, +                    "POD_Group_Test", -“SCIM_Null_Update_Test”, +                    "Restore_Group_Test", -“Validate_Credentials_Test” +"Schema_Discoverability_Test", - +"SCIM_Null_Update_Test", + +"SCIM_User_Create_Test", + +"SCIM_Group_Create_Test", + +"SCIM_User_Update_Test", + +"SCIM_Group_Update_Test", + +"SCIM_Update_Manager_Test", -31. IsSoftDeleted can be ‘true’ or ‘false’. Set to true only if soft - deletion is supported and defined in your SCIM schema. This property - indicates that the user resource is marked for soft deletion—meaning - it is flagged for removal but not permanently deleted. - “Disable_User_Test’ and “Delete_User_Test” are dependent on the - correct value of this parameter. If ‘IsSoftDeleted’ is false, then - “Disable_User_Test” will be skipped. +"SCIM_User_Pagination_Test", - +"SCIM_Group_Pagination_Test", -32. Update scimClientId with client id. +"Validate_Credentials_Test", + +"Federated_Identity_Test" + + + +31. Update scimClientId with client id. -33. Update client secret +32. Update client secret -34. Update ISV token endpoint +33. Update ISV token endpoint ## Run the Logic App -35. You’re now ready to run the Logic app! Navigate to **WorkFlows\>** +34. You’re now ready to run the Logic app! Navigate to **WorkFlows\>** Select **Orchestrtor_workflow**, -36. From the Orchestartor_workflow’s designer, select “**Run**” +35. From the Orchestartor_workflow’s designer, select “**Run**” @@ -1004,7 +1019,7 @@ style="width:6.5in;height:2.47153in" /> ## Verify the Runs -37. You can view logs of your runs in the **Runs history** blade. When +36. You can view logs of your runs in the **Runs history** blade. When clicking on an entry in **Runs history**, you check the final results of that entry, including the list of tests that were run, alongside status and any errors that may have come up. @@ -1014,7 +1029,7 @@ style="width:6.5in;height:1.42639in" /> ## Debugging -38. Debugging Logic App: +37. Debugging Logic App: Check the Final_TestResults action of the Orchestrator_workflow’s run to learn about the tests and their results. @@ -1045,18 +1060,29 @@ correct, no action is needed. The Logic App automatically detects these capabilities during initialization and skips tests that do not apply. \(1\) User_Update_Manager_Test: Skipped when the manager attribute is -not mapped in the target directory schema. Only applies if your app +not present in the target directory schema. Only applies if your app supports manager provisioning. -\(2\) All Group tests (Create_Group_Test, Update_Group_Test, -Delete_Group_Test, Group_Update_Add_Member_Test, -Group_Update_Remove_Member_Test, POD_Group_Test, Restore_Group_Test) and -SCIM Group tests (SCIM_Group_Create_Test, SCIM_Group_Update_Test, -SCIM_Group_Pagination_Test): Skipped when the application does not -support group provisioning. +\(2\) SCIM_Update_Manager_Test: Skipped when the manager attribute is +not present in the target directory schema. This test validates direct +SCIM PATCH calls to set, change, and remove the manager attribute. -\(3\) Disable_User_Test: Skipped when IsSoftDeleted is set to false or -soft-delete is not supported by the target application. +\(3\) Federated_Identity_Test: Skipped when OAuth client credentials +are not configured. Only applies if your app uses Workload Identity +Federation. + +Note on WARNING results: The following tests produce a WARNING (not +FAILED) if they fail — they do not block overall validation: + +- Delete_User_Test (hard delete is optional) +- Delete_Group_Test (group delete is optional) +- Restore_Group_Test (group restore is optional) +- SCIM_Group_Pagination_Test (group pagination is optional) + +Note on Group tests: Group provisioning is mandatory. If your application +does not support groups (no Group schema in /Schemas endpoint), all group +tests will FAIL (not skip). Your application must support both Users and +Groups provisioning. “provisioningErrorDetails” gives the glimpse of Error information in case of failure. @@ -1076,7 +1102,7 @@ style="width:6.5in;height:3.65625in" /> # Understanding the Test Results -### The Logic App runs 23 tests across three workflows: 7 User tests, 7 Group tests, and 9 SCIM compliance tests. "For detailed descriptions of each test and what they validate, see the SCIM Validation Test Overview: [SCIMReferenceCode/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp at master · AzureAD/SCIMReferenceCode · GitHub](https://github.com/AzureAD/SCIMReferenceCode/tree/master/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp)" +### The Logic App runs 25 tests across three workflows: 7 User tests, 7 Group tests, and 11 SCIM compliance tests. "For detailed descriptions of each test and what they validate, see the SCIM Validation Test Overview: [SCIMReferenceCode/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp at master · AzureAD/SCIMReferenceCode · GitHub](https://github.com/AzureAD/SCIMReferenceCode/tree/master/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp)" ### Not all tests will run for every application — the Logic App automatically detects what your app supports and skips tests that do not apply. @@ -1123,7 +1149,7 @@ Here’s what each field means: |----|----|----| | `"success"` | Test passed — your SCIM endpoint handled the operation correctly | ✅ None | | `"FAILED - [Phase] Failed Action: "` | Test failed at a specific action. The phase (e.g., Create Phase, Update Phase, Delete Phase) and action name tell you exactly where it broke. | ❌ Yes — see Debugging below | -| `"skipped"` | Test was skipped because a prerequisite was not met (e.g., `Disable_User_Test` skipped when `IsSoftDeleted` is `false`, or group tests skipped when groups are not supported) | ✅ None (if intentional) | +| `"skipped"` | Test was skipped because a prerequisite was not met (e.g., `User_Update_Manager_Test` skipped when manager attribute is not in target schema, or `Federated_Identity_Test` skipped when OAuth is not configured) | ✅ None (if intentional) | | `"Token acquisition failed"` | Only for `Validate_Credentials_Test` — the OAuth token request failed. Expected when using a static bearer token. | ✅ None (if using static token) | | `"The SCIM schema does not support all attributes..."` | `Schema_Discoverability_Test` found that your SCIM schema doesn’t advertise all the attributes in the provisioning mappings. The details show how many are supported vs mapped. | ⚠️ Prune your attribute mappings to match | @@ -1136,22 +1162,34 @@ Here’s what each field means: { "testName": "Disable_User_Test", "testResult": "success" }, { "testName": "Delete_User_Test", "testResult": "success" }, { "testName": "User_Update_Manager_Test", "testResult": "success" }, + { "testName": "Restore_User_Test", "testResult": "success" }, + { "testName": "POD_User_Test", "testResult": "success" }, { "testName": "Create_Group_Test", "testResult": "success" }, { "testName": "Update_Group_Test", "testResult": "success" }, { "testName": "Delete_Group_Test", "testResult": "success" }, { "testName": "Group_Update_Add_Member_Test", "testResult": "success" }, { "testName": "Group_Update_Remove_Member_Test", "testResult": "success" }, + { "testName": "POD_Group_Test", "testResult": "success" }, + { "testName": "Restore_Group_Test", "testResult": "success" }, { "testName": "Schema_Discoverability_Test", "testResult": "success" }, { "testName": "SCIM_Null_Update_Test", "testResult": "success" }, - { "testName": "Validate_Credentials_Test", "testResult": "Token acquisition failed" } + { "testName": "SCIM_User_Create_Test", "testResult": "success" }, + { "testName": "SCIM_Group_Create_Test", "testResult": "success" }, + { "testName": "SCIM_User_Update_Test", "testResult": "success" }, + { "testName": "SCIM_Group_Update_Test", "testResult": "success" }, + { "testName": "SCIM_Update_Manager_Test", "testResult": "success" }, + { "testName": "SCIM_User_Pagination_Test", "testResult": "success" }, + { "testName": "SCIM_Group_Pagination_Test", "testResult": "success" }, + { "testName": "Validate_Credentials_Test", "testResult": "Token acquisition failed" }, + { "testName": "Federated_Identity_Test", "testResult": "skipped" } ], "overallResult": "Failed" } -> **Note:** Even with 12/13 tests passing, the `overallResult` shows +> **Note:** Even with 23/25 tests passing, the `overallResult` shows > `"Failed"` because `Validate_Credentials_Test` did not pass. This is > expected when using a static bearer token — it does not block -> onboarding. +> onboarding. `Federated_Identity_Test` is skipped when OAuth is not configured. ### Sample Results — With a Failure @@ -1199,24 +1237,18 @@ automatically and tells you the root cause and fix. To proceed with gallery onboarding, all applicable tests must pass. The following are acceptable exceptions: - -- Validate_Credentials_Test failing when using a static bearer token - (OAuth will be required for production). - -- Group tests being SKIPPED if your application does not support group - provisioning. This includes all 7 Group tests (Create_Group_Test, - Update_Group_Test, Delete_Group_Test, Group_Update_Add_Member_Test, - Group_Update_Remove_Member_Test, POD_Group_Test, Restore_Group_Test) - and 3 SCIM Group tests (SCIM_Group_Create_Test, - SCIM_Group_Update_Test, SCIM_Group_Pagination_Test). +- Validate_Credentials_Test or Federated_Identity_Test failing when + using a static bearer token (OAuth or WIF will be required for + production). At least one of these two must PASS. -- User_Update_Manager_Test being SKIPPED when the manager attribute is - not present in the target directory schema. The Logic App checks - whether manager is mapped and automatically skips this test if it is - not. +- User_Update_Manager_Test and SCIM_Update_Manager_Test being SKIPPED + when the manager attribute is not present in the target directory + schema. The Logic App checks whether manager is in the target + directory attributes and automatically skips these tests if it is not. -- Disable_User_Test being SKIPPED when IsSoftDeleted is set to false or - soft-delete is not supported. This test only runs when the application - supports soft-deletion of users. +- Delete_User_Test, Delete_Group_Test, Restore_Group_Test, and + SCIM_Group_Pagination_Test producing WARNING results. These are + optional tests — a failure does not block onboarding. ## Automatic Failure Diagnosis diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/UserTests_Workflow.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/UserTests_Workflow.json index 3ca3fa42..3c17d955 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/UserTests_Workflow.json +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/UserTests_Workflow.json @@ -801,7 +801,7 @@ "sourceAttribute": "@items('ForEach_SourceToTargetLookup')?['source']", "targetAttribute": "@items('ForEach_SourceToTargetLookup')?['target']", "expectedValue": "@outputs('Compose_Expected_Value')", - "actualValue": "@if(contains(items('ForEach_SourceToTargetLookup')?['target'], '[type eq'), if(contains(items('ForEach_SourceToTargetLookup')?['target'], '].'), first(body('Query_Filter_Array_By_Type'))?[last(split(items('ForEach_SourceToTargetLookup')?['target'], '.'))], first(body('Query_Filter_Array_By_Type'))?['value']), if(and(contains(items('ForEach_SourceToTargetLookup')?['target'], '.'), not(contains(items('ForEach_SourceToTargetLookup')?['target'], ':'))), body('Update_User_-_Query_User_By_Id_After_Attributes_Update')?[first(split(items('ForEach_SourceToTargetLookup')?['target'], '.'))]?[last(split(items('ForEach_SourceToTargetLookup')?['target'], '.'))], coalesce(body('Update_User_-_Query_User_By_Id_After_Attributes_Update')?['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User']?[last(split(last(split(items('ForEach_SourceToTargetLookup')?['target'], ':')), '.'))], body('Update_User_-_Query_User_By_Id_After_Attributes_Update')?[last(split(last(split(items('ForEach_SourceToTargetLookup')?['target'], ':')), '.'))])))" + "actualValue": "@if(contains(items('ForEach_SourceToTargetLookup')?['target'], '[type eq'), if(contains(items('ForEach_SourceToTargetLookup')?['target'], '].'), first(body('Query_Filter_Array_By_Type'))?[last(split(items('ForEach_SourceToTargetLookup')?['target'], '.'))], first(body('Query_Filter_Array_By_Type'))?['value']), if(and(contains(items('ForEach_SourceToTargetLookup')?['target'], '.'), not(contains(items('ForEach_SourceToTargetLookup')?['target'], ':'))), body('Update_User_-_Query_User_By_Id_After_Attributes_Update')?[first(split(items('ForEach_SourceToTargetLookup')?['target'], '.'))]?[last(split(items('ForEach_SourceToTargetLookup')?['target'], '.'))], if(contains(items('ForEach_SourceToTargetLookup')?['target'], ':'), coalesce(body('Update_User_-_Query_User_By_Id_After_Attributes_Update')?[substring(items('ForEach_SourceToTargetLookup')?['target'], 0, lastIndexOf(items('ForEach_SourceToTargetLookup')?['target'], ':'))]?[substring(items('ForEach_SourceToTargetLookup')?['target'], add(lastIndexOf(items('ForEach_SourceToTargetLookup')?['target'], ':'), 1), sub(length(items('ForEach_SourceToTargetLookup')?['target']), add(lastIndexOf(items('ForEach_SourceToTargetLookup')?['target'], ':'), 1)))], body('Update_User_-_Query_User_By_Id_After_Attributes_Update')?[substring(items('ForEach_SourceToTargetLookup')?['target'], add(lastIndexOf(items('ForEach_SourceToTargetLookup')?['target'], ':'), 1), sub(length(items('ForEach_SourceToTargetLookup')?['target']), add(lastIndexOf(items('ForEach_SourceToTargetLookup')?['target'], ':'), 1)))]), body('Update_User_-_Query_User_By_Id_After_Attributes_Update')?[items('ForEach_SourceToTargetLookup')?['target']])))" } } } diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION index 4fedf1d2..cc40bca6 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION @@ -1 +1 @@ -7.0 +8.0 diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md index 9ef170e6..99f62c1f 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md @@ -4,7 +4,7 @@ description: > SCIM provisioning validation agent for ISVs onboarding to the Microsoft Entra app gallery. Guides ISVs through the complete validation workflow: environment checks, Azure/Entra resource creation, Logic App deployment, test execution, failure diagnosis with auto-fix, and - submission of validation artifacts to Microsoft. + handoff to official Microsoft submission guidance after tests pass. Invocation examples: @scim-onboarding — start the full validation workflow @@ -24,9 +24,9 @@ description: > You are the **SCIM onboarding validation agent**. You help ISVs validate that their SCIM provisioning integration is ready to publish to the Microsoft Entra app gallery. -You convert a 28-step, 3-portal manual process into a guided conversational experience. You create all required Azure and Entra resources, deploy the validation Logic App, execute tests, diagnose failures, apply fixes, and re-run until the ISV has a clean validation report to submit to Microsoft. +You convert a 28-step, 3-portal manual process into a guided conversational experience. You create all required Azure and Entra resources, deploy the validation Logic App, execute tests, diagnose failures, apply fixes, and re-run until the ISV has a clean validation outcome and is ready to follow the official submission process. -**You are not an advisor — you are an executor.** When you identify a fixable issue, fix it. When you need ISV input (e.g., schema restrictions), ask precisely. When tests pass, generate the submission artifacts. +**You are not an advisor — you are an executor.** When you identify a fixable issue, fix it. When you need ISV input (e.g., schema restrictions), ask precisely. When tests pass, direct the ISV to the official Submit Test Results guidance. --- @@ -66,7 +66,7 @@ Collect the ISV's SCIM endpoint and bearer token, validate their Azure environme > Do NOT submit an empty box — empty submissions are treated as cancellation and the agent will stop. The agent treats `none` (case-insensitive) as an empty scope when writing `scimOAuthScope` to `parameters.json`. - - If **static bearer token**: record `authMethod = bearer`. The 4 OAuth fields (`scimClientId`, `scimClientSecret`, `scimTokenEndpoint`, `scimOAuthScope`) will be written as empty strings in Phase 4. (Logic App test behavior — including `Validate_Credentials_Test` — is out of scope for Phase 1; see Phase 4 for parameter handling and the Phase 7 report for expected results.) + - If **static bearer token**: record `authMethod = bearer`. The 4 OAuth fields (`scimClientId`, `scimClientSecret`, `scimTokenEndpoint`, `scimOAuthScope`) will be written as empty strings in Phase 4. (Logic App test behavior — including `Validate_Credentials_Test` — is out of scope for Phase 1; see Phase 4 for parameter handling and expected test behavior.) d. **Federated identity test inputs (for `Federated_Identity_Test`)**: - Ask: "Do you want to run the federated identity validation test now?" @@ -774,7 +774,7 @@ Provide choices: iii. **Create a new sync job:** `POST /servicePrincipals//synchronization/jobs` with body `{"templateId":"isvonboarding"}`. - iv. **Capture the new `jobId` from the response and replace the old `jobId` in agent state.** All subsequent calls (3e schema fetch, 3g start, Phase 6 debug, Phase 7 report) MUST use this new `jobId`. The old `jobId` no longer exists. + iv. **Capture the new `jobId` from the response and replace the old `jobId` in agent state.** All subsequent calls (3e schema fetch, 3g start, Phase 6 debug) MUST use this new `jobId`. The old `jobId` no longer exists. v. Loop back to Step 3e (fetch and display the reset schema using the new `jobId`). @@ -1058,7 +1058,7 @@ Only after the host is `Running` (and any modified workflows are `Healthy`), pro # Get the trigger URL TOKEN=$(az account get-access-token --query accessToken -o tsv) curl -X POST \ - "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.Web/sites//hostruntime/runtime/webhooks/workflow/api/management/workflows/Orchestrator_Workflow/triggers/Manual_Recurrence/run?api-version=2023-12-01" \ + "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.Web/sites//hostruntime/runtime/webhooks/workflow/api/management/workflows/Orchestrator_Workflow/triggers/Manual_Recurrence/run?api-version=2022-03-01" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{}' @@ -1172,7 +1172,7 @@ while ($true) { ``` ### Branch on the final printed status -- `Succeeded` → proceed to Phase 7 (Validate / generate report) +- `Succeeded` → perform Submit Test Results handoff, then proceed to Phase 7 (Cleanup) - `Failed` / `Cancelled` / `TimedOut` → proceed to Phase 6 (Debug) - Aborted (exceeded `$MaxMin`) → escalate to ISV; do NOT auto-retry @@ -1190,7 +1190,7 @@ The `Final_TestResults` action in the Orchestrator workflow contains per-test pa ```bash # Get the action details az rest --method GET \ - --url "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.Web/sites//hostruntime/runtime/webhooks/workflow/api/management/workflows/Orchestrator_Workflow/runs//actions/Final_TestResults?api-version=2023-12-01" + --url "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.Web/sites//hostruntime/runtime/webhooks/workflow/api/management/workflows/Orchestrator_Workflow/runs//actions/Final_TestResults?api-version=2022-03-01" ``` Then fetch the output content from the `outputsLink.uri` in the response. @@ -1217,13 +1217,15 @@ Follow this process for **every** failed test in `Final_TestResults`: `Final_TestResults` includes `childWorkflowRunLinks` — a map of workflow name → portal URL. Extract the `runId` from the URL path parameter. Match the failed test to its workflow: - `Create_User_Test`, `Update_User_Test`, `Delete_User_Test`, `Disable_User_Test`, `User_Update_Manager_Test`, `Restore_User_Test`, `POD_User_Test` → **UserTests_Workflow** - `Create_Group_Test`, `Update_Group_Test`, `Delete_Group_Test`, `Group_Update_Add_Member_Test`, `Group_Update_Remove_Member_Test`, `POD_Group_Test`, `Restore_Group_Test` → **GroupTests_Workflow** -- `Schema_Discoverability_Test`, `SCIM_Null_Update_Test`, `SCIM_User_Create_Test`, `SCIM_User_Update_Test`, `SCIM_Group_Create_Test`, `SCIM_Group_Update_Test`, `SCIM_User_Pagination_Test`, `SCIM_Group_Pagination_Test`, `Validate_Credentials_Test`, `Federated_Identity_Test` → **SCIMTests_Workflow** +- `Schema_Discoverability_Test`, `SCIM_Null_Update_Test`, `SCIM_User_Create_Test`, `SCIM_User_Update_Test`, `SCIM_Group_Create_Test`, `SCIM_Group_Update_Test`, `SCIM_User_Pagination_Test`, `SCIM_Group_Pagination_Test`, `Validate_Credentials_Test`, `Federated_Identity_Test`, `SCIM_Update_Manager_Test` → **SCIMTests_Workflow** + +> **Note:** `Delete_User_Test` is **optional** — a failure produces WARNING (not FAIL) in the overall result. `SCIM_Update_Manager_Test` is **mandatory when the `manager` attribute is present in the target directory schema**; it is skipped (not failed) when manager is not supported. The test validates set (Add), change (replace), and remove (replace with empty) of the manager attribute via direct SCIM PATCH calls. #### 2. List all executed actions in the child workflow ```bash az rest --method GET \ - --url "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.Web/sites//hostruntime/runtime/webhooks/workflow/api/management/workflows//runs//actions?api-version=2023-12-01" \ + --url "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.Web/sites//hostruntime/runtime/webhooks/workflow/api/management/workflows//runs//actions?api-version=2022-03-01" \ --query "value[?properties.status!='Skipped'].{name:name, status:properties.status}" -o table ``` @@ -1238,7 +1240,7 @@ For any action of interest, get its full details: ```bash az rest --method GET \ - --url "...//runs//actions/?api-version=2023-12-01" + --url "...//runs//actions/?api-version=2022-03-01" ``` The response contains: @@ -1253,7 +1255,7 @@ Polling loops execute the same action multiple times. Each iteration has its own ```bash az rest --method GET \ - --url "...//runs//actions//repetitions?api-version=2023-12-01" + --url "...//runs//actions//repetitions?api-version=2022-03-01" ``` Check the **last repetition** — it contains the most recent attempt and its error. @@ -1367,288 +1369,22 @@ while (test run fails): --- -## Phase 7: Generate Validation Report +## Submit Test Results (Official Guide) ### Goal -Generate a `validation-result-.json` file that validates the Logic App run against the expected template AND captures **the inputs/outputs of every action** (including the final iteration of every Until/Foreach loop) under a nested `allActionsDetailed` tree. - -> **Note:** The upstream `ValidateLogicAppRun.ps1` script targets Consumption Logic Apps (`Microsoft.Logic/workflows`). Our Logic App is Standard (multi-workflow under `Microsoft.Web/sites`), so the agent generates this report inline using the Standard Logic App `hostruntime` APIs. A reference PowerShell implementation is shipped in this repo as `ValidateLogicAppRun-Standard.ps1` — the agent and the script must produce the same JSON shape. - -### Quick path: run the bundled script - -Acquire `ValidateLogicAppRun-Standard.ps1` in this order: - -1. **GitHub raw URL first** — `curl -fsSL https://raw.githubusercontent.com/AzureAD/SCIMReferenceCode/master/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/ValidateLogicAppRun-Standard.ps1 -o ValidateLogicAppRun-Standard.ps1` (HTTP 200 only). -2. **Local workspace fallback** — use `./ValidateLogicAppRun-Standard.ps1` if it already exists next to this agent file. -3. **Last resort** — produce the report inline using the steps below (7a–7k). - -Log to the ISV which source (`github` / `local` / `inline`) was used. Then, if the script is available and the ISV is on PowerShell 7+, run: - -```powershell -./ValidateLogicAppRun-Standard.ps1 ` - -SubscriptionId ` - -ResourceGroup ` - -LogicAppName ` - -RunId -``` - -The script writes `validation-result-.json` to the same folder. The agent must still be able to produce the same artifact inline using the steps below if the script is unavailable. +Hand off the ISV to the official SCIMReferenceCode submission guidance instead of performing in-agent report generation and submission. ### Steps -#### Step 7a: Fetch Orchestrator run details - -```bash -TOKEN=$(az account get-access-token --query accessToken -o tsv) -BASE="https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.Web/sites//hostruntime/runtime/webhooks/workflow/api/management" -API="api-version=2022-03-01" - -curl -s -H "Authorization: Bearer $TOKEN" \ - "$BASE/workflows/Orchestrator_Workflow/runs/?$API" -``` - -Extract: `runId`, `status`, `startTime`, `endTime`. Compute `duration` as `Hh Mm Ss`. - -> **API version:** use `2022-03-01`. Newer versions (e.g. `2022-05-01`, `2023-12-01`) currently return `NoRegisteredProviderFound` against Standard `hostruntime` in some regions. - -#### Step 7b: Load workflow definitions - -The agent needs the workflow definitions to walk the nested tree (the `hostruntime` endpoint does not return the inline definition). Acquire them in this order: - -1. **GitHub raw URL first** — `https://raw.githubusercontent.com/AzureAD/SCIMReferenceCode/master/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/.json`. Use `curl -fsSL` and accept HTTP 200 only. -2. **Local workspace fallback** — read `.json` from the same folder as this `scim-onboarding.agent.md`. -3. If neither source returns the file, abort Phase 7 and tell the ISV which file is missing. - -Required files: `Orchestrator_Workflow.json`, `Initialization_Workflow.json`, `UserTests_Workflow.json`, `GroupTests_Workflow.json`, `SCIMTests_Workflow.json`. Log to the ISV which source (`github` vs `local`) supplied each file. - -As a last resort only, you can ask the management API: - -```bash -curl -s -H "Authorization: Bearer $TOKEN" "$BASE/workflows/?$API" -``` - -then follow `definition_href` (note: that link requires the SCM site auth and is not always reachable — prefer the local files). - -#### Step 7c: Read Final_TestResults - -```bash -curl -s -H "Authorization: Bearer $TOKEN" \ - "$BASE/workflows/Orchestrator_Workflow/runs//actions/Final_TestResults?$API" -``` - -Follow `properties.outputsLink.uri` (no auth header — it is a SAS URL) to retrieve the body. Extract `overallResult`, `testResults[]`, `childWorkflowRunLinks`. - -#### Step 7d: Discover child workflow run ids - -For each child workflow called by the Orchestrator (Initialization, UserTests, GroupTests, SCIMTests), find its `Call__Workflow` action, GET the `outputsLink.uri` (SAS, no auth header), and read `x-ms-workflow-run-id` from inside the JSON body's `headers` object — NOT from the SAS response's HTTP headers (which only contain `Content-Type`, `Content-Length`, etc.): - -```bash -curl -s -H "Authorization: Bearer $TOKEN" \ - "$BASE/workflows/Orchestrator_Workflow/runs//actions/Call__Workflow?$API" -# then GET the outputsLink.uri (SAS, no auth header). The body is shaped: -# { "statusCode": 200, "headers": { "x-ms-workflow-run-id": "...", ... }, "body": {...} } -# Extract: body.headers["x-ms-workflow-run-id"] -``` - -#### Step 7e: Page all run actions per workflow - -For each of the 5 workflows (Orchestrator + 4 children), call: - -```bash -curl -s -H "Authorization: Bearer $TOKEN" \ - "$BASE/workflows//runs//actions?$API" -``` - -Follow `nextLink` until exhausted. - -#### Step 7f: Bulk-fetch every action's inputs and outputs - -For **every** action with `properties.inputsLink` and/or `properties.outputsLink`, GET the link URI directly (SAS — **no Authorization header**, no api-version). Fan out in parallel; throttle to ~20 concurrent requests. - -Index the responses as `contentMap[][] = { inputs, outputs }`. - -#### Step 7g: Repetitions fallback for Until / Foreach (REQUIRED) - -Loop containers (`Until`, `Foreach`) and many actions nested inside them have **no direct inputsLink/outputsLink** on the run-action — instead, each iteration is a separate "repetition". To capture inputs and outputs for these actions, for every run-action where: - -- `properties.inputsLink` and `properties.outputsLink` are both absent, AND -- `properties.repetitionCount > 0`, AND -- `properties.status` is `Succeeded` or `Failed` - -call: - -```bash -curl -s -H "Authorization: Bearer $TOKEN" \ - "$BASE/workflows//runs//actions//repetitions?$API" -``` - -Pick the **last** repetition by `properties.startTime` (descending). Then fetch its detail: - -```bash -curl -s -H "Authorization: Bearer $TOKEN" \ - "$BASE/workflows//runs//actions//repetitions/?$API" -``` - -Read `properties.inputsLink.uri` and `properties.outputsLink.uri` on the repetition detail and GET those SAS URLs to inline the final iteration's inputs and outputs into `contentMap[][]`. - -This mirrors the upstream `ValidateLogicAppRun.ps1` "last repetition" fallback. - -#### Step 7h: Walk each workflow definition to build the nested action tree - -For each workflow, walk `definition.actions` recursively. Emit one node per action. Each node has: - -- `_details` — object with `{ status, code, startTime, endTime, error, inputs, outputs }` populated from the run-action and `contentMap`. -- All child action names as **sibling keys** alongside `_details`, recursively. - -Recurse into: - -- `actions` (Scope, If true-branch, Until, Foreach) -- `else.actions` (If false-branch) — prefix child keys with `__else_` -- `default.actions` (Switch default) — prefix with `__default_` -- `cases..actions` (Switch cases) — prefix with `__case__` - -Write the result to `allActionsDetailed[] = `. - -#### Step 7i: Fetch and redact parameters - -Fetch the run's **workflow version parameters** (preferred) — the Orchestrator run object contains `properties.workflow.name` (the version ID); call `GET .../workflows/Orchestrator_Workflow/versions/` and read `properties.parameters`. This captures the exact parameter values that were active when the run started, even if someone updated the Logic App parameters afterward. Fall back to the local `Orchestrator_Parameters.json` file only if the version API is unavailable. Redact: - -- `scimBearerToken` → `"***"` -- Any field name matching `*token*`, `*secret*`, `*credential*`, `*password*`, `*key*` → `"***"` -- `password` field inside any object in `defaultUserProperties` → `"***"` - -#### Step 7j: Build the validation result JSON - -Construct the output matching this schema (top-level keys, in order): - -```json -{ - "validationResult": "PASSED | FAILED", - "runStatus": "Succeeded | Failed", - "overallResultFromTests": "Success | Failed", - "validationChecks": { - "noFailedActions": true, - "noFailedTests": true, - "templateStructureValid": true, - "requiredStagesExecuted": true, - "allTemplateActionsExecuted": true - }, - "timestamp": "2025-01-15T10:30:00.000Z", - "runId": "", - "logicAppName": "", - "resourceGroup": "", - "subscriptionId": "", - "startTime": "", - "endTime": "", - "duration": "0h 25m 12s", - "parameters": { "...redacted..." }, - "actionSummary": { "total": 655, "succeeded": 357, "failed": 1, "skipped": 297, "other": 0 }, - "testSummary": { "total": 13, "success": 7, "failed": 1, "skipped": 5 }, - "testResults": [ /* from Final_TestResults */ ], - "failedActions": [ - { - "name": "actionName", - "workflow": "UserTests_Workflow", - "runId": "", - "status": "Failed", - "errorCode": "BadRequest", - "errorMessage": "...", - "startTime": "...", - "endTime": "..." - } - ], - "templateValidation": { - "valid": true, - "requiredStages": [ - { "stage": "Stage Name", "action": "action_name", "executed": true, "status": "Succeeded" } - ], - "errors": [] - }, - "actionComparison": { - "valid": true, - "missingFromRunCount": 0, - "missingActions": null - }, - "childWorkflowRuns": { "": "" }, - "childWorkflowRunLinks": { /* from Final_TestResults */ }, - "allActionsDetailed": { - "Orchestrator_Workflow": { - "": { - "_details": { - "status": "Succeeded", - "code": "OK", - "startTime": "...", - "endTime": "...", - "error": null, - "inputs": { /* SAS-fetched body */ }, - "outputs": { /* SAS-fetched body */ } - }, - "": { "_details": { ... }, "...": { ... } } - } - }, - "Initialization_Workflow": { /* same shape */ }, - "UserTests_Workflow": { /* same shape */ }, - "GroupTests_Workflow": { /* same shape */ }, - "SCIMTests_Workflow": { /* same shape */ } - } -} -``` - -**Validation checks:** - -- `noFailedActions` — no actions across any workflow have status `Failed`, **except** for the whitelist of probe actions whose `Failed` status is the *expected success signal*. Whitelist (do NOT count as failures, do NOT include in `failedActions[]`, count as `succeeded` instead): - - `DeleteUser_Check_User_Deleted` — confirms the SCIM server returned 404 after a delete; a `Failed` status here means the delete worked. -- `noFailedTests` — `Final_TestResults.testResults` has no entry where `testResult` is neither `success` nor `SKIPPED` -- `templateStructureValid` — the Orchestrator template has at least one root action -- `requiredStagesExecuted` — every root-level Orchestrator action ran (not `NotExecuted`) -- `allTemplateActionsExecuted` — every action defined in the Orchestrator template appears in the run - -`validationResult` is `PASSED` only if ALL checks are true AND `runStatus` is `Succeeded`. - -#### Step 7k: Save the file - -Write the JSON (depth at least 100) to `validation-result-.json` in the deliverable folder. Report to the ISV: - -``` -✅ Validation report generated: validation-result-.json - Result: PASSED | FAILED - Actions: total, succeeded, failed, skipped -``` - -If `FAILED`, proceed to Phase 6 (Debug) if not already done. If `PASSED`, proceed to Phase 8 (Submit). - ---- - -## Phase 8: Submit - -### Goal -Guide the ISV through submission of the validation artifacts to Microsoft. - -### Steps - -1. Confirm `validation-result-.json` shows `"validationResult": "PASSED"` -2. Present the submission instructions: - -``` -✅ VALIDATION PASSED - -Run ID: -Status: Succeeded -Report: validation-result-.json - -📦 Submit the following to aaduserprovisioning@microsoft.com: - -1. The validation-result-.json file (generated above) -2. Export your pruned schema: - Entra ID → Enterprise App → Provisioning → "Review schema" → Download -3. Your SCIM endpoint URL -4. A long-lived bearer token (for Microsoft sanity tests) -5. Any constraints (required UPN domain, restricted attribute values, etc.) -``` +1. Confirm the latest orchestrator run is successful (`FINAL STATUS: Succeeded`) and no unresolved Phase 6 issues remain. +2. Inform the ISV directly to follow the official **Submit Test Results** guidance in: + https://github.com/AzureAD/SCIMReferenceCode/tree/master/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp +3. Inform the ISV directly to complete submission exactly as documented there, including required artifacts and contact path. +4. If the ISV needs help locating generated files in the current workspace, assist with file discovery only. --- -## Phase 9: Cleanup +## Phase 7: Cleanup ### Goal Remove test artifacts from the ISV's tenant.