Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delete obsolete LocalAccounts on App startup #356

Merged
merged 56 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
d14a967
feat: add logic to start of runtime
Milena-Czierlinski Nov 20, 2024
8684bc4
feat: add backbone route
Milena-Czierlinski Nov 21, 2024
5690096
refactor: rename checkIdentityDeletionForUsername
Milena-Czierlinski Nov 27, 2024
2bedc22
feat: add checkIdentityDeletionForUsername use case
Milena-Czierlinski Nov 27, 2024
6c5ec80
Merge branch 'main' into local-data-deletion
Milena-Czierlinski Nov 28, 2024
fc4f3d8
feat: get username in transport
Milena-Czierlinski Nov 28, 2024
5b94813
refactor: rename to checkDeletionOfIdentity
Milena-Czierlinski Nov 28, 2024
f0a035f
test: identitycontroller
Milena-Czierlinski Nov 28, 2024
cfe7f8c
fix: rename consistently
Milena-Czierlinski Nov 28, 2024
5aac68f
Merge branch 'main' into local-data-deletion
Milena-Czierlinski Nov 29, 2024
fc389f5
test: add runtime test
Milena-Czierlinski Nov 29, 2024
0542ab3
feat: continue startApp
Milena-Czierlinski Nov 29, 2024
14bfbd9
Merge remote-tracking branch 'origin/main' into local-data-deletion
Milena-Czierlinski Nov 29, 2024
424d042
feat: delete accounts if identities are deleted
Milena-Czierlinski Dec 2, 2024
b6e06d2
feat: bump backbone
Milena-Czierlinski Dec 2, 2024
bed355e
Merge remote-tracking branch 'origin/main' into local-data-deletion
Milena-Czierlinski Dec 2, 2024
6fa867d
fix: provide config for updated backbone
Milena-Czierlinski Dec 2, 2024
01813a6
feat: add IdentityDeletionInfo object
Milena-Czierlinski Dec 2, 2024
9fd7749
test: add transport test for passed grace period
Milena-Czierlinski Dec 3, 2024
a9c4344
refactor: errors
Milena-Czierlinski Dec 3, 2024
3970100
Revert "feat: add IdentityDeletionInfo object"
Milena-Czierlinski Dec 3, 2024
314c16c
test: add runtime test for expired grace period
Milena-Czierlinski Dec 3, 2024
61437cd
refactor: naming
Milena-Czierlinski Dec 3, 2024
9b9c651
refactor: rename startAccounts
Milena-Czierlinski Dec 3, 2024
37f6a17
Merge remote-tracking branch 'origin/main' into local-data-deletion
Milena-Czierlinski Dec 3, 2024
adc81ce
Merge branch 'main' into local-data-deletion
jkoenig134 Dec 4, 2024
60f3e22
feat: configure gracePeriod initializing IdentityDeletionProcess
Milena-Czierlinski Dec 5, 2024
ee0b905
feat: allow to manually run deletion job on backbone
Milena-Czierlinski Dec 5, 2024
d23e65e
test: run deletion job in skipped test
Milena-Czierlinski Dec 5, 2024
73cec8b
Merge remote-tracking branch 'origin/main' into local-data-deletion
Milena-Czierlinski Dec 5, 2024
1be4ed4
test: deleted Identity
Milena-Czierlinski Dec 9, 2024
073d8d9
Merge remote-tracking branch 'origin/main' into local-data-deletion
Milena-Czierlinski Dec 9, 2024
d3d5d5a
feat: log error
Milena-Czierlinski Dec 9, 2024
f874c59
refactor: backbone response can be null
Milena-Czierlinski Dec 9, 2024
b9f1749
Merge remote-tracking branch 'origin/main' into local-data-deletion
Milena-Czierlinski Dec 10, 2024
5671fd3
feat: check for deleted Identities; wip
Milena-Czierlinski Dec 11, 2024
6772fbe
Merge remote-tracking branch 'origin/main' into local-data-deletion
Milena-Czierlinski Dec 11, 2024
7e3b130
Merge remote-tracking branch 'origin/main' into local-data-deletion
Milena-Czierlinski Dec 12, 2024
5993ec9
fix: return early for error other than noAuthGrant
Milena-Czierlinski Dec 12, 2024
822a2eb
feat: rename offoardDevice
Milena-Czierlinski Dec 13, 2024
c08f8a0
test: accounts of deleted identities are remove on app startup
Milena-Czierlinski Dec 13, 2024
a34d756
refactor: rename offboardDevice on AccountService
Milena-Czierlinski Dec 13, 2024
00df598
feat: add deleteAccount to AccountServices
Milena-Czierlinski Dec 13, 2024
1581da0
fix: app-runtime tests
Milena-Czierlinski Dec 13, 2024
4a48ccf
Merge remote-tracking branch 'origin/main' into local-data-deletion
Milena-Czierlinski Dec 13, 2024
698caf3
refactor: rename offboardAccount
Milena-Czierlinski Dec 13, 2024
1274612
chore: simplify
jkoenig134 Dec 13, 2024
c5c59e8
chore: simplify
jkoenig134 Dec 13, 2024
e095de2
Merge branch 'local-data-deletion' of github.com:nmshd/runtime into l…
Milena-Czierlinski Dec 13, 2024
7bcd234
refactor: rename checkIfIdentityIsDeleted
Milena-Czierlinski Dec 13, 2024
ea4ec44
test: make tests independent
Milena-Czierlinski Dec 13, 2024
ef05861
test: offboard account of identity with ongoing grace period
Milena-Czierlinski Dec 13, 2024
77c5815
test: adjust runDeletionJob
Milena-Czierlinski Dec 13, 2024
e526ce7
chore: package-lock
Milena-Czierlinski Dec 13, 2024
1979373
test: rename
Milena-Czierlinski Dec 13, 2024
be734fc
fix: make more tests independent
Milena-Czierlinski Dec 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,098 changes: 1,366 additions & 1,732 deletions package-lock.json

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions packages/app-runtime/src/AppRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,42 @@ export class AppRuntime extends Runtime<AppConfig> {
await this.lokiConnection.close().catch(logError);
}

