From 66bd52d3a7d1e9a1cbf96cd82dc421a61d94769f Mon Sep 17 00:00:00 2001 From: "Prashant Rane (ALLYIS INC)" Date: Thu, 15 Oct 2026 23:53:55 -0600 Subject: [PATCH] Support declared SCIM authentication methods Separate selected-method readiness and refreshable transport from portable reporting. Preserve existing functional HTTP contracts and fail closed for selected authentication failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitattributes | 4 + .gitignore | 3 + .../README.md | 23 + .../Authentication-Changes.md | 98 + .../Authentication_Workflow.json | 1007 ++++ .../StandardLogicApp/GroupTests_Workflow.json | 2017 +++++++- .../Initialization_Workflow.json | 172 +- .../Orchestrator_Parameters.json | 105 +- .../Orchestrator_Workflow.json | 28 +- .../SCIM-Validation-Test-Overview.md | 69 +- .../StandardLogicApp/SCIMTests_Workflow.json | 4587 ++++++++++++----- .../StandardLogicApp/SCIMToken_Workflow.json | 601 +++ .../SetupLogicApp-Standard-Agent.md | 122 +- .../StandardLogicApp/Test-Authentication.ps1 | 288 ++ .../StandardLogicApp/UserTests_Workflow.json | 1439 +++++- .../StandardLogicApp/VERSION | 2 +- .../StandardLogicApp/scim-onboarding.agent.md | 212 +- 17 files changed, 8858 insertions(+), 1919 deletions(-) create mode 100644 Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Authentication-Changes.md create mode 100644 Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Authentication_Workflow.json create mode 100644 Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMToken_Workflow.json create mode 100644 Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Test-Authentication.ps1 diff --git a/.gitattributes b/.gitattributes index 1ff0c423..ac40071c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,10 @@ ############################################################################### * text=auto +Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION text eol=lf +Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/*_Workflow.json text eol=lf +Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json text eol=lf + ############################################################################### # Set default behavior for command prompt diff. # diff --git a/.gitignore b/.gitignore index 8bcda8b6..443cc8a3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ *.userosscache *.sln.docstates +# Local SCIM onboarding run evidence and deployment overlays +scimrun-v*/ + # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/README.md b/Microsoft.SCIM.LogicAppValidationTemplate/README.md index f63c8a34..3a2bcc97 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/README.md +++ b/Microsoft.SCIM.LogicAppValidationTemplate/README.md @@ -6,6 +6,29 @@ This document explains: 1. Input parameters and what values are expected. 2. Each test: what it does, key steps, wait times, and how success/failure is determined. +## Standard Logic App agent: authentication-aware validation + +Use the canonical [SCIM onboarding agent](StandardLogicApp/scim-onboarding.agent.md) and its complete, same-revision bundle. The Standard implementation deploys **seven workflows**: Orchestrator, Initialization, UserTests, GroupTests, SCIMTests, Authentication, and the shared SCIMToken provider. There are still **25 reportable tests**. [VERSION](StandardLogicApp/VERSION) is the sole overall and agent version; bump it for every bundle change and synchronize `LogicAppVersion`. + +At startup, declare the authentication methods actually supported by the endpoint and available in the validation environment: + +| Environment | `supportedAuthenticationMethods` | `scimAuthenticationMethod` | +|-------------|----------------------------------|----------------------------| +| OAuth only | `["OAuth2ClientCredentials"]` | `OAuth2ClientCredentials` | +| Federation only | `["WorkloadIdentityFederation"]` | `WorkloadIdentityFederation` | +| Both | Both gallery method values | Choose which authenticates core SCIM requests | +| Static token only | `["Bearer"]` | `Bearer` | + +The agent accepts initial parameters or asks a specific multiple-choice question, then collects only applicable credentials. Undeclared authentication tests are **Not applicable** and make no token requests. A declared method with missing credentials is blocked/failed, not silently excluded. + +The authentication readiness policy is **AND across selected methods**: every declared method must pass its test. An undeclared method is Not applicable; a selected failure cannot be hidden by another success. `Authentication_Workflow` isolates these checks to keep SCIMTests below the 500-action limit. `SCIMTests_Workflow` explicitly forwards `authenticationModelResult`; a missing result fails closed in the Orchestrator. Other mandatory functional failures still block readiness. Static-token-only functional testing produces `NOT_GALLERY_READY`. + +For OAuth-only or federation-only runs, set `scimBearerToken` to `""`. The shared token workflow acquires access tokens, and each test suite refreshes its cache before expiry. There is no static-token fallback. Sending an OAuth access token in an HTTP Bearer header does not mean the endpoint supports caller-supplied static tokens. Entra provisioning connectivity is configured and validated separately with the supported method. + +See the [Standard setup walkthrough](StandardLogicApp/SetupLogicApp-Standard-Agent.md) and [test overview](StandardLogicApp/SCIM-Validation-Test-Overview.md) for the current behavior. The generic legacy test descriptions below do not override the Standard authentication contract. + +The [authentication change guide](StandardLogicApp/Authentication-Changes.md) explains capability selection, token transport, workflow limits, and verification boundaries. Portable reporting is a separate follow-up change. + --- ## 1. Input Parameters diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Authentication-Changes.md b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Authentication-Changes.md new file mode 100644 index 00000000..49710697 --- /dev/null +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Authentication-Changes.md @@ -0,0 +1,98 @@ +# Authentication-aware SCIM validation + +## Executive summary + +This authentication-first change lets an independent software vendor (ISV) +declare the authentication methods available in its validation environment. +Every selected method must pass. An undeclared method is Not applicable and +does not issue authentication requests. + +This change advances [VERSION](VERSION) from 13.0 to 14.0 and synchronizes +`LogicAppVersion`. Portable JSON/Markdown/ZIP reporting, report collection, +and agent lifecycle instrumentation are a separate dependent change. + +## Scope and behavior + +| Concern | Authentication-first behavior | +|---|---| +| Capability selection | Accept explicit `supportedAuthenticationMethods`, or ask a specific choice. | +| Credentials | Collect only credentials for declared methods. Missing selected credentials block readiness. | +| Readiness | AND across all selected methods; a selected failed, skipped, or missing result cannot be waived. | +| Undeclared method | Not applicable, with no token acquisition or federation requests. | +| Direct SCIM transport | Select one declared method using `scimAuthenticationMethod`; this does not deselect the other authentication tests. | +| Token lifetime | Acquire and refresh OAuth/federation tokens; no static-token fallback. | +| Static bearer | Connectivity/functional validation only; not gallery-ready without a selected passing gallery method. | +| Functional tests | Mandatory provisioning failures remain independent blockers. | + +For OAuth-only or federation-only runs, set `scimBearerToken` to an empty +string. Receiving an OAuth access token in an HTTP bearer header does not +mean that caller-supplied static bearer authentication is supported. + +## Workflow architecture + +```mermaid +flowchart TD + Agent[Canonical onboarding agent] --> Selection[Declared methods and applicable credentials] + Selection --> Provisioning[Entra provisioning connectivity] + Selection --> Orchestrator + Orchestrator --> Initialization + Initialization --> UserTests + Initialization --> GroupTests + Initialization --> SCIMTests + SCIMTests --> Authentication[Authentication child: every selected method must pass] + UserTests --> Token[Shared SCIMToken provider] + GroupTests --> Token + SCIMTests --> Token + Token --> Endpoint[Selected token service and SCIM endpoint] +``` + +There are seven workflows and 25 reportable tests. Extracting authentication +keeps the SCIM suite below the platform's 500-action limit. The shared token +provider supports long-running functional suites without requiring a static +token. Neither child adds a new gallery test. + +| Workflow | Nested actions | +|---|---:| +| Authentication | 42 | +| SCIMTests | 465 | +| UserTests | 465 | +| GroupTests | 404 | +| Initialization | 99 | +| Orchestrator | 29 | +| SCIMToken | 27 | + +## Worked example + +```json +{ + "supportedAuthenticationMethods": [ + "OAuth2ClientCredentials", + "WorkloadIdentityFederation" + ], + "scimAuthenticationMethod": "OAuth2ClientCredentials" +} +``` + +Core SCIM requests use OAuth, but both authentication tests must pass. +Choosing OAuth transport does not make federation Not applicable. A +federation-only environment instead declares only federation and needs no +invented OAuth endpoint. + +## Verification and boundaries + +Run [Test-Authentication.ps1](Test-Authentication.ps1). It evaluates actual +workflow expressions, policy combinations, token refresh, protected token +handling, platform limits, and all 99 direct SCIM request guards. + +Use the same-revision [canonical agent](scim-onboarding.agent.md), +[setup walkthrough](SetupLogicApp-Standard-Agent.md), and +[test overview](SCIM-Validation-Test-Overview.md). Keep production Graph +URLs in source; authorized development routing belongs in a private +deployment overlay. + +The earlier combined change set exercised OAuth and exposed group member +schema incompatibilities and a secondary diagnostic-expression defect. +That historical run is not certification of this newly separated branch. +A fresh auth-only validation is required. Live federation success still +requires a federation-capable endpoint. Do not change unrelated functional +tests or endpoint policy merely to obtain a passing demonstration. diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Authentication_Workflow.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Authentication_Workflow.json new file mode 100644 index 00000000..44d1d2ac --- /dev/null +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Authentication_Workflow.json @@ -0,0 +1,1007 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object", + "properties": { + "supportedAuthenticationMethods": { + "type": "array", + "items": { + "type": "string" + } + }, + "EnabledTests": { + "type": "string" + }, + "parentRunId": { + "type": "string" + } + }, + "required": [ + "supportedAuthenticationMethods", + "EnabledTests" + ] + } + } + } + }, + "actions": { + "Compose_Authentication_Methods": { + "type": "Compose", + "inputs": "@coalesce(triggerBody()?['supportedAuthenticationMethods'], json('[]'))", + "runAfter": {} + }, + "Initialize_OAuth_Result": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "ValidateCredentialsTestOutputs", + "type": "Object", + "value": { + "overallResult": "@if(contains(outputs('Compose_Authentication_Methods'), 'OAuth2ClientCredentials'), 'SKIPPED', 'NOT_APPLICABLE')", + "result": "@if(contains(outputs('Compose_Authentication_Methods'), 'OAuth2ClientCredentials'), 'SKIPPED', 'NOT_APPLICABLE - authentication method not declared for this validation')", + "summary": null, + "detailedResults": [], + "errorDetails": { + "summary": null + }, + "applicability": "@if(contains(outputs('Compose_Authentication_Methods'), 'OAuth2ClientCredentials'), 'applicable', 'notApplicable')", + "authenticationMethod": "OAuth2ClientCredentials", + "reason": "@if(contains(outputs('Compose_Authentication_Methods'), 'OAuth2ClientCredentials'), 'Declared method has not completed validation.', 'ISV did not declare this authentication method available.')" + } + } + ] + }, + "runAfter": { + "Compose_Authentication_Methods": [ + "Succeeded" + ] + } + }, + "Initialize_Federation_Result": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "FederatedIdentityTestOutputs", + "type": "Object", + "value": { + "overallResult": "@if(contains(outputs('Compose_Authentication_Methods'), 'WorkloadIdentityFederation'), 'SKIPPED', 'NOT_APPLICABLE')", + "result": "@if(contains(outputs('Compose_Authentication_Methods'), 'WorkloadIdentityFederation'), 'SKIPPED', 'NOT_APPLICABLE - authentication method not declared for this validation')", + "summary": null, + "detailedResults": [], + "errorDetails": { + "summary": null + }, + "applicability": "@if(contains(outputs('Compose_Authentication_Methods'), 'WorkloadIdentityFederation'), 'applicable', 'notApplicable')", + "authenticationMethod": "WorkloadIdentityFederation", + "reason": "@if(contains(outputs('Compose_Authentication_Methods'), 'WorkloadIdentityFederation'), 'Declared method has not completed validation.', 'ISV did not declare this authentication method available.')" + } + } + ] + }, + "runAfter": { + "Initialize_OAuth_Result": [ + "Succeeded" + ] + } + }, + "Initialize_Bearer_Result": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "BearerConnectivityTestOutputs", + "type": "Object", + "value": { + "overallResult": "@if(contains(outputs('Compose_Authentication_Methods'), 'Bearer'), 'FAILED', 'NOT_APPLICABLE')", + "authenticationMethod": "Bearer", + "applicability": "@if(contains(outputs('Compose_Authentication_Methods'), 'Bearer'), 'applicable', 'notApplicable')", + "result": "Static-token connectivity has not passed." + } + } + ] + }, + "runAfter": { + "Initialize_Federation_Result": [ + "Succeeded" + ] + } + }, + "Authentication_Tests": { + "type": "Scope", + "actions": { + "Validate_Credentials_Test": { + "runAfter": {}, + "actions": { + "Acquire_OAuth2_Token": { + "type": "Http", + "inputs": { + "uri": "@{parameters('scimTokenEndpoint')}", + "method": "POST", + "headers": "@if(equals(toLower(coalesce(parameters('scimCredentialLocationInRequest'), 'Header')), 'header'), json(concat('{\"Content-Type\":\"application/x-www-form-urlencoded\",\"Authorization\":\"Basic ', base64(concat(encodeUriComponent(parameters('scimClientId')), ':', encodeUriComponent(parameters('scimClientSecret')))), '\"}')), json('{\"Content-Type\":\"application/x-www-form-urlencoded\"}'))", + "body": "@{concat('grant_type=client_credentials', if(equals(toLower(coalesce(parameters('scimCredentialLocationInRequest'), 'Header')), 'body'), concat('&client_id=', encodeUriComponent(parameters('scimClientId')), '&client_secret=', encodeUriComponent(parameters('scimClientSecret'))), ''), if(empty(coalesce(parameters('scimOAuthScope'), '')), '', concat('&scope=', encodeUriComponent(parameters('scimOAuthScope')))))}" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Check_Token_Response": { + "runAfter": { + "Acquire_OAuth2_Token": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Acquire_OAuth2_Token')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Acquire_OAuth2_Token')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@coalesce(body('Acquire_OAuth2_Token')?['access_token'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Compose_Token_Validity_Minutes": { + "type": "Compose", + "inputs": "@div(coalesce(body('Acquire_OAuth2_Token')?['expires_in'], 3600), 60)" + }, + "Check_Token_Validity_Range": { + "runAfter": { + "Compose_Token_Validity_Minutes": [ + "Succeeded" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@outputs('Compose_Token_Validity_Minutes')", + 60 + ] + }, + { + "lessOrEquals": [ + "@outputs('Compose_Token_Validity_Minutes')", + 360 + ] + } + ] + }, + "actions": { + "SCIM_Validate_Get_Users": { + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users?startIndex=1&count=1')}", + "method": "GET", + "headers": { + "Authorization": "@{concat('Bearer ', body('Acquire_OAuth2_Token')?['access_token'])}", + "Accept": "@{parameters('scimContentType')}" + } + } + }, + "Compose_SCIM_Validation_Result": { + "runAfter": { + "SCIM_Validate_Get_Users": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "Compose", + "inputs": { + "passed": "@and(greaterOrEquals(coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], 0), 200), less(coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], 0), 300))", + "statusCode": "@coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], 0)", + "endpoint": "@concat(parameters('scimEndpoint'), '/Users?startIndex=1&count=1')", + "overallResult": "@if(and(greaterOrEquals(coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], 0), 200), less(coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], 0), 300)), 'PASSED', 'FAILED')" + } + } + }, + "else": { + "actions": { + "Compose_Token_Validity_Failure": { + "type": "Compose", + "inputs": { + "passed": false, + "validityMinutes": "@outputs('Compose_Token_Validity_Minutes')", + "expiresInSeconds": "@coalesce(body('Acquire_OAuth2_Token')?['expires_in'], 3600)", + "reason": "@if(less(outputs('Compose_Token_Validity_Minutes'), 60), 'Token validity is less than 60 minutes. Minimum required: 60 minutes.', 'Token validity exceeds 360 minutes (6 hours). Maximum allowed: 360 minutes.')", + "overallResult": "FAILED" + } + } + } + } + } + }, + "else": { + "actions": { + "Compose_Token_Acquisition_Failure": { + "type": "Compose", + "inputs": { + "passed": false, + "statusCode": "@coalesce(outputs('Acquire_OAuth2_Token')?['statusCode'], 0)", + "hasAccessToken": "@not(equals(coalesce(body('Acquire_OAuth2_Token')?['access_token'], ''), ''))", + "tokenEndpoint": "@parameters('scimTokenEndpoint')", + "reason": "@if(or(less(coalesce(outputs('Acquire_OAuth2_Token')?['statusCode'], 0), 200), greaterOrEquals(coalesce(outputs('Acquire_OAuth2_Token')?['statusCode'], 0), 300)), concat('Token endpoint returned HTTP ', string(coalesce(outputs('Acquire_OAuth2_Token')?['statusCode'], 0)), '. Expected 2xx.'), 'Token endpoint returned 2xx but response did not contain an access_token field.')", + "overallResult": "FAILED" + } + } + } + } + }, + "Set_ValidateCredentialsTestOutputs": { + "runAfter": { + "Check_Token_Response": [ + "Succeeded", + "Failed", + "TimedOut", + "Skipped" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "ValidateCredentialsTestOutputs", + "value": { + "overallResult": "@coalesce(outputs('Compose_SCIM_Validation_Result')?['overallResult'], outputs('Compose_Token_Validity_Failure')?['overallResult'], outputs('Compose_Token_Acquisition_Failure')?['overallResult'], 'FAILED')", + "result": "@if(equals(coalesce(outputs('Compose_SCIM_Validation_Result')?['overallResult'], outputs('Compose_Token_Validity_Failure')?['overallResult'], outputs('Compose_Token_Acquisition_Failure')?['overallResult'], 'FAILED'), 'PASSED'), 'success', concat('FAILED - Action: ', coalesce(if(equals(coalesce(outputs('Compose_SCIM_Validation_Result')?['passed'], true), false), 'SCIM_Validate_Get_Users', null), if(not(empty(coalesce(outputs('Compose_Token_Validity_Failure'), json('null')))), 'Check_Token_Validity_Range', null), 'Acquire_OAuth2_Token'), ' - ', coalesce(if(equals(coalesce(outputs('Compose_SCIM_Validation_Result')?['passed'], true), false), concat('SCIM /Users returned HTTP ', string(coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], 0)), '. Expected 2xx.'), null), outputs('Compose_Token_Validity_Failure')?['reason'], outputs('Compose_Token_Acquisition_Failure')?['reason'], 'Unexpected failure')))", + "summary": "@coalesce(outputs('Compose_SCIM_Validation_Result'), outputs('Compose_Token_Validity_Failure'), outputs('Compose_Token_Acquisition_Failure'))", + "detailedResults": { + "gate1_tokenAcquisition": { + "passed": "@not(equals(coalesce(outputs('Compose_Token_Validity_Minutes'), outputs('Compose_Token_Acquisition_Failure'), null), null))", + "statusCode": "@coalesce(outputs('Acquire_OAuth2_Token')?['statusCode'], 0)" + }, + "gate2_tokenLifetime": { + "passed": "@not(empty(coalesce(outputs('Compose_SCIM_Validation_Result'), outputs('Compose_Token_Validity_Failure'))))", + "validityMinutes": "@coalesce(outputs('Compose_Token_Validity_Minutes'), null)" + }, + "gate3_scimAccess": { + "passed": "@coalesce(outputs('Compose_SCIM_Validation_Result')?['passed'], false)", + "statusCode": "@coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], null)" + } + }, + "errorDetails": { + "failedAction": "@coalesce(if(equals(coalesce(outputs('Compose_SCIM_Validation_Result')?['passed'], true), false), 'SCIM_Validate_Get_Users', null), if(not(empty(coalesce(outputs('Compose_Token_Validity_Failure'), json('null')))), 'Check_Token_Validity_Range', null), if(not(empty(coalesce(outputs('Compose_Token_Acquisition_Failure'), json('null')))), 'Acquire_OAuth2_Token', null), null)", + "reason": "@coalesce(if(equals(coalesce(outputs('Compose_SCIM_Validation_Result')?['passed'], true), false), concat('SCIM /Users returned HTTP ', string(coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], 0)), '. Expected 2xx.'), null), outputs('Compose_Token_Validity_Failure')?['reason'], outputs('Compose_Token_Acquisition_Failure')?['reason'], null)" + } + } + } + } + }, + "else": { + "actions": {} + }, + "expression": { + "and": [ + { + "contains": [ + "@outputs('Compose_Authentication_Methods')", + "OAuth2ClientCredentials" + ] + }, + { + "or": [ + { + "equals": [ + "@triggerBody()?['EnabledTests']", + "All" + ] + }, + { + "equals": [ + "@triggerBody()?['EnabledTests']", + "SCIMTests" + ] + }, + { + "equals": [ + "@triggerBody()?['EnabledTests']", + "Validate_Credentials_Test" + ] + } + ] + } + ] + }, + "type": "If" + }, + "Federated_Identity_Test": { + "runAfter": {}, + "actions": { + "Acquire_Entra_Token": { + "type": "Http", + "inputs": { + "uri": "@{concat('https://login.microsoftonline.com/', parameters('federatedEntraTenantId'), '/oauth2/v2.0/token')}", + "method": "POST", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "@{concat('grant_type=client_credentials&client_id=', encodeUriComponent(parameters('federatedApplicationId')), '&client_secret=', encodeUriComponent(parameters('federatedApplicationClientSecret')), '&scope=', encodeUriComponent(concat('api://', parameters('federatedApplicationId'), '/.default')))}" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Check_Entra_Token_Acquired": { + "runAfter": { + "Acquire_Entra_Token": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Acquire_Entra_Token')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Acquire_Entra_Token')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@coalesce(body('Acquire_Entra_Token')?['access_token'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Check_Is_Google_SA_Flow": { + "type": "If", + "expression": { + "and": [ + { + "contains": [ + "@parameters('federatedTokenEndpoint')", + "googleapis.com" + ] + }, + { + "contains": [ + "@parameters('federatedTokenEndpoint')", + "serviceAccounts" + ] + } + ] + }, + "actions": { + "Acquire_Google_STS_Token": { + "type": "Http", + "inputs": { + "uri": "https://sts.googleapis.com/v1/token", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "audience": "@{parameters('federatedAudience')}", + "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "subject_token": "@{body('Acquire_Entra_Token')?['access_token']}", + "subject_token_type": "urn:ietf:params:oauth:token-type:id_token", + "scope": "https://www.googleapis.com/auth/cloud-platform" + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Check_Google_STS_Token": { + "runAfter": { + "Acquire_Google_STS_Token": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Acquire_Google_STS_Token')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Acquire_Google_STS_Token')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@coalesce(body('Acquire_Google_STS_Token')?['access_token'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Acquire_Google_SA_Token": { + "type": "Http", + "inputs": { + "uri": "@{parameters('federatedTokenEndpoint')}", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "@{concat('Bearer ', body('Acquire_Google_STS_Token')?['access_token'])}" + }, + "body": { + "scope": [ + "https://www.googleapis.com/auth/cloud-platform" + ] + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Check_Google_SA_Token": { + "runAfter": { + "Acquire_Google_SA_Token": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Acquire_Google_SA_Token')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Acquire_Google_SA_Token')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@coalesce(body('Acquire_Google_SA_Token')?['accessToken'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Compose_Federated_Validation_Summary_GoogleSA": { + "type": "Compose", + "inputs": { + "testName": "Federated_Identity_Test", + "timestamp": "@utcNow()", + "authMode": "FederatedIdentity_Google_SA", + "entraTokenAcquisition": { + "endpoint": "@concat('https://login.microsoftonline.com/', parameters('federatedEntraTenantId'), '/oauth2/v2.0/token')", + "statusCode": "@outputs('Acquire_Entra_Token')['statusCode']", + "passed": true + }, + "googleStsTokenExchange": { + "endpoint": "https://sts.googleapis.com/v1/token", + "statusCode": "@outputs('Acquire_Google_STS_Token')['statusCode']", + "passed": true + }, + "googleSaImpersonation": { + "endpoint": "@parameters('federatedTokenEndpoint')", + "statusCode": "@outputs('Acquire_Google_SA_Token')['statusCode']", + "passed": true + }, + "overallResult": "PASSED" + } + } + }, + "else": { + "actions": { + "Compose_Federated_Validation_Summary_GoogleSA_Fail": { + "type": "Compose", + "inputs": { + "testName": "Federated_Identity_Test", + "timestamp": "@utcNow()", + "authMode": "FederatedIdentity_Google_SA", + "entraTokenAcquisition": { + "passed": true + }, + "googleStsTokenExchange": { + "passed": true + }, + "googleSaImpersonation": { + "endpoint": "@parameters('federatedTokenEndpoint')", + "statusCode": "@coalesce(outputs('Acquire_Google_SA_Token')?['statusCode'], 0)", + "hasAccessToken": "@not(equals(coalesce(body('Acquire_Google_SA_Token')?['accessToken'], ''), ''))", + "passed": false + }, + "overallResult": "FAILED" + } + } + } + } + } + }, + "else": { + "actions": { + "Compose_Federated_Validation_Summary_GoogleSTS_Fail": { + "type": "Compose", + "inputs": { + "testName": "Federated_Identity_Test", + "timestamp": "@utcNow()", + "authMode": "FederatedIdentity_Google_SA", + "entraTokenAcquisition": { + "passed": true + }, + "googleStsTokenExchange": { + "endpoint": "https://sts.googleapis.com/v1/token", + "statusCode": "@coalesce(outputs('Acquire_Google_STS_Token')?['statusCode'], 0)", + "hasAccessToken": "@not(equals(coalesce(body('Acquire_Google_STS_Token')?['access_token'], ''), ''))", + "passed": false + }, + "overallResult": "FAILED" + } + } + } + } + } + }, + "else": { + "actions": { + "Check_Is_Google_STS_Flow": { + "type": "If", + "expression": { + "and": [ + { + "contains": [ + "@parameters('federatedTokenEndpoint')", + "googleapis.com" + ] + } + ] + }, + "actions": { + "Acquire_Google_STS_Token_Only": { + "type": "Http", + "inputs": { + "uri": "@{parameters('federatedTokenEndpoint')}", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "audience": "@{parameters('federatedAudience')}", + "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "subject_token": "@{body('Acquire_Entra_Token')?['access_token']}", + "subject_token_type": "urn:ietf:params:oauth:token-type:id_token", + "scope": "https://www.googleapis.com/auth/cloud-platform" + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Check_Google_STS_Only_Token": { + "runAfter": { + "Acquire_Google_STS_Token_Only": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Acquire_Google_STS_Token_Only')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Acquire_Google_STS_Token_Only')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@coalesce(body('Acquire_Google_STS_Token_Only')?['access_token'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Compose_Federated_Validation_Summary_GoogleSTSOnly": { + "type": "Compose", + "inputs": { + "testName": "Federated_Identity_Test", + "timestamp": "@utcNow()", + "authMode": "FederatedIdentity_Google_STS", + "entraTokenAcquisition": { + "endpoint": "@concat('https://login.microsoftonline.com/', parameters('federatedEntraTenantId'), '/oauth2/v2.0/token')", + "statusCode": "@outputs('Acquire_Entra_Token')['statusCode']", + "passed": true + }, + "googleStsTokenExchange": { + "endpoint": "@parameters('federatedTokenEndpoint')", + "statusCode": "@outputs('Acquire_Google_STS_Token_Only')['statusCode']", + "passed": true + }, + "overallResult": "PASSED" + } + } + }, + "else": { + "actions": { + "Compose_Federated_Validation_Summary_GoogleSTSOnly_Fail": { + "type": "Compose", + "inputs": { + "testName": "Federated_Identity_Test", + "timestamp": "@utcNow()", + "authMode": "FederatedIdentity_Google_STS", + "entraTokenAcquisition": { + "passed": true + }, + "googleStsTokenExchange": { + "endpoint": "@parameters('federatedTokenEndpoint')", + "statusCode": "@coalesce(outputs('Acquire_Google_STS_Token_Only')?['statusCode'], 0)", + "hasAccessToken": "@not(equals(coalesce(body('Acquire_Google_STS_Token_Only')?['access_token'], ''), ''))", + "passed": false + }, + "overallResult": "FAILED" + } + } + } + } + } + }, + "else": { + "actions": { + "Acquire_Federated_Token": { + "type": "Http", + "inputs": { + "uri": "@{parameters('federatedTokenEndpoint')}", + "method": "POST", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "@{concat('grant_type=client_credentials&client_id=', encodeUriComponent(parameters('federatedClientId')), '&client_assertion=', encodeUriComponent(body('Acquire_Entra_Token')?['access_token']), '&client_assertion_type=', encodeUriComponent('urn:ietf:params:oauth:client-assertion-type:jwt-bearer'), '&baseAddress=', encodeUriComponent(parameters('federatedBaseAddress')))}" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + } + }, + "Check_Federated_Token_Acquired": { + "runAfter": { + "Acquire_Federated_Token": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Acquire_Federated_Token')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Acquire_Federated_Token')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@coalesce(body('Acquire_Federated_Token')?['access_token'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Compose_Federated_Validation_Summary": { + "type": "Compose", + "inputs": { + "testName": "Federated_Identity_Test", + "timestamp": "@utcNow()", + "authMode": "FederatedIdentity", + "entraTokenAcquisition": { + "endpoint": "@concat('https://login.microsoftonline.com/', parameters('federatedEntraTenantId'), '/oauth2/v2.0/token')", + "statusCode": "@outputs('Acquire_Entra_Token')['statusCode']", + "passed": true + }, + "federatedTokenExchange": { + "endpoint": "@parameters('federatedTokenEndpoint')", + "statusCode": "@outputs('Acquire_Federated_Token')['statusCode']", + "passed": true + }, + "overallResult": "PASSED" + } + } + }, + "else": { + "actions": { + "Compose_Federated_Validation_Summary_TokenFail": { + "type": "Compose", + "inputs": { + "testName": "Federated_Identity_Test", + "timestamp": "@utcNow()", + "authMode": "FederatedIdentity", + "entraTokenAcquisition": { + "passed": true + }, + "federatedTokenExchange": { + "endpoint": "@parameters('federatedTokenEndpoint')", + "statusCode": "@coalesce(outputs('Acquire_Federated_Token')?['statusCode'], 0)", + "hasAccessToken": "@not(equals(coalesce(body('Acquire_Federated_Token')?['access_token'], ''), ''))", + "passed": false + }, + "overallResult": "FAILED" + } + } + } + } + } + } + } + } + } + } + } + }, + "else": { + "actions": { + "Compose_Federated_Validation_Summary_EntraFail": { + "type": "Compose", + "inputs": { + "testName": "Federated_Identity_Test", + "timestamp": "@utcNow()", + "authMode": "FederatedIdentity", + "entraTokenAcquisition": { + "endpoint": "@concat('https://login.microsoftonline.com/', parameters('federatedEntraTenantId'), '/oauth2/v2.0/token')", + "statusCode": "@coalesce(outputs('Acquire_Entra_Token')?['statusCode'], 0)", + "hasAccessToken": "@not(equals(coalesce(body('Acquire_Entra_Token')?['access_token'], ''), ''))", + "passed": false + }, + "federatedTokenExchange": null, + "scimEndpointValidation": null, + "overallResult": "FAILED" + } + } + } + } + }, + "Set_FederatedIdentityTestOutputs": { + "runAfter": { + "Check_Entra_Token_Acquired": [ + "Succeeded", + "Failed", + "TimedOut", + "Skipped" + ] + }, + "type": "SetVariable", + "inputs": { + "name": "FederatedIdentityTestOutputs", + "value": { + "overallResult": "@coalesce(outputs('Compose_Federated_Validation_Summary')?['overallResult'], outputs('Compose_Federated_Validation_Summary_TokenFail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSA')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSA_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTS_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_EntraFail')?['overallResult'], 'FAILED')", + "result": "@if(equals(coalesce(outputs('Compose_Federated_Validation_Summary')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSA')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly')?['overallResult'], outputs('Compose_Federated_Validation_Summary_TokenFail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSA_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTS_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_EntraFail')?['overallResult'], 'FAILED'), 'PASSED'), 'success', if(not(empty(coalesce(outputs('Compose_Federated_Validation_Summary_EntraFail'), json('null')))), 'Entra token acquisition failed', 'Federated token exchange failed'))", + "summary": "@coalesce(outputs('Compose_Federated_Validation_Summary'), outputs('Compose_Federated_Validation_Summary_TokenFail'), outputs('Compose_Federated_Validation_Summary_GoogleSA'), outputs('Compose_Federated_Validation_Summary_GoogleSA_Fail'), outputs('Compose_Federated_Validation_Summary_GoogleSTS_Fail'), outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly'), outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly_Fail'), outputs('Compose_Federated_Validation_Summary_EntraFail'))", + "detailedResults": [], + "errorDetails": { + "summary": "@if(equals(coalesce(outputs('Compose_Federated_Validation_Summary')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSA')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly')?['overallResult'], outputs('Compose_Federated_Validation_Summary_TokenFail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSA_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTS_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_EntraFail')?['overallResult'], 'FAILED'), 'PASSED'), null, if(not(empty(coalesce(outputs('Compose_Federated_Validation_Summary_EntraFail'), json('null')))), concat('Entra token acquisition failed. Status: ', string(coalesce(outputs('Acquire_Entra_Token')?['statusCode'], 0))), if(not(empty(coalesce(outputs('Compose_Federated_Validation_Summary_GoogleSTS_Fail'), json('null')))), concat('Google STS token exchange failed. Status: ', string(coalesce(outputs('Acquire_Google_STS_Token')?['statusCode'], 0))), if(not(empty(coalesce(outputs('Compose_Federated_Validation_Summary_GoogleSA_Fail'), json('null')))), concat('Google SA impersonation failed. Status: ', string(coalesce(outputs('Acquire_Google_SA_Token')?['statusCode'], 0))), if(not(empty(coalesce(outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly_Fail'), json('null')))), concat('Google STS token exchange failed. Status: ', string(coalesce(outputs('Acquire_Google_STS_Token_Only')?['statusCode'], 0))), concat('Federated token exchange failed. Status: ', string(coalesce(outputs('Acquire_Federated_Token')?['statusCode'], 0))))))))" + } + } + } + } + }, + "else": { + "actions": {} + }, + "expression": { + "and": [ + { + "contains": [ + "@outputs('Compose_Authentication_Methods')", + "WorkloadIdentityFederation" + ] + }, + { + "or": [ + { + "equals": [ + "@triggerBody()?['EnabledTests']", + "All" + ] + }, + { + "equals": [ + "@triggerBody()?['EnabledTests']", + "SCIMTests" + ] + }, + { + "equals": [ + "@triggerBody()?['EnabledTests']", + "Federated_Identity_Test" + ] + } + ] + } + ] + }, + "type": "If" + }, + "Bearer_Connectivity_Test": { + "type": "If", + "expression": "@contains(outputs('Compose_Authentication_Methods'), 'Bearer')", + "actions": { + "Validate_Bearer_Connectivity": { + "type": "Http", + "inputs": { + "uri": "@concat(parameters('scimEndpoint'), '/Users?startIndex=1&count=1')", + "method": "GET", + "headers": { + "Authorization": "@concat('Bearer ', parameters('scimBearerToken'))", + "Accept": "@parameters('scimContentType')" + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs" + ] + } + }, + "runAfter": {} + }, + "Set_Bearer_Result": { + "type": "SetVariable", + "inputs": { + "name": "BearerConnectivityTestOutputs", + "value": { + "overallResult": "@if(equals(outputs('Validate_Bearer_Connectivity')?['statusCode'], 200), 'PASSED', 'FAILED')", + "result": "@if(equals(outputs('Validate_Bearer_Connectivity')?['statusCode'], 200), 'success', 'FAILED - static-token connectivity did not return HTTP 200')", + "authenticationMethod": "Bearer", + "applicability": "applicable", + "statusCode": "@outputs('Validate_Bearer_Connectivity')?['statusCode']" + } + }, + "runAfter": { + "Validate_Bearer_Connectivity": [ + "Succeeded", + "Failed", + "TimedOut" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} + } + }, + "runAfter": { + "Initialize_Bearer_Result": [ + "Succeeded" + ] + } + }, + "Compose_Authentication_Model_Result": { + "type": "Compose", + "inputs": "@if(or(empty(outputs('Compose_Authentication_Methods')), not(equals(length(intersection(outputs('Compose_Authentication_Methods'), createArray('Bearer', 'OAuth2ClientCredentials', 'WorkloadIdentityFederation'))), length(outputs('Compose_Authentication_Methods')))), not(and(or(not(contains(outputs('Compose_Authentication_Methods'), 'OAuth2ClientCredentials')), equals(variables('ValidateCredentialsTestOutputs')?['overallResult'], 'PASSED')), or(not(contains(outputs('Compose_Authentication_Methods'), 'WorkloadIdentityFederation')), equals(variables('FederatedIdentityTestOutputs')?['overallResult'], 'PASSED')), or(not(contains(outputs('Compose_Authentication_Methods'), 'Bearer')), equals(variables('BearerConnectivityTestOutputs')?['overallResult'], 'PASSED'))))), 'FAILED', if(or(contains(outputs('Compose_Authentication_Methods'), 'OAuth2ClientCredentials'), contains(outputs('Compose_Authentication_Methods'), 'WorkloadIdentityFederation')), 'PASSED', 'NOT_GALLERY_READY'))", + "runAfter": { + "Authentication_Tests": [ + "Succeeded", + "Failed", + "Skipped", + "TimedOut" + ] + } + }, + "Return_Authentication_Results": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "body": { + "ValidateCredentialsTestOutputs": "@variables('ValidateCredentialsTestOutputs')", + "FederatedIdentityTestOutputs": "@variables('FederatedIdentityTestOutputs')", + "BearerConnectivityTestOutputs": "@variables('BearerConnectivityTestOutputs')", + "authenticationModelResult": "@outputs('Compose_Authentication_Model_Result')", + "supportedAuthenticationMethods": "@outputs('Compose_Authentication_Methods')", + "authenticationPolicy": "AllSelected", + "logicAppVersion": "@parameters('LogicAppVersion')" + } + }, + "runAfter": { + "Compose_Authentication_Model_Result": [ + "Succeeded" + ] + } + } + }, + "outputs": {} + }, + "kind": "Stateful" +} diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/GroupTests_Workflow.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/GroupTests_Workflow.json index 5322cd3b..21e8c06a 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/GroupTests_Workflow.json +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/GroupTests_Workflow.json @@ -64,154 +64,18 @@ } }, "Initialize_All_Group_Test_Variables": { - "runAfter": {}, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_20": [ + "Succeeded" + ] + }, "type": "InitializeVariable", "inputs": { "variables": [ { - "name": "CreateGroupFailedActionName", - "type": "string", - "value": "" - }, - { - "name": "CreateGroupFailedActionResponse", - "type": "Object", - "value": {} - }, - { - "name": "CreateGroupScopeExecutionStatus", - "type": "string", - "value": "InProgress" - }, - { - "name": "CreateGroupTestOutputs", - "type": "Object", - "value": { - "overallResult": "SKIPPED", - "result": "SKIPPED", - "provisioningLogsResult": "", - "scimCheckResult": "", - "errorDetails": { - "provisioningLogs": "", - "scimCheck": "" - } - } - }, - { - "name": "UpdateGroupFailedActionName", - "type": "string", - "value": "" - }, - { - "name": "UpdateGroupFailedActionResponse", - "type": "Object", - "value": {} - }, - { - "name": "UpdateGroupScopeExecutionStatus", - "type": "string", - "value": "InProgress" - }, - { - "name": "UpdateGroupTestOutputs", - "type": "Object", - "value": { - "overallResult": "SKIPPED", - "result": "SKIPPED", - "provisioningLogsResult": "", - "scimCheckResult": "", - "errorDetails": { - "provisioningLogs": "", - "scimCheck": "" - } - } - }, - { - "name": "DeleteGroupFailedActionName", - "type": "string", - "value": "" - }, - { - "name": "DeleteGroupFailedActionResponse", - "type": "Object", - "value": {} - }, - { - "name": "DeleteGroupScopeExecutionStatus", - "type": "string", - "value": "InProgress" - }, - { - "name": "DeleteGroupTestOutputs", - "type": "Object", - "value": { - "overallResult": "SKIPPED", - "result": "SKIPPED", - "provisioningLogsResult": "", - "scimCheckResult": "", - "errorDetails": { - "provisioningLogs": "", - "scimCheck": "" - } - } - }, - { - "name": "AddMemberFailedActionName", - "type": "string", - "value": "" - }, - { - "name": "AddMemberFailedActionResponse", - "type": "Object", - "value": {} - }, - { - "name": "AddMemberScopeExecutionStatus", - "type": "string", - "value": "InProgress" - }, - { - "name": "AddMemberTestOutputs", - "type": "Object", - "value": { - "overallResult": "SKIPPED", - "result": "SKIPPED", - "provisioningLogsResult": "", - "scimCheckResult": "", - "errorDetails": { - "provisioningLogs": "", - "scimCheck": "" - } - } - }, - { - "name": "RemoveMemberFailedActionName", - "type": "string", - "value": "" - }, - { - "name": "RemoveMemberFailedActionResponse", + "name": "SCIMAccessTokenCache", "type": "Object", "value": {} - }, - { - "name": "RemoveMemberScopeExecutionStatus", - "type": "string", - "value": "InProgress" - }, - { - "name": "RemoveMemberTestOutputs", - "type": "Object", - "value": { - "overallResult": "SKIPPED", - "result": "SKIPPED", - "provisioningLogsResult": "", - "scimCheckResult": "", - "errorDetails": { - "provisioningLogs": "", - "scimCheck": "" - } - } } ] } @@ -239,23 +103,13 @@ }, "Initialize_RestoreGroupTestVariables": { "runAfter": { - "Initialize_PODGroupTestOutputs": [ + "Initialize_RestoreGroupTestVariables_Variable_2": [ "Succeeded" ] }, "type": "InitializeVariable", "inputs": { "variables": [ - { - "name": "restoreEntraGroupId", - "type": "string", - "value": "" - }, - { - "name": "restoreGroupAppRoleAssignmentId", - "type": "string", - "value": "" - }, { "name": "RestoreGroupTestOutputs", "type": "Object", @@ -335,9 +189,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['createGroup'], '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Create_Group_Initial_Verify_Group_Exists": [ + "Succeeded" + ] } }, "Create_Group_Create_Group_Graph": { @@ -401,9 +260,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['createGroup'], '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Create_Group_Query_Create_Group": [ + "Succeeded" + ] } }, "Delay_Create_Group_15s": { @@ -420,6 +284,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_Create_Group_Query_Create_Group": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_IM_Token_Create_Group_Query_Create_Group": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_IM_Token_Create_Group_Query_Create_Group": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_IM_Token_Create_Group_Query_Create_Group')" + }, + "runAfter": { + "Acquire_Token_IM_Token_Create_Group_Query_Create_Group": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -493,6 +399,48 @@ "unit": "Minute" } } + }, + "Ensure_SCIM_Token_Create_Group_Initial_Verify_Group_Exists": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Create_Group_Initial_Verify_Group_Exists": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Create_Group_Initial_Verify_Group_Exists": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Create_Group_Initial_Verify_Group_Exists')" + }, + "runAfter": { + "Acquire_Token_Create_Group_Initial_Verify_Group_Exists": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "type": "Scope" @@ -694,9 +642,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['updateGroup'], '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Update_Group_Initial_Verify_Group_Exists": [ + "Succeeded" + ] } }, "Update_Group_Create_Group_Graph": { @@ -760,9 +713,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['updateGroup'], '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Update_Group_Check_Group_Create": [ + "Succeeded" + ] } }, "Update_Group_Delay_Creation_15s": { @@ -779,6 +737,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_Update_Group_Check_Group_Create": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_IM_Token_Update_Group_Check_Group_Create": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_IM_Token_Update_Group_Check_Group_Create": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_IM_Token_Update_Group_Check_Group_Create')" + }, + "runAfter": { + "Acquire_Token_IM_Token_Update_Group_Check_Group_Create": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -850,6 +850,48 @@ "unit": "Minute" } } + }, + "Ensure_SCIM_Token_Update_Group_Initial_Verify_Group_Exists": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Update_Group_Initial_Verify_Group_Exists": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Update_Group_Initial_Verify_Group_Exists": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Update_Group_Initial_Verify_Group_Exists')" + }, + "runAfter": { + "Acquire_Token_Update_Group_Initial_Verify_Group_Exists": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "type": "Scope" @@ -993,10 +1035,15 @@ "uri": "@concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['updateGroup'], '%22')", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } - } + }, + "runAfter": { + "Ensure_SCIM_Token_Update_Group_Query_Group_By_Id": [ + "Succeeded" + ] + } }, "Update_Group_Update_Group_Attributes": { "runAfter": { @@ -1030,9 +1077,14 @@ "uri": "@concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', concat(triggerBody()?['initializationData']?['testGroupNames']?['updateGroup'], '-Modified'), '%22')", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Update_Group_Query_Group_By_Id_After_Attributes_Update": [ + "Succeeded" + ] } }, "Update_Group_Delay_Update_15s": { @@ -1049,6 +1101,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_Update_Group_Query_Group_By_Id_After_Attributes_Update": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_uery_Group_By_Id_After_Attributes_Update": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_uery_Group_By_Id_After_Attributes_Update": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_uery_Group_By_Id_After_Attributes_Update')" + }, + "runAfter": { + "Acquire_Token_uery_Group_By_Id_After_Attributes_Update": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -1108,6 +1202,48 @@ "timeout": "PT15M" }, "type": "Until" + }, + "Ensure_SCIM_Token_Update_Group_Query_Group_By_Id": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_CIM_Token_Update_Group_Query_Group_By_Id": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_CIM_Token_Update_Group_Query_Group_By_Id": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_CIM_Token_Update_Group_Query_Group_By_Id')" + }, + "runAfter": { + "Acquire_Token_CIM_Token_Update_Group_Query_Group_By_Id": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "type": "Scope" @@ -1336,9 +1472,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['deleteGroup'], '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Delete_Step1_Check_Group_Exists": [ + "Succeeded" + ] } }, "Delete_Step2_Create_Group": { @@ -1399,9 +1540,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['deleteGroup'], '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Delete_Step3_Fetch_Group_By_Filter": [ + "Succeeded" + ] } }, "Delete_Group_Delay_Creation_15s": { @@ -1418,6 +1564,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_Delete_Step3_Fetch_Group_By_Filter": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Token_Delete_Step3_Fetch_Group_By_Filter": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Token_Delete_Step3_Fetch_Group_By_Filter": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Token_Delete_Step3_Fetch_Group_By_Filter')" + }, + "runAfter": { + "Acquire_Token_Token_Delete_Step3_Fetch_Group_By_Filter": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -1489,6 +1677,48 @@ "unit": "Minute" } } + }, + "Ensure_SCIM_Token_Delete_Step1_Check_Group_Exists": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_IM_Token_Delete_Step1_Check_Group_Exists": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_IM_Token_Delete_Step1_Check_Group_Exists": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_IM_Token_Delete_Step1_Check_Group_Exists')" + }, + "runAfter": { + "Acquire_Token_IM_Token_Delete_Step1_Check_Group_Exists": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "type": "Scope" @@ -1632,9 +1862,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['deleteGroup'], '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Delete_Step4_Query_Group_By_Id": [ + "Succeeded" + ] } }, "Delete_Step5_Delete_Group_By_Id": { @@ -1662,9 +1897,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['deleteGroup'], '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Delete_Step6_Check_Group_Exists_Final": [ + "Succeeded" + ] } }, "Delete_Group_Delay_Final_15s": { @@ -1681,6 +1921,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_Delete_Step6_Check_Group_Exists_Final": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_en_Delete_Step6_Check_Group_Exists_Final": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_en_Delete_Step6_Check_Group_Exists_Final": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_en_Delete_Step6_Check_Group_Exists_Final')" + }, + "runAfter": { + "Acquire_Token_en_Delete_Step6_Check_Group_Exists_Final": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -1740,6 +2022,48 @@ "timeout": "PT15M" }, "type": "Until" + }, + "Ensure_SCIM_Token_Delete_Step4_Query_Group_By_Id": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_CIM_Token_Delete_Step4_Query_Group_By_Id": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_CIM_Token_Delete_Step4_Query_Group_By_Id": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_CIM_Token_Delete_Step4_Query_Group_By_Id')" + }, + "runAfter": { + "Acquire_Token_CIM_Token_Delete_Step4_Query_Group_By_Id": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "type": "Scope" @@ -1950,9 +2274,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['memberGroup'], '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Add_Step4_Check_Group_And_User": [ + "Succeeded" + ] } }, "Add_Step4_Check_User": { @@ -1961,9 +2290,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['memberUser1'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Add_Step4_Check_User": [ + "Succeeded" + ] } }, "Add_Delay_Initial_15s": { @@ -1984,6 +2318,90 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_Add_Step4_Check_Group_And_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_CIM_Token_Add_Step4_Check_Group_And_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_CIM_Token_Add_Step4_Check_Group_And_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_CIM_Token_Add_Step4_Check_Group_And_User')" + }, + "runAfter": { + "Acquire_Token_CIM_Token_Add_Step4_Check_Group_And_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} + }, + "Ensure_SCIM_Token_Add_Step4_Check_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Ensure_SCIM_Token_Add_Step4_Check_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Ensure_SCIM_Token_Add_Step4_Check_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Ensure_SCIM_Token_Add_Step4_Check_User')" + }, + "runAfter": { + "Acquire_Token_Ensure_SCIM_Token_Add_Step4_Check_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -2194,9 +2612,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['memberGroup'], '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Add_Step1_Check_Group_Exists": [ + "Succeeded" + ] } }, "Delay_Before_Add_Group_AppRole": { @@ -2212,6 +2635,48 @@ "unit": "Minute" } } + }, + "Ensure_SCIM_Token_Add_Step1_Check_Group_Exists": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token__SCIM_Token_Add_Step1_Check_Group_Exists": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token__SCIM_Token_Add_Step1_Check_Group_Exists": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token__SCIM_Token_Add_Step1_Check_Group_Exists')" + }, + "runAfter": { + "Acquire_Token__SCIM_Token_Add_Step1_Check_Group_Exists": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "type": "Scope" @@ -2271,9 +2736,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['memberGroup'], '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Add_Step6_Query_Group_With_Members": [ + "Succeeded" + ] } }, "Add_Delay_Member_15s": { @@ -2290,6 +2760,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_Add_Step6_Query_Group_With_Members": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Token_Add_Step6_Query_Group_With_Members": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Token_Add_Step6_Query_Group_With_Members": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Token_Add_Step6_Query_Group_With_Members')" + }, + "runAfter": { + "Acquire_Token_Token_Add_Step6_Query_Group_With_Members": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -2306,10 +2818,8 @@ }, "Add_Step7_Query_Group_By_Id": { "runAfter": { - "Add_DoUntil_Wait_For_Member_Addition": [ - "Succeeded", - "TimedOut", - "Failed" + "Ensure_SCIM_Token_Add_Step7_Query_Group_By_Id": [ + "Succeeded" ] }, "type": "Http", @@ -2317,7 +2827,7 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['memberGroup'], '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } @@ -2366,32 +2876,80 @@ "timeout": "PT15M" }, "type": "Until" - } - }, - "type": "Scope" - }, - "Capture_AddMember_AddMemberPhase_Failed_Actions": { - "actions": { - "Get_Failed_AddMember_UpdatePhase_Actions": { - "type": "Compose", - "inputs": "@result('Add_Member_Verify_Update_And_Provisioning')" }, - "Filter_Failed_AddMember_UpdatePhase_Actions": { - "runAfter": { - "Get_Failed_AddMember_UpdatePhase_Actions": [ - "Succeeded" - ] - }, - "type": "Query", - "inputs": { - "from": "@outputs('Get_Failed_AddMember_UpdatePhase_Actions')", - "where": "@equals(item()?['status'], 'Failed')" - } - }, - "Set_AddMember_UpdatePhase_Failed_Action_Name": { - "runAfter": { - "Filter_Failed_AddMember_UpdatePhase_Actions": [ - "Succeeded" + "Ensure_SCIM_Token_Add_Step7_Query_Group_By_Id": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_e_SCIM_Token_Add_Step7_Query_Group_By_Id": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_e_SCIM_Token_Add_Step7_Query_Group_By_Id": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_e_SCIM_Token_Add_Step7_Query_Group_By_Id')" + }, + "runAfter": { + "Acquire_Token_e_SCIM_Token_Add_Step7_Query_Group_By_Id": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Add_DoUntil_Wait_For_Member_Addition": [ + "Succeeded", + "TimedOut", + "Failed" + ] + } + } + }, + "type": "Scope" + }, + "Capture_AddMember_AddMemberPhase_Failed_Actions": { + "actions": { + "Get_Failed_AddMember_UpdatePhase_Actions": { + "type": "Compose", + "inputs": "@result('Add_Member_Verify_Update_And_Provisioning')" + }, + "Filter_Failed_AddMember_UpdatePhase_Actions": { + "runAfter": { + "Get_Failed_AddMember_UpdatePhase_Actions": [ + "Succeeded" + ] + }, + "type": "Query", + "inputs": { + "from": "@outputs('Get_Failed_AddMember_UpdatePhase_Actions')", + "where": "@equals(item()?['status'], 'Failed')" + } + }, + "Set_AddMember_UpdatePhase_Failed_Action_Name": { + "runAfter": { + "Filter_Failed_AddMember_UpdatePhase_Actions": [ + "Succeeded" ] }, "type": "SetVariable", @@ -2716,9 +3274,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['memberGroup'], '-Remove%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Remove_Step4a_Check_Group_With_Member": [ + "Succeeded" + ] } }, "Remove_Step4b_Check_User": { @@ -2727,9 +3290,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['memberUser2'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Remove_Step4b_Check_User": [ + "Succeeded" + ] } }, "Remove_Delay_Initial_15s": { @@ -2750,6 +3318,90 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_Remove_Step4a_Check_Group_With_Member": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_en_Remove_Step4a_Check_Group_With_Member": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_en_Remove_Step4a_Check_Group_With_Member": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_en_Remove_Step4a_Check_Group_With_Member')" + }, + "runAfter": { + "Acquire_Token_en_Remove_Step4a_Check_Group_With_Member": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} + }, + "Ensure_SCIM_Token_Remove_Step4b_Check_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_sure_SCIM_Token_Remove_Step4b_Check_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_sure_SCIM_Token_Remove_Step4b_Check_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_sure_SCIM_Token_Remove_Step4b_Check_User')" + }, + "runAfter": { + "Acquire_Token_sure_SCIM_Token_Remove_Step4b_Check_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -2982,9 +3634,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['memberGroup'], '-Remove%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Remove_Step1_Check_Group_Exists": [ + "Succeeded" + ] } }, "Delay_Before_Remove_Group_AppRole": { @@ -3000,6 +3657,48 @@ "unit": "Minute" } } + }, + "Ensure_SCIM_Token_Remove_Step1_Check_Group_Exists": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_IM_Token_Remove_Step1_Check_Group_Exists": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_IM_Token_Remove_Step1_Check_Group_Exists": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_IM_Token_Remove_Step1_Check_Group_Exists')" + }, + "runAfter": { + "Acquire_Token_IM_Token_Remove_Step1_Check_Group_Exists": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "type": "Scope" @@ -3053,9 +3752,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', triggerBody()?['initializationData']?['testGroupNames']?['memberGroup'], '-Remove%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Remove_Step6_Query_Group_After_Remove": [ + "Succeeded" + ] } }, "Remove_Delay_Final_15s": { @@ -3072,6 +3776,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_Remove_Step6_Query_Group_After_Remove": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_en_Remove_Step6_Query_Group_After_Remove": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_en_Remove_Step6_Query_Group_After_Remove": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_en_Remove_Step6_Query_Group_After_Remove')" + }, + "runAfter": { + "Acquire_Token_en_Remove_Step6_Query_Group_After_Remove": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -3088,10 +3834,8 @@ }, "Remove_Step7_Query_Group_By_Id": { "runAfter": { - "Remove_DoUntil_Wait_For_Member_Removal": [ - "Succeeded", - "TimedOut", - "Failed" + "Ensure_SCIM_Token_Remove_Step7_Query_Group_By_Id": [ + "Succeeded" ] }, "type": "Http", @@ -3099,7 +3843,7 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', concat(triggerBody()?['initializationData']?['testGroupNames']?['memberGroup'], '-Remove'), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } @@ -3148,6 +3892,54 @@ "timeout": "PT15M" }, "type": "Until" + }, + "Ensure_SCIM_Token_Remove_Step7_Query_Group_By_Id": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_CIM_Token_Remove_Step7_Query_Group_By_Id": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_CIM_Token_Remove_Step7_Query_Group_By_Id": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_CIM_Token_Remove_Step7_Query_Group_By_Id')" + }, + "runAfter": { + "Acquire_Token_CIM_Token_Remove_Step7_Query_Group_By_Id": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Remove_DoUntil_Wait_For_Member_Removal": [ + "Succeeded", + "TimedOut", + "Failed" + ] + } } }, "type": "Scope" @@ -3514,7 +4306,7 @@ }, "POD_SCIM_Pre_Check_Group": { "runAfter": { - "POD_Create_Graph_Group": [ + "Ensure_SCIM_Token_POD_SCIM_Pre_Check_Group": [ "Succeeded" ] }, @@ -3523,7 +4315,7 @@ "method": "GET", "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', body('POD_Create_Graph_Group')?['displayName'], '%22')}", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } @@ -3649,7 +4441,7 @@ }, "POD_SCIM_Get_Group": { "runAfter": { - "POD_Group_Delay_After_Provision": [ + "Ensure_SCIM_Token_POD_SCIM_Get_Group": [ "Succeeded" ] }, @@ -3658,7 +4450,7 @@ "method": "GET", "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', body('POD_Create_Graph_Group')?['displayName'], '%22')}", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } @@ -3707,6 +4499,98 @@ } } } + }, + "Ensure_SCIM_Token_POD_SCIM_Pre_Check_Group": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_sure_SCIM_Token_POD_SCIM_Pre_Check_Group": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_sure_SCIM_Token_POD_SCIM_Pre_Check_Group": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_sure_SCIM_Token_POD_SCIM_Pre_Check_Group')" + }, + "runAfter": { + "Acquire_Token_sure_SCIM_Token_POD_SCIM_Pre_Check_Group": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "POD_Create_Graph_Group": [ + "Succeeded" + ] + } + }, + "Ensure_SCIM_Token_POD_SCIM_Get_Group": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Ensure_SCIM_Token_POD_SCIM_Get_Group": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Ensure_SCIM_Token_POD_SCIM_Get_Group": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Ensure_SCIM_Token_POD_SCIM_Get_Group')" + }, + "runAfter": { + "Acquire_Token_Ensure_SCIM_Token_POD_SCIM_Get_Group": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "POD_Group_Delay_After_Provision": [ + "Succeeded" + ] + } } } }, @@ -3756,10 +4640,8 @@ }, "POD_Group_Cleanup_SCIM_Delete": { "runAfter": { - "Set_PODGroupTestOutputs": [ - "Succeeded", - "Failed", - "Skipped" + "Ensure_SCIM_Token_POD_Group_Cleanup_SCIM_Delete": [ + "Succeeded" ] }, "type": "Http", @@ -3767,7 +4649,7 @@ "method": "DELETE", "uri": "@{concat(parameters('scimEndpoint'), '/Groups/', coalesce(first(body('POD_SCIM_Get_Group')?['Resources'])?['id'], 'not-found'))}", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } @@ -3807,28 +4689,76 @@ "audience": "https://graph.microsoft.com/" } } - } - }, - "runAfter": {}, - "expression": { - "and": [ - { - "equals": [ - "@coalesce(triggerBody()?['initializationData']?['isGroupSupported'], false)", - true - ] - }, - { - "or": [ - { - "equals": [ - "@triggerBody()?['EnabledTests']", - "All" - ] - }, - { - "equals": [ - "@triggerBody()?['EnabledTests']", + }, + "Ensure_SCIM_Token_POD_Group_Cleanup_SCIM_Delete": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_SCIM_Token_POD_Group_Cleanup_SCIM_Delete": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_SCIM_Token_POD_Group_Cleanup_SCIM_Delete": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_SCIM_Token_POD_Group_Cleanup_SCIM_Delete')" + }, + "runAfter": { + "Acquire_Token_SCIM_Token_POD_Group_Cleanup_SCIM_Delete": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Set_PODGroupTestOutputs": [ + "Succeeded", + "Failed", + "Skipped" + ] + } + } + }, + "runAfter": {}, + "expression": { + "and": [ + { + "equals": [ + "@coalesce(triggerBody()?['initializationData']?['isGroupSupported'], false)", + true + ] + }, + { + "or": [ + { + "equals": [ + "@triggerBody()?['EnabledTests']", + "All" + ] + }, + { + "equals": [ + "@triggerBody()?['EnabledTests']", "GroupTests" ] }, @@ -3859,9 +4789,14 @@ "method": "GET", "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', encodeUriComponent(coalesce(triggerBody()?['initializationData']?['testGroupNames']?['restoreGroup'], '')), '%22')}", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_RestoreGroupTest_SCIM_Pre_Check": [ + "Succeeded" + ] } }, "RestoreGroupTest_Create_Entra_Group": { @@ -3975,9 +4910,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', encodeUriComponent(body('RestoreGroupTest_Create_Entra_Group')?['displayName']), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_RestoreGroupTest_SCIM_Check_Created": [ + "Succeeded" + ] } }, "RestoreGroupTest_Delay_Create_15s": { @@ -3994,6 +4934,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_RestoreGroupTest_SCIM_Check_Created": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_oken_RestoreGroupTest_SCIM_Check_Created": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_oken_RestoreGroupTest_SCIM_Check_Created": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_oken_RestoreGroupTest_SCIM_Check_Created')" + }, + "runAfter": { + "Acquire_Token_oken_RestoreGroupTest_SCIM_Check_Created": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } } }, @@ -4041,6 +5023,48 @@ } } } + }, + "Ensure_SCIM_Token_RestoreGroupTest_SCIM_Pre_Check": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_IM_Token_RestoreGroupTest_SCIM_Pre_Check": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_IM_Token_RestoreGroupTest_SCIM_Pre_Check": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_IM_Token_RestoreGroupTest_SCIM_Pre_Check')" + }, + "runAfter": { + "Acquire_Token_IM_Token_RestoreGroupTest_SCIM_Pre_Check": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } } }, @@ -4207,9 +5231,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', encodeUriComponent(body('RestoreGroupTest_Create_Entra_Group')?['displayName']), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_RestoreGroupTest_SCIM_Check_Gone": [ + "Succeeded" + ] } }, "RestoreGroupTest_Delay_Gone_15s": { @@ -4226,6 +5255,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_RestoreGroupTest_SCIM_Check_Gone": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_M_Token_RestoreGroupTest_SCIM_Check_Gone": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_M_Token_RestoreGroupTest_SCIM_Check_Gone": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_M_Token_RestoreGroupTest_SCIM_Check_Gone')" + }, + "runAfter": { + "Acquire_Token_M_Token_RestoreGroupTest_SCIM_Check_Gone": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } } }, @@ -4449,9 +5520,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?filter=displayName%20eq%20%22', encodeUriComponent(body('RestoreGroupTest_Create_Entra_Group')?['displayName']), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_RestoreGroupTest_SCIM_Check_Restored": [ + "Succeeded" + ] } }, "RestoreGroupTest_Delay_Restored_15s": { @@ -4468,6 +5544,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_RestoreGroupTest_SCIM_Check_Restored": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_ken_RestoreGroupTest_SCIM_Check_Restored": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_ken_RestoreGroupTest_SCIM_Check_Restored": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_ken_RestoreGroupTest_SCIM_Check_Restored')" + }, + "runAfter": { + "Acquire_Token_ken_RestoreGroupTest_SCIM_Check_Restored": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } } }, @@ -4788,6 +5906,421 @@ "status": "@if(or(or(or(or(or(equals(variables('CreateGroupTestOutputs')['overallResult'], 'FAILED'), equals(variables('UpdateGroupTestOutputs')['overallResult'], 'FAILED')), equals(variables('DeleteGroupTestOutputs')['overallResult'], 'FAILED')), equals(variables('AddMemberTestOutputs')['overallResult'], 'FAILED')), equals(variables('RemoveMemberTestOutputs')['overallResult'], 'FAILED')), equals(variables('PODGroupTestOutputs')['overallResult'], 'FAILED')), 'Failed', 'Passed')", "workflowRunId": "@workflow().run.name" } + }, + "Initialize_All_Group_Test_Variables_Variable_1": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "CreateGroupFailedActionName", + "type": "string", + "value": "" + } + ] + }, + "runAfter": {} + }, + "Initialize_All_Group_Test_Variables_Variable_2": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "CreateGroupFailedActionResponse", + "type": "Object", + "value": {} + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_1": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_3": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "CreateGroupScopeExecutionStatus", + "type": "string", + "value": "InProgress" + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_2": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_4": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "CreateGroupTestOutputs", + "type": "Object", + "value": { + "overallResult": "SKIPPED", + "result": "SKIPPED", + "provisioningLogsResult": "", + "scimCheckResult": "", + "errorDetails": { + "provisioningLogs": "", + "scimCheck": "" + } + } + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_3": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_5": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "UpdateGroupFailedActionName", + "type": "string", + "value": "" + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_4": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_6": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "UpdateGroupFailedActionResponse", + "type": "Object", + "value": {} + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_5": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_7": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "UpdateGroupScopeExecutionStatus", + "type": "string", + "value": "InProgress" + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_6": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_8": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "UpdateGroupTestOutputs", + "type": "Object", + "value": { + "overallResult": "SKIPPED", + "result": "SKIPPED", + "provisioningLogsResult": "", + "scimCheckResult": "", + "errorDetails": { + "provisioningLogs": "", + "scimCheck": "" + } + } + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_7": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_9": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "DeleteGroupFailedActionName", + "type": "string", + "value": "" + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_8": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_10": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "DeleteGroupFailedActionResponse", + "type": "Object", + "value": {} + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_9": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_11": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "DeleteGroupScopeExecutionStatus", + "type": "string", + "value": "InProgress" + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_10": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_12": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "DeleteGroupTestOutputs", + "type": "Object", + "value": { + "overallResult": "SKIPPED", + "result": "SKIPPED", + "provisioningLogsResult": "", + "scimCheckResult": "", + "errorDetails": { + "provisioningLogs": "", + "scimCheck": "" + } + } + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_11": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_13": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "AddMemberFailedActionName", + "type": "string", + "value": "" + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_12": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_14": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "AddMemberFailedActionResponse", + "type": "Object", + "value": {} + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_13": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_15": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "AddMemberScopeExecutionStatus", + "type": "string", + "value": "InProgress" + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_14": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_16": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "AddMemberTestOutputs", + "type": "Object", + "value": { + "overallResult": "SKIPPED", + "result": "SKIPPED", + "provisioningLogsResult": "", + "scimCheckResult": "", + "errorDetails": { + "provisioningLogs": "", + "scimCheck": "" + } + } + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_15": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_17": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "RemoveMemberFailedActionName", + "type": "string", + "value": "" + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_16": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_18": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "RemoveMemberFailedActionResponse", + "type": "Object", + "value": {} + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_17": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_19": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "RemoveMemberScopeExecutionStatus", + "type": "string", + "value": "InProgress" + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_18": [ + "Succeeded" + ] + } + }, + "Initialize_All_Group_Test_Variables_Variable_20": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "RemoveMemberTestOutputs", + "type": "Object", + "value": { + "overallResult": "SKIPPED", + "result": "SKIPPED", + "provisioningLogsResult": "", + "scimCheckResult": "", + "errorDetails": { + "provisioningLogs": "", + "scimCheck": "" + } + } + } + ] + }, + "runAfter": { + "Initialize_All_Group_Test_Variables_Variable_19": [ + "Succeeded" + ] + } + }, + "Initialize_RestoreGroupTestVariables_Variable_1": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "restoreEntraGroupId", + "type": "string", + "value": "" + } + ] + }, + "runAfter": { + "Initialize_PODGroupTestOutputs": [ + "Succeeded" + ] + } + }, + "Initialize_RestoreGroupTestVariables_Variable_2": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "restoreGroupAppRoleAssignmentId", + "type": "string", + "value": "" + } + ] + }, + "runAfter": { + "Initialize_RestoreGroupTestVariables_Variable_1": [ + "Succeeded" + ] + } } }, "outputs": {} diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Initialization_Workflow.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Initialization_Workflow.json index 439563b9..5469c1df 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Initialization_Workflow.json +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Initialization_Workflow.json @@ -16,59 +16,14 @@ }, "actions": { "Initialize_Processing_Variables": { - "runAfter": {}, + "runAfter": { + "Initialize_Processing_Variables_Variable_6": [ + "Succeeded" + ] + }, "type": "InitializeVariable", "inputs": { "variables": [ - { - "name": "sourceToTargetLookup", - "type": "Array", - "value": [] - }, - { - "name": "userBody", - "type": "Object", - "value": { - "accountEnabled": true, - "passwordProfile": { - "forceChangePasswordNextSignIn": true, - "password": "@{concat('Tmp!', substring(guid(),0,8), '@', substring(guid(),24,8))}" - } - } - }, - { - "name": "updateUserBody", - "type": "Object", - "value": {} - }, - { - "name": "ExcludedList", - "type": "Array", - "value": [ - "IsSoftDeleted", - "accountEnabled", - "passwordProfile", - "manager", - "mail", - "userPrincipalName", - "id" - ] - }, - { - "name": "GraphArrayProperties", - "type": "Array", - "value": [ - "businessPhones", - "otherMails", - "proxyAddresses", - "imAddresses" - ] - }, - { - "name": "scimDirectUserCreateBody", - "type": "Object", - "value": {} - }, { "name": "additionalFunctionSources", "type": "Array", @@ -1273,6 +1228,123 @@ "isVerbosePatchSupported": "@outputs('Compose_IsVerbosePatchSupported')" } } + }, + "Initialize_Processing_Variables_Variable_1": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "sourceToTargetLookup", + "type": "Array", + "value": [] + } + ] + }, + "runAfter": {} + }, + "Initialize_Processing_Variables_Variable_2": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "userBody", + "type": "Object", + "value": { + "accountEnabled": true, + "passwordProfile": { + "forceChangePasswordNextSignIn": true, + "password": "@{concat('Tmp!', substring(guid(),0,8), '@', substring(guid(),24,8))}" + } + } + } + ] + }, + "runAfter": { + "Initialize_Processing_Variables_Variable_1": [ + "Succeeded" + ] + } + }, + "Initialize_Processing_Variables_Variable_3": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "updateUserBody", + "type": "Object", + "value": {} + } + ] + }, + "runAfter": { + "Initialize_Processing_Variables_Variable_2": [ + "Succeeded" + ] + } + }, + "Initialize_Processing_Variables_Variable_4": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "ExcludedList", + "type": "Array", + "value": [ + "IsSoftDeleted", + "accountEnabled", + "passwordProfile", + "manager", + "mail", + "userPrincipalName", + "id" + ] + } + ] + }, + "runAfter": { + "Initialize_Processing_Variables_Variable_3": [ + "Succeeded" + ] + } + }, + "Initialize_Processing_Variables_Variable_5": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "GraphArrayProperties", + "type": "Array", + "value": [ + "businessPhones", + "otherMails", + "proxyAddresses", + "imAddresses" + ] + } + ] + }, + "runAfter": { + "Initialize_Processing_Variables_Variable_4": [ + "Succeeded" + ] + } + }, + "Initialize_Processing_Variables_Variable_6": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimDirectUserCreateBody", + "type": "Object", + "value": {} + } + ] + }, + "runAfter": { + "Initialize_Processing_Variables_Variable_5": [ + "Succeeded" + ] + } } }, "outputs": {} diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json index ed7d6bc5..fe6c9c76 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Parameters.json @@ -34,12 +34,31 @@ "type": "String", "value": "All" }, + "supportedAuthenticationMethods": { + "metadata": { + "description": "Authentication methods available for this validation: Bearer, OAuth2ClientCredentials, WorkloadIdentityFederation. The agent must confirm this list. An empty list preserves legacy inference from configured token endpoints." + }, + "type": "Array", + "value": [] + }, + "scimAuthenticationMethod": { + "allowedValues": [ + "Bearer", + "OAuth2ClientCredentials", + "WorkloadIdentityFederation" + ], + "metadata": { + "description": "Authentication for direct SCIM requests. OAuth2ClientCredentials and WorkloadIdentityFederation acquire and refresh access tokens at runtime and never fall back to scimBearerToken. Bearer uses a caller-supplied access token." + }, + "type": "String", + "value": "Bearer" + }, "LogicAppVersion": { "metadata": { - "description": "Version of the Logic App for tracking and deployment purposes" + "description": "Overall bundle and agent version, copied from the same-bundle VERSION file." }, "type": "String", - "value": "10.0" + "value": "14.0" }, "attributeTransforms": { "metadata": { @@ -251,54 +270,54 @@ "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." - }, - "type": "String", - "value": "changeme-tenant-id" + "metadata": { + "description": "Entra tenant ID used to construct the token endpoint URL for 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-tenant-id" + }, + "federatedApplicationId": { + "metadata": { + "description": "OAuth2 Application ID 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-id" + }, + "federatedApplicationClientSecret": { + "metadata": { + "description": "OAuth2 Client Secret for the Entra app registration used in federated identity token acquisition." }, - "federatedTokenEndpoint": { - "metadata": { - "description": "ISV token endpoint URL for federated identity token exchange." - }, - "type": "String", - "value": "changeme-federated-endpoint" + "type": "String", + "value": "changeme-client-secret" + }, + "federatedTokenEndpoint": { + "metadata": { + "description": "ISV token endpoint URL for 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-endpoint" + }, + "federatedClientId": { + "metadata": { + "description": "ISV client ID used in federated identity token exchange." }, - "federatedBaseAddress": { - "metadata": { - "description": "ISV base address sent in the federated identity token exchange request." - }, - "type": "String", - "value": "changeme-base-address" + "type": "String", + "value": "changeme-federated-app-id" + }, + "federatedBaseAddress": { + "metadata": { + "description": "ISV base address sent in the federated identity token exchange request." }, - "federatedAudience": { - "metadata": { - "description": "Audience value for federated token exchange." - }, - "type": "String", - "value": "changeme-audience" + "type": "String", + "value": "changeme-base-address" + }, + "federatedAudience": { + "metadata": { + "description": "Audience value for federated token exchange." }, + "type": "String", + "value": "changeme-audience" + }, "servicePrincipalId": { "type": "String", "value": "3197053d-46d9-4a22-b843-6a9d98ec267e" diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Workflow.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Workflow.json index 3d2a3c1b..d7ac9e9f 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Workflow.json +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Orchestrator_Workflow.json @@ -504,9 +504,13 @@ "GroupTests_Workflow": "@outputs('Compose_GroupTests_RunLink')", "SCIMTests_Workflow": "@outputs('Compose_SCIMTests_RunLink')" }, - "overallLogicAppResult": "@if(or(not(equals(coalesce(body('Get_UserTests_Output_Content')?['CreateUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_UserTests_Output_Content')?['UpdateUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_UserTests_Output_Content')?['DisableUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isManagerAttributeSupported'],false),not(equals(coalesce(body('Get_UserTests_Output_Content')?['ManagerTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_UserTests_Output_Content')?['RestoreUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMNullUpdateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['CreateGroupTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['UpdateGroupTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['AddMemberTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['RemoveMemberTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SchemaDiscoverabilityTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserCreateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupCreateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserUpdateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupUpdateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserPaginationTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_UserTests_Output_Content')?['PODUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['PODGroupTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),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(equals(coalesce(body('Get_SCIMTests_Output_Content')?['authenticationModelResult'],'FAILED'),'FAILED'),and(coalesce(body('Call_Initialization_Workflow')?['isManagerAttributeSupported'],false),contains(string(coalesce(body('Call_Initialization_Workflow')?['targetDirectoryUserAttributes'],json('[]'))),'manager'),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUpdateManagerTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))))),'Failed',if(equals(coalesce(body('Poll_SCIMTests_Status')?['properties']?['status'],''),'Failed'),'Failed','Succeeded'))", + "overallLogicAppResult": "@if(or(not(equals(coalesce(body('Get_UserTests_Output_Content')?['CreateUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_UserTests_Output_Content')?['UpdateUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_UserTests_Output_Content')?['DisableUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isManagerAttributeSupported'],false),not(equals(coalesce(body('Get_UserTests_Output_Content')?['ManagerTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_UserTests_Output_Content')?['RestoreUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMNullUpdateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['CreateGroupTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['UpdateGroupTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['AddMemberTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['RemoveMemberTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SchemaDiscoverabilityTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserCreateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupCreateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserUpdateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupUpdateTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUserPaginationTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),not(equals(coalesce(body('Get_UserTests_Output_Content')?['PODUserTestOutputs']?['overallResult'],'SKIPPED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'],false),not(equals(coalesce(body('Get_GroupTests_Output_Content')?['PODGroupTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))),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(not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['authenticationModelResult'],'FAILED'),'PASSED')),and(coalesce(body('Call_Initialization_Workflow')?['isManagerAttributeSupported'],false),contains(string(coalesce(body('Call_Initialization_Workflow')?['targetDirectoryUserAttributes'],json('[]'))),'manager'),not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMUpdateManagerTestOutputs']?['overallResult'],'SKIPPED'),'PASSED'))))),'Failed',if(equals(coalesce(body('Poll_SCIMTests_Status')?['properties']?['status'],''),'Failed'),'Failed','Succeeded'))", "optionalSuccessTestsResult": "@if(or(not(equals(coalesce(body('Get_UserTests_Output_Content')?['DeleteUserTestOutputs']?['overallResult'], 'SKIPPED'), 'PASSED')), and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'], false), not(equals(coalesce(body('Get_GroupTests_Output_Content')?['DeleteGroupTestOutputs']?['overallResult'], 'SKIPPED'), 'PASSED'))), and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'], false), not(equals(coalesce(body('Get_GroupTests_Output_Content')?['RestoreGroupTestOutputs']?['overallResult'], 'SKIPPED'), 'PASSED'))), and(coalesce(body('Call_Initialization_Workflow')?['isGroupSupported'], false), not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['SCIMGroupPaginationTestOutputs']?['overallResult'], 'SKIPPED'), 'PASSED')))), 'WARNING', 'PASSED')", - "authenticationModelResult": "@coalesce(body('Get_SCIMTests_Output_Content')?['authenticationModelResult'], 'FAILED')" + "authenticationModelResult": "@coalesce(body('Get_SCIMTests_Output_Content')?['authenticationModelResult'], 'FAILED')", + "supportedAuthenticationMethods": "@coalesce(body('Get_SCIMTests_Output_Content')?['supportedAuthenticationMethods'], json('[]'))", + "logicAppVersion": "@parameters('LogicAppVersion')", + "authenticationPolicy": "AllSelected", + "BearerConnectivityTestOutputs": "@coalesce(body('Get_SCIMTests_Output_Content')?['BearerConnectivityTestOutputs'], json('{\"overallResult\":\"FAILED\",\"result\":\"FAILED - authentication child output unavailable\"}'))" } }, "Final_TestResults": { @@ -673,21 +677,25 @@ }, { "testName": "Validate_Credentials_Test", - "testCategory": "Mandatory/StrictOneSuccess", + "testCategory": "Declared/StrictSuccess", "testResult": "@coalesce(body('Get_SCIMTests_Output_Content')?['ValidateCredentialsTestOutputs']?['result'], 'SKIPPED')", "provisioningErrorDetails": "@if(equals(coalesce(body('Get_SCIMTests_Output_Content')?['ValidateCredentialsTestOutputs']?['overallResult'], 'SKIPPED'), 'FAILED'), body('Get_SCIMTests_Output_Content')?['ValidateCredentialsTestOutputs']?['errorDetails'], '')", "recommendationUrl": "@if(equals(coalesce(body('Get_SCIMTests_Output_Content')?['ValidateCredentialsTestOutputs']?['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')?['ValidateCredentialsTestOutputs']?['overallResult'], 'SKIPPED'), 'SKIPPED'), '', outputs('Compose_SCIMTests_RunLink'))", - "message": "@if(equals(coalesce(body('Get_SCIMTests_Output_Content')?['ValidateCredentialsTestOutputs']?['overallResult'], 'SKIPPED'), 'SKIPPED'), '', 'Click the runLink and search for the action Compose_Final_Results for more info.')" + "message": "@if(equals(coalesce(body('Get_SCIMTests_Output_Content')?['ValidateCredentialsTestOutputs']?['overallResult'], 'SKIPPED'), 'SKIPPED'), '', 'Click the runLink and search for the action Compose_Final_Results for more info.')", + "authenticationMethod": "OAuth2ClientCredentials", + "applicability": "@if(contains(coalesce(body('Get_SCIMTests_Output_Content')?['supportedAuthenticationMethods'], json('[]')), 'OAuth2ClientCredentials'), 'applicable', 'notApplicable')" }, { "testName": "Federated_Identity_Test", - "testCategory": "Mandatory/StrictOneSuccess", + "testCategory": "Declared/StrictSuccess", "testResult": "@coalesce(body('Get_SCIMTests_Output_Content')?['FederatedIdentityTestOutputs']?['result'], 'SKIPPED')", "provisioningErrorDetails": "@if(equals(coalesce(body('Get_SCIMTests_Output_Content')?['FederatedIdentityTestOutputs']?['overallResult'], 'SKIPPED'), 'FAILED'), body('Get_SCIMTests_Output_Content')?['FederatedIdentityTestOutputs']?['errorDetails'], '')", "recommendationUrl": "@if(equals(coalesce(body('Get_SCIMTests_Output_Content')?['FederatedIdentityTestOutputs']?['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')?['FederatedIdentityTestOutputs']?['overallResult'], 'SKIPPED'), 'SKIPPED'), '', outputs('Compose_SCIMTests_RunLink'))", - "message": "@if(equals(coalesce(body('Get_SCIMTests_Output_Content')?['FederatedIdentityTestOutputs']?['overallResult'], 'SKIPPED'), 'SKIPPED'), '', 'Click the runLink and search for the action Compose_Final_Results for more info.')" + "message": "@if(equals(coalesce(body('Get_SCIMTests_Output_Content')?['FederatedIdentityTestOutputs']?['overallResult'], 'SKIPPED'), 'SKIPPED'), '', 'Click the runLink and search for the action Compose_Final_Results for more info.')", + "authenticationMethod": "WorkloadIdentityFederation", + "applicability": "@if(contains(coalesce(body('Get_SCIMTests_Output_Content')?['supportedAuthenticationMethods'], json('[]')), 'WorkloadIdentityFederation'), 'applicable', 'notApplicable')" }, { "testName": "SCIM_User_Pagination_Test", @@ -751,7 +759,11 @@ "SCIMTests_Workflow": "@outputs('Compose_SCIMTests_RunLink')" }, "authenticationModelResult": "@outputs('Evaluate_Test_Results')?['authenticationModelResult']", - "overallResult": "@outputs('Evaluate_Test_Results')?['overallLogicAppResult']" + "overallResult": "@outputs('Evaluate_Test_Results')?['overallLogicAppResult']", + "supportedAuthenticationMethods": "@outputs('Evaluate_Test_Results')?['supportedAuthenticationMethods']", + "logicAppVersion": "@parameters('LogicAppVersion')", + "authenticationPolicy": "AllSelected", + "BearerConnectivityTestOutputs": "@coalesce(body('Get_SCIMTests_Output_Content')?['BearerConnectivityTestOutputs'], json('{\"overallResult\":\"FAILED\",\"result\":\"FAILED - authentication child output unavailable\"}'))" } }, "Terminate_With_Test_Status": { @@ -769,4 +781,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 3a05925a..c8c21a0b 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIM-Validation-Test-Overview.md +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIM-Validation-Test-Overview.md @@ -4,7 +4,7 @@ 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. +The Logic App is deployed as a Standard Azure Logic App with **seven workflows**. The three test suites execute in parallel after initialization. A full run commonly takes 30–90 minutes because provisioning schedules and consistency waits dominate elapsed time. The same-bundle [VERSION](VERSION) file supplies the overall and agent version; `LogicAppVersion` must match it. --- @@ -15,10 +15,23 @@ 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 — 11 tests (parallel with User/Group) - └── Final_TestResults — aggregates 25 results, determines pass/fail + └── SCIMTests_Workflow — 11 results (parallel with User/Group) + └── Authentication_Workflow — selected OAuth/WIF checks and optional static-token probe +Final_TestResults — Orchestrator aggregates 25 results +SCIMToken_Workflow — shared refreshable OAuth/WIF transport + called by User/Group/SCIM suites ``` +Authentication is declared before credential collection. The selected transport authenticates direct SCIM requests; the declared methods determine which authentication tests apply: + +```text +Declared capabilities -> Selected auth tests -> Every selected method passes? +Selected transport -> Core SCIM tests -> Mandatory checks pass? + Both results -> Readiness + sanitized report +``` + +Deploy `Authentication_Workflow.json` and `SCIMToken_Workflow.json` alongside the other five workflows and the complete `Orchestrator_Parameters.json`. Authentication is a synchronous child called once by SCIMTests; its OAuth/WIF results retain their existing test names. The helpers do not add a 26th test. A selected static-token probe is a separate prerequisite result. Each suite caches the selected OAuth/WIF token and refreshes it when empty or within five minutes of expiry. Token acquisition failures do not fall back to a static token. Entra's provisioning credentials are configured separately and must use a supported method too. + **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 | @@ -93,21 +106,32 @@ Tests that don't apply are reported as **SKIPPED** (not failures). However, grou 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. +- **`Succeeded`** — all applicable mandatory functional tests passed, every selected authentication method passed, and at least one gallery method was selected. Only documented capability skips are acceptable. +- **`Failed`** — any applicable mandatory functional test failed, any selected authentication method failed or did not execute, or no gallery method was selected. Missing authentication output fails closed. -### Authentication Model (OR logic) +### Authentication Model (AND across selected methods) -The authentication result uses **OR logic** across two tests: +`Authentication_Workflow` evaluates all selected methods and returns `authenticationPolicy: "AllSelected"`. `SCIMTests_Workflow.Compose_Final_Results` explicitly forwards `authenticationModelResult`. With both gallery methods selected, the rule is: | Validate_Credentials_Test | Federated_Identity_Test | Auth Model Result | |---------------------------|------------------------|-------------------| | PASSED | PASSED | **PASSED** | -| PASSED | FAILED/SKIPPED | **PASSED** | -| FAILED/SKIPPED | PASSED | **PASSED** | +| PASSED | FAILED/SKIPPED/missing | **FAILED** | +| FAILED/SKIPPED/missing | PASSED | **FAILED** | | 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. +Every selected method must pass. An undeclared method is **NOT_APPLICABLE**, issues no token requests, and does not participate in the AND check. If both are selected and one fails, authentication fails. A selected but unexecuted method is not a success. Invalid method names fail closed. The SCIM adapter normalizes duplicate declarations to a distinct set; direct calls to the authentication child reject duplicates. A selected static-token method must also pass its connectivity probe, but cannot satisfy gallery readiness by itself. + +| Environment | Declaration | Direct transport | Authentication result | +|-------------|-------------|------------------|-----------------------| +| OAuth only | `["OAuth2ClientCredentials"]` | `OAuth2ClientCredentials` | OAuth pass is sufficient; WIF Not applicable | +| WIF only | `["WorkloadIdentityFederation"]` | `WorkloadIdentityFederation` | WIF pass is sufficient; OAuth Not applicable | +| Both | Both gallery values | Ask which to use for core requests | Both must pass | +| Static token only | `["Bearer"]` | `Bearer` | `NOT_GALLERY_READY`; functional testing only | + +Set `scimAuthenticationMethod` separately from `supportedAuthenticationMethods`. OAuth access tokens travel in an HTTP Bearer header; this does **not** declare support for a caller-supplied static bearer token. Set `scimBearerToken` to `""` for OAuth-only or WIF-only runs and clear undeclared credentials. + +For older manual deployments only, an empty declaration array infers OAuth/WIF from nonempty token endpoints, or Bearer when neither endpoint is configured and a static token is supplied. New agent-driven runs always record an explicit declaration. ### Optional Tests (WARNING, not FAIL) @@ -126,8 +150,10 @@ These 4 tests are `testCategory: optional`. When they fail, the `testResult` fie |------|---------------|------------| | 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 | +| Validate_Credentials_Test | OAuth is not declared | NOT_APPLICABLE — no OAuth token request | +| Federated_Identity_Test | WIF is not declared | NOT_APPLICABLE — no federation token requests | + +Missing inputs for a **declared** method must be supplied or reported as blocked/failed, not silently converted to Not applicable. ### Group Tests Are Mandatory @@ -143,16 +169,25 @@ This applies to: Create_Group, Update_Group, Group_Update_Add_Member, Group_Upda |----------|---------------|--------| | 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 | +| OAuth declared + Validate_Credentials passes, federation undeclared | **Succeeded** | Ready — federation Not applicable | +| Only federation declared + Federated_Identity passes, OAuth Not applicable | **Succeeded** | Ready when other mandatory checks pass | +| Both declared, either authentication test fails/skips | **Failed** | Fix every selected method; never silently remove one | | 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 | ---- +## Authentication regression checks +Run `.\Test-Authentication.ps1` after changing authentication behavior. +The suite evaluates the actual workflow expressions and direct-request +token-cache wiring. Live federation still requires a federation-capable +environment; offline exchange parity is not a live authentication result. + +See [Authentication-Changes.md](Authentication-Changes.md) for the design +and the boundary between authentication readiness and functional readiness. + +--- ## How to Run ### Method A: AI Agent (Automated, 30–60 min) @@ -176,4 +211,4 @@ Both methods produce the same output: a `validation-result-.json` file to --- -*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.* +*Bundle and agent version: see [VERSION](VERSION). Covers 25 reportable tests across seven workflows. Includes User & Group Pagination, Restore tests, Schema_Discoverability_Test, SCIM_Update_Manager_Test, Federated_Identity_Test, Validate_Credentials_Test, scimTargetUserValues, and Provision on Demand.* diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_Workflow.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_Workflow.json index 690ec0a4..8f565c33 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_Workflow.json +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMTests_Workflow.json @@ -83,272 +83,31 @@ } }, "Initialize_SCIM_Test_Variables": { - "runAfter": {}, + "runAfter": { + "Initialize_SCIM_Test_Variables_Variable_15": [ + "Succeeded" + ] + }, "type": "InitializeVariable", "inputs": { "variables": [ { - "name": "SchemaDiscoverabilityTestOutputs", - "type": "Object", - "value": { - "overallResult": "SKIPPED", - "result": "SKIPPED", - "summary": null, - "detailedResults": [], - "errorDetails": { - "summary": null - } - } - }, - { - "name": "SCIMNullUpdateFailedActionName", - "type": "string", - "value": "" - }, - { - "name": "SCIMNullUpdateScopeExecutionStatus", - "type": "string", - "value": "InProgress" - }, - { - "name": "SCIMNullUpdateTestOutputs", - "type": "Object", - "value": { - "overallResult": "SKIPPED", - "result": "SKIPPED", - "summary": null, - "detailedResults": [], - "errorDetails": { - "summary": null - } - } - }, - { - "name": "scimNullPatchOperations", - "type": "Array", - "value": [] - }, - { - "name": "scimNullUpdateResults", - "type": "Array", - "value": [] - }, - { - "name": "scimNullCreateBody", - "type": "Object", - "value": {} - }, - { - "name": "ValidateCredentialsTestOutputs", - "type": "Object", - "value": { - "overallResult": "SKIPPED", - "result": "SKIPPED", - "summary": null, - "detailedResults": [], - "errorDetails": { - "summary": null - } - } - }, - { - "name": "FederatedIdentityTestOutputs", - "type": "Object", - "value": { - "overallResult": "SKIPPED", - "result": "SKIPPED", - "summary": null, - "detailedResults": [], - "errorDetails": { - "summary": null - } - } - }, - { - "name": "flattenedUserAttrNames", - "type": "Array", - "value": [] - }, - { - "name": "flattenedGroupAttrNames", - "type": "Array", - "value": [] - }, - { - "name": "scimDiscoveredGroupBody", - "type": "Object", - "value": {} - }, - { - "name": "scimDiscoveredGroupWritableAttributes", - "type": "Array", - "value": [] - }, - { - "name": "scimCurrentGroupComplexBody", + "name": "SCIMAccessTokenCache", "type": "Object", "value": {} - }, - { - "name": "scimGroupEmailExtensionUrn", - "type": "String", - "value": "" } ] } }, "Initialize_SCIM_Test_Variables_2": { "runAfter": { - "Initialize_SCIM_Test_Variables": [ + "Initialize_SCIM_Test_Variables_2_Variable_19": [ "Succeeded" ] }, "type": "InitializeVariable", "inputs": { "variables": [ - { - "name": "SCIMUserCreateTestOutputs", - "type": "Object", - "value": { - "overallResult": "SKIPPED", - "result": "SKIPPED", - "summary": null, - "detailedResults": [], - "errorDetails": { - "summary": null - } - } - }, - { - "name": "SCIMGroupCreateTestOutputs", - "type": "Object", - "value": { - "overallResult": "SKIPPED", - "result": "SKIPPED", - "summary": null, - "detailedResults": [], - "errorDetails": { - "summary": null - } - } - }, - { - "name": "scimDirectCreatedUserId", - "type": "string", - "value": "" - }, - { - "name": "scimDirectCreatedGroupId", - "type": "string", - "value": "" - }, - { - "name": "scimDirectUserCreateBody", - "type": "Object", - "value": {} - }, - { - "name": "scimDirectGroupCreateBody", - "type": "Object", - "value": {} - }, - { - "name": "SCIMUserUpdateTestOutputs", - "type": "Object", - "value": { - "overallResult": "SKIPPED", - "result": "SKIPPED", - "summary": null, - "detailedResults": [], - "errorDetails": { - "summary": null - } - } - }, - { - "name": "SCIMGroupUpdateTestOutputs", - "type": "Object", - "value": { - "overallResult": "SKIPPED", - "result": "SKIPPED", - "summary": null, - "detailedResults": [], - "errorDetails": { - "summary": null - } - } - }, - { - "name": "scimDirectUpdateUserId", - "type": "string", - "value": "" - }, - { - "name": "scimDirectUpdateGroupId", - "type": "string", - "value": "" - }, - { - "name": "SCIMUserPaginationTestOutputs", - "type": "Object", - "value": { - "overallResult": "SKIPPED", - "result": "SKIPPED", - "summary": null, - "detailedResults": [], - "errorDetails": { - "summary": null - } - } - }, - { - "name": "scimUserPaginationCreatedIds", - "type": "Array", - "value": [] - }, - { - "name": "scimUserPaginationNextUrl", - "type": "string", - "value": "" - }, - { - "name": "scimUserPaginationPageCount", - "type": "Integer", - "value": 0 - }, - { - "name": "scimUserPaginationResults", - "type": "Array", - "value": [] - }, - { - "value": { - "overallResult": "SKIPPED", - "result": "SKIPPED", - "summary": null, - "detailedResults": [], - "errorDetails": { - "summary": null - } - }, - "name": "SCIMGroupPaginationTestOutputs", - "type": "Object" - }, - { - "value": [], - "name": "scimGroupPaginationCreatedIds", - "type": "Array" - }, - { - "value": "", - "name": "scimGroupPaginationNextUrl", - "type": "string" - }, - { - "value": 0, - "name": "scimGroupPaginationPageCount", - "type": "Integer" - }, { "value": [], "name": "scimGroupPaginationResults", @@ -368,17 +127,25 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Schemas')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Get_Schemas": [ + "Succeeded" + ] } }, "Set_SchemaTestOutputs_On_Failure": { "runAfter": { - "SCIM_Get_Schemas": ["Failed", "TimedOut"] - }, - "type": "SetVariable", - "inputs": { + "SCIM_Get_Schemas": [ + "Failed", + "TimedOut" + ] + }, + "type": "SetVariable", + "inputs": { "name": "SchemaDiscoverabilityTestOutputs", "value": { "overallResult": "FAILED", @@ -1449,6 +1216,48 @@ "repetitions": 1 } } + }, + "Ensure_SCIM_Token_SCIM_Get_Schemas": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Ensure_SCIM_Token_SCIM_Get_Schemas": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Ensure_SCIM_Token_SCIM_Get_Schemas": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Ensure_SCIM_Token_SCIM_Get_Schemas')" + }, + "runAfter": { + "Acquire_Token_Ensure_SCIM_Token_SCIM_Get_Schemas": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "else": { @@ -1536,7 +1345,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:'), 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'))))))" + "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": { @@ -1565,7 +1374,7 @@ }, "SCIM_Create_Null_Test_User": { "runAfter": { - "ForEach_Convert_To_Arrays": [ + "Ensure_SCIM_Token_SCIM_Create_Null_Test_User": [ "Succeeded" ] }, @@ -1574,7 +1383,7 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users')}", "method": "POST", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, @@ -1606,7 +1415,7 @@ }, "SCIM_Get_User_Before_Null_Update": { "runAfter": { - "Delay_Before_SCIM_Get_User": [ + "Ensure_SCIM_Token_SCIM_Get_User_Before_Null_Update": [ "Succeeded" ] }, @@ -1615,14 +1424,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users/', outputs('Compose_SCIM_Null_User_Id'))}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } }, "SCIM_Get_Schemas_For_Null_Test": { "runAfter": { - "Compose_SCIM_Null_User_Id": [ + "Ensure_SCIM_Token_SCIM_Get_Schemas_For_Null_Test": [ "Succeeded" ] }, @@ -1631,7 +1440,7 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Schemas')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } @@ -1805,7 +1614,7 @@ }, "SCIM_Patch_Null_Update": { "runAfter": { - "Compose_SCIM_Null_Patch_Body": [ + "Ensure_SCIM_Token_SCIM_Patch_Null_Update": [ "Succeeded" ] }, @@ -1814,7 +1623,7 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users/', outputs('Compose_SCIM_Null_User_Id'))}", "method": "PATCH", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, @@ -1837,7 +1646,7 @@ }, "SCIM_Get_User_After_Null_Update": { "runAfter": { - "Delay_After_SCIM_Null_Update": [ + "Ensure_SCIM_Token_SCIM_Get_User_After_Null_Update": [ "Succeeded" ] }, @@ -1846,7 +1655,7 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users/', outputs('Compose_SCIM_Null_User_Id'))}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } @@ -2008,7 +1817,7 @@ }, "SCIM_Patch_Null_Update_Phase2": { "runAfter": { - "Compose_Null_Patch_Request_Phase2": [ + "Ensure_SCIM_Token_SCIM_Patch_Null_Update_Phase2": [ "Succeeded" ] }, @@ -2017,7 +1826,7 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users/', outputs('Compose_SCIM_Null_User_Id'))}", "method": "PATCH", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, @@ -2040,7 +1849,7 @@ }, "SCIM_Get_User_After_Null_Phase2": { "runAfter": { - "Delay_After_SCIM_Null_Update_Phase2": [ + "Ensure_SCIM_Token_SCIM_Get_User_After_Null_Phase2": [ "Succeeded" ] }, @@ -2049,7 +1858,7 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users/', outputs('Compose_SCIM_Null_User_Id'))}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } @@ -2240,6 +2049,328 @@ "repetitions": 1 } } + }, + "Ensure_SCIM_Token_SCIM_Create_Null_Test_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_re_SCIM_Token_SCIM_Create_Null_Test_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_re_SCIM_Token_SCIM_Create_Null_Test_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_re_SCIM_Token_SCIM_Create_Null_Test_User')" + }, + "runAfter": { + "Acquire_Token_re_SCIM_Token_SCIM_Create_Null_Test_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "ForEach_Convert_To_Arrays": [ + "Succeeded" + ] + } + }, + "Ensure_SCIM_Token_SCIM_Get_User_Before_Null_Update": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_M_Token_SCIM_Get_User_Before_Null_Update": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_M_Token_SCIM_Get_User_Before_Null_Update": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_M_Token_SCIM_Get_User_Before_Null_Update')" + }, + "runAfter": { + "Acquire_Token_M_Token_SCIM_Get_User_Before_Null_Update": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Delay_Before_SCIM_Get_User": [ + "Succeeded" + ] + } + }, + "Ensure_SCIM_Token_SCIM_Get_Schemas_For_Null_Test": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_CIM_Token_SCIM_Get_Schemas_For_Null_Test": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_CIM_Token_SCIM_Get_Schemas_For_Null_Test": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_CIM_Token_SCIM_Get_Schemas_For_Null_Test')" + }, + "runAfter": { + "Acquire_Token_CIM_Token_SCIM_Get_Schemas_For_Null_Test": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Compose_SCIM_Null_User_Id": [ + "Succeeded" + ] + } + }, + "Ensure_SCIM_Token_SCIM_Patch_Null_Update": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Ensure_SCIM_Token_SCIM_Patch_Null_Update": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Ensure_SCIM_Token_SCIM_Patch_Null_Update": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Ensure_SCIM_Token_SCIM_Patch_Null_Update')" + }, + "runAfter": { + "Acquire_Token_Ensure_SCIM_Token_SCIM_Patch_Null_Update": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Compose_SCIM_Null_Patch_Body": [ + "Succeeded" + ] + } + }, + "Ensure_SCIM_Token_SCIM_Get_User_After_Null_Update": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_IM_Token_SCIM_Get_User_After_Null_Update": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_IM_Token_SCIM_Get_User_After_Null_Update": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_IM_Token_SCIM_Get_User_After_Null_Update')" + }, + "runAfter": { + "Acquire_Token_IM_Token_SCIM_Get_User_After_Null_Update": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Delay_After_SCIM_Null_Update": [ + "Succeeded" + ] + } + }, + "Ensure_SCIM_Token_SCIM_Patch_Null_Update_Phase2": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_SCIM_Token_SCIM_Patch_Null_Update_Phase2": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_SCIM_Token_SCIM_Patch_Null_Update_Phase2": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_SCIM_Token_SCIM_Patch_Null_Update_Phase2')" + }, + "runAfter": { + "Acquire_Token_SCIM_Token_SCIM_Patch_Null_Update_Phase2": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Compose_Null_Patch_Request_Phase2": [ + "Succeeded" + ] + } + }, + "Ensure_SCIM_Token_SCIM_Get_User_After_Null_Phase2": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_IM_Token_SCIM_Get_User_After_Null_Phase2": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_IM_Token_SCIM_Get_User_After_Null_Phase2": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_IM_Token_SCIM_Get_User_After_Null_Phase2')" + }, + "runAfter": { + "Acquire_Token_IM_Token_SCIM_Get_User_After_Null_Phase2": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Delay_After_SCIM_Null_Update_Phase2": [ + "Succeeded" + ] + } } }, "type": "Scope" @@ -2367,14 +2498,61 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users/', outputs('Compose_SCIM_Null_User_Id'))}", "method": "DELETE", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Delete_Null_Test_User": [ + "Succeeded" + ] } - } - }, - "else": { - "actions": {} + }, + "Ensure_SCIM_Token_SCIM_Delete_Null_Test_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_re_SCIM_Token_SCIM_Delete_Null_Test_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_re_SCIM_Token_SCIM_Delete_Null_Test_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_re_SCIM_Token_SCIM_Delete_Null_Test_User')" + }, + "runAfter": { + "Acquire_Token_re_SCIM_Token_SCIM_Delete_Null_Test_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} + } + }, + "else": { + "actions": {} } } }, @@ -2422,7 +2600,7 @@ }, "SCIM_Direct_Create_User": { "runAfter": { - "Compose_SCIM_Create_Body": [ + "Ensure_SCIM_Token_SCIM_Direct_Create_User": [ "Succeeded" ] }, @@ -2431,7 +2609,7 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users')}", "method": "POST", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, @@ -2520,10 +2698,57 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users/', variables('scimDirectCreatedUserId'))}", "method": "DELETE", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Delete_Direct_Test_User": [ + "Succeeded" + ] } + }, + "Ensure_SCIM_Token_SCIM_Delete_Direct_Test_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token__SCIM_Token_SCIM_Delete_Direct_Test_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token__SCIM_Token_SCIM_Delete_Direct_Test_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token__SCIM_Token_SCIM_Delete_Direct_Test_User')" + }, + "runAfter": { + "Acquire_Token__SCIM_Token_SCIM_Delete_Direct_Test_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "else": { @@ -2538,6 +2763,52 @@ }, "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_SCIM_Direct_UserName'))" + }, + "Ensure_SCIM_Token_SCIM_Direct_Create_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_nsure_SCIM_Token_SCIM_Direct_Create_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_nsure_SCIM_Token_SCIM_Direct_Create_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_nsure_SCIM_Token_SCIM_Direct_Create_User')" + }, + "runAfter": { + "Acquire_Token_nsure_SCIM_Token_SCIM_Direct_Create_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Compose_SCIM_Create_Body": [ + "Succeeded" + ] + } } }, "runAfter": {}, @@ -2586,7 +2857,7 @@ }, "SCIM_Direct_Create_Group": { "runAfter": { - "Compose_SCIM_Group_Create_Body": [ + "Ensure_SCIM_Token_SCIM_Direct_Create_Group": [ "Succeeded" ] }, @@ -2595,7 +2866,7 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups')}", "method": "POST", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, @@ -2684,10 +2955,57 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups/', variables('scimDirectCreatedGroupId'))}", "method": "DELETE", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Delete_Direct_Test_Group": [ + "Succeeded" + ] } + }, + "Ensure_SCIM_Token_SCIM_Delete_Direct_Test_Group": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_SCIM_Token_SCIM_Delete_Direct_Test_Group": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_SCIM_Token_SCIM_Delete_Direct_Test_Group": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_SCIM_Token_SCIM_Delete_Direct_Test_Group')" + }, + "runAfter": { + "Acquire_Token_SCIM_Token_SCIM_Delete_Direct_Test_Group": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "else": { @@ -2702,6 +3020,52 @@ }, "type": "Compose", "inputs": "@if(empty(variables('scimGroupEmailExtensionUrn')), setProperty(if(contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryGroupAttributes'], json('[]'))), 'externalId'), setProperty(variables('scimDiscoveredGroupBody'), 'externalId', guid()), variables('scimDiscoveredGroupBody')), 'displayName', outputs('Generate_SCIM_Direct_GroupName')), union(setProperty(if(contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryGroupAttributes'], json('[]'))), 'externalId'), setProperty(variables('scimDiscoveredGroupBody'), 'externalId', guid()), variables('scimDiscoveredGroupBody')), 'displayName', outputs('Generate_SCIM_Direct_GroupName')), json(concat('{\"', variables('scimGroupEmailExtensionUrn'), '\":', string(setProperty(coalesce(variables('scimDiscoveredGroupBody')?[variables('scimGroupEmailExtensionUrn')], json('{}')), 'email', concat('scim-group-', substring(guid(),0,8), '@', parameters('testUserDomain')))), '}'))))" + }, + "Ensure_SCIM_Token_SCIM_Direct_Create_Group": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_sure_SCIM_Token_SCIM_Direct_Create_Group": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_sure_SCIM_Token_SCIM_Direct_Create_Group": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_sure_SCIM_Token_SCIM_Direct_Create_Group')" + }, + "runAfter": { + "Acquire_Token_sure_SCIM_Token_SCIM_Direct_Create_Group": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Compose_SCIM_Group_Create_Body": [ + "Succeeded" + ] + } } }, "runAfter": {}, @@ -2746,7 +3110,7 @@ }, "SCIM_Update_Create_User": { "runAfter": { - "Compose_SCIM_Update_Create_Body": [ + "Ensure_SCIM_Token_SCIM_Update_Create_User": [ "Succeeded" ] }, @@ -2755,7 +3119,7 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users')}", "method": "POST", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, @@ -2930,11 +3294,16 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users/', variables('scimDirectUpdateUserId'))}", "method": "PATCH", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, "body": "@outputs('Compose_Update_Patch_Body')" + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Update_Patch_User": [ + "Succeeded" + ] } }, "Delay_SCIM_Update_User_After_Patch": { @@ -2953,7 +3322,7 @@ }, "SCIM_Update_Get_User": { "runAfter": { - "Delay_SCIM_Update_User_After_Patch": [ + "Ensure_SCIM_Token_SCIM_Update_Get_User": [ "Succeeded" ] }, @@ -2962,7 +3331,7 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users/', variables('scimDirectUpdateUserId'))}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } @@ -2996,6 +3365,94 @@ "from": "@body('Compose_Update_Verification')", "where": "@equals(item()?['match'], false)" } + }, + "Ensure_SCIM_Token_SCIM_Update_Patch_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Ensure_SCIM_Token_SCIM_Update_Patch_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Ensure_SCIM_Token_SCIM_Update_Patch_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Ensure_SCIM_Token_SCIM_Update_Patch_User')" + }, + "runAfter": { + "Acquire_Token_Ensure_SCIM_Token_SCIM_Update_Patch_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} + }, + "Ensure_SCIM_Token_SCIM_Update_Get_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Ensure_SCIM_Token_SCIM_Update_Get_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Ensure_SCIM_Token_SCIM_Update_Get_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Ensure_SCIM_Token_SCIM_Update_Get_User')" + }, + "runAfter": { + "Acquire_Token_Ensure_SCIM_Token_SCIM_Update_Get_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Delay_SCIM_Update_User_After_Patch": [ + "Succeeded" + ] + } } }, "else": { @@ -3080,10 +3537,57 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users/', variables('scimDirectUpdateUserId'))}", "method": "DELETE", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Delete_Update_Test_User": [ + "Succeeded" + ] } + }, + "Ensure_SCIM_Token_SCIM_Delete_Update_Test_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token__SCIM_Token_SCIM_Delete_Update_Test_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token__SCIM_Token_SCIM_Delete_Update_Test_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token__SCIM_Token_SCIM_Delete_Update_Test_User')" + }, + "runAfter": { + "Acquire_Token__SCIM_Token_SCIM_Delete_Update_Test_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "else": { @@ -3098,29 +3602,75 @@ }, "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_SCIM_Update_UserName'))" - } - }, - "runAfter": {}, - "type": "Scope" - } - }, - "else": { - "actions": {} - }, - "expression": { - "or": [ - { - "equals": [ - "@triggerBody()?['EnabledTests']", - "All" - ] - }, - { - "equals": [ - "@triggerBody()?['EnabledTests']", - "SCIMTests" - ] - }, + }, + "Ensure_SCIM_Token_SCIM_Update_Create_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_nsure_SCIM_Token_SCIM_Update_Create_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_nsure_SCIM_Token_SCIM_Update_Create_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_nsure_SCIM_Token_SCIM_Update_Create_User')" + }, + "runAfter": { + "Acquire_Token_nsure_SCIM_Token_SCIM_Update_Create_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Compose_SCIM_Update_Create_Body": [ + "Succeeded" + ] + } + } + }, + "runAfter": {}, + "type": "Scope" + } + }, + "else": { + "actions": {} + }, + "expression": { + "or": [ + { + "equals": [ + "@triggerBody()?['EnabledTests']", + "All" + ] + }, + { + "equals": [ + "@triggerBody()?['EnabledTests']", + "SCIMTests" + ] + }, { "equals": [ "@triggerBody()?['EnabledTests']", @@ -3146,7 +3696,7 @@ }, "SCIM_GroupUpdate_Create_Group": { "runAfter": { - "Compose_GroupUpdate_Create_Body": [ + "Ensure_SCIM_Token_SCIM_GroupUpdate_Create_Group": [ "Succeeded" ] }, @@ -3155,7 +3705,7 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups')}", "method": "POST", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, @@ -3263,10 +3813,57 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups/', variables('scimDirectUpdateGroupId'))}", "method": "DELETE", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Delete_Update_Test_Group": [ + "Succeeded" + ] } + }, + "Ensure_SCIM_Token_SCIM_Delete_Update_Test_Group": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_SCIM_Token_SCIM_Delete_Update_Test_Group": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_SCIM_Token_SCIM_Delete_Update_Test_Group": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_SCIM_Token_SCIM_Delete_Update_Test_Group')" + }, + "runAfter": { + "Acquire_Token_SCIM_Token_SCIM_Delete_Update_Test_Group": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "else": { @@ -3350,11 +3947,16 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups/', variables('scimDirectUpdateGroupId'))}", "method": "PATCH", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, "body": "@outputs('Compose_GroupUpdate_Patch_Body')" + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_GroupUpdate_Patch_Group": [ + "Succeeded" + ] } }, "Delay_SCIM_GroupUpdate_After_Patch": { @@ -3373,7 +3975,7 @@ }, "SCIM_GroupUpdate_Get_Group": { "runAfter": { - "Delay_SCIM_GroupUpdate_After_Patch": [ + "Ensure_SCIM_Token_SCIM_GroupUpdate_Get_Group": [ "Succeeded" ] }, @@ -3382,7 +3984,7 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups/', variables('scimDirectUpdateGroupId'))}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } @@ -3416,10 +4018,144 @@ "from": "@body('Compose_GroupUpdate_Verification')", "where": "@equals(item()?['match'], false)" } + }, + "Ensure_SCIM_Token_SCIM_GroupUpdate_Patch_Group": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token__SCIM_Token_SCIM_GroupUpdate_Patch_Group": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token__SCIM_Token_SCIM_GroupUpdate_Patch_Group": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token__SCIM_Token_SCIM_GroupUpdate_Patch_Group')" + }, + "runAfter": { + "Acquire_Token__SCIM_Token_SCIM_GroupUpdate_Patch_Group": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} + }, + "Ensure_SCIM_Token_SCIM_GroupUpdate_Get_Group": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_re_SCIM_Token_SCIM_GroupUpdate_Get_Group": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_re_SCIM_Token_SCIM_GroupUpdate_Get_Group": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_re_SCIM_Token_SCIM_GroupUpdate_Get_Group')" + }, + "runAfter": { + "Acquire_Token_re_SCIM_Token_SCIM_GroupUpdate_Get_Group": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Delay_SCIM_GroupUpdate_After_Patch": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + } + }, + "Ensure_SCIM_Token_SCIM_GroupUpdate_Create_Group": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_SCIM_Token_SCIM_GroupUpdate_Create_Group": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_SCIM_Token_SCIM_GroupUpdate_Create_Group": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_SCIM_Token_SCIM_GroupUpdate_Create_Group')" + }, + "runAfter": { + "Acquire_Token_SCIM_Token_SCIM_GroupUpdate_Create_Group": [ + "Succeeded" + ] + } } }, "else": { "actions": {} + }, + "runAfter": { + "Compose_GroupUpdate_Create_Body": [ + "Succeeded" + ] } } }, @@ -3454,59 +4190,41 @@ }, "type": "If" }, - "Validate_Credentials_Test": { + "SCIM_User_Pagination_Test": { "runAfter": {}, + "type": "If", "actions": { - "Acquire_OAuth2_Token": { - "type": "Http", - "inputs": { - "uri": "@{parameters('scimTokenEndpoint')}", - "method": "POST", - "headers": "@if(equals(toLower(coalesce(parameters('scimCredentialLocationInRequest'), 'Header')), 'header'), json(concat('{\"Content-Type\":\"application/x-www-form-urlencoded\",\"Authorization\":\"Basic ', base64(concat(encodeUriComponent(parameters('scimClientId')), ':', encodeUriComponent(parameters('scimClientSecret')))), '\"}')), json('{\"Content-Type\":\"application/x-www-form-urlencoded\"}'))", - "body": "@{concat('grant_type=client_credentials', if(equals(toLower(coalesce(parameters('scimCredentialLocationInRequest'), 'Header')), 'body'), concat('&client_id=', encodeUriComponent(parameters('scimClientId')), '&client_secret=', encodeUriComponent(parameters('scimClientSecret'))), ''), if(empty(coalesce(parameters('scimOAuthScope'), '')), '', concat('&scope=', encodeUriComponent(parameters('scimOAuthScope')))))}" - } - }, - "Check_Token_Response": { - "runAfter": { - "Acquire_OAuth2_Token": [ - "Succeeded", - "Failed", - "TimedOut" - ] - }, - "type": "If", - "expression": { - "and": [ - { - "greaterOrEquals": [ - "@coalesce(outputs('Acquire_OAuth2_Token')?['statusCode'], 0)", - 200 - ] + "SCIM_User_Pagination_Test_Actions": { + "actions": { + "SCIM_User_Pagination_Check_Count": { + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users?startIndex=1&count=5')}", + "method": "GET", + "headers": { + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", + "Accept": "@{parameters('scimContentType')}" + } }, - { - "less": [ - "@coalesce(outputs('Acquire_OAuth2_Token')?['statusCode'], 0)", - 300 + "runAfter": { + "Ensure_SCIM_Token_SCIM_User_Pagination_Check_Count": [ + "Succeeded" ] - }, - { - "not": { - "equals": [ - "@coalesce(body('Acquire_OAuth2_Token')?['access_token'], '')", - "" - ] - } } - ] - }, - "actions": { - "Compose_Token_Validity_Minutes": { + }, + "Compose_User_Pagination_Users_To_Create": { + "runAfter": { + "SCIM_User_Pagination_Check_Count": [ + "Succeeded", + "Failed" + ] + }, "type": "Compose", - "inputs": "@div(coalesce(body('Acquire_OAuth2_Token')?['expires_in'], 3600), 60)" + "inputs": "@max(0, sub(11, coalesce(body('SCIM_User_Pagination_Check_Count')?['totalResults'], 0)))" }, - "Check_Token_Validity_Range": { + "Check_User_Pagination_Need_Create": { "runAfter": { - "Compose_Token_Validity_Minutes": [ + "Compose_User_Pagination_Users_To_Create": [ "Succeeded" ] }, @@ -3514,926 +4232,417 @@ "expression": { "and": [ { - "greaterOrEquals": [ - "@outputs('Compose_Token_Validity_Minutes')", - 60 - ] - }, - { - "lessOrEquals": [ - "@outputs('Compose_Token_Validity_Minutes')", - 360 + "greater": [ + "@outputs('Compose_User_Pagination_Users_To_Create')", + 0 ] } ] }, "actions": { - "SCIM_Validate_Get_Users": { - "type": "Http", - "inputs": { - "uri": "@{concat(parameters('scimEndpoint'), '/Users?startIndex=1&count=1')}", - "method": "GET", - "headers": { - "Authorization": "@{concat('Bearer ', body('Acquire_OAuth2_Token')?['access_token'])}", - "Accept": "@{parameters('scimContentType')}" - } - } - }, - "Compose_SCIM_Validation_Result": { - "runAfter": { - "SCIM_Validate_Get_Users": [ - "Succeeded", - "Failed", - "TimedOut" - ] - }, - "type": "Compose", - "inputs": { - "passed": "@and(greaterOrEquals(coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], 0), 200), less(coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], 0), 300))", - "statusCode": "@coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], 0)", - "endpoint": "@concat(parameters('scimEndpoint'), '/Users?startIndex=1&count=1')", - "overallResult": "@if(and(greaterOrEquals(coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], 0), 200), less(coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], 0), 300)), 'PASSED', 'FAILED')" - } - } - }, - "else": { - "actions": { - "Compose_Token_Validity_Failure": { - "type": "Compose", - "inputs": { - "passed": false, - "validityMinutes": "@outputs('Compose_Token_Validity_Minutes')", - "expiresInSeconds": "@coalesce(body('Acquire_OAuth2_Token')?['expires_in'], 3600)", - "reason": "@if(less(outputs('Compose_Token_Validity_Minutes'), 60), 'Token validity is less than 60 minutes. Minimum required: 60 minutes.', 'Token validity exceeds 360 minutes (6 hours). Maximum allowed: 360 minutes.')", - "overallResult": "FAILED" - } - } - } - } - } - }, - "else": { - "actions": { - "Compose_Token_Acquisition_Failure": { - "type": "Compose", - "inputs": { - "passed": false, - "statusCode": "@coalesce(outputs('Acquire_OAuth2_Token')?['statusCode'], 0)", - "hasAccessToken": "@not(equals(coalesce(body('Acquire_OAuth2_Token')?['access_token'], ''), ''))", - "tokenEndpoint": "@parameters('scimTokenEndpoint')", - "reason": "@if(or(less(coalesce(outputs('Acquire_OAuth2_Token')?['statusCode'], 0), 200), greaterOrEquals(coalesce(outputs('Acquire_OAuth2_Token')?['statusCode'], 0), 300)), concat('Token endpoint returned HTTP ', string(coalesce(outputs('Acquire_OAuth2_Token')?['statusCode'], 0)), '. Expected 2xx.'), 'Token endpoint returned 2xx but response did not contain an access_token field.')", - "overallResult": "FAILED" - } - } - } - } - }, - "Set_ValidateCredentialsTestOutputs": { - "runAfter": { - "Check_Token_Response": [ - "Succeeded", - "Failed", - "TimedOut", - "Skipped" - ] - }, - "type": "SetVariable", - "inputs": { - "name": "ValidateCredentialsTestOutputs", - "value": { - "overallResult": "@coalesce(outputs('Compose_SCIM_Validation_Result')?['overallResult'], outputs('Compose_Token_Validity_Failure')?['overallResult'], outputs('Compose_Token_Acquisition_Failure')?['overallResult'], 'FAILED')", - "result": "@if(equals(coalesce(outputs('Compose_SCIM_Validation_Result')?['overallResult'], outputs('Compose_Token_Validity_Failure')?['overallResult'], outputs('Compose_Token_Acquisition_Failure')?['overallResult'], 'FAILED'), 'PASSED'), 'success', concat('FAILED - Action: ', coalesce(if(equals(coalesce(outputs('Compose_SCIM_Validation_Result')?['passed'], true), false), 'SCIM_Validate_Get_Users', null), if(not(empty(coalesce(outputs('Compose_Token_Validity_Failure'), json('null')))), 'Check_Token_Validity_Range', null), 'Acquire_OAuth2_Token'), ' - ', coalesce(if(equals(coalesce(outputs('Compose_SCIM_Validation_Result')?['passed'], true), false), concat('SCIM /Users returned HTTP ', string(coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], 0)), '. Expected 2xx.'), null), outputs('Compose_Token_Validity_Failure')?['reason'], outputs('Compose_Token_Acquisition_Failure')?['reason'], 'Unexpected failure')))", - "summary": "@coalesce(outputs('Compose_SCIM_Validation_Result'), outputs('Compose_Token_Validity_Failure'), outputs('Compose_Token_Acquisition_Failure'))", - "detailedResults": { - "gate1_tokenAcquisition": { - "passed": "@not(equals(coalesce(outputs('Compose_Token_Validity_Minutes'), outputs('Compose_Token_Acquisition_Failure'), null), null))", - "statusCode": "@coalesce(outputs('Acquire_OAuth2_Token')?['statusCode'], 0)" - }, - "gate2_tokenLifetime": { - "passed": "@not(empty(coalesce(outputs('Compose_SCIM_Validation_Result'), outputs('Compose_Token_Validity_Failure'))))", - "validityMinutes": "@coalesce(outputs('Compose_Token_Validity_Minutes'), null)" - }, - "gate3_scimAccess": { - "passed": "@coalesce(outputs('Compose_SCIM_Validation_Result')?['passed'], false)", - "statusCode": "@coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], null)" + "ForEach_User_Pagination_Create": { + "type": "Foreach", + "foreach": "@range(0, outputs('Compose_User_Pagination_Users_To_Create'))", + "actions": { + "SCIM_User_Pagination_Create_User": { + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users')}", + "method": "POST", + "headers": { + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", + "Content-Type": "@{parameters('scimContentType')}", + "Accept": "@{parameters('scimContentType')}" + }, + "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)))" + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_User_Pagination_Create_User": [ + "Succeeded" + ] + } + }, + "Append_User_Pagination_UserId": { + "runAfter": { + "SCIM_User_Pagination_Create_User": [ + "Succeeded" + ] + }, + "type": "AppendToArrayVariable", + "inputs": { + "name": "scimUserPaginationCreatedIds", + "value": "@coalesce(body('SCIM_User_Pagination_Create_User')?['id'], '')" + } + }, + "Ensure_SCIM_Token_SCIM_User_Pagination_Create_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_M_Token_SCIM_User_Pagination_Create_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_M_Token_SCIM_User_Pagination_Create_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_M_Token_SCIM_User_Pagination_Create_User')" + }, + "runAfter": { + "Acquire_Token_M_Token_SCIM_User_Pagination_Create_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} + } + }, + "runtimeConfiguration": { + "concurrency": { + "repetitions": 5 + } + } } }, - "errorDetails": { - "failedAction": "@coalesce(if(equals(coalesce(outputs('Compose_SCIM_Validation_Result')?['passed'], true), false), 'SCIM_Validate_Get_Users', null), if(not(empty(coalesce(outputs('Compose_Token_Validity_Failure'), json('null')))), 'Check_Token_Validity_Range', null), if(not(empty(coalesce(outputs('Compose_Token_Acquisition_Failure'), json('null')))), 'Acquire_OAuth2_Token', null), null)", - "reason": "@coalesce(if(equals(coalesce(outputs('Compose_SCIM_Validation_Result')?['passed'], true), false), concat('SCIM /Users returned HTTP ', string(coalesce(outputs('SCIM_Validate_Get_Users')?['statusCode'], 0)), '. Expected 2xx.'), null), outputs('Compose_Token_Validity_Failure')?['reason'], outputs('Compose_Token_Acquisition_Failure')?['reason'], null)" + "else": { + "actions": {} } - } - } - } - }, - "else": { - "actions": {} - }, - "expression": { - "or": [ - { - "equals": [ - "@triggerBody()?['EnabledTests']", - "All" - ] - }, - { - "equals": [ - "@triggerBody()?['EnabledTests']", - "SCIMTests" - ] - }, - { - "equals": [ - "@triggerBody()?['EnabledTests']", - "Validate_Credentials_Test" - ] - } - ] - }, - "type": "If" - }, - "Federated_Identity_Test": { - "runAfter": {}, - "actions": { - "Acquire_Entra_Token": { - "type": "Http", - "inputs": { - "uri": "@{concat('https://login.microsoftonline.com/', parameters('federatedEntraTenantId'), '/oauth2/v2.0/token')}", - "method": "POST", - "headers": { - "Content-Type": "application/x-www-form-urlencoded" }, - "body": "@{concat('grant_type=client_credentials&client_id=', encodeUriComponent(parameters('federatedApplicationId')), '&client_secret=', encodeUriComponent(parameters('federatedApplicationClientSecret')), '&scope=', encodeUriComponent(concat('api://', parameters('federatedApplicationId'), '/.default')))}" - } - }, - "Check_Entra_Token_Acquired": { - "runAfter": { - "Acquire_Entra_Token": [ "Succeeded", "Failed", "TimedOut" ] - }, - "type": "If", - "expression": { - "and": [ - { - "greaterOrEquals": [ - "@coalesce(outputs('Acquire_Entra_Token')?['statusCode'], 0)", - 200 - ] - }, - { - "less": [ - "@coalesce(outputs('Acquire_Entra_Token')?['statusCode'], 0)", - 300 + "Set_User_Pagination_Start_Url": { + "runAfter": { + "Check_User_Pagination_Need_Create": [ + "Succeeded", + "Failed" ] }, - { - "not": { - "equals": [ - "@coalesce(body('Acquire_Entra_Token')?['access_token'], '')", - "" - ] - } + "type": "SetVariable", + "inputs": { + "name": "scimUserPaginationNextUrl", + "value": "@{concat(parameters('scimEndpoint'), '/Users?startIndex=1&count=5')}" } - ] - }, - "actions": { - "Check_Is_Google_SA_Flow": { - "type": "If", - "expression": { - "and": [ - { - "contains": [ - "@parameters('federatedTokenEndpoint')", - "googleapis.com" - ] - }, - { - "contains": [ - "@parameters('federatedTokenEndpoint')", - "serviceAccounts" - ] - } + }, + "Until_User_Pagination_Complete": { + "runAfter": { + "Set_User_Pagination_Start_Url": [ + "Succeeded" ] }, + "type": "Until", + "expression": "@or(greaterOrEquals(variables('scimUserPaginationPageCount'), 10), equals(variables('scimUserPaginationNextUrl'), ''))", + "limit": { + "count": 20, + "timeout": "PT5M" + }, "actions": { - "Acquire_Google_STS_Token": { + "SCIM_User_Pagination_Get_Page": { "type": "Http", "inputs": { - "uri": "https://sts.googleapis.com/v1/token", - "method": "POST", + "uri": "@{variables('scimUserPaginationNextUrl')}", + "method": "GET", "headers": { - "Content-Type": "application/json" - }, - "body": { - "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", - "audience": "@{parameters('federatedAudience')}", - "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", - "subject_token": "@{body('Acquire_Entra_Token')?['access_token']}", - "subject_token_type": "urn:ietf:params:oauth:token-type:id_token", - "scope": "https://www.googleapis.com/auth/cloud-platform" + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", + "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_User_Pagination_Get_Page": [ + "Succeeded" + ] } }, - "Check_Google_STS_Token": { + "Append_User_Page_Result": { "runAfter": { - "Acquire_Google_STS_Token": [ + "SCIM_User_Pagination_Get_Page": [ "Succeeded", - "Failed", - "TimedOut" + "Failed" ] }, - "type": "If", - "expression": { - "and": [ - { - "greaterOrEquals": [ - "@coalesce(outputs('Acquire_Google_STS_Token')?['statusCode'], 0)", - 200 - ] - }, - { - "less": [ - "@coalesce(outputs('Acquire_Google_STS_Token')?['statusCode'], 0)", - 300 - ] - }, - { - "not": { - "equals": [ - "@coalesce(body('Acquire_Google_STS_Token')?['access_token'], '')", - "" - ] - } - } + "type": "AppendToArrayVariable", + "inputs": { + "name": "scimUserPaginationResults", + "value": { + "page": "@variables('scimUserPaginationPageCount')", + "url": "@variables('scimUserPaginationNextUrl')", + "statusCode": "@coalesce(outputs('SCIM_User_Pagination_Get_Page')?['statusCode'], 0)", + "totalResults": "@coalesce(body('SCIM_User_Pagination_Get_Page')?['totalResults'], 0)", + "resourceCount": "@length(coalesce(body('SCIM_User_Pagination_Get_Page')?['Resources'], json('[]')))", + "responseStartIndex": "@coalesce(body('SCIM_User_Pagination_Get_Page')?['startIndex'], 0)", + "expectedStartIndex": "@add(mul(variables('scimUserPaginationPageCount'), 5), 1)", + "hasNextLink": "@not(empty(coalesce(body('SCIM_User_Pagination_Get_Page')?['nextPage'], body('SCIM_User_Pagination_Get_Page')?['nextLink'], '')))", + "nextLinkValue": "@coalesce(body('SCIM_User_Pagination_Get_Page')?['nextPage'], body('SCIM_User_Pagination_Get_Page')?['nextLink'], '')" + } + } + }, + "Increment_User_Page_Count": { + "runAfter": { + "Append_User_Page_Result": [ + "Succeeded" + ] + }, + "type": "IncrementVariable", + "inputs": { + "name": "scimUserPaginationPageCount", + "value": 1 + } + }, + "Compose_User_Next_Url": { + "runAfter": { + "Increment_User_Page_Count": [ + "Succeeded" + ] + }, + "type": "Compose", + "inputs": "@if(and(greater(length(coalesce(body('SCIM_User_Pagination_Get_Page')?['Resources'], json('[]'))), 0), lessOrEquals(add(coalesce(body('SCIM_User_Pagination_Get_Page')?['startIndex'], 1), length(coalesce(body('SCIM_User_Pagination_Get_Page')?['Resources'], json('[]')))), coalesce(body('SCIM_User_Pagination_Get_Page')?['totalResults'], 0))), concat(parameters('scimEndpoint'), '/Users?startIndex=', string(add(coalesce(body('SCIM_User_Pagination_Get_Page')?['startIndex'], 1), length(coalesce(body('SCIM_User_Pagination_Get_Page')?['Resources'], json('[]'))))), '&count=5'), '')" + }, + "Set_User_Next_Url": { + "runAfter": { + "Compose_User_Next_Url": [ + "Succeeded" ] }, + "type": "SetVariable", + "inputs": { + "name": "scimUserPaginationNextUrl", + "value": "@{outputs('Compose_User_Next_Url')}" + } + }, + "Ensure_SCIM_Token_SCIM_User_Pagination_Get_Page": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", "actions": { - "Acquire_Google_SA_Token": { - "type": "Http", + "Acquire_Token_SCIM_Token_SCIM_User_Pagination_Get_Page": { + "type": "Workflow", "inputs": { - "uri": "@{parameters('federatedTokenEndpoint')}", - "method": "POST", - "headers": { - "Content-Type": "application/json", - "Authorization": "@{concat('Bearer ', body('Acquire_Google_STS_Token')?['access_token'])}" + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } }, - "body": { - "scope": [ - "https://www.googleapis.com/auth/cloud-platform" + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" ] } - } + }, + "runAfter": {} }, - "Check_Google_SA_Token": { - "runAfter": { - "Acquire_Google_SA_Token": [ - "Succeeded", - "Failed", - "TimedOut" - ] + "Cache_Token_SCIM_Token_SCIM_User_Pagination_Get_Page": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_SCIM_Token_SCIM_User_Pagination_Get_Page')" }, - "type": "If", - "expression": { - "and": [ - { - "greaterOrEquals": [ - "@coalesce(outputs('Acquire_Google_SA_Token')?['statusCode'], 0)", - 200 - ] - }, - { - "less": [ - "@coalesce(outputs('Acquire_Google_SA_Token')?['statusCode'], 0)", - 300 - ] - }, - { - "not": { - "equals": [ - "@coalesce(body('Acquire_Google_SA_Token')?['accessToken'], '')", - "" - ] - } - } + "runAfter": { + "Acquire_Token_SCIM_Token_SCIM_User_Pagination_Get_Page": [ + "Succeeded" ] - }, - "actions": { - "Compose_Federated_Validation_Summary_GoogleSA": { - "type": "Compose", - "inputs": { - "testName": "Federated_Identity_Test", - "timestamp": "@utcNow()", - "authMode": "FederatedIdentity_Google_SA", - "entraTokenAcquisition": { - "endpoint": "@concat('https://login.microsoftonline.com/', parameters('federatedEntraTenantId'), '/oauth2/v2.0/token')", - "statusCode": "@outputs('Acquire_Entra_Token')['statusCode']", - "passed": true - }, - "googleStsTokenExchange": { - "endpoint": "https://sts.googleapis.com/v1/token", - "statusCode": "@outputs('Acquire_Google_STS_Token')['statusCode']", - "passed": true - }, - "googleSaImpersonation": { - "endpoint": "@parameters('federatedTokenEndpoint')", - "statusCode": "@outputs('Acquire_Google_SA_Token')['statusCode']", - "passed": true - }, - "overallResult": "PASSED" - } - } - }, - "else": { - "actions": { - "Compose_Federated_Validation_Summary_GoogleSA_Fail": { - "type": "Compose", - "inputs": { - "testName": "Federated_Identity_Test", - "timestamp": "@utcNow()", - "authMode": "FederatedIdentity_Google_SA", - "entraTokenAcquisition": { - "passed": true - }, - "googleStsTokenExchange": { - "passed": true - }, - "googleSaImpersonation": { - "endpoint": "@parameters('federatedTokenEndpoint')", - "statusCode": "@coalesce(outputs('Acquire_Google_SA_Token')?['statusCode'], 0)", - "hasAccessToken": "@not(equals(coalesce(body('Acquire_Google_SA_Token')?['accessToken'], ''), ''))", - "passed": false - }, - "overallResult": "FAILED" - } - } - } } } }, "else": { - "actions": { - "Compose_Federated_Validation_Summary_GoogleSTS_Fail": { - "type": "Compose", - "inputs": { - "testName": "Federated_Identity_Test", - "timestamp": "@utcNow()", - "authMode": "FederatedIdentity_Google_SA", - "entraTokenAcquisition": { - "passed": true - }, - "googleStsTokenExchange": { - "endpoint": "https://sts.googleapis.com/v1/token", - "statusCode": "@coalesce(outputs('Acquire_Google_STS_Token')?['statusCode'], 0)", - "hasAccessToken": "@not(equals(coalesce(body('Acquire_Google_STS_Token')?['access_token'], ''), ''))", - "passed": false - }, - "overallResult": "FAILED" - } - } - } - } - } - }, - "else": { - "actions": { - "Check_Is_Google_STS_Flow": { - "type": "If", - "expression": { - "and": [ - { - "contains": [ - "@parameters('federatedTokenEndpoint')", - "googleapis.com" - ] - } - ] - }, - "actions": { - "Acquire_Google_STS_Token_Only": { - "type": "Http", - "inputs": { - "uri": "@{parameters('federatedTokenEndpoint')}", - "method": "POST", - "headers": { - "Content-Type": "application/json" - }, - "body": { - "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", - "audience": "@{parameters('federatedAudience')}", - "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", - "subject_token": "@{body('Acquire_Entra_Token')?['access_token']}", - "subject_token_type": "urn:ietf:params:oauth:token-type:id_token", - "scope": "https://www.googleapis.com/auth/cloud-platform" - } - } - }, - "Check_Google_STS_Only_Token": { - "runAfter": { - "Acquire_Google_STS_Token_Only": [ - "Succeeded", - "Failed", - "TimedOut" - ] - }, - "type": "If", - "expression": { - "and": [ - { - "greaterOrEquals": [ - "@coalesce(outputs('Acquire_Google_STS_Token_Only')?['statusCode'], 0)", - 200 - ] - }, - { - "less": [ - "@coalesce(outputs('Acquire_Google_STS_Token_Only')?['statusCode'], 0)", - 300 - ] - }, - { - "not": { - "equals": [ - "@coalesce(body('Acquire_Google_STS_Token_Only')?['access_token'], '')", - "" - ] - } - } - ] - }, - "actions": { - "Compose_Federated_Validation_Summary_GoogleSTSOnly": { - "type": "Compose", - "inputs": { - "testName": "Federated_Identity_Test", - "timestamp": "@utcNow()", - "authMode": "FederatedIdentity_Google_STS", - "entraTokenAcquisition": { - "endpoint": "@concat('https://login.microsoftonline.com/', parameters('federatedEntraTenantId'), '/oauth2/v2.0/token')", - "statusCode": "@outputs('Acquire_Entra_Token')['statusCode']", - "passed": true - }, - "googleStsTokenExchange": { - "endpoint": "@parameters('federatedTokenEndpoint')", - "statusCode": "@outputs('Acquire_Google_STS_Token_Only')['statusCode']", - "passed": true - }, - "overallResult": "PASSED" - } - } - }, - "else": { - "actions": { - "Compose_Federated_Validation_Summary_GoogleSTSOnly_Fail": { - "type": "Compose", - "inputs": { - "testName": "Federated_Identity_Test", - "timestamp": "@utcNow()", - "authMode": "FederatedIdentity_Google_STS", - "entraTokenAcquisition": { - "passed": true - }, - "googleStsTokenExchange": { - "endpoint": "@parameters('federatedTokenEndpoint')", - "statusCode": "@coalesce(outputs('Acquire_Google_STS_Token_Only')?['statusCode'], 0)", - "hasAccessToken": "@not(equals(coalesce(body('Acquire_Google_STS_Token_Only')?['access_token'], ''), ''))", - "passed": false - }, - "overallResult": "FAILED" - } - } - } - } - } - }, - "else": { - "actions": { - "Acquire_Federated_Token": { - "type": "Http", - "inputs": { - "uri": "@{parameters('federatedTokenEndpoint')}", - "method": "POST", - "headers": { - "Content-Type": "application/x-www-form-urlencoded" - }, - "body": "@{concat('grant_type=client_credentials&client_id=', encodeUriComponent(parameters('federatedClientId')), '&client_assertion=', encodeUriComponent(body('Acquire_Entra_Token')?['access_token']), '&client_assertion_type=', encodeUriComponent('urn:ietf:params:oauth:client-assertion-type:jwt-bearer'), '&baseAddress=', encodeUriComponent(parameters('federatedBaseAddress')))}" - } - }, - "Check_Federated_Token_Acquired": { - "runAfter": { - "Acquire_Federated_Token": [ - "Succeeded", - "Failed", - "TimedOut" - ] - }, - "type": "If", - "expression": { - "and": [ - { - "greaterOrEquals": [ - "@coalesce(outputs('Acquire_Federated_Token')?['statusCode'], 0)", - 200 - ] - }, - { - "less": [ - "@coalesce(outputs('Acquire_Federated_Token')?['statusCode'], 0)", - 300 - ] - }, - { - "not": { - "equals": [ - "@coalesce(body('Acquire_Federated_Token')?['access_token'], '')", - "" - ] - } - } - ] - }, - "actions": { - "Compose_Federated_Validation_Summary": { - "type": "Compose", - "inputs": { - "testName": "Federated_Identity_Test", - "timestamp": "@utcNow()", - "authMode": "FederatedIdentity", - "entraTokenAcquisition": { - "endpoint": "@concat('https://login.microsoftonline.com/', parameters('federatedEntraTenantId'), '/oauth2/v2.0/token')", - "statusCode": "@outputs('Acquire_Entra_Token')['statusCode']", - "passed": true - }, - "federatedTokenExchange": { - "endpoint": "@parameters('federatedTokenEndpoint')", - "statusCode": "@outputs('Acquire_Federated_Token')['statusCode']", - "passed": true - }, - "overallResult": "PASSED" - } - } - }, - "else": { - "actions": { - "Compose_Federated_Validation_Summary_TokenFail": { - "type": "Compose", - "inputs": { - "testName": "Federated_Identity_Test", - "timestamp": "@utcNow()", - "authMode": "FederatedIdentity", - "entraTokenAcquisition": { - "passed": true - }, - "federatedTokenExchange": { - "endpoint": "@parameters('federatedTokenEndpoint')", - "statusCode": "@coalesce(outputs('Acquire_Federated_Token')?['statusCode'], 0)", - "hasAccessToken": "@not(equals(coalesce(body('Acquire_Federated_Token')?['access_token'], ''), ''))", - "passed": false - }, - "overallResult": "FAILED" - } - } - } - } - } - } - } - } - } - } - } - }, - "else": { - "actions": { - "Compose_Federated_Validation_Summary_EntraFail": { - "type": "Compose", - "inputs": { - "testName": "Federated_Identity_Test", - "timestamp": "@utcNow()", - "authMode": "FederatedIdentity", - "entraTokenAcquisition": { - "endpoint": "@concat('https://login.microsoftonline.com/', parameters('federatedEntraTenantId'), '/oauth2/v2.0/token')", - "statusCode": "@coalesce(outputs('Acquire_Entra_Token')?['statusCode'], 0)", - "hasAccessToken": "@not(equals(coalesce(body('Acquire_Entra_Token')?['access_token'], ''), ''))", - "passed": false - }, - "federatedTokenExchange": null, - "scimEndpointValidation": null, - "overallResult": "FAILED" + "actions": {} + }, + "runAfter": {} } } - } - } - }, - "Set_FederatedIdentityTestOutputs": { - "runAfter": { - "Check_Entra_Token_Acquired": [ - "Succeeded", - "Failed", - "TimedOut", - "Skipped" - ] - }, - "type": "SetVariable", - "inputs": { - "name": "FederatedIdentityTestOutputs", - "value": { - "overallResult": "@coalesce(outputs('Compose_Federated_Validation_Summary')?['overallResult'], outputs('Compose_Federated_Validation_Summary_TokenFail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSA')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSA_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTS_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_EntraFail')?['overallResult'], 'FAILED')", - "result": "@if(equals(coalesce(outputs('Compose_Federated_Validation_Summary')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSA')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly')?['overallResult'], outputs('Compose_Federated_Validation_Summary_TokenFail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSA_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTS_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_EntraFail')?['overallResult'], 'FAILED'), 'PASSED'), 'success', if(not(empty(coalesce(outputs('Compose_Federated_Validation_Summary_EntraFail'), json('null')))), 'Entra token acquisition failed', 'Federated token exchange failed'))", - "summary": "@coalesce(outputs('Compose_Federated_Validation_Summary'), outputs('Compose_Federated_Validation_Summary_TokenFail'), outputs('Compose_Federated_Validation_Summary_GoogleSA'), outputs('Compose_Federated_Validation_Summary_GoogleSA_Fail'), outputs('Compose_Federated_Validation_Summary_GoogleSTS_Fail'), outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly'), outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly_Fail'), outputs('Compose_Federated_Validation_Summary_EntraFail'))", - "detailedResults": [], - "errorDetails": { - "summary": "@if(equals(coalesce(outputs('Compose_Federated_Validation_Summary')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSA')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly')?['overallResult'], outputs('Compose_Federated_Validation_Summary_TokenFail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSA_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTS_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly_Fail')?['overallResult'], outputs('Compose_Federated_Validation_Summary_EntraFail')?['overallResult'], 'FAILED'), 'PASSED'), null, if(not(empty(coalesce(outputs('Compose_Federated_Validation_Summary_EntraFail'), json('null')))), concat('Entra token acquisition failed. Status: ', string(coalesce(outputs('Acquire_Entra_Token')?['statusCode'], 0))), if(not(empty(coalesce(outputs('Compose_Federated_Validation_Summary_GoogleSTS_Fail'), json('null')))), concat('Google STS token exchange failed. Status: ', string(coalesce(outputs('Acquire_Google_STS_Token')?['statusCode'], 0))), if(not(empty(coalesce(outputs('Compose_Federated_Validation_Summary_GoogleSA_Fail'), json('null')))), concat('Google SA impersonation failed. Status: ', string(coalesce(outputs('Acquire_Google_SA_Token')?['statusCode'], 0))), if(not(empty(coalesce(outputs('Compose_Federated_Validation_Summary_GoogleSTSOnly_Fail'), json('null')))), concat('Google STS token exchange failed. Status: ', string(coalesce(outputs('Acquire_Google_STS_Token_Only')?['statusCode'], 0))), concat('Federated token exchange failed. Status: ', string(coalesce(outputs('Acquire_Federated_Token')?['statusCode'], 0))))))))" - } - } - } - } - }, - "else": { - "actions": {} - }, - "expression": { - "or": [ - { - "equals": [ "@triggerBody()?['EnabledTests']", "All" ] - }, - { - "equals": [ "@triggerBody()?['EnabledTests']", "SCIMTests" ] - }, - { - "equals": [ - "@triggerBody()?['EnabledTests']", - "Federated_Identity_Test" - ] - } - ] - }, - "type": "If" - }, - "SCIM_User_Pagination_Test": { - "runAfter": {}, - "type": "If", - "actions": { - "SCIM_User_Pagination_Test_Actions": { - "actions": { - "SCIM_User_Pagination_Check_Count": { - "type": "Http", + }, + "Filter_User_Pagination_Failed_Pages": { + "runAfter": { + "Until_User_Pagination_Complete": [ + "Succeeded", + "Failed" + ] + }, + "type": "Query", "inputs": { - "uri": "@{concat(parameters('scimEndpoint'), '/Users?startIndex=1&count=5')}", - "method": "GET", - "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", - "Accept": "@{parameters('scimContentType')}" - } + "from": "@variables('scimUserPaginationResults')", + "where": "@or(not(equals(item()?['statusCode'], 200)), not(equals(item()?['responseStartIndex'], item()?['expectedStartIndex'])))" } }, - "Compose_User_Pagination_Users_To_Create": { + "Compose_SCIM_User_Pagination_Summary": { "runAfter": { - "SCIM_User_Pagination_Check_Count": [ + "Filter_User_Pagination_Failed_Pages": [ "Succeeded", - "Failed" + "Failed", + "Skipped" ] }, "type": "Compose", - "inputs": "@max(0, sub(11, coalesce(body('SCIM_User_Pagination_Check_Count')?['totalResults'], 0)))" + "inputs": { + "testName": "SCIM_User_Pagination_Test", + "timestamp": "@utcNow()", + "initialTotalResults": "@coalesce(body('SCIM_User_Pagination_Check_Count')?['totalResults'], 0)", + "usersCreated": "@length(variables('scimUserPaginationCreatedIds'))", + "pagesTraversed": "@variables('scimUserPaginationPageCount')", + "pageResults": "@variables('scimUserPaginationResults')", + "failedPages": "@body('Filter_User_Pagination_Failed_Pages')", + "missingNextLinkCount": "@length(body('Filter_User_Pagination_Failed_Pages'))", + "overallResult": "@if(and(greaterOrEquals(variables('scimUserPaginationPageCount'), 2), equals(length(body('Filter_User_Pagination_Failed_Pages')), 0)), 'PASSED', 'FAILED')" + } }, - "Check_User_Pagination_Need_Create": { + "Set_SCIMUserPaginationTestOutputs": { "runAfter": { - "Compose_User_Pagination_Users_To_Create": [ - "Succeeded" + "Compose_SCIM_User_Pagination_Summary": [ + "Succeeded", + "Failed", + "Skipped" ] }, - "type": "If", - "expression": { - "and": [ - { - "greater": [ - "@outputs('Compose_User_Pagination_Users_To_Create')", - 0 - ] + "type": "SetVariable", + "inputs": { + "name": "SCIMUserPaginationTestOutputs", + "value": { + "overallResult": "@coalesce(outputs('Compose_SCIM_User_Pagination_Summary')?['overallResult'], 'FAILED')", + "result": "@if(equals(coalesce(outputs('Compose_SCIM_User_Pagination_Summary')?['overallResult'], 'FAILED'), 'PASSED'), 'success', if(equals(coalesce(actions('Compose_SCIM_User_Pagination_Summary')?['status'], 'Skipped'), 'Skipped'), concat('FAILED - Action: SCIM_User_Pagination_Create_User - setup failed. Created=', string(length(variables('scimUserPaginationCreatedIds')))), concat('FAILED - Action: Filter_User_Pagination_Failed_Pages - Pages=', string(variables('scimUserPaginationPageCount')), ' failedPages=', string(coalesce(length(body('Filter_User_Pagination_Failed_Pages')), 0)))))", + "summary": "@coalesce(outputs('Compose_SCIM_User_Pagination_Summary'), json(concat('{\"error\":\"Pagination setup failed\",\"Check_User_Pagination_Need_Create\":\"', coalesce(actions('Check_User_Pagination_Need_Create')?['status'], 'Unknown'), '\",\"createdIds\":', string(length(variables('scimUserPaginationCreatedIds'))), '}')))", + "detailedResults": "@variables('scimUserPaginationResults')", + "errorDetails": { + "summary": "@if(equals(coalesce(outputs('Compose_SCIM_User_Pagination_Summary')?['overallResult'], 'FAILED'), 'PASSED'), null, if(equals(coalesce(actions('Compose_SCIM_User_Pagination_Summary')?['status'], 'Skipped'), 'Skipped'), concat('Pagination user creates failed. Check_User_Pagination_Need_Create status=', coalesce(actions('Check_User_Pagination_Need_Create')?['status'], 'Unknown')), coalesce(string(body('Filter_User_Pagination_Failed_Pages')), 'Unknown error')))" } + } + } + }, + "SCIM_User_Pagination_Cleanup": { + "runAfter": { + "Set_SCIMUserPaginationTestOutputs": [ + "Succeeded", + "Failed", + "Skipped" ] }, + "type": "Foreach", + "foreach": "@variables('scimUserPaginationCreatedIds')", "actions": { - "ForEach_User_Pagination_Create": { - "type": "Foreach", - "foreach": "@range(0, outputs('Compose_User_Pagination_Users_To_Create'))", - "actions": { - "SCIM_User_Pagination_Create_User": { - "type": "Http", - "inputs": { - "uri": "@{concat(parameters('scimEndpoint'), '/Users')}", - "method": "POST", - "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", - "Content-Type": "@{parameters('scimContentType')}", - "Accept": "@{parameters('scimContentType')}" - }, - "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)))" - } + "SCIM_User_Pagination_Delete_User": { + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', items('SCIM_User_Pagination_Cleanup'))}", + "method": "DELETE", + "headers": { + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", + "Accept": "@{parameters('scimContentType')}" + } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_User_Pagination_Delete_User": [ + "Succeeded" + ] + } + }, + "Ensure_SCIM_Token_SCIM_User_Pagination_Delete_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_M_Token_SCIM_User_Pagination_Delete_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} }, - "Append_User_Pagination_UserId": { + "Cache_Token_M_Token_SCIM_User_Pagination_Delete_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_M_Token_SCIM_User_Pagination_Delete_User')" + }, "runAfter": { - "SCIM_User_Pagination_Create_User": [ + "Acquire_Token_M_Token_SCIM_User_Pagination_Delete_User": [ "Succeeded" ] - }, - "type": "AppendToArrayVariable", - "inputs": { - "name": "scimUserPaginationCreatedIds", - "value": "@coalesce(body('SCIM_User_Pagination_Create_User')?['id'], '')" } } }, - "runtimeConfiguration": { - "concurrency": { - "repetitions": 5 - } - } + "else": { + "actions": {} + }, + "runAfter": {} } }, - "else": { - "actions": {} - } - }, - "Set_User_Pagination_Start_Url": { - "runAfter": { - "Check_User_Pagination_Need_Create": [ - "Succeeded", - "Failed" - ] - }, - "type": "SetVariable", - "inputs": { - "name": "scimUserPaginationNextUrl", - "value": "@{concat(parameters('scimEndpoint'), '/Users?startIndex=1&count=5')}" + "runtimeConfiguration": { + "concurrency": { + "repetitions": 5 + } } }, - "Until_User_Pagination_Complete": { - "runAfter": { - "Set_User_Pagination_Start_Url": [ - "Succeeded" - ] - }, - "type": "Until", - "expression": "@or(greaterOrEquals(variables('scimUserPaginationPageCount'), 10), equals(variables('scimUserPaginationNextUrl'), ''))", - "limit": { - "count": 20, - "timeout": "PT5M" - }, + "Ensure_SCIM_Token_SCIM_User_Pagination_Check_Count": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", "actions": { - "SCIM_User_Pagination_Get_Page": { - "type": "Http", + "Acquire_Token_M_Token_SCIM_User_Pagination_Check_Count": { + "type": "Workflow", "inputs": { - "uri": "@{variables('scimUserPaginationNextUrl')}", - "method": "GET", - "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", - "Accept": "@{parameters('scimContentType')}" - } - } - }, - "Append_User_Page_Result": { - "runAfter": { - "SCIM_User_Pagination_Get_Page": [ - "Succeeded", - "Failed" - ] + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} }, - "type": "AppendToArrayVariable", - "inputs": { - "name": "scimUserPaginationResults", - "value": { - "page": "@variables('scimUserPaginationPageCount')", - "url": "@variables('scimUserPaginationNextUrl')", - "statusCode": "@coalesce(outputs('SCIM_User_Pagination_Get_Page')?['statusCode'], 0)", - "totalResults": "@coalesce(body('SCIM_User_Pagination_Get_Page')?['totalResults'], 0)", - "resourceCount": "@length(coalesce(body('SCIM_User_Pagination_Get_Page')?['Resources'], json('[]')))", - "responseStartIndex": "@coalesce(body('SCIM_User_Pagination_Get_Page')?['startIndex'], 0)", - "expectedStartIndex": "@add(mul(variables('scimUserPaginationPageCount'), 5), 1)", - "hasNextLink": "@not(empty(coalesce(body('SCIM_User_Pagination_Get_Page')?['nextPage'], body('SCIM_User_Pagination_Get_Page')?['nextLink'], '')))", - "nextLinkValue": "@coalesce(body('SCIM_User_Pagination_Get_Page')?['nextPage'], body('SCIM_User_Pagination_Get_Page')?['nextLink'], '')" + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] } - } - }, - "Increment_User_Page_Count": { - "runAfter": { - "Append_User_Page_Result": [ - "Succeeded" - ] }, - "type": "IncrementVariable", - "inputs": { - "name": "scimUserPaginationPageCount", - "value": 1 - } + "runAfter": {} }, - "Compose_User_Next_Url": { - "runAfter": { - "Increment_User_Page_Count": [ - "Succeeded" - ] + "Cache_Token_M_Token_SCIM_User_Pagination_Check_Count": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_M_Token_SCIM_User_Pagination_Check_Count')" }, - "type": "Compose", - "inputs": "@if(and(greater(length(coalesce(body('SCIM_User_Pagination_Get_Page')?['Resources'], json('[]'))), 0), lessOrEquals(add(coalesce(body('SCIM_User_Pagination_Get_Page')?['startIndex'], 1), length(coalesce(body('SCIM_User_Pagination_Get_Page')?['Resources'], json('[]')))), coalesce(body('SCIM_User_Pagination_Get_Page')?['totalResults'], 0))), concat(parameters('scimEndpoint'), '/Users?startIndex=', string(add(coalesce(body('SCIM_User_Pagination_Get_Page')?['startIndex'], 1), length(coalesce(body('SCIM_User_Pagination_Get_Page')?['Resources'], json('[]'))))), '&count=5'), '')" - }, - "Set_User_Next_Url": { "runAfter": { - "Compose_User_Next_Url": [ + "Acquire_Token_M_Token_SCIM_User_Pagination_Check_Count": [ "Succeeded" ] - }, - "type": "SetVariable", - "inputs": { - "name": "scimUserPaginationNextUrl", - "value": "@{outputs('Compose_User_Next_Url')}" - } - } - } - }, - "Filter_User_Pagination_Failed_Pages": { - "runAfter": { - "Until_User_Pagination_Complete": [ - "Succeeded", - "Failed" - ] - }, - "type": "Query", - "inputs": { - "from": "@variables('scimUserPaginationResults')", - "where": "@or(not(equals(item()?['statusCode'], 200)), not(equals(item()?['responseStartIndex'], item()?['expectedStartIndex'])))" - } - }, - "Compose_SCIM_User_Pagination_Summary": { - "runAfter": { - "Filter_User_Pagination_Failed_Pages": [ - "Succeeded", - "Failed", - "Skipped" - ] - }, - "type": "Compose", - "inputs": { - "testName": "SCIM_User_Pagination_Test", - "timestamp": "@utcNow()", - "initialTotalResults": "@coalesce(body('SCIM_User_Pagination_Check_Count')?['totalResults'], 0)", - "usersCreated": "@length(variables('scimUserPaginationCreatedIds'))", - "pagesTraversed": "@variables('scimUserPaginationPageCount')", - "pageResults": "@variables('scimUserPaginationResults')", - "failedPages": "@body('Filter_User_Pagination_Failed_Pages')", - "missingNextLinkCount": "@length(body('Filter_User_Pagination_Failed_Pages'))", - "overallResult": "@if(and(greaterOrEquals(variables('scimUserPaginationPageCount'), 2), equals(length(body('Filter_User_Pagination_Failed_Pages')), 0)), 'PASSED', 'FAILED')" - } - }, - "Set_SCIMUserPaginationTestOutputs": { - "runAfter": { - "Compose_SCIM_User_Pagination_Summary": [ - "Succeeded", - "Failed", - "Skipped" - ] - }, - "type": "SetVariable", - "inputs": { - "name": "SCIMUserPaginationTestOutputs", - "value": { - "overallResult": "@coalesce(outputs('Compose_SCIM_User_Pagination_Summary')?['overallResult'], 'FAILED')", - "result": "@if(equals(coalesce(outputs('Compose_SCIM_User_Pagination_Summary')?['overallResult'], 'FAILED'), 'PASSED'), 'success', if(equals(coalesce(actions('Compose_SCIM_User_Pagination_Summary')?['status'], 'Skipped'), 'Skipped'), concat('FAILED - Action: SCIM_User_Pagination_Create_User - setup failed. Created=', string(length(variables('scimUserPaginationCreatedIds')))), concat('FAILED - Action: Filter_User_Pagination_Failed_Pages - Pages=', string(variables('scimUserPaginationPageCount')), ' failedPages=', string(coalesce(length(body('Filter_User_Pagination_Failed_Pages')), 0)))))", - "summary": "@coalesce(outputs('Compose_SCIM_User_Pagination_Summary'), json(concat('{\"error\":\"Pagination setup failed\",\"Check_User_Pagination_Need_Create\":\"', coalesce(actions('Check_User_Pagination_Need_Create')?['status'], 'Unknown'), '\",\"createdIds\":', string(length(variables('scimUserPaginationCreatedIds'))), '}')))", - "detailedResults": "@variables('scimUserPaginationResults')", - "errorDetails": { - "summary": "@if(equals(coalesce(outputs('Compose_SCIM_User_Pagination_Summary')?['overallResult'], 'FAILED'), 'PASSED'), null, if(equals(coalesce(actions('Compose_SCIM_User_Pagination_Summary')?['status'], 'Skipped'), 'Skipped'), concat('Pagination user creates failed. Check_User_Pagination_Need_Create status=', coalesce(actions('Check_User_Pagination_Need_Create')?['status'], 'Unknown')), coalesce(string(body('Filter_User_Pagination_Failed_Pages')), 'Unknown error')))" } } - } - }, - "SCIM_User_Pagination_Cleanup": { - "runAfter": { - "Set_SCIMUserPaginationTestOutputs": [ - "Succeeded", - "Failed", - "Skipped" - ] }, - "type": "Foreach", - "foreach": "@variables('scimUserPaginationCreatedIds')", - "actions": { - "SCIM_User_Pagination_Delete_User": { - "type": "Http", - "inputs": { - "uri": "@{concat(parameters('scimEndpoint'), '/Users/', items('SCIM_User_Pagination_Cleanup'))}", - "method": "DELETE", - "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", - "Accept": "@{parameters('scimContentType')}" - } - } - } + "else": { + "actions": {} }, - "runtimeConfiguration": { - "concurrency": { - "repetitions": 5 - } - } + "runAfter": {} } }, "runAfter": {}, @@ -4493,9 +4702,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups?startIndex=1&count=5')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Group_Pagination_Check_Count": [ + "Succeeded" + ] } }, "Compose_Group_Pagination_Groups_To_Create": { @@ -4536,11 +4750,16 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups')}", "method": "POST", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, "body": "@if(empty(variables('scimGroupEmailExtensionUrn')), setProperty(if(contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryGroupAttributes'], json('[]'))), 'externalId'), setProperty(variables('scimDiscoveredGroupBody'), 'externalId', guid()), variables('scimDiscoveredGroupBody')), 'displayName', concat('PagGroup ', substring(guid(),0,8))), union(setProperty(if(contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryGroupAttributes'], json('[]'))), 'externalId'), setProperty(variables('scimDiscoveredGroupBody'), 'externalId', guid()), variables('scimDiscoveredGroupBody')), 'displayName', concat('PagGroup ', substring(guid(),0,8))), json(concat('{\"', variables('scimGroupEmailExtensionUrn'), '\":', string(setProperty(coalesce(variables('scimDiscoveredGroupBody')?[variables('scimGroupEmailExtensionUrn')], json('{}')), 'email', concat('scim-group-', substring(guid(),0,8), '@', parameters('testUserDomain')))), '}'))))" + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Group_Pagination_Create_Group": [ + "Succeeded" + ] } }, "Append_Group_Pagination_GroupId": { @@ -4554,6 +4773,48 @@ "name": "scimGroupPaginationCreatedIds", "value": "@coalesce(body('SCIM_Group_Pagination_Create_Group')?['id'], '')" } + }, + "Ensure_SCIM_Token_SCIM_Group_Pagination_Create_Group": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Token_SCIM_Group_Pagination_Create_Group": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Token_SCIM_Group_Pagination_Create_Group": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Token_SCIM_Group_Pagination_Create_Group')" + }, + "runAfter": { + "Acquire_Token_Token_SCIM_Group_Pagination_Create_Group": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runtimeConfiguration": { @@ -4599,9 +4860,14 @@ "uri": "@{variables('scimGroupPaginationNextUrl')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Group_Pagination_Get_Page": [ + "Succeeded" + ] } }, "Append_Group_Page_Result": { @@ -4659,7 +4925,49 @@ "name": "scimGroupPaginationNextUrl", "value": "@{outputs('Compose_Group_Next_Url')}" } - } + }, + "Ensure_SCIM_Token_SCIM_Group_Pagination_Get_Page": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_CIM_Token_SCIM_Group_Pagination_Get_Page": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_CIM_Token_SCIM_Group_Pagination_Get_Page": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_CIM_Token_SCIM_Group_Pagination_Get_Page')" + }, + "runAfter": { + "Acquire_Token_CIM_Token_SCIM_Group_Pagination_Get_Page": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} + } } }, "Filter_Group_Pagination_Failed_Pages": { @@ -4735,10 +5043,57 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Groups/', items('SCIM_Group_Pagination_Cleanup'))}", "method": "DELETE", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Group_Pagination_Delete_Group": [ + "Succeeded" + ] } + }, + "Ensure_SCIM_Token_SCIM_Group_Pagination_Delete_Group": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Token_SCIM_Group_Pagination_Delete_Group": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Token_SCIM_Group_Pagination_Delete_Group": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Token_SCIM_Group_Pagination_Delete_Group')" + }, + "runAfter": { + "Acquire_Token_Token_SCIM_Group_Pagination_Delete_Group": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runtimeConfiguration": { @@ -4746,6 +5101,48 @@ "repetitions": 5 } } + }, + "Ensure_SCIM_Token_SCIM_Group_Pagination_Check_Count": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token__Token_SCIM_Group_Pagination_Check_Count": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token__Token_SCIM_Group_Pagination_Check_Count": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token__Token_SCIM_Group_Pagination_Check_Count')" + }, + "runAfter": { + "Acquire_Token__Token_SCIM_Group_Pagination_Check_Count": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": {}, @@ -4802,21 +5199,25 @@ }, "Compose_Manager_TestUser_Body": { "runAfter": { - "Generate_Manager_TestUser_Name": ["Succeeded"] + "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"] + "Ensure_SCIM_Token_SCIM_Manager_Create_TestUser": [ + "Succeeded" + ] }, "type": "Http", "inputs": { "uri": "@{concat(parameters('scimEndpoint'), '/Users')}", "method": "POST", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, @@ -4825,28 +5226,34 @@ }, "Generate_Manager_ManagerUser_Name": { "runAfter": { - "SCIM_Manager_Create_TestUser": ["Succeeded"] + "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"] + "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"] + "Ensure_SCIM_Token_SCIM_Manager_Create_ManagerUser": [ + "Succeeded" + ] }, "type": "Http", "inputs": { "uri": "@{concat(parameters('scimEndpoint'), '/Users')}", "method": "POST", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, @@ -4855,7 +5262,9 @@ }, "Delay_Manager_Initial_Users_Ready": { "runAfter": { - "SCIM_Manager_Create_ManagerUser": [ "Succeeded" ] + "SCIM_Manager_Create_ManagerUser": [ + "Succeeded" + ] }, "type": "Wait", "inputs": { @@ -4867,7 +5276,9 @@ }, "Compose_Manager_Set_Patch_Body": { "runAfter": { - "Delay_Manager_Initial_Users_Ready": [ "Succeeded" ] + "Delay_Manager_Initial_Users_Ready": [ + "Succeeded" + ] }, "type": "Compose", "inputs": { @@ -4879,14 +5290,16 @@ }, "SCIM_Manager_Patch_Set_Manager": { "runAfter": { - "Compose_Manager_Set_Patch_Body": [ "Succeeded" ] + "Ensure_SCIM_Token_SCIM_Manager_Patch_Set_Manager": [ + "Succeeded" + ] }, "type": "Http", "inputs": { "uri": "@{concat(parameters('scimEndpoint'), '/Users/', body('SCIM_Manager_Create_TestUser')?['id'])}", "method": "PATCH", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, @@ -4895,7 +5308,9 @@ }, "Delay_Manager_Set_Verification": { "runAfter": { - "SCIM_Manager_Patch_Set_Manager": [ "Succeeded" ] + "SCIM_Manager_Patch_Set_Manager": [ + "Succeeded" + ] }, "type": "Wait", "inputs": { @@ -4907,42 +5322,50 @@ }, "SCIM_Manager_Get_After_Set": { "runAfter": { - "Delay_Manager_Set_Verification": [ "Succeeded" ] + "Ensure_SCIM_Token_SCIM_Manager_Get_After_Set": [ + "Succeeded" + ] }, "type": "Http", "inputs": { "uri": "@{concat(parameters('scimEndpoint'), '/Users/', body('SCIM_Manager_Create_TestUser')?['id'])}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } }, "Generate_Manager_SecondManager_Name": { "runAfter": { - "SCIM_Manager_Get_After_Set": ["Succeeded"] + "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"] + "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"] + "Ensure_SCIM_Token_SCIM_Manager_Create_SecondManager": [ + "Succeeded" + ] }, "type": "Http", "inputs": { "uri": "@{concat(parameters('scimEndpoint'), '/Users')}", "method": "POST", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, @@ -4951,7 +5374,9 @@ }, "Delay_Manager_Second_Manager_Ready": { "runAfter": { - "SCIM_Manager_Create_SecondManager": [ "Succeeded" ] + "SCIM_Manager_Create_SecondManager": [ + "Succeeded" + ] }, "type": "Wait", "inputs": { @@ -4963,7 +5388,9 @@ }, "Compose_Manager_Change_Patch_Body": { "runAfter": { - "Delay_Manager_Second_Manager_Ready": ["Succeeded"] + "Delay_Manager_Second_Manager_Ready": [ + "Succeeded" + ] }, "type": "Compose", "inputs": { @@ -4975,14 +5402,16 @@ }, "SCIM_Manager_Patch_Change_Manager": { "runAfter": { - "Compose_Manager_Change_Patch_Body": ["Succeeded"] + "Ensure_SCIM_Token_SCIM_Manager_Patch_Change_Manager": [ + "Succeeded" + ] }, "type": "Http", "inputs": { "uri": "@{concat(parameters('scimEndpoint'), '/Users/', body('SCIM_Manager_Create_TestUser')?['id'])}", "method": "PATCH", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, @@ -4991,7 +5420,9 @@ }, "Delay_Manager_Change_Verification": { "runAfter": { - "SCIM_Manager_Patch_Change_Manager": [ "Succeeded" ] + "SCIM_Manager_Patch_Change_Manager": [ + "Succeeded" + ] }, "type": "Wait", "inputs": { @@ -5003,21 +5434,25 @@ }, "SCIM_Manager_Get_After_Change": { "runAfter": { - "Delay_Manager_Change_Verification": ["Succeeded"] + "Ensure_SCIM_Token_SCIM_Manager_Get_After_Change": [ + "Succeeded" + ] }, "type": "Http", "inputs": { "uri": "@{concat(parameters('scimEndpoint'), '/Users/', body('SCIM_Manager_Create_TestUser')?['id'])}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } }, "Compose_Manager_Remove_Patch_Body": { "runAfter": { - "SCIM_Manager_Get_After_Change": [ "Succeeded" ] + "SCIM_Manager_Get_After_Change": [ + "Succeeded" + ] }, "type": "Compose", "inputs": { @@ -5034,14 +5469,16 @@ }, "SCIM_Manager_Patch_Remove_Manager": { "runAfter": { - "Compose_Manager_Remove_Patch_Body": [ "Succeeded" ] + "Ensure_SCIM_Token_SCIM_Manager_Patch_Remove_Manager": [ + "Succeeded" + ] }, "type": "Http", "inputs": { "uri": "@{concat(parameters('scimEndpoint'), '/Users/', body('SCIM_Manager_Create_TestUser')?['id'])}", "method": "PATCH", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, @@ -5050,7 +5487,9 @@ }, "Delay_Manager_Remove_Verification": { "runAfter": { - "SCIM_Manager_Patch_Remove_Manager": [ "Succeeded" ] + "SCIM_Manager_Patch_Remove_Manager": [ + "Succeeded" + ] }, "type": "Wait", "inputs": { @@ -5062,21 +5501,26 @@ }, "SCIM_Manager_Get_After_Remove": { "runAfter": { - "Delay_Manager_Remove_Verification": [ "Succeeded" ] + "Ensure_SCIM_Token_SCIM_Manager_Get_After_Remove": [ + "Succeeded" + ] }, "type": "Http", "inputs": { "uri": "@{concat(parameters('scimEndpoint'), '/Users/', body('SCIM_Manager_Create_TestUser')?['id'])}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } }, "Compose_SCIM_Manager_Test_Summary": { "runAfter": { - "SCIM_Manager_Get_After_Remove": [ "Succeeded", "Failed" ] + "SCIM_Manager_Get_After_Remove": [ + "Succeeded", + "Failed" + ] }, "type": "Compose", "inputs": { @@ -5091,120 +5535,460 @@ "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_Deactivate_TestUser": { - "type": "Http", - "inputs": { - "uri": "@{concat(parameters('scimEndpoint'), '/Users/', coalesce(body('SCIM_Manager_Create_TestUser')?['id'], 'UNKNOWN'))}", - "method": "PATCH", - "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", - "Content-Type": "@{parameters('scimContentType')}", - "Accept": "@{parameters('scimContentType')}" - }, - "body": { - "schemas": [ - "urn:ietf:params:scim:api:messages:2.0:PatchOp" - ], - "Operations": [ - { - "op": "replace", - "path": "active", - "value": false + }, + "Ensure_SCIM_Token_SCIM_Manager_Create_TestUser": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token__SCIM_Token_SCIM_Manager_Create_TestUser": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] } - ] + }, + "runAfter": {} + }, + "Cache_Token__SCIM_Token_SCIM_Manager_Create_TestUser": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token__SCIM_Token_SCIM_Manager_Create_TestUser')" + }, + "runAfter": { + "Acquire_Token__SCIM_Token_SCIM_Manager_Create_TestUser": [ + "Succeeded" + ] + } } - } - }, - "SCIM_Manager_Delete_TestUser": { - "runAfter": { - "SCIM_Manager_Deactivate_TestUser": [ "Succeeded", "Failed", "Skipped" ] }, - "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')}" - } + "else": { + "actions": {} + }, + "runAfter": { + "Compose_Manager_TestUser_Body": [ + "Succeeded" + ] } }, - "SCIM_Manager_Deactivate_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": "PATCH", - "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", - "Content-Type": "@{parameters('scimContentType')}", - "Accept": "@{parameters('scimContentType')}" - }, - "body": { - "schemas": [ - "urn:ietf:params:scim:api:messages:2.0:PatchOp" - ], - "Operations": [ - { - "op": "replace", - "path": "active", - "value": false + "Ensure_SCIM_Token_SCIM_Manager_Create_ManagerUser": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_IM_Token_SCIM_Manager_Create_ManagerUser": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] } - ] + }, + "runAfter": {} + }, + "Cache_Token_IM_Token_SCIM_Manager_Create_ManagerUser": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_IM_Token_SCIM_Manager_Create_ManagerUser')" + }, + "runAfter": { + "Acquire_Token_IM_Token_SCIM_Manager_Create_ManagerUser": [ + "Succeeded" + ] + } } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Compose_Manager_ManagerUser_Body": [ + "Succeeded" + ] } }, - "SCIM_Manager_Delete_ManagerUser": { - "runAfter": { - "SCIM_Manager_Deactivate_ManagerUser": [ "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')}" + "Ensure_SCIM_Token_SCIM_Manager_Patch_Set_Manager": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_CIM_Token_SCIM_Manager_Patch_Set_Manager": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_CIM_Token_SCIM_Manager_Patch_Set_Manager": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_CIM_Token_SCIM_Manager_Patch_Set_Manager')" + }, + "runAfter": { + "Acquire_Token_CIM_Token_SCIM_Manager_Patch_Set_Manager": [ + "Succeeded" + ] + } } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Compose_Manager_Set_Patch_Body": [ + "Succeeded" + ] } }, - "SCIM_Manager_Deactivate_SecondManager": { + "Ensure_SCIM_Token_SCIM_Manager_Get_After_Set": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_re_SCIM_Token_SCIM_Manager_Get_After_Set": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_re_SCIM_Token_SCIM_Manager_Get_After_Set": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_re_SCIM_Token_SCIM_Manager_Get_After_Set')" + }, + "runAfter": { + "Acquire_Token_re_SCIM_Token_SCIM_Manager_Get_After_Set": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Delay_Manager_Set_Verification": [ + "Succeeded" + ] + } + }, + "Ensure_SCIM_Token_SCIM_Manager_Create_SecondManager": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token__Token_SCIM_Manager_Create_SecondManager": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token__Token_SCIM_Manager_Create_SecondManager": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token__Token_SCIM_Manager_Create_SecondManager')" + }, + "runAfter": { + "Acquire_Token__Token_SCIM_Manager_Create_SecondManager": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Compose_Manager_SecondManager_Body": [ + "Succeeded" + ] + } + }, + "Ensure_SCIM_Token_SCIM_Manager_Patch_Change_Manager": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token__Token_SCIM_Manager_Patch_Change_Manager": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token__Token_SCIM_Manager_Patch_Change_Manager": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token__Token_SCIM_Manager_Patch_Change_Manager')" + }, + "runAfter": { + "Acquire_Token__Token_SCIM_Manager_Patch_Change_Manager": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Compose_Manager_Change_Patch_Body": [ + "Succeeded" + ] + } + }, + "Ensure_SCIM_Token_SCIM_Manager_Get_After_Change": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_SCIM_Token_SCIM_Manager_Get_After_Change": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_SCIM_Token_SCIM_Manager_Get_After_Change": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_SCIM_Token_SCIM_Manager_Get_After_Change')" + }, + "runAfter": { + "Acquire_Token_SCIM_Token_SCIM_Manager_Get_After_Change": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Delay_Manager_Change_Verification": [ + "Succeeded" + ] + } + }, + "Ensure_SCIM_Token_SCIM_Manager_Patch_Remove_Manager": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token__Token_SCIM_Manager_Patch_Remove_Manager": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token__Token_SCIM_Manager_Patch_Remove_Manager": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token__Token_SCIM_Manager_Patch_Remove_Manager')" + }, + "runAfter": { + "Acquire_Token__Token_SCIM_Manager_Patch_Remove_Manager": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, "runAfter": { - "SCIM_Manager_Delete_ManagerUser": [ "Succeeded", "Failed", "Skipped" ] + "Compose_Manager_Remove_Patch_Body": [ + "Succeeded" + ] + } + }, + "Ensure_SCIM_Token_SCIM_Manager_Get_After_Remove": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_SCIM_Token_SCIM_Manager_Get_After_Remove": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_SCIM_Token_SCIM_Manager_Get_After_Remove": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_SCIM_Token_SCIM_Manager_Get_After_Remove')" + }, + "runAfter": { + "Acquire_Token_SCIM_Token_SCIM_Manager_Get_After_Remove": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} }, + "runAfter": { + "Delay_Manager_Remove_Verification": [ + "Succeeded" + ] + } + } + }, + "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_Deactivate_TestUser": { "type": "Http", "inputs": { - "uri": "@{concat(parameters('scimEndpoint'), '/Users/', coalesce(body('SCIM_Manager_Create_SecondManager')?['id'], 'UNKNOWN'))}", + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', coalesce(body('SCIM_Manager_Create_TestUser')?['id'], 'UNKNOWN'))}", "method": "PATCH", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Content-Type": "@{parameters('scimContentType')}", "Accept": "@{parameters('scimContentType')}" }, @@ -5220,94 +6004,1121 @@ } ] } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Manager_Deactivate_TestUser": [ + "Succeeded" + ] } }, - "SCIM_Manager_Delete_SecondManager": { + "SCIM_Manager_Delete_TestUser": { "runAfter": { - "SCIM_Manager_Deactivate_SecondManager": [ "Succeeded", "Failed", "Skipped" ] + "Ensure_SCIM_Token_SCIM_Manager_Delete_TestUser": [ + "Succeeded" + ] }, "type": "Http", "inputs": { - "uri": "@{concat(parameters('scimEndpoint'), '/Users/', coalesce(body('SCIM_Manager_Create_SecondManager')?['id'], 'UNKNOWN'))}", + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', coalesce(body('SCIM_Manager_Create_TestUser')?['id'], 'UNKNOWN'))}", "method": "DELETE", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } - } - } - } - }, - "else": { - "actions": {} - }, - "expression": { - "and": [ - { - "equals": [ - "@contains(string(coalesce(triggerBody()?['initializationData']?['targetDirectoryUserAttributes'], json('[]'))), 'manager')", - true - ] - }, - { - "or": [ - { - "equals": [ - "@triggerBody()?['EnabledTests']", - "All" + }, + "SCIM_Manager_Deactivate_ManagerUser": { + "runAfter": { + "Ensure_SCIM_Token_SCIM_Manager_Deactivate_ManagerUser": [ + "Succeeded" ] }, - { - "equals": [ - "@triggerBody()?['EnabledTests']", - "SCIMTests" + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', coalesce(body('SCIM_Manager_Create_ManagerUser')?['id'], 'UNKNOWN'))}", + "method": "PATCH", + "headers": { + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", + "Content-Type": "@{parameters('scimContentType')}", + "Accept": "@{parameters('scimContentType')}" + }, + "body": { + "schemas": [ + "urn:ietf:params:scim:api:messages:2.0:PatchOp" + ], + "Operations": [ + { + "op": "replace", + "path": "active", + "value": false + } + ] + } + } + }, + "SCIM_Manager_Delete_ManagerUser": { + "runAfter": { + "Ensure_SCIM_Token_SCIM_Manager_Delete_ManagerUser": [ + "Succeeded" ] }, - { - "equals": [ - "@triggerBody()?['EnabledTests']", - "SCIM_Update_Manager_Test" + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', coalesce(body('SCIM_Manager_Create_ManagerUser')?['id'], 'UNKNOWN'))}", + "method": "DELETE", + "headers": { + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", + "Accept": "@{parameters('scimContentType')}" + } + } + }, + "SCIM_Manager_Deactivate_SecondManager": { + "runAfter": { + "Ensure_SCIM_Token_SCIM_Manager_Deactivate_SecondManager": [ + "Succeeded" + ] + }, + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', coalesce(body('SCIM_Manager_Create_SecondManager')?['id'], 'UNKNOWN'))}", + "method": "PATCH", + "headers": { + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", + "Content-Type": "@{parameters('scimContentType')}", + "Accept": "@{parameters('scimContentType')}" + }, + "body": { + "schemas": [ + "urn:ietf:params:scim:api:messages:2.0:PatchOp" + ], + "Operations": [ + { + "op": "replace", + "path": "active", + "value": false + } + ] + } + } + }, + "SCIM_Manager_Delete_SecondManager": { + "runAfter": { + "Ensure_SCIM_Token_SCIM_Manager_Delete_SecondManager": [ + "Succeeded" ] + }, + "type": "Http", + "inputs": { + "uri": "@{concat(parameters('scimEndpoint'), '/Users/', coalesce(body('SCIM_Manager_Create_SecondManager')?['id'], 'UNKNOWN'))}", + "method": "DELETE", + "headers": { + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", + "Accept": "@{parameters('scimContentType')}" + } } - ] + }, + "Ensure_SCIM_Token_SCIM_Manager_Deactivate_TestUser": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_M_Token_SCIM_Manager_Deactivate_TestUser": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_M_Token_SCIM_Manager_Deactivate_TestUser": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_M_Token_SCIM_Manager_Deactivate_TestUser')" + }, + "runAfter": { + "Acquire_Token_M_Token_SCIM_Manager_Deactivate_TestUser": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} + }, + "Ensure_SCIM_Token_SCIM_Manager_Delete_TestUser": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token__SCIM_Token_SCIM_Manager_Delete_TestUser": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token__SCIM_Token_SCIM_Manager_Delete_TestUser": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token__SCIM_Token_SCIM_Manager_Delete_TestUser')" + }, + "runAfter": { + "Acquire_Token__SCIM_Token_SCIM_Manager_Delete_TestUser": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "SCIM_Manager_Deactivate_TestUser": [ + "Succeeded", + "Failed", + "Skipped" + ] + } + }, + "Ensure_SCIM_Token_SCIM_Manager_Deactivate_ManagerUser": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_oken_SCIM_Manager_Deactivate_ManagerUser": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_oken_SCIM_Manager_Deactivate_ManagerUser": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_oken_SCIM_Manager_Deactivate_ManagerUser')" + }, + "runAfter": { + "Acquire_Token_oken_SCIM_Manager_Deactivate_ManagerUser": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "SCIM_Manager_Delete_TestUser": [ + "Succeeded", + "Failed", + "Skipped" + ] + } + }, + "Ensure_SCIM_Token_SCIM_Manager_Delete_ManagerUser": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_IM_Token_SCIM_Manager_Delete_ManagerUser": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_IM_Token_SCIM_Manager_Delete_ManagerUser": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_IM_Token_SCIM_Manager_Delete_ManagerUser')" + }, + "runAfter": { + "Acquire_Token_IM_Token_SCIM_Manager_Delete_ManagerUser": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "SCIM_Manager_Deactivate_ManagerUser": [ + "Succeeded", + "Failed", + "Skipped" + ] + } + }, + "Ensure_SCIM_Token_SCIM_Manager_Deactivate_SecondManager": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_en_SCIM_Manager_Deactivate_SecondManager": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_en_SCIM_Manager_Deactivate_SecondManager": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_en_SCIM_Manager_Deactivate_SecondManager')" + }, + "runAfter": { + "Acquire_Token_en_SCIM_Manager_Deactivate_SecondManager": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "SCIM_Manager_Delete_ManagerUser": [ + "Succeeded", + "Failed", + "Skipped" + ] + } + }, + "Ensure_SCIM_Token_SCIM_Manager_Delete_SecondManager": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token__Token_SCIM_Manager_Delete_SecondManager": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token__Token_SCIM_Manager_Delete_SecondManager": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token__Token_SCIM_Manager_Delete_SecondManager')" + }, + "runAfter": { + "Acquire_Token__Token_SCIM_Manager_Delete_SecondManager": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "SCIM_Manager_Deactivate_SecondManager": [ + "Succeeded", + "Failed", + "Skipped" + ] + } + } + } + } + }, + "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" + }, + "Call_Authentication_Workflow": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "Authentication_Workflow" + } + }, + "body": { + "supportedAuthenticationMethods": "@outputs('Compose_Authentication_Methods')", + "EnabledTests": "@triggerBody()?['EnabledTests']", + "parentRunId": "@workflow().run.name" + } + }, + "runAfter": {} + } + }, + "runAfter": { + "Response_Immediate": [ + "Succeeded" + ] + }, + "type": "Scope" + }, + "Compose_Final_Results": { + "runAfter": { + "Compose_Authentication_Model_Result": [ + "Succeeded" + ] + }, + "type": "Compose", + "inputs": { + "SchemaDiscoverabilityTestOutputs": "@variables('SchemaDiscoverabilityTestOutputs')", + "SCIMNullUpdateTestOutputs": "@variables('SCIMNullUpdateTestOutputs')", + "ValidateCredentialsTestOutputs": "@coalesce(body('Call_Authentication_Workflow')?['ValidateCredentialsTestOutputs'], json('{\"overallResult\":\"FAILED\",\"result\":\"FAILED - authentication child output unavailable\"}'))", + "FederatedIdentityTestOutputs": "@coalesce(body('Call_Authentication_Workflow')?['FederatedIdentityTestOutputs'], json('{\"overallResult\":\"FAILED\",\"result\":\"FAILED - authentication child output unavailable\"}'))", + "SCIMUserCreateTestOutputs": "@variables('SCIMUserCreateTestOutputs')", + "SCIMGroupCreateTestOutputs": "@variables('SCIMGroupCreateTestOutputs')", + "SCIMUserUpdateTestOutputs": "@variables('SCIMUserUpdateTestOutputs')", + "SCIMGroupUpdateTestOutputs": "@variables('SCIMGroupUpdateTestOutputs')", + "SCIMUserPaginationTestOutputs": "@variables('SCIMUserPaginationTestOutputs')", + "SCIMGroupPaginationTestOutputs": "@variables('SCIMGroupPaginationTestOutputs')", + "SCIMUpdateManagerTestOutputs": "@variables('SCIMUpdateManagerTestOutputs')", + "testSuiteName": "SCIMTests", + "status": "@if(or(or(or(equals(variables('SchemaDiscoverabilityTestOutputs')['overallResult'], 'FAILED'), equals(variables('SCIMNullUpdateTestOutputs')['overallResult'], 'FAILED')), or(not(equals(outputs('Compose_Authentication_Model_Result'), 'PASSED')), 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", + "authenticationModelResult": "@outputs('Compose_Authentication_Model_Result')", + "supportedAuthenticationMethods": "@outputs('Compose_Authentication_Methods')", + "BearerConnectivityTestOutputs": "@coalesce(body('Call_Authentication_Workflow')?['BearerConnectivityTestOutputs'], json('{\"overallResult\":\"FAILED\",\"result\":\"FAILED - authentication child output unavailable\"}'))", + "authenticationPolicy": "AllSelected", + "logicAppVersion": "@parameters('LogicAppVersion')" + } + }, + "Compose_Authentication_Methods": { + "type": "Compose", + "inputs": "@if(empty(parameters('supportedAuthenticationMethods')), union(if(not(empty(parameters('scimTokenEndpoint'))), createArray('OAuth2ClientCredentials'), json('[]')), if(not(empty(parameters('federatedTokenEndpoint'))), createArray('WorkloadIdentityFederation'), json('[]')), if(and(empty(parameters('scimTokenEndpoint')), empty(parameters('federatedTokenEndpoint')), not(empty(parameters('scimBearerToken')))), createArray('Bearer'), json('[]'))), union(parameters('supportedAuthenticationMethods'), json('[]')))", + "runAfter": {} + }, + "Compose_Authentication_Model_Result": { + "type": "Compose", + "inputs": "@coalesce(body('Call_Authentication_Workflow')?['authenticationModelResult'], 'FAILED')", + "runAfter": { + "SCIMTests_Scope": [ + "Succeeded", + "Failed", + "Skipped", + "TimedOut" + ] + } + }, + "Initialize_SCIM_Test_Variables_Variable_1": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "SchemaDiscoverabilityTestOutputs", + "type": "Object", + "value": { + "overallResult": "SKIPPED", + "result": "SKIPPED", + "summary": null, + "detailedResults": [], + "errorDetails": { + "summary": null + } + } + } + ] + }, + "runAfter": { + "Compose_Authentication_Methods": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_Variable_2": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "SCIMNullUpdateFailedActionName", + "type": "string", + "value": "" + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_Variable_1": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_Variable_3": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "SCIMNullUpdateScopeExecutionStatus", + "type": "string", + "value": "InProgress" + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_Variable_2": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_Variable_4": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "SCIMNullUpdateTestOutputs", + "type": "Object", + "value": { + "overallResult": "SKIPPED", + "result": "SKIPPED", + "summary": null, + "detailedResults": [], + "errorDetails": { + "summary": null + } + } + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_Variable_3": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_Variable_5": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimNullPatchOperations", + "type": "Array", + "value": [] + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_Variable_4": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_Variable_6": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimNullUpdateResults", + "type": "Array", + "value": [] + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_Variable_5": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_Variable_7": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimNullCreateBody", + "type": "Object", + "value": {} + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_Variable_6": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_Variable_10": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "flattenedUserAttrNames", + "type": "Array", + "value": [] + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_Variable_7": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_Variable_11": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "flattenedGroupAttrNames", + "type": "Array", + "value": [] + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_Variable_10": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_Variable_12": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimDiscoveredGroupBody", + "type": "Object", + "value": {} + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_Variable_11": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_Variable_13": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimDiscoveredGroupWritableAttributes", + "type": "Array", + "value": [] + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_Variable_12": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_Variable_14": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimCurrentGroupComplexBody", + "type": "Object", + "value": {} + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_Variable_13": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_Variable_15": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimGroupEmailExtensionUrn", + "type": "String", + "value": "" + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_Variable_14": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_1": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "SCIMUserCreateTestOutputs", + "type": "Object", + "value": { + "overallResult": "SKIPPED", + "result": "SKIPPED", + "summary": null, + "detailedResults": [], + "errorDetails": { + "summary": null + } + } + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_2": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "SCIMGroupCreateTestOutputs", + "type": "Object", + "value": { + "overallResult": "SKIPPED", + "result": "SKIPPED", + "summary": null, + "detailedResults": [], + "errorDetails": { + "summary": null + } + } + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_1": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_3": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimDirectCreatedUserId", + "type": "string", + "value": "" + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_2": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_4": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimDirectCreatedGroupId", + "type": "string", + "value": "" + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_3": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_5": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimDirectUserCreateBody", + "type": "Object", + "value": {} + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_4": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_6": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimDirectGroupCreateBody", + "type": "Object", + "value": {} + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_5": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_7": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "SCIMUserUpdateTestOutputs", + "type": "Object", + "value": { + "overallResult": "SKIPPED", + "result": "SKIPPED", + "summary": null, + "detailedResults": [], + "errorDetails": { + "summary": null } - ] - }, - "type": "If" - } + } + } + ] }, "runAfter": { - "Response_Immediate": [ + "Initialize_SCIM_Test_Variables_2_Variable_6": [ "Succeeded" ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_8": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "SCIMGroupUpdateTestOutputs", + "type": "Object", + "value": { + "overallResult": "SKIPPED", + "result": "SKIPPED", + "summary": null, + "detailedResults": [], + "errorDetails": { + "summary": null + } + } + } + ] }, - "type": "Scope" + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_7": [ + "Succeeded" + ] + } }, - "Compose_Final_Results": { + "Initialize_SCIM_Test_Variables_2_Variable_9": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimDirectUpdateUserId", + "type": "string", + "value": "" + } + ] + }, "runAfter": { - "SCIMTests_Scope": [ "Succeeded", "Failed", "Skipped", "TimedOut" ] + "Initialize_SCIM_Test_Variables_2_Variable_8": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_10": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimDirectUpdateGroupId", + "type": "string", + "value": "" + } + ] }, - "type": "Compose", + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_9": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_11": { + "type": "InitializeVariable", "inputs": { - "SchemaDiscoverabilityTestOutputs": "@variables('SchemaDiscoverabilityTestOutputs')", - "SCIMNullUpdateTestOutputs": "@variables('SCIMNullUpdateTestOutputs')", - "ValidateCredentialsTestOutputs": "@variables('ValidateCredentialsTestOutputs')", - "FederatedIdentityTestOutputs": "@variables('FederatedIdentityTestOutputs')", - "SCIMUserCreateTestOutputs": "@variables('SCIMUserCreateTestOutputs')", - "SCIMGroupCreateTestOutputs": "@variables('SCIMGroupCreateTestOutputs')", - "SCIMUserUpdateTestOutputs": "@variables('SCIMUserUpdateTestOutputs')", - "SCIMGroupUpdateTestOutputs": "@variables('SCIMGroupUpdateTestOutputs')", - "SCIMUserPaginationTestOutputs": "@variables('SCIMUserPaginationTestOutputs')", - "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" + "variables": [ + { + "name": "SCIMUserPaginationTestOutputs", + "type": "Object", + "value": { + "overallResult": "SKIPPED", + "result": "SKIPPED", + "summary": null, + "detailedResults": [], + "errorDetails": { + "summary": null + } + } + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_10": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_12": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimUserPaginationCreatedIds", + "type": "Array", + "value": [] + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_11": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_13": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimUserPaginationNextUrl", + "type": "string", + "value": "" + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_12": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_14": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimUserPaginationPageCount", + "type": "Integer", + "value": 0 + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_13": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_15": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "scimUserPaginationResults", + "type": "Array", + "value": [] + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_14": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_16": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "value": { + "overallResult": "SKIPPED", + "result": "SKIPPED", + "summary": null, + "detailedResults": [], + "errorDetails": { + "summary": null + } + }, + "name": "SCIMGroupPaginationTestOutputs", + "type": "Object" + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_15": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_17": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "value": [], + "name": "scimGroupPaginationCreatedIds", + "type": "Array" + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_16": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_18": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "value": "", + "name": "scimGroupPaginationNextUrl", + "type": "string" + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_17": [ + "Succeeded" + ] + } + }, + "Initialize_SCIM_Test_Variables_2_Variable_19": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "value": 0, + "name": "scimGroupPaginationPageCount", + "type": "Integer" + } + ] + }, + "runAfter": { + "Initialize_SCIM_Test_Variables_2_Variable_18": [ + "Succeeded" + ] } } }, "outputs": {} }, "kind": "Stateful" -} \ No newline at end of file +} diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMToken_Workflow.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMToken_Workflow.json new file mode 100644 index 00000000..ecb71991 --- /dev/null +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SCIMToken_Workflow.json @@ -0,0 +1,601 @@ +{ + "definition": { + "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#", + "contentVersion": "1.0.0.0", + "triggers": { + "manual": { + "type": "Request", + "kind": "Http", + "inputs": { + "schema": { + "type": "object" + } + } + } + }, + "actions": { + "Select_SCIM_Authentication": { + "type": "If", + "expression": "@equals(parameters('scimAuthenticationMethod'), 'OAuth2ClientCredentials')", + "actions": { + "Acquire_SCIM_OAuth_Token": { + "type": "Http", + "inputs": { + "uri": "@parameters('scimTokenEndpoint')", + "method": "POST", + "headers": "@if(equals(toLower(coalesce(parameters('scimCredentialLocationInRequest'), 'Header')), 'header'), setProperty(json('{\"Content-Type\":\"application/x-www-form-urlencoded\"}'), 'Authorization', concat('Basic ', base64(concat(encodeUriComponent(parameters('scimClientId')), ':', encodeUriComponent(parameters('scimClientSecret')))))), json('{\"Content-Type\":\"application/x-www-form-urlencoded\"}'))", + "body": "@concat('grant_type=client_credentials', if(equals(toLower(coalesce(parameters('scimCredentialLocationInRequest'), 'Header')), 'body'), concat('&client_id=', encodeUriComponent(parameters('scimClientId')), '&client_secret=', encodeUriComponent(parameters('scimClientSecret'))), ''), if(empty(coalesce(parameters('scimOAuthScope'), '')), '', concat('&scope=', encodeUriComponent(parameters('scimOAuthScope')))))", + "retryPolicy": { + "type": "exponential", + "count": 3, + "interval": "PT5S" + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Check_SCIM_OAuth_Response": { + "type": "If", + "expression": "@and(greaterOrEquals(coalesce(outputs('Acquire_SCIM_OAuth_Token')?['statusCode'], 0), 200), less(coalesce(outputs('Acquire_SCIM_OAuth_Token')?['statusCode'], 0), 300), not(empty(body('Acquire_SCIM_OAuth_Token')?['access_token'])), greater(int(coalesce(body('Acquire_SCIM_OAuth_Token')?['expires_in'], 3600)), 0), equals(toLower(coalesce(body('Acquire_SCIM_OAuth_Token')?['token_type'], 'Bearer')), 'bearer'))", + "actions": { + "Return_SCIM_OAuth_Token": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "body": { + "accessToken": "@body('Acquire_SCIM_OAuth_Token')?['access_token']", + "expiresAt": "@addSeconds(utcNow(), int(coalesce(body('Acquire_SCIM_OAuth_Token')?['expires_in'], 3600)))" + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs" + ] + } + } + } + }, + "else": { + "actions": { + "Return_SCIM_OAuth_Error": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 502, + "body": { + "error": "SCIMTokenAcquisitionFailed", + "message": "OAuth token acquisition failed or returned an invalid token response. No bearer-token fallback was attempted.", + "tokenEndpointStatus": "@coalesce(outputs('Acquire_SCIM_OAuth_Token')?['statusCode'], 0)" + } + } + } + } + }, + "runAfter": { + "Acquire_SCIM_OAuth_Token": [ + "Succeeded", + "Failed", + "TimedOut" + ] + } + } + }, + "else": { + "actions": { + "Select_SCIM_Federation": { + "type": "If", + "expression": "@equals(parameters('scimAuthenticationMethod'), 'WorkloadIdentityFederation')", + "actions": { + "Acquire_Entra_Token": { + "type": "Http", + "inputs": { + "uri": "@{concat('https://login.microsoftonline.com/', parameters('federatedEntraTenantId'), '/oauth2/v2.0/token')}", + "method": "POST", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "@{concat('grant_type=client_credentials&client_id=', encodeUriComponent(parameters('federatedApplicationId')), '&client_secret=', encodeUriComponent(parameters('federatedApplicationClientSecret')), '&scope=', encodeUriComponent(concat('api://', parameters('federatedApplicationId'), '/.default')))}" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Check_Entra_Token_Acquired": { + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Acquire_Entra_Token')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Acquire_Entra_Token')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@coalesce(body('Acquire_Entra_Token')?['access_token'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Check_Is_Google_SA_Flow": { + "type": "If", + "expression": { + "and": [ + { + "contains": [ + "@parameters('federatedTokenEndpoint')", + "googleapis.com" + ] + }, + { + "contains": [ + "@parameters('federatedTokenEndpoint')", + "serviceAccounts" + ] + } + ] + }, + "actions": { + "Acquire_Google_STS_Token": { + "type": "Http", + "inputs": { + "uri": "https://sts.googleapis.com/v1/token", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "audience": "@{parameters('federatedAudience')}", + "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "subject_token": "@{body('Acquire_Entra_Token')?['access_token']}", + "subject_token_type": "urn:ietf:params:oauth:token-type:id_token", + "scope": "https://www.googleapis.com/auth/cloud-platform" + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Check_Google_STS_Token": { + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Acquire_Google_STS_Token')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Acquire_Google_STS_Token')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@coalesce(body('Acquire_Google_STS_Token')?['access_token'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Acquire_Google_SA_Token": { + "type": "Http", + "inputs": { + "uri": "@{parameters('federatedTokenEndpoint')}", + "method": "POST", + "headers": { + "Content-Type": "application/json", + "Authorization": "@{concat('Bearer ', body('Acquire_Google_STS_Token')?['access_token'])}" + }, + "body": { + "scope": [ + "https://www.googleapis.com/auth/cloud-platform" + ] + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Check_Google_SA_Token": { + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Acquire_Google_SA_Token')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Acquire_Google_SA_Token')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@coalesce(body('Acquire_Google_SA_Token')?['accessToken'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Return_Google_SA_Token": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "body": { + "accessToken": "@body('Acquire_Google_SA_Token')?['accessToken']", + "expiresAt": "@coalesce(body('Acquire_Google_SA_Token')?['expireTime'], addSeconds(utcNow(), 3600))" + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs" + ] + } + } + } + }, + "else": { + "actions": { + "Return_Check_Google_SA_Token_Error": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 502, + "body": { + "error": "SCIMTokenAcquisitionFailed", + "stage": "Check_Google_SA_Token", + "message": "The selected authentication flow failed. No alternative or static token was used." + } + } + } + } + }, + "runAfter": { + "Acquire_Google_SA_Token": [ + "Succeeded", + "Failed", + "TimedOut" + ] + } + } + }, + "else": { + "actions": { + "Return_Check_Google_STS_Token_Error": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 502, + "body": { + "error": "SCIMTokenAcquisitionFailed", + "stage": "Check_Google_STS_Token", + "message": "The selected authentication flow failed. No alternative or static token was used." + } + } + } + } + }, + "runAfter": { + "Acquire_Google_STS_Token": [ + "Succeeded", + "Failed", + "TimedOut" + ] + } + } + }, + "else": { + "actions": { + "Check_Is_Google_STS_Flow": { + "type": "If", + "expression": { + "and": [ + { + "contains": [ + "@parameters('federatedTokenEndpoint')", + "googleapis.com" + ] + } + ] + }, + "actions": { + "Acquire_Google_STS_Token_Only": { + "type": "Http", + "inputs": { + "uri": "@{parameters('federatedTokenEndpoint')}", + "method": "POST", + "headers": { + "Content-Type": "application/json" + }, + "body": { + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "audience": "@{parameters('federatedAudience')}", + "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", + "subject_token": "@{body('Acquire_Entra_Token')?['access_token']}", + "subject_token_type": "urn:ietf:params:oauth:token-type:id_token", + "scope": "https://www.googleapis.com/auth/cloud-platform" + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Check_Google_STS_Only_Token": { + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Acquire_Google_STS_Token_Only')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Acquire_Google_STS_Token_Only')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@coalesce(body('Acquire_Google_STS_Token_Only')?['access_token'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Return_Google_STS_Token": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "body": { + "accessToken": "@body('Acquire_Google_STS_Token_Only')?['access_token']", + "expiresAt": "@addSeconds(utcNow(), int(coalesce(body('Acquire_Google_STS_Token_Only')?['expires_in'], 3600)))" + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs" + ] + } + } + } + }, + "else": { + "actions": { + "Return_Check_Google_STS_Only_Token_Error": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 502, + "body": { + "error": "SCIMTokenAcquisitionFailed", + "stage": "Check_Google_STS_Only_Token", + "message": "The selected authentication flow failed. No alternative or static token was used." + } + } + } + } + }, + "runAfter": { + "Acquire_Google_STS_Token_Only": [ + "Succeeded", + "Failed", + "TimedOut" + ] + } + } + }, + "else": { + "actions": { + "Acquire_Federated_Token": { + "type": "Http", + "inputs": { + "uri": "@{parameters('federatedTokenEndpoint')}", + "method": "POST", + "headers": { + "Content-Type": "application/x-www-form-urlencoded" + }, + "body": "@{concat('grant_type=client_credentials&client_id=', encodeUriComponent(parameters('federatedClientId')), '&client_assertion=', encodeUriComponent(body('Acquire_Entra_Token')?['access_token']), '&client_assertion_type=', encodeUriComponent('urn:ietf:params:oauth:client-assertion-type:jwt-bearer'), '&baseAddress=', encodeUriComponent(parameters('federatedBaseAddress')))}" + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Check_Federated_Token_Acquired": { + "type": "If", + "expression": { + "and": [ + { + "greaterOrEquals": [ + "@coalesce(outputs('Acquire_Federated_Token')?['statusCode'], 0)", + 200 + ] + }, + { + "less": [ + "@coalesce(outputs('Acquire_Federated_Token')?['statusCode'], 0)", + 300 + ] + }, + { + "not": { + "equals": [ + "@coalesce(body('Acquire_Federated_Token')?['access_token'], '')", + "" + ] + } + } + ] + }, + "actions": { + "Return_Federated_Token": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 200, + "body": { + "accessToken": "@body('Acquire_Federated_Token')?['access_token']", + "expiresAt": "@addSeconds(utcNow(), int(coalesce(body('Acquire_Federated_Token')?['expires_in'], 3600)))" + } + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs" + ] + } + } + } + }, + "else": { + "actions": { + "Return_Check_Federated_Token_Acquired_Error": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 502, + "body": { + "error": "SCIMTokenAcquisitionFailed", + "stage": "Check_Federated_Token_Acquired", + "message": "The selected authentication flow failed. No alternative or static token was used." + } + } + } + } + }, + "runAfter": { + "Acquire_Federated_Token": [ + "Succeeded", + "Failed", + "TimedOut" + ] + } + } + } + } + } + } + } + } + }, + "else": { + "actions": { + "Return_Check_Entra_Token_Acquired_Error": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 502, + "body": { + "error": "SCIMTokenAcquisitionFailed", + "stage": "Check_Entra_Token_Acquired", + "message": "The selected authentication flow failed. No alternative or static token was used." + } + } + } + } + }, + "runAfter": { + "Acquire_Entra_Token": [ + "Succeeded", + "Failed", + "TimedOut" + ] + } + } + }, + "else": { + "actions": { + "Reject_Unsupported_Transport": { + "type": "Response", + "kind": "Http", + "inputs": { + "statusCode": 502, + "body": { + "error": "SCIMTokenAcquisitionFailed", + "stage": "Select_SCIM_Authentication", + "message": "The selected authentication flow failed. No alternative or static token was used." + } + } + } + } + } + } + } + }, + "runAfter": {} + } + }, + "outputs": {} + }, + "kind": "Stateful" +} diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md index 44b27ece..515e5882 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/SetupLogicApp-Standard-Agent.md @@ -60,23 +60,21 @@ requirements: - Support a SCIM 2.0 user or group endpoint (only one is required, but supporting both a user and group endpoint is recommended) -- Support the OAuth 2.0 Client Credentials grant as your primary - authentication method - - - Note: Client Credentials is not required to participate in this - pilot (i.e. you need to use ***long lived* bearer token** to test - the Logic app). However, Client Credentials will be required to - onboard to the Microsoft Entra app gallery - - - Currently, Client Credentials is the only authentication method we - support for requests to onboard new provisioning integrations to the - Microsoft Entra app gallery - - - Requirements for Client Credentials: An admin portal where a - customer can generate a client ID and secret - - - Best practices: Support the ability to rotate secrets and delete old - secrets +- Support at least one gallery authentication method tested by this + bundle: **OAuth 2.0 client credentials** or **workload identity + federation (WIF)**. Declare which methods are actually available in + the validation environment; an unsupported alternative is Not + applicable, not a failed requirement. + + - OAuth integrations need a client ID, secret, token endpoint, + credential location (Header or Body), and optional scope. + - WIF integrations supply the parameters in the + [federation setup guide](SCIMTests_WorkloadIdentityFederation_Setup.md). + Preserve and validate the application's supported federated + provisioning configuration rather than inventing connectivity keys. + - Static-token-only environments can run functional tests but do + **not** meet gallery authentication readiness. + - Support secret rotation and removal of old secrets where applicable. - Support updating multiple group memberships with a single PATCH request @@ -303,7 +301,7 @@ Before using the agent, ensure you have: #### Validate my SCIM integration for Entra app gallery onboarding -#### The agent will ask for your SCIM endpoint, bearer token, OAuth credentials (if applicable), and guide you through the entire validation workflow. +#### The agent accepts initial parameters, asks which authentication methods are available, collects only applicable credentials, and guides you through validation and report export. When the agent requests to run commands, review and approve them @@ -315,11 +313,13 @@ what to expect: Agent: What is your SCIM endpoint URL? You: https://api.myapp.com/scim/v2 - Agent: What is your bearer token? - You: eyJhbGciOiJSUzI1NiIs... + Agent: Which authentication methods are available in your validation environment? + You: OAuth 2.0 client credentials only - Agent: Does your SCIM endpoint use OAuth client credentials or a static bearer token? - You: Static bearer token + Agent: What is your OAuth client ID? + You: + + Agent: I'll collect the remaining OAuth settings. No static token or WIF credentials are needed. Agent: ✅ Azure CLI — Logged in as admin@contoso.onmicrosoft.com ✅ SCIM endpoint — HTTP 200 @@ -387,26 +387,20 @@ Microsoft Entra ID will send provisioning requests to. this URL. If your endpoint requires this flag, it should only be configured in the Entra portal’s Tenant URL field, not here. -### Question 2: Bearer Token - -> “What is your bearer token for the SCIM endpoint?” - -Provide a long-lived bearer token that the Logic App will use to -authenticate against your SCIM endpoint. - -**Important:** The token must remain valid for at least 2 hours (the -test run can take 60–90 minutes). If your token is a short-lived JWT, -the tests will fail partway through. Use a token that lasts at least 24 -hours. +### Question 2: Available Authentication Methods -### Question 3: Authentication Method +Choose **OAuth only**, **WIF only**, **both**, or **static token only +(functional validation, not gallery-ready)**. You may instead supply +`supportedAuthenticationMethods` initially, using +`OAuth2ClientCredentials`, `WorkloadIdentityFederation`, and/or `Bearer`. +If both gallery methods are available, choose which should authenticate +the core SCIM requests using `scimAuthenticationMethod`. -> “Does your SCIM endpoint use OAuth client credentials or a static -> bearer token?” +### Question 3: Applicable Credentials -- **Static bearer token** — Most common for pilot testing. The agent - will note that `Validate_Credentials_Test` will be skipped (this is - expected). +- **Static bearer token** — Collect a token only for selected Bearer + transport. It must remain valid for the complete run. Neither gallery + method is satisfied merely by successful static-token connectivity. - **OAuth client credentials** — If you choose this, the agent will ask 5 follow-up questions: - **Client ID** — Your OAuth application’s client ID @@ -418,6 +412,41 @@ hours. - **OAuth Scope** — Optional; enter multiple scopes as one string separated by spaces, or enter `none` +- **Workload identity federation** — Collect only the applicable + federation parameters from the setup guide. No OAuth token endpoint + is required merely to keep the parameter contract complete. + +OAuth-only and WIF-only runs set `scimBearerToken` to `""`. +`SCIMToken_Workflow` acquires tokens at runtime, and each core suite +refreshes its cache before expiry. It never falls back to a static +token. Entra provisioning credentials are configured and validated +separately using the selected supported method. + +If an OAuth probe succeeds but Entra's saved-credential Test Connection +fails, the agent distinguishes invalid credentials from a persistence +problem. An inline OAuth success is diagnostic only. The agent can +persist that same validated OAuth credential array through +`/synchronization/secrets`, then must revalidate the **saved** credentials +before starting provisioning. It never substitutes a static token. + +The agent clears undeclared credentials and reports those tests as +**Not applicable** without token requests. Missing credentials for a +declared method must be supplied or reported as blocked; they are not +silently treated as an unsupported method. + +### Authentication readiness + +Every selected authentication method must pass its own test (AND). +Selecting both OAuth and WIF requires both to pass; a selected failure, +skip, or missing result blocks authentication readiness. An undeclared +method is Not applicable and issues no authentication requests. +Other mandatory functional failures remain independent readiness gates. + +Use one coherent bundle: seven workflows, the complete parameters file, +the canonical agent, authentication tests, and [VERSION](VERSION). +Keep `LogicAppVersion` synchronized with VERSION. This change does not +require a portable report exporter; reporting is a separate follow-up. +See [Authentication-Changes.md](Authentication-Changes.md). ### Question 4: Azure Subscription Selection > “Which Azure subscription would you like to use?” @@ -820,9 +849,13 @@ necessary roles to the managed identity of your Logic app. ### Logic App Explanation: The Logic App is built on Azure Logic Apps Standard and is divided into -separate sections. It consists of 5 workflows that work together using a +separate sections. It consists of 7 workflows that work together using a nested workflow architecture. The Orchestrator workflow is the entry -point, and it calls the other workflows as child workflows. +point, and it calls the initialization and test workflows as children. +SCIMTests calls Authentication to run the selected authentication checks +without approaching the 500-action per-workflow limit. The test suites +call the shared SCIMToken workflow when the selected OAuth/WIF token cache +needs refreshing; neither helper increases the 25-test inventory. Initialization workflow initializes the required steps to run the tests in the Logic App. @@ -1244,9 +1277,10 @@ 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 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. +- An undeclared OAuth or WIF method is Not applicable. Every selected + method must pass; at least one gallery method must be selected. + A static-token-only run is not gallery-ready even when connectivity + and functional tests pass. - User_Update_Manager_Test and SCIM_Update_Manager_Test being SKIPPED when the manager attribute is not present in the target directory diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Test-Authentication.ps1 b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Test-Authentication.ps1 new file mode 100644 index 00000000..cee6b399 --- /dev/null +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/Test-Authentication.ps1 @@ -0,0 +1,288 @@ +[CmdletBinding()] +param() +$ErrorActionPreference = 'Stop' +$root = $PSScriptRoot +$scim = Get-Content -LiteralPath "$root\SCIMTests_Workflow.json" -Raw | ConvertFrom-Json -Depth 100 +$authentication = Get-Content -LiteralPath "$root\Authentication_Workflow.json" -Raw | ConvertFrom-Json -Depth 100 +$orchestrator = Get-Content -LiteralPath "$root\Orchestrator_Workflow.json" -Raw | ConvertFrom-Json -Depth 100 +$parameters = Get-Content -LiteralPath "$root\Orchestrator_Parameters.json" -Raw | ConvertFrom-Json -Depth 100 +$script:Assertions = 0 + +function Assert-Equal($Actual, $Expected, [string]$Message) { + $script:Assertions++ + if (($Actual | ConvertTo-Json -Depth 50 -Compress) -cne ($Expected | ConvertTo-Json -Depth 50 -Compress)) { + throw "$Message`: expected '$Expected', got '$Actual'." + } +} + +function Invoke-WdlExpression([string]$Expression, [hashtable]$Context) { + $source = $Expression.TrimStart('@') + if ($source.StartsWith('{')) { $source = $source.Substring(1, $source.Length - 2) } + $tokens = [Collections.Generic.List[object]]::new() + $pattern = "\G\s*(?:(?'(?:[^']|'')*')|(?[A-Za-z_][A-Za-z0-9_]*)|(?-?\d+)|(?\?\[|[\(\),\]\[]))" + $position = 0 + while ($position -lt $source.Length) { + $match = [regex]::Match($source, $pattern, [Text.RegularExpressions.RegexOptions]::None, [timespan]::FromSeconds(1)) + if (-not $match.Success -or $match.Index -ne $position) { + throw "Unsupported WDL syntax at '$($source.Substring($position))'." + } + $kind = @('literal','word','number','symbol') | Where-Object {$match.Groups[$_].Success} | Select-Object -First 1 + $tokens.Add(@{kind=$kind;value=$match.Groups[$kind].Value}) + $position += $match.Length + # \G is relative to the input; consume the recognized prefix explicitly. + $source = $source.Substring($position) + $position = 0 + } + $state = @{index=0;tokens=$tokens;context=$Context} + $result = Read-WdlValue $state + if ($state.index -ne $tokens.Count) { throw 'Trailing WDL tokens.' } + return $result.value +} + +function Read-WdlValue([hashtable]$State) { + $token = $State.tokens[$State.index++] + if ($token.kind -eq 'literal') { + $value = $token.value.Substring(1,$token.value.Length-2).Replace("''","'") + } elseif ($token.kind -eq 'number') { + $value = [long]$token.value + } elseif ($token.value -in @('true','false','null')) { + $value = switch ($token.value) { 'true' {$true} 'false' {$false} 'null' {$null} } + } else { + if ($State.tokens[$State.index++].value -ne '(') { throw 'Expected WDL function call.' } + $arguments = [Collections.Generic.List[object]]::new() + if ($State.tokens[$State.index].value -ne ')') { + do { + $arguments.Add((Read-WdlValue $State).value) + if ($State.tokens[$State.index].value -ne ',') { break } + $State.index++ + } while ($true) + } + if ($State.tokens[$State.index++].value -ne ')') { throw 'Unclosed WDL call.' } + $a = $arguments.ToArray() + $value = switch ($token.value) { + 'parameters' { ,$State.context.parameters[$a[0]] } + 'variables' { ,$State.context.variables[$a[0]] } + 'outputs' { ,$State.context.outputs[$a[0]] } + 'body' { ,$State.context.body[$a[0]] } + 'actions' { ,$State.context.actions[$a[0]] } + 'triggerBody' { $State.context.triggerBody } + 'if' { if ($a[0]) { ,$a[1] } else { ,$a[2] } } + 'and' { $a -notcontains $false } + 'or' { $a -contains $true } + 'not' { -not $a[0] } + 'equals' { ($a[0] | ConvertTo-Json -Depth 30 -Compress) -ceq ($a[1] | ConvertTo-Json -Depth 30 -Compress) } + 'contains' { if ($a[0] -is [string]) {$a[0].Contains([string]$a[1])} else {$a[0] -ccontains $a[1]} } + 'empty' { $null -eq $a[0] -or $a[0].Count -eq 0 -or $a[0] -ceq '' } + 'length' { if ($a[0] -is [string]) {$a[0].Length} else {@($a[0]).Count} } + 'createArray' { ,$a } + 'json' { ,(ConvertFrom-Json -InputObject $a[0] -Depth 50 -NoEnumerate) } + 'union' { ,@($a | ForEach-Object {$_} | Select-Object -Unique) } + 'intersection' { ,@($a[0] | Where-Object {$_ -cin $a[1]} | Select-Object -Unique) } + 'coalesce' { ,($a | Where-Object {$null -ne $_} | Select-Object -First 1) } + 'concat' { ($a | ForEach-Object {[string]$_}) -join '' } + 'ticks' { ([datetimeoffset]::Parse($a[0])).Ticks } + 'utcNow' { $State.context.now } + 'addMinutes' { ([datetimeoffset]::Parse($a[0])).AddMinutes([double]$a[1]).ToString('o') } + 'lessOrEquals' { $a[0] -le $a[1] } + 'string' { if ($a[0] -is [string]) {$a[0]} else {ConvertTo-Json -InputObject $a[0] -Depth 30 -Compress} } + default { throw "Unsupported WDL function '$($token.value)'." } + } + } + while ($State.index -lt $State.tokens.Count -and $State.tokens[$State.index].value -in @('[','?[')) { + $State.index++ + $key = (Read-WdlValue $State).value + if ($State.tokens[$State.index++].value -ne ']') { throw 'Unclosed WDL property access.' } + $value = if ($null -eq $value) {$null} elseif ($value -is [Collections.IDictionary]) {$value[$key]} else {$value.$key} + } + return [pscustomobject]@{value=$value} +} + +function Invoke-WdlCondition($Condition, [hashtable]$Context) { + if ($Condition -is [string]) { + if ($Condition.StartsWith('@')) { return Invoke-WdlExpression $Condition $Context } + return $Condition + } + if ($Condition -isnot [pscustomobject]) { return $Condition } + $property = @($Condition.PSObject.Properties)[0] + $values = @($property.Value | ForEach-Object {Invoke-WdlCondition $_ $Context}) + switch ($property.Name) { + 'and' { return $values -notcontains $false } + 'or' { return $values -contains $true } + 'contains' { return $values[0] -ccontains $values[1] } + 'equals' { return $values[0] -ceq $values[1] } + default { throw "Unsupported structured condition '$($property.Name)'." } + } +} + +function New-TestContext([string[]]$Methods, [string]$OAuth, [string]$Wif) { + @{ + parameters=@{supportedAuthenticationMethods=$Methods;scimTokenEndpoint='';federatedTokenEndpoint='';scimBearerToken='static-sentinel';scimAuthenticationMethod='Bearer'} + variables=@{ValidateCredentialsTestOutputs=@{overallResult=$OAuth};FederatedIdentityTestOutputs=@{overallResult=$Wif};BearerConnectivityTestOutputs=@{overallResult='PASSED'};SCIMAccessTokenCache=@{}} + outputs=@{Compose_Authentication_Methods=$Methods} + body=@{} + actions=@{} + triggerBody=@{EnabledTests='All';initializationData=@{targetDirectoryUserAttributes=@()}} + now='2026-01-01T00:00:00Z' + } +} + +$cases = @( + @{methods=@('OAuth2ClientCredentials');oauth='PASSED';wif='NOT_APPLICABLE';expected='PASSED'}, + @{methods=@('WorkloadIdentityFederation');oauth='NOT_APPLICABLE';wif='PASSED';expected='PASSED'}, + @{methods=@('OAuth2ClientCredentials','WorkloadIdentityFederation');oauth='PASSED';wif='PASSED';expected='PASSED'}, + @{methods=@('OAuth2ClientCredentials','WorkloadIdentityFederation');oauth='PASSED';wif='FAILED';expected='FAILED'}, + @{methods=@('OAuth2ClientCredentials','WorkloadIdentityFederation');oauth='FAILED';wif='PASSED';expected='FAILED'}, + @{methods=@('OAuth2ClientCredentials','WorkloadIdentityFederation');oauth='FAILED';wif='FAILED';expected='FAILED'}, + @{methods=@('OAuth2ClientCredentials');oauth='FAILED';wif='PASSED';expected='FAILED'}, + @{methods=@('WorkloadIdentityFederation');oauth='PASSED';wif='SKIPPED';expected='FAILED'}, + @{methods=@('Bearer');oauth='NOT_APPLICABLE';wif='NOT_APPLICABLE';expected='NOT_GALLERY_READY'}, + @{methods=@();oauth='SKIPPED';wif='SKIPPED';expected='FAILED'}, + @{methods=@('invalid','OAuth2ClientCredentials');oauth='PASSED';wif='SKIPPED';expected='FAILED'}, + @{methods=@('OAuth2ClientCredentials','WorkloadIdentityFederation');oauth='PASSED';wif='NOT_APPLICABLE';expected='FAILED'}, + @{methods=@('OAuth2ClientCredentials','WorkloadIdentityFederation');oauth='PASSED';wif=$null;expected='FAILED'}, + @{methods=@('Bearer','OAuth2ClientCredentials');oauth='PASSED';wif='NOT_APPLICABLE';expected='PASSED'} +) +foreach ($case in $cases) { + $context = New-TestContext $case.methods $case.oauth $case.wif + $actual = Invoke-WdlExpression $authentication.definition.actions.Compose_Authentication_Model_Result.inputs $context + Assert-Equal $actual $case.expected "Auth matrix $($case.methods -join ',')" +} +$oauth = New-TestContext @('OAuth2ClientCredentials') 'PASSED' 'NOT_APPLICABLE' +Assert-Equal (Invoke-WdlCondition $authentication.definition.actions.Authentication_Tests.actions.Validate_Credentials_Test.expression $oauth) $true 'Declared OAuth executes' +Assert-Equal (Invoke-WdlCondition $authentication.definition.actions.Authentication_Tests.actions.Federated_Identity_Test.expression $oauth) $false 'Undeclared WIF makes no token request' +$wif = New-TestContext @('WorkloadIdentityFederation') 'NOT_APPLICABLE' 'PASSED' +Assert-Equal (Invoke-WdlCondition $authentication.definition.actions.Authentication_Tests.actions.Validate_Credentials_Test.expression $wif) $false 'Undeclared OAuth makes no token request' +Assert-Equal (Invoke-WdlCondition $authentication.definition.actions.Authentication_Tests.actions.Federated_Identity_Test.expression $wif) $true 'Declared WIF executes' +$mixedBearer = New-TestContext @('Bearer','OAuth2ClientCredentials') 'PASSED' 'NOT_APPLICABLE' +$mixedBearer.variables.BearerConnectivityTestOutputs.overallResult = 'FAILED' +Assert-Equal (Invoke-WdlExpression $authentication.definition.actions.Compose_Authentication_Model_Result.inputs $mixedBearer) 'FAILED' 'Every selected method includes static-token connectivity' +Assert-Equal (Invoke-WdlCondition $authentication.definition.actions.Authentication_Tests.actions.Bearer_Connectivity_Test.expression $oauth) $false 'OAuth does not execute static-token connectivity' +$legacy = New-TestContext @() 'SKIPPED' 'PASSED' +$legacy.parameters.federatedTokenEndpoint = 'https://example.invalid/exchange' +Assert-Equal @(Invoke-WdlExpression $scim.definition.actions.Compose_Authentication_Methods.inputs $legacy) @('WorkloadIdentityFederation') 'Legacy WIF-only inference' +$legacy.parameters.federatedTokenEndpoint = '' +$legacy.parameters.scimTokenEndpoint = 'https://example.invalid/token' +Assert-Equal @(Invoke-WdlExpression $scim.definition.actions.Compose_Authentication_Methods.inputs $legacy) @('OAuth2ClientCredentials') 'Legacy OAuth-only inference' +$defaults = @($authentication.definition.actions.PSObject.Properties | Where-Object {$_.Value.type -eq 'InitializeVariable'} | ForEach-Object {$_.Value.inputs.variables}) +foreach ($variable in $defaults | Where-Object name -in @('ValidateCredentialsTestOutputs','FederatedIdentityTestOutputs')) { + $result = Invoke-WdlExpression $variable.value.overallResult $oauth + $expected = if ($variable.name -eq 'ValidateCredentialsTestOutputs') {'SKIPPED'} else {'NOT_APPLICABLE'} + Assert-Equal $result $expected 'Applicability output default' +} +$both = New-TestContext @('OAuth2ClientCredentials','WorkloadIdentityFederation') 'PASSED' 'PASSED' +foreach ($context in @($oauth,$wif,$both)) { + foreach ($name in @('SchemaDiscoverabilityTestOutputs','SCIMNullUpdateTestOutputs','SCIMUserCreateTestOutputs','SCIMGroupCreateTestOutputs','SCIMUserUpdateTestOutputs','SCIMGroupUpdateTestOutputs','SCIMUpdateManagerTestOutputs')) { + $context.variables[$name] = @{overallResult='PASSED'} + } + $missingChild = New-TestContext @('OAuth2ClientCredentials') 'PASSED' 'NOT_APPLICABLE' + Assert-Equal (Invoke-WdlExpression $scim.definition.actions.Compose_Authentication_Model_Result.inputs $missingChild) 'FAILED' 'Missing authentication workflow output fails closed' + $missingChild.body.Call_Authentication_Workflow = @{authenticationModelResult='PASSED'} + Assert-Equal (Invoke-WdlExpression $scim.definition.actions.Compose_Authentication_Model_Result.inputs $missingChild) 'PASSED' 'SCIM suite propagates the observed authentication result' + foreach ($case in $cases) { + $context = New-TestContext $case.methods $case.oauth $case.wif + foreach ($name in @('SchemaDiscoverabilityTestOutputs','SCIMNullUpdateTestOutputs','SCIMUserCreateTestOutputs','SCIMGroupCreateTestOutputs','SCIMUserUpdateTestOutputs','SCIMGroupUpdateTestOutputs','SCIMUpdateManagerTestOutputs')) { + $context.variables[$name] = @{overallResult='PASSED'} + } + $context.outputs.Compose_Authentication_Model_Result = Invoke-WdlExpression $authentication.definition.actions.Compose_Authentication_Model_Result.inputs $context + $expected = if ($case.expected -eq 'PASSED') {'Passed'} else {'Failed'} + Assert-Equal (Invoke-WdlExpression $scim.definition.actions.Compose_Final_Results.inputs.status $context) $expected 'Selected-method aggregate controls suite success' + } + $context.outputs.Compose_Authentication_Model_Result = 'PASSED' + Assert-Equal (Invoke-WdlExpression $scim.definition.actions.Compose_Final_Results.inputs.status $context) 'Passed' 'Undeclared authentication does not fail the SCIM suite' + $context.variables.SchemaDiscoverabilityTestOutputs.overallResult = 'FAILED' + Assert-Equal (Invoke-WdlExpression $scim.definition.actions.Compose_Final_Results.inputs.status $context) 'Failed' 'Core failure is not hidden by valid auth' +} + +function Get-Actions($Actions, [int]$Depth=0) { + foreach ($property in $Actions.PSObject.Properties) { + [pscustomobject]@{name=$property.Name;action=$property.Value;siblings=$Actions;depth=$Depth} + if ($property.Value.actions) { Get-Actions $property.Value.actions ($Depth + 1) } + if ($property.Value.else.actions) { Get-Actions $property.Value.else.actions ($Depth + 1) } + foreach ($case in $property.Value.cases.PSObject.Properties) { + if ($case.Value.actions) { Get-Actions $case.Value.actions ($Depth + 1) } + } + if ($property.Value.default.actions) { Get-Actions $property.Value.default.actions ($Depth + 1) } + } +} +$requestCount = 0 +foreach ($file in Get-ChildItem -LiteralPath $root -Filter '*_Workflow.json') { + $workflow = Get-Content -LiteralPath $file.FullName -Raw | ConvertFrom-Json -Depth 100 + Assert-Equal ([IO.File]::ReadAllText($file.FullName).Contains("`r")) $false "Workflow JSON uses LF line endings: $($file.Name)" + $actions = @(Get-Actions $workflow.definition.actions) + Assert-Equal ($actions.Count -le 500) $true "Workflow remains within the 500-action limit: $($file.Name), $($actions.Count)" + Assert-Equal ($file.BaseName.Length -le 32) $true "Standard workflow name stays within 32 characters: $($file.BaseName)" + Assert-Equal @($actions | Group-Object name | Where-Object Count -gt 1).Count 0 "Unique action names in $($file.Name)" + foreach ($entry in $actions) { + Assert-Equal ($entry.name.Length -le 80) $true "Action name stays within 80 characters: $($entry.name)" + Assert-Equal ($entry.depth -le 8) $true "Action nesting stays within eight levels: $($entry.name)" + foreach ($dependency in $entry.action.runAfter.PSObject.Properties.Name) { + Assert-Equal ($dependency -in @($entry.siblings.PSObject.Properties.Name)) $true "Dependency exists in the same scope: $($entry.name) -> $dependency" + } + if ($entry.action.type -eq 'InitializeVariable') { + Assert-Equal @($entry.action.inputs.variables).Count 1 'Standard runtime initializes one variable per action' + } + $securedProperties = @($entry.action.runtimeConfiguration.secureData.properties | Where-Object {$_}) + if ($entry.action.type -in @('InitializeVariable','SetVariable','AppendToArrayVariable','AppendToStringVariable')) { + Assert-Equal $securedProperties.Count 0 'Variable actions do not use unsupported secureData' + } + if ($entry.action.type -in @('Response','Compose','ParseJson')) { + Assert-Equal @($securedProperties | Where-Object {$_ -ne 'inputs'}).Count 0 'Response and Compose support secured inputs, not secured outputs' + } + } + foreach ($entry in $actions | Where-Object {$_.action.type -eq 'Http' -and $_.action.inputs.headers.Authorization -is [string] -and $_.action.inputs.headers.Authorization.Contains("parameters('scimBearerToken')")}) { + if ($file.Name -eq 'Authentication_Workflow.json') { continue } + $requestCount++ + $action = $entry.action + $dependency = @($action.runAfter.PSObject.Properties.Name) + Assert-Equal $dependency.Count 1 'SCIM request has exactly one auth dependency' + $ensure = $entry.siblings.($dependency[0]) + Assert-Equal $ensure.type 'If' 'SCIM request waits for token cache guard' + $context = New-TestContext @('OAuth2ClientCredentials') 'PASSED' 'NOT_APPLICABLE' + $context.parameters.scimAuthenticationMethod = 'OAuth2ClientCredentials' + Assert-Equal (Invoke-WdlExpression $ensure.expression $context) $true 'Empty OAuth cache refreshes' + $context.variables.SCIMAccessTokenCache = @{accessToken='minted-token';expiresAt='2026-01-01T00:30:00Z'} + Assert-Equal (Invoke-WdlExpression $ensure.expression $context) $false 'Unexpired OAuth cache is reused' + Assert-Equal (Invoke-WdlExpression $action.inputs.headers.Authorization $context) 'Bearer minted-token' 'OAuth never falls back to the static token' + $context.variables.SCIMAccessTokenCache.expiresAt = '2026-01-01T00:04:00Z' + Assert-Equal (Invoke-WdlExpression $ensure.expression $context) $true 'Near-expiry OAuth cache refreshes' + $context.parameters.scimAuthenticationMethod = 'WorkloadIdentityFederation' + Assert-Equal (Invoke-WdlExpression $ensure.expression $context) $true 'Near-expiry WIF cache refreshes' + Assert-Equal (Invoke-WdlExpression $action.inputs.headers.Authorization $context) 'Bearer minted-token' 'WIF never falls back to a static token' + $context.parameters.scimAuthenticationMethod = 'Bearer' + Assert-Equal (Invoke-WdlExpression $ensure.expression $context) $false 'Bearer transport makes no OAuth request' + Assert-Equal (Invoke-WdlExpression $action.inputs.headers.Authorization $context) 'Bearer static-sentinel' 'Existing bearer behavior is preserved' + } +} +Assert-Equal $requestCount 99 'All direct SCIM requests are covered' +Assert-Equal $parameters.scimAuthenticationMethod.value 'Bearer' 'Legacy transport remains the default' +Assert-Equal @($parameters.scimAuthenticationMethod.allowedValues) @('Bearer','OAuth2ClientCredentials','WorkloadIdentityFederation') 'All supported direct-request transports are available' +$tokenWorkflow = Get-Content -LiteralPath "$root\SCIMToken_Workflow.json" -Raw | ConvertFrom-Json -Depth 100 +$tokenActions = @(Get-Actions $tokenWorkflow.definition.actions) +$existingWifActions = @(Get-Actions $authentication.definition.actions.Authentication_Tests.actions.Federated_Identity_Test.actions) +foreach ($entry in $existingWifActions | Where-Object {$_.action.type -eq 'Http'}) { + $tokenAction = @($tokenActions | Where-Object name -eq $entry.name) + Assert-Equal $tokenAction.Count 1 "WIF exchange is reused: $($entry.name)" + Assert-Equal $tokenAction[0].action.inputs $entry.action.inputs "WIF exchange contract preserved: $($entry.name)" + Assert-Equal @($tokenAction[0].action.runtimeConfiguration.secureData.properties) @('inputs','outputs') 'WIF exchange secrets are protected' +} +Assert-Equal $scim.definition.actions.Compose_Final_Results.inputs.authenticationModelResult "@outputs('Compose_Authentication_Model_Result')" 'SCIM suite emits the missing authentication result' +Assert-Equal $scim.definition.actions.SCIMTests_Scope.actions.Call_Authentication_Workflow.inputs.host.workflow.id 'Authentication_Workflow' 'Authentication runs in a separate horizontal child workflow' +Assert-Equal $authentication.definition.actions.Return_Authentication_Results.inputs.body.authenticationPolicy 'AllSelected' 'Authentication policy is explicit in run output' +Assert-Equal $parameters.LogicAppVersion.value (Get-Content -LiteralPath "$root\VERSION" -Raw).Trim() 'VERSION is the only bundle and agent version source' +$versionBytes = [IO.File]::ReadAllBytes("$root\VERSION") +Assert-Equal @($versionBytes | Where-Object {$_ -gt 127 -or $_ -eq 13}).Count 0 'VERSION is ASCII without BOM or CRLF' +Assert-Equal ([IO.File]::ReadAllText("$root\VERSION")) ($parameters.LogicAppVersion.value + "`n") 'VERSION has exactly one LF terminator' +Assert-Equal ([IO.File]::ReadAllText("$root\Orchestrator_Parameters.json").Contains("`r")) $false 'Parameter JSON uses LF line endings' +foreach ($name in @('Evaluate_Test_Results','Final_TestResults')) { + Assert-Equal $orchestrator.definition.actions.$name.inputs.logicAppVersion "@parameters('LogicAppVersion')" 'Run artifacts include deployed bundle version' + Assert-Equal $orchestrator.definition.actions.$name.inputs.authenticationPolicy 'AllSelected' 'Orchestrator output identifies AND policy' +} +$authenticationTests = @($orchestrator.definition.actions.Final_TestResults.inputs.testResults | Where-Object authenticationMethod -in @('OAuth2ClientCredentials','WorkloadIdentityFederation')) +Assert-Equal $authenticationTests.Count 2 'The existing report inventory retains both authentication test names' +foreach ($test in $authenticationTests) { + Assert-Equal $test.testCategory 'Declared/StrictSuccess' 'Each declared authentication test is individually strict' +} +if (-not $orchestrator.definition.actions.Evaluate_Test_Results.inputs.overallLogicAppResult.Contains("not(equals(coalesce(body('Get_SCIMTests_Output_Content')?['authenticationModelResult'],'FAILED'),'PASSED'))")) { + throw 'Orchestrator must reject missing or non-passing authentication readiness.' +} +Write-Output "PASS: $script:Assertions assertions; auth policy matrix and $requestCount direct SCIM requests verified." diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/UserTests_Workflow.json b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/UserTests_Workflow.json index 7f2d667b..66f625e9 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/UserTests_Workflow.json +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/UserTests_Workflow.json @@ -90,14 +90,18 @@ } }, "Initialize_AttributeMatchExcludeList": { - "runAfter": {}, + "runAfter": { + "Initialize_AttributeMatchExcludeList_Variable_1": [ + "Succeeded" + ] + }, "type": "InitializeVariable", "inputs": { "variables": [ { - "name": "attributeMatchExcludeList", - "type": "Array", - "value": "@triggerBody()?['initializationData']?['updateTestExcludeList']" + "name": "SCIMAccessTokenCache", + "type": "Object", + "value": {} } ] } @@ -114,9 +118,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['createUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Create_User_Initial_Verify_User_Exists": [ + "Succeeded" + ] } }, "Compose_CreateUser_Body": { @@ -180,9 +189,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['createUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Query_Created_User": [ + "Succeeded" + ] } }, "Delay_Create_User_15s": { @@ -199,6 +213,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_SCIM_Query_Created_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_nsure_SCIM_Token_SCIM_Query_Created_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_nsure_SCIM_Token_SCIM_Query_Created_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_nsure_SCIM_Token_SCIM_Query_Created_User')" + }, + "runAfter": { + "Acquire_Token_nsure_SCIM_Token_SCIM_Query_Created_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -258,6 +314,48 @@ "timeout": "PT15M" }, "type": "Until" + }, + "Ensure_SCIM_Token_Create_User_Initial_Verify_User_Exists": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_n_Create_User_Initial_Verify_User_Exists": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_n_Create_User_Initial_Verify_User_Exists": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_n_Create_User_Initial_Verify_User_Exists')" + }, + "runAfter": { + "Acquire_Token_n_Create_User_Initial_Verify_User_Exists": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "type": "Scope" @@ -449,9 +547,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['updateUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Update_User_-_Initial_Verify_User_Exists": [ + "Succeeded" + ] } }, "Update_User_-_Create_User_Graph": { @@ -506,10 +609,15 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['updateUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}", "Content-Type": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Update_User_Verify_User_Created": [ + "Succeeded" + ] } }, "Update_Delay_Creation_15s": { @@ -526,6 +634,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_Update_User_Verify_User_Created": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_IM_Token_Update_User_Verify_User_Created": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_IM_Token_Update_User_Verify_User_Created": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_IM_Token_Update_User_Verify_User_Created')" + }, + "runAfter": { + "Acquire_Token_IM_Token_Update_User_Verify_User_Created": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -583,6 +733,48 @@ "timeout": "PT15M" }, "type": "Until" + }, + "Ensure_SCIM_Token_Update_User_-_Initial_Verify_User_Exists": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Update_User_-_Initial_Verify_User_Exists": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Update_User_-_Initial_Verify_User_Exists": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Update_User_-_Initial_Verify_User_Exists')" + }, + "runAfter": { + "Acquire_Token_Update_User_-_Initial_Verify_User_Exists": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "type": "Scope" @@ -620,13 +812,17 @@ "Update_User_Verify_Update_And_Provisioning": { "actions": { "Update_User_Query_User_By_Id": { - "runAfter": {}, + "runAfter": { + "Ensure_SCIM_Token_Update_User_Query_User_By_Id": [ + "Succeeded" + ] + }, "type": "Http", "inputs": { "uri": "@concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['updateUser'], ''), '%22')", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } @@ -660,9 +856,14 @@ "uri": "@concat(parameters('scimEndpoint'), '/Users/', coalesce(first(body('Update_User_Query_User_By_Id')?['Resources'])?['id'], ''))", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Update_User_-_Query_User_By_Id_After_Attributes_Update": [ + "Succeeded" + ] } }, "Update_Delay_Final_15s": { @@ -679,6 +880,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_Update_User_-_Query_User_By_Id_After_Attributes_Update": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Query_User_By_Id_After_Attributes_Update": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Query_User_By_Id_After_Attributes_Update": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Query_User_By_Id_After_Attributes_Update')" + }, + "runAfter": { + "Acquire_Token_Query_User_By_Id_After_Attributes_Update": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -883,6 +1126,48 @@ "timeout": "PT15M" }, "type": "Until" + }, + "Ensure_SCIM_Token_Update_User_Query_User_By_Id": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token__SCIM_Token_Update_User_Query_User_By_Id": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token__SCIM_Token_Update_User_Query_User_By_Id": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token__SCIM_Token_Update_User_Query_User_By_Id')" + }, + "runAfter": { + "Acquire_Token__SCIM_Token_Update_User_Query_User_By_Id": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "type": "Scope" @@ -1225,9 +1510,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['deleteUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_DeleteUser_Check_Delete_User_Exists_Initial": [ + "Succeeded" + ] } }, "Compose_DeleteUser_Body": { @@ -1291,9 +1581,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['deleteUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_DeleteUser_Check_User_Synced": [ + "Succeeded" + ] } }, "DeleteUser_Delay_Creation_15s": { @@ -1310,6 +1605,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_DeleteUser_Check_User_Synced": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token__SCIM_Token_DeleteUser_Check_User_Synced": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token__SCIM_Token_DeleteUser_Check_User_Synced": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token__SCIM_Token_DeleteUser_Check_User_Synced')" + }, + "runAfter": { + "Acquire_Token__SCIM_Token_DeleteUser_Check_User_Synced": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -1369,6 +1706,48 @@ "timeout": "PT15M" }, "type": "Until" + }, + "Ensure_SCIM_Token_DeleteUser_Check_Delete_User_Exists_Initial": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_eteUser_Check_Delete_User_Exists_Initial": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_eteUser_Check_Delete_User_Exists_Initial": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_eteUser_Check_Delete_User_Exists_Initial')" + }, + "runAfter": { + "Acquire_Token_eteUser_Check_Delete_User_Exists_Initial": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "type": "Scope" @@ -1512,9 +1891,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['deleteUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_DeleteUser_Query_User_By_Id": [ + "Succeeded" + ] } }, "DeleteUser_Delete_User_From_Graph": { @@ -1558,9 +1942,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users/', coalesce(first(body('DeleteUser_Query_User_By_Id')?['Resources'])?['id'], ''))}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_DeleteUser_Check_User_Deleted": [ + "Succeeded" + ] } }, "DeleteUser_Delay_Deletion_15s": { @@ -1577,6 +1966,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_DeleteUser_Check_User_Deleted": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_SCIM_Token_DeleteUser_Check_User_Deleted": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_SCIM_Token_DeleteUser_Check_User_Deleted": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_SCIM_Token_DeleteUser_Check_User_Deleted')" + }, + "runAfter": { + "Acquire_Token_SCIM_Token_DeleteUser_Check_User_Deleted": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -1651,6 +2082,48 @@ "Succeeded" ] } + }, + "Ensure_SCIM_Token_DeleteUser_Query_User_By_Id": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_e_SCIM_Token_DeleteUser_Query_User_By_Id": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_e_SCIM_Token_DeleteUser_Query_User_By_Id": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_e_SCIM_Token_DeleteUser_Query_User_By_Id')" + }, + "runAfter": { + "Acquire_Token_e_SCIM_Token_DeleteUser_Query_User_By_Id": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "type": "Scope" @@ -1854,9 +2327,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['managerUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_Step1_Check_User_Exists": [ + "Succeeded" + ] } }, "Compose_ManagerTest_UserBody": { @@ -2026,9 +2504,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['managerUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Query_Test_User": [ + "Succeeded" + ] } }, "SCIM_Query_Initial_Manager": { @@ -2037,9 +2520,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['managerUserInitial'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Query_Initial_Manager": [ + "Succeeded" + ] } }, "SCIM_Query_New_Manager": { @@ -2048,9 +2536,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['managerUserNew'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Query_New_Manager": [ + "Succeeded" + ] } }, "Delay_15s": { @@ -2075,6 +2568,132 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_SCIM_Query_Test_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Ensure_SCIM_Token_SCIM_Query_Test_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Ensure_SCIM_Token_SCIM_Query_Test_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Ensure_SCIM_Token_SCIM_Query_Test_User')" + }, + "runAfter": { + "Acquire_Token_Ensure_SCIM_Token_SCIM_Query_Test_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} + }, + "Ensure_SCIM_Token_SCIM_Query_Initial_Manager": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_re_SCIM_Token_SCIM_Query_Initial_Manager": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_re_SCIM_Token_SCIM_Query_Initial_Manager": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_re_SCIM_Token_SCIM_Query_Initial_Manager')" + }, + "runAfter": { + "Acquire_Token_re_SCIM_Token_SCIM_Query_Initial_Manager": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} + }, + "Ensure_SCIM_Token_SCIM_Query_New_Manager": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Ensure_SCIM_Token_SCIM_Query_New_Manager": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Ensure_SCIM_Token_SCIM_Query_New_Manager": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Ensure_SCIM_Token_SCIM_Query_New_Manager')" + }, + "runAfter": { + "Acquire_Token_Ensure_SCIM_Token_SCIM_Query_New_Manager": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -2170,6 +2789,48 @@ "timeout": "PT15M" }, "type": "Until" + }, + "Ensure_SCIM_Token_Step1_Check_User_Exists": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_nsure_SCIM_Token_Step1_Check_User_Exists": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_nsure_SCIM_Token_Step1_Check_User_Exists": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_nsure_SCIM_Token_Step1_Check_User_Exists')" + }, + "runAfter": { + "Acquire_Token_nsure_SCIM_Token_Step1_Check_User_Exists": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "type": "Scope" @@ -2391,9 +3052,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['managerUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Verify_Manager_Set": [ + "Succeeded" + ] } }, "Delay_Manager_Set_15s": { @@ -2410,6 +3076,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_SCIM_Verify_Manager_Set": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_nsure_SCIM_Token_SCIM_Verify_Manager_Set": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_nsure_SCIM_Token_SCIM_Verify_Manager_Set": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_nsure_SCIM_Token_SCIM_Verify_Manager_Set')" + }, + "runAfter": { + "Acquire_Token_nsure_SCIM_Token_SCIM_Verify_Manager_Set": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -2665,9 +3373,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['managerUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Verify_Manager_Change": [ + "Succeeded" + ] } }, "Delay_Manager_Change_15s": { @@ -2684,6 +3397,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_SCIM_Verify_Manager_Change": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_re_SCIM_Token_SCIM_Verify_Manager_Change": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_re_SCIM_Token_SCIM_Verify_Manager_Change": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_re_SCIM_Token_SCIM_Verify_Manager_Change')" + }, + "runAfter": { + "Acquire_Token_re_SCIM_Token_SCIM_Verify_Manager_Change": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -2936,9 +3691,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['managerUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_SCIM_Verify_Manager_Remove": [ + "Succeeded" + ] } }, "Delay_Manager_Remove_15s": { @@ -2955,6 +3715,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_SCIM_Verify_Manager_Remove": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_re_SCIM_Token_SCIM_Verify_Manager_Remove": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_re_SCIM_Token_SCIM_Verify_Manager_Remove": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_re_SCIM_Token_SCIM_Verify_Manager_Remove')" + }, + "runAfter": { + "Acquire_Token_re_SCIM_Token_SCIM_Verify_Manager_Remove": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -3183,10 +3985,15 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['disableUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}", "Content-Type": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_DisableUserTest_Disable_Step1_Check_User_Exists": [ + "Succeeded" + ] } }, "Compose_DisableUser_Body": { @@ -3250,9 +4057,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['disableUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_DisableUserTest_Disable_Step3_Fetch_User_By_Filter": [ + "Succeeded" + ] } }, "DisableUserTest_Disable_Delay_Creation_15s": { @@ -3269,6 +4081,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_DisableUserTest_Disable_Step3_Fetch_User_By_Filter": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_rTest_Disable_Step3_Fetch_User_By_Filter": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_rTest_Disable_Step3_Fetch_User_By_Filter": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_rTest_Disable_Step3_Fetch_User_By_Filter')" + }, + "runAfter": { + "Acquire_Token_rTest_Disable_Step3_Fetch_User_By_Filter": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -3308,26 +4162,68 @@ }, "type": "Wait", "inputs": { - "interval": { - "count": 15, - "unit": "Second" - } + "interval": { + "count": 15, + "unit": "Second" + } + } + } + }, + "runAfter": { + "DisableUserTest_Disable_DoUntil_Wait_For_User_Creation": [ + "Succeeded", + "Failed", + "TimedOut" + ] + }, + "expression": "@greater(length(coalesce(body('DisableUserTest_Disable_Create_Check_Provisioning_Logs')?['value'], json('[]'))), 0)", + "limit": { + "count": 150, + "timeout": "PT15M" + }, + "type": "Until" + }, + "Ensure_SCIM_Token_DisableUserTest_Disable_Step1_Check_User_Exists": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_UserTest_Disable_Step1_Check_User_Exists": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_UserTest_Disable_Step1_Check_User_Exists": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_UserTest_Disable_Step1_Check_User_Exists')" + }, + "runAfter": { + "Acquire_Token_UserTest_Disable_Step1_Check_User_Exists": [ + "Succeeded" + ] } } }, - "runAfter": { - "DisableUserTest_Disable_DoUntil_Wait_For_User_Creation": [ - "Succeeded", - "Failed", - "TimedOut" - ] - }, - "expression": "@greater(length(coalesce(body('DisableUserTest_Disable_Create_Check_Provisioning_Logs')?['value'], json('[]'))), 0)", - "limit": { - "count": 150, - "timeout": "PT15M" + "else": { + "actions": {} }, - "type": "Until" + "runAfter": {} } }, "type": "Scope" @@ -3469,9 +4365,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['disableUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_DisableUserTest_Disable_Step4_Query_User_By_Id": [ + "Succeeded" + ] } }, "DisableUserTest_Disable_Step5_Disable_User": { @@ -3505,9 +4406,14 @@ "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['disableUser'], ''), '%22')}", "method": "GET", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_DisableUserTest_Disable_Step6_Check_User_After_Disable": [ + "Succeeded" + ] } }, "DisableUserTest_Disable_Delay_Final_15s": { @@ -3524,6 +4430,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_DisableUserTest_Disable_Step6_Check_User_After_Disable": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_t_Disable_Step6_Check_User_After_Disable": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_t_Disable_Step6_Check_User_After_Disable": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_t_Disable_Step6_Check_User_After_Disable')" + }, + "runAfter": { + "Acquire_Token_t_Disable_Step6_Check_User_After_Disable": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "runAfter": { @@ -3583,6 +4531,48 @@ "timeout": "PT15M" }, "type": "Until" + }, + "Ensure_SCIM_Token_DisableUserTest_Disable_Step4_Query_User_By_Id": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_eUserTest_Disable_Step4_Query_User_By_Id": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_eUserTest_Disable_Step4_Query_User_By_Id": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_eUserTest_Disable_Step4_Query_User_By_Id')" + }, + "runAfter": { + "Acquire_Token_eUserTest_Disable_Step4_Query_User_By_Id": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } }, "type": "Scope" @@ -3803,9 +4793,14 @@ "method": "GET", "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['restoreUser'], ''), '%22')}", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_RestoreUserTest_SCIM_Pre_Check": [ + "Succeeded" + ] } }, "Compose_RestoreUser_Body": { @@ -3892,9 +4887,14 @@ "method": "GET", "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['restoreUser'], ''), '%22')}", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_RestoreUserTest_SCIM_Get_Create": [ + "Succeeded" + ] } }, "RestoreUserTest_Wait_Create_30s": { @@ -3911,6 +4911,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_RestoreUserTest_SCIM_Get_Create": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_IM_Token_RestoreUserTest_SCIM_Get_Create": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_IM_Token_RestoreUserTest_SCIM_Get_Create": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_IM_Token_RestoreUserTest_SCIM_Get_Create')" + }, + "runAfter": { + "Acquire_Token_IM_Token_RestoreUserTest_SCIM_Get_Create": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } } }, @@ -3966,6 +5008,48 @@ } } } + }, + "Ensure_SCIM_Token_RestoreUserTest_SCIM_Pre_Check": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_CIM_Token_RestoreUserTest_SCIM_Pre_Check": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_CIM_Token_RestoreUserTest_SCIM_Pre_Check": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_CIM_Token_RestoreUserTest_SCIM_Pre_Check')" + }, + "runAfter": { + "Acquire_Token_CIM_Token_RestoreUserTest_SCIM_Pre_Check": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } } }, @@ -4132,9 +5216,14 @@ "method": "GET", "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['restoreUser'], ''), '%22')}", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_RestoreUserTest_SCIM_Get_Deprovision": [ + "Succeeded" + ] } }, "RestoreUserTest_Wait_Deprovision_SCIM_15s": { @@ -4151,6 +5240,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_RestoreUserTest_SCIM_Get_Deprovision": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_ken_RestoreUserTest_SCIM_Get_Deprovision": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_ken_RestoreUserTest_SCIM_Get_Deprovision": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_ken_RestoreUserTest_SCIM_Get_Deprovision')" + }, + "runAfter": { + "Acquire_Token_ken_RestoreUserTest_SCIM_Get_Deprovision": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } } }, @@ -4375,9 +5506,14 @@ "method": "GET", "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', coalesce(triggerBody()?['initializationData']?['testUserNames']?['restoreUser'], ''), '%22')}", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } + }, + "runAfter": { + "Ensure_SCIM_Token_RestoreUserTest_SCIM_Get_Restore": [ + "Succeeded" + ] } }, "RestoreUserTest_Wait_Restore_30s": { @@ -4394,6 +5530,48 @@ "unit": "Second" } } + }, + "Ensure_SCIM_Token_RestoreUserTest_SCIM_Get_Restore": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_M_Token_RestoreUserTest_SCIM_Get_Restore": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_M_Token_RestoreUserTest_SCIM_Get_Restore": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_M_Token_RestoreUserTest_SCIM_Get_Restore')" + }, + "runAfter": { + "Acquire_Token_M_Token_RestoreUserTest_SCIM_Get_Restore": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": {} } } }, @@ -4732,7 +5910,7 @@ }, "POD_SCIM_Pre_Check_User": { "runAfter": { - "POD_Create_Graph_User": [ + "Ensure_SCIM_Token_POD_SCIM_Pre_Check_User": [ "Succeeded" ] }, @@ -4741,7 +5919,7 @@ "method": "GET", "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', body('POD_Create_Graph_User')?['userPrincipalName'], '%22')}", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } @@ -4867,7 +6045,7 @@ }, "POD_SCIM_Get_User": { "runAfter": { - "POD_Delay_After_Provision": [ + "Ensure_SCIM_Token_POD_SCIM_Get_User": [ "Succeeded" ] }, @@ -4876,7 +6054,7 @@ "method": "GET", "uri": "@{concat(parameters('scimEndpoint'), '/Users?filter=userName%20eq%20%22', body('POD_Create_Graph_User')?['userPrincipalName'], '%22')}", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } @@ -4925,6 +6103,98 @@ } } } + }, + "Ensure_SCIM_Token_POD_SCIM_Pre_Check_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_nsure_SCIM_Token_POD_SCIM_Pre_Check_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_nsure_SCIM_Token_POD_SCIM_Pre_Check_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_nsure_SCIM_Token_POD_SCIM_Pre_Check_User')" + }, + "runAfter": { + "Acquire_Token_nsure_SCIM_Token_POD_SCIM_Pre_Check_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "POD_Create_Graph_User": [ + "Succeeded" + ] + } + }, + "Ensure_SCIM_Token_POD_SCIM_Get_User": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token_Ensure_SCIM_Token_POD_SCIM_Get_User": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token_Ensure_SCIM_Token_POD_SCIM_Get_User": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token_Ensure_SCIM_Token_POD_SCIM_Get_User')" + }, + "runAfter": { + "Acquire_Token_Ensure_SCIM_Token_POD_SCIM_Get_User": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "POD_Delay_After_Provision": [ + "Succeeded" + ] + } } } }, @@ -4974,10 +6244,8 @@ }, "POD_User_Cleanup_SCIM_Delete": { "runAfter": { - "Set_PODUserTestOutputs": [ - "Succeeded", - "Failed", - "Skipped" + "Ensure_SCIM_Token_POD_User_Cleanup_SCIM_Delete": [ + "Succeeded" ] }, "type": "Http", @@ -4985,7 +6253,7 @@ "method": "DELETE", "uri": "@{concat(parameters('scimEndpoint'), '/Users/', coalesce(first(body('POD_SCIM_Get_User')?['Resources'])?['id'], 'not-found'))}", "headers": { - "Authorization": "@{concat('Bearer ', parameters('scimBearerToken'))}", + "Authorization": "@{concat('Bearer ', if(equals(parameters('scimAuthenticationMethod'), 'Bearer'), parameters('scimBearerToken'), variables('SCIMAccessTokenCache')?['accessToken']))}", "Accept": "@{parameters('scimContentType')}" } } @@ -5007,6 +6275,54 @@ "audience": "https://graph.microsoft.com/" } } + }, + "Ensure_SCIM_Token_POD_User_Cleanup_SCIM_Delete": { + "type": "If", + "expression": "@and(not(equals(parameters('scimAuthenticationMethod'), 'Bearer')), or(empty(variables('SCIMAccessTokenCache')?['accessToken']), lessOrEquals(ticks(coalesce(variables('SCIMAccessTokenCache')?['expiresAt'], '1970-01-01T00:00:00Z')), ticks(addMinutes(utcNow(), 5)))))", + "actions": { + "Acquire_Token__SCIM_Token_POD_User_Cleanup_SCIM_Delete": { + "type": "Workflow", + "inputs": { + "host": { + "workflow": { + "id": "SCIMToken_Workflow" + } + }, + "body": {} + }, + "runtimeConfiguration": { + "secureData": { + "properties": [ + "inputs", + "outputs" + ] + } + }, + "runAfter": {} + }, + "Cache_Token__SCIM_Token_POD_User_Cleanup_SCIM_Delete": { + "type": "SetVariable", + "inputs": { + "name": "SCIMAccessTokenCache", + "value": "@body('Acquire_Token__SCIM_Token_POD_User_Cleanup_SCIM_Delete')" + }, + "runAfter": { + "Acquire_Token__SCIM_Token_POD_User_Cleanup_SCIM_Delete": [ + "Succeeded" + ] + } + } + }, + "else": { + "actions": {} + }, + "runAfter": { + "Set_PODUserTestOutputs": [ + "Succeeded", + "Failed", + "Skipped" + ] + } } }, "runAfter": {}, @@ -5582,6 +6898,19 @@ } ] } + }, + "Initialize_AttributeMatchExcludeList_Variable_1": { + "type": "InitializeVariable", + "inputs": { + "variables": [ + { + "name": "attributeMatchExcludeList", + "type": "Array", + "value": "@triggerBody()?['initializationData']?['updateTestExcludeList']" + } + ] + }, + "runAfter": {} } }, "outputs": {} diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION index f0750610..ed9d00f9 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/VERSION @@ -1 +1 @@ -13.0 +14.0 diff --git a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md index 68db78b5..652b4d9c 100644 --- a/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md +++ b/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/scim-onboarding.agent.md @@ -17,11 +17,12 @@ description: > - Azure CLI installed and logged in (az login) - Application Administrator role in the Entra tenant - Azure subscription with Logic App contributor permissions - - ISV's SCIM endpoint URL and a long-lived bearer token + - ISV's SCIM endpoint URL and credentials for each selected authentication method --- # SCIM Onboarding Validation Agent +The same-bundle [VERSION](VERSION) file is the sole source of the bundle version. Read it before deployment and synchronize `LogicAppVersion`. Use the canonical agent, parameters, and all seven workflow definitions from one source revision. Do not overwrite a locally supplied authentication-aware bundle with older downloads. 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 outcome and is ready to follow the official submission process. @@ -33,7 +34,7 @@ You convert a 28-step, 3-portal manual process into a guided conversational expe ## Phase 1: Gather Inputs & Validate Environment ### Goal -Collect the ISV's SCIM endpoint and bearer token, validate their Azure environment, and probe the SCIM endpoint for capabilities. +Collect the ISV's SCIM endpoint and available authentication methods, validate their Azure environment, and probe the SCIM endpoint for capabilities. ### Steps @@ -47,14 +48,24 @@ Collect the ISV's SCIM endpoint and bearer token, validate their Azure environme 2. **Collect ALL inputs before proceeding** — use `ask_user` for each input. Do NOT skip any. Do NOT proceed to Step 3 until all inputs are collected. - Ask the ISV for each of the following, **one at a time**: + Accept explicitly supplied initial parameters instead of asking for the same value again. Ask for missing values **one at a time**. Never infer that a method is available merely because it is supported by this template. a. **SCIM endpoint URL** (e.g., `https://scim.example.com/v2`) - b. **Bearer token** (must be long-lived — warn if JWT expires within 2 hours) + b. **Available authentication methods** — accept `supportedAuthenticationMethods` as an initial array containing `Bearer`, `OAuth2ClientCredentials`, and/or `WorkloadIdentityFederation`. Otherwise use `ask_user`: + + "Which authentication methods are available in your validation environment and supported by your SCIM endpoint?" + + Choices: + - "OAuth 2.0 client credentials only" + - "Workload identity federation only" + - "Both OAuth 2.0 client credentials and workload identity federation" + - "Static bearer token only (validation only; not gallery-ready)" + + Record the exact selection. **Every selected method must pass its authentication test (AND).** Undeclared methods are **Not applicable**, not failed tests. Do not ask for their credentials or invent a token endpoint. Set `scimAuthenticationMethod` to the method that should authenticate direct SCIM requests and verify it belongs to the selection. If both gallery methods are available, ask which should authenticate the core tests; this does not remove the other selected authentication test. - c. **Authentication method** — ask: "Does your SCIM endpoint use OAuth client credentials or a static bearer token?" - - If **OAuth**: ask for all 5 parameters, one at a time: + c. **Credentials for declared methods**: + - If **OAuth2ClientCredentials is declared**: collect all 5 parameters, one at a time, unless already explicitly supplied: - Client ID - Client Secret - Token endpoint URL (e.g., `https://auth.example.com/oauth/token`) @@ -69,11 +80,13 @@ 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 5 OAuth fields (`scimClientId`, `scimClientSecret`, `scimTokenEndpoint`, `scimCredentialLocationInRequest`, `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.) + - If **Bearer is the selected transport**, collect `scimBearerToken`. Warn if its remaining lifetime is shorter than the run. Bearer-only functional validation does not satisfy gallery authentication readiness. + - Collect `scimBearerToken` only when `Bearer` is declared, including when initial parameters declare it alongside a gallery method. `Authentication_Workflow` probes static-token connectivity for that selection, independently of the core transport. + - If **Bearer is not declared**, set `scimBearerToken` to `""`. For OAuth or federation transport, `SCIMToken_Workflow` acquires access tokens at runtime and the core workflows refresh their caches before expiry. No static token is used as a fallback. The `Authorization: Bearer` wire format of an OAuth access token does not make static-token authentication available. + - Clear all five OAuth fields when OAuth is not declared; do not clear credentials for a declared method merely because another method works. d. **Federated identity test inputs (for `Federated_Identity_Test`)**: - - Ask: "Do you want to run the federated identity validation test now?" - - If **yes**, collect these values one at a time: + - If **WorkloadIdentityFederation is declared**, collect these values one at a time, unless already supplied: - `federatedEntraTenantId` - `federatedApplicationId` - `federatedApplicationClientSecret` @@ -81,9 +94,11 @@ Collect the ISV's SCIM endpoint and bearer token, validate their Azure environme - `federatedClientId` - `federatedBaseAddress` - `federatedAudience` (required for Google STS/SA flow, optional otherwise) - - If **no**, set all federated fields to empty strings in Phase 4 so the parameters contract remains complete. + - Otherwise set all federated fields to empty strings. The federation test must be Not applicable and must issue no Entra, Google, or ISV token requests. - **You MUST ask these questions using `ask_user`. Do NOT assume values, do NOT skip OAuth questions, and do NOT proceed until the ISV has answered.** + e. **Run work directory** — accept an explicit working directory or ask where to save this validation run. Keep runs separate, retain the exact Azure run ID, and never persist credentials, tokens, raw parameters, or app settings. + + **Use `ask_user` for missing applicable inputs. Explicit initial values count as answers. Do not ask OAuth questions in a WIF-only environment, or federation questions in an OAuth-only environment. Missing credentials for a declared method are a configuration error, not permission to silently mark that method unavailable.** 3. **Validate Azure CLI login**: ```bash @@ -109,6 +124,8 @@ Collect the ISV's SCIM endpoint and bearer token, validate their Azure environme 6. **Probe the SCIM endpoint**: + Obtain a probe access token **in memory** using the selected authentication flow. For OAuth, use the supplied Header/Body credential location and scope. For federation, reuse the exchange described in `SCIMToken_Workflow.json` and the federation setup guide. For Bearer, use the supplied token. Do not require a static token for OAuth-only or WIF-only endpoints, save token responses, or create Azure resources before the allowlisting gate. + > **IMPORTANT (Windows/PowerShell):** SCIM servers return `Content-Type: application/scim+json` which PowerShell's `Invoke-WebRequest` does NOT auto-decode as text — it returns a **byte array**. You MUST use `Invoke-RestMethod` (which auto-decodes) or decode manually with `[System.Text.Encoding]::UTF8.GetString($response.RawContentStream.ToArray())`. Do NOT save raw `$response.Content` to a file — it will be integer bytes, not JSON. ```powershell @@ -204,26 +221,30 @@ Extract from response: #### Step 2b: Configure provisioning credentials -> **Scope of this step.** This step configures what **Entra's sync engine** uses when it calls SCIM (including provisionOnDemand / POD tests). It is independent of the Logic App test workflows. The Logic App tests **always** call SCIM directly using `scimEndpoint` + `scimBearerToken` from `Orchestrator_Parameters.json` (set in Phase 4) — they never go through Entra. +> **Scope of this step.** This configures **Entra's sync engine**, including provisionOnDemand tests. Direct Logic App SCIM requests use `scimAuthenticationMethod`: a caller-supplied token for Bearer, or refreshable OAuth/federation tokens from `SCIMToken_Workflow`. Configure both surfaces consistently. > -> **`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. +> **`scimBearerToken` is required only for Bearer transport.** OAuth-only and WIF-only endpoints do not need a static token. Never fall back to a token or method the ISV did not select. > > **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. +> File-based examples below illustrate payload shape. For actual credentials, use `Invoke-RestMethod` with an in-memory JSON body and an in-memory Graph authorization header. Never save credential payloads, authorization headers, or token responses to the work directory or report. **Determine the branch** based on what the ISV provided in Phase 1: | ISV provided | Branch | Entra sync uses | Save connectivity with | |---|---|---|---| | Bearer only | A | Bearer | `authenticationType` + `baseAddress` + `secretToken` | -| Bearer + OAuth | B | OAuth | `authenticationType` + `baseAddress` + `oauth2ClientId` + `oauth2ClientSecret` + `oauth2TokenExchangeUri` + `credentialLocationInRequest` | +| OAuth2ClientCredentials (no static token required) | B | OAuth | OAuth connectivity keys below | +| WorkloadIdentityFederation | Federation | Supported federated provisioning | Preserve and validate the existing supported configuration; do not invent Graph keys | +| Missing credentials for a declared method | Blocked | None | Stop and export the report | For Branch B, also include the optional lower-case `scope` connectivity parameter when the ISV provided scopes. Omit `scope` when the ISV answered `none`. --- -**Sub-step 2b-1: validateCredentials (bearer — ALWAYS run this)** +**Sub-step 2b-1: validateCredentials (Bearer transport only)** + +Skip this bearer-specific payload for OAuth-only and WIF-only environments. For OAuth, save only Branch B's connectivity parameters and run the saved-credential Test Connection below. For WIF-only environments, preserve and validate the application's supported federated provisioning configuration using the [federation setup guide](SCIMTests_WorkloadIdentityFederation_Setup.md). Do not overwrite it with OAuth or bearer credentials or invent unsupported Graph connectivity keys. If provisioning connectivity cannot be determined, stop with a blocked report. Write the bearer credentials to `validate_creds_bearer.json`, then POST: @@ -295,7 +316,7 @@ az rest --method PATCH \ > > 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**: +Then validate saved credentials work (the selected supported configuration) — **this is the Test Connection**: ```bash # validate_saved.json: # {"templateId":"isvonboarding","useSavedCredentials":true} @@ -307,6 +328,21 @@ az rest --method POST \ **Classify the saved-credential validation response — do NOT proceed until this passes:** +**OAuth persistence diagnostic:** If direct OAuth token acquisition and SCIM access succeed but saved-credential validation fails, do not immediately blame the endpoint or ask for a static token. Test the same OAuth credentials inline with `useSavedCredentials: false`, using only these supported secret keys: `AuthenticationType`, `BaseAddress`, `Oauth2ClientId`, `Oauth2ClientSecret`, and `Oauth2TokenExchangeUri`. Do not include `CredentialLocationInRequest` in this array; keep credential location and optional scope in the beta connectivity configuration. + +If inline validation succeeds but saved validation fails, explicitly report the persistence discrepancy, then persist the **same validated OAuth credential array** through the supported secret-store API: + +```powershell +# $validatedOAuthCredentials contains the five keys above and original caller-held values. +# AuthenticationType is OAuth2ClientCredentialsGrant. Never reuse masked read-back secrets. +$secretsUri = "https://graph.microsoft.com/beta/servicePrincipals/$servicePrincipalId/synchronization/secrets" +$null = Invoke-RestMethod -Uri $secretsUri -Method Put -Headers $graphHeaders ` + -ContentType 'application/json' ` + -Body (@{value=$validatedOAuthCredentials} | ConvertTo-Json -Depth 10) +``` + +Preserve the selected credential location and scope settings. Re-run `useSavedCredentials: true` after persistence; **only its HTTP 200/204 clears the gate**. An inline success alone does not authorize provisioning or validation. If saved validation still fails, stop with sanitized diagnostics. This repairs persistence for the declared method; it is not an authentication-method fallback. + | 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 | @@ -357,10 +393,10 @@ else: **Hand-off to Phase 4 (do this when you reach Phase 4, not now):** -| Parameter | Branch A (bearer only) | Branch B (bearer + OAuth) | +| Parameter | Branch A (bearer only) | Branch B (OAuth; no static token required) | |---|---|---| | `scimEndpoint` | `` | `` | -| `scimBearerToken` | `` | `` (always needed — LA uses it directly) | +| `scimBearerToken` | `` | `""` (tokens acquired and refreshed at runtime) | | `scimClientId` | `""` | `` | | `scimClientSecret` | `""` | `` | | `scimTokenEndpoint` | `""` | `` | @@ -385,11 +421,15 @@ For federated identity testing, also pass these Phase 4 parameters: | Test | Branch A | Branch B | |---|---|---| -| Core SCIM tests | Use `scimBearerToken` ✓ | Use `scimBearerToken` ✓ | -| `Validate_Credentials_Test` | **SKIPPED** (empty tokenEndpoint) | **RUNS** (exercises OAuth) | -| `Federated_Identity_Test` | **SKIPPED/FAILED based on missing federated inputs** | **RUNS** when federated inputs are populated; a non-passing result is acceptable when `Validate_Credentials_Test` succeeds | +| Core SCIM tests | Use supplied `scimBearerToken` | Acquire and refresh OAuth tokens at runtime | +| `Validate_Credentials_Test` | **Not applicable** when OAuth is undeclared | **RUNS** when OAuth is declared | +| `Federated_Identity_Test` | **Not applicable** when federation is undeclared | **RUNS** only when federation is declared | | POD / sync engine | Uses bearer from stored secrets | Uses OAuth from stored secrets | +For WIF-only validation, set both `supportedAuthenticationMethods = ["WorkloadIdentityFederation"]` and `scimAuthenticationMethod = "WorkloadIdentityFederation"`, clear OAuth fields and `scimBearerToken`, and supply the federation fields. Runtime token acquisition reuses the existing Google STS, Google service-account, and generic federation exchanges. + +**Readiness is AND across all selected authentication methods.** OAuth-only requires OAuth to pass; WIF-only requires WIF to pass; selecting both requires both to pass. A selected static-token method also requires its connectivity probe to pass. `authenticationModelResult = "PASSED"` additionally requires at least one selected gallery method. Undeclared methods are Not applicable, never issue token requests, and do not satisfy readiness. Missing or skipped selected results fail closed. Bearer-only produces `NOT_GALLERY_READY` even if connectivity passes. Other mandatory functional failures still block submission. + #### Step 2c: Create resource group (if needed) ```bash az group create --name "rg-scim-validation" --location "eastus" @@ -420,9 +460,11 @@ az webapp identity assign --name "" --resource-group "rg-scim-vali Note the `principalId` — this is the **managedIdentityObjectId**. #### Step 2f: Deploy workflow templates -The Logic App has 5 workflows + 1 parameters file. Deploy them via the Kudu VFS API. +The Logic App has **7 workflows + 1 parameters file**. `Authentication_Workflow` runs the selected authentication tests as a synchronous child of SCIMTests; `SCIMToken_Workflow` provides shared runtime tokens. Neither adds to the existing 25 reportable tests. The selected static-token connectivity probe is an additional prerequisite result. Deploy all seven via the Kudu VFS API. + +**File acquisition (REQUIRED — use one coherent agent/template bundle):** -**File acquisition (REQUIRED — try GitHub first, fall back to local):** +When this agent is supplied with local workflow templates and authentication tests, use that complete bundle. Never replace locally updated files with older downloads from a different revision. Record source revision, `VERSION`, dirty state, and file hashes. Otherwise download all files from the same repository revision; do not mix agent, parameter, workflow, and authentication-test versions. The canonical source of truth for the template files is: @@ -430,10 +472,7 @@ The canonical source of truth for the template files is: Raw download base: `https://raw.githubusercontent.com/AzureAD/SCIMReferenceCode/master/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp/` -For each of the 6 files below: - -1. **First, attempt the GitHub raw URL.** Use `curl -fsSL -o ` (or the agent's web fetch tool). Treat HTTP 200 as success. If the response is HTTP 404/403/5xx or a network error, fall back to step 2. -2. **Fallback: read the file from the local workspace** (the same folder that contains this `scim-onboarding.agent.md`). If neither source returns the file, abort Phase 2 and tell the ISV which file is missing. +Resolve one complete local bundle or one immutable Git commit first. The deployment set contains seven workflows and one parameters template; retain `VERSION`, the canonical agent and authentication tests alongside them. Do not fall back file-by-file to a different revision. If any required file is missing, abort Phase 2 and report the missing file. Files to acquire (exact names, case-sensitive): @@ -444,24 +483,26 @@ Files to acquire (exact names, case-sensitive): | `UserTests_Workflow.json` | `UserTests_Workflow/workflow.json` | | `GroupTests_Workflow.json` | `GroupTests_Workflow/workflow.json` | | `SCIMTests_Workflow.json` | `SCIMTests_Workflow/workflow.json` | +| `SCIMToken_Workflow.json` | `SCIMToken_Workflow/workflow.json` | +| `Authentication_Workflow.json` | `Authentication_Workflow/workflow.json` | | `Orchestrator_Parameters.json` | basis for `parameters.json` (after override, see below) | 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) +# Use this acquisition path only after choosing GitHub, in an empty source subfolder of the run directory. +# SOURCE_REVISION is the selected immutable commit, not a moving branch name. +BASE_RAW="https://raw.githubusercontent.com/AzureAD/SCIMReferenceCode/$SOURCE_REVISION/Microsoft.SCIM.LogicAppValidationTemplate/StandardLogicApp" +FILES=(Orchestrator_Workflow.json Initialization_Workflow.json UserTests_Workflow.json GroupTests_Workflow.json SCIMTests_Workflow.json SCIMToken_Workflow.json Authentication_Workflow.json Orchestrator_Parameters.json VERSION scim-onboarding.agent.md Test-Authentication.ps1) for f in "${FILES[@]}"; do - if curl -fsSL "$BASE_RAW/$f" -o "$f"; then + if curl -fsSL "$BASE_RAW/$f" -o "$f.download"; then + mv "$f.download" "$f" echo " [github] $f" - elif [ -f "./$f" ]; then - echo " [local] $f (github fetch failed)" 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. @@ -557,6 +598,8 @@ Workflows to deploy: 3. `UserTests_Workflow.json` → `UserTests_Workflow/workflow.json` 4. `GroupTests_Workflow.json` → `GroupTests_Workflow/workflow.json` 5. `SCIMTests_Workflow.json` → `SCIMTests_Workflow/workflow.json` +6. `SCIMToken_Workflow.json` → `SCIMToken_Workflow/workflow.json` +7. `Authentication_Workflow.json` → `Authentication_Workflow/workflow.json` #### Step 2g: Assign Owner role to managed identity ```bash @@ -655,7 +698,7 @@ Required permissions: ✅ Provisioning credentials configured, sync job created () ✅ Resource group "rg-scim-validation" in eastus ✅ Standard Logic App "" with managed identity () -✅ 5 workflows deployed (Orchestrator, Initialization, UserTests, GroupTests, SCIMTests) +✅ 7 workflows deployed (Orchestrator, Initialization, UserTests, GroupTests, SCIMTests, Authentication, SCIMToken) ✅ Owner role assigned ✅ 8 Graph permissions assigned (including ProvisioningLog.Read.All, User.DeleteRestore.All) @@ -895,25 +938,39 @@ If restrictions were auto-discovered from `/Schemas`, prepend a line like: > > Do NOT construct a new JSON object containing only the table keys — that drops `scimContentType`, `defaultGroupProperties` sub-fields, or anything else the template ships with, and the workflows fail at trigger with `template parameter 'xxx' not found`. -Read the current parameters: -```bash -TOKEN=$(az account get-access-token --query accessToken -o tsv) -curl -s -H "Authorization: Bearer $TOKEN" \ - "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.Web/sites//extensions/api/vfs/site/wwwroot/parameters.json?api-version=2022-03-01" +Read the current parameters into memory; do not print them or save a live credential-bearing copy: +```powershell +$armToken = az account get-access-token --tenant $tenantId ` + --resource https://management.azure.com/ --output json | ConvertFrom-Json +if ($LASTEXITCODE -ne 0 -or $armToken.tenant -ne $tenantId) { + throw 'ARM token acquisition or tenant verification failed.' +} +$headers = @{ Authorization = "Bearer $($armToken.accessToken)"; 'If-Match' = '*' } +$parametersUri = "https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Web/sites/$logicAppName/extensions/api/vfs/site/wwwroot/parameters.json?api-version=2022-03-01" +$parameters = Invoke-RestMethod -Uri $parametersUri -Headers $headers +$template = Get-Content -LiteralPath $sourceParametersPath -Raw | ConvertFrom-Json +foreach ($property in $template.PSObject.Properties) { + if ($property.Name -notin $parameters.PSObject.Properties.Name) { + $parameters | Add-Member -NotePropertyName $property.Name -NotePropertyValue $property.Value + } +} ``` Update these parameters in the JSON: | Parameter | Value | Source | |-----------|-------|--------| +| `LogicAppVersion` | Exact content of the same-bundle `VERSION` | Verify the source template agrees, then write/read back the deployed value and record it. | +| `supportedAuthenticationMethods` | Explicit confirmed array using `Bearer`, `OAuth2ClientCredentials`, `WorkloadIdentityFederation` | Required in agent-driven runs. An empty array is only a compatibility fallback for manual deployments, inferred from configured token endpoints. | +| `scimAuthenticationMethod` | Selected direct-request transport from the same three values | OAuth and WIF acquire and refresh runtime tokens. Bearer is the legacy supplied-token transport; never use it as an automatic fallback. | | `servicePrincipalId` | `` | **Entra enterprise app SP id — NOT the Logic App MI objectId.** Both are GUIDs; swapping them silently breaks every Graph call against `/servicePrincipals//...` with a 404. | | `scimEndpoint` | `` | **MANDATORY.** The Logic App tests call this URL directly. Strip any `aadOptscim062020` feature flags. | -| `scimBearerToken` | `` | **MANDATORY.** Used by both the Logic App tests (direct SCIM calls) AND by the Entra sync engine (via `SecretToken` in Step 2b). If the ISV did not provide one, abort and request it. | +| `scimBearerToken` | Supplied token only for Bearer transport; otherwise `""` | OAuth-only and WIF-only endpoints require no static token. | | `testUserDomain` | `` | Auto from Phase 1 | | `EnabledTests` | `All` | Auto — **MANDATORY: always overwrite to `All`. Do NOT preserve any existing value from the current `parameters.json`.** | | `IsSoftDeleted` | `true` if `active` attribute detected | Auto from Phase 1 | | `defaultUserProperties` | **DO NOT REPLACE — keep base template profiles.** Only modify individual attribute values if the ISV provided restrictions. See below. | The base `Orchestrator_Parameters.json` ships 3 user profiles with all required properties (25 fields including `displayName`, `hireDate`, `employeeOrgData.costCenter`, `passwordProfile.forceChangePasswordNextSignIn`). Never build these from scratch. | | `defaultGroupProperties` | **DO NOT REPLACE — keep base template values.** | Auto | -| `scimClientId` | `` | From Phase 1 — set if the ISV provided OAuth credentials. Used by the LA's `Validate_Credentials_Test` to exercise the OAuth flow independently. Empty string if not provided. **Note: Entra sync always uses bearer token (Step 2b), NOT these OAuth values.** | +| `scimClientId` | `` | Required when OAuth is declared; otherwise empty. Used by the OAuth test and OAuth direct-request transport. Entra provisioning uses the separately saved and validated OAuth configuration from Step 2b. | | `scimClientSecret` | `` | From Phase 1 — same. Empty string if not provided. | | `scimTokenEndpoint` | `` | From Phase 1 — same. Empty string if not provided. | | `scimCredentialLocationInRequest` | `` | From Phase 1 — mandatory when OAuth client credentials are provided. Empty string for static bearer authentication. | @@ -931,6 +988,7 @@ Update these parameters in the JSON: After patching the JSON in memory, assert every key below exists at the top level of `parameters.json` (value may be empty string for optional fields, but the key MUST be present — the workflows reference each one via `parameters('xxx')`): ``` +supportedAuthenticationMethods, scimAuthenticationMethod, servicePrincipalId, scimEndpoint, scimBearerToken, scimContentType, testUserDomain, EnabledTests, IsSoftDeleted, defaultUserProperties, defaultGroupProperties, scimTargetUserValues, @@ -946,9 +1004,9 @@ If any key is missing, abort Phase 4 and tell the ISV exactly which key is missi After the PUT completes, re-GET `parameters.json` and for each key in the patch table above, assert the returned value matches what was sent. If `servicePrincipalId` was supposed to be `aaa-bbb-ccc` but the read-back shows something else (or the key is missing), abort Phase 4 with the specific mismatch. Do NOT proceed to Phase 5. -If the ISV did not provide OAuth credentials, leave `scimClientId`, `scimClientSecret`, `scimTokenEndpoint`, `scimCredentialLocationInRequest`, and `scimOAuthScope` as empty strings. The `Validate_Credentials_Test` will be SKIPPED — note this as expected in the final report. This is unrelated to the Entra sync engine, which always uses the bearer token (`SecretToken`) configured in Step 2b. +When OAuth is undeclared, clear `scimClientId`, `scimClientSecret`, `scimTokenEndpoint`, `scimCredentialLocationInRequest`, and `scimOAuthScope`. `Validate_Credentials_Test` is Not applicable and makes no token request. When OAuth is declared, missing required credentials are a blocker, not a skip. Configure and validate Entra provisioning separately with the selected supported method. -If the ISV did not provide federated identity inputs, leave all federated parameters as empty strings (`federatedEntraTenantId`, `federatedApplicationId`, `federatedApplicationClientSecret`, `federatedTokenEndpoint`, `federatedClientId`, `federatedBaseAddress`, `federatedAudience`). +When WIF is undeclared, clear all federated parameters (`federatedEntraTenantId`, `federatedApplicationId`, `federatedApplicationClientSecret`, `federatedTokenEndpoint`, `federatedClientId`, `federatedBaseAddress`, `federatedAudience`). When WIF is declared, require its applicable inputs. #### defaultUserProperties handling (CRITICAL — do NOT build from scratch) @@ -971,14 +1029,16 @@ The base template profiles have these 25 properties per user: If any property is missing, the Logic App will fail with `InvalidTemplate` error. -Write updated parameters: -```bash -curl -X PUT \ - "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.Web/sites//extensions/api/vfs/site/wwwroot/parameters.json?api-version=2022-03-01" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -H "If-Match: *" \ - --data-binary @updated_parameters.json +Write the complete patched object directly from memory, then verify each patched value using JSON-aware comparison (including arrays and booleans), without logging values: +```powershell +$null = Invoke-RestMethod -Uri $parametersUri -Method Put -Headers $headers ` + -ContentType 'application/json' -Body ($parameters | ConvertTo-Json -Depth 100) +$readback = Invoke-RestMethod -Uri $parametersUri -Headers $headers +foreach ($name in $patch.Keys) { + $actual = ConvertTo-Json -InputObject $readback.$name.value -Depth 100 -Compress + $expected = ConvertTo-Json -InputObject $patch[$name] -Depth 100 -Compress + if ($actual -cne $expected) { throw "Parameter read-back mismatch: $name" } +} ``` ### Allowed EnabledTests values @@ -1048,17 +1108,18 @@ Then poll the hostruntime until it is back online (typically ~60s): ```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" +HOST_STATUS="https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.Web/sites//hostruntime/admin/host/status?api-version=2022-03-01" for i in {1..36}; do - state=$(curl -s -H "Authorization: Bearer $TOKEN" "$BASE/host/default/properties/status?api-version=2022-03-01" | jq -r .state 2>/dev/null) + state=$(curl -s -H "Authorization: Bearer $TOKEN" "$HOST_STATUS" | jq -r .state 2>/dev/null) echo "[$i] host state: $state" if [ "$state" = "Running" ]; then break; fi sleep 5 done ``` -**Pass criteria:** `state == "Running"`. If the runtime is still not `Running` after 3 minutes, abort and investigate (commonly a `workflow.json` syntax error or UTF-8 BOM corruption left the runtime in `Error` state — see the BOM validation in Step 2f). +**Pass criteria:** `state == "Running"` **and** the workflow management API responds successfully with every deployed workflow healthy. Host Running alone does not mean workflow management is ready. Retry only documented transient warm-up errors within the bounded readiness wait; do not trigger during warm-up. On timeout, stop and export a blocked report (commonly a workflow syntax error or UTF-8 BOM left registration unhealthy; see Step 2f). -**Optional sanity check (recommended on re-deploys):** Before triggering, GET each modified workflow and assert `health.state == "Healthy"`: +**Required management readiness check:** Before triggering, GET all seven workflows and assert `health.state == "Healthy"`. Run `Test-Authentication.ps1` on the exact bundle before deployment; it checks nested action counts against the 500-action limit and validates child-workflow wiring: ```bash curl -s -H "Authorization: Bearer $TOKEN" "$BASE/workflows/?api-version=2022-03-01" | jq '{state:.health.state, err:.health.errorMessage.error.message}' ``` @@ -1079,6 +1140,9 @@ Common causes: invalid parameter type (Bool param with string value), malformed Only after the host is `Running` (and any modified workflows are `Healthy`), proceed to Step 5b. ### Step 5b: Trigger the Orchestrator workflow + +Before the POST, capture the complete, paginated set of existing `Manual_Recurrence` trigger-history IDs, the workflow version, and the request start time. Keep this baseline for exact association if the management response does not contain a run ID. Read response headers in memory; do not rely only on the response body. + ```bash # Get the trigger URL TOKEN=$(az account get-access-token --query accessToken -o tsv) @@ -1091,7 +1155,11 @@ curl -X POST \ ### Capture the runId from the trigger response -The POST above returns headers including `x-ms-workflow-run-id`. **Capture this header value as ``.** This is THE runId for this test execution. Use it for every subsequent call (polling, debug, validation report). Do NOT use `runs?$top=1` to find it — that risks grabbing a stale or unrelated run. +When the POST returns `x-ms-workflow-run-id`, capture that value as ``. Standard management endpoints may instead acknowledge the request with HTTP 200/202 and an empty body without that header. **An accepted response without a run-ID header is not permission to trigger again.** + +If the header is absent, enumerate the paginated trigger histories and compare them with the captured pre-trigger baseline. Require exactly one new fired history for the same workflow version and invocation window, then use its explicit `properties.run.id` reference and verify the linked run's trigger `originHistoryName` matches that history. Record the history ID, run reference, baseline, and association method in the run manifest. Allow a bounded wait for the accepted invocation to appear. If association is absent or ambiguous, stop with an explicit diagnostic; never guess or start a duplicate. + +Use the exact associated run ID for all polling, debugging, and reports. Do NOT use `runs?$top=1` or select whichever history is newest; those can substitute a stale or unrelated run. ### Poll using ONE sync terminal call with an internal loop @@ -1220,13 +1288,15 @@ az rest --method GET \ Then fetch the output content from the `outputsLink.uri` in the response. -The sibling `Evaluate_Test_Results` action on the same run also exposes three top-level aggregation fields (fetch its `outputsLink.uri` the same way): +The sibling `Evaluate_Test_Results` action on the same run also exposes these aggregation and provenance fields (fetch its `outputsLink.uri` the same way): | Field | Values | Meaning | |---|---|---| | `overallLogicAppResult` | `"Succeeded"` \| `"Failed"` | LA pass/fail verdict. `"Failed"` when any Strict test (User/SCIM/Group) is not `PASSED` — including illegitimate SKIP. Group Strict tests are legitimately excused when `isGroupSupported=false`; in the ISV summary, explain that `/Schemas` does not advertise the SCIM Group resource and the Entra Group mapping is disabled when those facts were verified. Manager tests are legitimately excused when `isManagerAttributeSupported=false`. | | `optionalSuccessTestsResult` | `"PASSED"` \| `"WARNING"` | Signals WARNING when any of Delete_User, Delete_Group, Restore_Group, or SCIM_Group_Pagination is not `PASSED` (including illegitimate SKIP). Never causes `overallLogicAppResult="Failed"`. | -| `authenticationModelResult` | `"PASSED"` \| `"FAILED"` | `"PASSED"` when at least one of `Validate_Credentials_Test` or `Federated_Identity_Test` succeeded. `"FAILED"` when neither passed — also trips `overallLogicAppResult="Failed"`. | +| `authenticationModelResult` | `"PASSED"` \| `"FAILED"` \| `"NOT_GALLERY_READY"` | Every selected method must pass, with at least one selected gallery method. Undeclared methods are Not applicable; missing selected results fail closed. Static-token-only functional validation is not gallery-ready. | +| `authenticationPolicy` | `"AllSelected"` | Identifies AND aggregation. Do not apply an older run's policy to a new run or relabel historical evidence. | +| `logicAppVersion` | Same-bundle `VERSION` content | Actual deployed version used by this exact run, distinct from the currently inspected source. | ### Step 6b: Parse each test result @@ -1256,7 +1326,7 @@ Test result semantics: - **`"SKIPPED - Group object mapping disabled in Entra sync schema"`** — legitimate skip when `isGroupSupported=false`. Before describing why, verify the Phase 1 `/Schemas` evidence and the current Entra mapping state. When `/Schemas` does not advertise `urn:ietf:params:scim:schemas:core:2.0:Group` and the Entra Group object mapping is disabled, tell the ISV exactly that. Overall run stays `"Succeeded"`. - **`"SKIPPED - IsManagerAttributeSupported is false"`** — legitimate skip when `isManagerAttributeSupported=false`. Overall run stays `"Succeeded"`. -**Authentication pair exception:** `Validate_Credentials_Test` and `Federated_Identity_Test` are `Mandatory/StrictOneSuccess`, not independently strict. Evaluate them as a pair using `authenticationModelResult`. When `authenticationModelResult == "PASSED"`, one model succeeded and the other model may be skipped or may contain a diagnostic non-success result such as `"Entra token acquisition failed"`. Do not classify that non-passing counterpart as a failed validation test, do not block submission, and do not include it in `Tests failed`. When `authenticationModelResult == "FAILED"`, authentication fails and submission is blocked. +**Selected authentication methods are independently strict:** `Validate_Credentials_Test` and `Federated_Identity_Test` use `Declared/StrictSuccess`. Every selected method must pass; a selected failure, skip, or missing result blocks submission even if another selected method passed. Only an undeclared method may be excluded as Not applicable. Check `BearerConnectivityTestOutputs` too when static-token authentication was selected. Do not treat an undeclared method as a failed test, and do not hide any selected method's failure. ### Step 6c: Drill into child workflow actions to find the real error (BEFORE pattern matching) @@ -1303,14 +1373,14 @@ Fetch the content from these URIs to see the actual request/response payloads. * #### 4. For actions inside Until/DoUntil loops, check repetitions -Polling loops execute the same action multiple times. Each iteration has its own input/output: +Polling loops execute the same nested action multiple times. Query repetitions of the nested HTTP action, not the Until container: ```bash az rest --method GET \ - --url "...//runs//actions//repetitions?api-version=2022-03-01" + --url "...//runs//actions//repetitions?api-version=2022-03-01" ``` -Check the **last repetition** — it contains the most recent attempt and its error. +Preserve every repetition's timing, status, and errors. Inspect the last attempt and every failed or timed-out attempt's request/response. Until containers do not support this `/repetitions` route on the tested Standard runtime; retain their action-level timing and the supported nested-action history instead. #### 5. Diagnose and act @@ -1363,8 +1433,8 @@ All 8 required permissions must be present: | # | Pattern | Root Cause | Auto-Fixable? | Fix | |---|---------|-----------|---------------|-----| | 1 | `aadOptscim062020` in error | Feature flag in endpoint | ✅ Yes | Remove flag from `scimEndpoint` parameter, re-run | -| 2 | `401`, `unauthorized`, `token expired` | Bearer token expired | ❌ No | Ask ISV for new long-lived token | -| 3 | `Get_Templates.*Unauthorized` | MI permissions not propagated AND/OR MI access token cached pre-assignment | ✅ Yes | Wait 10 min for Entra propagation, then **`az webapp restart`** the LA (mandatory — wait alone does NOT refresh the MI's cached token), poll `/host/default/properties/status` until `state=Running` (~60s), re-run. See Step 5a2 for the full restart pattern. | +| 2 | `401`, `unauthorized`, `token expired` | Selected credentials rejected or token acquisition/refresh failed | No automatic method switch | For selected Bearer transport, request a valid replacement token. For OAuth/WIF, inspect `SCIMToken_Workflow`, credential validity, scope/audience, and the failing exchange. Never ask for an undeclared static token or fall back to it. | +| 3 | `Get_Templates.*Unauthorized` | MI permissions not propagated AND/OR MI access token cached pre-assignment | ✅ Yes | Wait 10 min for Entra propagation, then **`az webapp restart`** the LA (mandatory — wait alone does NOT refresh the MI's cached token), poll `/hostruntime/admin/host/status` until `state=Running` (~60s), re-run. See Step 5a2 for the full restart pattern. | | 4 | `filter.*fail`, `Bad Request.*filter` | SCIM filter not supported | ❌ No | ISV must implement filter support on matching properties | | 5 | `409.*conflict` | SCIM 409 conflict | ⚠️ Maybe | Re-run (often transient). If persistent, ISV must fix idempotency | | 6 | `404.*not found.*user`, `filter.*404` | SCIM returns 404 for empty queries | ❌ No | ISV must return 200 + empty results (mandatory requirement) | @@ -1374,10 +1444,10 @@ All 8 required permissions must be present: | 10 | `InvalidTemplate`, `property '...' doesn't exist` | Missing fields in `defaultUserProperties` | ✅ Yes | Add the missing property to all 3 user profiles, re-run | | 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 (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. | +| 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 `/hostruntime/admin/host/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 (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`). For WIF, follow Step 2b's supported federation configuration without inventing keys or switching auth methods. 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. | | 15 | Entra `/validateConnectivity` behavior (surfaced through Graph `validateCredentials`) returns `SystemForCrossDomainIdentityManagementServiceIncompatible`, `CredentialValidationUnavailable`, or inner HTTP `404` involving Group connectivity **and** Phase 1 found `/Groups` unsupported, `/Schemas` does not advertise the standard Group schema, **and** the job schema has an enabled Group object mapping | Entra processes or probes Group because the synchronization schema enables its Group object mapping, but the SCIM endpoint is user-only | ✅ Yes | Follow the mandatory Pattern #15 procedure below: disable every Group object mapping with a full-schema `PUT`, verify, restart/start the job, and revalidate. | -| 16 | Any Group test (`Create_Group_Test`, `Update_Group_Test`, `Delete_Group_Test`, `Group_Update_Add_Member_Test`, `Group_Update_Remove_Member_Test`, `SCIM_Group_Create_Test`, `SCIM_Group_Update_Test`, `SCIM_Group_Pagination_Test`, `POD_Group_Test`, `Restore_Group_Test`) fails with `404`, `Not Found`, or SCIM error **and** Phase 1 found `/Groups` unsupported | The Logic App test workflows call `/Groups` directly using the bearer token — Pattern #15 only disables the Entra sync-engine Group mapping, not the LA tests themselves | ❌ No | **Do NOT auto-fix or suppress.** Report to the ISV: *"Your SCIM endpoint does not implement the `/Groups` resource type. The following Group tests failed because your server returned 404 for `/Groups` requests: [list failed tests]. To pass validation and submit to the Entra app gallery, your SCIM server must support the `/Groups` endpoint and return HTTP 200 (even for an empty group list). These failures block submission."* Do NOT allow submission. Do NOT re-run — the same tests will fail identically. Wait for the ISV to confirm they have added `/Groups` support, then re-probe `/Groups` (Phase 1 Step 6) to confirm before re-triggering. | +| 16 | Any Group test (`Create_Group_Test`, `Update_Group_Test`, `Delete_Group_Test`, `Group_Update_Add_Member_Test`, `Group_Update_Remove_Member_Test`, `SCIM_Group_Create_Test`, `SCIM_Group_Update_Test`, `SCIM_Group_Pagination_Test`, `POD_Group_Test`, `Restore_Group_Test`) fails with `404`, `Not Found`, or SCIM error **and** Phase 1 found `/Groups` unsupported | The Logic App test workflows call `/Groups` directly using the selected authentication transport — Pattern #15 only disables the Entra sync-engine Group mapping, not the LA tests themselves | ❌ No | **Do NOT auto-fix or suppress.** Report to the ISV: *"Your SCIM endpoint does not implement the `/Groups` resource type. The following Group tests failed because your server returned 404 for `/Groups` requests: [list failed tests]. To pass validation and submit to the Entra app gallery, your SCIM server must support the `/Groups` endpoint and return HTTP 200 (even for an empty group list). These failures block submission."* Do NOT allow submission. Do NOT re-run — the same tests will fail identically. Wait for the ISV to confirm they have added `/Groups` support, then re-probe `/Groups` (Phase 1 Step 6) to confirm before re-triggering. | #### Pattern #15: Disable Group object mappings for a user-only endpoint @@ -1450,7 +1520,7 @@ while (test run fails): if any workflow.json was modified OR any appRoleAssignment / owner / secret / sync schema / sync-job state was changed during this iteration: az webapp restart -n -g - poll /host/default/properties/status until state == "Running" (~60s) + wait for /hostruntime/admin/host/status Running AND all seven workflow definitions Healthy re-trigger orchestrator (Step 5b) and watch (Step 5b loop) if same failure repeats after re-run → escalate to ISV ``` @@ -1471,9 +1541,9 @@ Give the ISV a complete, copy-ready submission summary for the exact successful 1. **Apply the success gate.** Continue only when ALL of the following are true: - The exact captured orchestrator `` reached terminal status `Succeeded`. - `Evaluate_Test_Results.overallLogicAppResult == "Succeeded"` (fetched from the `Evaluate_Test_Results` action inputs on that run). - - `Evaluate_Test_Results.authenticationModelResult == "PASSED"` (at least one auth model succeeded — this is a hard requirement). + - `Evaluate_Test_Results.authenticationModelResult == "PASSED"` (every selected authentication method succeeded, and at least one selected gallery method is present). - Every **non-authentication** entry in `Final_TestResults.testResults[]` has `testResult` starting with `"success"`, `"SKIPPED"`, or `"WARNING"` — NEVER `"FAILED"`. - - Treat `Validate_Credentials_Test` and `Federated_Identity_Test` as one `Mandatory/StrictOneSuccess` authentication pair. Because `authenticationModelResult == "PASSED"` is already required, a skipped or non-passing counterpart (including a diagnostic result such as `"Entra token acquisition failed"`) is acceptable and does not block submission. Report which authentication model passed, but do not count the counterpart in `Tests failed`. + - Require each declared `Validate_Credentials_Test` and `Federated_Identity_Test` result to pass independently. Exclude only undeclared Not applicable methods. Require the static-token connectivity result too when `Bearer` was selected. No selected failure may be hidden by another method's success. - `optionalSuccessTestsResult == "PASSED"` is preferred; `"WARNING"` is acceptable — call out each warning test explicitly in the summary. - No Phase 6 issue remains unresolved.