Codeunit 6400 Flow Service Management, source in 29

Source29

src/Layers/W1/BaseApp/Modules/System/PowerAutomate/FlowServiceManagement.Codeunit.al315 lines, Copyright (c) Microsoft Corporation. MIT

namespace System.Automation;

using System;
using System.Azure.Identity;
using System.Environment;
using System.Integration;

codeunit 6400 "Flow Service Management"
{
    // // Manages access to Microsoft Power Automate (previously called Microsoft Flow) service API
    InherentEntitlements = X;
    InherentPermissions = X;
    Permissions = TableData "Flow Service Configuration" = r;

    trigger OnRun()
    begin
    end;

    var
        AzureAdMgt: Codeunit "Azure AD Mgt.";
        JObject: DotNet JObject;

        FlowUrlProdTxt: Label 'https://make.powerautomate.com/', Locked = true;
        FlowUrlTip1Txt: Label 'https://make.test.powerautomate.com/', Locked = true;
        FlowARMResourceUrlTxt: Label 'https://management.core.windows.net/', Locked = true;
        GenericErr: Label 'An error occurred while trying to access the Power Automate service. Please try again or contact your system administrator if the error persists.';
        FlowResourceNameTxt: Label 'Flow Services';
        FlowAccessDeniedErr: Label 'Windows Azure Service Management API permissions need to be enabled for Power Automate in the Azure portal. Contact your system administrator.';
        FlowLinkUrlFormatTxt: Label '%1/flows/%2/details', Locked = true;
        FlowLinkInvalidFlowIdErr: Label 'An invalid flow ID was provided.';
        NullGuidReceivedMsg: Label 'Encountered an null GUID value as Power Automate Environment ID.', Locked = true;
        EmptyAccessTokenTelemetryMsg: Label 'Encountered an empty access token for Power Automate services.', Locked = true;
        EmptyPowerPlatformTenantTelemetryMsg: Label 'Encountered an empty Power Platform Tenant URL for Power Automate services.', Locked = true;
        PowerAutomateURLTelemetryMsg: Label 'Power Automate Environment URL: %1', Locked = true, Comment = '%1: URL used to access Power Automate environments';
        PowerAutomatePickerTelemetryCategoryLbl: Label 'AL Power Automate Environment Picker', Locked = true;
        MicrosoftPowerAutomatePrivacyIdTxt: Label 'Power Automate', Locked = true;


    procedure GetFlowUrl(): Text
    var
        FlowServiceConfiguration: Record "Flow Service Configuration";
        FlowUrl: Text;
    begin
        FlowUrl := FlowUrlProdTxt;
        if FlowServiceConfiguration.FindFirst() then
            case FlowServiceConfiguration."Flow Service" of
                FlowServiceConfiguration."Flow Service"::"Testing Service (TIP 1)":
                    FlowUrl := FlowUrlTip1Txt;
            end;

        exit(FlowUrl);
    end;

    procedure GetFlowEnvironmentsApi(): Text
    var
        FlowEnvironmentsApi: Text;
    begin
        if TryGetFlowEnvironmentsApi(FlowEnvironmentsApi) then
            exit(FlowEnvironmentsApi);

        exit('');
    end;

    procedure GetFlowDetailsUrl(FlowId: Guid) FlowDetailsUrl: Text
    begin
        if IsNullGuid(FlowId) then
            Error(FlowLinkInvalidFlowIdErr);

        FlowDetailsUrl := StrSubstNo(FlowLinkUrlFormatTxt, GetFlowUrl(), LowerCase(Format(FlowId, 0, 4)));
    end;

    procedure GetGenericError(): Text
    begin
        exit(GenericErr);
    end;

    procedure CanApproveForAll(): Boolean
    var
        [SecurityFiltering(SecurityFilter::Ignored)]
        FlowUserEnvironmentConfig: Record "Flow User Environment Config";
    begin
        exit(FlowUserEnvironmentConfig.WritePermission());
    end;

    procedure GetPowerAutomatePrivacyNoticeId(): Code[50]
    begin
        exit(MicrosoftPowerAutomatePrivacyIdTxt);
    end;

    procedure GetFlowEnvironmentID() FlowEnvironmentId: Text
    var
        FlowUserEnvironmentConfig: Record "Flow User Environment Config";
        EmptyGuid: Guid;
    begin
        // Check if a user has a specific environment configured
        if FlowUserEnvironmentConfig.Get(UserSecurityId()) then
            FlowEnvironmentId := FlowUserEnvironmentConfig."Environment ID"
        else
            // If not, check if the default environment is configured
            if FlowUserEnvironmentConfig.Get(EmptyGuid) then
                FlowEnvironmentId := FlowUserEnvironmentConfig."Environment ID";
    end;

    procedure GetSelectedFlowEnvironmentName() FlowEnvironmentName: Text
    var
        FlowUserEnvironmentConfig: Record "Flow User Environment Config";
        EmptyGuid: Guid;
    begin
        FlowEnvironmentName := '';
        if not HasUserSelectedFlowEnvironment() then
            exit;
        // Check if a user has a specific environment configured
        if FlowUserEnvironmentConfig.Get(UserSecurityId()) then
            FlowEnvironmentName := FlowUserEnvironmentConfig."Environment Display Name"
        else
            // If not, check if the default environment is configured
            if FlowUserEnvironmentConfig.Get(EmptyGuid) then
                FlowEnvironmentName := FlowUserEnvironmentConfig."Environment Display Name"
    end;

    [Scope('OnPrem')]
    procedure GetEnvironments(var TempFlowUserEnvironmentBuffer: Record "Flow User Environment Buffer" temporary)
    var
        WebRequestHelper: Codeunit "Web Request Helper";
        ResponseText: Text;
        Handled: Boolean;
        AccessToken: SecretText;
    begin
        Handled := false;
        OnBeforeSendGetEnvironmentRequest(ResponseText, Handled);
        if not Handled then begin
            AccessToken := AzureAdMgt.GetAccessTokenAsSecretText(FlowARMResourceUrlTxt, FlowResourceNameTxt, false);

            if AccessToken.IsEmpty() then
                Session.LogMessage('0000MJX', EmptyAccessTokenTelemetryMsg, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', PowerAutomatePickerTelemetryCategoryLbl);

            // Gets a list of Flow user environments from the Flow API.
            if not WebRequestHelper.GetResponseTextUsingCharset('GET', GetFlowEnvironmentsApi(), AccessToken, ResponseText)
            then
                Error(GenericErr);
        end;

        ParseResponseTextForEnvironments(ResponseText, TempFlowUserEnvironmentBuffer);
    end;

    procedure ParseResponseTextForEnvironments(ResponseText: Text; var TempFlowUserEnvironmentBuffer: Record "Flow User Environment Buffer" temporary)
    var
        FlowUserEnvironmentConfig: Record "Flow User Environment Config";
        EnvironmentInformation: Codeunit "Environment Information";
        Current: DotNet GenericKeyValuePair2;
        JObj: DotNet JObject;
        JObjProp: DotNet JObject;
        ObjectEnumerator: DotNet IEnumerator;
        JArray: DotNet JArray;
        ArrayEnumerator: DotNet IEnumerator;
        JToken: DotNet JToken;
        JProperty: DotNet JProperty;
    begin
        // Parse the ResponseText from Flow environments api for a list of environments
        ObjectEnumerator := JObject.Parse(ResponseText).GetEnumerator();

        while ObjectEnumerator.MoveNext() do begin
            Current := ObjectEnumerator.Current;

            if Format(Current.Key) = 'value' then begin
                JArray := Current.Value();
                ArrayEnumerator := JArray.GetEnumerator();

                while ArrayEnumerator.MoveNext() do begin
                    JObj := ArrayEnumerator.Current;
                    JObjProp := JObj.SelectToken('properties');

                    if not IsNull(JObjProp) then begin
                        JProperty := JObjProp.Property('provisioningState');

                        // only interested in those that succeeded
                        if LowerCase(Format(JProperty.Value)) = 'succeeded' then begin
                            JToken := JObj.SelectToken('name');
                            JProperty := JObjProp.Property('displayName');

                            TempFlowUserEnvironmentBuffer.Init();
                            TempFlowUserEnvironmentBuffer."Environment ID" := JToken.ToString();
                            TempFlowUserEnvironmentBuffer."Environment Display Name" := Format(JProperty.Value);

                            if EnvironmentInformation.GetLinkedPowerPlatformEnvironmentId() = TempFlowUserEnvironmentBuffer."Environment Id" then
                                TempFlowUserEnvironmentBuffer.Linked := true;

                            // mark current environment as enabled/selected if it is currently the user selected environment
                            FlowUserEnvironmentConfig.Reset();
                            FlowUserEnvironmentConfig.SetRange("Environment ID", JToken.ToString());
                            FlowUserEnvironmentConfig.SetRange("User Security ID", UserSecurityId());
                            TempFlowUserEnvironmentBuffer.Enabled := FlowUserEnvironmentConfig.FindFirst();

                            // check if environment is the default
                            JProperty := JObjProp.Property('isDefault');
                            if LowerCase(Format(JProperty.Value)) = 'true' then
                                TempFlowUserEnvironmentBuffer.Default := true;

                            TempFlowUserEnvironmentBuffer.Insert();
                        end;
                    end;
                end;
            end;
        end;
    end;

    procedure SaveFlowUserEnvironmentSelection(var TempFlowUserEnvironmentBuffer: Record "Flow User Environment Buffer" temporary)
    var
        FlowUserEnvironmentConfig: Record "Flow User Environment Config";
    begin
        // User previously selected environment so update
        if FlowUserEnvironmentConfig.Get(UserSecurityId()) then begin
            FlowUserEnvironmentConfig."Environment ID" := TempFlowUserEnvironmentBuffer."Environment ID";
            FlowUserEnvironmentConfig."Environment Display Name" := TempFlowUserEnvironmentBuffer."Environment Display Name";
            FlowUserEnvironmentConfig.Modify();
            exit;
        end;

        // User has no previous selection so add new one
        FlowUserEnvironmentConfig.Init();
        FlowUserEnvironmentConfig."User Security ID" := UserSecurityId();
        FlowUserEnvironmentConfig."Environment ID" := TempFlowUserEnvironmentBuffer."Environment ID";
        FlowUserEnvironmentConfig."Environment Display Name" := TempFlowUserEnvironmentBuffer."Environment Display Name";
        FlowUserEnvironmentConfig.Insert();
    end;

    procedure UseLinkedEnvironment()
    var
        FlowUserEnvironmentConfig: Record "Flow User Environment Config";
    begin
        // Remove all previous selections if exists
        FlowUserEnvironmentConfig.Reset();
        FlowUserEnvironmentConfig.DeleteAll();
    end;

    procedure SaveFlowEnvironmentSelectionForAll(var TempFlowUserEnvironmentBuffer: Record "Flow User Environment Buffer" temporary)
    var
        FlowUserEnvironmentConfig: Record "Flow User Environment Config";
        EmptyGuid: Guid;
    begin
        // Remove all previous selections
        FlowUserEnvironmentConfig.Reset();
        FlowUserEnvironmentConfig.DeleteAll();

        // Add new selection for all users
        FlowUserEnvironmentConfig.Init();
        FlowUserEnvironmentConfig."User Security ID" := EmptyGuid;
        FlowUserEnvironmentConfig."Environment ID" := TempFlowUserEnvironmentBuffer."Environment ID";
        FlowUserEnvironmentConfig."Environment Display Name" := TempFlowUserEnvironmentBuffer."Environment Display Name";
        FlowUserEnvironmentConfig.Insert();
    end;

    procedure HasUserSelectedFlowEnvironment(): Boolean
    var
        FlowUserEnvironmentConfig: Record "Flow User Environment Config";
        EmptyGuid: Guid;
    begin
        exit(FlowUserEnvironmentConfig.Get(UserSecurityId()) or FlowUserEnvironmentConfig.Get(EmptyGuid));
    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::"System Action Triggers", 'GetPowerPlatformEnvironmentId', '', true, true)]
    local procedure GetEnvironmentId(Scenario: Text; var EnvironmentId: Text)
    var
        EnvironmentInformation: Codeunit "Environment Information";
        GUIDValue: Guid;
        LinkedEnvironmentId: Text;
        EmptyGuidText: Text;
    begin
        EmptyGuidText := '00000000-0000-0000-0000-000000000000';
        EnvironmentId := '';
        if HasUserSelectedFlowEnvironment() then
            // if a user has a specific environment configured, use it
            EnvironmentId := GetFlowEnvironmentID()
        else begin
            // if not, use the linked environment if exists
            LinkedEnvironmentId := EnvironmentInformation.GetLinkedPowerPlatformEnvironmentId();
            if (LinkedEnvironmentId <> '') and (Evaluate(GUIDValue, LinkedEnvironmentId)) then begin
                EnvironmentId := LinkedEnvironmentId;

                if (LinkedEnvironmentId = EmptyGuidText) then
                    Session.LogMessage('0000NBM', NullGuidReceivedMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', PowerAutomatePickerTelemetryCategoryLbl);
                ;
            end;
        end;
    end;

    [EventSubscriber(ObjectType::Page, Page::"Azure AD Access Dialog", 'OnOAuthAccessDenied', '', false, false)]
    local procedure CheckOAuthAccessDenied(description: Text; resourceFriendlyName: Text)
    begin
        if StrPos(resourceFriendlyName, FlowResourceNameTxt) > 0 then
            if StrPos(description, 'AADSTS65005') > 0 then
                Error(FlowAccessDeniedErr);
    end;

    [TryFunction]
    local procedure TryGetFlowEnvironmentsApi(var FlowEnvironmentsApi: Text)
    var
        AzureADTenant: Codeunit "Azure AD Tenant";
    begin
        if AzureADTenant.GetPowerPlatformTenantURL() = '' then begin
            Session.LogMessage('0000Q79', EmptyPowerPlatformTenantTelemetryMsg, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', PowerAutomatePickerTelemetryCategoryLbl);
            FlowEnvironmentsApi := '';
            exit;
        end;

        FlowEnvironmentsApi := 'https://' + AzureADTenant.GetPowerPlatformTenantURL() + '/powerautomate/environments?api-version=1';
        Session.LogMessage('0000Q7A', StrSubstNo(PowerAutomateURLTelemetryMsg, FlowEnvironmentsApi), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', PowerAutomatePickerTelemetryCategoryLbl);
    end;

    [InternalEvent(false)]
    internal procedure OnBeforeSendGetEnvironmentRequest(var ResponseText: Text; var Handled: Boolean)
    begin
    end;

}