Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# External File Storage - SharePoint Connector

Implements the External File Storage framework's connector interface for SharePoint Online. This is one of four first-party connectors (alongside Azure Blob, Azure File, and SFTP) that plug into BC's unified file storage API. It delegates to the SharePoint system module, which wraps the SharePoint REST API with OAuth 2.0 authentication via Microsoft Entra ID.

## Quick reference

- **ID range**: 4580--4589
- **Namespace**: `System.ExternalFileStorage`
- **Depends on**: External File Storage module (framework), SharePoint module (REST API wrapper) -- both in System Application

## How it works

The connector registers itself by extending the `Ext. File Storage Connector` enum with a `SharePoint` value that maps to `Ext. SharePoint Connector Impl` -- a codeunit implementing the `External File Storage Connector` interface. The framework discovers connectors by iterating enum values.

Each SharePoint account stores a site URL, base folder path, and Azure AD app registration credentials (tenant ID, client ID, and either a client secret or a certificate). Credentials are stored in BC's IsolatedStorage (Company scope) via Guid key references -- the table never holds the actual secrets. Authentication is mutually exclusive: setting a client secret clears any stored certificate, and vice versa.

Every file operation follows the same pattern: look up the account, compose the server-relative path (site path + base folder + relative path), initialize a SharePoint client with OAuth credentials, perform the operation, and check for errors via `SharePointClient.GetDiagnostics()`. The SharePoint module handles the OAuth token lifecycle -- this connector only needs to supply credentials and scopes.

The most non-obvious part is path composition. SharePoint uses server-relative URLs (e.g., `/sites/ProjectX/Shared Documents/Reports/file.pdf`). The `InitPath` procedure parses the site URL to extract the site path (`/sites/ProjectX`), combines the base folder with the caller's relative path, and prepends the site path. This is more complex than the SFTP connector's simple path concatenation.

## Structure

- `src/` -- All business logic: account table, account card page, setup wizard, auth enum, connector enum extension, and the implementation codeunit
- `permissions/` -- Permission sets and extensions integrating with the framework's permission model
- `Entitlements/` -- Implicit entitlement granting edit access

## Documentation

- [docs/data-model.md](docs/data-model.md) -- Account table design, IsolatedStorage secret pattern, relationship to framework
- [docs/business-logic.md](docs/business-logic.md) -- Account registration, OAuth initialization, path composition, file operations

## Things to know

- **Requires Azure AD app registration** -- the SharePoint site's tenant must have a registered app with `Sites.ReadWrite.All` permission. The connector stores the tenant ID and client ID in the account table.
- **Two auth methods, mutually exclusive** -- Client Secret (OAuth authorization code) or Certificate (client credentials). Setting one clears the other via `ClearCertificateAuthentication` / `ClearClientSecretAuthentication`.
- **OAuth scope is hardcoded** -- `00000003-0000-0ff1-ce00-000000000000/.default` (the SharePoint resource GUID). This targets SharePoint Online specifically.
- **Path composition is complex** -- the connector parses the SharePoint site URL to extract the site path, then combines it with the base folder and relative path to form a server-relative URL. The `GetSitePathFromUrl` procedure handles URL parsing via the `Uri` codeunit.
- **CopyFile and MoveFile are download + re-upload** -- the SharePoint module doesn't expose native copy/move operations, so the connector downloads to a TempBlob and re-uploads. MoveFile additionally deletes the source after upload.
- **FileExists uses directory listing** -- there is no direct "file exists" API call. Instead, the connector lists the parent directory, filters by `ServerRelativeUrl` matching the target path, and checks if any results come back.
- **Certificate auth uses .pfx/.p12 files** -- unlike the SFTP connector which accepts .pk/.ppk/.pub SSH keys, this connector expects PKCS#12 certificates with optional passphrase.
- **Sandbox safety** -- the `EnvironmentCleanup_OnClearCompanyConfig` subscriber auto-disables all accounts when a sandbox is created from production.
- **Not extensible** -- pages are `Extensible = false`, the auth enum is `Extensible = false`. No integration events are published.
- **GetFile has a stream workaround** -- same as SFTP: the InStream from `SharePointClient.DownloadFileContentByServerRelativeUrl` dies after crossing the interface boundary, so content is copied through an HttpContent intermediary.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Business logic

## Overview

The SharePoint connector has two responsibilities: managing SharePoint account configurations and executing file/directory operations against SharePoint Online. All business logic lives in `ExtSharePointConnectorImpl.Codeunit.al`, which implements the `External File Storage Connector` interface. The account table handles credential storage, and the wizard page guides initial account creation.

## Account registration

Account creation uses a wizard page (`Ext. SharePoint Account Wizard`) running on a temporary record. The wizard collects: account name, tenant ID, client ID, authentication type, credentials (client secret or certificate + optional password), SharePoint URL, and base folder path. Each field validates via `IsAccountValid` (all required fields non-empty, tenant/client IDs non-null). The "Next" action calls `CreateAccount`, which:

1. Copies fields from the temporary record to a new `Ext. SharePoint Account` record
2. Generates a new Guid as the account ID
3. Stores credentials in IsolatedStorage based on auth type
4. Inserts the record
5. Returns a `File Account` record to the framework

```mermaid
flowchart TD
A[User opens wizard] --> B[Fill connection details]
B --> C{All required fields valid?}
C -->|No| D[Next button disabled]
C -->|Yes| E[Click Next]
E --> F{Auth type?}
F -->|Client Secret| G[Store secret in IsolatedStorage]
F -->|Certificate| H[Store cert + optional password in IsolatedStorage]
G --> I[Insert Ext. SharePoint Account record]
H --> I
I --> J[Return File Account to framework]
```

