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

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 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
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace System.AI;

table 7767 "AOAIAccountVerificationLog"
christian-andersen-msft marked this conversation as resolved.
Show resolved Hide resolved
{
Caption = 'AOAI Account Verification Log';
Access = Internal;
Extensible = false;
InherentEntitlements = RIMDX;
InherentPermissions = X;
DataPerCompany = false;
ReplicateData = false;

fields
{
field(1; AccountName; Text[100])
{
Caption = 'Account Name';
DataClassification = CustomerContent;
}

field(2; LastSuccessfulVerification; DateTime)
{
Caption = 'Access Verified';
DataClassification = SystemMetadata;
}
}

keys
{
key(PrimaryKey; AccountName)
{
Clustered = false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace System.AI;

using System;

using System.Telemetry;
christian-andersen-msft marked this conversation as resolved.
Show resolved Hide resolved
/// <summary>
/// Store the authorization information for the AOAI service.
/// </summary>
Expand All @@ -14,6 +14,7 @@ codeunit 7767 "AOAI Authorization"
Access = Internal;
InherentEntitlements = X;
InherentPermissions = X;
Permissions = tabledata AOAIAccountVerificationLog = RIMD;

var
[NonDebuggable]
Expand All @@ -25,6 +26,10 @@ codeunit 7767 "AOAI Authorization"
[NonDebuggable]
ManagedResourceDeployment: Text;
ResourceUtilization: Enum "AOAI Resource Utilization";
[NonDebuggable]
christian-andersen-msft marked this conversation as resolved.
Show resolved Hide resolved
FirstPartyAuthorization: Boolean;
SelfManagedAuthorization: Boolean;
MicrosoftManagedAuthorization: Boolean;

[NonDebuggable]
procedure IsConfigured(CallerModule: ModuleInfo): Boolean
Expand All @@ -37,11 +42,11 @@ codeunit 7767 "AOAI Authorization"

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());
exit(MicrosoftManagedAuthorization and AzureOpenAiImpl.IsTenantAllowlistedForFirstPartyCopilotCalls());
end;

exit(false);
Expand All @@ -57,6 +62,22 @@ codeunit 7767 "AOAI Authorization"
Deployment := NewDeployment;
ApiKey := NewApiKey;
ManagedResourceDeployment := NewManagedResourceDeployment;
MicrosoftManagedAuthorization := true;
end;

[NonDebuggable]
procedure SetMicrosoftManagedAuthorization(AOAIAccountName: Text; NewApiKey: SecretText; NewManagedResourceDeployment: Text)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmmm I suggest we keep the pattern that this file was using before (=> the Set... functions are just setting values, and then we check that the values are valid inside IsConfigured).

This saves us from possible bugs if some other part of the code is changing the authorization variables (and also delays checking the access and the validity of the values until they are actually needed).

Which means, here you just need to set the values for RecourceUtilization, ManagedResourceDeployment, and AOAIAccountName to some global variable. And then you can move the call to VerifyAOAIAccount to inside IsConfigured instead, and you don't need the three boolean variables.

var
IsVerified: Boolean;
begin
ClearVariables();
IsVerified := VerifyAOAIAccount(AOAIAccountName, NewApiKey.Unwrap());
christian-andersen-msft marked this conversation as resolved.
Show resolved Hide resolved

if IsVerified then begin
ResourceUtilization := Enum::"AOAI Resource Utilization"::"Microsoft Managed";
ManagedResourceDeployment := NewManagedResourceDeployment;
MicrosoftManagedAuthorization := true;
end;
end;

[NonDebuggable]
Expand All @@ -68,6 +89,7 @@ codeunit 7767 "AOAI Authorization"
Endpoint := NewEndpoint;
Deployment := NewDeployment;
ApiKey := NewApiKey;
SelfManagedAuthorization := true;
end;

[NonDebuggable]
Expand All @@ -77,6 +99,7 @@ codeunit 7767 "AOAI Authorization"

ResourceUtilization := Enum::"AOAI Resource Utilization"::"First Party";
ManagedResourceDeployment := NewDeployment;
FirstPartyAuthorization := true;
end;

[NonDebuggable]
Expand Down Expand Up @@ -115,5 +138,131 @@ codeunit 7767 "AOAI Authorization"
Clear(Deployment);
Clear(ManagedResourceDeployment);
Clear(ResourceUtilization);
Clear(FirstPartyAuthorization);
clear(SelfManagedAuthorization);
Clear(MicrosoftManagedAuthorization);
end;

[NonDebuggable]
local procedure PerformAOAIAccountVerification(AOAIAccountName: Text; NewApiKey: Text): 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to handle this part of the request in platform? We handle other things there like ensuring the calls are aligned on api-version (I know we spoke about this previously so just wondering if there was a better reason for doing this in sys app)

Now we will have to remember to maintain both app and platform when moving to a newer api-version (and in app we would have to HF all minor releases).

Additionally, the verification log could be a system table so all the tracking and gating could be handled there

begin
Url := StrSubstNo(UrlFormatTxt, AOAIAccountName);

HttpContent.GetHeaders(ContentHeaders);
if ContentHeaders.Contains('Content-Type') then
ContentHeaders.Remove('Content-Type');
ContentHeaders.Add('Content-Type', 'application/json');
ContentHeaders.Add('api-key', NewApiKey);
christian-andersen-msft marked this conversation as resolved.
Show resolved Hide resolved

HttpRequestMessage.Method := 'GET';
HttpRequestMessage.SetRequestUri(Url);
HttpRequestMessage.Content(HttpContent);

IsSuccessful := HttpClient.Send(HttpRequestMessage, HttpResponseMessage);

if not IsSuccessful then
exit(false);

if not HttpResponseMessage.IsSuccessStatusCode() then
exit(false);

exit(true);
end;

local procedure VerifyAOAIAccount(AOAIAccountName: Text; NewApiKey: Text): Boolean
var
Notif: Notification;
IsVerified: Boolean;
GracePeriod: Duration;
CachePeriod: Duration;
TruncatedAccountName: Text[100];
begin
GracePeriod := 15 * 60 * 1000;//14 * 24 * 60 * 60 * 1000; // 2 weeks in milliseconds
christian-andersen-msft marked this conversation as resolved.
Show resolved Hide resolved
CachePeriod := 1 * 60 * 1000;//24 * 60 * 60 * 1000; // 1 day in milliseconds

TruncatedAccountName := CopyStr(AOAIAccountName, 1, 100);

if IsAccountVerifiedWithinPeriod(TruncatedAccountName, CachePeriod) then begin
Message('Verification skipped (within cache period).');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess messages here are also for testing purposes. I think it's still a good idea to log this to telemetry though, so later you can add some Session.LogMessage instead of the Messages.

exit(true);
end;

IsVerified := PerformAOAIAccountVerification(AOAIAccountName, NewApiKey);

// Handle failed verification
if not IsVerified then begin
SendNotification(Notif);
LogTelemetry(AOAIAccountName, Today);
if IsAccountVerifiedWithinPeriod(TruncatedAccountName, GracePeriod) then begin
Message('Verification failed, but account is still valid (within grace period).');
exit(true); // Verified if within grace period
end;
Message('Verification failed, and account is no longer valid (grace period expired).');
exit(false); // Failed verification if grace period has been exceeded
end;
SaveVerificationTime(TruncatedAccountName);
Message('Verification successful. Record saved.');
exit(true);
end;

local procedure IsAccountVerifiedWithinPeriod(AccountName: Text[100]; Period: Duration): Boolean
var
Rec: Record "AOAIAccountVerificationLog";
begin
if Rec.Get(AccountName) then
exit(CurrentDateTime - Rec.LastSuccessfulVerification <= Period);
exit(false);
end;

local procedure SaveVerificationTime(AccountName: Text[100])
var
Rec: Record "AOAIAccountVerificationLog";
begin
if Rec.Get(AccountName) then begin
Rec.LastSuccessfulVerification := CurrentDateTime;
Rec.Modify(true);
end else begin
Rec.Init();
Rec.AccountName := AccountName;
Rec.LastSuccessfulVerification := CurrentDateTime;
Rec.Insert(true);
end;
end;

local procedure SendNotification(var Notif: Notification)
var
MessageLbl: Label 'Azure Open AI authorization failed. AI functionality will be disabled within 2 weeks. Please contact your system administrator or the extension developer for assistance.';
begin
Notif.Message := MessageLbl;
Notif.Scope := NotificationScope::LocalScope;
Notif.Send();
end;

local procedure LogTelemetry(AccountName: Text; VerificationDate: Date)
var
Telemetry: Codeunit Telemetry;
MessageLbl: 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 2 weeks if not rectified', 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';
CustomDimensions: Dictionary of [Text, Text];
begin
CustomDimensions.Add('AccountName', AccountName);
CustomDimensions.Add('VerificationDate', Format(VerificationDate, 0, '<Year4>-<Month,2>-<Day,2>'));

Telemetry.LogMessage(
'0000AA1', // Event ID
StrSubstNo(MessageLbl, AccountName, VerificationDate), // Message
Verbosity::Warning,
DataClassification::SystemMetadata,
Enum::"AL Telemetry Scope"::All,
CustomDimensions
);
end;
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ codeunit 7771 "Azure OpenAI"
AzureOpenAIImpl.SetManagedResourceAuthorization(ModelType, Endpoint, Deployment, ApiKey, ManagedResourceDeployment);
christian-andersen-msft marked this conversation as resolved.
Show resolved Hide resolved
end;

/// <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">Azure OpenAI account name)</param>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: In case of parameters that are not super obvious, I like to add a sample of how they look like in the documentation comment. For example here you could do: An Azure OpenAI account name. For example, if your Azure OpenAI endpoint is "contoso.openai.azure.com", use "contoso" for this parameter.

/// <param name="ApiKey">The API key to use to verify access to Azure OpenAI. This is used only for verification, not for actual Azure OpenAI 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.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,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
Expand Down Expand Up @@ -739,5 +754,4 @@ codeunit 7772 "Azure OpenAI Impl"
Session.LogMessage('0000MLE', TelemetryTenantAllowlistedMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', CopilotCapabilityImpl.GetAzureOpenAICategory());
exit(true);
end;

}
}
Loading