From e8227e4d2d26db28ce670fd864cf3676e63e6fc9 Mon Sep 17 00:00:00 2001 From: Atralupus Date: Fri, 6 Oct 2023 03:08:19 +0900 Subject: [PATCH 1/5] Add redis, sqlite access control service Apply AccessControlService --- Lib9c | 2 +- .../Configuration.cs | 2 + NineChronicles.Headless.Executable/Program.cs | 8 +++- .../NineChronicles.Headless.csproj | 2 + .../NineChroniclesNodeService.cs | 21 ++++++++-- .../Properties/AccessControlServiceOptions.cs | 20 +++++++++ .../NineChroniclesNodeServiceProperties.cs | 6 ++- .../Services/AccessControlServiceFactory.cs | 34 +++++++++++++++ .../Services/RedisAccessControlService.cs | 22 ++++++++++ .../Services/SQLiteAccessControlService.cs | 41 +++++++++++++++++++ NineChronicles.RPC.Shared | 2 +- 11 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 NineChronicles.Headless/Properties/AccessControlServiceOptions.cs create mode 100644 NineChronicles.Headless/Services/AccessControlServiceFactory.cs create mode 100644 NineChronicles.Headless/Services/RedisAccessControlService.cs create mode 100644 NineChronicles.Headless/Services/SQLiteAccessControlService.cs diff --git a/Lib9c b/Lib9c index 0bf659679..a1d99f34e 160000 --- a/Lib9c +++ b/Lib9c @@ -1 +1 @@ -Subproject commit 0bf659679f55122846691ed4fec4cfcb76d81cda +Subproject commit a1d99f34e0739bdac08e2f9bdec5b6e274971f3b 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..1cb67a931 100644 --- a/NineChronicles.Headless.Executable/Program.cs +++ b/NineChronicles.Headless.Executable/Program.cs @@ -205,7 +205,11 @@ public async Task Run( Description = "Absolute path of \"appsettings.json\" file to provide headless configurations.")] string? configPath = "appsettings.json", [Option(Description = "Sentry DSN")] - string? sentryDsn = "", + string? sentryDsn = "", + [Option(Description = "AccessControlService Type")] + string? acsType = null, + [Option(Description = "AccessControlService ConnectionString")] + string? acsConnectionString = null, [Option(Description = "Trace sample rate for sentry")] double? sentryTraceSampleRate = null, [Ignore] CancellationToken? cancellationToken = null @@ -436,7 +440,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/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..41fcddd27 --- /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..7729cee34 --- /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 + { + private 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..62df4c795 --- /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)"; + + private 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 From b62f4dd427e1ff3404bd3d3c0614f3146e3a7899 Mon Sep 17 00:00:00 2001 From: Atralupus Date: Fri, 6 Oct 2023 17:06:30 +0900 Subject: [PATCH 2/5] feat: Introduce AccessControlCenter project Port settings fix lint Add Dockerfiles Remove arguments --- Dockerfile.ACC | 37 ++++++++ Dockerfile.ACC.amd64 | 37 ++++++++ Dockerfile.ACC.arm64v8 | 37 ++++++++ .../IMutableAccessControlService.cs | 13 +++ .../MutableAccessControlServiceFactory.cs | 33 +++++++ .../MutableRedisAccessControlService.cs | 36 +++++++ .../MutableSqliteAccessControlService.cs | 61 ++++++++++++ .../AcsService.cs | 95 +++++++++++++++++++ .../Configuration.cs | 11 +++ .../AccessControlServiceController.cs | 48 ++++++++++ ...nicles.Headless.AccessControlCenter.csproj | 32 +++++++ .../Program.cs | 32 +++++++ .../appsettings.json | 5 + NineChronicles.Headless.Executable.sln | 20 ++++ NineChronicles.Headless.Executable/Program.cs | 6 +- .../GraphQLStartupTest.cs | 2 +- .../GraphTypes/StandaloneSubscriptionTest.cs | 1 - .../Properties/AccessControlServiceOptions.cs | 2 +- .../Services/RedisAccessControlService.cs | 2 +- .../Services/SQLiteAccessControlService.cs | 2 +- 20 files changed, 502 insertions(+), 10 deletions(-) create mode 100644 Dockerfile.ACC create mode 100644 Dockerfile.ACC.amd64 create mode 100644 Dockerfile.ACC.arm64v8 create mode 100644 NineChronicles.Headless.AccessControlCenter/AccessControlService/IMutableAccessControlService.cs create mode 100644 NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableAccessControlServiceFactory.cs create mode 100644 NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableRedisAccessControlService.cs create mode 100644 NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableSqliteAccessControlService.cs create mode 100644 NineChronicles.Headless.AccessControlCenter/AcsService.cs create mode 100644 NineChronicles.Headless.AccessControlCenter/Configuration.cs create mode 100644 NineChronicles.Headless.AccessControlCenter/Controllers/AccessControlServiceController.cs create mode 100644 NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj create mode 100644 NineChronicles.Headless.AccessControlCenter/Program.cs create mode 100644 NineChronicles.Headless.AccessControlCenter/appsettings.json 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..eec152cd3 --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableRedisAccessControlService.cs @@ -0,0 +1,36 @@ +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()); + var keys = server + .Keys() + .Select(k => new Address(k.ToString())) + .ToList(); + + return keys.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..04cea4ae2 --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/AcsService.cs @@ -0,0 +1,95 @@ +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 + { + private readonly string _acsType; + private readonly string _acsConnectionString; + + public AcsService(string acsType, string acsConnectionString) + { + _acsType = acsType; + _acsConnectionString = acsConnectionString; + } + + public IHostBuilder Configure(IHostBuilder hostBuilder, int port) + { + return hostBuilder.ConfigureWebHostDefaults(builder => + { + builder.UseStartup( + x => + new RestApiStartup( + x.Configuration, + _acsType, + _acsConnectionString + ) + ); + builder.ConfigureKestrel(options => + { + options.ListenAnyIP( + port, + listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + } + ); + }); + }); + } + + internal class RestApiStartup + { + private readonly string _acsType; + private readonly string _acsConnectionString; + + public RestApiStartup( + IConfiguration configuration, + string acsType, + string acsConnectionString + ) + { + Configuration = configuration; + _acsType = acsType; + _acsConnectionString = acsConnectionString; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + + var accessControlService = MutableAccessControlServiceFactory.Create( + Enum.Parse(_acsType, true), + _acsConnectionString + ); + + 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..a48cd2312 --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/Program.cs @@ -0,0 +1,32 @@ +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("ACS_CONFIG_FILE") ?? "appsettings.json"; + + var configurationBuilder = new ConfigurationBuilder() + .AddJsonFile(configPath) + .AddEnvironmentVariables("ACS_"); + IConfiguration config = configurationBuilder.Build(); + + var acsConfig = new Configuration(); + config.Bind(acsConfig); + + var service = new AcsService( + acsConfig.AccessControlServiceType, + acsConfig.AccessControlServiceConnectionString + ); + 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/Program.cs b/NineChronicles.Headless.Executable/Program.cs index 1cb67a931..731fb0380 100644 --- a/NineChronicles.Headless.Executable/Program.cs +++ b/NineChronicles.Headless.Executable/Program.cs @@ -205,11 +205,7 @@ public async Task Run( Description = "Absolute path of \"appsettings.json\" file to provide headless configurations.")] string? configPath = "appsettings.json", [Option(Description = "Sentry DSN")] - string? sentryDsn = "", - [Option(Description = "AccessControlService Type")] - string? acsType = null, - [Option(Description = "AccessControlService ConnectionString")] - string? acsConnectionString = null, + string? sentryDsn = "", [Option(Description = "Trace sample rate for sentry")] double? sentryTraceSampleRate = null, [Ignore] CancellationToken? cancellationToken = null 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/Properties/AccessControlServiceOptions.cs b/NineChronicles.Headless/Properties/AccessControlServiceOptions.cs index 41fcddd27..3cb185f2d 100644 --- a/NineChronicles.Headless/Properties/AccessControlServiceOptions.cs +++ b/NineChronicles.Headless/Properties/AccessControlServiceOptions.cs @@ -8,7 +8,7 @@ public class AccessControlServiceOptions { [Required] public string AccessControlServiceType { get; set; } = null!; - + [Required] public string AccessControlServiceConnectionString { get; set; } = null!; diff --git a/NineChronicles.Headless/Services/RedisAccessControlService.cs b/NineChronicles.Headless/Services/RedisAccessControlService.cs index 7729cee34..b1dfb5e74 100644 --- a/NineChronicles.Headless/Services/RedisAccessControlService.cs +++ b/NineChronicles.Headless/Services/RedisAccessControlService.cs @@ -6,7 +6,7 @@ namespace NineChronicles.Headless.Services { public class RedisAccessControlService : IAccessControlService { - private IDatabase _db; + protected IDatabase _db; public RedisAccessControlService(string storageUri) { diff --git a/NineChronicles.Headless/Services/SQLiteAccessControlService.cs b/NineChronicles.Headless/Services/SQLiteAccessControlService.cs index 62df4c795..0a9c1e456 100644 --- a/NineChronicles.Headless/Services/SQLiteAccessControlService.cs +++ b/NineChronicles.Headless/Services/SQLiteAccessControlService.cs @@ -11,7 +11,7 @@ public class SQLiteAccessControlService : IAccessControlService private const string CheckAccessSql = "SELECT EXISTS(SELECT 1 FROM blocklist WHERE address=@Address)"; - private readonly string _connectionString; + protected readonly string _connectionString; public SQLiteAccessControlService(string connectionString) { From 47e440222ba59fe5b1acf2be9c7520604113b441 Mon Sep 17 00:00:00 2001 From: Atralupus Date: Mon, 16 Oct 2023 19:12:53 +0900 Subject: [PATCH 3/5] Configuration DI --- .../AcsService.cs | 39 ++++++------------- .../Program.cs | 9 ++--- 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/NineChronicles.Headless.AccessControlCenter/AcsService.cs b/NineChronicles.Headless.AccessControlCenter/AcsService.cs index 04cea4ae2..596d54fad 100644 --- a/NineChronicles.Headless.AccessControlCenter/AcsService.cs +++ b/NineChronicles.Headless.AccessControlCenter/AcsService.cs @@ -11,27 +11,18 @@ namespace NineChronicles.Headless.AccessControlCenter { public class AcsService { - private readonly string _acsType; - private readonly string _acsConnectionString; - - public AcsService(string acsType, string acsConnectionString) + public AcsService(Configuration configuration) { - _acsType = acsType; - _acsConnectionString = acsConnectionString; + Configuration = configuration; } + public Configuration Configuration { get; } + public IHostBuilder Configure(IHostBuilder hostBuilder, int port) { return hostBuilder.ConfigureWebHostDefaults(builder => { - builder.UseStartup( - x => - new RestApiStartup( - x.Configuration, - _acsType, - _acsConnectionString - ) - ); + builder.UseStartup(x => new RestApiStartup(Configuration)); builder.ConfigureKestrel(options => { options.ListenAnyIP( @@ -47,29 +38,23 @@ public IHostBuilder Configure(IHostBuilder hostBuilder, int port) internal class RestApiStartup { - private readonly string _acsType; - private readonly string _acsConnectionString; - - public RestApiStartup( - IConfiguration configuration, - string acsType, - string acsConnectionString - ) + public RestApiStartup(Configuration configuration) { Configuration = configuration; - _acsType = acsType; - _acsConnectionString = acsConnectionString; } - public IConfiguration Configuration { get; } + public Configuration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddControllers(); var accessControlService = MutableAccessControlServiceFactory.Create( - Enum.Parse(_acsType, true), - _acsConnectionString + Enum.Parse( + Configuration.AccessControlServiceType, + true + ), + Configuration.AccessControlServiceConnectionString ); services.AddSingleton(accessControlService); diff --git a/NineChronicles.Headless.AccessControlCenter/Program.cs b/NineChronicles.Headless.AccessControlCenter/Program.cs index a48cd2312..ee262e673 100644 --- a/NineChronicles.Headless.AccessControlCenter/Program.cs +++ b/NineChronicles.Headless.AccessControlCenter/Program.cs @@ -10,20 +10,17 @@ public static void Main(string[] args) { // Get configuration string configPath = - Environment.GetEnvironmentVariable("ACS_CONFIG_FILE") ?? "appsettings.json"; + Environment.GetEnvironmentVariable("ACC_CONFIG_FILE") ?? "appsettings.json"; var configurationBuilder = new ConfigurationBuilder() .AddJsonFile(configPath) - .AddEnvironmentVariables("ACS_"); + .AddEnvironmentVariables("ACC_"); IConfiguration config = configurationBuilder.Build(); var acsConfig = new Configuration(); config.Bind(acsConfig); - var service = new AcsService( - acsConfig.AccessControlServiceType, - acsConfig.AccessControlServiceConnectionString - ); + var service = new AcsService(acsConfig); var hostBuilder = service.Configure(Host.CreateDefaultBuilder(), acsConfig.Port); var host = hostBuilder.Build(); host.Run(); From a30840f92b3c522250a5ad66f3956328af43a329 Mon Sep 17 00:00:00 2001 From: Atralupus Date: Tue, 17 Oct 2023 00:29:30 +0900 Subject: [PATCH 4/5] Remove unnecessary evaluation --- .../MutableRedisAccessControlService.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableRedisAccessControlService.cs b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableRedisAccessControlService.cs index eec152cd3..89c614a0a 100644 --- a/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableRedisAccessControlService.cs +++ b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableRedisAccessControlService.cs @@ -7,7 +7,6 @@ namespace NineChronicles.Headless.AccessControlCenter.AccessControlService { public class MutableRedisAccessControlService : RedisAccessControlService, IMutableAccessControlService { - public MutableRedisAccessControlService(string storageUri) : base(storageUri) { } @@ -25,12 +24,12 @@ public void AllowAccess(Address address) public List
ListBlockedAddresses(int offset, int limit) { var server = _db.Multiplexer.GetServer(_db.Multiplexer.GetEndPoints().First()); - var keys = server + return server .Keys() .Select(k => new Address(k.ToString())) + .Skip(offset) + .Take(limit) .ToList(); - - return keys.Skip(offset).Take(limit).ToList(); } } } From 2cce89c940f4c200e61a2c2f5133a317afe38d30 Mon Sep 17 00:00:00 2001 From: Atralupus Date: Tue, 17 Oct 2023 00:31:29 +0900 Subject: [PATCH 5/5] bump lib9c --- Lib9c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib9c b/Lib9c index a1d99f34e..aa95dcffd 160000 --- a/Lib9c +++ b/Lib9c @@ -1 +1 @@ -Subproject commit a1d99f34e0739bdac08e2f9bdec5b6e274971f3b +Subproject commit aa95dcffd5f6dac64da413aa1b67bbf6a61b0be9