## SharePoint client initialization

`InitSharePointClient` is called before every file operation. It loads the account, checks the `Disabled` flag (errors if true), then dispatches based on auth type:

- **Client Secret**: calls `SharePointAuth.CreateAuthorizationCode` with the tenant ID, client ID, secret (fetched from IsolatedStorage), and the SharePoint resource scope (`00000003-0000-0ff1-ce00-000000000000/.default`).
- **Certificate**: calls `SharePointAuth.CreateClientCredentials` with the tenant ID, client ID, certificate stream (decoded from Base64 in IsolatedStorage), optional certificate password, and the same scope.

The resulting authorization object is passed to `SharePointClient.Initialize` along with the account's SharePoint URL.

## Path composition

This is the most complex part of the connector. SharePoint REST API requires server-relative URLs like `/sites/ProjectX/Shared Documents/Reports/file.pdf`. The `InitPath` procedure builds these from three inputs:

1. **SharePoint URL** (e.g., `https://contoso.sharepoint.com/sites/ProjectX`) -- `GetSitePathFromUrl` parses this via the `Uri` codeunit to extract `/sites/ProjectX`
2. **Base Relative Folder Path** (e.g., `Shared Documents`) -- configured on the account
3. **Caller's relative path** (e.g., `Reports/file.pdf`) -- passed to each file operation

These are combined with `CombinePath` (handles slash normalization), then the site path is prepended if not already present. Helper procedures `GetParentPath` and `GetFileName` split paths at the last `/` for operations like `CreateFile` (which needs the parent folder and filename separately for `SharePointClient.AddFileToFolder`).

## File and directory operations

Every operation follows the same pattern: `InitPath` to compose the server-relative URL, `InitSharePointClient` to get an authenticated client, call the appropriate `SharePointClient` method, check the boolean return value, and call `ShowError` (which reads `SharePointClient.GetDiagnostics().GetErrorMessage()`) if it failed.

### Notable operation details

- **CopyFile** downloads the source file to a TempBlob InStream via `GetFile`, then re-uploads via `CreateFile`. The entire file passes through BC server memory.
- **MoveFile** does the same as CopyFile, then deletes the source file. This is a three-step operation (download, upload, delete) -- not atomic. A failure during upload or delete leaves partial state.
- **FileExists** has no direct API equivalent. The connector calls `ListFiles` on the parent directory, then iterates results checking if `ServerRelativeUrl` matches the target path. This means checking existence requires loading the full directory listing.
- **DirectoryExists** uses `SharePointClient.FolderExistsByServerRelativeUrl` -- unlike FileExists, this has a dedicated API call.
- **ListDirectories** filters results by the `SharePoint Folder` record type (not `SharePoint File`), similar to how ListFiles filters.
- **GetFile** wraps the downloaded stream through an HttpContent intermediary due to a platform stream lifetime issue.

## Environment cleanup

The codeunit subscribes to `EnvironmentCleanup.OnClearCompanyConfig`. When a sandbox is created from production, the subscriber sets `Disabled = true` on all active SharePoint accounts. This prevents sandbox environments from accidentally modifying files on production SharePoint sites. There is no automated undo -- admins must manually re-enable accounts.
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Data model

## Overview

The SharePoint connector has a single table (`Ext. SharePoint Account`) that stores everything needed to connect to a SharePoint Online site via OAuth. Like the other file storage connectors, credentials live in IsolatedStorage rather than the table itself. The connector populates the framework's `File Account` table on demand via `GetAccounts`, but there is no foreign key between them.

## Account and secrets

```mermaid
erDiagram
"Ext. SharePoint Account" ||--o| "IsolatedStorage (Client Secret)" : "Client Secret Key"
"Ext. SharePoint Account" ||--o| "IsolatedStorage (Certificate)" : "Certificate Key"
"Ext. SharePoint Account" ||--o| "IsolatedStorage (Cert Password)" : "Certificate Password Key"
"Ext. SharePoint Account" }o..o| "File Account" : "populated by GetAccounts"
```

The `Ext. SharePoint Account` table (4580) uses a Guid primary key (`Id`). This Guid becomes the `Account Id` in the framework's `File Account` record. The table also stores the SharePoint site URL, a base relative folder path (scoping all operations to a subdirectory), and Azure AD identifiers (Tenant Id and Client Id).

### Secret storage pattern

Three fields -- `Client Secret Key`, `Certificate Key`, and `Certificate Password Key` -- are Guid pointers into IsolatedStorage (Company scope). When a secret is set, a new Guid is generated if one doesn't exist, and the value is written to IsolatedStorage. The `OnDelete` trigger calls `TryDeleteIsolatedStorageValue` for all three keys. Certificates (.pfx/.p12) are stored as Base64-encoded SecretText.

### Authentication type

The `Ext. SharePoint Auth Type` enum (4585) has two values: `Client Secret` (0, default) and `Certificate` (1). This enum is `Extensible = false`. The auth type controls which OAuth flow is used during `InitSharePointClient`:

- **Client Secret** -- calls `SharePointAuth.CreateAuthorizationCode(TenantId, ClientId, Secret, Scopes)`
- **Certificate** -- calls `SharePointAuth.CreateClientCredentials(TenantId, ClientId, Certificate, Password, Scopes)`

Setting credentials for one auth type clears the other. `SetClientSecret` calls `ClearCertificateAuthentication`, and `SetCertificate` calls `ClearClientSecretAuthentication`. This ensures only one set of credentials exists at any time.

### Disabled flag

The `Disabled` boolean is automatically set to `true` by the environment cleanup event subscriber when a sandbox is created from production. A disabled account errors immediately in `InitSharePointClient`.
Loading