//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace MUnique.OpenMU.Persistence.EntityFramework;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using MUnique.OpenMU.Persistence.EntityFramework.Model;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
///
/// A which takes care of required database roles.
///
internal class MyNpgsqlMigrationsSqlGenerator : NpgsqlMigrationsSqlGenerator
{
///
/// A set which contains the names of tables of the data-schema which are required by the config-Role.
///
private static readonly IReadOnlySet DataTablesRequiredByConfigRole = new HashSet
{
nameof(Item),
nameof(ItemOptionLink),
nameof(ItemItemOfItemSet),
nameof(ItemStorage),
};
///
/// A set which contains the names of tables of the data-schema which are required by the guild-Role.
///
private static readonly IReadOnlySet DataTablesRequiredByGuildRole = new HashSet
{
nameof(Character),
};
///
/// A set which contains the names of tables of the data-schema which are required by the friend-Role.
///
private static readonly IReadOnlySet DataTablesRequiredByFriendRole = new HashSet
{
nameof(Character),
};
///
/// A dictionary which contains the names of the schemas as keys for the corresponding s.
///
private static readonly IDictionary DatabaseRoles = new Dictionary
{
{ SchemaNames.AccountData, DatabaseRole.Account },
{ SchemaNames.Configuration, DatabaseRole.Configuration },
{ SchemaNames.Guild, DatabaseRole.Guild },
{ SchemaNames.Friend, DatabaseRole.Friend },
};
///
/// Initializes a new instance of the class.
///
/// The dependencies of the generator.
/// The options.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "We are extending internals of the NpgsqlMigrationsSqlGenerator.")]
public MyNpgsqlMigrationsSqlGenerator(MigrationsSqlGeneratorDependencies dependencies, INpgsqlSingletonOptions npgsqlOptions)
: base(dependencies, npgsqlOptions)
{
if (!ConnectionConfigurator.IsInitialized)
{
ConnectionConfigurator.Initialize(new ConfigFileDatabaseConnectionStringProvider());
}
}
///
///
/// Can be overridden by database providers to build commands for the given
/// by making calls on the given .
///
///
/// Note that the default implementation of this method throws . Providers
/// must override if they are to support this kind of operation.
///
///
/// The operation.
/// The target model which may be if the operations exist without a model.
/// The command builder to use to build the commands.
protected override void Generate(EnsureSchemaOperation operation, IModel? model, MigrationCommandListBuilder builder)
{
base.Generate(operation, model, builder);
var schemaName = operation.Name;
if (DatabaseRoles.TryGetValue(schemaName, out var databaseRole))
{
var roleName = ConnectionConfigurator.GetRoleName(databaseRole);
builder
.AppendLine($"""
DO
$do$
BEGIN
IF EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '{roleName}') THEN
RAISE NOTICE 'Role "{roleName}" already exists. Skipping.';
ELSE
CREATE ROLE {roleName} WITH LOGIN PASSWORD '{ConnectionConfigurator.GetRolePassword(databaseRole)}';
END IF;
END
$do$;
""")
.EndCommand()
.AppendLine($"GRANT SELECT, UPDATE, INSERT, DELETE ON ALL TABLES IN SCHEMA {schemaName} TO GROUP {roleName};")
.EndCommand()
.AppendLine($"ALTER DEFAULT PRIVILEGES IN SCHEMA {schemaName} GRANT ALL ON TABLES TO {roleName};")
.EndCommand()
.AppendLine($"GRANT USAGE ON SCHEMA {schemaName} TO GROUP {roleName};")
.EndCommand();
}
}
///
/// Builds commands for the given by making calls on the given
/// , and then terminates the final command.
///
/// The operation.
/// The target model which may be if the operations exist without a model.
/// The command builder to use to build the commands.
protected override void Generate(DropSchemaOperation operation, IModel? model, MigrationCommandListBuilder builder)
{
base.Generate(operation, model, builder);
var schemaName = operation.Name;
if (DatabaseRoles.TryGetValue(schemaName, out var databaseRole))
{
var roleName = ConnectionConfigurator.GetRoleName(databaseRole);
builder
.AppendLine($"DROP ROLE IF EXISTS {roleName};")
.EndCommand();
}
}
///
/// Builds commands for the given by making calls on the given
/// .
///
/// The operation.
/// The target model which may be if the operations exist without a model.
/// The command builder to use to build the commands.
/// Indicates whether or not to terminate the command after generating SQL for the operation.
protected override void Generate(CreateTableOperation operation, IModel? model, MigrationCommandListBuilder builder, bool terminate = true)
{
base.Generate(operation, model, builder, terminate);
if (operation.Schema != "data")
{
return;
}
if (DataTablesRequiredByConfigRole.Contains(operation.Name))
{
var configRoleName = ConnectionConfigurator.GetRoleName(DatabaseRole.Configuration);
builder
.AppendLine($"GRANT SELECT ON TABLE data.\"{operation.Name}\" TO GROUP {configRoleName};")
.EndCommand()
.AppendLine($"GRANT USAGE ON SCHEMA data TO GROUP {configRoleName};")
.EndCommand();
}
if (DataTablesRequiredByGuildRole.Contains(operation.Name))
{
var guildRoleName = ConnectionConfigurator.GetRoleName(DatabaseRole.Guild);
builder
.AppendLine($"GRANT SELECT ON TABLE data.\"{operation.Name}\" TO GROUP {guildRoleName};")
.EndCommand()
.AppendLine($"GRANT USAGE ON SCHEMA data TO GROUP {guildRoleName};")
.EndCommand();
}
if (DataTablesRequiredByFriendRole.Contains(operation.Name))
{
var friendRoleName = ConnectionConfigurator.GetRoleName(DatabaseRole.Friend);
builder
.AppendLine($"GRANT SELECT ON TABLE data.\"{operation.Name}\" TO GROUP {friendRoleName};")
.EndCommand()
.AppendLine($"GRANT USAGE ON SCHEMA data TO GROUP {friendRoleName};")
.EndCommand();
}
}
}