diff --git a/Dockerfile.ACC b/Dockerfile.ACC new file mode 100644 index 000000000..40861ddc3 --- /dev/null +++ b/Dockerfile.ACC @@ -0,0 +1,37 @@ +# Use the SDK image to build the app +FROM mcr.microsoft.com/dotnet/sdk:6.0-jammy AS build-env +WORKDIR /app +ARG COMMIT + +# Copy csproj and restore as distinct layers +COPY ./Lib9c/Lib9c/Lib9c.csproj ./Lib9c/ +COPY ./NineChronicles.Headless/NineChronicles.Headless.csproj ./NineChronicles.Headless/ +COPY ./NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj ./NineChronicles.Headless.AccessControlCenter/ +RUN dotnet restore Lib9c +RUN dotnet restore NineChronicles.Headless +RUN dotnet restore NineChronicles.Headless.AccessControlCenter + +# Copy everything else and build +COPY . ./ +RUN dotnet publish NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj \ + -c Release \ + -r linux-x64 \ + -o out \ + --self-contained \ + --version-suffix $COMMIT + +# Build runtime image +FROM mcr.microsoft.com/dotnet/aspnet:6.0 +WORKDIR /app +RUN apt-get update && apt-get install -y libc6-dev +COPY --from=build-env /app/out . + +# Install native deps & utilities for production +RUN apt-get update \ + && apt-get install -y --allow-unauthenticated \ + libc6-dev jq curl \ + && rm -rf /var/lib/apt/lists/* + +VOLUME /data + +ENTRYPOINT ["dotnet", "NineChronicles.Headless.AccessControlCenter.dll"] diff --git a/Dockerfile.ACC.amd64 b/Dockerfile.ACC.amd64 new file mode 100644 index 000000000..eeb7814d7 --- /dev/null +++ b/Dockerfile.ACC.amd64 @@ -0,0 +1,37 @@ +# Use the SDK image to build the app +FROM mcr.microsoft.com/dotnet/sdk:6.0-jammy AS build-env +WORKDIR /app +ARG COMMIT + +# Copy csproj and restore as distinct layers +COPY ./Lib9c/Lib9c/Lib9c.csproj ./Lib9c/ +COPY ./NineChronicles.Headless/NineChronicles.Headless.csproj ./NineChronicles.Headless/ +COPY ./NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj ./NineChronicles.Headless.AccessControlCenter/ +RUN dotnet restore Lib9c +RUN dotnet restore NineChronicles.Headless +RUN dotnet restore NineChronicles.Headless.AccessControlCenter + +# Copy everything else and build +COPY . ./ +RUN dotnet publish NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj \ + -c Release \ + -r linux-x64 \ + -o out \ + --self-contained \ + --version-suffix $COMMIT + +# Build runtime image +FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim +WORKDIR /app +RUN apt-get update && apt-get install -y libc6-dev +COPY --from=build-env /app/out . + +# Install native deps & utilities for production +RUN apt-get update \ + && apt-get install -y --allow-unauthenticated \ + libc6-dev jq curl \ + && rm -rf /var/lib/apt/lists/* + +VOLUME /data + +ENTRYPOINT ["dotnet", "NineChronicles.Headless.AccessControlCenter.dll"] diff --git a/Dockerfile.ACC.arm64v8 b/Dockerfile.ACC.arm64v8 new file mode 100644 index 000000000..510a04d02 --- /dev/null +++ b/Dockerfile.ACC.arm64v8 @@ -0,0 +1,37 @@ +# Use the SDK image to build the app +FROM mcr.microsoft.com/dotnet/sdk:6.0-jammy AS build-env +WORKDIR /app +ARG COMMIT + +# Copy csproj and restore as distinct layers +COPY ./Lib9c/Lib9c/Lib9c.csproj ./Lib9c/ +COPY ./NineChronicles.Headless/NineChronicles.Headless.csproj ./NineChronicles.Headless/ +COPY ./NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj ./NineChronicles.Headless.AccessControlCenter/ +RUN dotnet restore Lib9c +RUN dotnet restore NineChronicles.Headless +RUN dotnet restore NineChronicles.Headless.AccessControlCenter + +# Copy everything else and build +COPY . ./ +RUN dotnet publish NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj \ + -c Release \ + -r linux-arm64 \ + -o out \ + --self-contained \ + --version-suffix $COMMIT + +# Build runtime image +FROM --platform=linux/arm64 mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim-arm64v8 +WORKDIR /app +RUN apt-get update && apt-get install -y libc6-dev +COPY --from=build-env /app/out . + +# Install native deps & utilities for production +RUN apt-get update \ + && apt-get install -y --allow-unauthenticated \ + libc6-dev jq curl \ + && rm -rf /var/lib/apt/lists/* + +VOLUME /data + +ENTRYPOINT ["dotnet", "NineChronicles.Headless.AccessControlCenter.dll"] diff --git a/NineChronicles.Headless.AccessControlCenter/AccessControlService/IMutableAccessControlService.cs b/NineChronicles.Headless.AccessControlCenter/AccessControlService/IMutableAccessControlService.cs new file mode 100644 index 000000000..aa8207174 --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/AccessControlService/IMutableAccessControlService.cs @@ -0,0 +1,13 @@ +using Libplanet.Crypto; +using System.Collections.Generic; +using Nekoyume.Blockchain; + +namespace NineChronicles.Headless.AccessControlCenter.AccessControlService +{ + public interface IMutableAccessControlService : IAccessControlService + { + void DenyAccess(Address address); + void AllowAccess(Address address); + List
ListBlockedAddresses(int offset, int limit); + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableAccessControlServiceFactory.cs b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableAccessControlServiceFactory.cs new file mode 100644 index 000000000..f9f98ac68 --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableAccessControlServiceFactory.cs @@ -0,0 +1,33 @@ +using System; + +namespace NineChronicles.Headless.AccessControlCenter.AccessControlService +{ + public static class MutableAccessControlServiceFactory + { + public enum StorageType + { + /// + /// Use Redis + /// + Redis, + + /// + /// Use SQLite + /// + SQLite + } + + public static IMutableAccessControlService Create( + StorageType storageType, + string connectionString + ) + { + return storageType switch + { + StorageType.Redis => new MutableRedisAccessControlService(connectionString), + StorageType.SQLite => new MutableSqliteAccessControlService(connectionString), + _ => throw new ArgumentOutOfRangeException(nameof(storageType), storageType, null) + }; + } + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableRedisAccessControlService.cs b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableRedisAccessControlService.cs new file mode 100644 index 000000000..89c614a0a --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableRedisAccessControlService.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using Libplanet.Crypto; +using NineChronicles.Headless.Services; + +namespace NineChronicles.Headless.AccessControlCenter.AccessControlService +{ + public class MutableRedisAccessControlService : RedisAccessControlService, IMutableAccessControlService + { + public MutableRedisAccessControlService(string storageUri) : base(storageUri) + { + } + + public void DenyAccess(Address address) + { + _db.StringSet(address.ToString(), "denied"); + } + + public void AllowAccess(Address address) + { + _db.KeyDelete(address.ToString()); + } + + public List
ListBlockedAddresses(int offset, int limit) + { + var server = _db.Multiplexer.GetServer(_db.Multiplexer.GetEndPoints().First()); + return server + .Keys() + .Select(k => new Address(k.ToString())) + .Skip(offset) + .Take(limit) + .ToList(); + } + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableSqliteAccessControlService.cs b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableSqliteAccessControlService.cs new file mode 100644 index 000000000..1d9455118 --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableSqliteAccessControlService.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using Microsoft.Data.Sqlite; +using Libplanet.Crypto; +using NineChronicles.Headless.Services; + +namespace NineChronicles.Headless.AccessControlCenter.AccessControlService +{ + public class MutableSqliteAccessControlService : SQLiteAccessControlService, IMutableAccessControlService + { + private const string DenyAccessSql = + "INSERT OR IGNORE INTO blocklist (address) VALUES (@Address)"; + private const string AllowAccessSql = "DELETE FROM blocklist WHERE address=@Address"; + + public MutableSqliteAccessControlService(string connectionString) : base(connectionString) + { + } + + public void DenyAccess(Address address) + { + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = DenyAccessSql; + command.Parameters.AddWithValue("@Address", address.ToString()); + command.ExecuteNonQuery(); + } + + public void AllowAccess(Address address) + { + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = AllowAccessSql; + command.Parameters.AddWithValue("@Address", address.ToString()); + command.ExecuteNonQuery(); + } + + public List
ListBlockedAddresses(int offset, int limit) + { + var blockedAddresses = new List
(); + + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = $"SELECT address FROM blocklist LIMIT @Limit OFFSET @Offset"; + command.Parameters.AddWithValue("@Limit", limit); + command.Parameters.AddWithValue("@Offset", offset); + + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + blockedAddresses.Add(new Address(reader.GetString(0))); + } + + return blockedAddresses; + } + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/AcsService.cs b/NineChronicles.Headless.AccessControlCenter/AcsService.cs new file mode 100644 index 000000000..596d54fad --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/AcsService.cs @@ -0,0 +1,80 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using NineChronicles.Headless.AccessControlCenter.AccessControlService; + +namespace NineChronicles.Headless.AccessControlCenter +{ + public class AcsService + { + public AcsService(Configuration configuration) + { + Configuration = configuration; + } + + public Configuration Configuration { get; } + + public IHostBuilder Configure(IHostBuilder hostBuilder, int port) + { + return hostBuilder.ConfigureWebHostDefaults(builder => + { + builder.UseStartup(x => new RestApiStartup(Configuration)); + builder.ConfigureKestrel(options => + { + options.ListenAnyIP( + port, + listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + } + ); + }); + }); + } + + internal class RestApiStartup + { + public RestApiStartup(Configuration configuration) + { + Configuration = configuration; + } + + public Configuration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + + var accessControlService = MutableAccessControlServiceFactory.Create( + Enum.Parse( + Configuration.AccessControlServiceType, + true + ), + Configuration.AccessControlServiceConnectionString + ); + + services.AddSingleton(accessControlService); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseRouting(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/Configuration.cs b/NineChronicles.Headless.AccessControlCenter/Configuration.cs new file mode 100644 index 000000000..22ca923df --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/Configuration.cs @@ -0,0 +1,11 @@ +namespace NineChronicles.Headless.AccessControlCenter +{ + public class Configuration + { + public int Port { get; set; } + + public string AccessControlServiceType { get; set; } = null!; + + public string AccessControlServiceConnectionString { get; set; } = null!; + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/Controllers/AccessControlServiceController.cs b/NineChronicles.Headless.AccessControlCenter/Controllers/AccessControlServiceController.cs new file mode 100644 index 000000000..685dca35e --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/Controllers/AccessControlServiceController.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using NineChronicles.Headless.AccessControlCenter.AccessControlService; +using System.Linq; +using Libplanet.Crypto; + +namespace NineChronicles.Headless.AccessControlCenter.Controllers +{ + [ApiController] + public class AccessControlServiceController : ControllerBase + { + private readonly IMutableAccessControlService _accessControlService; + + public AccessControlServiceController(IMutableAccessControlService accessControlService) + { + _accessControlService = accessControlService; + } + + [HttpGet("entries/{address}")] + public ActionResult IsAccessDenied(string address) + { + return _accessControlService.IsAccessDenied(new Address(address)); + } + + [HttpPost("entries/{address}/deny")] + public ActionResult DenyAccess(string address) + { + _accessControlService.DenyAccess(new Address(address)); + return Ok(); + } + + [HttpPost("entries/{address}/allow")] + public ActionResult AllowAccess(string address) + { + _accessControlService.AllowAccess(new Address(address)); + return Ok(); + } + + [HttpGet("entries")] + public ActionResult> ListBlockedAddresses(int offset, int limit) + { + return _accessControlService + .ListBlockedAddresses(offset, limit) + .Select(a => a.ToString()) + .ToList(); + } + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj b/NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj new file mode 100644 index 000000000..74be55c3b --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj @@ -0,0 +1,32 @@ + + + net6 + true + ..\NineChronicles.Headless.Common.ruleset + enable + Debug;Release;DevEx + AnyCPU + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/NineChronicles.Headless.AccessControlCenter/Program.cs b/NineChronicles.Headless.AccessControlCenter/Program.cs new file mode 100644 index 000000000..ee262e673 --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/Program.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace NineChronicles.Headless.AccessControlCenter +{ + public static class Program + { + public static void Main(string[] args) + { + // Get configuration + string configPath = + Environment.GetEnvironmentVariable("ACC_CONFIG_FILE") ?? "appsettings.json"; + + var configurationBuilder = new ConfigurationBuilder() + .AddJsonFile(configPath) + .AddEnvironmentVariables("ACC_"); + IConfiguration config = configurationBuilder.Build(); + + var acsConfig = new Configuration(); + config.Bind(acsConfig); + + var service = new AcsService(acsConfig); + var hostBuilder = service.Configure(Host.CreateDefaultBuilder(), acsConfig.Port); + var host = hostBuilder.Build(); + host.Run(); + } + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/appsettings.json b/NineChronicles.Headless.AccessControlCenter/appsettings.json new file mode 100644 index 000000000..fbf1f5da2 --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/appsettings.json @@ -0,0 +1,5 @@ +{ + "Port": "31259", + "AccessControlServiceType": "redis", + "AccessControlServiceConnectionString": "localhost:6379" +} diff --git a/NineChronicles.Headless.Executable.sln b/NineChronicles.Headless.Executable.sln index 03b81380f..b14931789 100644 --- a/NineChronicles.Headless.Executable.sln +++ b/NineChronicles.Headless.Executable.sln @@ -78,6 +78,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Extensions.Remote EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Extensions.RemoteBlockChainStates", "Lib9c\.Libplanet.Extensions.RemoteBlockChainStates\Libplanet.Extensions.RemoteBlockChainStates.csproj", "{8F9E5505-C157-4DF3-A419-FF0108731397}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NineChronicles.Headless.AccessControlCenter", "NineChronicles.Headless.AccessControlCenter\NineChronicles.Headless.AccessControlCenter.csproj", "{162C0F4B-A1D9-4132-BC34-31F1247BC26B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -703,6 +705,24 @@ Global {8F9E5505-C157-4DF3-A419-FF0108731397}.Release|x64.Build.0 = Release|Any CPU {8F9E5505-C157-4DF3-A419-FF0108731397}.Release|x86.ActiveCfg = Release|Any CPU {8F9E5505-C157-4DF3-A419-FF0108731397}.Release|x86.Build.0 = Release|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Debug|x64.ActiveCfg = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Debug|x64.Build.0 = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Debug|x86.ActiveCfg = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Debug|x86.Build.0 = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.DevEx|Any CPU.ActiveCfg = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.DevEx|Any CPU.Build.0 = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.DevEx|x64.ActiveCfg = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.DevEx|x64.Build.0 = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.DevEx|x86.ActiveCfg = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.DevEx|x86.Build.0 = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Release|Any CPU.Build.0 = Release|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Release|x64.ActiveCfg = Release|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Release|x64.Build.0 = Release|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Release|x86.ActiveCfg = Release|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/NineChronicles.Headless.Executable/Configuration.cs b/NineChronicles.Headless.Executable/Configuration.cs index edba40765..28b2d40cc 100644 --- a/NineChronicles.Headless.Executable/Configuration.cs +++ b/NineChronicles.Headless.Executable/Configuration.cs @@ -89,6 +89,8 @@ public class Configuration public StateServiceManagerServiceOptions? StateServiceManagerService { get; set; } + public AccessControlServiceOptions? AccessControlService { get; set; } + public void Overwrite( string? appProtocolVersionString, string[]? trustedAppProtocolVersionSignerStrings, diff --git a/NineChronicles.Headless.Executable/Program.cs b/NineChronicles.Headless.Executable/Program.cs index 7db80836c..731fb0380 100644 --- a/NineChronicles.Headless.Executable/Program.cs +++ b/NineChronicles.Headless.Executable/Program.cs @@ -436,7 +436,7 @@ IActionLoader MakeSingleActionLoader() : new PrivateKey(ByteUtil.ParseHex(headlessConfig.MinerPrivateKeyString)); TimeSpan minerBlockInterval = TimeSpan.FromMilliseconds(headlessConfig.MinerBlockIntervalMilliseconds); var nineChroniclesProperties = - new NineChroniclesNodeServiceProperties(actionLoader, headlessConfig.StateServiceManagerService) + new NineChroniclesNodeServiceProperties(actionLoader, headlessConfig.StateServiceManagerService, headlessConfig.AccessControlService) { MinerPrivateKey = minerPrivateKey, Libplanet = properties, diff --git a/NineChronicles.Headless.Tests/GraphQLStartupTest.cs b/NineChronicles.Headless.Tests/GraphQLStartupTest.cs index f582243e0..c64ea79d0 100644 --- a/NineChronicles.Headless.Tests/GraphQLStartupTest.cs +++ b/NineChronicles.Headless.Tests/GraphQLStartupTest.cs @@ -4,8 +4,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using static NineChronicles.Headless.Tests.GraphQLTestUtils; using Xunit; +using static NineChronicles.Headless.Tests.GraphQLTestUtils; namespace NineChronicles.Headless.Tests { diff --git a/NineChronicles.Headless.Tests/GraphTypes/StandaloneSubscriptionTest.cs b/NineChronicles.Headless.Tests/GraphTypes/StandaloneSubscriptionTest.cs index 419ec81f7..c441f7d8b 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/StandaloneSubscriptionTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/StandaloneSubscriptionTest.cs @@ -54,7 +54,6 @@ public async Task SubscribeTipChangedEvent() BlockChain.Append(block, GenerateBlockCommit(block.Index, block.Hash, GenesisValidators)); // var data = (Dictionary)((ExecutionNode) result.Data!).ToValue()!; - Assert.Equal(index, BlockChain.Tip.Index); await Task.Delay(TimeSpan.FromSeconds(1)); diff --git a/NineChronicles.Headless/NineChronicles.Headless.csproj b/NineChronicles.Headless/NineChronicles.Headless.csproj index 27814de54..6bd8f6ce0 100644 --- a/NineChronicles.Headless/NineChronicles.Headless.csproj +++ b/NineChronicles.Headless/NineChronicles.Headless.csproj @@ -39,7 +39,9 @@ + + diff --git a/NineChronicles.Headless/NineChroniclesNodeService.cs b/NineChronicles.Headless/NineChroniclesNodeService.cs index b265edf87..181f14e6e 100644 --- a/NineChronicles.Headless/NineChroniclesNodeService.cs +++ b/NineChronicles.Headless/NineChroniclesNodeService.cs @@ -20,6 +20,7 @@ using Nekoyume.Blockchain.Policy; using NineChronicles.Headless.Properties; using NineChronicles.Headless.Utils; +using NineChronicles.Headless.Services; using NineChronicles.RPC.Shared.Exceptions; using Nito.AsyncEx; using Serilog; @@ -78,14 +79,27 @@ public NineChroniclesNodeService( bool ignorePreloadFailure = false, bool strictRendering = false, TimeSpan txLifeTime = default, - int txQuotaPerSigner = 10 + int txQuotaPerSigner = 10, + AccessControlServiceOptions? acsOptions = null ) { MinerPrivateKey = minerPrivateKey; Properties = properties; LogEventLevel logLevel = LogEventLevel.Debug; - IStagePolicy stagePolicy = new NCStagePolicy(txLifeTime, txQuotaPerSigner); + + IAccessControlService? accessControlService = null; + + if (acsOptions != null) + { + accessControlService = AccessControlServiceFactory.Create( + acsOptions.GetStorageType(), + acsOptions.AccessControlServiceConnectionString + ); + } + + IStagePolicy stagePolicy = new NCStagePolicy( + txLifeTime, txQuotaPerSigner, accessControlService); BlockRenderer = new BlockRenderer(); ActionRenderer = new ActionRenderer(); @@ -201,7 +215,8 @@ StandaloneContext context ignorePreloadFailure: properties.IgnorePreloadFailure, strictRendering: properties.StrictRender, txLifeTime: properties.TxLifeTime, - txQuotaPerSigner: properties.TxQuotaPerSigner + txQuotaPerSigner: properties.TxQuotaPerSigner, + acsOptions: properties.AccessControlServiceOptions ); service.ConfigureContext(context); var meter = new Meter("NineChronicles"); diff --git a/NineChronicles.Headless/Properties/AccessControlServiceOptions.cs b/NineChronicles.Headless/Properties/AccessControlServiceOptions.cs new file mode 100644 index 000000000..3cb185f2d --- /dev/null +++ b/NineChronicles.Headless/Properties/AccessControlServiceOptions.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel.DataAnnotations; +using NineChronicles.Headless.Services; + +namespace NineChronicles.Headless.Properties +{ + public class AccessControlServiceOptions + { + [Required] + public string AccessControlServiceType { get; set; } = null!; + + [Required] + public string AccessControlServiceConnectionString { get; set; } = null!; + + public AccessControlServiceFactory.StorageType GetStorageType() + { + return Enum.Parse(AccessControlServiceType, true); + } + } +} diff --git a/NineChronicles.Headless/Properties/NineChroniclesNodeServiceProperties.cs b/NineChronicles.Headless/Properties/NineChroniclesNodeServiceProperties.cs index 38a47a8a4..b69914881 100644 --- a/NineChronicles.Headless/Properties/NineChroniclesNodeServiceProperties.cs +++ b/NineChronicles.Headless/Properties/NineChroniclesNodeServiceProperties.cs @@ -12,10 +12,12 @@ namespace NineChronicles.Headless.Properties { public class NineChroniclesNodeServiceProperties { - public NineChroniclesNodeServiceProperties(IActionLoader actionLoader, StateServiceManagerServiceOptions? stateServiceManagerServiceOptions) + public NineChroniclesNodeServiceProperties( + IActionLoader actionLoader, StateServiceManagerServiceOptions? stateServiceManagerServiceOptions, AccessControlServiceOptions? accessControlServiceOptions) { ActionLoader = actionLoader; StateServiceManagerService = stateServiceManagerServiceOptions; + AccessControlServiceOptions = accessControlServiceOptions; } /// @@ -54,6 +56,8 @@ public NineChroniclesNodeServiceProperties(IActionLoader actionLoader, StateServ public StateServiceManagerServiceOptions? StateServiceManagerService { get; } + public AccessControlServiceOptions? AccessControlServiceOptions { get; } + public static LibplanetNodeServiceProperties GenerateLibplanetNodeServiceProperties( string? appProtocolVersionToken = null, diff --git a/NineChronicles.Headless/Services/AccessControlServiceFactory.cs b/NineChronicles.Headless/Services/AccessControlServiceFactory.cs new file mode 100644 index 000000000..0ff8e476a --- /dev/null +++ b/NineChronicles.Headless/Services/AccessControlServiceFactory.cs @@ -0,0 +1,34 @@ +using System; +using Nekoyume.Blockchain; + +namespace NineChronicles.Headless.Services +{ + public static class AccessControlServiceFactory + { + public enum StorageType + { + /// + /// Use Redis + /// + Redis, + + /// + /// Use SQLite + /// + SQLite + } + + public static IAccessControlService Create( + StorageType storageType, + string connectionString + ) + { + return storageType switch + { + StorageType.Redis => new RedisAccessControlService(connectionString), + StorageType.SQLite => new SQLiteAccessControlService(connectionString), + _ => throw new ArgumentOutOfRangeException(nameof(storageType), storageType, null) + }; + } + } +} diff --git a/NineChronicles.Headless/Services/RedisAccessControlService.cs b/NineChronicles.Headless/Services/RedisAccessControlService.cs new file mode 100644 index 000000000..b1dfb5e74 --- /dev/null +++ b/NineChronicles.Headless/Services/RedisAccessControlService.cs @@ -0,0 +1,22 @@ +using StackExchange.Redis; +using Libplanet.Crypto; +using Nekoyume.Blockchain; + +namespace NineChronicles.Headless.Services +{ + public class RedisAccessControlService : IAccessControlService + { + protected IDatabase _db; + + public RedisAccessControlService(string storageUri) + { + var redis = ConnectionMultiplexer.Connect(storageUri); + _db = redis.GetDatabase(); + } + + public bool IsAccessDenied(Address address) + { + return _db.KeyExists(address.ToString()); + } + } +} diff --git a/NineChronicles.Headless/Services/SQLiteAccessControlService.cs b/NineChronicles.Headless/Services/SQLiteAccessControlService.cs new file mode 100644 index 000000000..0a9c1e456 --- /dev/null +++ b/NineChronicles.Headless/Services/SQLiteAccessControlService.cs @@ -0,0 +1,41 @@ +using Microsoft.Data.Sqlite; +using Libplanet.Crypto; +using Nekoyume.Blockchain; + +namespace NineChronicles.Headless.Services +{ + public class SQLiteAccessControlService : IAccessControlService + { + private const string CreateTableSql = + "CREATE TABLE IF NOT EXISTS blocklist (address VARCHAR(42))"; + private const string CheckAccessSql = + "SELECT EXISTS(SELECT 1 FROM blocklist WHERE address=@Address)"; + + protected readonly string _connectionString; + + public SQLiteAccessControlService(string connectionString) + { + _connectionString = connectionString; + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = CreateTableSql; + command.ExecuteNonQuery(); + } + + public bool IsAccessDenied(Address address) + { + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = CheckAccessSql; + command.Parameters.AddWithValue("@Address", address.ToString()); + + var result = command.ExecuteScalar(); + + return result is not null && (long)result == 1; + } + } +} diff --git a/NineChronicles.RPC.Shared b/NineChronicles.RPC.Shared index 2dcbbb19a..cff68421f 160000 --- a/NineChronicles.RPC.Shared +++ b/NineChronicles.RPC.Shared @@ -1 +1 @@ -Subproject commit 2dcbbb19a0c90f3f41de03506802bbd527c0aaba +Subproject commit cff68421fabb098f3d0bfd20fdef55755db309e4