Skip to content

Commit

Permalink
Prevent damage by invalid migration attempt
Browse files Browse the repository at this point in the history
+ Automate test to assert that framework rejects migration if it results in an invalid serial representation of the new backend state.
+ Expand the productive implementation to conform with this constraint and also provide a specific error message for this case.
+ Also, fix some bugs in the implementation for migrations.
  • Loading branch information
Viir committed Apr 4, 2020
1 parent 722ff3e commit 52a03f1
Show file tree
Hide file tree
Showing 12 changed files with 700 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ public interface IProcessStoreReader
ReductionRecord GetReduction(byte[] reducedCompositionHash);
}

public class EmptyProcessStoreReader : IProcessStoreReader
{
public IEnumerable<byte[]> EnumerateSerializedCompositionsRecordsReverse()
{
yield break;
}

public ReductionRecord GetReduction(byte[] reducedCompositionHash) => null;
}

public class ProcessStoreInFileDirectory : ProcessStoreInFileStore
{
public ProcessStoreInFileDirectory(string directoryPath, Func<IImmutableList<string>> getCompositionLogRequestedNextFilePath)
Expand Down
127 changes: 127 additions & 0 deletions implement/PersistentProcess/PersistentProcess.Test/TestWebHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -988,6 +988,133 @@ HttpClient createClientWithAuthorizationHeader(Microsoft.AspNetCore.TestHost.Tes
}
}

[TestMethod]
public void Web_host_supporting_migrations_prevents_damaging_backend_state_with_invalid_migration()
{
const string rootPassword = "Root-Password_1234567";

var elmApp = TestSetup.GetElmAppFromExampleName("test-prevent-damage-by-migrate-webapp");

var webAppConfig = new WebAppConfiguration().WithElmApp(elmApp);

var webAppConfigZipArchive = ZipArchive.ZipArchiveFromEntries(webAppConfig.AsFiles());

var migrateElmAppZipArchive = ZipArchive.ZipArchiveFromEntries(elmApp);

Func<IWebHostBuilder, IWebHostBuilder> webHostBuilderMap =
builder => builder.WithSettingAdminRootPassword(rootPassword);

HttpClient createClientWithAuthorizationHeader(Microsoft.AspNetCore.TestHost.TestServer server)
{
var adminClient = server.CreateClient();

adminClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Basic",
Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(
WebHost.Configuration.BasicAuthenticationForAdminRoot(rootPassword))));

return adminClient;
}

