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 55 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
43 changes: 43 additions & 0 deletions packages/app-runtime/src/AppRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,13 +293,56 @@ export class AppRuntime extends Runtime<AppConfig> {
return Promise.resolve();
}

public override async start(): Promise<void> {
await super.start();

await this.startAccounts();
}

public override async stop(): Promise<void> {
const logError = (e: any) => this.logger.error(e);

await super.stop().catch(logError);
await this.lokiConnection.close().catch(logError);
}

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

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

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.checkIfIdentityIsDeleted();

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

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

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

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.offboardAccount(CoreId.from(id));
}

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

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

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

public async deleteAccount(id: CoreId): Promise<void> {
delete this._openAccounts[id.toString()];

await this.databaseConnection.deleteDatabase(`acc-${id.toString()}`);
await this._localAccounts.delete({ id: id.toString() });
Expand Down
28 changes: 28 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,29 @@ 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.getBackboneVersion();
const appsettingsOverrideLocation = process.env.APPSETTINGS_OVERRIDE_LOCATION ?? `${__dirname}/../../../../.dev/appsettings.override.json`;

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

private static getBackboneVersion() {
if (process.env.BACKBONE_VERSION) return process.env.BACKBONE_VERSION;

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["BACKBONE_VERSION"];
}
}
38 changes: 35 additions & 3 deletions packages/app-runtime/test/runtime/Offboarding.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { IdentityDeletionProcessStatus } from "@nmshd/runtime";
import { AppRuntime, AppRuntimeServices } from "../../src";
import { TestUtil } from "../lib";

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

let services1: AppRuntimeServices;
let services2: AppRuntimeServices;
let localAccount2Id: string;
let device2Id: string;

Expand All @@ -14,7 +16,9 @@ describe("Offboarding", function () {
const configOverride = { allowMultipleAccountsWithSameAddress: true };
runtime = await TestUtil.createRuntime(configOverride);
await runtime.start();
});

beforeEach(async function () {
const [localAccount1] = await TestUtil.provideAccounts(runtime, 1);
services1 = await runtime.getServices(localAccount1.id);

Expand All @@ -25,7 +29,7 @@ describe("Offboarding", function () {

const localAccount2 = await runtime.accountServices.onboardAccount(onboardingInfoResult.value);
localAccount2Id = localAccount2.id;
const services2 = await runtime.getServices(localAccount2.id);
services2 = await runtime.getServices(localAccount2.id);

await services2.transportServices.account.syncDatawallet();
await services1.transportServices.account.syncDatawallet();
Expand All @@ -35,8 +39,36 @@ describe("Offboarding", function () {
await runtime.stop();
});

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

const accounts = await runtime.accountServices.getAccounts();
expect(accounts).toHaveLength(1);

const devicesResult = await services1.transportServices.devices.getDevices();
const filteredDevice = devicesResult.value.find((d) => d.id === device2Id);

expect(filteredDevice).toBeDefined();
expect(filteredDevice!.isOffboarded).toBe(true);

const deviceResult = await services1.transportServices.devices.getDevice({ id: device2Id });
const device = deviceResult.value;

expect(device.isOffboarded).toBe(true);

await expect(runtime.getServices(localAccount2Id)).rejects.toThrow("error.transport.recordNotFound");
await expect(runtime.selectAccount(localAccount2Id)).rejects.toThrow("error.transport.recordNotFound");
});

test("offboard Account for Identity within grace period of IdentityDeletionProcess", async function () {
await services1.transportServices.identityDeletionProcesses.initiateIdentityDeletionProcess();
await services2.transportServices.account.syncDatawallet();

const identityDeletionProcessOnSecondAccount = (await services2.transportServices.identityDeletionProcesses.getActiveIdentityDeletionProcess()).value;
expect(identityDeletionProcessOnSecondAccount.status).toStrictEqual(IdentityDeletionProcessStatus.Approved);

await runtime.accountServices.offboardAccount(localAccount2Id);
await services1.transportServices.account.syncDatawallet();

const accounts = await runtime.accountServices.getAccounts();
Expand Down
39 changes: 38 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,40 @@ describe("Runtime Startup", function () {
expect(selectedAccount.account.id.toString()).toBe(localAccount.id.toString());
});
});

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

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

beforeEach(async function () {
const accounts = await TestUtil.provideAccounts(runtime, 1);
session = await runtime.selectAccount(accounts[0].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(session.account.id)).resolves.not.toThrow();
});

test("should delete Account running startAccounts for an Identity with expired grace period", async function () {
await session.transportServices.identityDeletionProcesses["initiateIdentityDeletionProcessUseCase"]["identityDeletionProcessController"].initiateIdentityDeletionProcess(0);

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

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

await runtime["startAccounts"]();
await expect(runtime.selectAccount(session.account.id)).rejects.toThrow("error.transport.recordNotFound");
});
});
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 {
CheckIfIdentityIsDeletedResponse,
CheckIfIdentityIsDeletedUseCase,
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 checkIfIdentityIsDeletedUseCase: CheckIfIdentityIsDeletedUseCase
) {}

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 checkIfIdentityIsDeleted(): Promise<Result<CheckIfIdentityIsDeletedResponse, ApplicationError>> {
return await this.checkIfIdentityIsDeletedUseCase.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 CheckIfIdentityIsDeletedResponse {
isDeleted: boolean;
deletionDate?: string;
}

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

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

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 "./CheckIfIdentityIsDeleted";
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("CheckIfIdentityIsDeleted", () => {
test("check deletion of Identity that is not deleted", async () => {
const result = await sTransportServices.account.checkIfIdentityIsDeleted();
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.checkIfIdentityIsDeleted();
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 checkIfIdentityIsDeleted(): Promise<
Result<{
isDeleted: boolean;
deletionDate?: string;
}>
> {
const currentDeviceCredentials = await this.parent.activeDevice.getCredentials();
const identityDeletionResult = await this.identityClient.checkIfIdentityIsDeleted(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 BackboneCheckIfIdentityIsDeletedResponse {
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 { BackboneCheckIfIdentityIsDeletedResponse } from "./BackboneCheckIfIdentityIsDeleted";
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 checkIfIdentityIsDeleted(username: string): Promise<ClientResult<BackboneCheckIfIdentityIsDeletedResponse>> {
return await this.get<BackboneCheckIfIdentityIsDeletedResponse>(`/api/v1/Identities/IsDeleted?username=${username}`);
}
}
Loading
Loading