diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/Entitlements/ExtBlobStorageConnector.Entitlement.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/Entitlements/ExtBlobStorageConnector.Entitlement.al new file mode 100644 index 0000000000..9233f60fd8 --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/Entitlements/ExtBlobStorageConnector.Entitlement.al @@ -0,0 +1,13 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +entitlement "Ext. Blob Storage Connector" +{ + + ObjectEntitlements = "Ext. Blob Stor. - Edit"; + Type = Implicit; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/ExtensionLogo.png b/Apps/W1/External File Storage - Azure Blob Service Connector/app/ExtensionLogo.png new file mode 100644 index 0000000000..30941b354f Binary files /dev/null and b/Apps/W1/External File Storage - Azure Blob Service Connector/app/ExtensionLogo.png differ diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/README.md b/Apps/W1/External File Storage - Azure Blob Service Connector/app/README.md new file mode 100644 index 0000000000..9e127e4852 --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/README.md @@ -0,0 +1,2 @@ +# External File Storage - Azure Blob Storage Connector +This connector allows access to Azure Blob Storage Containers. diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/app.json b/Apps/W1/External File Storage - Azure Blob Service Connector/app/app.json new file mode 100644 index 0000000000..e79567a968 --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/app.json @@ -0,0 +1,37 @@ +{ + "id": "c9ce86fe-cb70-4b79-be03-d21856b1a4ca", + "name": "External File Storage - Azure Blob Service Connector", + "publisher": "Microsoft", + "brief": "Enables file and folder operations for Azure Blob Service Containers via the External File Storage Module with Business Central.", + "description": "This app enables file and folder operations for Azure Blob Service Containers via the External File Storage Module with Business Central.", + "version": "26.0.0.0", + "privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009", + "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120", + "help": "https://go.microsoft.com/fwlink/?linkid=2134520", + "url": "https://go.microsoft.com/fwlink/?linkid=724011", + "logo": "ExtensionLogo.png", + "application": "26.0.0.0", + "platform": "26.0.0.0", + "internalsVisibleTo": [ + { + "id": "adcda309-4da8-43b8-b05d-d0287462ed42", + "name": "External File Storage - Azure Blob Service Connector Tests", + "publisher": "Microsoft" + } + ], + "dependencies": [], + "screenshots": [], + "idRanges": [ + { + "from": 4560, + "to": 4569 + } + ], + "resourceExposurePolicy": { + "allowDebugging": true, + "allowDownloadingSource": true, + "includeSourceInSymbolFile": true + }, + "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520", + "resourceFolders": ["data"] +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/data/connector-logo.png b/Apps/W1/External File Storage - Azure Blob Service Connector/app/data/connector-logo.png new file mode 100644 index 0000000000..d94371448c Binary files /dev/null and b/Apps/W1/External File Storage - Azure Blob Service Connector/app/data/connector-logo.png differ diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorEdit.PermissionSet.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorEdit.PermissionSet.al new file mode 100644 index 0000000000..344a6585cc --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorEdit.PermissionSet.al @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +permissionset 4562 "Ext. Blob Stor. - Edit" +{ + Access = Public; + Assignable = false; + Caption = 'Blob Storage - Edit'; + + IncludedPermissionSets = "Ext. Blob Stor. - Read"; + + Permissions = + tabledata "Ext. Blob Storage Account" = imd; +} diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorObjects.PermissionSet.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorObjects.PermissionSet.al new file mode 100644 index 0000000000..cf89506c00 --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorObjects.PermissionSet.al @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +permissionset 4560 "Ext. Blob Stor. - Objects" +{ + Access = Public; + Assignable = false; + Caption = 'Blob Storage - Objects'; + + Permissions = + table "Ext. Blob Storage Account" = X, + page "Ext. Blob Stor. Account Wizard" = X, + page "Ext. Blob Sto Container Lookup" = X, + page "Ext. Blob Storage Account" = X; +} diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorRead.PermissionSet.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorRead.PermissionSet.al new file mode 100644 index 0000000000..183d766d32 --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorRead.PermissionSet.al @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +permissionset 4561 "Ext. Blob Stor. - Read" +{ + Access = Public; + Assignable = false; + Caption = 'Blob Storage - Read'; + + IncludedPermissionSets = "Ext. Blob Stor. - Objects"; + + Permissions = + tabledata "Ext. Blob Storage Account" = r; +} diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageAdminExtBlobStorage.PermissionSetExt.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageAdminExtBlobStorage.PermissionSetExt.al new file mode 100644 index 0000000000..0c91b5322c --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageAdminExtBlobStorage.PermissionSetExt.al @@ -0,0 +1,11 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +permissionsetextension 4560 "File Storage - Admin - Ext. Blob Storage" extends "File Storage - Admin" +{ + IncludedPermissionSets = "Ext. Blob Stor. - Edit"; +} diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageEditExtBlobStorage.PermissionSetExt.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageEditExtBlobStorage.PermissionSetExt.al new file mode 100644 index 0000000000..1c70d0195a --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageEditExtBlobStorage.PermissionSetExt.al @@ -0,0 +1,11 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +permissionsetextension 4561 "File Storage - Edit - Ext. Blob Storage" extends "File Storage - Edit" +{ + IncludedPermissionSets = "Ext. Blob Stor. - Read"; +} diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoConnectorImpl.Codeunit.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoConnectorImpl.Codeunit.al new file mode 100644 index 0000000000..f15c208587 --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoConnectorImpl.Codeunit.al @@ -0,0 +1,508 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +using System.Text; +using System.Utilities; +using System.Azure.Storage; +using System.DataAdministration; + +codeunit 4560 "Ext. Blob Sto. Connector Impl." implements "External File Storage Connector" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = tabledata "Ext. Blob Storage Account" = rimd; + + var + ConnectorDescriptionTxt: Label 'Use Azure Blob Storage to store and retrieve files.'; + NotRegisteredAccountErr: Label 'We could not find the account. Typically, this is because the account has been deleted.'; + MarkerFileNameTok: Label 'BusinessCentral.FileSystem.txt', Locked = true; + + /// + /// Gets a List of Files stored on the provided account. + /// + /// The file account ID which is used to get the file. + /// The file path to list. + /// Defines the pagination data. + /// A list with all files stored in the path. + procedure ListFiles(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) + var + ABSContainerContent: Record "ABS Container Content"; + ABSBlobClient: Codeunit "ABS Blob Client"; + ABSOperationResponse: Codeunit "ABS Operation Response"; + ABSOptionalParameters: Codeunit "ABS Optional Parameters"; + begin + InitBlobClient(AccountId, ABSBlobClient); + CheckPath(Path); + InitOptionalParameters(Path, FilePaginationData, ABSOptionalParameters); + ABSOptionalParameters.Delimiter('/'); + ABSOperationResponse := ABSBlobClient.ListBlobs(ABSContainerContent, ABSOptionalParameters); + ValidateListingResponse(FilePaginationData, ABSOperationResponse); + + ABSContainerContent.SetFilter("Blob Type", '<>%1', ''); + ABSContainerContent.SetFilter(Name, '<>%1', MarkerFileNameTok); + if not ABSContainerContent.FindSet() then + exit; + + repeat + TempFileAccountContent.Init(); + TempFileAccountContent.Name := ABSContainerContent.Name; + TempFileAccountContent.Type := TempFileAccountContent.Type::"File"; + TempFileAccountContent."Parent Directory" := ABSContainerContent."Parent Directory"; + TempFileAccountContent.Insert(); + until ABSContainerContent.Next() = 0; + end; + + /// + /// Gets a file from the provided account. + /// + /// The file account ID which is used to get the file. + /// The file path inside the file account. + /// The Stream were the file is read to. + procedure GetFile(AccountId: Guid; Path: Text; Stream: InStream) + var + ABSBlobClient: Codeunit "ABS Blob Client"; + ABSOperationResponse: Codeunit "ABS Operation Response"; + begin + InitBlobClient(AccountId, ABSBlobClient); + ABSOperationResponse := ABSBlobClient.GetBlobAsStream(Path, Stream); + + if ABSOperationResponse.IsSuccessful() then + exit; + + Error(ABSOperationResponse.GetError()); + end; + + /// + /// Create a file in the provided account. + /// + /// The file account ID which is used to send out the file. + /// The file path inside the file account. + /// The Stream were the file is read from. + procedure CreateFile(AccountId: Guid; Path: Text; Stream: InStream) + var + ABSBlobClient: Codeunit "ABS Blob Client"; + ABSOperationResponse: Codeunit "ABS Operation Response"; + begin + InitBlobClient(AccountId, ABSBlobClient); + ABSOperationResponse := ABSBlobClient.PutBlobBlockBlobStream(Path, Stream); + + if ABSOperationResponse.IsSuccessful() then + exit; + + Error(ABSOperationResponse.GetError()); + end; + + /// + /// Copies as file inside the provided account. + /// + /// The file account ID which is used to send out the file. + /// The source file path. + /// The target file path. + procedure CopyFile(AccountId: Guid; SourcePath: Text; TargetPath: Text) + var + ABSBlobClient: Codeunit "ABS Blob Client"; + ABSOperationResponse: Codeunit "ABS Operation Response"; + begin + InitBlobClient(AccountId, ABSBlobClient); + ABSOperationResponse := ABSBlobClient.CopyBlob(TargetPath, SourcePath); + + if ABSOperationResponse.IsSuccessful() then + exit; + + Error(ABSOperationResponse.GetError()); + end; + + /// + /// Move as file inside the provided account. + /// + /// The file account ID which is used to send out the file. + /// The source file path. + /// The target file path. + procedure MoveFile(AccountId: Guid; SourcePath: Text; TargetPath: Text) + var + ABSBlobClient: Codeunit "ABS Blob Client"; + ABSOperationResponse: Codeunit "ABS Operation Response"; + begin + InitBlobClient(AccountId, ABSBlobClient); + ABSOperationResponse := ABSBlobClient.CopyBlob(TargetPath, SourcePath); + if not ABSOperationResponse.IsSuccessful() then + Error(ABSOperationResponse.GetError()); + + ABSOperationResponse := ABSBlobClient.DeleteBlob(SourcePath); + if not ABSOperationResponse.IsSuccessful() then + Error(ABSOperationResponse.GetError()); + end; + + /// + /// Checks if a file exists on the provided account. + /// + /// The file account ID which is used to send out the file. + /// The file path inside the file account. + /// Returns true if the file exists + procedure FileExists(AccountId: Guid; Path: Text): Boolean + var + ABSContainerContent: Record "ABS Container Content"; + ABSBlobClient: Codeunit "ABS Blob Client"; + ABSOperationResponse: Codeunit "ABS Operation Response"; + ABSOptionalParameters: Codeunit "ABS Optional Parameters"; + begin + if Path = '' then + exit(false); + + InitBlobClient(AccountId, ABSBlobClient); + ABSOptionalParameters.Prefix(Path); + ABSOperationResponse := ABSBlobClient.ListBlobs(ABSContainerContent, ABSOptionalParameters); + if not ABSOperationResponse.IsSuccessful() then + Error(ABSOperationResponse.GetError()); + + exit(not ABSContainerContent.IsEmpty()); + end; + + /// + /// Deletes a file exists on the provided account. + /// + /// The file account ID which is used to send out the file. + /// The file path inside the file account. + procedure DeleteFile(AccountId: Guid; Path: Text) + var + ABSBlobClient: Codeunit "ABS Blob Client"; + ABSOperationResponse: Codeunit "ABS Operation Response"; + begin + InitBlobClient(AccountId, ABSBlobClient); + ABSOperationResponse := ABSBlobClient.DeleteBlob(Path); + + if ABSOperationResponse.IsSuccessful() then + exit; + + Error(ABSOperationResponse.GetError()); + end; + + /// + /// Gets a List of Directories stored on the provided account. + /// + /// The file account ID which is used to get the file. + /// The file path to list. + /// Defines the pagination data. + /// A list with all directories stored in the path. + procedure ListDirectories(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) + var + ABSContainerContent: Record "ABS Container Content"; + ABSBlobClient: Codeunit "ABS Blob Client"; + ABSOperationResponse: Codeunit "ABS Operation Response"; + ABSOptionalParameters: Codeunit "ABS Optional Parameters"; + begin + InitBlobClient(AccountId, ABSBlobClient); + CheckPath(Path); + InitOptionalParameters(Path, FilePaginationData, ABSOptionalParameters); + ABSOperationResponse := ABSBlobClient.ListBlobs(ABSContainerContent, ABSOptionalParameters); + ValidateListingResponse(FilePaginationData, ABSOperationResponse); + + ABSContainerContent.SetRange("Parent Directory", Path); + ABSContainerContent.SetRange("Blob Type", ''); + if not ABSContainerContent.FindSet() then + exit; + + repeat + TempFileAccountContent.Init(); + TempFileAccountContent.Name := ABSContainerContent.Name; + TempFileAccountContent.Type := TempFileAccountContent.Type::Directory; + TempFileAccountContent."Parent Directory" := ABSContainerContent."Parent Directory"; + TempFileAccountContent.Insert(); + until ABSContainerContent.Next() = 0; + end; + + /// + /// Creates a directory on the provided account. + /// + /// The file account ID which is used to send out the file. + /// The directory path inside the file account. + procedure CreateDirectory(AccountId: Guid; Path: Text) + var + TempBlob: Codeunit "Temp Blob"; + IStream: InStream; + OStream: OutStream; + DirectoryAlreadyExistsErr: Label 'Directory already exists.'; + MarkerFileContentTok: Label 'This is a directory marker file created by Business Central. It is safe to delete it.', Locked = true; + begin + if DirectoryExists(AccountId, Path) then + Error(DirectoryAlreadyExistsErr); + + Path := CombinePath(Path, MarkerFileNameTok); + TempBlob.CreateOutStream(OStream); + OStream.WriteText(MarkerFileContentTok); + + TempBlob.CreateInStream(IStream); + CreateFile(AccountId, Path, IStream); + end; + + /// + /// Checks if a directory exists on the provided account. + /// + /// The file account ID which is used to send out the file. + /// The directory path inside the file account. + /// Returns true if the directory exists + procedure DirectoryExists(AccountId: Guid; Path: Text): Boolean + var + ABSContainerContent: Record "ABS Container Content"; + ABSBlobClient: Codeunit "ABS Blob Client"; + ABSOperationResponse: Codeunit "ABS Operation Response"; + ABSOptionalParameters: Codeunit "ABS Optional Parameters"; + begin + if Path = '' then + exit(true); + + InitBlobClient(AccountId, ABSBlobClient); + ABSOptionalParameters.Prefix(Path); + ABSOptionalParameters.MaxResults(1); + ABSOperationResponse := ABSBlobClient.ListBlobs(ABSContainerContent, ABSOptionalParameters); + if not ABSOperationResponse.IsSuccessful() then + Error(ABSOperationResponse.GetError()); + + exit(not ABSContainerContent.IsEmpty()); + end; + + /// + /// Deletes a directory exists on the provided account. + /// + /// The file account ID which is used to send out the file. + /// The directory path inside the file account. + procedure DeleteDirectory(AccountId: Guid; Path: Text) + var + TempFileAccountContent: Record "File Account Content" temporary; + FilePaginationData: Codeunit "File Pagination Data"; + DirectoryMustBeEmptyErr: Label 'Directory is not empty.'; + begin + ListFiles(AccountId, Path, FilePaginationData, TempFileAccountContent); + ListDirectories(AccountId, Path, FilePaginationData, TempFileAccountContent); + TempFileAccountContent.SetFilter(Name, '<>%1', MarkerFileNameTok); + if not TempFileAccountContent.IsEmpty() then + Error(DirectoryMustBeEmptyErr); + + DeleteFile(AccountId, CombinePath(Path, MarkerFileNameTok)); + end; + + /// + /// Gets the registered accounts for the Blob Storage connector. + /// + /// Out parameter holding all the registered accounts for the Blob Storage connector. + procedure GetAccounts(var TempAccounts: Record "File Account" temporary) + var + Account: Record "Ext. Blob Storage Account"; + begin + if not Account.FindSet() then + exit; + + repeat + TempAccounts."Account Id" := Account.Id; + TempAccounts.Name := Account.Name; + TempAccounts.Connector := Enum::"Ext. File Storage Connector"::"Blob Storage"; + TempAccounts.Insert(); + until Account.Next() = 0; + end; + + /// + /// Shows accounts information. + /// + /// The ID of the account to show. + procedure ShowAccountInformation(AccountId: Guid) + var + BlobStorageAccountLocal: Record "Ext. Blob Storage Account"; + begin + if not BlobStorageAccountLocal.Get(AccountId) then + Error(NotRegisteredAccountErr); + + BlobStorageAccountLocal.SetRecFilter(); + Page.Run(Page::"Ext. Blob Storage Account", BlobStorageAccountLocal); + end; + + /// + /// Register an file account for the Blob Storage connector. + /// + /// Out parameter holding details of the registered account. + /// True if the registration was successful; false - otherwise. + procedure RegisterAccount(var TempAccount: Record "File Account" temporary): Boolean + var + BlobStorageAccountWizard: Page "Ext. Blob Stor. Account Wizard"; + begin + BlobStorageAccountWizard.RunModal(); + + exit(BlobStorageAccountWizard.GetAccount(TempAccount)); + end; + + /// + /// Deletes an file account for the Blob Storage connector. + /// + /// The ID of the Blob Storage account + /// True if an account was deleted. + procedure DeleteAccount(AccountId: Guid): Boolean + var + BlobStorageAccountLocal: Record "Ext. Blob Storage Account"; + begin + if BlobStorageAccountLocal.Get(AccountId) then + exit(BlobStorageAccountLocal.Delete()); + + exit(false); + end; + + /// + /// Gets a description of the Blob Storage connector. + /// + /// A short description of the Blob Storage connector. + procedure GetDescription(): Text[250] + begin + exit(ConnectorDescriptionTxt); + end; + + /// + /// Gets the Blob Storage connector logo. + /// + /// A base64-formatted image to be used as logo. + procedure GetLogoAsBase64(): Text + var + Base64Convert: Codeunit "Base64 Convert"; + Stream: InStream; + begin + NavApp.GetResource('connector-logo.png', Stream); + exit(Base64Convert.ToBase64(Stream)); + end; + + internal procedure IsAccountValid(var TempAccount: Record "Ext. Blob Storage Account" temporary): Boolean + begin + if TempAccount.Name = '' then + exit(false); + + if TempAccount."Storage Account Name" = '' then + exit(false); + + if TempAccount."Container Name" = '' then + exit(false); + + exit(true); + end; + + internal procedure CreateAccount(var AccountToCopy: Record "Ext. Blob Storage Account"; Password: SecretText; var FileAccount: Record "File Account") + var + NewBlobStorageAccount: Record "Ext. Blob Storage Account"; + begin + NewBlobStorageAccount.TransferFields(AccountToCopy); + + NewBlobStorageAccount.Id := CreateGuid(); + NewBlobStorageAccount.SetSecret(Password); + + NewBlobStorageAccount.Insert(); + + FileAccount."Account Id" := NewBlobStorageAccount.Id; + FileAccount.Name := NewBlobStorageAccount.Name; + FileAccount.Connector := Enum::"Ext. File Storage Connector"::"Blob Storage"; + end; + + internal procedure LookUpContainer(var Account: Record "Ext. Blob Storage Account"; AuthType: Enum "Ext. Blob Storage Auth. Type"; Secret: SecretText; var NewContainerName: Text[2048]) + var + ABSContainers: Record "ABS Container"; + ABSContainerClient: Codeunit "ABS Container Client"; + StorageServiceAuthorization: Codeunit "Storage Service Authorization"; + ABSOperationResponse: Codeunit "ABS Operation Response"; + Authorization: Interface "Storage Service Authorization"; + begin + Account.TestField("Storage Account Name"); + case AuthType of + AuthType::SasToken: + Authorization := SetReadySAS(StorageServiceAuthorization, Secret); + AuthType::SharedKey: + Authorization := StorageServiceAuthorization.CreateSharedKey(Secret); + end; + + ABSContainerClient.Initialize(Account."Storage Account Name", Authorization); + ABSOperationResponse := ABSContainerClient.ListContainers(ABSContainers); + if not ABSOperationResponse.IsSuccessful() then + Error(ABSOperationResponse.GetError()); + + if not ABSContainers.Get(NewContainerName) then + if ABSContainers.FindFirst() then; + + if (Page.RunModal(Page::"Ext. Blob Sto Container Lookup", ABSContainers) <> Action::LookupOK) then + exit; + + NewContainerName := ABSContainers.Name; + end; + + local procedure InitBlobClient(var AccountId: Guid; var ABSBlobClient: Codeunit "ABS Blob Client") + var + BlobStorageAccount: Record "Ext. Blob Storage Account"; + StorageServiceAuthorization: Codeunit "Storage Service Authorization"; + Authorization: Interface "Storage Service Authorization"; + AccountDisabledErr: Label 'The account "%1" is disabled.', Comment = '%1 - Account Name'; + begin + BlobStorageAccount.Get(AccountId); + if BlobStorageAccount.Disabled then + Error(AccountDisabledErr, BlobStorageAccount.Name); + + case BlobStorageAccount."Authorization Type" of + "Ext. Blob Storage Auth. Type"::SasToken: + Authorization := SetReadySAS(StorageServiceAuthorization, BlobStorageAccount.GetSecret(BlobStorageAccount."Secret Key")); + "Ext. Blob Storage Auth. Type"::SharedKey: + Authorization := StorageServiceAuthorization.CreateSharedKey(BlobStorageAccount.GetSecret(BlobStorageAccount."Secret Key")); + end; + ABSBlobClient.Initialize(BlobStorageAccount."Storage Account Name", BlobStorageAccount."Container Name", Authorization); + end; + + local procedure CheckPath(var Path: Text) + begin + if (Path <> '') and not Path.EndsWith(PathSeparator()) then + Path += PathSeparator(); + end; + + local procedure CombinePath(Path: Text; ChildPath: Text): Text + begin + if Path = '' then + exit(ChildPath); + + if not Path.EndsWith(PathSeparator()) then + Path += PathSeparator(); + + exit(Path + ChildPath); + end; + + local procedure InitOptionalParameters(Path: Text; var FilePaginationData: Codeunit "File Pagination Data"; var ABSOptionalParameters: Codeunit "ABS Optional Parameters") + begin + ABSOptionalParameters.Prefix(Path); + ABSOptionalParameters.MaxResults(500); + ABSOptionalParameters.NextMarker(FilePaginationData.GetMarker()); + end; + + local procedure ValidateListingResponse(var FilePaginationData: Codeunit "File Pagination Data"; var ABSOperationResponse: Codeunit "ABS Operation Response") + begin + if not ABSOperationResponse.IsSuccessful() then + Error(ABSOperationResponse.GetError()); + + FilePaginationData.SetMarker(ABSOperationResponse.GetNextMarker()); + FilePaginationData.SetEndOfListing(ABSOperationResponse.GetNextMarker() = ''); + end; + + local procedure SetReadySAS(var StorageServiceAuthorization: Codeunit "Storage Service Authorization"; Secret: SecretText): Interface System.Azure.Storage."Storage Service Authorization" + begin + exit(StorageServiceAuthorization.UseReadySAS(Secret)); + end; + + local procedure PathSeparator(): Text + begin + exit('/'); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Environment Cleanup", OnClearCompanyConfig, '', false, false)] + local procedure EnvironmentCleanup_OnClearCompanyConfig(CompanyName: Text; SourceEnv: Enum "Environment Type"; DestinationEnv: Enum "Environment Type") + var + ExtBlobStorageAccount: Record "Ext. Blob Storage Account"; + begin + ExtBlobStorageAccount.SetRange(Disabled, false); + if ExtBlobStorageAccount.IsEmpty() then + exit; + + ExtBlobStorageAccount.ModifyAll(Disabled, true); + end; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoContainerLookup.Page.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoContainerLookup.Page.al new file mode 100644 index 0000000000..c74726ba8e --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoContainerLookup.Page.al @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +using System.Azure.Storage; + +page 4562 "Ext. Blob Sto Container Lookup" +{ + ApplicationArea = All; + Caption = 'Container Lookup'; + Editable = false; + Extensible = false; + PageType = List; + SourceTable = "ABS Container"; + UsageCategory = None; + + layout + { + area(content) + { + repeater(General) + { + field(Name; Rec.Name) + { + ToolTip = 'Specifies the Name of the container.'; + } + } + } + } +} diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorAccountWizard.Page.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorAccountWizard.Page.al new file mode 100644 index 0000000000..d8139bebef --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorAccountWizard.Page.al @@ -0,0 +1,166 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +Using System.Environment; + +/// +/// Displays an account that is being registered via the Blob Storage connector. +/// +page 4561 "Ext. Blob Stor. Account Wizard" +{ + ApplicationArea = All; + Caption = 'Setup Azure Blob Storage Account'; + Editable = true; + Extensible = false; + PageType = NavigatePage; + Permissions = tabledata "Ext. Blob Storage Account" = rimd; + SourceTable = "Ext. Blob Storage Account"; + SourceTableTemporary = true; + + layout + { + area(Content) + { + group(TopBanner) + { + Editable = false; + ShowCaption = false; + Visible = TopBannerVisible; + field(NotDoneIcon; MediaResources."Media Reference") + { + Editable = false; + ShowCaption = false; + ToolTip = ' ', Locked = true; + } + } + + field(NameField; Rec.Name) + { + Caption = 'Account Name'; + NotBlank = true; + ShowMandatory = true; + ToolTip = 'Specifies the name of the Azure Blob Storage account.'; + + trigger OnValidate() + begin + IsNextEnabled := BlobStorageConnectorImpl.IsAccountValid(Rec); + end; + } + + field(StorageAccountNameField; Rec."Storage Account Name") + { + Caption = 'Storage Account Name'; + ShowMandatory = true; + + trigger OnValidate() + begin + IsNextEnabled := BlobStorageConnectorImpl.IsAccountValid(Rec); + end; + } + + field("Authorization Type"; Rec."Authorization Type") + { + } + + field(SecretField; Secret) + { + Caption = 'Secret'; + ExtendedDatatype = Masked; + ShowMandatory = true; + ToolTip = 'Specifies the Shared access signature Token or SharedKey.'; + } + + field(ContainerNameField; Rec."Container Name") + { + Caption = 'Container Name'; + ShowMandatory = true; + ToolTip = 'Specifies the container to use of the Storage Blob.'; + + trigger OnLookup(var Text: Text): Boolean + var + BlobStorageConnectorImpl: Codeunit "Ext. Blob Sto. Connector Impl."; + NewContainerName: Text[2048]; + begin + CurrPage.Update(); + NewContainerName := CopyStr(Text, 1, MaxStrLen(NewContainerName)); + BlobStorageConnectorImpl.LookUpContainer(Rec, Rec."Authorization Type", Secret, NewContainerName); + Text := NewContainerName; + exit(true); + end; + + trigger OnValidate() + begin + IsNextEnabled := BlobStorageConnectorImpl.IsAccountValid(Rec); + end; + } + } + } + + actions + { + area(processing) + { + action(Back) + { + Caption = 'Back'; + Image = Cancel; + InFooterBar = true; + ToolTip = 'Move to the previous step.'; + + trigger OnAction() + begin + CurrPage.Close(); + end; + } + + action(Next) + { + Caption = 'Next'; + Enabled = IsNextEnabled; + Image = NextRecord; + InFooterBar = true; + ToolTip = 'Move to the next step.'; + + trigger OnAction() + begin + BlobStorageConnectorImpl.CreateAccount(Rec, Secret, BlobStorageAccount); + CurrPage.Close(); + end; + } + } + } + + var + BlobStorageAccount: Record "File Account"; + MediaResources: Record "Media Resources"; + BlobStorageConnectorImpl: Codeunit "Ext. Blob Sto. Connector Impl."; + [NonDebuggable] + Secret: Text; + IsNextEnabled: Boolean; + TopBannerVisible: Boolean; + + trigger OnOpenPage() + var + AssistedSetupLogoTok: Label 'ASSISTEDSETUP-NOTEXT-400PX.PNG', Locked = true; + begin + Rec.Init(); + Rec.Insert(); + + if MediaResources.Get(AssistedSetupLogoTok) and (CurrentClientType() = ClientType::Web) then + TopBannerVisible := MediaResources."Media Reference".HasValue(); + end; + + internal procedure GetAccount(var FileAccount: Record "File Account"): Boolean + begin + if IsNullGuid(BlobStorageAccount."Account Id") then + exit(false); + + FileAccount := BlobStorageAccount; + + exit(true); + end; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Page.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Page.al new file mode 100644 index 0000000000..b69415a238 --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Page.al @@ -0,0 +1,80 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +/// +/// Displays an account that was registered via the Blob Storage connector. +/// +page 4560 "Ext. Blob Storage Account" +{ + ApplicationArea = All; + Caption = 'Azure Blob Storage Account'; + DataCaptionExpression = Rec.Name; + Extensible = false; + InsertAllowed = false; + PageType = Card; + Permissions = tabledata "Ext. Blob Storage Account" = rimd; + SourceTable = "Ext. Blob Storage Account"; + UsageCategory = None; + + layout + { + area(Content) + { + field(NameField; Rec.Name) + { + NotBlank = true; + ShowMandatory = true; + } + field(StorageAccountNameField; Rec."Storage Account Name") { } + field("Authorization Type"; Rec."Authorization Type") { } + field(SecretField; Secret) + { + Caption = 'Password'; + Editable = SecretEditable; + ExtendedDatatype = Masked; + ToolTip = 'Specifies the Shared access signature Token or SharedKey.'; + + trigger OnValidate() + begin + Rec.SetSecret(Secret); + end; + } + field(ContainerNameField; Rec."Container Name") + { + trigger OnLookup(var Text: Text): Boolean + var + BlobStorageConnectorImpl: Codeunit "Ext. Blob Sto. Connector Impl."; + NewContainerName: Text[2048]; + begin + CurrPage.Update(); + NewContainerName := CopyStr(Text, 1, MaxStrLen(NewContainerName)); + BlobStorageConnectorImpl.LookUpContainer(Rec, Rec."Authorization Type", Rec.GetSecret(Rec."Secret Key"), NewContainerName); + Text := NewContainerName; + exit(true); + end; + } + field(DisabledField; Rec.Disabled) { } + } + } + + var + SecretEditable: Boolean; + [NonDebuggable] + Secret: Text; + + trigger OnOpenPage() + begin + Rec.SetCurrentKey(Name); + end; + + trigger OnAfterGetCurrRecord() + begin + SecretEditable := CurrPage.Editable(); + if not IsNullGuid(Rec."Secret Key") then + Secret := '***'; + end; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Table.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Table.al new file mode 100644 index 0000000000..66a1ba23d4 --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Table.al @@ -0,0 +1,91 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +/// +/// Holds the information for all file accounts that are registered via the Blob Storage connector +/// +table 4560 "Ext. Blob Storage Account" +{ + Caption = 'Azure Blob Storage Account'; + DataClassification = CustomerContent; + + fields + { + field(1; "Id"; Guid) + { + AllowInCustomizations = Never; + Caption = 'Primary Key'; + DataClassification = SystemMetadata; + } + + field(2; Name; Text[250]) + { + Caption = 'Account Name'; + ToolTip = 'Specifies the name of the Storage account connection.'; + } + field(3; "Storage Account Name"; Text[2048]) + { + Caption = 'Storage Account Name'; + ToolTip = 'Specifies the Azure Storage name.'; + } + field(4; "Container Name"; Text[2048]) + { + Caption = 'Container Name'; + ToolTip = 'Specifies the Azure Storage Container name.'; + } + field(7; "Authorization Type"; Enum "Ext. Blob Storage Auth. Type") + { + Access = Internal; + Caption = 'Authorization Type'; + ToolTip = 'The way of authorizing used to access the Blob Storage.'; + } + field(8; "Secret Key"; Guid) + { + Access = Internal; + Caption = 'Secret Key'; + DataClassification = SystemMetadata; + } + field(9; Disabled; Boolean) + { + Caption = 'Disabled'; + ToolTip = 'Specifies if the account is disabled. This happens automatically when a sandbox is created.'; + } + } + + keys + { + key(PK; Id) + { + Clustered = true; + } + } + + var + UnableToGetSecretMsg: Label 'Unable to get Blob Storage secret.'; + UnableToSetSecretMsg: Label 'Unable to set Blob Storage secret.'; + + trigger OnDelete() + begin + if not IsNullGuid(Rec."Secret Key") then + if IsolatedStorage.Delete(Rec."Secret Key") then; + end; + + procedure SetSecret(Secret: SecretText) + begin + if IsNullGuid(Rec."Secret Key") then + Rec."Secret Key" := CreateGuid(); + + if not IsolatedStorage.Set(Format(Rec."Secret Key"), Secret, DataScope::Company) then + Error(UnableToSetSecretMsg); + end; + + procedure GetSecret(SecretKey: Guid) Secret: SecretText + begin + if not IsolatedStorage.Get(Format(SecretKey), DataScope::Company, Secret) then + Error(UnableToGetSecretMsg); + end; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAuthType.Enum.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAuthType.Enum.al new file mode 100644 index 0000000000..876a7ed6fe --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAuthType.Enum.al @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +enum 4560 "Ext. Blob Storage Auth. Type" +{ + Access = Internal; + + value(0; SasToken) + { + Caption = 'Shared Access Signature'; + } + value(1; SharedKey) + { + Caption = 'Shared Key'; + } +} diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageConnector.EnumExt.al b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageConnector.EnumExt.al new file mode 100644 index 0000000000..b93624b36c --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageConnector.EnumExt.al @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +/// +/// Enum extension to register the Blob Storage connector. +/// +enumextension 4560 "Ext. Blob Storage Connector" extends "Ext. File Storage Connector" +{ + /// + /// The Blob Storage connector. + /// + value(4560; "Blob Storage") + { + Caption = 'Blob Storage'; + Implementation = "External File Storage Connector" = "Ext. Blob Sto. Connector Impl."; + } +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/test/ExtensionLogo.png b/Apps/W1/External File Storage - Azure Blob Service Connector/test/ExtensionLogo.png new file mode 100644 index 0000000000..30941b354f Binary files /dev/null and b/Apps/W1/External File Storage - Azure Blob Service Connector/test/ExtensionLogo.png differ diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/test/README.md b/Apps/W1/External File Storage - Azure Blob Service Connector/test/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/test/app.json b/Apps/W1/External File Storage - Azure Blob Service Connector/test/app.json new file mode 100644 index 0000000000..c181f8e710 --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/test/app.json @@ -0,0 +1,55 @@ +{ + "id": "adcda309-4da8-43b8-b05d-d0287462ed42", + "name": "External File Storage - Azure Blob Service Connector Tests", + "publisher": "Microsoft", + "brief": "Tests for the External File Storage - Azure Blob Service Connector app", + "description": "Tests for the External File Storage - Azure Blob Service Connector app", + "version": "26.0.0.0", + "privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009", + "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120", + "help": "https://go.microsoft.com/fwlink/?linkid=2134520", + "url": "https://go.microsoft.com/fwlink/?linkid=724011", + "logo": "ExtensionLogo.png", + "application": "26.0.0.0", + "dependencies": [ + { + "id": "c9ce86fe-cb70-4b79-be03-d21856b1a4ca", + "name": "External File Storage - Azure Blob Service Connector", + "publisher": "Microsoft", + "version": "26.0.0.0" + }, + { + "id": "dd0be2ea-f733-4d65-bb34-a28f4624fb14", + "name": "Library Assert", + "publisher": "Microsoft", + "version": "26.0.0.0" + }, + { + "id": "e7320ebb-08b3-4406-b1ec-b4927d3e280b", + "name": "Any", + "publisher": "Microsoft", + "version": "26.0.0.0" + }, + { + "id": "9856ae4f-d1a7-46ef-89bb-6ef056398228", + "name": "System Application Test Library", + "publisher": "Microsoft", + "version": "26.0.0.0" + } + ], + "screenshots": [], + "platform": "26.0.0.0", + "idRanges": [ + { + "from": 100000, + "to": 150000 + } + ], + "target": "OnPrem", + "resourceExposurePolicy": { + "allowDebugging": true, + "allowDownloadingSource": true, + "includeSourceInSymbolFile": true + }, + "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520" +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/test/src/ExtAzureBlobServiceTest.Codeunit.al b/Apps/W1/External File Storage - Azure Blob Service Connector/test/src/ExtAzureBlobServiceTest.Codeunit.al new file mode 100644 index 0000000000..1f3f76827d --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/test/src/ExtAzureBlobServiceTest.Codeunit.al @@ -0,0 +1,150 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +codeunit 144566 "Ext. Azure Blob Service Test" +{ + Subtype = Test; + TestPermissions = Disabled; + + [Test] + [Scope('OnPrem')] + [HandlerFunctions('AccountRegisterPageHandler')] + [TransactionModel(TransactionModel::AutoRollback)] + procedure TestMultipleAccountsCanBeRegistered() + var + FileAccount: Record "File Account"; + ExtFileConnector: Codeunit "Ext. Blob Sto. Connector Impl."; + FileAccounts: TestPage "File Accounts"; + AccountIds: array[3] of Guid; + AccountName: array[3] of Text[250]; + Index: Integer; + begin + // [Scenario] Create multiple accounts + Initialize(); + + // [When] Multiple accounts are registered + for Index := 1 to 3 do begin + SetBasicAccount(); + + Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.'); + AccountIds[Index] := FileAccount."Account Id"; + AccountName[Index] := FileAccountMock.Name(); + + // [Then] Accounts are retrieved from the GetAccounts method + FileAccount.DeleteAll(); + ExtFileConnector.GetAccounts(FileAccount); + Assert.RecordCount(FileAccount, Index); + end; + + FileAccounts.OpenView(); + for Index := 1 to 3 do begin + FileAccounts.GoToKey(AccountIds[Index], Enum::"Ext. File Storage Connector"::"Blob Storage"); + Assert.AreEqual(AccountName[Index], FileAccounts.NameField.Value(), 'A different name was expected.'); + end; + end; + + + [Test] + [Scope('OnPrem')] + [HandlerFunctions('AccountRegisterPageHandler')] + [TransactionModel(TransactionModel::AutoRollback)] + procedure TestEnviromentCleanupDisablesAccounts() + var + FileAccount: Record "File Account"; + ExtSharePointAccount: Record "Ext. Blob Storage Account"; + ExtFileConnector: Codeunit "Ext. Blob Sto. Connector Impl."; + EnvironmentTriggers: Codeunit "Environment Triggers"; + AccountIds: array[3] of Guid; + Index: Integer; + begin + // [Scenario] Create multiple accounts + Initialize(); + + // [When] Multiple accounts are registered + for Index := 1 to 3 do begin + SetBasicAccount(); + + Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.'); + AccountIds[Index] := FileAccount."Account Id"; + + // [Then] Accounts are retrieved from the GetAccounts method + FileAccount.DeleteAll(); + ExtFileConnector.GetAccounts(FileAccount); + Assert.RecordCount(FileAccount, Index); + end; + + ExtSharePointAccount.SetRange(Disabled, true); + Assert.IsTrue(ExtSharePointAccount.IsEmpty(), 'Accounts are already disabled.'); + + EnvironmentTriggers.OnAfterCopyEnvironmentPerCompany(0, Any.AlphabeticText(30), 1, Any.AlphabeticText(30)); + + Assert.IsFalse(ExtSharePointAccount.IsEmpty(), 'Accounts are not disabled.'); + end; + + [Test] + [Scope('OnPrem')] + [HandlerFunctions('AccountRegisterPageHandler,AccountShowPageHandler')] + [TransactionModel(TransactionModel::AutoRollback)] + procedure TestShowAccountInformation() + var + FileAccount: Record "File Account"; + FileConnector: Codeunit "Ext. Blob Sto. Connector Impl."; + begin + // [Scenario] Account Information is displayed in the Account page. + + // [Given] An file account + Initialize(); + SetBasicAccount(); + FileConnector.RegisterAccount(FileAccount); + + // [When] The ShowAccountInformation method is invoked + FileConnector.ShowAccountInformation(FileAccount."Account Id"); + + // [Then] The account page opens and displays the information + // Verify in AccountModalPageHandler + end; + + local procedure Initialize() + var + ExtBlobStorageAccount: Record "Ext. Blob Storage Account"; + begin + ExtBlobStorageAccount.DeleteAll(); + end; + + local procedure SetBasicAccount() + begin + FileAccountMock.Name(CopyStr(Any.AlphanumericText(250), 1, 250)); + FileAccountMock.StorageAccountName(CopyStr(Any.AlphanumericText(250), 1, 250)); + FileAccountMock.ContainerName(CopyStr(Any.AlphanumericText(250), 1, 250)); + FileAccountMock.Password('testpassword'); + end; + + [ModalPageHandler] + procedure AccountRegisterPageHandler(var AccountWizard: TestPage "Ext. Blob Stor. Account Wizard") + begin + // Setup account + AccountWizard.NameField.SetValue(FileAccountMock.Name()); + AccountWizard.StorageAccountNameField.SetValue(FileAccountMock.StorageAccountName()); + AccountWizard.ContainerNameField.SetValue(FileAccountMock.ContainerName()); + AccountWizard."Authorization Type".SetValue(FileAccountMock.AuthorizationType()); + AccountWizard.SecretField.SetValue(FileAccountMock.Password()); + AccountWizard.Next.Invoke(); + end; + + [PageHandler] + procedure AccountShowPageHandler(var Account: TestPage "Ext. Blob Storage Account") + begin + // Verify the account + Assert.AreEqual(FileAccountMock.Name(), Account.NameField.Value(), 'A different name was expected.'); + Assert.AreEqual(FileAccountMock.StorageAccountName(), Account.StorageAccountNameField.Value(), 'A different storage account name was expected.'); + Assert.AreEqual(FileAccountMock.ContainerName(), Account.ContainerNameField.Value(), 'A different container name was expected.'); + Assert.AreEqual(FileAccountMock.AuthorizationType(), Account."Authorization Type".AsInteger(), 'A different authorization type was expected.'); + end; + + var + Any: Codeunit Any; + Assert: Codeunit "Library Assert"; + FileAccountMock: Codeunit "Ext. Blob Account Mock"; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure Blob Service Connector/test/src/mocks/ExtBlobAccountMock.Codeunit.al b/Apps/W1/External File Storage - Azure Blob Service Connector/test/src/mocks/ExtBlobAccountMock.Codeunit.al new file mode 100644 index 0000000000..b58825e030 --- /dev/null +++ b/Apps/W1/External File Storage - Azure Blob Service Connector/test/src/mocks/ExtBlobAccountMock.Codeunit.al @@ -0,0 +1,67 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +codeunit 144565 "Ext. Blob Account Mock" +{ + Access = Internal; + SingleInstance = true; + + procedure Name(): Text[250] + begin + exit(AccName); + end; + + procedure Name(Value: Text[250]) + begin + AccName := Value; + end; + + procedure StorageAccountName(): Text[250] + begin + exit(AccStorageAccountName); + end; + + procedure StorageAccountName(Value: Text[250]) + begin + AccStorageAccountName := Value; + end; + + procedure ContainerName(): Text[250] + begin + exit(AccContainerName); + end; + + procedure ContainerName(Value: Text[250]) + begin + AccContainerName := Value; + end; + + procedure Password(): Text + begin + exit(AccPassword); + end; + + procedure Password(Value: Text) + begin + AccPassword := Value; + end; + + procedure AuthorizationType(Value: Enum "Ext. Blob Storage Auth. Type") + begin + AccAuthorizationType := Value; + end; + + procedure AuthorizationType(): Enum "Ext. Blob Storage Auth. Type" + begin + exit(AccAuthorizationType); + end; + + var + AccName: Text[250]; + AccStorageAccountName: Text[250]; + AccContainerName: Text[250]; + AccPassword: Text; + AccAuthorizationType: Enum "Ext. Blob Storage Auth. Type"; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/Entitlements/ExtFileShareConnector.Entitlement.al b/Apps/W1/External File Storage - Azure File Service Connector/app/Entitlements/ExtFileShareConnector.Entitlement.al new file mode 100644 index 0000000000..d9d40915a7 --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/app/Entitlements/ExtFileShareConnector.Entitlement.al @@ -0,0 +1,13 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +entitlement "Ext. File Share Connector" +{ + + ObjectEntitlements = "Ext. File Share - Edit"; + Type = Implicit; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/ExtensionLogo.png b/Apps/W1/External File Storage - Azure File Service Connector/app/ExtensionLogo.png new file mode 100644 index 0000000000..30941b354f Binary files /dev/null and b/Apps/W1/External File Storage - Azure File Service Connector/app/ExtensionLogo.png differ diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/README.md b/Apps/W1/External File Storage - Azure File Service Connector/app/README.md new file mode 100644 index 0000000000..1ec1f8f8ff --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/app/README.md @@ -0,0 +1,2 @@ +# External File Storage - Azure File Share Connector +This connector allows access to Azure File Shares. diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/app.json b/Apps/W1/External File Storage - Azure File Service Connector/app/app.json new file mode 100644 index 0000000000..0817284637 --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/app/app.json @@ -0,0 +1,37 @@ +{ + "id": "79447b11-8301-4d02-a546-2261eb811296", + "name": "External File Storage - Azure File Service Connector", + "publisher": "Microsoft", + "brief": "Enables file and folder operations for Azure File Services via the External File Storage Module with Business Central.", + "description": "This app enables file and folder operations for Azure File Services via the External File Storage Module with Business Central.", + "version": "26.0.0.0", + "privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009", + "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120", + "help": "https://go.microsoft.com/fwlink/?linkid=2134520", + "url": "https://go.microsoft.com/fwlink/?linkid=724011", + "logo": "ExtensionLogo.png", + "application": "26.0.0.0", + "platform": "26.0.0.0", + "internalsVisibleTo": [ + { + "id": "80ef626f-e8de-4050-b144-0e3d4993a718", + "name": "External File Storage - Azure File Service Connector Tests", + "publisher": "Microsoft" + } + ], + "dependencies": [], + "screenshots": [], + "idRanges": [ + { + "from": 4570, + "to": 4579 + } + ], + "resourceExposurePolicy": { + "allowDebugging": true, + "allowDownloadingSource": true, + "includeSourceInSymbolFile": true + }, + "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520", + "resourceFolders": ["data"] +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/data/connector-logo.png b/Apps/W1/External File Storage - Azure File Service Connector/app/data/connector-logo.png new file mode 100644 index 0000000000..98f852e166 Binary files /dev/null and b/Apps/W1/External File Storage - Azure File Service Connector/app/data/connector-logo.png differ diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareEdit.PermissionSet.al b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareEdit.PermissionSet.al new file mode 100644 index 0000000000..85f14aed8f --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareEdit.PermissionSet.al @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +permissionset 4572 "Ext. File Share - Edit" +{ + Access = Public; + Assignable = false; + Caption = 'File Share - Edit'; + + IncludedPermissionSets = "Ext. File Share - Read"; + + Permissions = + tabledata "Ext. File Share Account" = imd; +} diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareObjects.PermissionSet.al b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareObjects.PermissionSet.al new file mode 100644 index 0000000000..405e2c3d4e --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareObjects.PermissionSet.al @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +permissionset 4570 "Ext. File Share - Objects" +{ + Access = Public; + Assignable = false; + Caption = 'File Share - Objects'; + + Permissions = + table "Ext. File Share Account" = X, + page "Ext. File Share Account Wizard" = X, + page "Ext. File Share Account" = X; +} diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareRead.PermissionSet.al b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareRead.PermissionSet.al new file mode 100644 index 0000000000..58377dff9f --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareRead.PermissionSet.al @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +permissionset 4571 "Ext. File Share - Read" +{ + Access = Public; + Assignable = false; + Caption = 'File Share - Read'; + + IncludedPermissionSets = "Ext. File Share - Objects"; + + Permissions = + tabledata "Ext. File Share Account" = r; +} diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageAdminExtFileShare.PermissionSetExt.al b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageAdminExtFileShare.PermissionSetExt.al new file mode 100644 index 0000000000..ccefc7bcda --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageAdminExtFileShare.PermissionSetExt.al @@ -0,0 +1,11 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +permissionsetextension 4570 "File Storage - Admin - Ext. File Share" extends "File Storage - Admin" +{ + IncludedPermissionSets = "Ext. File Share - Edit"; +} diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageEditExtFileShare.PermissionSetExt.al b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageEditExtFileShare.PermissionSetExt.al new file mode 100644 index 0000000000..ef4938c666 --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageEditExtFileShare.PermissionSetExt.al @@ -0,0 +1,11 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +permissionsetextension 4571 "File Storage - Edit - Ext. File Share" extends "File Storage - Edit" +{ + IncludedPermissionSets = "Ext. File Share - Read"; +} diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Page.al b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Page.al new file mode 100644 index 0000000000..d01de2a1ae --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Page.al @@ -0,0 +1,67 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +/// +/// Displays an account that was registered via the File Share connector. +/// +page 4570 "Ext. File Share Account" +{ + ApplicationArea = All; + Caption = 'Azure File Share Account'; + DataCaptionExpression = Rec.Name; + Extensible = false; + InsertAllowed = false; + PageType = Card; + Permissions = tabledata "Ext. File Share Account" = rimd; + SourceTable = "Ext. File Share Account"; + UsageCategory = None; + + layout + { + area(Content) + { + field(NameField; Rec.Name) + { + NotBlank = true; + ShowMandatory = true; + } + field(StorageAccountNameField; Rec."Storage Account Name") { } + field("Authorization Type"; Rec."Authorization Type") { } + field(SecretField; Secret) + { + Caption = 'Password'; + Editable = SecretEditable; + ExtendedDatatype = Masked; + ToolTip = 'Specifies the Shared access signature Token or SharedKey.'; + + trigger OnValidate() + begin + Rec.SetSecret(Secret); + end; + } + field(FileShareNameField; Rec."File Share Name") { } + field(DisabledField; Rec.Disabled) { } + } + } + + var + SecretEditable: Boolean; + [NonDebuggable] + Secret: Text; + + trigger OnOpenPage() + begin + Rec.SetCurrentKey(Name); + end; + + trigger OnAfterGetCurrRecord() + begin + SecretEditable := CurrPage.Editable(); + if not IsNullGuid(Rec."Secret Key") then + Secret := '***'; + end; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Table.al b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Table.al new file mode 100644 index 0000000000..8e69d42133 --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Table.al @@ -0,0 +1,89 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +/// +/// Holds the information for all file accounts that are registered via the File Share connector +/// +table 4570 "Ext. File Share Account" +{ + Caption = 'Azure File Share Account'; + DataClassification = CustomerContent; + + fields + { + field(1; "Id"; Guid) + { + AllowInCustomizations = Never; + Caption = 'Primary Key'; + DataClassification = SystemMetadata; + } + field(2; Name; Text[250]) + { + Caption = 'Account Name'; + ToolTip = 'Specifies the name of the storage account connection.'; + } + field(3; "Storage Account Name"; Text[2048]) + { + Caption = 'Storage Account Name'; + ToolTip = 'Specifies the Azure Storage name.'; + } + field(4; "File Share Name"; Text[2048]) + { + Caption = 'File Share Name'; + ToolTip = 'Specifies the Azure File Share name.'; + } + field(7; "Authorization Type"; Enum "Ext. File Share Auth. Type") + { + Access = Internal; + Caption = 'Authorization Type'; + ToolTip = 'The way of authorizing used to access the Blob Storage.'; + } + field(8; "Secret Key"; Guid) + { + Access = Internal; + DataClassification = SystemMetadata; + } + field(9; Disabled; Boolean) + { + Caption = 'Disabled'; + ToolTip = 'Specifies if the account is disabled. This happens automatically when a sandbox is created.'; + } + } + + keys + { + key(PK; Id) + { + Clustered = true; + } + } + + var + UnableToGetSecretMsg: Label 'Unable to get File Share Account secret.'; + UnableToSetSecretMsg: Label 'Unable to set File Share Account secret.'; + + trigger OnDelete() + begin + if not IsNullGuid(Rec."Secret Key") then + if IsolatedStorage.Delete(Rec."Secret Key") then; + end; + + procedure SetSecret(Secret: SecretText) + begin + if IsNullGuid(Rec."Secret Key") then + Rec."Secret Key" := CreateGuid(); + + if not IsolatedStorage.Set(Format(Rec."Secret Key"), Secret, DataScope::Company) then + Error(UnableToSetSecretMsg); + end; + + procedure GetSecret(SecretKey: Guid) Secret: SecretText + begin + if not IsolatedStorage.Get(Format(SecretKey), DataScope::Company, Secret) then + Error(UnableToGetSecretMsg); + end; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccountWizard.Page.al b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccountWizard.Page.al new file mode 100644 index 0000000000..e3fbc70f18 --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccountWizard.Page.al @@ -0,0 +1,154 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +Using System.Environment; + +/// +/// Displays an account that is being registered via the File Share connector. +/// +page 4571 "Ext. File Share Account Wizard" +{ + ApplicationArea = All; + Caption = 'Setup Azure File Share Account'; + Editable = true; + Extensible = false; + PageType = NavigatePage; + Permissions = tabledata "Ext. File Share Account" = rimd; + SourceTable = "Ext. File Share Account"; + SourceTableTemporary = true; + + layout + { + area(Content) + { + group(TopBanner) + { + Editable = false; + ShowCaption = false; + Visible = TopBannerVisible; + field(NotDoneIcon; MediaResources."Media Reference") + { + Editable = false; + ShowCaption = false; + ToolTip = ' ', Locked = true; + } + } + + field(NameField; Rec.Name) + { + Caption = 'Account Name'; + NotBlank = true; + ShowMandatory = true; + ToolTip = 'Specifies the name of the Azure File Share account.'; + + trigger OnValidate() + begin + IsNextEnabled := FileShareConnectorImpl.IsAccountValid(Rec); + end; + } + + field(StorageAccountNameField; Rec."Storage Account Name") + { + Caption = 'Storage Account Name'; + ShowMandatory = true; + + trigger OnValidate() + begin + IsNextEnabled := FileShareConnectorImpl.IsAccountValid(Rec); + end; + } + + field("Authorization Type"; Rec."Authorization Type") + { + } + + field(SecretField; Secret) + { + Caption = 'Secret'; + ExtendedDatatype = Masked; + ShowMandatory = true; + ToolTip = 'Specifies the Shared access signature Token or SharedKey.'; + } + + field(FileShareNameField; Rec."File Share Name") + { + Caption = 'File Share Name'; + ShowMandatory = true; + ToolTip = 'Specifies the file share to use of the storage account.'; + + trigger OnValidate() + begin + IsNextEnabled := FileShareConnectorImpl.IsAccountValid(Rec); + end; + } + } + } + + actions + { + area(processing) + { + action(Back) + { + Caption = 'Back'; + Image = Cancel; + InFooterBar = true; + ToolTip = 'Move to the previous step.'; + + trigger OnAction() + begin + CurrPage.Close(); + end; + } + + action(Next) + { + Caption = 'Next'; + Enabled = IsNextEnabled; + Image = NextRecord; + InFooterBar = true; + ToolTip = 'Move to the next step.'; + + trigger OnAction() + begin + FileShareConnectorImpl.CreateAccount(Rec, Secret, FileShareAccount); + CurrPage.Close(); + end; + } + } + } + + var + FileShareAccount: Record "File Account"; + MediaResources: Record "Media Resources"; + FileShareConnectorImpl: Codeunit "Ext. File Share Connector Impl"; + [NonDebuggable] + Secret: Text; + IsNextEnabled: Boolean; + TopBannerVisible: Boolean; + + trigger OnOpenPage() + var + AssistedSetupLogoTok: Label 'ASSISTEDSETUP-NOTEXT-400PX.PNG', Locked = true; + begin + Rec.Init(); + Rec.Insert(); + + if MediaResources.Get(AssistedSetupLogoTok) and (CurrentClientType() = ClientType::Web) then + TopBannerVisible := MediaResources."Media Reference".HasValue(); + end; + + internal procedure GetAccount(var FileAccount: Record "File Account"): Boolean + begin + if IsNullGuid(FileShareAccount."Account Id") then + exit(false); + + FileAccount := FileShareAccount; + + exit(true); + end; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAuthType.Enum.al b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAuthType.Enum.al new file mode 100644 index 0000000000..88b9376c58 --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAuthType.Enum.al @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +enum 4570 "Ext. File Share Auth. Type" +{ + Access = Internal; + + value(0; SasToken) + { + Caption = 'Shared Access Signature'; + } + value(1; SharedKey) + { + Caption = 'Shared Key'; + } +} diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnector.EnumExt.al b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnector.EnumExt.al new file mode 100644 index 0000000000..7930b74146 --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnector.EnumExt.al @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +/// +/// Enum extension to register the File Share connector. +/// +enumextension 4570 "Ext. File Share Connector" extends "Ext. File Storage Connector" +{ + /// + /// The File Share connector. + /// + value(4570; "File Share") + { + Caption = 'File Share'; + Implementation = "External File Storage Connector" = "Ext. File Share Connector Impl"; + } +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnectorImpl.Codeunit.al b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnectorImpl.Codeunit.al new file mode 100644 index 0000000000..3038fa649c --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnectorImpl.Codeunit.al @@ -0,0 +1,472 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +using System.Text; +using System.Azure.Storage; +using System.Azure.Storage.Files; +using System.DataAdministration; + +codeunit 4570 "Ext. File Share Connector Impl" implements "External File Storage Connector" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = tabledata "Ext. File Share Account" = rimd; + + var + ConnectorDescriptionTxt: Label 'Use Azure File Share to store and retrieve files.'; + NotRegisteredAccountErr: Label 'We could not find the account. Typically, this is because the account has been deleted.'; + NotFoundTok: Label '404', Locked = true; + + /// + /// Gets a List of Files stored on the provided account. + /// + /// The file account ID which is used to get the file. + /// The file path to list. + /// Defines the pagination data. + /// A list with all files stored in the path. + procedure ListFiles(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) + var + AFSDirectoryContent: Record "AFS Directory Content"; + begin + GetDirectoryContent(AccountId, Path, FilePaginationData, AFSDirectoryContent); + + AFSDirectoryContent.SetRange("Parent Directory", Path); + AFSDirectoryContent.SetRange("Resource Type", AFSDirectoryContent."Resource Type"::File); + if not AFSDirectoryContent.FindSet() then + exit; + + repeat + TempFileAccountContent.Init(); + TempFileAccountContent.Name := AFSDirectoryContent.Name; + TempFileAccountContent.Type := TempFileAccountContent.Type::"File"; + TempFileAccountContent."Parent Directory" := AFSDirectoryContent."Parent Directory"; + TempFileAccountContent.Insert(); + until AFSDirectoryContent.Next() = 0; + end; + + /// + /// Gets a file from the provided account. + /// + /// The file account ID which is used to get the file. + /// The file path inside the file account. + /// The Stream were the file is read to. + procedure GetFile(AccountId: Guid; Path: Text; Stream: InStream) + var + AFSFileClient: Codeunit "AFS File Client"; + AFSOperationResponse: Codeunit "AFS Operation Response"; + begin + InitFileClient(AccountId, AFSFileClient); + AFSOperationResponse := AFSFileClient.GetFileAsStream(Path, Stream); + + if AFSOperationResponse.IsSuccessful() then + exit; + + Error(AFSOperationResponse.GetError()); + end; + + /// + /// Create a file in the provided account. + /// + /// The file account ID which is used to send out the file. + /// The file path inside the file account. + /// The Stream were the file is read from. + procedure CreateFile(AccountId: Guid; Path: Text; Stream: InStream) + var + AFSFileClient: Codeunit "AFS File Client"; + AFSOperationResponse: Codeunit "AFS Operation Response"; + begin + InitFileClient(AccountId, AFSFileClient); + + AFSOperationResponse := AFSFileClient.CreateFile(Path, Stream); + if not AFSOperationResponse.IsSuccessful() then + Error(AFSOperationResponse.GetError()); + + AFSOperationResponse := AFSFileClient.PutFileStream(Path, Stream); + if not AFSOperationResponse.IsSuccessful() then + Error(AFSOperationResponse.GetError()); + end; + + /// + /// Copies as file inside the provided account. + /// + /// The file account ID which is used to send out the file. + /// The source file path. + /// The target file path. + procedure CopyFile(AccountId: Guid; SourcePath: Text; TargetPath: Text) + var + AFSFileClient: Codeunit "AFS File Client"; + AFSOperationResponse: Codeunit "AFS Operation Response"; + begin + InitFileClient(AccountId, AFSFileClient); + AFSOperationResponse := AFSFileClient.CopyFile(TargetPath, SourcePath); + + if AFSOperationResponse.IsSuccessful() then + exit; + + Error(AFSOperationResponse.GetError()); + end; + + /// + /// Move as file inside the provided account. + /// + /// The file account ID which is used to send out the file. + /// The source file path. + /// The target file path. + procedure MoveFile(AccountId: Guid; SourcePath: Text; TargetPath: Text) + var + AFSFileClient: Codeunit "AFS File Client"; + AFSOperationResponse: Codeunit "AFS Operation Response"; + begin + InitFileClient(AccountId, AFSFileClient); + AFSOperationResponse := AFSFileClient.RenameFile(TargetPath, SourcePath); + if not AFSOperationResponse.IsSuccessful() then + Error(AFSOperationResponse.GetError()); + end; + + /// + /// Checks if a file exists on the provided account. + /// + /// The file account ID which is used to send out the file. + /// The file path inside the file account. + /// Returns true if the file exists + procedure FileExists(AccountId: Guid; Path: Text): Boolean + var + AFSFileClient: Codeunit "AFS File Client"; + AFSOperationResponse: Codeunit "AFS Operation Response"; + AFSOptionalParameters: Codeunit "AFS Optional Parameters"; + TargetText: Text; + begin + if Path = '' then + exit(false); + + InitFileClient(AccountId, AFSFileClient); + AFSOptionalParameters.Range(0, 1); + + AFSOperationResponse := AFSFileClient.GetFileAsText(Path, TargetText, AFSOptionalParameters); + if AFSOperationResponse.IsSuccessful() then + exit(true); + + if AFSOperationResponse.GetError().Contains(NotFoundTok) then + exit(false); + + Error(AFSOperationResponse.GetError()); + end; + + /// + /// Deletes a file exists on the provided account. + /// + /// The file account ID which is used to send out the file. + /// The file path inside the file account. + procedure DeleteFile(AccountId: Guid; Path: Text) + var + AFSFileClient: Codeunit "AFS File Client"; + AFSOperationResponse: Codeunit "AFS Operation Response"; + begin + InitFileClient(AccountId, AFSFileClient); + AFSOperationResponse := AFSFileClient.DeleteFile(Path); + + if AFSOperationResponse.IsSuccessful() then + exit; + + Error(AFSOperationResponse.GetError()); + end; + + /// + /// Gets a List of Directories stored on the provided account. + /// + /// The file account ID which is used to get the file. + /// The file path to list. + /// Defines the pagination data. + /// A list with all directories stored in the path. + procedure ListDirectories(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) + var + AFSDirectoryContent: Record "AFS Directory Content"; + begin + GetDirectoryContent(AccountId, Path, FilePaginationData, AFSDirectoryContent); + + AFSDirectoryContent.SetRange("Parent Directory", Path); + AFSDirectoryContent.SetRange("Resource Type", AFSDirectoryContent."Resource Type"::Directory); + if not AFSDirectoryContent.FindSet() then + exit; + + repeat + TempFileAccountContent.Init(); + TempFileAccountContent.Name := AFSDirectoryContent.Name; + TempFileAccountContent.Type := TempFileAccountContent.Type::Directory; + TempFileAccountContent."Parent Directory" := AFSDirectoryContent."Parent Directory"; + TempFileAccountContent.Insert(); + until AFSDirectoryContent.Next() = 0; + end; + + /// + /// Creates a directory on the provided account. + /// + /// The file account ID which is used to send out the file. + /// The directory path inside the file account. + procedure CreateDirectory(AccountId: Guid; Path: Text) + var + AFSFileClient: Codeunit "AFS File Client"; + AFSOperationResponse: Codeunit "AFS Operation Response"; + DirectoryAlreadyExistsErr: Label 'Directory already exists.'; + begin + if DirectoryExists(AccountId, Path) then + Error(DirectoryAlreadyExistsErr); + + InitFileClient(AccountId, AFSFileClient); + AFSOperationResponse := AFSFileClient.CreateDirectory(Path); + if not AFSOperationResponse.IsSuccessful() then + Error(AFSOperationResponse.GetError()); + end; + + /// + /// Checks if a directory exists on the provided account. + /// + /// The file account ID which is used to send out the file. + /// The directory path inside the file account. + /// Returns true if the directory exists + procedure DirectoryExists(AccountId: Guid; Path: Text): Boolean + var + AFSDirectoryContent: Record "AFS Directory Content"; + AFSFileClient: Codeunit "AFS File Client"; + AFSOperationResponse: Codeunit "AFS Operation Response"; + AFSOptionalParameters: Codeunit "AFS Optional Parameters"; + begin + if Path = '' then + exit(true); + + InitFileClient(AccountId, AFSFileClient); + AFSOptionalParameters.MaxResults(1); + AFSOperationResponse := AFSFileClient.ListDirectory(CopyStr(Path, 1, 2048), AFSDirectoryContent, AFSOptionalParameters); + if AFSOperationResponse.IsSuccessful() then + exit(not AFSDirectoryContent.IsEmpty()); + + if AFSOperationResponse.GetError().Contains(NotFoundTok) then + exit(false) + else + Error(AFSOperationResponse.GetError()); + + end; + + /// + /// Deletes a directory exists on the provided account. + /// + /// The file account ID which is used to send out the file. + /// The directory path inside the file account. + procedure DeleteDirectory(AccountId: Guid; Path: Text) + var + AFSFileClient: Codeunit "AFS File Client"; + AFSOperationResponse: Codeunit "AFS Operation Response"; + begin + InitFileClient(AccountId, AFSFileClient); + AFSOperationResponse := AFSFileClient.DeleteDirectory(Path); + + if AFSOperationResponse.IsSuccessful() then + exit; + + Error(AFSOperationResponse.GetError()); + end; + + /// + /// Gets the registered accounts for the File Share connector. + /// + /// Out parameter holding all the registered accounts for the File Share connector. + procedure GetAccounts(var TempAccounts: Record "File Account" temporary) + var + Account: Record "Ext. File Share Account"; + begin + if not Account.FindSet() then + exit; + + repeat + TempAccounts."Account Id" := Account.Id; + TempAccounts.Name := Account.Name; + TempAccounts.Connector := Enum::"Ext. File Storage Connector"::"File Share"; + TempAccounts.Insert(); + until Account.Next() = 0; + end; + + /// + /// Shows accounts information. + /// + /// The ID of the account to show. + procedure ShowAccountInformation(AccountId: Guid) + var + FileShareAccountLocal: Record "Ext. File Share Account"; + begin + if not FileShareAccountLocal.Get(AccountId) then + Error(NotRegisteredAccountErr); + + FileShareAccountLocal.SetRecFilter(); + Page.Run(Page::"Ext. File Share Account", FileShareAccountLocal); + end; + + /// + /// Register an file account for the File Share connector. + /// + /// Out parameter holding details of the registered account. + /// True if the registration was successful; false - otherwise. + procedure RegisterAccount(var TempAccount: Record "File Account" temporary): Boolean + var + FileShareAccountWizard: Page "Ext. File Share Account Wizard"; + begin + FileShareAccountWizard.RunModal(); + + exit(FileShareAccountWizard.GetAccount(TempAccount)); + end; + + /// + /// Deletes an file account for the File Share connector. + /// + /// The ID of the File Share account + /// True if an account was deleted. + procedure DeleteAccount(AccountId: Guid): Boolean + var + FileShareAccountLocal: Record "Ext. File Share Account"; + begin + if FileShareAccountLocal.Get(AccountId) then + exit(FileShareAccountLocal.Delete()); + + exit(false); + end; + + /// + /// Gets a description of the File Share connector. + /// + /// A short description of the File Share connector. + procedure GetDescription(): Text[250] + begin + exit(ConnectorDescriptionTxt); + end; + + /// + /// Gets the File Share connector logo. + /// + /// A base64-formatted image to be used as logo. + procedure GetLogoAsBase64(): Text + var + Base64Convert: Codeunit "Base64 Convert"; + Stream: InStream; + begin + NavApp.GetResource('connector-logo.png', Stream); + exit(Base64Convert.ToBase64(Stream)); + end; + + internal procedure IsAccountValid(var Account: Record "Ext. File Share Account" temporary): Boolean + begin + if Account.Name = '' then + exit(false); + + if Account."Storage Account Name" = '' then + exit(false); + + if Account."File Share Name" = '' then + exit(false); + + exit(true); + end; + + internal procedure CreateAccount(var AccountToCopy: Record "Ext. File Share Account"; Password: SecretText; var TempFileAccount: Record "File Account" temporary) + var + NewFileShareAccount: Record "Ext. File Share Account"; + begin + NewFileShareAccount.TransferFields(AccountToCopy); + + NewFileShareAccount.Id := CreateGuid(); + NewFileShareAccount.SetSecret(Password); + + NewFileShareAccount.Insert(); + + TempFileAccount."Account Id" := NewFileShareAccount.Id; + TempFileAccount.Name := NewFileShareAccount.Name; + TempFileAccount.Connector := Enum::"Ext. File Storage Connector"::"File Share"; + end; + + local procedure InitFileClient(var AccountId: Guid; var AFSFileClient: Codeunit "AFS File Client") + var + FileShareAccount: Record "Ext. File Share Account"; + StorageServiceAuthorization: Codeunit "Storage Service Authorization"; + Authorization: Interface "Storage Service Authorization"; + AccountDisabledErr: Label 'The account "%1" is disabled.', Comment = '%1 - Account Name'; + begin + FileShareAccount.Get(AccountId); + if FileShareAccount.Disabled then + Error(AccountDisabledErr, FileShareAccount.Name); + + case FileShareAccount."Authorization Type" of + FileShareAccount."Authorization Type"::SasToken: + Authorization := SetReadySAS(StorageServiceAuthorization, FileShareAccount.GetSecret(FileShareAccount."Secret Key")); + FileShareAccount."Authorization Type"::SharedKey: + Authorization := StorageServiceAuthorization.CreateSharedKey(FileShareAccount.GetSecret(FileShareAccount."Secret Key")); + end; + + AFSFileClient.Initialize(FileShareAccount."Storage Account Name", FileShareAccount."File Share Name", Authorization); + end; + + local procedure CheckPath(var Path: Text) + var + PathToLongErr: Label 'The path is too long. The maximum length is 2048 characters.'; + begin + if (Path <> '') and not Path.EndsWith(PathSeparator()) then + Path += PathSeparator(); + + if StrLen(Path) > 2048 then + Error(PathToLongErr); + end; + + local procedure InitOptionalParameters(var FilePaginationData: Codeunit "File Pagination Data"; var AFSOptionalParameters: Codeunit "AFS Optional Parameters") + begin + AFSOptionalParameters.MaxResults(500); + AFSOptionalParameters.Marker(FilePaginationData.GetMarker()); + end; + + local procedure ValidateListingResponse(var FilePaginationData: Codeunit "File Pagination Data"; var AFSOperationResponse: Codeunit "AFS Operation Response") + begin + if not AFSOperationResponse.IsSuccessful() then + Error(AFSOperationResponse.GetError()); + + FilePaginationData.SetEndOfListing(true); + end; + + local procedure GetDirectoryContent(var AccountId: Guid; var PassedPath: Text; var FilePaginationData: Codeunit "File Pagination Data"; var AFSDirectoryContent: Record "AFS Directory Content") + var + AFSFileClient: Codeunit "AFS File Client"; + AFSOperationResponse: Codeunit "AFS Operation Response"; + AFSOptionalParameters: Codeunit "AFS Optional Parameters"; + Path: Text[2048]; + begin + InitFileClient(AccountId, AFSFileClient); + CheckPath(PassedPath); + InitOptionalParameters(FilePaginationData, AFSOptionalParameters); + Path := CopyStr(PassedPath, 1, MaxStrLen(Path)); + AFSOperationResponse := AFSFileClient.ListDirectory(Path, AFSDirectoryContent, AFSOptionalParameters); + PassedPath := Path; + ValidateListingResponse(FilePaginationData, AFSOperationResponse); + end; + + local procedure SetReadySAS(var StorageServiceAuthorization: Codeunit "Storage Service Authorization"; Secret: SecretText): Interface System.Azure.Storage."Storage Service Authorization" + begin + exit(StorageServiceAuthorization.UseReadySAS(Secret)); + end; + + local procedure PathSeparator(): Text + begin + exit('/'); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Environment Cleanup", OnClearCompanyConfig, '', false, false)] + local procedure EnvironmentCleanup_OnClearCompanyConfig(CompanyName: Text; SourceEnv: Enum "Environment Type"; DestinationEnv: Enum "Environment Type") + var + ExtFileShareAccount: Record "Ext. File Share Account"; + begin + ExtFileShareAccount.SetRange(Disabled, false); + if ExtFileShareAccount.IsEmpty() then + exit; + + ExtFileShareAccount.ModifyAll(Disabled, true); + end; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure File Service Connector/test/ExtensionLogo.png b/Apps/W1/External File Storage - Azure File Service Connector/test/ExtensionLogo.png new file mode 100644 index 0000000000..30941b354f Binary files /dev/null and b/Apps/W1/External File Storage - Azure File Service Connector/test/ExtensionLogo.png differ diff --git a/Apps/W1/External File Storage - Azure File Service Connector/test/README.md b/Apps/W1/External File Storage - Azure File Service Connector/test/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Apps/W1/External File Storage - Azure File Service Connector/test/app.json b/Apps/W1/External File Storage - Azure File Service Connector/test/app.json new file mode 100644 index 0000000000..a6eadeb709 --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/test/app.json @@ -0,0 +1,55 @@ +{ + "id": "80ef626f-e8de-4050-b144-0e3d4993a718", + "name": "External File Storage - Azure File Service Connector Tests", + "publisher": "Microsoft", + "brief": "Tests for the External File Storage - Azure File Service Connector app", + "description": "Tests for the External File Storage - Azure File Service Connector app", + "version": "26.0.0.0", + "privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009", + "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120", + "help": "https://go.microsoft.com/fwlink/?linkid=2134520", + "url": "https://go.microsoft.com/fwlink/?linkid=724011", + "logo": "ExtensionLogo.png", + "application": "26.0.0.0", + "dependencies": [ + { + "id": "79447b11-8301-4d02-a546-2261eb811296", + "name": "External File Storage - Azure File Service Connector", + "publisher": "Microsoft", + "version": "26.0.0.0" + }, + { + "id": "dd0be2ea-f733-4d65-bb34-a28f4624fb14", + "name": "Library Assert", + "publisher": "Microsoft", + "version": "26.0.0.0" + }, + { + "id": "e7320ebb-08b3-4406-b1ec-b4927d3e280b", + "name": "Any", + "publisher": "Microsoft", + "version": "26.0.0.0" + }, + { + "id": "9856ae4f-d1a7-46ef-89bb-6ef056398228", + "name": "System Application Test Library", + "publisher": "Microsoft", + "version": "26.0.0.0" + } + ], + "screenshots": [], + "platform": "26.0.0.0", + "idRanges": [ + { + "from": 100000, + "to": 150000 + } + ], + "target": "OnPrem", + "resourceExposurePolicy": { + "allowDebugging": true, + "allowDownloadingSource": true, + "includeSourceInSymbolFile": true + }, + "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520" +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure File Service Connector/test/src/ExtAzureFileServiceTest.Codeunit.al b/Apps/W1/External File Storage - Azure File Service Connector/test/src/ExtAzureFileServiceTest.Codeunit.al new file mode 100644 index 0000000000..33f42c9f47 --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/test/src/ExtAzureFileServiceTest.Codeunit.al @@ -0,0 +1,150 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +codeunit 144571 "Ext. Azure File Service Test" +{ + Subtype = Test; + TestPermissions = Disabled; + + [Test] + [Scope('OnPrem')] + [HandlerFunctions('AccountRegisterPageHandler')] + [TransactionModel(TransactionModel::AutoRollback)] + procedure TestMultipleAccountsCanBeRegistered() + var + FileAccount: Record "File Account"; + ExtFileConnector: Codeunit "Ext. File Share Connector Impl"; + FileAccounts: TestPage "File Accounts"; + AccountIds: array[3] of Guid; + AccountName: array[3] of Text[250]; + Index: Integer; + begin + // [Scenario] Create multiple accounts + Initialize(); + + // [When] Multiple accounts are registered + for Index := 1 to 3 do begin + SetBasicAccount(); + + Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.'); + AccountIds[Index] := FileAccount."Account Id"; + AccountName[Index] := FileAccountMock.Name(); + + // [Then] Accounts are retrieved from the GetAccounts method + FileAccount.DeleteAll(); + ExtFileConnector.GetAccounts(FileAccount); + Assert.RecordCount(FileAccount, Index); + end; + + FileAccounts.OpenView(); + for Index := 1 to 3 do begin + FileAccounts.GoToKey(AccountIds[Index], Enum::"Ext. File Storage Connector"::"File Share"); + Assert.AreEqual(AccountName[Index], FileAccounts.NameField.Value(), 'A different name was expected.'); + end; + end; + + + [Test] + [Scope('OnPrem')] + [HandlerFunctions('AccountRegisterPageHandler')] + [TransactionModel(TransactionModel::AutoRollback)] + procedure TestEnviromentCleanupDisablesAccounts() + var + FileAccount: Record "File Account"; + ExtSharePointAccount: Record "Ext. File Share Account"; + ExtFileConnector: Codeunit "Ext. File Share Connector Impl"; + EnvironmentTriggers: Codeunit "Environment Triggers"; + AccountIds: array[3] of Guid; + Index: Integer; + begin + // [Scenario] Create multiple accounts + Initialize(); + + // [When] Multiple accounts are registered + for Index := 1 to 3 do begin + SetBasicAccount(); + + Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.'); + AccountIds[Index] := FileAccount."Account Id"; + + // [Then] Accounts are retrieved from the GetAccounts method + FileAccount.DeleteAll(); + ExtFileConnector.GetAccounts(FileAccount); + Assert.RecordCount(FileAccount, Index); + end; + + ExtSharePointAccount.SetRange(Disabled, true); + Assert.IsTrue(ExtSharePointAccount.IsEmpty(), 'Accounts are already disabled.'); + + EnvironmentTriggers.OnAfterCopyEnvironmentPerCompany(0, Any.AlphabeticText(30), 1, Any.AlphabeticText(30)); + + Assert.IsFalse(ExtSharePointAccount.IsEmpty(), 'Accounts are not disabled.'); + end; + + [Test] + [Scope('OnPrem')] + [HandlerFunctions('AccountRegisterPageHandler,AccountShowPageHandler')] + [TransactionModel(TransactionModel::AutoRollback)] + procedure TestShowAccountInformation() + var + FileAccount: Record "File Account"; + FileConnector: Codeunit "Ext. File Share Connector Impl"; + begin + // [Scenario] Account Information is displayed in the Account page. + + // [Given] An file account + Initialize(); + SetBasicAccount(); + FileConnector.RegisterAccount(FileAccount); + + // [When] The ShowAccountInformation method is invoked + FileConnector.ShowAccountInformation(FileAccount."Account Id"); + + // [Then] The account page opens and displays the information + // Verify in AccountModalPageHandler + end; + + local procedure Initialize() + var + ExtFileShareAccount: Record "Ext. File Share Account"; + begin + ExtFileShareAccount.DeleteAll(); + end; + + local procedure SetBasicAccount() + begin + FileAccountMock.Name(CopyStr(Any.AlphanumericText(250), 1, 250)); + FileAccountMock.StorageAccountName(CopyStr(Any.AlphanumericText(250), 1, 250)); + FileAccountMock.FileShareName(CopyStr(Any.AlphanumericText(250), 1, 250)); + FileAccountMock.Password('testpassword'); + end; + + [ModalPageHandler] + procedure AccountRegisterPageHandler(var AccountWizard: TestPage "Ext. File Share Account Wizard") + begin + // Setup account + AccountWizard.NameField.SetValue(FileAccountMock.Name()); + AccountWizard.StorageAccountNameField.SetValue(FileAccountMock.StorageAccountName()); + AccountWizard.FileShareNameField.SetValue(FileAccountMock.FileShareName()); + AccountWizard."Authorization Type".SetValue(FileAccountMock.AuthorizationType()); + AccountWizard.SecretField.SetValue(FileAccountMock.Password()); + AccountWizard.Next.Invoke(); + end; + + [PageHandler] + procedure AccountShowPageHandler(var Account: TestPage "Ext. File Share Account") + begin + // Verify the account + Assert.AreEqual(FileAccountMock.Name(), Account.NameField.Value(), 'A different name was expected.'); + Assert.AreEqual(FileAccountMock.StorageAccountName(), Account.StorageAccountNameField.Value(), 'A different storage account name was expected.'); + Assert.AreEqual(FileAccountMock.FileShareName(), Account.FileShareNameField.Value(), 'A different file share name was expected.'); + Assert.AreEqual(FileAccountMock.AuthorizationType(), Account."Authorization Type".AsInteger(), 'A different authorization type was expected.'); + end; + + var + Any: Codeunit Any; + Assert: Codeunit "Library Assert"; + FileAccountMock: Codeunit "Ext. File Account Mock"; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - Azure File Service Connector/test/src/mocks/ExtFileAccountMock.Codeunit.al b/Apps/W1/External File Storage - Azure File Service Connector/test/src/mocks/ExtFileAccountMock.Codeunit.al new file mode 100644 index 0000000000..f397be5cd0 --- /dev/null +++ b/Apps/W1/External File Storage - Azure File Service Connector/test/src/mocks/ExtFileAccountMock.Codeunit.al @@ -0,0 +1,68 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +codeunit 144570 "Ext. File Account Mock" +{ + Access = Internal; + SingleInstance = true; + + procedure Name(): Text[250] + begin + exit(AccName); + end; + + procedure Name(Value: Text[250]) + begin + AccName := Value; + end; + + procedure StorageAccountName(): Text[250] + begin + exit(AccStorageAccountName); + end; + + procedure StorageAccountName(Value: Text[250]) + begin + AccStorageAccountName := Value; + end; + + + procedure FileShareName(): Text[250] + begin + exit(AccFileShareName); + end; + + procedure FileShareName(Value: Text[250]) + begin + AccFileShareName := Value; + end; + + procedure Password(): Text + begin + exit(AccPassword); + end; + + procedure Password(Value: Text) + begin + AccPassword := Value; + end; + + procedure AuthorizationType(Value: Enum "Ext. File Share Auth. Type") + begin + AccAuthorizationType := Value; + end; + + procedure AuthorizationType(): Enum "Ext. File Share Auth. Type" + begin + exit(AccAuthorizationType); + end; + + var + AccName: Text[250]; + AccStorageAccountName: Text[250]; + AccFileShareName: Text[250]; + AccPassword: Text; + AccAuthorizationType: Enum "Ext. File Share Auth. Type"; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/Entitlements/ExtSharePointConnector.Entitlement.al b/Apps/W1/External File Storage - SharePoint Connector/app/Entitlements/ExtSharePointConnector.Entitlement.al new file mode 100644 index 0000000000..b7efbd1973 --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/app/Entitlements/ExtSharePointConnector.Entitlement.al @@ -0,0 +1,13 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +entitlement "Ext. SharePoint Connector" +{ + + ObjectEntitlements = "Ext. SharePoint - Edit"; + Type = Implicit; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/ExtensionLogo.png b/Apps/W1/External File Storage - SharePoint Connector/app/ExtensionLogo.png new file mode 100644 index 0000000000..30941b354f Binary files /dev/null and b/Apps/W1/External File Storage - SharePoint Connector/app/ExtensionLogo.png differ diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/README.md b/Apps/W1/External File Storage - SharePoint Connector/app/README.md new file mode 100644 index 0000000000..121e78fdc0 --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/app/README.md @@ -0,0 +1,3 @@ +# External File Storage - SharePoint Connector +This connector allows access to Share Point Files and Folder. +A proper App Registration with Sites.ReadWrite.All permission is needed. \ No newline at end of file diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/app.json b/Apps/W1/External File Storage - SharePoint Connector/app/app.json new file mode 100644 index 0000000000..696c2d7a7f --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/app/app.json @@ -0,0 +1,37 @@ +{ + "id": "34bfcef7-f8ed-449f-94be-74024cadba3b", + "name": "External File Storage - SharePoint Connector", + "publisher": "Microsoft", + "brief": "Enables file and folder operations for SharePoint folders and files via the External File Storage Module with Business Central.", + "description": "This app enables file and folder operations for SharePoint folders and files via the External File Storage Module with Business Central.", + "version": "26.0.0.0", + "privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009", + "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120", + "help": "https://go.microsoft.com/fwlink/?linkid=2134520", + "url": "https://go.microsoft.com/fwlink/?linkid=724011", + "logo": "ExtensionLogo.png", + "application": "26.0.0.0", + "platform": "26.0.0.0", + "internalsVisibleTo": [ + { + "id": "b072f3f0-db0e-4331-b30d-4c0ebbcde681", + "name": "External File Storage - SharePoint Connector Tests", + "publisher": "Microsoft" + } + ], + "dependencies": [], + "screenshots": [], + "idRanges": [ + { + "from": 4580, + "to": 4589 + } + ], + "resourceExposurePolicy": { + "allowDebugging": true, + "allowDownloadingSource": true, + "includeSourceInSymbolFile": true + }, + "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520", + "resourceFolders": ["data"] +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/data/connector-logo.png b/Apps/W1/External File Storage - SharePoint Connector/app/data/connector-logo.png new file mode 100644 index 0000000000..3c4d410a37 Binary files /dev/null and b/Apps/W1/External File Storage - SharePoint Connector/app/data/connector-logo.png differ diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointEdit.PermissionSet.al b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointEdit.PermissionSet.al new file mode 100644 index 0000000000..233b0008bb --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointEdit.PermissionSet.al @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +permissionset 4582 "Ext. SharePoint - Edit" +{ + Access = Public; + Assignable = false; + Caption = 'SharePoint - Edit'; + + IncludedPermissionSets = "Ext. SharePoint - Read"; + + Permissions = + tabledata "Ext. SharePoint Account" = imd; +} diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointObjects.PermissionSet.al b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointObjects.PermissionSet.al new file mode 100644 index 0000000000..b961d80baf --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointObjects.PermissionSet.al @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +permissionset 4580 "Ext. SharePoint - Objects" +{ + Access = Public; + Assignable = false; + Caption = 'SharePoint - Objects'; + + Permissions = + table "Ext. SharePoint Account" = X, + page "Ext. SharePoint Account Wizard" = X, + page "Ext. SharePoint Account" = X; +} diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointRead.PermissionSet.al b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointRead.PermissionSet.al new file mode 100644 index 0000000000..95cd2cfe6e --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointRead.PermissionSet.al @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +permissionset 4581 "Ext. SharePoint - Read" +{ + Access = Public; + Assignable = false; + Caption = 'SharePoint - Read'; + + IncludedPermissionSets = "Ext. SharePoint - Objects"; + + Permissions = + tabledata "Ext. SharePoint Account" = r; +} diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageAdminExtSharePoint.PermissionSetExt.al b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageAdminExtSharePoint.PermissionSetExt.al new file mode 100644 index 0000000000..695880b292 --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageAdminExtSharePoint.PermissionSetExt.al @@ -0,0 +1,11 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +permissionsetextension 4580 "File Storage - Admin - Ext. SharePoint" extends "File Storage - Admin" +{ + IncludedPermissionSets = "Ext. SharePoint - Edit"; +} diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageEditExtSharePoint.PermissionSetExt.al b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageEditExtSharePoint.PermissionSetExt.al new file mode 100644 index 0000000000..1600e2563f --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageEditExtSharePoint.PermissionSetExt.al @@ -0,0 +1,11 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +permissionsetextension 4581 "File Storage - Edit - Ext. SharePoint" extends "File Storage - Edit" +{ + IncludedPermissionSets = "Ext. SharePoint - Read"; +} diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Page.al b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Page.al new file mode 100644 index 0000000000..2bf9f69569 --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Page.al @@ -0,0 +1,68 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +/// +/// Displays an account that was registered via the SharePoint connector. +/// +page 4580 "Ext. SharePoint Account" +{ + ApplicationArea = All; + Caption = 'SharePoint Account'; + DataCaptionExpression = Rec.Name; + Extensible = false; + InsertAllowed = false; + PageType = Card; + Permissions = tabledata "Ext. SharePoint Account" = rimd; + SourceTable = "Ext. SharePoint Account"; + UsageCategory = None; + + layout + { + area(Content) + { + field(NameField; Rec.Name) + { + NotBlank = true; + ShowMandatory = true; + } + field("Tenant Id"; Rec."Tenant Id") { } + field("Client Id"; Rec."Client Id") { } + field(SecretField; ClientSecret) + { + Caption = 'Password'; + Editable = ClientSecretEditable; + ExtendedDatatype = Masked; + ToolTip = 'Specifies the the Client Secret of the App Registration.'; + + trigger OnValidate() + begin + Rec.SetClientSecret(ClientSecret); + end; + } + field("SharePoint Url"; Rec."SharePoint Url") { } + field("Base Relative Folder Path"; Rec."Base Relative Folder Path") { } + field(Disabled; Rec.Disabled) { } + } + } + + var + ClientSecretEditable: Boolean; + [NonDebuggable] + ClientSecret: Text; + + trigger OnOpenPage() + begin + Rec.SetCurrentKey(Name); + end; + + trigger OnAfterGetCurrRecord() + begin + ClientSecretEditable := CurrPage.Editable(); + if not IsNullGuid(Rec."Client Secret Key") then + ClientSecret := '***'; + end; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Table.al b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Table.al new file mode 100644 index 0000000000..a175e1c7dc --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Table.al @@ -0,0 +1,95 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +/// +/// Holds the information for all file accounts that are registered via the SharePoint connector +/// +table 4580 "Ext. SharePoint Account" +{ + Caption = 'SharePoint Account'; + DataClassification = CustomerContent; + + fields + { + field(1; "Id"; Guid) + { + AllowInCustomizations = Never; + Caption = 'Primary Key'; + DataClassification = SystemMetadata; + } + field(2; Name; Text[250]) + { + Caption = 'Account Name'; + ToolTip = 'Specifies the name of the storage account connection.'; + } + field(4; "SharePoint Url"; Text[2048]) + { + Caption = 'SharePoint Url'; + ToolTip = 'Specifies the the url to your SharePoint site.'; + } + field(5; "Base Relative Folder Path"; Text[2048]) + { + Caption = 'Base Relative Folder Path'; + ToolTip = 'Specifies the folder path relative to the site collection. Start with the document library or folder name (e.g., Shared Documents/Reports). This path can be copied from the URL of the folder in SharePoint after the site collection (e.g., /Shared Documents/Reports from https://mysharepoint.sharepoint.com/sites/ProjectX/Shared%20Documents/Reports).'; + } + field(6; "Tenant Id"; Guid) + { + Access = Internal; + Caption = 'Tenant Id'; + ToolTip = 'Specifies the Tenant Id of the App Registration.'; + } + field(7; "Client Id"; Guid) + { + Access = Internal; + Caption = 'Client Id'; + ToolTip = 'Specifies the the Client Id of the App Registration.'; + } + field(8; "Client Secret Key"; Guid) + { + Access = Internal; + DataClassification = SystemMetadata; + } + field(9; Disabled; Boolean) + { + Caption = 'Disabled'; + ToolTip = 'Specifies if the account is disabled. This happens automatically when a sandbox is created.'; + } + } + + keys + { + key(PK; Id) + { + Clustered = true; + } + } + + var + UnableToGetClientMsg: Label 'Unable to get SharePoint Account Client Secret.'; + UnableToSetClientSecretMsg: Label 'Unable to set SharePoint Client Secret.'; + + trigger OnDelete() + begin + if not IsNullGuid(Rec."Client Secret Key") then + if IsolatedStorage.Delete(Rec."Client Secret Key") then; + end; + + procedure SetClientSecret(ClientSecret: SecretText) + begin + if IsNullGuid(Rec."Client Secret Key") then + Rec."Client Secret Key" := CreateGuid(); + + if not IsolatedStorage.Set(Format(Rec."Client Secret Key"), ClientSecret, DataScope::Company) then + Error(UnableToSetClientSecretMsg); + end; + + procedure GetClientSecret(ClientSecretKey: Guid) ClientSecret: SecretText + begin + if not IsolatedStorage.Get(Format(ClientSecretKey), DataScope::Company, ClientSecret) then + Error(UnableToGetClientMsg); + end; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccountWizard.Page.al b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccountWizard.Page.al new file mode 100644 index 0000000000..1757e06e6c --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccountWizard.Page.al @@ -0,0 +1,169 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +Using System.Environment; + +/// +/// Displays an account that is being registered via the SharePoint connector. +/// +page 4581 "Ext. SharePoint Account Wizard" +{ + ApplicationArea = All; + Caption = 'Setup SharePoint Account'; + Editable = true; + Extensible = false; + PageType = NavigatePage; + Permissions = tabledata "Ext. SharePoint Account" = rimd; + SourceTable = "Ext. SharePoint Account"; + SourceTableTemporary = true; + + layout + { + area(Content) + { + group(TopBanner) + { + Editable = false; + ShowCaption = false; + Visible = TopBannerVisible; + field(NotDoneIcon; MediaResources."Media Reference") + { + Editable = false; + ShowCaption = false; + ToolTip = ' ', Locked = true; + } + } + + field(NameField; Rec.Name) + { + Caption = 'Account Name'; + NotBlank = true; + ShowMandatory = true; + ToolTip = 'Specifies the name of the Azure SharePoint account.'; + + trigger OnValidate() + begin + IsNextEnabled := SharePointConnectorImpl.IsAccountValid(Rec); + end; + } + + field("Tenant Id"; Rec."Tenant Id") + { + ShowMandatory = true; + + trigger OnValidate() + begin + IsNextEnabled := SharePointConnectorImpl.IsAccountValid(Rec); + end; + } + + field("Client Id"; Rec."Client Id") + { + ShowMandatory = true; + + trigger OnValidate() + begin + IsNextEnabled := SharePointConnectorImpl.IsAccountValid(Rec); + end; + } + + field(ClientSecretField; ClientSecret) + { + Caption = 'Client Secret'; + ExtendedDatatype = Masked; + ShowMandatory = true; + ToolTip = 'Specifies the Client Secret of the App Registration.'; + } + + field("SharePoint Url"; Rec."SharePoint Url") + { + Caption = 'SharePoint Name'; + ShowMandatory = true; + ToolTip = 'Specifies the SharePoint to use of the storage account.'; + + trigger OnValidate() + begin + IsNextEnabled := SharePointConnectorImpl.IsAccountValid(Rec); + end; + } + + field("Base Relative Folder Path"; Rec."Base Relative Folder Path") + { + ShowMandatory = true; + + trigger OnValidate() + begin + IsNextEnabled := SharePointConnectorImpl.IsAccountValid(Rec); + end; + } + } + } + + actions + { + area(processing) + { + action(Back) + { + Caption = 'Back'; + Image = Cancel; + InFooterBar = true; + ToolTip = 'Move to previous step.'; + + trigger OnAction() + begin + CurrPage.Close(); + end; + } + + action(Next) + { + Caption = 'Next'; + Enabled = IsNextEnabled; + Image = NextRecord; + InFooterBar = true; + ToolTip = 'Move to next step.'; + + trigger OnAction() + begin + SharePointConnectorImpl.CreateAccount(Rec, ClientSecret, SharePointAccount); + CurrPage.Close(); + end; + } + } + } + + var + SharePointAccount: Record "File Account"; + MediaResources: Record "Media Resources"; + SharePointConnectorImpl: Codeunit "Ext. SharePoint Connector Impl"; + [NonDebuggable] + ClientSecret: Text; + IsNextEnabled: Boolean; + TopBannerVisible: Boolean; + + trigger OnOpenPage() + var + AssistedSetupLogoTok: Label 'ASSISTEDSETUP-NOTEXT-400PX.PNG', Locked = true; + begin + Rec.Init(); + Rec.Insert(); + + if MediaResources.Get(AssistedSetupLogoTok) and (CurrentClientType() = ClientType::Web) then + TopBannerVisible := MediaResources."Media Reference".HasValue(); + end; + + internal procedure GetAccount(var FileAccount: Record "File Account"): Boolean + begin + if IsNullGuid(SharePointAccount."Account Id") then + exit(false); + + FileAccount := SharePointAccount; + + exit(true); + end; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnector.EnumExt.al b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnector.EnumExt.al new file mode 100644 index 0000000000..cafd9b5652 --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnector.EnumExt.al @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +/// +/// Enum extension to register the SharePoint connector. +/// +enumextension 4580 "Ext. SharePoint Connector" extends "Ext. File Storage Connector" +{ + /// + /// The SharePoint connector. + /// + value(4580; "SharePoint") + { + Caption = 'SharePoint'; + Implementation = "External File Storage Connector" = "Ext. SharePoint Connector Impl"; + } +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnectorImpl.Codeunit.al b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnectorImpl.Codeunit.al new file mode 100644 index 0000000000..aece559dad --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnectorImpl.Codeunit.al @@ -0,0 +1,458 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +namespace System.ExternalFileStorage; + +using System.Text; +using System.Integration.Sharepoint; +using System.Utilities; +using System.DataAdministration; + +codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage Connector" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = tabledata "Ext. SharePoint Account" = rimd; + + var + ConnectorDescriptionTxt: Label 'Use SharePoint to store and retrieve files.', MaxLength = 250; + NotRegisteredAccountErr: Label 'We could not find the account. Typically, this is because the account has been deleted.'; + + /// + /// Gets a List of Files stored on the provided account. + /// + /// The file account ID which is used to get the file. + /// The file path to list. + /// Defines the pagination data. + /// A list with all files stored in the path. + procedure ListFiles(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) + var + SharePointFile: Record "SharePoint File"; + SharePointClient: Codeunit "SharePoint Client"; + OrginalPath: Text; + begin + OrginalPath := Path; + InitPath(AccountId, Path); + InitSharePointClient(AccountId, SharePointClient); + if not SharePointClient.GetFolderFilesByServerRelativeUrl(Path, SharePointFile) then + ShowError(SharePointClient); + + FilePaginationData.SetEndOfListing(true); + + if not SharePointFile.FindSet() then + exit; + + repeat + TempFileAccountContent.Init(); + TempFileAccountContent.Name := SharePointFile.Name; + TempFileAccountContent.Type := TempFileAccountContent.Type::"File"; + TempFileAccountContent."Parent Directory" := CopyStr(OrginalPath, 1, MaxStrLen(TempFileAccountContent."Parent Directory")); + TempFileAccountContent.Insert(); + until SharePointFile.Next() = 0; + end; + + /// + /// Gets a file from the provided account. + /// + /// The file account ID which is used to get the file. + /// The file path inside the file account. + /// The Stream were the file is read to. + procedure GetFile(AccountId: Guid; Path: Text; Stream: InStream) + var + SharePointClient: Codeunit "SharePoint Client"; + Content: HttpContent; + TempBlobStream: InStream; + begin + InitPath(AccountId, Path); + InitSharePointClient(AccountId, SharePointClient); + + if not SharePointClient.DownloadFileContentByServerRelativeUrl(Path, TempBlobStream) then + ShowError(SharePointClient); + + // Platform fix: For some reason the Stream from DownloadFileContentByServerRelativeUrl dies after leaving the interface + Content.WriteFrom(TempBlobStream); + Content.ReadAs(Stream); + end; + + /// + /// Create a file in the provided account. + /// + /// The file account ID which is used to send out the file. + /// The file path inside the file account. + /// The Stream were the file is read from. + procedure CreateFile(AccountId: Guid; Path: Text; Stream: InStream) + var + SharePointFile: Record "SharePoint File"; + SharePointClient: Codeunit "SharePoint Client"; + ParentPath, FileName : Text; + begin + InitPath(AccountId, Path); + InitSharePointClient(AccountId, SharePointClient); + SplitPath(Path, ParentPath, FileName); + if SharePointClient.AddFileToFolder(ParentPath, FileName, Stream, SharePointFile, false) then + exit; + + ShowError(SharePointClient); + end; + + /// + /// Copies as file inside the provided account. + /// + /// The file account ID which is used to send out the file. + /// The source file path. + /// The target file path. + procedure CopyFile(AccountId: Guid; SourcePath: Text; TargetPath: Text) + var + TempBlob: Codeunit "Temp Blob"; + Stream: InStream; + begin + TempBlob.CreateInStream(Stream); + + GetFile(AccountId, SourcePath, Stream); + CreateFile(AccountId, TargetPath, Stream); + end; + + /// + /// Move as file inside the provided account. + /// + /// The file account ID which is used to send out the file. + /// The source file path. + /// The target file path. + procedure MoveFile(AccountId: Guid; SourcePath: Text; TargetPath: Text) + var + Stream: InStream; + begin + GetFile(AccountId, SourcePath, Stream); + CreateFile(AccountId, TargetPath, Stream); + DeleteFile(AccountId, SourcePath); + end; + + /// + /// Checks if a file exists on the provided account. + /// + /// The file account ID which is used to send out the file. + /// The file path inside the file account. + /// Returns true if the file exists + procedure FileExists(AccountId: Guid; Path: Text): Boolean + var + SharePointFile: Record "SharePoint File"; + SharePointClient: Codeunit "SharePoint Client"; + begin + InitPath(AccountId, Path); + InitSharePointClient(AccountId, SharePointClient); + if not SharePointClient.GetFolderFilesByServerRelativeUrl(GetParentPath(Path), SharePointFile) then + ShowError(SharePointClient); + + SharePointFile.SetRange(Name, GetFileName(Path)); + exit(not SharePointFile.IsEmpty()); + end; + + /// + /// Deletes a file exists on the provided account. + /// + /// The file account ID which is used to send out the file. + /// The file path inside the file account. + procedure DeleteFile(AccountId: Guid; Path: Text) + var + SharePointClient: Codeunit "SharePoint Client"; + begin + InitPath(AccountId, Path); + InitSharePointClient(AccountId, SharePointClient); + if SharePointClient.DeleteFileByServerRelativeUrl(Path) then + exit; + + ShowError(SharePointClient); + end; + + /// + /// Gets a List of Directories stored on the provided account. + /// + /// The file account ID which is used to get the file. + /// The file path to list. + /// Defines the pagination data. + /// A list with all directories stored in the path. + procedure ListDirectories(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) + var + SharePointFolder: Record "SharePoint Folder"; + SharePointClient: Codeunit "SharePoint Client"; + OrginalPath: Text; + begin + OrginalPath := Path; + InitPath(AccountId, Path); + InitSharePointClient(AccountId, SharePointClient); + if not SharePointClient.GetSubFoldersByServerRelativeUrl(Path, SharePointFolder) then + ShowError(SharePointClient); + + FilePaginationData.SetEndOfListing(true); + + if not SharePointFolder.FindSet() then + exit; + + repeat + TempFileAccountContent.Init(); + TempFileAccountContent.Name := SharePointFolder.Name; + TempFileAccountContent.Type := TempFileAccountContent.Type::Directory; + TempFileAccountContent."Parent Directory" := CopyStr(OrginalPath, 1, MaxStrLen(TempFileAccountContent."Parent Directory")); + TempFileAccountContent.Insert(); + until SharePointFolder.Next() = 0; + end; + + /// + /// Creates a directory on the provided account. + /// + /// The file account ID which is used to send out the file. + /// The directory path inside the file account. + procedure CreateDirectory(AccountId: Guid; Path: Text) + var + SharePointFolder: Record "SharePoint Folder"; + SharePointClient: Codeunit "SharePoint Client"; + begin + InitPath(AccountId, Path); + InitSharePointClient(AccountId, SharePointClient); + if SharePointClient.CreateFolder(Path, SharePointFolder) then + exit; + + ShowError(SharePointClient); + end; + + /// + /// Checks if a directory exists on the provided account. + /// + /// The file account ID which is used to send out the file. + /// The directory path inside the file account. + /// Returns true if the directory exists + procedure DirectoryExists(AccountId: Guid; Path: Text): Boolean + var + SharePointFolder: Record "SharePoint Folder"; + SharePointClient: Codeunit "SharePoint Client"; + begin + InitPath(AccountId, Path); + InitSharePointClient(AccountId, SharePointClient); + if SharePointClient.GetSubFoldersByServerRelativeUrl(Path, SharePointFolder) then + exit; + + ShowError(SharePointClient); + end; + + /// + /// Deletes a directory exists on the provided account. + /// + /// The file account ID which is used to send out the file. + /// The directory path inside the file account. + procedure DeleteDirectory(AccountId: Guid; Path: Text) + var + SharePointClient: Codeunit "SharePoint Client"; + begin + InitPath(AccountId, Path); + InitSharePointClient(AccountId, SharePointClient); + if SharePointClient.DeleteFolderByServerRelativeUrl(Path) then + exit; + + ShowError(SharePointClient); + end; + + /// + /// Gets the registered accounts for the SharePoint connector. + /// + /// Out parameter holding all the registered accounts for the SharePoint connector. + procedure GetAccounts(var TempAccounts: Record "File Account" temporary) + var + Account: Record "Ext. SharePoint Account"; + begin + if not Account.FindSet() then + exit; + + repeat + TempAccounts."Account Id" := Account.Id; + TempAccounts.Name := Account.Name; + TempAccounts.Connector := Enum::"Ext. File Storage Connector"::"SharePoint"; + TempAccounts.Insert(); + until Account.Next() = 0; + end; + + /// + /// Shows accounts information. + /// + /// The ID of the account to show. + procedure ShowAccountInformation(AccountId: Guid) + var + SharePointAccountLocal: Record "Ext. SharePoint Account"; + begin + if not SharePointAccountLocal.Get(AccountId) then + Error(NotRegisteredAccountErr); + + SharePointAccountLocal.SetRecFilter(); + Page.Run(Page::"Ext. SharePoint Account", SharePointAccountLocal); + end; + + /// + /// Register an file account for the SharePoint connector. + /// + /// Out parameter holding details of the registered account. + /// True if the registration was successful; false - otherwise. + procedure RegisterAccount(var TempAccount: Record "File Account" temporary): Boolean + var + SharePointAccountWizard: Page "Ext. SharePoint Account Wizard"; + begin + SharePointAccountWizard.RunModal(); + + exit(SharePointAccountWizard.GetAccount(TempAccount)); + end; + + /// + /// Deletes an file account for the SharePoint connector. + /// + /// The ID of the SharePoint account + /// True if an account was deleted. + procedure DeleteAccount(AccountId: Guid): Boolean + var + SharePointAccountLocal: Record "Ext. SharePoint Account"; + begin + if SharePointAccountLocal.Get(AccountId) then + exit(SharePointAccountLocal.Delete()); + + exit(false); + end; + + /// + /// Gets a description of the SharePoint connector. + /// + /// A short description of the SharePoint connector. + procedure GetDescription(): Text[250] + begin + exit(ConnectorDescriptionTxt); + end; + + /// + /// Gets the SharePoint connector logo. + /// + /// A base64-formatted image to be used as logo. + procedure GetLogoAsBase64(): Text + var + Base64Convert: Codeunit "Base64 Convert"; + Stream: InStream; + begin + NavApp.GetResource('connector-logo.png', Stream); + exit(Base64Convert.ToBase64(Stream)); + end; + + internal procedure IsAccountValid(var TempAccount: Record "Ext. SharePoint Account" temporary): Boolean + begin + if TempAccount.Name = '' then + exit(false); + + if IsNullGuid(TempAccount."Client Id") then + exit(false); + + if IsNullGuid(TempAccount."Tenant Id") then + exit(false); + + if TempAccount."SharePoint Url" = '' then + exit(false); + + if TempAccount."Base Relative Folder Path" = '' then + exit(false); + + exit(true); + end; + + internal procedure CreateAccount(var AccountToCopy: Record "Ext. SharePoint Account"; Password: SecretText; var TempFileAccount: Record "File Account" temporary) + var + NewExtSharePointAccount: Record "Ext. SharePoint Account"; + begin + NewExtSharePointAccount.TransferFields(AccountToCopy); + + NewExtSharePointAccount.Id := CreateGuid(); + NewExtSharePointAccount.SetClientSecret(Password); + + NewExtSharePointAccount.Insert(); + + TempFileAccount."Account Id" := NewExtSharePointAccount.Id; + TempFileAccount.Name := NewExtSharePointAccount.Name; + TempFileAccount.Connector := Enum::"Ext. File Storage Connector"::"SharePoint"; + end; + + local procedure InitSharePointClient(var AccountId: Guid; var SharePointClient: Codeunit "SharePoint Client") + var + SharePointAccount: Record "Ext. SharePoint Account"; + SharePointAuth: Codeunit "SharePoint Auth."; + SharePointAuthorization: Interface "SharePoint Authorization"; + Scopes: List of [Text]; + AccountDisabledErr: Label 'The account "%1" is disabled.', Comment = '%1 - Account Name'; + begin + SharePointAccount.Get(AccountId); + if SharePointAccount.Disabled then + Error(AccountDisabledErr, SharePointAccount.Name); + + Scopes.Add('00000003-0000-0ff1-ce00-000000000000/.default'); + SharePointAuthorization := SharePointAuth.CreateAuthorizationCode(Format(SharePointAccount."Tenant Id", 0, 4), Format(SharePointAccount."Client Id", 0, 4), SharePointAccount.GetClientSecret(SharePointAccount."Client Secret Key"), Scopes); + SharePointClient.Initialize(SharePointAccount."SharePoint Url", SharePointAuthorization); + end; + + local procedure PathSeparator(): Text + begin + exit('/'); + end; + + local procedure ShowError(var SharePointClient: Codeunit "SharePoint Client") + var + ErrorOccuredErr: Label 'An error occured.\%1', Comment = '%1 - Error message from sharepoint'; + begin + Error(ErrorOccuredErr, SharePointClient.GetDiagnostics().GetErrorMessage()); + end; + + local procedure GetParentPath(Path: Text) ParentPath: Text + begin + if (Path.TrimEnd(PathSeparator()).Contains(PathSeparator())) then + ParentPath := Path.TrimEnd(PathSeparator()).Substring(1, Path.LastIndexOf(PathSeparator())); + end; + + local procedure GetFileName(Path: Text) FileName: Text + begin + if (Path.TrimEnd(PathSeparator()).Contains(PathSeparator())) then + FileName := Path.TrimEnd(PathSeparator()).Substring(Path.LastIndexOf(PathSeparator()) + 1); + end; + + local procedure InitPath(AccountId: Guid; var Path: Text) + var + SharePointAccount: Record "Ext. SharePoint Account"; + begin + SharePointAccount.Get(AccountId); + Path := CombinePath(SharePointAccount."Base Relative Folder Path", Path); + end; + + local procedure CombinePath(Parent: Text; Child: Text): Text + begin + if Parent = '' then + exit(Child); + + if Child = '' then + exit(Parent); + + if not Parent.EndsWith(PathSeparator()) then + Parent += PathSeparator(); + + exit(Parent + Child); + end; + + local procedure SplitPath(Path: Text; var ParentPath: Text; var FileName: Text) + begin + ParentPath := Path.TrimEnd(PathSeparator()).Substring(1, Path.LastIndexOf(PathSeparator())); + FileName := Path.TrimEnd(PathSeparator()).Substring(Path.LastIndexOf(PathSeparator()) + 1); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Environment Cleanup", OnClearCompanyConfig, '', false, false)] + local procedure EnvironmentCleanup_OnClearCompanyConfig(CompanyName: Text; SourceEnv: Enum "Environment Type"; DestinationEnv: Enum "Environment Type") + var + ExtSharePointAccount: Record "Ext. SharePoint Account"; + begin + ExtSharePointAccount.SetRange(Disabled, false); + if ExtSharePointAccount.IsEmpty() then + exit; + + ExtSharePointAccount.ModifyAll(Disabled, true); + end; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - SharePoint Connector/test/ExtensionLogo.png b/Apps/W1/External File Storage - SharePoint Connector/test/ExtensionLogo.png new file mode 100644 index 0000000000..30941b354f Binary files /dev/null and b/Apps/W1/External File Storage - SharePoint Connector/test/ExtensionLogo.png differ diff --git a/Apps/W1/External File Storage - SharePoint Connector/test/README.md b/Apps/W1/External File Storage - SharePoint Connector/test/README.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Apps/W1/External File Storage - SharePoint Connector/test/app.json b/Apps/W1/External File Storage - SharePoint Connector/test/app.json new file mode 100644 index 0000000000..be07b1676a --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/test/app.json @@ -0,0 +1,55 @@ +{ + "id": "b072f3f0-db0e-4331-b30d-4c0ebbcde681", + "name": "External File Storage - SharePoint Connector Tests", + "publisher": "Microsoft", + "brief": "Tests for the External File Storage - SharePoint Connector app", + "description": "Tests for the External File Storage - SharePoint Connector app", + "version": "26.0.0.0", + "privacyStatement": "https://go.microsoft.com/fwlink/?linkid=724009", + "EULA": "https://go.microsoft.com/fwlink/?linkid=2009120", + "help": "https://go.microsoft.com/fwlink/?linkid=2134520", + "url": "https://go.microsoft.com/fwlink/?linkid=724011", + "logo": "ExtensionLogo.png", + "application": "26.0.0.0", + "dependencies": [ + { + "id": "34bfcef7-f8ed-449f-94be-74024cadba3b", + "name": "External File Storage - SharePoint Connector", + "publisher": "Microsoft", + "version": "26.0.0.0" + }, + { + "id": "dd0be2ea-f733-4d65-bb34-a28f4624fb14", + "name": "Library Assert", + "publisher": "Microsoft", + "version": "26.0.0.0" + }, + { + "id": "e7320ebb-08b3-4406-b1ec-b4927d3e280b", + "name": "Any", + "publisher": "Microsoft", + "version": "26.0.0.0" + }, + { + "id": "9856ae4f-d1a7-46ef-89bb-6ef056398228", + "name": "System Application Test Library", + "publisher": "Microsoft", + "version": "26.0.0.0" + } + ], + "screenshots": [], + "platform": "26.0.0.0", + "idRanges": [ + { + "from": 100000, + "to": 150000 + } + ], + "target": "OnPrem", + "resourceExposurePolicy": { + "allowDebugging": true, + "allowDownloadingSource": true, + "includeSourceInSymbolFile": true + }, + "contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2134520" +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - SharePoint Connector/test/src/ExtSharePointConnectorTest.Codeunit.al b/Apps/W1/External File Storage - SharePoint Connector/test/src/ExtSharePointConnectorTest.Codeunit.al new file mode 100644 index 0000000000..f9b97f721a --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/test/src/ExtSharePointConnectorTest.Codeunit.al @@ -0,0 +1,153 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +codeunit 144581 "Ext. SharePoint Connector Test" +{ + Subtype = Test; + TestPermissions = Disabled; + + [Test] + [Scope('OnPrem')] + [HandlerFunctions('AccountRegisterPageHandler')] + [TransactionModel(TransactionModel::AutoRollback)] + procedure TestMultipleAccountsCanBeRegistered() + var + FileAccount: Record "File Account"; + ExtFileConnector: Codeunit "Ext. SharePoint Connector Impl"; + FileAccounts: TestPage "File Accounts"; + AccountIds: array[3] of Guid; + AccountName: array[3] of Text[250]; + Index: Integer; + begin + // [Scenario] Create multiple accounts + Initialize(); + + // [When] Multiple accounts are registered + for Index := 1 to 3 do begin + SetBasicAccount(); + + Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.'); + AccountIds[Index] := FileAccount."Account Id"; + AccountName[Index] := FileAccountMock.Name(); + + // [Then] Accounts are retrieved from the GetAccounts method + FileAccount.DeleteAll(); + ExtFileConnector.GetAccounts(FileAccount); + Assert.RecordCount(FileAccount, Index); + end; + + FileAccounts.OpenView(); + for Index := 1 to 3 do begin + FileAccounts.GoToKey(AccountIds[Index], Enum::"Ext. File Storage Connector"::SharePoint); + Assert.AreEqual(AccountName[Index], FileAccounts.NameField.Value(), 'A different name was expected.'); + end; + end; + + [Test] + [Scope('OnPrem')] + [HandlerFunctions('AccountRegisterPageHandler')] + [TransactionModel(TransactionModel::AutoRollback)] + procedure TestEnviromentCleanupDisablesAccounts() + var + FileAccount: Record "File Account"; + ExtSharePointAccount: Record "Ext. SharePoint Account"; + ExtFileConnector: Codeunit "Ext. SharePoint Connector Impl"; + EnvironmentTriggers: Codeunit "Environment Triggers"; + AccountIds: array[3] of Guid; + Index: Integer; + begin + // [Scenario] Create multiple accounts + Initialize(); + + // [When] Multiple accounts are registered + for Index := 1 to 3 do begin + SetBasicAccount(); + + Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.'); + AccountIds[Index] := FileAccount."Account Id"; + + // [Then] Accounts are retrieved from the GetAccounts method + FileAccount.DeleteAll(); + ExtFileConnector.GetAccounts(FileAccount); + Assert.RecordCount(FileAccount, Index); + end; + + ExtSharePointAccount.SetRange(Disabled, true); + Assert.IsTrue(ExtSharePointAccount.IsEmpty(), 'Accounts are already disabled.'); + + EnvironmentTriggers.OnAfterCopyEnvironmentPerCompany(0, Any.AlphabeticText(30), 1, Any.AlphabeticText(30)); + + Assert.IsFalse(ExtSharePointAccount.IsEmpty(), 'Accounts are not disabled.'); + end; + + [Test] + [Scope('OnPrem')] + [HandlerFunctions('AccountRegisterPageHandler,AccountShowPageHandler')] + [TransactionModel(TransactionModel::AutoRollback)] + procedure TestShowAccountInformation() + var + FileAccount: Record "File Account"; + FileConnector: Codeunit "Ext. SharePoint Connector Impl"; + begin + // [Scenario] Account Information is displayed in the Account page. + + // [Given] An file account + Initialize(); + SetBasicAccount(); + FileConnector.RegisterAccount(FileAccount); + + // [When] The ShowAccountInformation method is invoked + FileConnector.ShowAccountInformation(FileAccount."Account Id"); + + // [Then] The account page opens and displays the information + // Verify in AccountModalPageHandler + end; + + local procedure Initialize() + var + ExtSharePointAccount: Record "Ext. SharePoint Account"; + begin + ExtSharePointAccount.DeleteAll(); + end; + + local procedure SetBasicAccount() + begin + FileAccountMock.Name(CopyStr(Any.AlphanumericText(250), 1, 250)); + FileAccountMock.SharePointUrl(CopyStr(Any.AlphanumericText(250), 1, 250)); + FileAccountMock.BaseRelativeFolderPath(CopyStr(Any.AlphanumericText(250), 1, 250)); + FileAccountMock.TenantId(CreateGuid()); + FileAccountMock.ClientId(CreateGuid()); + FileAccountMock.Password('testpassword'); + end; + + [ModalPageHandler] + procedure AccountRegisterPageHandler(var AccountWizard: TestPage "Ext. SharePoint Account Wizard") + begin + // Setup account + AccountWizard.NameField.SetValue(FileAccountMock.Name()); + AccountWizard."SharePoint Url".SetValue(FileAccountMock.SharePointUrl()); + AccountWizard."Base Relative Folder Path".SetValue(FileAccountMock.BaseRelativeFolderPath()); + AccountWizard."Tenant Id".SetValue(FileAccountMock.TenantId()); + AccountWizard."Client Id".SetValue(FileAccountMock.ClientId()); + AccountWizard.ClientSecretField.SetValue(FileAccountMock.Password()); + AccountWizard.Next.Invoke(); + end; + + [PageHandler] + procedure AccountShowPageHandler(var Account: TestPage "Ext. SharePoint Account") + begin + // Verify the account + Assert.AreEqual(FileAccountMock.Name(), Account.NameField.Value(), 'A different name was expected.'); + Assert.AreEqual(FileAccountMock.SharePointUrl(), Account."SharePoint Url".Value(), 'A different sharepoint url was expected.'); + Assert.AreEqual(FileAccountMock.BaseRelativeFolderPath(), Account."Base Relative Folder Path".Value(), 'A different base relative folder path was expected.'); + Assert.AreEqual(Format(FileAccountMock.TenantId()).ToLower(), Format(Account."Tenant Id").ToLower(), 'A different tenant id was expected.'); + Assert.AreEqual(Format(FileAccountMock.ClientId()).ToLower(), Format(Account."Client Id").ToLower(), 'A different client id was expected.'); + end; + + var + Any: Codeunit Any; + Assert: Codeunit "Library Assert"; + FileAccountMock: Codeunit "Ext. SharePoint Account Mock"; +} \ No newline at end of file diff --git a/Apps/W1/External File Storage - SharePoint Connector/test/src/mocks/ExtSharePointAccountMock.Codeunit.al b/Apps/W1/External File Storage - SharePoint Connector/test/src/mocks/ExtSharePointAccountMock.Codeunit.al new file mode 100644 index 0000000000..714cdb073d --- /dev/null +++ b/Apps/W1/External File Storage - SharePoint Connector/test/src/mocks/ExtSharePointAccountMock.Codeunit.al @@ -0,0 +1,79 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ + +codeunit 144580 "Ext. SharePoint Account Mock" +{ + Access = Internal; + SingleInstance = true; + + procedure Name(): Text[250] + begin + exit(AccName); + end; + + procedure Name(Value: Text[250]) + begin + AccName := Value; + end; + + procedure SharePointUrl(): Text[250] + begin + exit(AccSharePointUrl); + end; + + procedure SharePointUrl(Value: Text[250]) + begin + AccSharePointUrl := Value; + end; + + + procedure BaseRelativeFolderPath(): Text[250] + begin + exit(AccBaseRelativeFolderPath); + end; + + procedure BaseRelativeFolderPath(Value: Text[250]) + begin + AccBaseRelativeFolderPath := Value; + end; + + procedure Password(): Text + begin + exit(AccPassword); + end; + + procedure Password(Value: Text) + begin + AccPassword := Value; + end; + + procedure ClientId(): Guid + begin + exit(AccClientId); + end; + + procedure ClientId(Value: Guid) + begin + AccClientId := Value; + end; + + procedure TenantId(): Guid + begin + exit(AccTenantId); + end; + + procedure TenantId(Value: Guid) + begin + AccTenantId := Value; + end; + + var + AccName: Text[250]; + AccSharePointUrl: Text[250]; + AccBaseRelativeFolderPath: Text[250]; + AccPassword: Text; + AccTenantId: Guid; + AccClientId: Guid; +} \ No newline at end of file diff --git a/Build/projects/1st Party Apps Tests (W1)/.AL-Go/settings.json b/Build/projects/1st Party Apps Tests (W1)/.AL-Go/settings.json index 67ee6bfadc..372f66451a 100644 --- a/Build/projects/1st Party Apps Tests (W1)/.AL-Go/settings.json +++ b/Build/projects/1st Party Apps Tests (W1)/.AL-Go/settings.json @@ -58,7 +58,10 @@ "..\\..\\..\\Apps\\W1\\PowerBIReports\\test-library", "..\\..\\..\\Apps\\W1\\EDocumentConnectors\\Avalara\\test", "..\\..\\..\\Apps\\W1\\EDocumentConnectors\\Logiq\\test", - "..\\..\\..\\Apps\\W1\\EDocumentConnectors\\Microsoft365\\test" + "..\\..\\..\\Apps\\W1\\EDocumentConnectors\\Microsoft365\\test", + "..\\..\\..\\Apps\\W1\\External File Storage - Azure Blob Service Connector\\test", + "..\\..\\..\\Apps\\W1\\External File Storage - Azure File Service Connector\\test", + "..\\..\\..\\Apps\\W1\\External File Storage - SharePoint Connector\\test" ], "doNotRunTests": false } \ No newline at end of file