diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json index bd852a3..3871c1b 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json @@ -39,7 +39,7 @@ "description": "Version of the Logic App for tracking and deployment purposes" }, "type": "String", - "value": "8.0" + "value": "9.0" }, "attributeTransforms": { "metadata": { diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Workflow.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Workflow.json index bd8a91d..aadd628 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Workflow.json +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Workflow.json @@ -504,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(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'))", + "overallLogicAppResult": "@if(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_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')),or(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'))),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')" } @@ -707,6 +707,15 @@ "runLink": "@if(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupPaginationTestOutputs']?['overallResult'], 'SKIPPED'), 'SKIPPED'), '', outputs('Compose_SCIMTests_RunLink'))", "message": "@if(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupPaginationTestOutputs']?['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": "POD_User_Test", "testCategory": "mandatory", @@ -725,15 +734,6 @@ "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", @@ -768,4 +768,4 @@ "outputs": {} }, "kind": "Stateful" -} +} \ No newline at end of file diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIM-Validation-Test-Overview.md b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIM-Validation-Test-Overview.md index 391852e..9d72bd4 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIM-Validation-Test-Overview.md +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIM-Validation-Test-Overview.md @@ -2,7 +2,7 @@ ## Purpose -The SCIM Validation Logic App runs **23 automated tests** against an ISV's SCIM 2.0 endpoint to verify it is ready for publication in the Microsoft Entra app gallery. Tests cover the full provisioning lifecycle — user and group CRUD, attribute mappings, soft delete, provision-on-demand, direct SCIM compliance, and credential validation. +The SCIM Validation Logic App runs **25 automated tests** against an ISV's SCIM 2.0 endpoint to verify it is ready for publication in the Microsoft Entra app gallery. Tests cover the full provisioning lifecycle — user and group CRUD, attribute mappings, soft delete, provision-on-demand, direct SCIM compliance, credential validation, and federated identity. The Logic App is deployed as a Standard Azure Logic App with 5 workflows that execute in parallel, completing a full validation run in 30–60 minutes. @@ -15,63 +15,63 @@ Orchestrator_Workflow (entry point) ├── Initialization_Workflow — reads sync schema, builds dynamic test bodies ├── UserTests_Workflow — 7 tests (parallel with Group/SCIM) ├── GroupTests_Workflow — 7 tests (parallel with User/SCIM) - └── SCIMTests_Workflow — 9 tests (parallel with User/Group) - └── Final_TestResults — aggregates 23 results, determines pass/fail + └── SCIMTests_Workflow — 11 tests (parallel with User/Group) + └── Final_TestResults — aggregates 25 results, determines pass/fail ``` **Dynamic capability detection:** The Initialization workflow reads the provisioning schema and automatically determines which tests apply based on the ISV's attribute mappings: | Capability | How detected | Tests affected | |------------|-------------|----------------| -| Group support | Group object mapping enabled in sync rules | 7 group-related tests | -| Manager support | `manager` attribute in User mappings | User_Update_Manager_Test | -| Soft delete support | `active` attribute in User mappings | Disable_User_Test | +| Group support | Group schema (`urn:ietf:params:scim:schemas:core:2.0:Group`) in `/Schemas` response | 7 group-related tests + SCIM_Group_Create/Update/Pagination | +| Manager support | `manager` attribute in User mappings / target directory attributes | User_Update_Manager_Test, SCIM_Update_Manager_Test | +| Soft delete support | `active` attribute in User mappings | Disable_User_Test (always runs — mandatory) | -Tests that don't apply are reported as **SKIPPED** (not failures). +Tests that don't apply are reported as **SKIPPED** (not failures). However, group tests report **FAILED** (not SKIPPED) if `/Schemas` doesn't return the Group schema — group provisioning is mandatory. --- -## Test Inventory (22 Tests) +## Test Inventory (25 Tests) ### User Provisioning Tests (UserTests_Workflow) — 7 tests -| # | Test Name | What It Validates | -|---|-----------|-------------------| -| 1 | **Create_User_Test** | Creates a user in Entra ID, triggers a provisioning cycle, and verifies the user appears on the SCIM endpoint via `GET /Users?filter=userName eq "..."`. Validates the full create pipeline: Entra → Provisioning Engine → SCIM POST /Users → verification. | -| 2 | **Update_User_Test** | Modifies mapped attributes (e.g., jobTitle, department) on an existing provisioned user in Entra ID, triggers a sync cycle, and verifies the updated values are reflected on the SCIM endpoint via PATCH. | -| 3 | **Disable_User_Test** | Sets `accountEnabled=false` on a provisioned user in Entra ID, triggers a sync cycle, and verifies the SCIM endpoint receives `active: false`. **Skipped** if the ISV's schema has no `active` attribute mapping. | -| 4 | **Delete_User_Test** | Deletes a provisioned user from Entra ID, triggers a sync cycle, and verifies the user is removed from the SCIM endpoint (hard delete via DELETE /Users/{id} or soft delete depending on configuration). | -| 5 | **User_Update_Manager_Test** | Assigns a manager to a provisioned user and verifies the SCIM endpoint receives the manager reference update. **Skipped** if the ISV's schema has no `manager` attribute mapping. | -| 6 | **Restore_User_Test** | Validates the unassign → reassign lifecycle. Creates and provisions a user (Phase 1), unassigns the app role to trigger deprovisioning — verifies the user disappears from the SCIM endpoint (Phase 2), then reassigns the app role and verifies the user is re-provisioned (Phase 3). Checks whether the SCIM identity link is preserved across the cycle. | -| 7 | **POD_User_Test** | Tests **Provision on Demand** — creates a user, assigns them to the app, calls the Graph `provisionOnDemand` API, and verifies the user appears on the SCIM endpoint without waiting for a full sync cycle. | +| # | Test Name | Category | What It Validates | Pass Criteria | +|---|-----------|----------|-------------------|---------------| +| 1 | **Create_User_Test** | mandatory | Creates a user in Entra ID, assigns app role, triggers provisioning cycle, verifies user appears on SCIM endpoint. | Provisioning audit log with `provisioningAction=Create` and `status=success` found; user found on SCIM via `GET /Users?filter=userName eq "..."`. | +| 2 | **Update_User_Test** | mandatory | Modifies mapped attributes (e.g., jobTitle, department) on a provisioned user in Entra ID, triggers sync, verifies updates on SCIM. | Provisioning audit log with `provisioningAction=Update` and `status=success` found; all updated attribute values match on SCIM endpoint. | +| 3 | **Disable_User_Test** | mandatory | Sets `accountEnabled=false` on a provisioned user, triggers sync, verifies SCIM receives `active: false`. Always runs — soft-delete support is required. | Provisioning audit log with `provisioningAction=Disable` and `status=success` found. | +| 4 | **Delete_User_Test** | optional | Soft-deletes user from Entra, purges from recycle bin, triggers sync, verifies user removed from SCIM. Failure = WARNING (does not block overall pass). | Provisioning audit log with `provisioningAction=Delete` and `status=success` found; user no longer on SCIM (404 or `totalResults=0`). | +| 5 | **User_Update_Manager_Test** | mandatory | Three-phase test: (1) assign manager, (2) change to different manager, (3) remove manager. Verifies provisioning logs capture each manager change. **Skipped** if no `manager` attribute mapping. | Provisioning logs found for each phase with `modifiedProperties` containing `manager`; `newValue` matches expected manager ID on set/change; `newValue` is empty on remove. | +| 6 | **Restore_User_Test** | mandatory | Three-phase unassign/reassign lifecycle: create+provision user (Phase 1), remove app role assignment to trigger deprovision (Phase 2), re-assign app role to re-provision (Phase 3). | Phase 1: Create provisioning log with `status=success`, user on SCIM. Phase 2: Delete provisioning log with `status=success`, user gone from SCIM. Phase 3: Create (restore) provisioning log with `status=success`, user back on SCIM. Result includes `(SoftDeleteRestore)` or `(UnassignReassign)` tag. | +| 7 | **POD_User_Test** | mandatory | Tests Provision on Demand — creates user, assigns to app, calls `POST /servicePrincipals/{id}/synchronization/jobs/{jobId}/provisionOnDemand`, verifies user appears on SCIM without waiting for sync cycle. | `provisionOnDemand` returns HTTP 200; provisioning logs confirm success; user found on SCIM endpoint. | ### Group Provisioning Tests (GroupTests_Workflow) — 7 tests -| # | Test Name | What It Validates | -|---|-----------|-------------------| -| 8 | **Create_Group_Test** | Creates a group in Entra ID, assigns it to the app, triggers a sync cycle, and verifies the group appears on the SCIM endpoint via `GET /Groups?filter=displayName eq "..."`. | -| 9 | **Update_Group_Test** | Modifies group attributes (e.g., displayName) and verifies the change propagates to the SCIM endpoint. | -| 10 | **Delete_Group_Test** | Removes a group and verifies it is deleted from the SCIM endpoint. | -| 11 | **Group_Update_Add_Member_Test** | Adds a user as a member of a provisioned group and verifies the SCIM endpoint receives a PATCH with the member addition. Validates multi-member PATCH support. | -| 12 | **Group_Update_Remove_Member_Test** | Removes a member from a group and verifies the SCIM endpoint receives the member removal PATCH. | -| 13 | **POD_Group_Test** | Tests **Provision on Demand** for groups — creates a group, assigns it to the app, calls `provisionOnDemand`, and verifies the group appears on the SCIM endpoint. | -| 14 | **Restore_Group_Test** | Unassigns a group from the provisioning app, triggers a sync cycle (group should be deleted from SCIM endpoint), then reassigns and re-provisions. Validates that the SCIM endpoint handles group re-creation correctly. Includes a 30-second delay before AppRole reassignment. | - -> **Note:** All 7 group tests are **skipped** if the ISV's schema does not have an enabled Group object mapping. - -### SCIM Compliance Tests (SCIMTests_Workflow) — 9 tests - -| # | Test Name | What It Validates | -|---|-----------|-------------------| -| 15 | **Schema_Discoverability_Test** | Calls `GET /Schemas` on the SCIM endpoint and flattens the response into attribute name strings (e.g., `emails[type eq "work"].value`, `name.givenName`, `roles[primary eq "True"].value`). Compares against the target directory attributes from the provisioning job schema. Reports any missing attributes. Uses case-insensitive pipe-delimited matching. | -| 16 | **SCIM_Null_Update_Test** | Sends a PATCH request that sets an attribute to `null` and verifies the SCIM endpoint handles null/empty attribute updates without error (HTTP 200). This is a common compliance gap. | -| 17 | **SCIM_User_Create_Test** | Directly calls `POST /Users` on the SCIM endpoint (bypassing the Entra provisioning engine) with a well-formed SCIM user body built from the ISV's schema. Verifies HTTP 201 and a valid response body. | -| 18 | **SCIM_User_Update_Test** | Directly calls `PATCH /Users/{id}` on the SCIM endpoint with attribute updates. Update values are auto-generated (`upd-{guid}`) unless overridden via `scimTargetUserValues[1]`. Verifies the endpoint accepts standard SCIM PATCH operations. | -| 19 | **SCIM_Group_Create_Test** | Directly calls `POST /Groups` with a SCIM group body. **Skipped** if groups are not supported. | -| 20 | **SCIM_Group_Update_Test** | Directly calls `PATCH /Groups/{id}` with attribute updates. **Skipped** if groups are not supported. | -| 21 | **SCIM_User_Pagination_Test** | Ensures ≥11 users exist on the endpoint (creates throwaway users from `initializationData.scimUserBody` if needed), then paginates `/Users?startIndex=N&count=5` across multiple pages. Verifies `startIndex`, `totalResults`, and page traversal. Cleans up created users afterward. | -| 22 | **SCIM_Group_Pagination_Test** | Ensures ≥11 groups exist on the endpoint (creates throwaway groups from `initializationData.scimGroupBody` if needed), then paginates `/Groups?startIndex=N&count=5` across multiple pages. Verifies `startIndex`, `totalResults`, and page traversal. Cleans up created groups afterward. **Skipped** if groups are not supported. | -| 23 | **Validate_Credentials_Test** | Tests the OAuth 2.0 Client Credentials flow — acquires a token from the ISV's token endpoint using client ID/secret, then validates the SCIM connection. **Skipped** when `scimTokenEndpoint` is empty (static bearer token setup). | +| # | Test Name | Category | What It Validates | Pass Criteria | +|---|-----------|----------|-------------------|---------------| +| 8 | **Create_Group_Test** | mandatory | Creates a group in Entra ID, assigns to app, triggers sync, verifies group on SCIM. **FAILED** (not skipped) if `/Schemas` doesn't return Group schema. | Provisioning audit log with `provisioningAction=Create` and `status=success`; group found on SCIM via `GET /Groups?filter=displayName eq "..."`. | +| 9 | **Update_Group_Test** | mandatory | Modifies group displayName/description, triggers sync, verifies update propagates to SCIM. | Provisioning audit log with `provisioningAction=Update` and `status=success`; updated displayName found on SCIM. | +| 10 | **Delete_Group_Test** | optional | Deletes group from Entra, verifies removal from SCIM. Failure = WARNING. | Provisioning audit log with `provisioningAction=Delete` and `status=success`; group returns `totalResults=0` on SCIM. | +| 11 | **Group_Update_Add_Member_Test** | mandatory | Adds a user member to a provisioned group, verifies SCIM receives PATCH with member addition. | Provisioning log with `provisioningAction=Update` and `modifiedProperties` containing `members`; `newValue` contains the user ID; `oldValue` is empty. | +| 12 | **Group_Update_Remove_Member_Test** | mandatory | Removes a member from a group, verifies SCIM receives the member removal. | Provisioning log with `modifiedProperties` containing `members`; `oldValue` contains the member ID; `newValue` is empty/null. | +| 13 | **POD_Group_Test** | mandatory | Tests Provision on Demand for groups — calls `provisionOnDemand`, verifies group on SCIM without sync cycle. **FAILED** (not skipped) if groups not supported. | `provisionOnDemand` returns HTTP 200; provisioning logs confirm success; group found on SCIM. | +| 14 | **Restore_Group_Test** | optional | Unassign/reassign lifecycle for groups (same 3-phase pattern as Restore_User_Test). Failure = WARNING. | Phase 1: Create+group on SCIM. Phase 2: Delete+group gone. Phase 3: Re-create+group back. Result tagged `(UnassignReassign)`. | + +### SCIM Compliance Tests (SCIMTests_Workflow) — 11 tests + +| # | Test Name | Category | What It Validates | Pass Criteria | +|---|-----------|----------|-------------------|---------------| +| 15 | **Schema_Discoverability_Test** | mandatory | Calls `GET /Schemas`, flattens response into attribute name strings (e.g., `emails[type eq "work"].value`), compares against target directory attributes from provisioning schema. | HTTP 200 from `/Schemas`; response contains `Resources` array; all mapped user and group attributes discoverable with case-insensitive matching; zero missing attributes. | +| 16 | **SCIM_Null_Update_Test** | mandatory | Two-phase null/empty handling: (1) PATCH with empty strings on optional attributes, verify they become null/empty; (2) PATCH with null values, verify they remain null/empty. | User created successfully; Phase 1 PATCH returns 200/204 and all target attributes are null/empty in verification GET; Phase 2 PATCH returns 200/204 and attributes remain null/empty; zero failed verifications. | +| 17 | **SCIM_User_Create_Test** | mandatory | Direct `POST /Users` with well-formed SCIM body (bypasses Entra provisioning engine). | HTTP 200 or 201; response contains non-empty `id`; returned `userName` matches sent `userName` (round-trip validation). | +| 18 | **SCIM_User_Update_Test** | mandatory | Direct `PATCH /Users/{id}` with auto-generated update values for eligible attributes (single-valued, string/integer/datetime/boolean, excluding groups/roles/id/schemas/meta/active). | PATCH returns 200/204; subsequent GET returns 200/204; all PATCHed attribute values match in the response; zero mismatches. Passes with `(no updatable attributes)` if no eligible attributes exist. | +| 19 | **SCIM_Group_Create_Test** | mandatory | Direct `POST /Groups` with SCIM group body. **FAILED** if groups not supported. | HTTP 200 or 201; response contains non-empty `id`; returned `displayName` matches sent `displayName`. | +| 20 | **SCIM_Group_Update_Test** | mandatory | Direct `PATCH /Groups/{id}` with attribute updates (single-valued, non-reference, excluding id/members/schemas/meta). | PATCH returns 200/204; GET returns 200/204; all PATCHed attributes match; zero mismatches. | +| 21 | **Validate_Credentials_Test** | mandatory | OAuth 2.0 Client Credentials flow — acquires token from ISV's `scimTokenEndpoint`, then validates SCIM access with the token. **Skipped** when `scimTokenEndpoint` is empty (static bearer token). | Token endpoint returns 2xx with `access_token`; token validity between 60–360 minutes; SCIM endpoint returns 2xx when called with the token. | +| 22 | **Federated_Identity_Test** | mandatory | Workload identity federation — acquires Entra ID token, then exchanges it for ISV token via one of three flows: Google Service Account, Google STS, or generic federated endpoint. **Skipped** when federated parameters are empty. | Entra token acquisition returns 2xx with `access_token`; federated token exchange returns 2xx with valid token. Supports Google SA flow (STS exchange → SA impersonation), Google STS flow (direct exchange), and generic flow (client assertion). | +| 23 | **SCIM_User_Pagination_Test** | mandatory | Ensures ≥11 users exist (creates throwaway users if needed), then paginates `GET /Users?startIndex=N&count=5` across pages. Cleans up created users afterward. | At least 2 pages traversed; all pages return HTTP 200; response `startIndex` matches expected value (`page * 5 + 1`); zero failed page verifications. | +| 24 | **SCIM_Group_Pagination_Test** | optional | Same as user pagination but for `/Groups`. Failure = WARNING. **Skipped** if groups not supported. | At least 2 pages traversed; all pages return HTTP 200; response `startIndex` matches expected; zero failed verifications. | +| 25 | **SCIM_Update_Manager_Test** | mandatory | Direct SCIM PATCH test for manager attribute: set (Add), change (Replace), remove (Replace with empty). **Skipped** if `manager` not in target directory attributes. | Each PATCH returns 200/204; GET after each operation confirms manager value matches expected; all three operations (set/change/remove) succeed. | --- @@ -81,24 +81,75 @@ Tests that don't apply are reported as **SKIPPED** (not failures). |----------|-------|-----------| | **User Lifecycle** | 7 | Full CRUD + Manager + Restore + POD via Entra provisioning engine | | **Group Lifecycle** | 7 | Full CRUD + Membership + POD + Restore via Entra provisioning engine | -| **SCIM Direct Compliance** | 8 | Direct HTTP calls to SCIM endpoint — schema, CRUD, null update, user & group pagination | -| **Credential Validation** | 1 | OAuth client credentials flow | -| **Total Scored** | **23** | +| **SCIM Direct Compliance** | 9 | Direct HTTP calls to SCIM endpoint — schema, CRUD, null update, user & group pagination, manager set/change/remove | +| **Authentication** | 2 | OAuth client credentials flow + workload identity federation | +| **Total** | **25** | --- ## What "Passing" Means -| Scenario | Acceptable? | -|----------|-------------| -| All 23 tests: `success` | **Ready for gallery submission** | -| Group tests: `SKIPPED` (no group mapping) | Acceptable if ISV only supports /Users | -| Disable_User_Test: `SKIPPED` (no `active` mapping) | Acceptable — ISV should document | -| Manager test: `SKIPPED` (no `manager` mapping) | Acceptable — ISV should document | -| Validate_Credentials_Test: `SKIPPED` | Expected with static bearer token — no OAuth configured | -| Schema_Discoverability_Test: `FAILED` with missing attrs | Must add missing attributes to `/Schemas` response | -| Pagination tests: `FAILED` | Must implement pagination per RFC 7644 §3.4.2.4 | -| Any test: `FAILED` | **Must fix before submission** | +### Overall Result Logic + +The Orchestrator's `Evaluate_Test_Results` action computes `overallLogicAppResult` as follows: + +- **`Succeeded`** — ALL mandatory tests returned `PASSED` or `SKIPPED`, AND at least one authentication test passed (see below). +- **`Failed`** — ANY mandatory test returned `FAILED`, OR both authentication tests failed. + +### Authentication Model (OR logic) + +The authentication result uses **OR logic** across two tests: + +| Validate_Credentials_Test | Federated_Identity_Test | Auth Model Result | +|---------------------------|------------------------|-------------------| +| PASSED | PASSED | **PASSED** | +| PASSED | FAILED/SKIPPED | **PASSED** | +| FAILED/SKIPPED | PASSED | **PASSED** | +| FAILED/SKIPPED | FAILED/SKIPPED | **FAILED** — blocks overall `Succeeded` | + +This means an ISV only needs to pass **one** of the two authentication tests. If they configured OAuth only, a `Federated_Identity_Test` failure does not block submission. + +### Optional Tests (WARNING, not FAIL) + +These 4 tests are `testCategory: optional`. When they fail, the `testResult` field shows `WARNING` instead of `FAILED`, and they do **not** block the overall `Succeeded` result: + +| Test | Why optional | +|------|-------------| +| Delete_User_Test | Hard delete support varies by ISV | +| Delete_Group_Test | Hard delete support varies by ISV | +| Restore_Group_Test | Group unassign/reassign lifecycle | +| SCIM_Group_Pagination_Test | Group pagination is recommended but not required | + +### Mandatory Tests That Can Be SKIPPED + +| Test | Skip condition | Treated as | +|------|---------------|------------| +| User_Update_Manager_Test | `isManagerAttributeSupported = false` | SKIPPED (pass) | +| SCIM_Update_Manager_Test | `manager` not in target directory attributes | SKIPPED (pass) | +| Validate_Credentials_Test | `scimTokenEndpoint` is empty (static bearer token) | SKIPPED — ok if Federated_Identity passes | +| Federated_Identity_Test | Federated parameters are empty | SKIPPED — ok if Validate_Credentials passes | + +### Group Tests Are Mandatory + +Unlike individual capability-dependent tests, group tests report **FAILED** (not SKIPPED) when `/Schemas` doesn't return the Group schema (`urn:ietf:params:scim:schemas:core:2.0:Group`). The error message is: + +> `FAILED - [Initialization Phase] Schema_Discovery - /Schemas did not return Group schema. Group provisioning is mandatory.` + +This applies to: Create_Group, Update_Group, Group_Update_Add_Member, Group_Update_Remove_Member, POD_Group, SCIM_Group_Create, SCIM_Group_Update. + +### Decision Table + +| Scenario | Overall Result | Action | +|----------|---------------|--------| +| All 25 tests: `success` | **Succeeded** | Ready for gallery submission | +| All mandatory pass, optional tests show `WARNING` | **Succeeded** | Ready — warnings are informational | +| OAuth configured + Validate_Credentials passes, Federated_Identity fails (no federated params) | **Succeeded** | Ready — only one auth test needed | +| Federated configured + Federated_Identity passes, Validate_Credentials skipped | **Succeeded** | Ready — only one auth test needed | +| Both Validate_Credentials AND Federated_Identity fail/skip | **Failed** | Must fix — at least one auth test must pass | +| Manager tests `SKIPPED` (no manager mapping) | **Succeeded** | Ready — ISV should document no manager support | +| Disable_User_Test `FAILED` | **Failed** | Must implement soft-delete (`active: false`) on SCIM endpoint | +| Group tests `FAILED` (no Group schema) | **Failed** | Must add Group support to SCIM endpoint | +| Any mandatory test `FAILED` | **Failed** | Must fix failing test(s) before submission | --- @@ -125,4 +176,4 @@ Both methods produce the same output: a `validation-result-.json` file to --- -*Document version: June 2026 — Covers Logic App validation template v4 with 23 tests across 5 workflows. Includes User & Group Pagination, Restore tests, Schema_Discoverability_Test v2 with flatten loops, scimTargetUserValues, and Provision on Demand.* +*Document version: August 2026 — Covers Logic App validation template v5 with 25 tests across 5 workflows. Includes User & Group Pagination, Restore tests, Schema_Discoverability_Test v2, SCIM_Update_Manager_Test, Federated_Identity_Test, Validate_Credentials_Test, scimTargetUserValues, and Provision on Demand.* diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION index cc40bca..dd98ee6 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION @@ -1 +1 @@ -8.0 +9.0 diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md index 99f62c1..fea0b33 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md @@ -41,7 +41,7 @@ Collect the ISV's SCIM endpoint and bearer token, validate their Azure environme 📄 **https://forms.microsoft.com/pages/responsepage.aspx?id=v4j5cvGGr0GRqy180BHbR3elR4YvzS1IhaP_XITThvJUODY1UTJSSUFXTzFYMTQ0SkxSWTY4OTYzRi4u&route=shorturl** - After submitting, the ISV must **wait for an explicit confirmation from the Microsoft team** that their tenant has been whitelisted. Do not attempt to continue with the onboarding steps until this confirmation is received — any Azure or Entra resource creation will fail if the tenant has not yet been approved. + After submitting, the ISV must **wait for an explicit confirmation from the Microsoft team** that their tenant has been whitelisted. This typically takes **2 business days**. Do not attempt to continue with the onboarding steps until this confirmation is received — any Azure or Entra resource creation will fail if the tenant has not yet been approved. Use `ask_user` to confirm the ISV has received whitelisting confirmation from Microsoft before proceeding. Do NOT continue to Step 2 until confirmed. @@ -179,7 +179,7 @@ az rest --method POST \ > > 📄 **https://forms.microsoft.com/pages/responsepage.aspx?id=v4j5cvGGr0GRqy180BHbR3elR4YvzS1IhaP_XITThvJUODY1UTJSSUFXTzFYMTQ0SkxSWTY4OTYzRi4u&route=shorturl** > -> *After submitting, wait for an explicit confirmation from the Microsoft team that your tenant has been whitelisted. Do not attempt to continue until you receive this confirmation. Once confirmed, restart the agent from the beginning.* +> *After submitting, wait for an explicit confirmation from the Microsoft team that your tenant has been whitelisted (typically 2 business days). Do not attempt to continue until you receive this confirmation. Once confirmed, restart the agent from the beginning.* **If the instantiate call succeeds (HTTP 200/201):** @@ -195,16 +195,16 @@ Extract from response: > > **`scimBearerToken` is ALWAYS required** — the LA workflows use it for all 22 direct SCIM HTTP calls. If the ISV did not provide a bearer token at all, the Logic App tests cannot run — abort and ask for one. > -> **NEVER mix** bearer + OAuth keys in the same PUT `/synchronization/secrets` call — Graph returns 500 and silently drops the entire payload. +> **NEVER mix** bearer + OAuth keys in the same credential-save call. Use the beta portal path `PATCH /servicePrincipals//synchronization/connectivityParameters` with only the keys for the selected auth mode. > Always pass the body via a file (`--body '@file.json'`). Inline JSON gets corrupted on Windows pwsh. **Determine the branch** based on what the ISV provided in Phase 1: -| ISV provided | Branch | Entra sync uses | PUT secrets with | +| ISV provided | Branch | Entra sync uses | Save connectivity with | |---|---|---|---| -| Bearer only | A | Bearer | `BaseAddress` + `SecretToken` | -| Bearer + OAuth | B | OAuth | `BaseAddress` + `Oauth2ClientId` + `Oauth2ClientSecret` + `Oauth2TokenExchangeUri` | +| Bearer only | A | Bearer | `authenticationType` + `baseAddress` + `secretToken` | +| Bearer + OAuth | B | OAuth | `authenticationType` + `baseAddress` + `oauth2ClientId` + `oauth2ClientSecret` + `oauth2TokenExchangeUri` + `credentialLocationInRequest` | --- @@ -234,55 +234,53 @@ az rest --method POST \ | HTTP 400 `RequestMissingRequiredParameter` | Body is missing `templateId` or `credentials` array | Fix the body shape. | | Any other 4xx/5xx | Unexpected | Surface verbatim and abort. | -**Sub-step 2b-2: PUT the credentials into `/synchronization/secrets`** +**Sub-step 2b-2: PATCH the credentials into `/synchronization/connectivityParameters` (portal path)** -> **IMPORTANT (PUT secrets — both branches):** Every PUT to `/synchronization/secrets` **MUST** include `SyncNotificationSettings` and `SyncAll` keys alongside the auth-specific keys. Without them, Graph silently drops the entire payload — no error is returned, but no secrets are persisted. The subsequent `useSavedCredentials: true` validation then fails because there are no saved credentials. +> **IMPORTANT (beta portal path):** Save sync connectivity using `PATCH https://graph.microsoft.com/beta/servicePrincipals//synchronization/connectivityParameters` with the lower-case key names shown below. This is the portal-style path and avoids the `Oauth2TokenExchangeUri ... NotInAllowList` failure seen on `PUT /v1.0/.../synchronization/secrets` for OAuth. > -> This matches what the Entra portal sends (observable in Developer Tools → Network tab on the "Test Connection" button). The portal always includes these keys. +> `SyncNotificationSettings` and `SyncAll` are not part of the `connectivityParameters` payload. Do not include them here. **Branch A (bearer only):** ```bash -# secrets.json: +# connectivity.json: # {"value":[ -# {"key":"SyncNotificationSettings","value":"{\"Enabled\":false,\"DeleteThresholdEnabled\":true,\"DeleteThresholdValue\":500}"}, -# {"key":"SyncAll","value":"false"}, -# {"key":"BaseAddress","value":""}, -# {"key":"SecretToken","value":""} +# {"key":"authenticationType","value":"AuthenticationToken"}, +# {"key":"baseAddress","value":""}, +# {"key":"secretToken","value":""} # ]} -az rest --method PUT \ - --url "https://graph.microsoft.com/v1.0/servicePrincipals//synchronization/secrets" \ +az rest --method PATCH \ + --url "https://graph.microsoft.com/beta/servicePrincipals//synchronization/connectivityParameters" \ --headers "Content-Type=application/json" \ - --body '@secrets.json' + --body '@connectivity.json' ``` **Branch B (OAuth — ISV provided OAuth creds):** ```bash -# secrets.json: +# connectivity.json: # {"value":[ -# {"key":"SyncNotificationSettings","value":"{\"Enabled\":false,\"DeleteThresholdEnabled\":true,\"DeleteThresholdValue\":500}"}, -# {"key":"SyncAll","value":"false"}, -# {"key":"AuthenticationType","value":"OAuth2ClientCredentialsGrant"}, -# {"key":"BaseAddress","value":""}, -# {"key":"Oauth2ClientId","value":""}, -# {"key":"Oauth2ClientSecret","value":""}, -# {"key":"Oauth2TokenExchangeUri","value":""} +# {"key":"authenticationType","value":"OAuth2ClientCredentialsGrant"}, +# {"key":"baseAddress","value":""}, +# {"key":"oauth2ClientId","value":""}, +# {"key":"oauth2ClientSecret","value":""}, +# {"key":"oauth2TokenExchangeUri","value":""}, +# {"key":"credentialLocationInRequest","value":"Header"} # ]} -az rest --method PUT \ - --url "https://graph.microsoft.com/v1.0/servicePrincipals//synchronization/secrets" \ +az rest --method PATCH \ + --url "https://graph.microsoft.com/beta/servicePrincipals//synchronization/connectivityParameters" \ --headers "Content-Type=application/json" \ - --body '@secrets.json' + --body '@connectivity.json' ``` -> Note: `AuthenticationType=OAuth2ClientCredentialsGrant` is required for OAuth — without it, Graph won't attempt OAuth token acquisition from the stored creds. +> Note: `authenticationType=OAuth2ClientCredentialsGrant` is required for OAuth — without it, Graph won't attempt OAuth token acquisition from the stored creds. > -> Inline `validateCredentials` with OAuth keys (i.e. `useSavedCredentials: false` + credentials array including OAuth keys) **works** — this is exactly what the Entra portal does. Earlier advice to avoid inline OAuth validation was incorrect; the real issue was missing `SyncNotificationSettings`/`SyncAll` in the PUT payload. +> Inline `validateCredentials` with OAuth keys (i.e. `useSavedCredentials: false` + credentials array including OAuth keys) **works**, but do **not** include `CredentialLocationInRequest` in that inline payload — Graph returns `InternalError: Requested value 'CredentialLocationInRequest' was not found.` Keep that key only in the beta `connectivityParameters` PATCH payload. Then validate saved credentials work (both branches) — **this is the Test Connection**: ```bash # validate_saved.json: # {"templateId":"isvonboarding","useSavedCredentials":true} az rest --method POST \ - --url "https://graph.microsoft.com/v1.0/servicePrincipals//synchronization/jobs/validateCredentials" \ + --url "https://graph.microsoft.com/beta/servicePrincipals//synchronization/jobs/validateCredentials" \ --headers "Content-Type=application/json" \ --body '@validate_saved.json' ``` @@ -292,11 +290,11 @@ az rest --method POST \ | Response | What it means | Action | |---|---|---| | HTTP 200/204 (empty body) | Test Connection succeeded — Entra can reach the ISV's SCIM endpoint using the saved credentials | Proceed to 2b-3 | -| HTTP 400 `CredentialValidationUnavailable` | **Branch A:** bearer token rejected by the ISV's SCIM server (401/403/5xx). **Branch B:** OAuth token exchange failed — wrong client ID, wrong client secret, bad token endpoint URL, scope issue, or the ISV's token endpoint issued a token that their SCIM server rejected. | Surface the inner error verbatim to the ISV. **ABORT.** Do NOT create the sync job — it will immediately quarantine. Wait for the ISV to provide corrected credentials, then re-PUT secrets and re-validate. | -| HTTP 500 `InternalError` — `"Requested value 'X' was not found"` | Secrets payload was silently rejected — wrong key names or mixed bearer + OAuth keys in the same PUT | Re-PUT secrets with the correct keys for the branch, **always including `SyncNotificationSettings` and `SyncAll`** (Branch A: + `BaseAddress` + `SecretToken`; Branch B: + `AuthenticationType` + `BaseAddress` + `Oauth2ClientId` + `Oauth2ClientSecret` + `Oauth2TokenExchangeUri`). Then re-validate. | +| HTTP 400 `CredentialValidationUnavailable` | **Branch A:** bearer token rejected by the ISV's SCIM server (401/403/5xx). **Branch B:** OAuth token exchange failed — wrong client ID, wrong client secret, bad token endpoint URL, scope issue, or the ISV's token endpoint issued a token that their SCIM server rejected. | Surface the inner error verbatim to the ISV. **ABORT.** Do NOT create the sync job — it will immediately quarantine. Wait for the ISV to provide corrected credentials, then re-PATCH `connectivityParameters` and re-validate. | +| HTTP 500 `InternalError` — `"Requested value 'X' was not found"` | Inline validation payload included an unsupported key (for example `CredentialLocationInRequest`) or the connectivity parameters used the wrong key casing | Re-run inline validation with only the supported OAuth keys, then re-PATCH `connectivityParameters` using the lower-case portal keys (`authenticationType`, `baseAddress`, `oauth2ClientId`, `oauth2ClientSecret`, `oauth2TokenExchangeUri`, `credentialLocationInRequest`). Then re-validate. | | Any other 4xx/5xx | Unexpected | Surface verbatim and **ABORT**. | -> ⚠️ **GATE: Do NOT proceed to Sub-step 2b-3 until the saved-credential Test Connection returns 200/204.** Creating a sync job with invalid credentials causes immediate quarantine, and recovering from quarantine requires a full restart cycle (re-PUT secrets → restart job with `resetScope: Full` → start → re-verify). It is far cheaper to fix credentials now. +> ⚠️ **GATE: Do NOT proceed to Sub-step 2b-3 until the saved-credential Test Connection returns 200/204.** Creating a sync job with invalid credentials causes immediate quarantine, and recovering from quarantine requires a full restart cycle (re-PATCH connectivity parameters → restart job with `resetScope: Full` → start → re-verify). It is far cheaper to fix credentials now. **Sub-step 2b-3: Create the sync job** ```bash @@ -329,7 +327,7 @@ else: > > 📄 **https://forms.microsoft.com/pages/responsepage.aspx?id=v4j5cvGGr0GRqy180BHbR3elR4YvzS1IhaP_XITThvJUODY1UTJSSUFXTzFYMTQ0SkxSWTY4OTYzRi4u&route=shorturl** > -> *After submitting, wait for an explicit confirmation from the Microsoft team that your tenant has been whitelisted. Do not attempt to continue until you receive this confirmation. The Entra app and sync job have been left in place. Once confirmed, restart the agent — it will detect the existing resources and continue from where it left off.* +> *After submitting, wait for an explicit confirmation from the Microsoft team that your tenant has been whitelisted (typically 2 business days). Do not attempt to continue until you receive this confirmation. The Entra app and sync job have been left in place. Once confirmed, restart the agent — it will detect the existing resources and continue from where it left off.* **If the ISV's tenant IS in the whitelist** → proceed normally. @@ -427,6 +425,7 @@ Files to acquire (exact names, case-sensitive): Example acquisition loop the agent must follow before any upload: +done ```bash BASE_RAW="https://raw.githubusercontent.com/AzureAD/SCIMReferenceCode/master/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp" FILES=(Orchestrator_Workflow.json Initialization_Workflow.json UserTests_Workflow.json GroupTests_Workflow.json SCIMTests_Workflow.json Orchestrator_Parameters.json) @@ -439,11 +438,10 @@ for f in "${FILES[@]}"; do else echo " [MISSING] $f — abort"; exit 1 fi -done + ``` Always log to the ISV which source (`github` vs `local`) was used per file, so they know whether they are deploying the upstream version or a locally modified one. - **Pre-deploy validation (MANDATORY)** — Before uploading ANY files, run these checks. Do NOT deploy invalid files — the runtime will silently fail with `WorkflowNotFound` on ALL workflows and the error only appears in Kudu host logs (`/api/vfs/LogFiles/Application/Functions/Host/`). ```python @@ -809,8 +807,8 @@ Pass criteria (ALL must hold): - `status.quarantine` is `null` - `schedule.state` is `Active` -If `status.code == Quarantine` with `lastExecError == SystemForCrossDomainIdentityManagementInvalidCredentials` and `lastExecMsg` mentions `BaseAddress`/`SecretToken`/credential, the Step 2b secrets payload was silently rejected. Recovery: -1. Re-PUT secrets with ONLY the supported keys for the auth mode (`BaseAddress` + `SecretToken` for bearer, or `BaseAddress` + `ClientId` + `ClientSecret` + `TokenEndpoint` for OAuth). +If `status.code == Quarantine` with `lastExecError == SystemForCrossDomainIdentityManagementInvalidCredentials` and `lastExecMsg` mentions `BaseAddress`/`SecretToken`/credential, the Step 2b connectivity parameters were rejected or incomplete. Recovery: +1. Re-PATCH `https://graph.microsoft.com/beta/servicePrincipals//synchronization/connectivityParameters` with ONLY the supported keys for the auth mode (bearer: `authenticationType` + `baseAddress` + `secretToken`; OAuth: `authenticationType` + `baseAddress` + `oauth2ClientId` + `oauth2ClientSecret` + `oauth2TokenExchangeUri` + `credentialLocationInRequest`). 2. `POST /servicePrincipals//synchronization/jobs//restart` with body `{"criteria":{"resetScope":"Full"}}`. (Note: `credentials`/`watermark`/`escrows`/`quarantineState` are NOT valid restart criteria properties — schema only allows `resetScope`.) 3. `POST /jobs//start` again. 4. Re-run this Step 3h check. If still quarantined, abort and report the exact error. @@ -998,7 +996,7 @@ Pass criteria (ALL must hold — identical to Phase 3h): Failure handling: - `status.code == Paused` or `schedule.state == Disabled` → **DO NOT trigger.** Call `POST /jobs//start`, wait 10s, re-check. If still Paused, abort and tell the ISV the job was manually paused in the portal. - `status.code == NotRun` with `schedule.state == Active` is **acceptable** — the scheduler is enabled and POD will work; the first scheduled cycle just hasn't fired yet. Proceed. -- `status.code == Quarantine` → follow Phase 3h Quarantine recovery (re-PUT minimal secrets, `/restart` with `{"criteria":{"resetScope":"Full"}}`, `/start`, re-check). DO NOT trigger the orchestrator until status flips to `Active`/`InProgress`. +- `status.code == Quarantine` → follow Phase 3h Quarantine recovery (re-PATCH `connectivityParameters` with supported keys only, `/restart` with `{"criteria":{"resetScope":"Full"}}`, `/start`, re-check). DO NOT trigger the orchestrator until status flips to `Active`/`InProgress`. - Any other unhealthy state → abort and report. Only after this check passes, proceed to Step 5a2. @@ -1323,7 +1321,7 @@ All 8 required permissions must be present: | 11 | `Request_ResourceNotFound` on group assignment | Graph API eventual consistency race | ⚠️ Maybe | Re-run (group wasn't replicated yet). Usually passes on retry | | 12 | `NO_LOGS` / `PROVISIONING_LOGS_MISSING` **after Step 6c confirms no permission error AND Pattern #14 is ruled out** | Entra sync cycle too slow | ⚠️ Maybe | Re-run once (sync service gets faster on subsequent cycles). If same failure repeats, escalate. | | 13 | `Authentication_MSGraphPermissionMissing` | MI missing Graph permission | ✅ Yes | Parse the missing permission name(s) from the error, find the appRoleId from the Graph SP, assign via `appRoleAssignments`. This is NOT a propagation delay — the permission was never assigned. **After assigning, you MUST `az webapp restart` the LA** to force a new MI token that carries the added appRole; without restart the cached token still lacks the permission and the next run will fail identically. Poll `/host/default/properties/status` until `state=Running` (~60s), then re-run. | -| 14 | `NO_LOGS_FOUND` on **every** UserTests/GroupTests test AND/OR `POD_User_Test`/`POD_Group_Test` returns 401 from `provisionOnDemand` AND/OR `GET /synchronization/jobs/.status.code == Quarantine` | Entra sync job quarantined (secrets missing/rejected) OR LA MI is not an owner of the App + SP (synchronization owner required for `provisionOnDemand`) | ✅ Yes | **Before any re-run of the orchestrator**, GET `/synchronization/jobs/` and check `status.code`. If `Quarantine`: re-PUT minimal `/synchronization/secrets` (only `BaseAddress` + `SecretToken` for bearer; `BaseAddress` + `ClientId` + `ClientSecret` + `TokenEndpoint` for OAuth — extra keys cause 500 and silent drop), then `POST /jobs//restart` with `{"criteria":{"resetScope":"Full"}}`, then `POST /jobs//start`, then re-verify `status.code` is `Active` and `lastExecution.error` is `null`. If `provisionOnDemand` still 401s after the job is healthy: add the LA MI's enterprise object id as owner of BOTH the application and the SP — `POST /applications//owners/$ref` and `POST /servicePrincipals//owners/$ref` with body `{"@odata.id":"https://graph.microsoft.com/v1.0/directoryObjects/"}` — then `az webapp restart` the LA and re-trigger the orchestrator. | +| 14 | `NO_LOGS_FOUND` on **every** UserTests/GroupTests test AND/OR `POD_User_Test`/`POD_Group_Test` returns 401 from `provisionOnDemand` AND/OR `GET /synchronization/jobs/.status.code == Quarantine` | Entra sync job quarantined (connectivity parameters missing/rejected) OR LA MI is not an owner of the App + SP (synchronization owner required for `provisionOnDemand`) | ✅ Yes | **Before any re-run of the orchestrator**, GET `/synchronization/jobs/` and check `status.code`. If `Quarantine`: re-PATCH `https://graph.microsoft.com/beta/servicePrincipals//synchronization/connectivityParameters` with only the supported keys for the auth mode (bearer: `authenticationType` + `baseAddress` + `secretToken`; OAuth: `authenticationType` + `baseAddress` + `oauth2ClientId` + `oauth2ClientSecret` + `oauth2TokenExchangeUri` + `credentialLocationInRequest`). Then `POST /jobs//restart` with `{"criteria":{"resetScope":"Full"}}`, then `POST /jobs//start`, then re-verify `status.code` is `Active` and `lastExecution.error` is `null`. If `provisionOnDemand` still 401s after the job is healthy: add the LA MI's enterprise object id as owner of BOTH the application and the SP — `POST /applications//owners/$ref` and `POST /servicePrincipals//owners/$ref` with body `{"@odata.id":"https://graph.microsoft.com/v1.0/directoryObjects/"}` — then `az webapp restart` the LA and re-trigger the orchestrator. | ### Step 6e: Extract canonical values from schema validation errors @@ -1378,7 +1376,7 @@ Hand off the ISV to the official SCIMReferenceCode submission guidance instead o 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 + https://github.com/AzureAD/SCIMReferenceCode/blob/master/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md#submittest-results 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.