protected override async startAccounts(): Promise<void> {
const accounts = await this._multiAccountController.getAccounts();

for (const account of accounts) {
const session = await this.selectAccount(account.id.toString());

const syncResult = await session.transportServices.account.syncDatawallet();
if (syncResult.isError) this.logger.error(syncResult.error);

session.accountController.authenticator.clear();
try {
await session.accountController.authenticator.getToken();
continue;
} catch (error) {
this.logger.error(error);

if (!(typeof error === "object" && error !== null && "code" in error)) {
continue;
}

if (!(error.code === "error.transport.request.noAuthGrant")) continue;
}

const checkDeletionResult = await session.transportServices.account.checkDeletionOfIdentity();

if (checkDeletionResult.isError) {
this.logger.error(checkDeletionResult.error);
continue;
}

if (checkDeletionResult.value.isDeleted) {
await this._multiAccountController.deleteAccount(account.id);
}
}
}

private translationProvider: INativeTranslationProvider = {
translate: (key: string) => Promise.resolve(Result.ok(key))
};
Expand Down
4 changes: 4 additions & 0 deletions packages/app-runtime/src/multiAccount/AccountServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export class AccountServices {
return LocalAccountMapper.toLocalAccountDTO(localAccount);
}

public async offboardAccount(id: string): Promise<void> {
await this.multiAccountController.offboardDevice(CoreId.from(id));
}

public async deleteAccount(id: string): Promise<void> {
await this.multiAccountController.deleteAccount(CoreId.from(id));
}
Expand Down
10 changes: 8 additions & 2 deletions packages/app-runtime/src/multiAccount/MultiAccountController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,18 @@ export class MultiAccountController {
return [localAccount, accountController];
}

