From 77cfb3966b47c7c53f39278b8f70f8bc0f0a38dc Mon Sep 17 00:00:00 2001 From: IceOnly Date: Wed, 29 Jan 2025 14:35:28 +0100 Subject: [PATCH] Azure Blob Storage, Azure File Share and SharePoint Online Connector Apps (#23225) This PR contains three new connector apps, to connect Azure Blob Storage, Azure File Share and SharePoint Online with the New File System Module in the System App. File System Module PR: https://github.com/microsoft/BCApps/pull/663 Here are some Screenshots: ![image](https://user-images.githubusercontent.com/3911556/236433843-11ee0b26-ee9c-4da2-8bc6-efb32675090c.png) ![image](https://user-images.githubusercontent.com/3911556/236433887-bf2bf2f2-f68e-4efa-9db5-074a6f68fad0.png) ![image](https://user-images.githubusercontent.com/3911556/236433950-e8117496-16f6-4b25-a575-42ba729ca6c3.png) ![image](https://user-images.githubusercontent.com/3911556/236434182-97fd304a-95ac-4180-9d52-9ead78899822.png) ![image](https://user-images.githubusercontent.com/3911556/236435173-a01a77bc-04de-4063-b0bb-bf18e28b2817.png) ![image](https://user-images.githubusercontent.com/3911556/236435258-56d68d50-7581-4527-bcdd-5b6e3ed53ac8.png) Fixes #22691 Fixes [AB#559148](https://dynamicssmb2.visualstudio.com/1fcb79e7-ab07-432a-a3c6-6cf5a88ba4a5/_workitems/edit/559148) --- .../ExtBlobStorageConnector.Entitlement.al | 13 + .../app/ExtensionLogo.png | Bin 0 -> 4681 bytes .../app/README.md | 2 + .../app/app.json | 37 ++ .../app/data/connector-logo.png | Bin 0 -> 2140 bytes .../ExtBlobStorEdit.PermissionSet.al | 18 + .../ExtBlobStorObjects.PermissionSet.al | 19 + .../ExtBlobStorRead.PermissionSet.al | 18 + ...ageAdminExtBlobStorage.PermissionSetExt.al | 11 + ...rageEditExtBlobStorage.PermissionSetExt.al | 11 + .../src/ExtBlobStoConnectorImpl.Codeunit.al | 508 ++++++++++++++++++ .../app/src/ExtBlobStoContainerLookup.Page.al | 33 ++ .../app/src/ExtBlobStorAccountWizard.Page.al | 166 ++++++ .../app/src/ExtBlobStorageAccount.Page.al | 80 +++ .../app/src/ExtBlobStorageAccount.Table.al | 91 ++++ .../app/src/ExtBlobStorageAuthType.Enum.al | 20 + .../src/ExtBlobStorageConnector.EnumExt.al | 21 + .../test/ExtensionLogo.png | Bin 0 -> 4681 bytes .../test/README.md | 0 .../test/app.json | 55 ++ .../src/ExtAzureBlobServiceTest.Codeunit.al | 150 ++++++ .../src/mocks/ExtBlobAccountMock.Codeunit.al | 67 +++ .../ExtFileShareConnector.Entitlement.al | 13 + .../app/ExtensionLogo.png | Bin 0 -> 4681 bytes .../app/README.md | 2 + .../app/app.json | 37 ++ .../app/data/connector-logo.png | Bin 0 -> 4430 bytes .../ExtFileShareEdit.PermissionSet.al | 18 + .../ExtFileShareObjects.PermissionSet.al | 18 + .../ExtFileShareRead.PermissionSet.al | 18 + ...orageAdminExtFileShare.PermissionSetExt.al | 11 + ...torageEditExtFileShare.PermissionSetExt.al | 11 + .../app/src/ExtFileShareAccount.Page.al | 67 +++ .../app/src/ExtFileShareAccount.Table.al | 89 +++ .../app/src/ExtFileShareAccountWizard.Page.al | 154 ++++++ .../app/src/ExtFileShareAuthType.Enum.al | 20 + .../app/src/ExtFileShareConnector.EnumExt.al | 21 + .../src/ExtFileShareConnectorImpl.Codeunit.al | 472 ++++++++++++++++ .../test/ExtensionLogo.png | Bin 0 -> 4681 bytes .../test/README.md | 0 .../test/app.json | 55 ++ .../src/ExtAzureFileServiceTest.Codeunit.al | 150 ++++++ .../src/mocks/ExtFileAccountMock.Codeunit.al | 68 +++ .../ExtSharePointConnector.Entitlement.al | 13 + .../app/ExtensionLogo.png | Bin 0 -> 4681 bytes .../app/README.md | 3 + .../app/app.json | 37 ++ .../app/data/connector-logo.png | Bin 0 -> 13403 bytes .../ExtSharePointEdit.PermissionSet.al | 18 + .../ExtSharePointObjects.PermissionSet.al | 18 + .../ExtSharePointRead.PermissionSet.al | 18 + ...rageAdminExtSharePoint.PermissionSetExt.al | 11 + ...orageEditExtSharePoint.PermissionSetExt.al | 11 + .../app/src/ExtSharePointAccount.Page.al | 68 +++ .../app/src/ExtSharePointAccount.Table.al | 95 ++++ .../src/ExtSharePointAccountWizard.Page.al | 169 ++++++ .../app/src/ExtSharePointConnector.EnumExt.al | 21 + .../ExtSharePointConnectorImpl.Codeunit.al | 458 ++++++++++++++++ .../test/ExtensionLogo.png | Bin 0 -> 4681 bytes .../test/README.md | 0 .../test/app.json | 55 ++ .../ExtSharePointConnectorTest.Codeunit.al | 153 ++++++ .../ExtSharePointAccountMock.Codeunit.al | 79 +++ .../.AL-Go/settings.json | 5 +- 64 files changed, 3775 insertions(+), 1 deletion(-) create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/Entitlements/ExtBlobStorageConnector.Entitlement.al create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/ExtensionLogo.png create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/README.md create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/app.json create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/data/connector-logo.png create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorEdit.PermissionSet.al create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorObjects.PermissionSet.al create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/ExtBlobStorRead.PermissionSet.al create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageAdminExtBlobStorage.PermissionSetExt.al create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/permissions/FileStorageEditExtBlobStorage.PermissionSetExt.al create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoConnectorImpl.Codeunit.al create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStoContainerLookup.Page.al create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorAccountWizard.Page.al create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Page.al create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAccount.Table.al create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageAuthType.Enum.al create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/app/src/ExtBlobStorageConnector.EnumExt.al create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/test/ExtensionLogo.png create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/test/README.md create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/test/app.json create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/test/src/ExtAzureBlobServiceTest.Codeunit.al create mode 100644 Apps/W1/External File Storage - Azure Blob Service Connector/test/src/mocks/ExtBlobAccountMock.Codeunit.al create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/Entitlements/ExtFileShareConnector.Entitlement.al create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/ExtensionLogo.png create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/README.md create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/app.json create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/data/connector-logo.png create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareEdit.PermissionSet.al create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareObjects.PermissionSet.al create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/permissions/ExtFileShareRead.PermissionSet.al create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageAdminExtFileShare.PermissionSetExt.al create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/permissions/FileStorageEditExtFileShare.PermissionSetExt.al create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Page.al create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccount.Table.al create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAccountWizard.Page.al create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareAuthType.Enum.al create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnector.EnumExt.al create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/app/src/ExtFileShareConnectorImpl.Codeunit.al create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/test/ExtensionLogo.png create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/test/README.md create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/test/app.json create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/test/src/ExtAzureFileServiceTest.Codeunit.al create mode 100644 Apps/W1/External File Storage - Azure File Service Connector/test/src/mocks/ExtFileAccountMock.Codeunit.al create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/Entitlements/ExtSharePointConnector.Entitlement.al create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/ExtensionLogo.png create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/README.md create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/app.json create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/data/connector-logo.png create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointEdit.PermissionSet.al create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointObjects.PermissionSet.al create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/permissions/ExtSharePointRead.PermissionSet.al create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageAdminExtSharePoint.PermissionSetExt.al create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/permissions/FileStorageEditExtSharePoint.PermissionSetExt.al create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Page.al create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccount.Table.al create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointAccountWizard.Page.al create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnector.EnumExt.al create mode 100644 Apps/W1/External File Storage - SharePoint Connector/app/src/ExtSharePointConnectorImpl.Codeunit.al create mode 100644 Apps/W1/External File Storage - SharePoint Connector/test/ExtensionLogo.png create mode 100644 Apps/W1/External File Storage - SharePoint Connector/test/README.md create mode 100644 Apps/W1/External File Storage - SharePoint Connector/test/app.json create mode 100644 Apps/W1/External File Storage - SharePoint Connector/test/src/ExtSharePointConnectorTest.Codeunit.al create mode 100644 Apps/W1/External File Storage - SharePoint Connector/test/src/mocks/ExtSharePointAccountMock.Codeunit.al 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 0000000000000000000000000000000000000000..30941b354fa335cad3ea5426ac24cadb2ee328e5 GIT binary patch literal 4681 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d94371448c610492afc354a236ae1754c36f48b9 GIT binary patch literal 2140 zcmd5;c{JNu8@?g7sv5OqG*m}AMNNb1Am~_Xt)`aI`DO&897;`VB9_qciE&ya9X%~- zrm^p;gi&jVRM06J8GLAxqOlJ_s`l;sMLXY|`D6b7?m73|``r8f&Ut_D^E~glDQ=e? z6%HLc1OQNQcCzyT00wPgKvo)Byu-_a#E9VGXbY;?8gl^1syW-)coMUOug56f*VJW} zXU-ebd9P3MuHZblgBz&pl_jb>?lz&tWP}gb!;5E8EU{ZO1{pCkdazllEyR_mTcJC!QsuOe?VbGnr<#T#r&Zv1$;Q;4N-r{@rf zo)bT@G^o-D8D? zT}-TOD@~q?(wAKGN;R`0SMq%K+F7)v2=-(QgqttDzj)J&3C6fZaChoIKrjpbC~7}Om0>LUlyoRM_`j#^v9C3 z2}i)`Qy|Y@*^wk00D8}A0?$jpx|1(OBF{m`65wi$94;=i7K_(p`WYrC$|yQqEMHl_Q?DSM__dc8d2@5qEb?;GBGjPLQ4jD;ZcB-Er^nq;766q&gQ2?J-ZIm!rFTBin;A>U9FZIkW-Ya=pU zR<;(RL8Y(}1fSFFEwf3u2S0ri-uQ)TrZb0}5llu_P;fUI6szXx@DXK4`JL|&2$0lo z$S`U+N|CB}sYbFiM9be}z8Ffc({r**Kv~;$WQTlmGq=si_hqJFvaID=IcuspU%gB7 zrL8xjym8H{fYS;??a_0&njxPoe*7@=k)b{r2_r44G#G!x(fm_4E%G4Z!Ux5m4x}P> zdQE{NY1ni~z%Qn>4uO9K;0^DxWU7{gyQP(HwD;wg-%?8_N@>UMt)7X83%I72u$F82 zyosKq(XnVfp2{#RtF=+xDt3xy6b^f!UX$@$v@-0Qg6diNqXpSGog=r=k1YM{-$DKI zrLJq@;!829RAgKxxFz@&IUkoCT4B|lkMnFQJ zTheRu=I{%A!m=A4Wqlu)7`GaymC`p#Bfe>1S}AXKJHE!53OCq8S{D=xR1Y%18_I&KlQP?Jp5)?Vb_2xR79jw_9| z?(c2K$21v?tmf|K#;v_VVFxt0!uf=XOahzrW_dDh{q_1iMyU8v@Q;IJz9aLicZ4v7 SVbm`Ua<;#0S7qz><9`9f9HPqr literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..30941b354fa335cad3ea5426ac24cadb2ee328e5 GIT binary patch literal 4681 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..30941b354fa335cad3ea5426ac24cadb2ee328e5 GIT binary patch literal 4681 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..98f852e166fe7ca4d8c8c188feeee78c2c3dd210 GIT binary patch literal 4430 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 TiwojUAwW;d=w6kEW90t;R3DOa literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..30941b354fa335cad3ea5426ac24cadb2ee328e5 GIT binary patch literal 4681 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..30941b354fa335cad3ea5426ac24cadb2ee328e5 GIT binary patch literal 4681 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3c4d410a3714e5411eaec2aeab2f8493458bc3b8 GIT binary patch literal 13403 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 +/// 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 0000000000000000000000000000000000000000..30941b354fa335cad3ea5426ac24cadb2ee328e5 GIT binary patch literal 4681 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 literal 0 HcmV?d00001 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