Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/Host/BravoServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static IServiceCollection AddBravoInitializationServices(this IServiceCol
// redirection belong to the entry point and stay out of the container.
services.AddSingleton<IInstanceActivationEvents>(context.Instance);

services.AddSingleton<IPolicies>(PoliciesFactory.Create());
services.AddSingleton<IPolicyService>(new PolicyServiceFactory().Create());
services.AddSingleton<ITelemetryService>(TelemetryService.Instance);

return services;
Expand Down
8 changes: 4 additions & 4 deletions src/Infrastructure/AppWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,18 @@ internal partial class AppWindow : Form
private readonly IOptions<StartupSettings> _startupSettingsOptionsAccessor;
private readonly WebView2ProxyAuthHandler _proxyAuthHandler;
private readonly Color _startupThemeColor;
private readonly IPolicies _policies;
private readonly IPolicyService _policyService;

public AppWindow(
IInstanceActivationEvents instanceActivationEvents,
IServerAddressProvider serverAddressProvider,
IOptions<StartupSettings> startupSettingsOptionsAccessor,
IPolicies policies)
IPolicyService policyService)
{
_instanceActivationEvents = instanceActivationEvents;
_serverAddressProvider = serverAddressProvider;
_startupSettingsOptionsAccessor = startupSettingsOptionsAccessor;
_policies = policies;
_policyService = policyService;
_proxyAuthHandler = new WebView2ProxyAuthHandler(WebProxyWrapper.Current);
_startupThemeColor = ThemeHelper.ShouldUseDarkMode(UserPreferences.Current.Theme) ? AppEnvironment.ThemeColorDark : AppEnvironment.ThemeColorLight;

Expand Down Expand Up @@ -328,7 +328,7 @@ private MemoryStream GetConfigJs()
token = AppEnvironment.ApiAuthenticationToken,
version = AppVersion.SemanticVersion,
options = BravoOptions.CreateFromUserPreferences(),
policies = _policies,
policies = _policyService.Current,
culture = new
{
ietfLanguageTag = CultureInfo.CurrentCulture.IetfLanguageTag,
Expand Down
6 changes: 6 additions & 0 deletions src/Infrastructure/Policies/IPolicyReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Sqlbi.Bravo.Infrastructure.Policies;

internal interface IPolicyReader
{
PolicySnapshot Read();
}
22 changes: 7 additions & 15 deletions src/Infrastructure/Policies/IPolicySource.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,13 @@
using System;
using Microsoft.Win32;

namespace Sqlbi.Bravo.Infrastructure.Policies;

/// <summary>
/// Abstraction over a single raw policy value store (e.g. a registry key), so that
/// <see cref="Policies"/>' parsing/precedence logic does not depend on <see cref="RegistryKey"/>
/// directly and can be unit tested against a fake, without touching the real registry.
/// </summary>
internal interface IPolicySource
internal interface IPolicySource : IDisposable
{
int? GetInt(string name);
string? GetString(string name);
}

/// <summary>
/// Typed reading conventions shared by every <see cref="IPolicySource"/>: a policy is a
/// DWORD (0/1 -> bool, or a defined enum member) or a string. Kept as extensions rather than
/// interface members so <see cref="IPolicySource"/> itself stays minimal (raw int/string only).
/// </summary>
internal static class PolicySourceExtensions
{
private const int PolicyDisabledValue = 0;
Expand All @@ -37,10 +26,13 @@ internal static class PolicySourceExtensions
};
}

public T? GetEnum<T>(string name) where T : struct, Enum
/// <remarks>
/// TEnum must have int as its underlying type.
/// </remarks>
public TEnum? GetEnum<TEnum>(string name) where TEnum : struct, Enum
{
if (source.GetInt(name) is { } value && Enum.IsDefined(typeof(T), value))
return (T)(object)value;
if (source.GetInt(name) is { } value && Enum.IsDefined(typeof(TEnum), value))
return (TEnum)(object)value;

return null; // Policy not set or invalid
}
Expand Down
6 changes: 6 additions & 0 deletions src/Infrastructure/Policies/IPolicySourceFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Sqlbi.Bravo.Infrastructure.Policies;

internal interface IPolicySourceFactory
{
IPolicySource Open(PolicyScope scope);
}
28 changes: 0 additions & 28 deletions src/Infrastructure/Policies/Policies.cs

This file was deleted.

7 changes: 7 additions & 0 deletions src/Infrastructure/Policies/PolicyScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Sqlbi.Bravo.Infrastructure.Policies;

internal enum PolicyScope
{
Computer,
User,
}
25 changes: 25 additions & 0 deletions src/Infrastructure/Policies/PolicyService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

namespace Sqlbi.Bravo.Infrastructure.Policies;

public interface IPolicyService
{
/// <summary>
/// Gets the policy snapshot initialized once for this service instance.
/// </summary>
PolicySnapshot Current { get; }
}

internal sealed class PolicyService : IPolicyService
{
private readonly IPolicyReader _reader;
private readonly Lazy<PolicySnapshot> _current;

public PolicyService(IPolicyReader reader)
{
_reader = reader;
_current = new Lazy<PolicySnapshot>(_reader.Read);
}

public PolicySnapshot Current => _current.Value;
}
12 changes: 12 additions & 0 deletions src/Infrastructure/Policies/PolicyServiceFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Sqlbi.Bravo.Infrastructure.Policies;

internal interface IPolicyServiceFactory
{
IPolicyService Create();
}

internal sealed class PolicyServiceFactory : IPolicyServiceFactory
{
public IPolicyService Create()
=> new PolicyService(new RegistryPolicyReader(new RegistryPolicySourceFactory()));
}
21 changes: 21 additions & 0 deletions src/Infrastructure/Policies/PolicySnapshot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Sqlbi.Bravo.Infrastructure.Configuration.Settings;

namespace Sqlbi.Bravo.Infrastructure.Policies;

/// <summary>
/// Contains the effective policy values from one read of the configured sources.
/// </summary>
/// <remarks>
/// Null means not configured; false and zero are configured values.
/// </remarks>
public sealed record PolicySnapshot(
bool? TelemetryEnabled,
UpdateChannelType? UpdateChannel,
bool? UpdateCheckEnabled,
bool? UseSystemBrowserForAuthentication,
bool? BuiltInTemplatesEnabled,
bool? CustomTemplatesEnabled,
string? CustomTemplatesOrganizationRepositoryPath)
{
public static PolicySnapshot NotConfigured { get; } = new(null, null, null, null, null, null, null);
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
using Microsoft.Win32;
using Sqlbi.Bravo.Infrastructure.Configuration.Settings;
using Sqlbi.Bravo.Infrastructure.Configuration.Settings;

namespace Sqlbi.Bravo.Infrastructure.Policies;

/// <summary>
/// Builds <see cref="Policies"/> instances: parses a single <see cref="IPolicySource"/>,
/// and composes the effective policy set from LocalMachine + CurrentUser with precedence.
/// </summary>
internal static class PoliciesFactory
internal sealed class RegistryPolicyReader(IPolicySourceFactory sourceFactory) : IPolicyReader
{
private const string OptionSettingsSubKeyName = @"SOFTWARE\Policies\SQLBI\Bravo\OptionSettings";
private readonly IPolicySourceFactory _sourceFactory = sourceFactory;

public static Policies Create()
public PolicySnapshot Read()
{
using var machineKey = Registry.LocalMachine.OpenSubKey(OptionSettingsSubKeyName);
var machinePolicies = FromSource(new RegistryPolicySource(machineKey));
using var machineSource = _sourceFactory.Open(PolicyScope.Computer);
var machinePolicies = FromSource(machineSource);

using var userKey = Registry.CurrentUser.OpenSubKey(OptionSettingsSubKeyName);
var userPolicies = FromSource(new RegistryPolicySource(userKey));
using var userSource = _sourceFactory.Open(PolicyScope.User);
var userPolicies = FromSource(userSource);

return Merge(machinePolicies, userPolicies);
}

internal static Policies FromSource(IPolicySource source) => new(
internal static PolicySnapshot FromSource(IPolicySource source) => new(
TelemetryEnabled: source.GetBool("TelemetryEnabled"),
UpdateChannel: source.GetEnum<UpdateChannelType>("UpdateChannel"),
UpdateCheckEnabled: source.GetBool("UpdateCheckEnabled"),
Expand All @@ -31,10 +26,10 @@ public static Policies Create()
CustomTemplatesEnabled: source.GetBool("CustomTemplatesEnabled"),
CustomTemplatesOrganizationRepositoryPath: source.GetString("CustomTemplatesOrganizationRepositoryPath"));

internal static Policies Merge(Policies machinePolicies, Policies userPolicies)
internal static PolicySnapshot Merge(PolicySnapshot machinePolicies, PolicySnapshot userPolicies)
{
// LocalMachine takes precedence over CurrentUser when both are configured
return new Policies(
return new PolicySnapshot(
TelemetryEnabled: machinePolicies.TelemetryEnabled ?? userPolicies.TelemetryEnabled,
UpdateChannel: machinePolicies.UpdateChannel ?? userPolicies.UpdateChannel,
UpdateCheckEnabled: machinePolicies.UpdateCheckEnabled ?? userPolicies.UpdateCheckEnabled,
Expand Down
5 changes: 3 additions & 2 deletions src/Infrastructure/Policies/RegistryPolicySource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
namespace Sqlbi.Bravo.Infrastructure.Policies;

/// <summary>
/// Adapter that bridges <see cref="IPolicySource"/> to a real <see cref="RegistryKey"/>.
/// Intentionally a thin pass-through with no logic of its own.
/// Owns a read-only registry key containing raw policy values.
/// </summary>
internal sealed class RegistryPolicySource(RegistryKey? key) : IPolicySource
{
Expand All @@ -15,4 +14,6 @@ internal sealed class RegistryPolicySource(RegistryKey? key) : IPolicySource

public string? GetString(string name)
=> _key?.GetValue(name) as string;

public void Dispose() => _key?.Dispose();
}
21 changes: 21 additions & 0 deletions src/Infrastructure/Policies/RegistryPolicySourceFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using Microsoft.Win32;

namespace Sqlbi.Bravo.Infrastructure.Policies;

internal sealed class RegistryPolicySourceFactory : IPolicySourceFactory
{
private const string OptionSettingsSubKeyName = @"SOFTWARE\Policies\SQLBI\Bravo\OptionSettings";

public IPolicySource Open(PolicyScope scope)
{
var hive = scope switch
{
PolicyScope.Computer => Registry.LocalMachine,
PolicyScope.User => Registry.CurrentUser,
_ => throw new ArgumentOutOfRangeException(nameof(scope)),
};

return new RegistryPolicySource(hive.OpenSubKey(OptionSettingsSubKeyName));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ public interface ICloudAuthenticationClient
/// <summary>
/// Handles authentication with Microsoft Entra ID (Azure AD) using MSAL.NET, including token acquisition and cache management.
/// </summary>
internal sealed class CloudAuthenticationClient(IPolicies policies) : ICloudAuthenticationClient
internal sealed class CloudAuthenticationClient(IPolicyService policyService) : ICloudAuthenticationClient
{
private const string SystemBrowserRedirectUri = "http://localhost";
private const string OrganizationalAccountsOnlyQueryParameter = "msafed=0"; // no Microsoft accounts (MSA) allowed

private readonly IPolicies _policies = policies;
private readonly IPolicyService _policyService = policyService;

public async Task<AuthenticationResult> AcquireTokenAsync(
CloudEnvironment environment, string email, CancellationToken cancellationToken)
Expand Down Expand Up @@ -77,7 +77,7 @@ public async Task ClearTokenCacheAsync(CloudEnvironment environment)
private async Task<Msal.AuthenticationResult> AcquireTokenInteractiveAsync(
IPublicClientApplication client, string[] scopes, string email, string claims, CancellationToken cancellationToken)
{
var useSystemBrowser = _policies.UseSystemBrowserForAuthentication ?? UserPreferences.Current.UseSystemBrowserForAuthentication;
var useSystemBrowser = _policyService.Current.UseSystemBrowserForAuthentication ?? UserPreferences.Current.UseSystemBrowserForAuthentication;
var useEmbeddedBrowser = !useSystemBrowser;
var extraQueryParameters = OrganizationalAccountsOnlyQueryParameter;
var prompt = Prompt.SelectAccount;
Expand Down Expand Up @@ -125,7 +125,7 @@ public async Task ClearTokenCacheAsync(CloudEnvironment environment)

private IPublicClientApplication CreatePublicClient(CloudEnvironment environment)
{
var useSystemBrowser = _policies.UseSystemBrowserForAuthentication ?? UserPreferences.Current.UseSystemBrowserForAuthentication;
var useSystemBrowser = _policyService.Current.UseSystemBrowserForAuthentication ?? UserPreferences.Current.UseSystemBrowserForAuthentication;
var useEmbeddedBrowser = !useSystemBrowser;
var redirectUri = (useEmbeddedBrowser ? environment.RedirectUri : SystemBrowserRedirectUri);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ internal class DaxTemplateManager
internal static readonly string UserPath = Path.Combine(AppEnvironment.ApplicationDataPath, @"ManageDates\Templates");

private readonly object _cacheSyncLock = new();
private readonly IPolicies _policies;
private readonly IPolicyService _policyService;

public DaxTemplateManager(IPolicies policies)
public DaxTemplateManager(IPolicyService policyService)
{
_policies = policies;
_policyService = policyService;
InitializeCache();
}

Expand All @@ -46,7 +46,7 @@ public Package GetPackage(string path)

public IEnumerable<Package> GetPackages()
{
if (_policies.BuiltInTemplatesEnabled is false)
if (_policyService.Current.BuiltInTemplatesEnabled is false)
{
return Array.Empty<Package>();
}
Expand Down
6 changes: 3 additions & 3 deletions src/Infrastructure/Telemetry/TelemetryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal sealed class TelemetryService : ITelemetryService
{
private readonly TelemetryConfiguration _configuration;
private readonly TelemetryClient _client;
private readonly IPolicies _policies;
private readonly PolicySnapshot _policies;

private static readonly Lazy<TelemetryService> _instance = new(CreateInstance, isThreadSafe: true);

Expand All @@ -37,7 +37,7 @@ private static TelemetryService CreateInstance()
configuration.TelemetryInitializers.Add(new DefaultTelemetryInitializer());
configuration.ConnectionString = TelemetrySessionInfo.ConnectionString;

var policies = PoliciesFactory.Create();
var policies = new PolicyServiceFactory().Create().Current;

// Determine whether telemetry is enabled based on policies and user settings.
var telemetryEnabled = policies.TelemetryEnabled ?? UserPreferences.Current.TelemetryEnabled;
Expand All @@ -48,7 +48,7 @@ private static TelemetryService CreateInstance()
return new TelemetryService(configuration, policies);
}

private TelemetryService(TelemetryConfiguration configuration, IPolicies policies)
private TelemetryService(TelemetryConfiguration configuration, PolicySnapshot policies)
{
_configuration = configuration;
_client = new TelemetryClient(configuration);
Expand Down
4 changes: 2 additions & 2 deletions src/Services/ManageDatesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ internal class ManageDatesService : IManageDatesService
{
private readonly DaxTemplateManager _templateManager;

public ManageDatesService(IPolicies policies)
public ManageDatesService(IPolicyService policyService)
{
_templateManager = new DaxTemplateManager(policies);
_templateManager = new DaxTemplateManager(policyService);
}

public IEnumerable<DateConfiguration> GetConfigurations(PBIDesktopReport report, CancellationToken cancellationToken)
Expand Down
Loading
Loading