public async deleteAccount(id: CoreId): Promise<void> {
const [localAccount, accountController] = await this.selectAccount(id);
public async offboardDevice(id: CoreId): Promise<void> {
const accountController = (await this.selectAccount(id))[1];
await accountController.unregisterPushNotificationToken();
await accountController.activeDevice.markAsOffboarded();
await accountController.close();

await this.deleteAccount(id);
}

public async deleteAccount(id: CoreId): Promise<void> {
const localAccount = (await this.selectAccount(id))[0];

delete this._openAccounts[localAccount.id.toString()];

await this.databaseConnection.deleteDatabase(`acc-${id.toString()}`);
Expand Down
25 changes: 25 additions & 0 deletions packages/app-runtime/test/lib/TestUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import {
SyncEverythingResponse
} from "@nmshd/runtime";
import { IConfigOverwrite, TransportLoggerFactory } from "@nmshd/transport";
import fs from "fs";
import path from "path";
import { GenericContainer, Wait } from "testcontainers";
import { LogLevel } from "typescript-logging";
import { AppConfig, AppRuntime, IUIBridge, LocalAccountDTO, LocalAccountSession, createAppConfig as runtime_createAppConfig } from "../../src";
import { FakeUIBridge } from "./FakeUIBridge";
Expand Down Expand Up @@ -262,4 +265,26 @@ export class TestUtil {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
expect(result.isSuccess, `${result.error?.code} | ${result.error?.message}`).toBe(true);
}

public static async runDeletionJob(): Promise<void> {
const backboneVersion = this.getBackboneEnvVar("BACKBONE_VERSION");

await new GenericContainer(`ghcr.io/nmshd/backbone-identity-deletion-jobs:${backboneVersion}`)
.withWaitStrategy(Wait.forOneShotStartup())
.withCommand(["--Worker", "ActualDeletionWorker"])
.withNetworkMode("backbone")
.withCopyFilesToContainer([{ source: `${__dirname}/../../../../.dev/appsettings.override.json`, target: "/app/appsettings.override.json" }])
.start();
}

private static getBackboneEnvVar(name: string) {
const envFile = fs.readFileSync(path.resolve(`${__dirname}/../../../../.dev/compose.backbone.env`));
const env = envFile
.toString()
.split("\n")
.map((line) => line.split("="))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {} as Record<string, string>);

return env[name];
}
}
4 changes: 2 additions & 2 deletions packages/app-runtime/test/runtime/Offboarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ describe("Offboarding", function () {
await runtime.stop();
});

