diff --git a/src/System Application/App/AI/src/Azure OpenAI/AOAIAccountVerificationLog.Table.al b/src/System Application/App/AI/src/Azure OpenAI/AOAIAccountVerificationLog.Table.al
new file mode 100644
index 0000000000..e38e8380a4
--- /dev/null
+++ b/src/System Application/App/AI/src/Azure OpenAI/AOAIAccountVerificationLog.Table.al
@@ -0,0 +1,35 @@
+namespace System.AI;
+
+table 7767 "AOAI Account Verification Log"
+{
+ Access = Internal;
+ Caption = 'AOAI Account Verification Log';
+ DataPerCompany = false;
+ Extensible = false;
+ InherentEntitlements = RIMDX;
+ InherentPermissions = X;
+ ReplicateData = false;
+
+ fields
+ {
+ field(1; AccountName; Text[100])
+ {
+ Caption = 'Account Name';
+ DataClassification = CustomerContent;
+ }
+
+ field(2; LastSuccessfulVerification; DateTime)
+ {
+ Caption = 'Time of last successful verification';
+ DataClassification = SystemMetadata;
+ }
+ }
+
+ keys
+ {
+ key(PrimaryKey; AccountName)
+ {
+ Clustered = false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/System Application/App/AI/src/Azure OpenAI/AOAIAuthorization.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/AOAIAuthorization.Codeunit.al
index 2e5ba3e42a..6e2950f938 100644
--- a/src/System Application/App/AI/src/Azure OpenAI/AOAIAuthorization.Codeunit.al
+++ b/src/System Application/App/AI/src/Azure OpenAI/AOAIAuthorization.Codeunit.al
@@ -6,6 +6,7 @@ namespace System.AI;
using System;
+using System.Telemetry;
///
/// Store the authorization information for the AOAI service.
///
@@ -14,8 +15,10 @@ codeunit 7767 "AOAI Authorization"
Access = Internal;
InherentEntitlements = X;
InherentPermissions = X;
+ Permissions = tabledata "AOAI Account Verification Log" = RIMD;
var
+ CopilotCapabilityImpl: Codeunit "Copilot Capability Impl";
[NonDebuggable]
Endpoint: Text;
[NonDebuggable]
@@ -24,7 +27,18 @@ codeunit 7767 "AOAI Authorization"
ApiKey: SecretText;
[NonDebuggable]
ManagedResourceDeployment: Text;
+ [NonDebuggable]
+ AOAIAccountName: Text;
ResourceUtilization: Enum "AOAI Resource Utilization";
+ FirstPartyAuthorization: Boolean;
+ SelfManagedAuthorization: Boolean;
+ MicrosoftManagedAuthorizationWithDeployment: Boolean;
+ MicrosoftManagedAuthorizationWithAOAIAccount: Boolean;
+ TelemetryAOAIVerificationFailedTxt: Label 'Failed to authenticate account against Azure Open AI', Locked = true;
+ TelemetryAOAIVerificationSucceededTxt: Label 'Successfully authenticated account against Azure Open AI', Locked = true;
+ TelemetryAccessWithinCachePeriodTxt: Label 'Cached access to Azure Open AI was used', Locked = true;
+ TelemetryAccessTokenWithinGracePeriodTxt: Label 'Failed to authenticate against Azure Open AI but last successful authentication is within grace period. System still has access for %1', Locked = true;
+ TelemetryAccessTokenOutsideCachePeriodTxt: Label 'Failed to authenticate against Azure Open AI and last successful authentication is outside grace period. System no longer has access', Locked = true;
[NonDebuggable]
procedure IsConfigured(CallerModule: ModuleInfo): Boolean
@@ -32,18 +46,24 @@ codeunit 7767 "AOAI Authorization"
AzureOpenAiImpl: Codeunit "Azure OpenAI Impl";
CurrentModule: ModuleInfo;
ALCopilotFunctions: DotNet ALCopilotFunctions;
+ AOAIAccountIsVerified: Boolean;
begin
NavApp.GetCurrentModuleInfo(CurrentModule);
case ResourceUtilization of
Enum::"AOAI Resource Utilization"::"First Party":
- exit((ManagedResourceDeployment <> '') and ALCopilotFunctions.IsPlatformAuthorizationConfigured(CallerModule.Publisher(), CurrentModule.Publisher()));
+ exit(FirstPartyAuthorization and ALCopilotFunctions.IsPlatformAuthorizationConfigured(CallerModule.Publisher(), CurrentModule.Publisher()));
Enum::"AOAI Resource Utilization"::"Self-Managed":
- exit((Deployment <> '') and (Endpoint <> '') and (not ApiKey.IsEmpty()));
+ exit(SelfManagedAuthorization);
Enum::"AOAI Resource Utilization"::"Microsoft Managed":
- exit((Deployment <> '') and (Endpoint <> '') and (not ApiKey.IsEmpty()) and (ManagedResourceDeployment <> '') and AzureOpenAiImpl.IsTenantAllowlistedForFirstPartyCopilotCalls());
+ if MicrosoftManagedAuthorizationWithAOAIAccount then begin
+ AOAIAccountIsVerified := VerifyAOAIAccount(AOAIAccountName, ApiKey);
+ exit(AOAIAccountIsVerified and AzureOpenAiImpl.IsTenantAllowlistedForFirstPartyCopilotCalls());
+ end
+ else
+ if MicrosoftManagedAuthorizationWithDeployment then
+ exit(AzureOpenAiImpl.IsTenantAllowlistedForFirstPartyCopilotCalls());
end;
-
exit(false);
end;
@@ -57,6 +77,19 @@ codeunit 7767 "AOAI Authorization"
Deployment := NewDeployment;
ApiKey := NewApiKey;
ManagedResourceDeployment := NewManagedResourceDeployment;
+ MicrosoftManagedAuthorizationWithDeployment := true;
+ end;
+
+ [NonDebuggable]
+ procedure SetMicrosoftManagedAuthorization(NewAOAIAccountName: Text; NewApiKey: SecretText; NewManagedResourceDeployment: Text)
+ begin
+ ClearVariables();
+
+ ResourceUtilization := Enum::"AOAI Resource Utilization"::"Microsoft Managed";
+ AOAIAccountName := NewAOAIAccountName;
+ ApiKey := NewApiKey;
+ ManagedResourceDeployment := NewManagedResourceDeployment;
+ MicrosoftManagedAuthorizationWithAOAIAccount := true;
end;
[NonDebuggable]
@@ -68,6 +101,7 @@ codeunit 7767 "AOAI Authorization"
Endpoint := NewEndpoint;
Deployment := NewDeployment;
ApiKey := NewApiKey;
+ SelfManagedAuthorization := true;
end;
[NonDebuggable]
@@ -77,6 +111,7 @@ codeunit 7767 "AOAI Authorization"
ResourceUtilization := Enum::"AOAI Resource Utilization"::"First Party";
ManagedResourceDeployment := NewDeployment;
+ FirstPartyAuthorization := true;
end;
[NonDebuggable]
@@ -113,7 +148,173 @@ codeunit 7767 "AOAI Authorization"
Clear(Endpoint);
Clear(ApiKey);
Clear(Deployment);
+ Clear(AOAIAccountName);
Clear(ManagedResourceDeployment);
Clear(ResourceUtilization);
+ Clear(FirstPartyAuthorization);
+ clear(SelfManagedAuthorization);
+ Clear(MicrosoftManagedAuthorizationWithDeployment);
+ Clear(MicrosoftManagedAuthorizationWithAOAIAccount);
+ end;
+
+ [NonDebuggable]
+ local procedure PerformAOAIAccountVerification(AOAIAccountNameToVerify: Text; NewApiKey: SecretText): Boolean
+ var
+ HttpClient: HttpClient;
+ HttpRequestMessage: HttpRequestMessage;
+ HttpResponseMessage: HttpResponseMessage;
+ HttpContent: HttpContent;
+ ContentHeaders: HttpHeaders;
+ Url: Text;
+ IsSuccessful: Boolean;
+ UrlFormatTxt: Label 'https://%1.openai.azure.com/openai/models?api-version=2024-06-01', Locked = true;
+ begin
+ Url := StrSubstNo(UrlFormatTxt, AOAIAccountNameToVerify);
+
+ HttpContent.GetHeaders(ContentHeaders);
+ if ContentHeaders.Contains('Content-Type') then
+ ContentHeaders.Remove('Content-Type');
+ ContentHeaders.Add('Content-Type', 'application/json');
+ ContentHeaders.Add('api-key', NewApiKey);
+
+ HttpRequestMessage.Method := 'GET';
+ HttpRequestMessage.SetRequestUri(Url);
+ HttpRequestMessage.Content(HttpContent);
+
+ IsSuccessful := HttpClient.Send(HttpRequestMessage, HttpResponseMessage);
+
+ if not IsSuccessful or not HttpResponseMessage.IsSuccessStatusCode() then begin
+ Session.LogMessage('0000OLQ', TelemetryAOAIVerificationFailedTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CopilotCapabilityImpl.GetAzureOpenAICategory());
+ exit(false);
+ end;
+
+ Session.LogMessage('0000OLR', TelemetryAOAIVerificationSucceededTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CopilotCapabilityImpl.GetAzureOpenAICategory());
+ exit(true);
+ end;
+
+ local procedure VerifyAOAIAccount(AccountName: Text; NewApiKey: SecretText): Boolean
+ var
+ VerificationLog: Record "AOAI Account Verification Log";
+ AccountVerified: Boolean;
+ GracePeriod: Duration;
+ CachePeriod: Duration;
+ TruncatedAccountName: Text[100];
+ IsWithinCachePeriod: Boolean;
+ RemainingGracePeriod: Duration;
+ AuthFailedWithinGracePeriodLogMessageLbl: Label 'Azure Open AI authorization failed for account %1 on %2 because it is not authorized to access AI services. The connection will be terminated within %3 if not rectified', Comment = 'Telemetry message where %1 is the name of the Azure Open AI account name, %2 is the date where verification has taken place, and %3 is the remaining time until the grace period expires';
+ AuthFailedOutsideGracePeriodLogMessageLbl: Label 'Azure Open AI authorization failed for account %1 on %2 because it is not authorized to access AI services. The grace period has been exceeded and the connection has been terminated', Comment = 'Telemetry message where %1 is the name of the Azure Open AI account name and %2 is the date where verification has taken place';
+ AuthFailedWithinGracePeriodUserNotificationLbl: Label 'Azure Open AI authorization failed. AI functionality will be disabled within %1. Please contact your system administrator or the extension developer for assistance.', Comment = 'User notification explaining that AI functionality will be disabled soon, where %1 is the remaining time until the grace period expires';
+ AuthFailedOutsideGracePeriodUserNotificationLbl: Label 'Azure Open AI authorization failed and the AI functionality has been disabled. Please contact your system administrator or the extension developer for assistance.', Comment = 'User notification explaining that AI functionality has been disabled';
+
+ begin
+ GracePeriod := 14 * 24 * 60 * 60 * 1000; // 2 weeks in milliseconds
+ CachePeriod := 24 * 60 * 60 * 1000; // 1 day in milliseconds
+ TruncatedAccountName := CopyStr(DelChr(AccountName, '<>', ' '), 1, 100);
+
+ IsWithinCachePeriod := IsAccountVerifiedWithinPeriod(TruncatedAccountName, CachePeriod);
+ // Within CACHE period
+ if IsWithinCachePeriod then begin
+ Session.LogMessage('0000OLS', TelemetryAccessWithinCachePeriodTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CopilotCapabilityImpl.GetAzureOpenAICategory());
+ exit(true);
+ end;
+
+ // Outside CACHE period
+ AccountVerified := PerformAOAIAccountVerification(AccountName, NewApiKey);
+
+ if not AccountVerified then begin
+ if VerificationLog.Get(TruncatedAccountName) then
+ RemainingGracePeriod := GracePeriod - (CurrentDateTime - VerificationLog.LastSuccessfulVerification)
+ else
+ RemainingGracePeriod := GracePeriod;
+
+ // Within GRACE period
+ if IsAccountVerifiedWithinPeriod(TruncatedAccountName, GracePeriod) then begin
+ ShowUserNotification(StrSubstNo(AuthFailedWithinGracePeriodUserNotificationLbl, FormatDurationAsDays(RemainingGracePeriod)));
+ LogTelemetry(AccountName, Today, StrSubstNo(AuthFailedWithinGracePeriodLogMessageLbl, AccountName, Today, FormatDurationAsDays(RemainingGracePeriod)));
+ Session.LogMessage('0000OLT', StrSubstNo(TelemetryAccessTokenWithinGracePeriodTxt, FormatDurationAsDays(RemainingGracePeriod)), Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CopilotCapabilityImpl.GetAzureOpenAICategory());
+ exit(true);
+ end
+ // Outside GRACE period
+ else begin
+ ShowUserNotification(AuthFailedOutsideGracePeriodUserNotificationLbl);
+ LogTelemetry(AccountName, Today, AuthFailedOutsideGracePeriodLogMessageLbl);
+ Session.LogMessage('0000OLU', TelemetryAccessTokenOutsideCachePeriodTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CopilotCapabilityImpl.GetAzureOpenAICategory());
+ exit(false);
+ end;
+ end;
+
+ SaveVerificationTime(TruncatedAccountName);
+ exit(true);
+ end;
+
+ local procedure IsAccountVerifiedWithinPeriod(AccountName: Text[100]; Period: Duration): Boolean
+ var
+ VerificationLog: Record "AOAI Account Verification Log";
+ IsVerified: Boolean;
+ begin
+ if VerificationLog.Get(AccountName) then begin
+ IsVerified := CurrentDateTime - VerificationLog.LastSuccessfulVerification <= Period;
+ exit(IsVerified);
+ end;
+
+ exit(false);
+ end;
+
+ local procedure SaveVerificationTime(AccountName: Text[100])
+ var
+ VerificationLog: Record "AOAI Account Verification Log";
+ begin
+ if VerificationLog.Get(AccountName) then begin
+ VerificationLog.LastSuccessfulVerification := CurrentDateTime;
+ VerificationLog.Modify();
+ end else begin
+ VerificationLog.Init();
+ VerificationLog.AccountName := AccountName;
+ VerificationLog.LastSuccessfulVerification := CurrentDateTime;
+ VerificationLog.Insert()
+ end;
+ end;
+
+ local procedure ShowUserNotification(Message: Text)
+ var
+ Notif: Notification;
+ begin
+ Notif.Message := Message;
+ Notif.Scope := NotificationScope::LocalScope;
+ Notif.Send();
+ end;
+
+ local procedure LogTelemetry(AccountName: Text; VerificationDate: Date; LogMessage: Text)
+ var
+ Telemetry: Codeunit Telemetry;
+ CustomDimensions: Dictionary of [Text, Text];
+ begin
+ CustomDimensions.Add('AccountName', AccountName);
+ CustomDimensions.Add('VerificationDate', Format(VerificationDate));
+
+ Telemetry.LogMessage(
+ '0000AA1', // Event ID
+ StrSubstNo(LogMessage, AccountName, VerificationDate),
+ Verbosity::Warning,
+ DataClassification::SystemMetadata,
+ Enum::"AL Telemetry Scope"::All,
+ CustomDimensions
+ );
+ end;
+
+ local procedure FormatDurationAsDays(DurationValue: Duration): Text
+ var
+ Days: Decimal;
+ DaysLabelLbl: Label '%1 days', Comment = 'Days in plural. %1 is the number of days';
+ DayLabelLbl: Label '1 day', Comment = 'A single day';
+ begin
+ Days := DurationValue / (24 * 60 * 60 * 1000);
+
+ if Days <= 1 then
+ exit(DayLabelLbl)
+ else
+ // Round up to the nearest whole day
+ Days := Round(Days, 1, '>');
+ exit(StrSubstNo(DaysLabelLbl, Format(Days, 0, 0)));
end;
}
\ No newline at end of file
diff --git a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAI.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAI.Codeunit.al
index 7992af7d3f..6a8bc4f1bf 100644
--- a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAI.Codeunit.al
+++ b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAI.Codeunit.al
@@ -100,11 +100,26 @@ codeunit 7771 "Azure OpenAI"
/// Deployment would look like: gpt-35-turbo-16k
///
[NonDebuggable]
+ [Obsolete('Using Managed AI resources now requires different input parameters. Use the other overload for SetManagedResourceAuthorization instead.', '26.0')]
procedure SetManagedResourceAuthorization(ModelType: Enum "AOAI Model Type"; Endpoint: Text; Deployment: Text; ApiKey: SecretText; ManagedResourceDeployment: Text)
begin
AzureOpenAIImpl.SetManagedResourceAuthorization(ModelType, Endpoint, Deployment, ApiKey, ManagedResourceDeployment);
end;
+ ///
+ /// Sets the managed Azure OpenAI API authorization to use for a specific model type.
+ /// This will send the Azure OpenAI call to the deployment specified in , and will use the other parameters to verify that you have access to Azure OpenAI.
+ ///
+ /// The model type to set authorization for.
+ /// Name of the Azure Open AI resource like "MyAzureOpenAIResource"
+ /// The API key to access the Azure Open AI resource. This is used only for verification of access, not for actual Azure Open AI calls.
+ /// The managed deployment to use for the model type.
+ [NonDebuggable]
+ procedure SetManagedResourceAuthorization(ModelType: Enum "AOAI Model Type"; AOAIAccountName: Text; ApiKey: SecretText; ManagedResourceDeployment: Text)
+ begin
+ AzureOpenAIImpl.SetManagedResourceAuthorization(ModelType, AOAIAccountName, ApiKey, ManagedResourceDeployment);
+ end;
+
///
/// Sets the Azure OpenAI API authorization to use for a specific model type and endpoint.
///
diff --git a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al
index 932a32220b..6d43985055 100644
--- a/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al
+++ b/src/System Application/App/AI/src/Azure OpenAI/AzureOpenAIImpl.Codeunit.al
@@ -207,6 +207,21 @@ codeunit 7772 "Azure OpenAI Impl"
end;
end;
+ [NonDebuggable]
+ procedure SetManagedResourceAuthorization(ModelType: Enum "AOAI Model Type"; AOAIAccountName: Text; ApiKey: SecretText; ManagedResourceDeployment: Text)
+ begin
+ case ModelType of
+ Enum::"AOAI Model Type"::"Text Completions":
+ TextCompletionsAOAIAuthorization.SetMicrosoftManagedAuthorization(AOAIAccountName, ApiKey, ManagedResourceDeployment);
+ Enum::"AOAI Model Type"::Embeddings:
+ EmbeddingsAOAIAuthorization.SetMicrosoftManagedAuthorization(AOAIAccountName, ApiKey, ManagedResourceDeployment);
+ Enum::"AOAI Model Type"::"Chat Completions":
+ ChatCompletionsAOAIAuthorization.SetMicrosoftManagedAuthorization(AOAIAccountName, ApiKey, ManagedResourceDeployment);
+ else
+ Error(InvalidModelTypeErr);
+ end;
+ end;
+
[NonDebuggable]
procedure GenerateTextCompletion(Prompt: SecretText; var AOAIOperationResponse: Codeunit "AOAI Operation Response"; CallerModuleInfo: ModuleInfo): Text
var
@@ -757,5 +772,4 @@ codeunit 7772 "Azure OpenAI Impl"
Session.LogMessage('0000MLE', TelemetryTenantAllowlistedMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CopilotCapabilityImpl.GetAzureOpenAICategory());
exit(true);
end;
-
-}
\ No newline at end of file
+}