From de3d313f6a209016a956ff31758ac25363b48013 Mon Sep 17 00:00:00 2001 From: Sam <122809235+msft-sam@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:43:17 +0100 Subject: [PATCH] [25.x] [Copilot] Backport privacy check support (#2680) #### Summary Backport the support to check privacy notices for Copilot capabilities. Start the geo expansion of chat. #### Work Item(s) Fixes [AB#502565](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/502565) --------- Co-authored-by: Edward Lynch-Milner <47575833+edwardUL99@users.noreply.github.com> Co-authored-by: Eddy Lynch --- .../src/Copilot/CopilotCapabilitiesGA.Page.al | 11 ++++-- .../CopilotCapabilitiesPreview.Page.al | 11 ++++-- .../src/Copilot/CopilotCapability.Codeunit.al | 11 ++++++ .../Copilot/CopilotCapabilityImpl.Codeunit.al | 16 ++++++++- .../CopilotCapabilityInstall.Codeunit.al | 13 ++++++- .../AI/src/Copilot/CopilotSettings.Table.al | 34 +++++++++++++++++++ .../Privacy Notice/src/PrivacyNotices.Page.al | 1 + .../src/SystemPrivacyNoticeReg.Codeunit.al | 16 ++++++++- 8 files changed, 104 insertions(+), 9 deletions(-) diff --git a/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesGA.Page.al b/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesGA.Page.al index c0fbbc521a..b76b8e4350 100644 --- a/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesGA.Page.al +++ b/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesGA.Page.al @@ -35,7 +35,7 @@ page 7774 "Copilot Capabilities GA" Editable = false; Width = 30; } - field(Status; Rec.Status) + field(Status; Rec.EvaluateStatus()) { ApplicationArea = All; Caption = 'Status'; @@ -89,6 +89,9 @@ page 7774 "Copilot Capabilities GA" trigger OnAction() begin + if not Rec.EnsurePrivacyNoticesApproved() then + exit; + Rec.Status := Rec.Status::Active; Rec.Modify(true); @@ -179,17 +182,19 @@ page 7774 "Copilot Capabilities GA" local procedure SetStatusStyle() begin - if (Rec.Status = Rec.Status::Active) then + if (Rec.EvaluateStatus() = Rec.Status::Active) then StatusStyleExpr := 'Favorable' else StatusStyleExpr := ''; end; local procedure SetActionsEnabled() + var + CopilotCapability: Codeunit "Copilot Capability"; begin if CopilotCapabilityImpl.IsAdmin() then begin ActionsEnabled := (Rec.Capability.AsInteger() <> 0) and DataMovementEnabled; - CapabilityEnabled := Rec.Status = Rec.Status::Active; + CapabilityEnabled := CopilotCapability.IsCapabilityActive(Rec.Capability, Rec."App Id"); end else begin ActionsEnabled := false; diff --git a/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesPreview.Page.al b/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesPreview.Page.al index dc3da9694c..38e90454e1 100644 --- a/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesPreview.Page.al +++ b/src/System Application/App/AI/src/Copilot/CopilotCapabilitiesPreview.Page.al @@ -35,7 +35,7 @@ page 7773 "Copilot Capabilities Preview" Editable = false; Width = 30; } - field(Status; Rec.Status) + field(Status; Rec.EvaluateStatus()) { ApplicationArea = All; Caption = 'Status'; @@ -89,6 +89,9 @@ page 7773 "Copilot Capabilities Preview" trigger OnAction() begin + if not Rec.EnsurePrivacyNoticesApproved() then + exit; + Rec.Status := Rec.Status::Active; Rec.Modify(true); @@ -166,17 +169,19 @@ page 7773 "Copilot Capabilities Preview" local procedure SetStatusStyle() begin - if (Rec.Status = Rec.Status::Active) then + if (Rec.EvaluateStatus() = Rec.Status::Active) then StatusStyleExpr := 'Favorable' else StatusStyleExpr := ''; end; local procedure SetActionsEnabled() + var + CopilotCapability: Codeunit "Copilot Capability"; begin if CopilotCapabilityImpl.IsAdmin() then begin ActionsEnabled := (Rec.Capability.AsInteger() <> 0) and DataMovementEnabled; - CapabilityEnabled := Rec.Status = Rec.Status::Active; + CapabilityEnabled := CopilotCapability.IsCapabilityActive(Rec.Capability, Rec."App Id"); end else begin ActionsEnabled := false; diff --git a/src/System Application/App/AI/src/Copilot/CopilotCapability.Codeunit.al b/src/System Application/App/AI/src/Copilot/CopilotCapability.Codeunit.al index 5f4b18bec3..0f6215c940 100644 --- a/src/System Application/App/AI/src/Copilot/CopilotCapability.Codeunit.al +++ b/src/System Application/App/AI/src/Copilot/CopilotCapability.Codeunit.al @@ -118,4 +118,15 @@ codeunit 7773 "Copilot Capability" begin exit(CopilotCapabilityImpl.IsCapabilityActive(CopilotCapability, AppId)); end; + + /// + /// Event that is raised to specify if a Copilot capability depends on any additional privacy notices. + /// + /// The capability. + /// The app id associated with the capability. + /// The list of required privacy notices for the capability to be enabled. + [IntegrationEvent(false, false)] + procedure OnGetRequiredPrivacyNotices(CopilotCapability: Enum "Copilot Capability"; AppId: Guid; var RequiredPrivacyNotices: List of [Code[50]]) + begin + end; } \ No newline at end of file diff --git a/src/System Application/App/AI/src/Copilot/CopilotCapabilityImpl.Codeunit.al b/src/System Application/App/AI/src/Copilot/CopilotCapabilityImpl.Codeunit.al index af52a26f86..872f3a2d42 100644 --- a/src/System Application/App/AI/src/Copilot/CopilotCapabilityImpl.Codeunit.al +++ b/src/System Application/App/AI/src/Copilot/CopilotCapabilityImpl.Codeunit.al @@ -130,13 +130,27 @@ codeunit 7774 "Copilot Capability Impl" procedure IsCapabilityActive(CopilotCapability: Enum "Copilot Capability"; AppId: Guid): Boolean var CopilotSettings: Record "Copilot Settings"; + CopilotCapabilityCU: Codeunit "Copilot Capability"; + PrivacyNotice: Codeunit "Privacy Notice"; + RequiredPrivacyNotices: List of [Code[50]]; + RequiredPrivacyNotice: Code[50]; begin CopilotSettings.ReadIsolation(IsolationLevel::ReadCommitted); CopilotSettings.SetLoadFields(Status); if not CopilotSettings.Get(CopilotCapability, AppId) then exit(false); - exit(CopilotSettings.Status = Enum::"Copilot Status"::Active); + CopilotCapabilityCU.OnGetRequiredPrivacyNotices(CopilotCapability, AppId, RequiredPrivacyNotices); + + if (CopilotSettings.Status <> Enum::"Copilot Status"::Active) or (RequiredPrivacyNotices.Count() <= 0) then + exit(CopilotSettings.Status = Enum::"Copilot Status"::Active); + + // check privacy notices + foreach RequiredPrivacyNotice in RequiredPrivacyNotices do + if (PrivacyNotice.GetPrivacyNoticeApprovalState(RequiredPrivacyNotice, true) <> Enum::"Privacy Notice Approval State"::Agreed) then + exit(false); + + exit(true); end; procedure SendActivateTelemetry(CopilotCapability: Enum "Copilot Capability"; AppId: Guid) diff --git a/src/System Application/App/AI/src/Copilot/CopilotCapabilityInstall.Codeunit.al b/src/System Application/App/AI/src/Copilot/CopilotCapabilityInstall.Codeunit.al index a15e3dca3f..29e5f817ee 100644 --- a/src/System Application/App/AI/src/Copilot/CopilotCapabilityInstall.Codeunit.al +++ b/src/System Application/App/AI/src/Copilot/CopilotCapabilityInstall.Codeunit.al @@ -5,6 +5,7 @@ namespace System.AI; using System.Environment; +using System; codeunit 7760 "Copilot Capability Install" { @@ -24,11 +25,13 @@ codeunit 7760 "Copilot Capability Install" internal procedure RegisterCapabilities() var EnvironmentInformation: Codeunit "Environment Information"; + WithinEUDB: Boolean; ApplicationFamily: Text; begin ApplicationFamily := EnvironmentInformation.GetApplicationFamily(); + if TryGetIsWithinEUDB(WithinEUDB) then; - if ApplicationFamily = 'US' then + if ApplicationFamily in ['US', 'MX'] or WithinEUDB then RegisterSaaSCapability(Enum::"Copilot Capability"::Chat, Enum::"Copilot Availability"::Preview, ChatLearnMoreLbl); RegisterSaaSCapability(Enum::"Copilot Capability"::"Analyze List", Enum::"Copilot Availability"::Preview, AnalyzeListLearnMoreLbl); @@ -44,6 +47,14 @@ codeunit 7760 "Copilot Capability Install" CopilotCapability.RegisterCapability(Capability, Availability, LearnMoreUrl); end; + [TryFunction] + local procedure TryGetIsWithinEUDB(var WithinEUDB: Boolean) + var + ALCopilotFunctions: DotNet ALCopilotFunctions; + begin + WithinEUDB := ALCopilotFunctions.IsWithinEUDB(); + end; + [EventSubscriber(ObjectType::Page, Page::"Copilot AI Capabilities", 'OnRegisterCopilotCapability', '', false, false)] local procedure OnRegisterCopilotCapability() begin diff --git a/src/System Application/App/AI/src/Copilot/CopilotSettings.Table.al b/src/System Application/App/AI/src/Copilot/CopilotSettings.Table.al index 2f2644e30b..9ee6c06bfb 100644 --- a/src/System Application/App/AI/src/Copilot/CopilotSettings.Table.al +++ b/src/System Application/App/AI/src/Copilot/CopilotSettings.Table.al @@ -4,6 +4,8 @@ // ------------------------------------------------------------------------------------------------ namespace System.AI; +using System.Privacy; + /// /// Table to keep track of each Copilot Capability settings. /// @@ -51,4 +53,36 @@ table 7775 "Copilot Settings" Clustered = true; } } + + procedure EvaluateStatus(): Enum "Copilot Status" + var + CopilotCapability: Codeunit "Copilot Capability"; + begin + if Rec.Status <> Rec.Status::Active then + exit(Rec.Status); + + if CopilotCapability.IsCapabilityActive(Rec.Capability, Rec."App Id") then + exit(Rec.Status::Active) + else + exit(Rec.Status::Inactive); + end; + + procedure EnsurePrivacyNoticesApproved(): Boolean + var + CopilotCapability: Codeunit "Copilot Capability"; + PrivacyNotice: Codeunit "Privacy Notice"; + RequiredPrivacyNotices: List of [Code[50]]; + RequiredPrivacyNotice: Code[50]; + begin + CopilotCapability.OnGetRequiredPrivacyNotices(Rec.Capability, Rec."App Id", RequiredPrivacyNotices); + + if RequiredPrivacyNotices.Count() <= 0 then + exit(true); + + foreach RequiredPrivacyNotice in RequiredPrivacyNotices do + if not PrivacyNotice.ConfirmPrivacyNoticeApproval(RequiredPrivacyNotice, true) then + exit(false); + + exit(true); + end; } \ No newline at end of file diff --git a/src/System Application/App/Privacy Notice/src/PrivacyNotices.Page.al b/src/System Application/App/Privacy Notice/src/PrivacyNotices.Page.al index 290732a3df..9207710f58 100644 --- a/src/System Application/App/Privacy Notice/src/PrivacyNotices.Page.al +++ b/src/System Application/App/Privacy Notice/src/PrivacyNotices.Page.al @@ -158,6 +158,7 @@ page 1565 "Privacy Notices" trigger OnAfterGetRecord() begin + Rec.CalcFields(Rec.Enabled, Rec.Disabled); Accepted := Rec.Enabled; Rejected := Rec.Disabled; UserDecides := not (Accepted or Rejected); diff --git a/src/System Application/App/Privacy Notice/src/SystemPrivacyNoticeReg.Codeunit.al b/src/System Application/App/Privacy Notice/src/SystemPrivacyNoticeReg.Codeunit.al index 7c7538f19e..00c72fa1d0 100644 --- a/src/System Application/App/Privacy Notice/src/SystemPrivacyNoticeReg.Codeunit.al +++ b/src/System Application/App/Privacy Notice/src/SystemPrivacyNoticeReg.Codeunit.al @@ -5,9 +5,11 @@ namespace System.Privacy; +/// +/// This codeunit registers platform level privacy notices and provides procedures to consistently reference the privacy notices. +/// codeunit 1566 "System Privacy Notice Reg." { - Access = Internal; InherentEntitlements = X; InherentPermissions = X; @@ -28,16 +30,28 @@ codeunit 1566 "System Privacy Notice Reg." if not TempPrivacyNotice.Insert() then; end; + /// + /// Gets the Microsoft Teams privacy notice identifier. + /// + /// The privacy notice id for Microsoft Teams. procedure GetTeamsPrivacyNoticeId(): Code[50] begin exit(MicrosoftTeamsTxt); end; + /// + /// Gets the Power Automate privacy notice identifier. + /// + /// The privacy notice id for Power Automate. procedure GetPowerAutomatePrivacyNoticeId(): Code[50] begin exit(PowerAutomateIdTxt); end; + /// + /// Gets the Power Automate privacy notice name. + /// + /// The privacy notice name for Power Automate. procedure GetPowerAutomatePrivacyNoticeName(): Code[250] begin exit(PowerAutomateLabelTxt);