test("delete account", async function () {
await runtime.accountServices.deleteAccount(localAccount2Id);
test("offboard Account", async function () {
await runtime.accountServices.offboardAccount(localAccount2Id);
await services1.transportServices.account.syncDatawallet();

const accounts = await runtime.accountServices.getAccounts();
Expand Down
45 changes: 44 additions & 1 deletion packages/app-runtime/test/runtime/Startup.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AppRuntime, LocalAccountDTO } from "../../src";
import { AppRuntime, LocalAccountDTO, LocalAccountSession } from "../../src";
import { EventListener, TestUtil } from "../lib";

describe("Runtime Startup", function () {
Expand Down Expand Up @@ -64,3 +64,46 @@ describe("Runtime Startup", function () {
expect(selectedAccount.account.id.toString()).toBe(localAccount.id.toString());
});
});

describe("Start Accounts", function () {
let runtime: AppRuntime;

let sessionA: LocalAccountSession;
let sessionB: LocalAccountSession;

beforeAll(async function () {
runtime = await TestUtil.createRuntime();
await runtime.start();

const accounts = await TestUtil.provideAccounts(runtime, 2);
sessionA = await runtime.selectAccount(accounts[0].id);
sessionB = await runtime.selectAccount(accounts[1].id);
});

afterAll(async () => await runtime.stop());

test("should not delete Account running startAccounts for an active Identity", async function () {
await runtime["startAccounts"]();
await expect(runtime.selectAccount(sessionA.account.id)).resolves.not.toThrow();
});

// TODO: Jest did not exit one second after the test run has completed.
test("should delete Account running startAccounts for an Identity with expired grace period", async function () {
await sessionA.transportServices.identityDeletionProcesses["initiateIdentityDeletionProcessUseCase"]["identityDeletionProcessController"].initiateIdentityDeletionProcess(
0
);

await runtime["startAccounts"]();
await expect(runtime.selectAccount(sessionA.account.id)).rejects.toThrow("error.transport.recordNotFound");
});

test("should delete Account running startAccounts for a deleted Identity", async function () {
await sessionB.transportServices.identityDeletionProcesses["initiateIdentityDeletionProcessUseCase"]["identityDeletionProcessController"].initiateIdentityDeletionProcess(
0
);
await TestUtil.runDeletionJob();

await runtime["startAccounts"]();
await expect(runtime.selectAccount(sessionB.account.id)).rejects.toThrow("error.transport.recordNotFound");
});
});
5 changes: 5 additions & 0 deletions packages/runtime/src/Runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ export abstract class Runtime<TConfig extends RuntimeConfig = RuntimeConfig> {

await this.startInfrastructure();
await this.startModules();
await this.startAccounts();

this._isStarted = true;
}
Expand Down Expand Up @@ -478,6 +479,10 @@ export abstract class Runtime<TConfig extends RuntimeConfig = RuntimeConfig> {
this.logger.info("Started all modules.");
}

protected startAccounts(): void | Promise<void> {
return;
}

protected getModuleName(moduleConfiguration: ModuleConfiguration | RuntimeModule): string {
return moduleConfiguration.displayName || moduleConfiguration.name || JSON.stringify(moduleConfiguration);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { ApplicationError, Result } from "@js-soft/ts-utils";
import { Inject } from "@nmshd/typescript-ioc";
import { DeviceDTO } from "../../../types";
import {
CheckDeletionOfIdentityResponse,
CheckDeletionOfIdentityUseCase,
DisableAutoSyncUseCase,
EnableAutoSyncUseCase,
GetDeviceInfoUseCase,
Expand Down Expand Up @@ -32,7 +34,8 @@ export class AccountFacade {
@Inject private readonly getSyncInfoUseCase: GetSyncInfoUseCase,
@Inject private readonly disableAutoSyncUseCase: DisableAutoSyncUseCase,
@Inject private readonly enableAutoSyncUseCase: EnableAutoSyncUseCase,
@Inject private readonly loadItemFromTruncatedReferenceUseCase: LoadItemFromTruncatedReferenceUseCase
@Inject private readonly loadItemFromTruncatedReferenceUseCase: LoadItemFromTruncatedReferenceUseCase,
@Inject private readonly checkDeletionOfIdentityUseCase: CheckDeletionOfIdentityUseCase
) {}

public async getIdentityInfo(): Promise<Result<GetIdentityInfoResponse, ApplicationError>> {
Expand Down Expand Up @@ -74,4 +77,8 @@ export class AccountFacade {
public async loadItemFromTruncatedReference(request: LoadItemFromTruncatedReferenceRequest): Promise<Result<LoadItemFromTruncatedReferenceResponse, ApplicationError>> {
return await this.loadItemFromTruncatedReferenceUseCase.execute(request);
}

public async checkDeletionOfIdentity(): Promise<Result<CheckDeletionOfIdentityResponse, ApplicationError>> {
return await this.checkDeletionOfIdentityUseCase.execute();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Result } from "@js-soft/ts-utils";
import { IdentityController } from "@nmshd/transport";
import { Inject } from "@nmshd/typescript-ioc";
import { UseCase } from "../../common";

export interface CheckDeletionOfIdentityResponse {
isDeleted: boolean;
deletionDate?: string;
}

export class CheckDeletionOfIdentityUseCase extends UseCase<void, CheckDeletionOfIdentityResponse> {
public constructor(@Inject private readonly identityController: IdentityController) {
super();
}

protected async executeInternal(): Promise<Result<CheckDeletionOfIdentityResponse>> {
const result = await this.identityController.checkDeletionOfIdentity();

if (result.isError) return Result.fail(result.error);

return Result.ok(result.value);
}
}
1 change: 1 addition & 0 deletions packages/runtime/src/useCases/transport/account/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./CheckDeletionOfIdentity";
export * from "./DisableAutoSync";
export * from "./EnableAutoSync";
export * from "./GetDeviceInfo";
Expand Down
19 changes: 19 additions & 0 deletions packages/runtime/test/transport/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,22 @@ describe("Un-/RegisterPushNotificationToken", () => {
expect(result).toBeSuccessful();
});
});

describe("CheckDeletionOfIdentity", () => {
test("check deletion of Identity that is not deleted", async () => {
const result = await sTransportServices.account.checkDeletionOfIdentity();
expect(result.isSuccess).toBe(true);
expect(result.value.isDeleted).toBe(false);
expect(result.value.deletionDate).toBeUndefined();
});

test("check deletion of Identity that has IdentityDeletionProcess with expired grace period", async () => {
const identityDeletionProcess =
await sTransportServices.identityDeletionProcesses["initiateIdentityDeletionProcessUseCase"]["identityDeletionProcessController"].initiateIdentityDeletionProcess(0);

const result = await sTransportServices.account.checkDeletionOfIdentity();
expect(result.isSuccess).toBe(true);
expect(result.value.isDeleted).toBe(true);
expect(result.value.deletionDate).toBe(identityDeletionProcess.cache!.gracePeriodEndsAt!.toString());
});
});
24 changes: 23 additions & 1 deletion packages/transport/src/modules/accounts/IdentityController.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { log } from "@js-soft/ts-utils";
import { log, Result } from "@js-soft/ts-utils";
import { CoreAddress } from "@nmshd/core-types";
import { CoreBuffer, CryptoSignature, CryptoSignaturePrivateKey, CryptoSignaturePublicKey } from "@nmshd/crypto";
import { ControllerName, CoreCrypto, TransportController, TransportCoreErrors } from "../../core";
import { AccountController } from "../accounts/AccountController";
import { DeviceSecretType } from "../devices/DeviceSecretController";
import { IdentityClient } from "./backbone/IdentityClient";
import { Identity } from "./data/Identity";

export class IdentityController extends TransportController {
public identityClient: IdentityClient;

public get address(): CoreAddress {
return this._identity.address;
}
Expand All @@ -22,6 +25,8 @@ export class IdentityController extends TransportController {

public constructor(parent: AccountController) {
super(ControllerName.Identity, parent);

this.identityClient = new IdentityClient(this.config, this.transport.correlator);
}

@log()
Expand Down Expand Up @@ -57,4 +62,21 @@ export class IdentityController extends TransportController {
const valid = await CoreCrypto.verify(content, signature, this.publicKey);
return valid;
}

public async checkDeletionOfIdentity(): Promise<
Result<{
isDeleted: boolean;
deletionDate?: string;
}>
> {
const currentDeviceCredentials = await this.parent.activeDevice.getCredentials();
const identityDeletionResult = await this.identityClient.checkDeletionOfIdentity(currentDeviceCredentials.username);

if (identityDeletionResult.isError) return Result.fail(identityDeletionResult.error);

return Result.ok({
isDeleted: identityDeletionResult.value.isDeleted,
deletionDate: identityDeletionResult.value.deletionDate ?? undefined
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface BackboneCheckDeletionOfIdentityResponse {
isDeleted: boolean;
deletionDate: string | null;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RESTClient, RESTClientLogDirective } from "../../../core";
import { ClientResult } from "../../../core/backbone/ClientResult";
import { BackboneCheckDeletionOfIdentityResponse } from "./BackboneCheckDeletionOfIdentity";
import { BackbonePostIdentityRequest, BackbonePostIdentityResponse } from "./BackbonePostIdentity";

export class IdentityClient extends RESTClient {
Expand All @@ -8,4 +9,8 @@ export class IdentityClient extends RESTClient {
public async createIdentity(value: BackbonePostIdentityRequest): Promise<ClientResult<BackbonePostIdentityResponse>> {
return await this.post<BackbonePostIdentityResponse>("/api/v1/Identities", value);
}

public async checkDeletionOfIdentity(username: string): Promise<ClientResult<BackboneCheckDeletionOfIdentityResponse>> {
return await this.get<BackboneCheckDeletionOfIdentityResponse>(`/api/v1/Identities/IsDeleted?username=${username}`);
}
}
51 changes: 51 additions & 0 deletions packages/transport/test/modules/account/IdentityController.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { IDatabaseConnection } from "@js-soft/docdb-access-abstractions";
import { AccountController, Transport } from "../../../src";
import { TestUtil } from "../../testHelpers/TestUtil";

describe("IdentityController", function () {
let connection: IDatabaseConnection;
let transport: Transport;
let account1: AccountController;
let account2: AccountController;

beforeAll(async function () {
connection = await TestUtil.createDatabaseConnection();
transport = TestUtil.createTransport(connection);

await transport.init();

[account1, account2] = await TestUtil.provideAccounts(transport, 2);
await account1.init();
await account2.init();
});

afterAll(async function () {
await account1.close();
await account2.close();
await connection.close();
});

test("should return Identity is not deleted for active Identity", async function () {
const result = await account1.identity.checkDeletionOfIdentity();
expect(result.value.isDeleted).toBe(false);
expect(result.value.deletionDate).toBeUndefined();
});

test("should return gracePeriodEndsAt for Identity having IdentityDeletionProcess with expired grace period", async function () {
const identityDeletionProcess = await account1.identityDeletionProcess.initiateIdentityDeletionProcess(0);

const result = await account1.identity.checkDeletionOfIdentity();
expect(result.value.isDeleted).toBe(true);
expect(result.value.deletionDate).toBe(identityDeletionProcess.cache!.gracePeriodEndsAt!.toString());
});

test("should return actual deletionDate for Identity that is deleted", async function () {
const identityDeletionProcess = await account2.identityDeletionProcess.initiateIdentityDeletionProcess(0);
await TestUtil.runDeletionJob();

const result = await account2.identity.checkDeletionOfIdentity();
expect(result.value.isDeleted).toBe(true);
expect(result.value.deletionDate).toBeDefined();
expect(result.value.deletionDate).not.toBe(identityDeletionProcess.cache!.gracePeriodEndsAt!.toString());
});
});
Loading