diff --git a/proprietary/Office365/APIHelper.cs b/proprietary/Office365/APIHelper.cs index 61f5f91ce..6cfb77f80 100644 --- a/proprietary/Office365/APIHelper.cs +++ b/proprietary/Office365/APIHelper.cs @@ -56,6 +56,11 @@ internal class APIHelper : IDisposable /// private readonly string? _certificatePassword; + /// + /// The scope for the API requests + /// + private readonly string _scope; + /// /// The timeout options for the backend /// @@ -71,7 +76,8 @@ internal class APIHelper : IDisposable /// The timeout options /// The path to the certificate file /// The password for the certificate file - private APIHelper(HttpClient httpClient, AuthOptionsHelper.AuthOptions authOptions, string tenantId, string graphBaseUrl, TimeoutOptionsHelper.Timeouts timeouts, string? certificatePath, string? certificatePassword) + /// The scope for the API requests + private APIHelper(HttpClient httpClient, AuthOptionsHelper.AuthOptions authOptions, string tenantId, string graphBaseUrl, TimeoutOptionsHelper.Timeouts timeouts, string? certificatePath, string? certificatePassword, string scope) { _httpClient = httpClient; _authOptions = authOptions; @@ -80,6 +86,7 @@ internal class APIHelper : IDisposable _timeouts = timeouts; _certificatePath = certificatePath; _certificatePassword = certificatePassword; + _scope = scope; } /// @@ -91,8 +98,9 @@ internal class APIHelper : IDisposable /// The timeout options /// The path to the certificate file /// The password for the certificate file + /// The scope for the API requests /// A new instance of the class - public static APIHelper Create(AuthOptionsHelper.AuthOptions authOptions, string tenantId, string graphBaseUrl, TimeoutOptionsHelper.Timeouts timeouts, string? certificatePath = null, string? certificatePassword = null) + public static APIHelper Create(AuthOptionsHelper.AuthOptions authOptions, string tenantId, string graphBaseUrl, TimeoutOptionsHelper.Timeouts timeouts, string? certificatePath = null, string? certificatePassword = null, string scope = "https://graph.microsoft.com/.default") { var handler = new HttpClientHandler { @@ -103,7 +111,7 @@ internal class APIHelper : IDisposable httpClient.Timeout = Timeout.InfiniteTimeSpan; httpClient.DefaultRequestHeaders.Add("User-Agent", $"Duplicati/{System.Reflection.Assembly.GetExecutingAssembly().GetName().Version}"); - return new APIHelper(httpClient, authOptions, tenantId, graphBaseUrl, timeouts, certificatePath, certificatePassword); + return new APIHelper(httpClient, authOptions, tenantId, graphBaseUrl, timeouts, certificatePath, certificatePassword, scope); } /// @@ -134,7 +142,7 @@ internal class APIHelper : IDisposable { ["grant_type"] = "client_credentials", ["client_id"] = _authOptions.Username!, - ["scope"] = $"{_graphBaseUrl.TrimEnd('/')}/.default" + ["scope"] = _scope }; if (!string.IsNullOrWhiteSpace(_certificatePath)) diff --git a/proprietary/Office365/OptionsHelper.cs b/proprietary/Office365/OptionsHelper.cs index 0cedd3682..85bdf80a0 100644 --- a/proprietary/Office365/OptionsHelper.cs +++ b/proprietary/Office365/OptionsHelper.cs @@ -12,6 +12,9 @@ internal static class OptionsHelper internal const string OFFICE_CERTIFICATE_PATH_OPTION = "office-certificate-path"; internal const string OFFICE_CERTIFICATE_PASSWORD_OPTION = "office-certificate-password"; internal const string OFFICE_GRAPH_BASE_OPTION = "office-graph-base-url"; + internal const string OFFICE_SCOPE_OPTION = "office-scope"; + internal const string OFFICE_SCOPE_OPTION_DEFAULT = "https://graph.microsoft.com/.default"; + internal const string OFFICE_IGNORE_EXISTING_OPTION = "office-ignore-existing"; internal const string DEFAULT_GRAPH_BASE_URL = "https://graph.microsoft.com"; @@ -23,7 +26,8 @@ internal static class OptionsHelper AuthOptionsHelper.AuthOptions AuthOptions, string GraphBaseUrl, string? CertificatePath, - string? CertificatePassword + string? CertificatePassword, + string Scope ); internal static ParsedOptions ParseAndValidateOptions(string url, Dictionary options) @@ -41,12 +45,17 @@ internal static class OptionsHelper var _certificatePath = options.GetValueOrDefault(OFFICE_CERTIFICATE_PATH_OPTION); var _certificatePassword = options.GetValueOrDefault(OFFICE_CERTIFICATE_PASSWORD_OPTION); + var _scope = options.GetValueOrDefault(OFFICE_SCOPE_OPTION); + if (string.IsNullOrWhiteSpace(_scope)) + _scope = OFFICE_SCOPE_OPTION_DEFAULT; + return new ParsedOptions( TenantId: _tenantId, AuthOptions: _authOptions, GraphBaseUrl: _graphBaseUrl, CertificatePath: _certificatePath, - CertificatePassword: _certificatePassword + CertificatePassword: _certificatePassword, + Scope: _scope ); } @@ -57,6 +66,7 @@ internal static class OptionsHelper new CommandLineArgument(OFFICE_SECRET_OPTION, CommandLineArgument.ArgumentType.Password, Strings.OfficeSecretOptionShort, Strings.OfficeSecretOptionLong, null, [AuthOptionsHelper.AuthPasswordOption], null), new CommandLineArgument(OFFICE_CERTIFICATE_PATH_OPTION, CommandLineArgument.ArgumentType.Path, Strings.OfficeCertificatePathOptionShort, Strings.OfficeCertificatePathOptionLong), new CommandLineArgument(OFFICE_CERTIFICATE_PASSWORD_OPTION, CommandLineArgument.ArgumentType.Password, Strings.OfficeCertificatePasswordOptionShort, Strings.OfficeCertificatePasswordOptionLong), - new CommandLineArgument(OFFICE_GRAPH_BASE_OPTION, CommandLineArgument.ArgumentType.String, Strings.OfficeGraphBaseOptionShort, Strings.OfficeGraphBaseOptionLong, DEFAULT_GRAPH_BASE_URL) + new CommandLineArgument(OFFICE_GRAPH_BASE_OPTION, CommandLineArgument.ArgumentType.String, Strings.OfficeGraphBaseOptionShort, Strings.OfficeGraphBaseOptionLong, DEFAULT_GRAPH_BASE_URL), + new CommandLineArgument(OFFICE_SCOPE_OPTION, CommandLineArgument.ArgumentType.String, Strings.OfficeScopeOptionShort, Strings.OfficeScopeOptionLong, OFFICE_SCOPE_OPTION_DEFAULT) ]; } \ No newline at end of file diff --git a/proprietary/Office365/RestoreProvider/RestoreProvider.Calendar.cs b/proprietary/Office365/RestoreProvider/RestoreProvider.Calendar.cs index 91c6ac750..6ce9e7e55 100644 --- a/proprietary/Office365/RestoreProvider/RestoreProvider.Calendar.cs +++ b/proprietary/Office365/RestoreProvider/RestoreProvider.Calendar.cs @@ -497,7 +497,7 @@ partial class RestoreProvider { try { - if (!Provider._overwrite && !string.IsNullOrWhiteSpace(eventItem.Subject)) + if (!Provider._ignoreExisting && !string.IsNullOrWhiteSpace(eventItem.Subject)) { DateTimeOffset? start = null; if (eventItem.Start is JsonElement startElem && startElem.ValueKind == JsonValueKind.Object) @@ -542,7 +542,7 @@ partial class RestoreProvider { try { - if (!Provider._overwrite && !string.IsNullOrWhiteSpace(eventItem.Subject)) + if (!Provider._ignoreExisting && !string.IsNullOrWhiteSpace(eventItem.Subject)) { DateTimeOffset? start = null; if (eventItem.Start is JsonElement startElem && startElem.ValueKind == JsonValueKind.Object) diff --git a/proprietary/Office365/RestoreProvider/RestoreProvider.cs b/proprietary/Office365/RestoreProvider/RestoreProvider.cs index f3632b381..f810db443 100644 --- a/proprietary/Office365/RestoreProvider/RestoreProvider.cs +++ b/proprietary/Office365/RestoreProvider/RestoreProvider.cs @@ -110,7 +110,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule /// /// Whether to overwrite existing items /// - internal readonly bool _overwrite; + internal readonly bool _ignoreExisting; /// /// Default constructor for the restore provider. @@ -140,10 +140,14 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule graphBaseUrl: parsedOptions.GraphBaseUrl, certificatePath: parsedOptions.CertificatePath, certificatePassword: parsedOptions.CertificatePassword, - timeouts: TimeoutOptionsHelper.Parse(options) + timeouts: TimeoutOptionsHelper.Parse(options), + scope: parsedOptions.Scope ); - _overwrite = Utility.ParseBoolOption(options, "overwrite"); + _ignoreExisting = Utility.ParseBoolOption(options, OptionsHelper.OFFICE_IGNORE_EXISTING_OPTION); + var overwrite = Utility.ParseBoolOption(options, "overwrite"); + if (!overwrite) + throw new UserInformationException(Strings.RestoreTargetMissingOverwriteOption(OptionsHelper.OFFICE_IGNORE_EXISTING_OPTION), "OverwriteOptionNotSet"); var sourceOpts = new Dictionary(options) { @@ -167,7 +171,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule public string Description => Strings.ProviderDescription; /// - public IList SupportedCommands => OptionsHelper.SupportedCommands; + public IList SupportedCommands => OptionsHelper.SupportedCommands.Concat([new CommandLineArgument(OptionsHelper.OFFICE_IGNORE_EXISTING_OPTION, CommandLineArgument.ArgumentType.Boolean, Strings.OfficeIgnoreExistingOptionShort, Strings.OfficeIgnoreExistingOptionLong)]).ToList(); /// public string TargetDestination => _restorePath; @@ -847,7 +851,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule targetFolderId = mappedFolderId; } - if (!string.IsNullOrWhiteSpace(internetMessageId) && await EmailApi.EmailExistsInFolderByInternetMessageIdAsync(userId, targetFolderId, internetMessageId, cancel)) + if (!_ignoreExisting && !string.IsNullOrWhiteSpace(internetMessageId) && await EmailApi.EmailExistsInFolderByInternetMessageIdAsync(userId, targetFolderId, internetMessageId, cancel)) { Log.WriteInformationMessage(LOGTAG, "RestoreUserEmailsSkipExisting", null, $"Email with InternetMessageId {internetMessageId} already exists in target mailbox, skipping restore for {emailSource.Key}."); } @@ -1099,7 +1103,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule var displayName = file.Value.GetValueOrDefault("o365:Name") ?? Path.GetFileName(originalPath); - if (!_overwrite) + if (!_ignoreExisting) { var existingItem = await DriveApi.GetDriveItemAsync(driveId, parentId, displayName, cancel); if (existingItem != null) @@ -1316,7 +1320,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule targetFolderId = mappedFolderId; } - if (!_overwrite) + if (!_ignoreExisting) { using (var checkStream = SystemIO.IO_OS.FileOpenRead(contentEntry)) { @@ -1496,7 +1500,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule continue; } - if (!_overwrite) + if (!_ignoreExisting) { if (!existingTasksByPlan.TryGetValue(planId, out var existingTasks)) { @@ -1576,7 +1580,7 @@ public partial class RestoreProvider : IRestoreDestinationProviderModule taskData?.AppliedCategories, cancel); - if (!_overwrite && existingTasksByPlan.TryGetValue(planId, out var currentTasks)) + if (!_ignoreExisting && existingTasksByPlan.TryGetValue(planId, out var currentTasks)) { currentTasks.Add(newTask); } diff --git a/proprietary/Office365/SourceProvider/SourceProvider.cs b/proprietary/Office365/SourceProvider/SourceProvider.cs index f3cd35528..ab61dbd54 100644 --- a/proprietary/Office365/SourceProvider/SourceProvider.cs +++ b/proprietary/Office365/SourceProvider/SourceProvider.cs @@ -129,7 +129,8 @@ public sealed partial class SourceProvider : ISourceProviderModule, IDisposable graphBaseUrl: parsedOptions.GraphBaseUrl, timeouts: _timeouts, certificatePath: parsedOptions.CertificatePath, - certificatePassword: parsedOptions.CertificatePassword + certificatePassword: parsedOptions.CertificatePassword, + scope: parsedOptions.Scope ); } diff --git a/proprietary/Office365/Strings.cs b/proprietary/Office365/Strings.cs index 5542bd824..3f7cdf5a1 100644 --- a/proprietary/Office365/Strings.cs +++ b/proprietary/Office365/Strings.cs @@ -20,6 +20,9 @@ internal static class Strings public static string MetadataStorageNotEnabled(string optionname) => LC.L( $"Storing metadata content in the database must be enabled to use the Microsoft 365 source provider. Use the option: --{optionname}"); + public static string RestoreTargetMissingOverwriteOption(string optionname) => LC.L($"The Office365 restore target must have the overwrite option set to true. Use the provider specific option --{optionname} to avoid overwriting existing items."); + + public static string MissingTenantId => LC.L("A tenant ID must be supplied to query the Office 365 Management API."); public static string OfficeTenantOptionShort => LC.L("Azure AD tenant identifier."); @@ -44,6 +47,12 @@ internal static class Strings public static string OfficeGraphBaseOptionLong => LC.L("Base URL for Microsoft Graph if targeting a sovereign cloud."); + public static string OfficeIgnoreExistingOptionShort => LC.L("Ignore existing items."); + public static string OfficeIgnoreExistingOptionLong => LC.L("If set, existing items in the destination will not be overwritten."); + + public static string OfficeScopeOptionShort => LC.L("Microsoft Graph API scope."); + public static string OfficeScopeOptionLong => LC.L("The scope to use when requesting an access token for the Microsoft Graph API."); + public static string WebModuleOperationShort => LC.L("The operation to perform."); public static string WebModuleOperationLong => LC.L("The operation that the web module should perform.");