// // 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(); } } }