Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic access verification for AOAI services to develop and run on CAPI/managed AI resources #2764

Merged
merged 44 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1702f14
Slice 535826: [AI][Public Preview]Copilot Toolkit: Automatic access v…
Nov 12, 2024
09f9d04
Restored SetManagedResourceAuthorization with old parameters
Nov 12, 2024
4119ef8
Removed unnessecary variables when verifying Microsoft managed resour…
Nov 12, 2024
9ed666f
Merge commit '9fc5c8aac7c0c57e78b1f75d0830ee8ea718f9cb' into aoai-acc…
Nov 26, 2024
da93eee
Merge branch 'main' of https://github.com/microsoft/BCApps into aoai-…
christian-andersen-msft Dec 12, 2024
0264589
Merge branch 'main' of https://github.com/microsoft/BCApps into aoai-…
christian-andersen-msft Dec 16, 2024
22af527
Simplified configuration check and made it more flexible
christian-andersen-msft Dec 16, 2024
f9abbb7
Merge branch 'main' into aoai-access-verification-rebranch
christian-andersen-msft Dec 19, 2024
5fc3ec2
Merge branch 'main' into aoai-access-verification-rebranch
christian-andersen-msft Jan 6, 2025
2b18667
Temporary Database and notification templates added
christian-andersen-msft Jan 8, 2025
8a08eb2
tmp template changes
christian-andersen-msft Jan 10, 2025
d6c54fe
Grace period with caching added
christian-andersen-msft Jan 10, 2025
54ad50a
Added notification and telemetry to verification logic as well as reo…
christian-andersen-msft Jan 13, 2025
02bd29c
corercted comments
christian-andersen-msft Jan 13, 2025
16e7169
added debugging messages
christian-andersen-msft Jan 14, 2025
e3680d2
Merge branch 'main' into aoai-access-verification-rebranch
christian-andersen-msft Jan 14, 2025
8c41f6d
removed wrong reference
christian-andersen-msft Jan 14, 2025
10ec3e8
Added execution permissions to table and increased test timeouts
christian-andersen-msft Jan 14, 2025
e6ce450
added additional debugging messages
christian-andersen-msft Jan 15, 2025
b6d0848
Added obsolete to old SetManagedResourceAuthorization, Removed NonDeb…
christian-andersen-msft Jan 15, 2025
91c85c7
Updated debugging messages to handle duration
christian-andersen-msft Jan 16, 2025
8d03c7f
fixed errors in debugging formatting
christian-andersen-msft Jan 17, 2025
efd3c2e
Merge branch 'main' into aoai-access-verification-rebranch
christian-andersen-msft Jan 17, 2025
8f292ec
Merge branch 'main' into aoai-access-verification-rebranch
christian-andersen-msft Jan 21, 2025
b7750b2
Updated quality and functionality of logging messages and user notifi…
christian-andersen-msft Jan 22, 2025
e711ea4
Merge branch 'main' into aoai-access-verification-rebranch
christian-andersen-msft Jan 22, 2025
69ae41a
Notification text improved. Logic regarding grace period updated. Deb…
christian-andersen-msft Jan 23, 2025
a1bf072
Variables sorted alphabetically
christian-andersen-msft Jan 23, 2025
41f99e6
field caption clarified
christian-andersen-msft Jan 23, 2025
2df6e9a
Description of parameters improved
christian-andersen-msft Jan 23, 2025
1d2a760
Added session telemetry
christian-andersen-msft Jan 23, 2025
09e7ac7
Added telemetry tags
christian-andersen-msft Jan 23, 2025
8047054
Implemented suggestions from PR
christian-andersen-msft Feb 7, 2025
9b32e37
Merge branch 'main' into aoai-access-verification-rebranch
christian-andersen-msft Feb 7, 2025
cb7580e
Clean tag moved to the correct procedure
christian-andersen-msft Feb 12, 2025
76ae32e
Validates aoai account name and aoai url before account validation
christian-andersen-msft Feb 12, 2025
9582ffb
Merge branch 'main' into aoai-access-verification-rebranch
christian-andersen-msft Feb 12, 2025
485d1f8
fixed hostname verification
christian-andersen-msft Feb 12, 2025
659f07e
Telemetry tags added
christian-andersen-msft Feb 12, 2025
f17d2c1
Merge branch 'main' into aoai-access-verification-rebranch
christian-andersen-msft Feb 13, 2025
d6fa420
Merge branch 'main' into aoai-access-verification-rebranch
christian-andersen-msft Feb 13, 2025
9c25aea
Merge branch 'main' into aoai-access-verification-rebranch
christian-andersen-msft Feb 14, 2025
cf1f046
Replaced CopilotCapability reference with AzureOpenAI
christian-andersen-msft Feb 14, 2025
a48ce42
added references to URI and Regex
christian-andersen-msft Feb 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/System Application/App/AI/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@
"url": "https://go.microsoft.com/fwlink/?linkid=724011",
"logo": "",
"dependencies": [
{
"id": "1b2efb4b-8c44-4d74-a56f-60646645bb21",
"name": "URI",
"publisher": "Microsoft",
"version": "26.0.0.0"
},
{
"id": "b185fd4a-677b-48d3-a701-768de7563df0",
"name": "Regex",
"publisher": "Microsoft",
"version": "26.0.0.0"
},
{
"id": "daa5d70e-eaf5-4256-bf80-53545ef7629a",
"name": "Privacy Notice",
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace System.AI;

using System;
using System.Telemetry;
using System.Utilities;

/// <summary>
/// Store the authorization information for the AOAI service.
Expand All @@ -14,8 +16,10 @@ codeunit 7767 "AOAI Authorization"
Access = Internal;
InherentEntitlements = X;
InherentPermissions = X;
Permissions = tabledata "AOAI Account Verification Log" = RIMD;

var
AzureOpenAIImpl: Codeunit "Azure OpenAI Impl";
[NonDebuggable]
Endpoint: Text;
[NonDebuggable]
Expand All @@ -24,12 +28,20 @@ codeunit 7767 "AOAI Authorization"
ApiKey: SecretText;
[NonDebuggable]
ManagedResourceDeployment: Text;
[NonDebuggable]
AOAIAccountName: Text;
ResourceUtilization: Enum "AOAI Resource Utilization";
TelemetryInvalidAOAIAccountNameFormatTxt: Label 'Attempted use of invalid Azure Open AI account name', Locked = true;
TelemetryInvalidAOAIUrlTxt: Label 'Attempted call with invalid URL', Locked = true;
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
var
AzureOpenAiImpl: Codeunit "Azure OpenAI Impl";
CurrentModule: ModuleInfo;
ALCopilotFunctions: DotNet ALCopilotFunctions;
begin
Expand All @@ -41,12 +53,21 @@ codeunit 7767 "AOAI Authorization"
Enum::"AOAI Resource Utilization"::"Self-Managed":
exit((Deployment <> '') and (Endpoint <> '') and (not ApiKey.IsEmpty()));
Enum::"AOAI Resource Utilization"::"Microsoft Managed":
exit((Deployment <> '') and (Endpoint <> '') and (not ApiKey.IsEmpty()) and (ManagedResourceDeployment <> '') and AzureOpenAiImpl.IsTenantAllowlistedForFirstPartyCopilotCalls());
#if CLEAN26
if (AOAIAccountName <> '') and (ManagedResourceDeployment <> '') and (not ApiKey.IsEmpty()) then
exit(VerifyAOAIAccount(AOAIAccountName, ApiKey) and AzureOpenAiImpl.IsTenantAllowlistedForFirstPartyCopilotCalls())
#else
if (AOAIAccountName <> '') and (ManagedResourceDeployment <> '') and (not ApiKey.IsEmpty()) then
exit(VerifyAOAIAccount(AOAIAccountName, ApiKey) and AzureOpenAiImpl.IsTenantAllowlistedForFirstPartyCopilotCalls())
else
exit((Deployment <> '') and (Endpoint <> '') and (not ApiKey.IsEmpty()) and (ManagedResourceDeployment <> '') and AzureOpenAiImpl.IsTenantAllowlistedForFirstPartyCopilotCalls());
#endif
end;

exit(false);
end;

#if not CLEAN26
[NonDebuggable]
procedure SetMicrosoftManagedAuthorization(NewEndpoint: Text; NewDeployment: Text; NewApiKey: SecretText; NewManagedResourceDeployment: Text)
begin
Expand All @@ -58,6 +79,18 @@ codeunit 7767 "AOAI Authorization"
ApiKey := NewApiKey;
ManagedResourceDeployment := NewManagedResourceDeployment;
end;
#endif

[NonDebuggable]
procedure SetMicrosoftManagedAuthorization(NewAOAIAccountName: Text; NewApiKey: SecretText; NewManagedResourceDeployment: Text)
begin
ClearVariables();

ResourceUtilization := Enum::"AOAI Resource Utilization"::"Microsoft Managed";
AOAIAccountName := NewAOAIAccountName;
ApiKey := NewApiKey;
ManagedResourceDeployment := NewManagedResourceDeployment;
end;

[NonDebuggable]
procedure SetSelfManagedAuthorization(NewEndpoint: Text; NewDeployment: Text; NewApiKey: SecretText)
Expand Down Expand Up @@ -113,7 +146,207 @@ codeunit 7767 "AOAI Authorization"
Clear(Endpoint);
Clear(ApiKey);
Clear(Deployment);
Clear(AOAIAccountName);
Clear(ManagedResourceDeployment);
Clear(ResourceUtilization);
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;
TrustedDomainTxt: Label 'openai.azure.com', Locked = true;
begin
if not IsValidAOAIAccountName(AOAIAccountNameToVerify) then begin
Session.LogMessage('0000OQL', TelemetryInvalidAOAIAccountNameFormatTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', AzureOpenAIImpl.GetAzureOpenAICategory());
exit(false);
end;

Url := StrSubstNo(UrlFormatTxt, AOAIAccountNameToVerify);

if not IsValidUrl(Url, TrustedDomainTxt) then begin
Session.LogMessage('0000OQM', TelemetryInvalidAOAIUrlTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', AzureOpenAIImpl.GetAzureOpenAICategory());
exit(false);
end;

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', AzureOpenAIImpl.GetAzureOpenAICategory());
exit(false);
end;

Session.LogMessage('0000OLR', TelemetryAOAIVerificationSucceededTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', AzureOpenAIImpl.GetAzureOpenAICategory());
exit(true);
end;

local procedure IsValidAOAIAccountName(Subdomain: Text): Boolean
var
RegexPattern: Codeunit Regex;
begin
// Regular expression to validate the Azure OpenAI Instance name according to these requirements "Only alphanumeric characters and hyphens are allowed. The value must be 2-64 characters long and cannot start or end with a hyphen."
// ^[a-zA-Z0-9] : Starts with an alphanumeric character
// [a-zA-Z0-9\-]{0,62} : Allows alphanumeric characters and hyphens, up to 62 characters
// [a-zA-Z0-9]$ : Ends with an alphanumeric character
// Total length: 2-64 characters (1 + 62 + 1)
exit(RegexPattern.IsMatch(Subdomain, '^[a-zA-Z0-9][a-zA-Z0-9\-]{0,62}[a-zA-Z0-9]$'));
end;

local procedure IsValidUrl(Url: Text; TrustedDomain: Text): Boolean
var
UriBuilder: Codeunit "Uri Builder";
HostName: Text;
begin
if (Url = '') or not Url.StartsWith('https://') then
exit(false);

UriBuilder.Init(Url);
HostName := UriBuilder.GetHost();

if HostName.EndsWith(TrustedDomain) then
exit(false);

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', AzureOpenAIImpl.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
exit(false);

// 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', AzureOpenAIImpl.GetAzureOpenAICategory());
exit(true);
end
// Outside GRACE period
else begin
ShowUserNotification(AuthFailedOutsideGracePeriodUserNotificationLbl);
LogTelemetry(AccountName, Today, StrSubstNo(AuthFailedOutsideGracePeriodLogMessageLbl, AccountName, Today));
Session.LogMessage('0000OLU', TelemetryAccessTokenOutsideCachePeriodTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', AzureOpenAIImpl.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; FormattedLogMessage: 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
FormattedLogMessage,
Verbosity::Warning,
DataClassification::OrganizationIdentifiableInformation,
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ codeunit 7771 "Azure OpenAI"
exit(AzureOpenAIImpl.IsInitialized(CopilotCapability, ModelType, CallerModuleInfo));
end;

#if not CLEAN26
/// <summary>
/// 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 <paramref name="ManagedResourceDeployment"/>, and will use the other parameters to verify that you have access to Azure OpenAI.
Expand All @@ -100,10 +101,26 @@ codeunit 7771 "Azure OpenAI"
/// Deployment would look like: gpt-35-turbo-16k
/// </remarks>
[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;
#endif

/// <summary>
/// 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 <paramref name="ManagedResourceDeployment"/>, and will use the other parameters to verify that you have access to Azure OpenAI.
/// </summary>
/// <param name="ModelType">The model type to set authorization for.</param>
/// <param name="AOAIAccountName">Name of the Azure Open AI resource like "MyAzureOpenAIResource"</param>
/// <param name="ApiKey">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.</param>
/// <param name="ManagedResourceDeployment">The managed deployment to use for the model type.</param>
[NonDebuggable]
procedure SetManagedResourceAuthorization(ModelType: Enum "AOAI Model Type"; AOAIAccountName: Text; ApiKey: SecretText; ManagedResourceDeployment: Text)
begin
AzureOpenAIImpl.SetManagedResourceAuthorization(ModelType, AOAIAccountName, ApiKey, ManagedResourceDeployment);
end;

/// <summary>
/// Sets the Azure OpenAI API authorization to use for a specific model type and endpoint.
Expand Down
Loading
Loading