feat: add tenant provisioning and key lifecycle (#5)
quality-gate / quality (push) Successful in 50s
quality-gate / quality (push) Successful in 50s
Closes #5
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
using FinalFactory.Rendezvous.Contracts;
|
||||
|
||||
namespace FinalFactory.Rendezvous.Server.Provisioning;
|
||||
|
||||
internal sealed class GamePolicyRegistry
|
||||
{
|
||||
private readonly Dictionary<(GameId Game, EnvironmentId Environment), GamePolicy> _policies;
|
||||
|
||||
private GamePolicyRegistry(
|
||||
Dictionary<(GameId Game, EnvironmentId Environment), GamePolicy> policies) =>
|
||||
_policies = policies;
|
||||
|
||||
public bool HasEnabledPolicies => _policies.Values.Any(static policy => policy.Enabled);
|
||||
public IEnumerable<GamePolicy> EnabledPolicies =>
|
||||
_policies.Values.Where(static policy => policy.Enabled);
|
||||
|
||||
public static GamePolicyRegistry Create(IEnumerable<GamePolicyOptions> options)
|
||||
{
|
||||
GamePolicyOptions[] configuredPolicies = options.ToArray();
|
||||
if (configuredPolicies.Length > ProvisioningLimits.MaxGamePolicies)
|
||||
{
|
||||
throw new ProvisioningConfigurationException(
|
||||
$"At most {ProvisioningLimits.MaxGamePolicies} game policies may be configured.");
|
||||
}
|
||||
|
||||
Dictionary<(GameId Game, EnvironmentId Environment), GamePolicy> policies = [];
|
||||
foreach (GamePolicyOptions policyOptions in configuredPolicies)
|
||||
{
|
||||
Validate(policyOptions);
|
||||
GamePolicy policy = new(policyOptions);
|
||||
if (!policies.TryAdd((policy.GameId, policy.EnvironmentId), policy))
|
||||
{
|
||||
throw new ProvisioningConfigurationException(
|
||||
$"Duplicate game/environment policy: {policy.GameId}/{policy.EnvironmentId}.");
|
||||
}
|
||||
}
|
||||
|
||||
return new GamePolicyRegistry(policies);
|
||||
}
|
||||
|
||||
public bool TryGet(
|
||||
GameId gameId,
|
||||
EnvironmentId environmentId,
|
||||
out GamePolicy? policy)
|
||||
{
|
||||
if (_policies.TryGetValue((gameId, environmentId), out GamePolicy? candidate)
|
||||
&& candidate.Enabled)
|
||||
{
|
||||
policy = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
policy = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void Validate(GamePolicyOptions options)
|
||||
{
|
||||
if (!GameId.TryParse(options.GameId, out _)
|
||||
|| !EnvironmentId.TryParse(options.EnvironmentId, out _))
|
||||
{
|
||||
throw new ProvisioningConfigurationException(
|
||||
"Game policies require valid game and environment IDs.");
|
||||
}
|
||||
|
||||
if (options.ProtocolVersions.Count is 0 or > ProvisioningLimits.MaxProtocolVersionsPerPolicy
|
||||
|| options.ProtocolVersions.Contains(0)
|
||||
|| options.ProtocolVersions.Count != options.ProtocolVersions.Distinct().Count())
|
||||
{
|
||||
throw new ProvisioningConfigurationException(
|
||||
$"Policy {options.GameId}/{options.EnvironmentId} requires unique non-zero protocol versions.");
|
||||
}
|
||||
|
||||
if (options.Regions.Count is 0 or > ProvisioningLimits.MaxRegionsPerPolicy
|
||||
|| options.Regions.Any(static region => !RegionId.TryParse(region, out _))
|
||||
|| options.Regions.Count != options.Regions.Distinct(StringComparer.Ordinal).Count())
|
||||
{
|
||||
throw new ProvisioningConfigurationException(
|
||||
$"Policy {options.GameId}/{options.EnvironmentId} requires unique valid regions.");
|
||||
}
|
||||
|
||||
if (options.VisibilityModes.Count == 0
|
||||
|| options.VisibilityModes.Any(static mode => !Enum.IsDefined(mode))
|
||||
|| options.VisibilityModes.Count != options.VisibilityModes.Distinct().Count()
|
||||
|| options.PublisherTrustModes.Count == 0
|
||||
|| options.PublisherTrustModes.Any(static mode => !Enum.IsDefined(mode))
|
||||
|| options.PublisherTrustModes.Count != options.PublisherTrustModes.Distinct().Count())
|
||||
{
|
||||
throw new ProvisioningConfigurationException(
|
||||
$"Policy {options.GameId}/{options.EnvironmentId} requires valid visibility and trust modes.");
|
||||
}
|
||||
|
||||
if (options.MetadataMaxBytes is < 2 or > ContractLimits.MetadataMaxBytes
|
||||
|| options.MetadataMaxKeys is < 0 or > ContractLimits.MetadataMaxKeys
|
||||
|| options.MetadataValueMaxBytes.Count > options.MetadataMaxKeys
|
||||
|| options.MetadataValueMaxBytes.Any(static item =>
|
||||
string.IsNullOrWhiteSpace(item.Key)
|
||||
|| !ContractValidation.IsUtf8LengthWithin(item.Key, ContractLimits.MetadataKeyMaxBytes)
|
||||
|| item.Value is < 0 or > ContractLimits.MetadataValueMaxBytes)
|
||||
|| options.RequiredMetadataKeys.Any(key =>
|
||||
!options.MetadataValueMaxBytes.ContainsKey(key)))
|
||||
{
|
||||
throw new ProvisioningConfigurationException(
|
||||
$"Policy {options.GameId}/{options.EnvironmentId} has an invalid metadata schema.");
|
||||
}
|
||||
|
||||
if (options.MaxListingsPerPrincipal is < 1 or > ProvisioningLimits.MaxListingsPerPrincipal
|
||||
|| options.MaxAnonymousListingsPerAddress < 0
|
||||
|| options.MaxAnonymousListingsPerAddress > options.MaxListingsPerPrincipal
|
||||
|| options.MaxActiveJoinAttempts is < 1
|
||||
or > ProvisioningLimits.MaxActiveJoinAttemptsPerPolicy
|
||||
|| !Enum.IsDefined(options.FallbackPolicy))
|
||||
{
|
||||
throw new ProvisioningConfigurationException(
|
||||
$"Policy {options.GameId}/{options.EnvironmentId} has invalid quotas or fallback policy.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user