using (var testSetup = WebHostSupportingMigrationsTestSetup.Setup(webHostBuilderMap))
{
using (var server = testSetup.BuildServer())
{
using (var adminClient = createClientWithAuthorizationHeader(server))
{
var setAppConfigResponse = adminClient.PostAsync(
StartupSupportingMigrations.PathApiSetAppConfigAndInitState,
new ByteArrayContent(webAppConfigZipArchive)).Result;

Assert.IsTrue(setAppConfigResponse.IsSuccessStatusCode, "set-app response IsSuccessStatusCode");
}

var stateToTriggerInvalidMigration =
@"{""attemptSetMaybeStringOnMigration"":true,""maybeString"":{""Nothing"":[]},""otherState"":""""}";

using (var client = testSetup.BuildPublicAppHttpClient())
{
var httpResponse =
client.PostAsync("", new StringContent(stateToTriggerInvalidMigration, System.Text.Encoding.UTF8)).Result;

Assert.IsTrue(
httpResponse.IsSuccessStatusCode,
"Set state httpResponse.IsSuccessStatusCode (" + httpResponse.Content?.ReadAsStringAsync().Result + ")");
}

using (var client = testSetup.BuildPublicAppHttpClient())
{
var httpResponse = client.GetAsync("").Result;

Assert.AreEqual(
httpResponse.Content?.ReadAsStringAsync().Result,
stateToTriggerInvalidMigration,
"Get same state back.");
}

using (var adminClient = createClientWithAuthorizationHeader(server))
{
var migrateHttpResponse = adminClient.PostAsync(
StartupSupportingMigrations.PathApiMigrateElmState,
new ByteArrayContent(migrateElmAppZipArchive)).Result;

Assert.AreEqual(
System.Net.HttpStatusCode.BadRequest,
migrateHttpResponse.StatusCode,
"migrate-elm-state response status code is BadRequest");

Assert.IsTrue(
(migrateHttpResponse.Content?.ReadAsStringAsync().Result ?? "").Contains("Failed to load the migrated serialized state"),
"HTTP response content contains matching message");
}

using (var client = testSetup.BuildPublicAppHttpClient())
{
var httpResponse = client.GetAsync("").Result;

Assert.AreEqual(
stateToTriggerInvalidMigration,
httpResponse.Content?.ReadAsStringAsync().Result,
"Get same state back after attempted migration.");
}

var stateNotTriggeringInvalidMigration =
@"{""attemptSetMaybeStringOnMigration"":false,""maybeString"":{""Nothing"":[]},""otherState"":""sometext""}";

using (var client = testSetup.BuildPublicAppHttpClient())
{
var httpResponse =
client.PostAsync("", new StringContent(stateNotTriggeringInvalidMigration, System.Text.Encoding.UTF8)).Result;

Assert.IsTrue(
httpResponse.IsSuccessStatusCode,
"Set state httpResponse.IsSuccessStatusCode (" + httpResponse.Content?.ReadAsStringAsync().Result + ")");
}

using (var adminClient = createClientWithAuthorizationHeader(server))
{
var migrateHttpResponse = adminClient.PostAsync(
StartupSupportingMigrations.PathApiMigrateElmState,
new ByteArrayContent(migrateElmAppZipArchive)).Result;

Assert.IsTrue(
migrateHttpResponse.IsSuccessStatusCode,
"migrateHttpResponse.IsSuccessStatusCode (" + migrateHttpResponse.Content?.ReadAsStringAsync().Result + ")");
}

using (var client = testSetup.BuildPublicAppHttpClient())
{
var httpResponse = client.GetAsync("").Result;

Assert.AreEqual(
stateNotTriggeringInvalidMigration.Replace("sometext", "sometext8"),
httpResponse.Content?.ReadAsStringAsync().Result,
"Get expected state from public app, reflecting the mapping coded in the Elm migration code.");
}
}
}
}

class FileStoreFromDelegates : IFileStore
{
readonly Action<IImmutableList<string>, byte[]> setFileContent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,16 +283,15 @@ byte[] getPublicAppConfigFileFromStore() =>

var migrateElmAppConfigZipArchive = memoryStream.ToArray();

var prepareMigrateResult = PrepareMigrateSerializedValueWithoutChangingElmType(migrateElmAppConfigZipArchive);
var prepareMigrateResult = PrepareMigrateSerializedValueWithoutChangingElmType(
migrateElmAppConfigZipArchive,
publicAppConfigZipArchive: getPublicAppConfigFileFromStore());

if (prepareMigrateResult?.Ok == null)
{
if (publicAppHost == null)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Failed to prepare migration with this Elm app:\n" + prepareMigrateResult?.Err);
return;
}
context.Response.StatusCode = 400;
await context.Response.WriteAsync("Failed to prepare migration with this Elm app:\n" + prepareMigrateResult?.Err);
return;
}

if (publicAppHost == null)
Expand Down Expand Up @@ -351,7 +350,8 @@ class Result<ErrT, OkT>
}

static Result<string, Func<string, Result<string, string>>> PrepareMigrateSerializedValueWithoutChangingElmType(
byte[] migrateElmAppZipArchive)
byte[] migrateElmAppZipArchive,
byte[] publicAppConfigZipArchive)
{
var migrateElmAppOriginalFiles =
ElmApp.ToFlatDictionaryWithPathComparer(
Expand All @@ -364,8 +364,7 @@ static Result<string, Func<string, Result<string, string>>> PrepareMigrateSerial
var pathToInterfaceModuleFile = ElmApp.FilePathFromModuleName(MigrationElmAppInterfaceModuleName);
var pathToCompilationRootModuleFile = ElmApp.FilePathFromModuleName(MigrationElmAppCompilationRootModuleName);

var migrateElmAppInterfaceModuleOriginalFile =
migrateElmAppOriginalFiles[pathToInterfaceModuleFile];
migrateElmAppOriginalFiles.TryGetValue(pathToInterfaceModuleFile, out var migrateElmAppInterfaceModuleOriginalFile);

if (migrateElmAppInterfaceModuleOriginalFile == null)
return new Result<string, Func<string, Result<string, string>>>
Expand Down Expand Up @@ -415,7 +414,7 @@ import Json.Decode
import Json.Encode
decodeMigrateAndEncode : " + stateTypeCanonicalName + @" -> Result String " + stateTypeCanonicalName + @"
decodeMigrateAndEncode : String -> Result String String
decodeMigrateAndEncode =
Json.Decode.decodeString jsonDecodeBackendState
>> Result.map (" + MigrationElmAppInterfaceModuleName + "." + MigrateElmFunctionNameInModule + @" >> jsonEncodeBackendState >> Json.Encode.encode 0)
Expand Down Expand Up @@ -534,6 +533,31 @@ Ok valueToEncodeOk ->
};
}

var publicAppConfigElmApp =
ElmApp.ToFlatDictionaryWithPathComparer(
WebAppConfiguration.FromFiles(
ZipArchive.EntriesFromZipArchive(publicAppConfigZipArchive)
.Select(entry =>
(path: (IImmutableList<string>)entry.name.Split(new[] { '/', '\\' }).ToImmutableList(),
content: (IImmutableList<byte>)entry.content.ToImmutableList())
).ToImmutableList()).ElmAppFiles);

using (var testProcess = new PersistentProcessWithHistoryOnFileFromElm019Code(
new EmptyProcessStoreReader(),
publicAppConfigElmApp,
logger: logEntry => { }))
{
testProcess.SetState(elmAppStateMigratedSerialized);

var resultingState = testProcess.ReductionRecordForCurrentState()?.ReducedValueLiteralString;

if (resultingState != elmAppStateMigratedSerialized)
return new Result<string, string>
{
Err = "Failed to load the migrated serialized state with the current public app configuration. resulting State:\n" + resultingState
};
}

return new Result<string, string>
{
Ok = elmAppStateMigratedSerialized
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/elm-stuff/

*.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/core": "1.0.0",
"elm/json": "1.0.0"
},
"indirect": {
"elm/time": "1.0.0",
"elm/url": "1.0.0"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}
Loading

0 comments on commit 52a03f1

Please sign in to comment.