Skip to content
Snippets Groups Projects
Commit ca60712d authored by Steffen Schulze's avatar Steffen Schulze
Browse files

Merge branch 'multitenancy' into 'main'

feat(ssi): multitenancy support

See merge request eclipse/xfsc/ocm/ocm-engine!12
parents f9c6aaac d25cec02
No related branches found
No related tags found
No related merge requests found
Showing
with 294 additions and 168 deletions
......@@ -22,6 +22,7 @@
},
"dependencies": {
"@aries-framework/core": "0.4.2",
"@aries-framework/tenants": "^0.4.2",
"@elastic/ecs-winston-format": "^1.5.0",
"@nestjs/common": "^10.2.10",
"@nestjs/microservices": "^10.2.10",
......@@ -32,10 +33,10 @@
"winston": "^3.11.0"
},
"devDependencies": {
"@types/jest": "^29.5.9",
"@types/node": "^20.9.3",
"@nestjs/cli": "^10.2.1",
"@nestjs/testing": "^10.2.10",
"@types/jest": "^29.5.9",
"@types/node": "^20.9.3",
"rimraf": "^5.0.5",
"supertest": "^6.1.3",
"ts-jest": "^29.1.1",
......
......@@ -6,7 +6,7 @@ describe('Base Events', () => {
});
it('should create a new base event', () => {
const baseEvent = new BaseEvent({ some: 'data' });
const baseEvent = new BaseEvent({ some: 'data' }, 'tenantId');
expect(typeof baseEvent.id).toStrictEqual('string');
expect(baseEvent.type).toStrictEqual('BaseEvent');
......
......@@ -17,7 +17,7 @@ describe('Connection Events', () => {
});
it('should create a new connections get all event', () => {
const event = new EventDidcommConnectionsGetAll([]);
const event = new EventDidcommConnectionsGetAll([], 'tenantId');
expect(typeof event.id).toStrictEqual('string');
expect(event.type).toStrictEqual('EventDidcommConnectionsGetAll');
......@@ -26,7 +26,7 @@ describe('Connection Events', () => {
});
it('should create a new connections get by id event', () => {
const event = new EventDidcommConnectionsGetById(null);
const event = new EventDidcommConnectionsGetById(null, 'tenantId');
expect(typeof event.id).toStrictEqual('string');
expect(event.type).toStrictEqual('EventDidcommConnectionsGetById');
......@@ -40,6 +40,7 @@ describe('Connection Events', () => {
role: DidExchangeRole.Requester,
state: DidExchangeState.Completed,
}),
'tenantId',
);
expect(typeof event.id).toStrictEqual('string');
......@@ -57,6 +58,7 @@ describe('Connection Events', () => {
role: DidExchangeRole.Requester,
state: DidExchangeState.Completed,
}),
'tenantId',
);
expect(typeof event.id).toStrictEqual('string');
......
import { DidDocument } from '@aries-framework/core';
import { EventDidsPublicDid, EventDidsResolve } from '../didEvents.js';
describe('Did Events', () => {
it('should return module', () => {
jest.requireActual('../didEvents');
});
it('should create get public did event', () => {
const doc = new DidDocument({ id: 'did:web:123.com' });
const event = new EventDidsPublicDid(doc, 'tenantId');
expect(typeof event.id).toStrictEqual('string');
expect(event.type).toStrictEqual('EventDidsPublicDid');
expect(event.timestamp).toBeInstanceOf(Date);
expect(event.instance).toMatchObject(doc);
});
it('should create did resolve event', () => {
const doc = new DidDocument({ id: 'did:my:id' });
const event = new EventDidsResolve(doc, 'tenantId');
expect(typeof event.id).toStrictEqual('string');
expect(event.type).toStrictEqual('EventDidsResolve');
expect(event.timestamp).toBeInstanceOf(Date);
expect(event.instance).toMatchObject(doc);
});
});
import { TenantRecord } from '@aries-framework/tenants';
import { EventTenantsCreate } from '../tenantEvents.js';
describe('Tenant Events', () => {
it('should return module', () => {
jest.requireActual('../tenantEvents');
});
it('should create a create tenant event', () => {
const tenantRecord = new TenantRecord({
config: {
label: 'my-label',
walletConfig: { id: 'some-id', key: 'some-key' },
},
});
const event = new EventTenantsCreate(tenantRecord, undefined);
expect(typeof event.id).toStrictEqual('string');
expect(event.type).toStrictEqual('EventTenantsCreate');
expect(event.timestamp).toBeInstanceOf(Date);
expect(event.instance).toMatchObject(tenantRecord);
});
});
import { utils } from '@aries-framework/core';
export class BaseEvent<T = Record<string, unknown>> {
export class BaseEvent<T = Record<string, unknown>, TenantIdType = string> {
public readonly id: string;
public readonly type: string;
public readonly timestamp: Date;
public readonly data: T;
public readonly tenantId: TenantIdType;
public constructor(
data: T,
tenantId: TenantIdType,
id?: string,
type?: string,
timestamp?: Date,
) {
this.data = data;
this.tenantId = tenantId;
public constructor(data: T, id?: string, type?: string, timestamp?: Date) {
this.id = id ?? utils.uuid();
this.type = type ?? this.constructor.name;
this.timestamp = timestamp ?? new Date();
this.data = data;
}
}
export type BaseEventInput<
T extends Record<string, unknown> = Record<string, unknown>,
TenantIdType extends undefined | string = string,
> = TenantIdType extends string ? { tenantId: string } & T : T;
import type { BaseEventInput } from './baseEvents.js';
import { ConnectionRecord, JsonTransformer } from '@aries-framework/core';
import { BaseEvent } from './baseEvents.js';
export type EventDidcommConnectionsGetAllInput = BaseEventInput;
export class EventDidcommConnectionsGetAll extends BaseEvent<
Array<ConnectionRecord>
> {
......@@ -12,10 +15,19 @@ export class EventDidcommConnectionsGetAll extends BaseEvent<
}
public static fromEvent(e: EventDidcommConnectionsGetAll) {
return new EventDidcommConnectionsGetAll(e.data, e.id, e.type, e.timestamp);
return new EventDidcommConnectionsGetAll(
e.data,
e.tenantId,
e.id,
e.type,
e.timestamp,
);
}
}
export type EventDidcommConnectionsGetByIdInput = BaseEventInput<{
id: string;
}>;
export class EventDidcommConnectionsGetById extends BaseEvent<ConnectionRecord | null> {
public static token = 'didcomm.connections.getById';
......@@ -28,6 +40,7 @@ export class EventDidcommConnectionsGetById extends BaseEvent<ConnectionRecord |
public static fromEvent(e: EventDidcommConnectionsGetById) {
return new EventDidcommConnectionsGetById(
e.data,
e.tenantId,
e.id,
e.type,
e.timestamp,
......@@ -35,6 +48,7 @@ export class EventDidcommConnectionsGetById extends BaseEvent<ConnectionRecord |
}
}
export type EventDidcommConnectionsCreateWithSelfInput = BaseEventInput;
export class EventDidcommConnectionsCreateWithSelf extends BaseEvent<ConnectionRecord> {
public static token = 'didcomm.connections.createWithSelf';
......@@ -45,6 +59,7 @@ export class EventDidcommConnectionsCreateWithSelf extends BaseEvent<ConnectionR
public static fromEvent(e: EventDidcommConnectionsCreateWithSelf) {
return new EventDidcommConnectionsCreateWithSelf(
e.data,
e.tenantId,
e.id,
e.type,
e.timestamp,
......@@ -52,6 +67,9 @@ export class EventDidcommConnectionsCreateWithSelf extends BaseEvent<ConnectionR
}
}
export type EventDidcommConnectionsBlockInput = BaseEventInput<{
idOrDid: string;
}>;
export class EventDidcommConnectionsBlock extends BaseEvent<ConnectionRecord | null> {
public static token = 'didcomm.connections.block';
......@@ -62,6 +80,12 @@ export class EventDidcommConnectionsBlock extends BaseEvent<ConnectionRecord | n
}
public static fromEvent(e: EventDidcommConnectionsBlock) {
return new EventDidcommConnectionsBlock(e.data, e.id, e.type, e.timestamp);
return new EventDidcommConnectionsBlock(
e.data,
e.tenantId,
e.id,
e.type,
e.timestamp,
);
}
}
import type { BaseEventInput } from './baseEvents.js';
import { DidDocument, JsonTransformer } from '@aries-framework/core';
import { BaseEvent } from './baseEvents.js';
......@@ -7,18 +9,31 @@ import { BaseEvent } from './baseEvents.js';
* @todo: this should be removed as it is a weird event that should not be needed
*
*/
export class EventInfoPublicDid extends BaseEvent<DidDocument> {
export type EventDidsPublicDidInput = BaseEventInput;
/**
*
* @todo: this should be removed as it is a weird event that should not be needed
*
*/
export class EventDidsPublicDid extends BaseEvent<DidDocument> {
public static token = 'dids.publicDid';
public get instance() {
return JsonTransformer.fromJSON(this.data, DidDocument);
}
public static fromEvent(e: EventInfoPublicDid) {
return new EventInfoPublicDid(e.data, e.id, e.type, e.timestamp);
public static fromEvent(e: EventDidsPublicDid) {
return new EventDidsPublicDid(
e.data,
e.tenantId,
e.id,
e.type,
e.timestamp,
);
}
}
export type EventDidsResolveInput = BaseEventInput<{ did: string }>;
export class EventDidsResolve extends BaseEvent<DidDocument> {
public static token = 'dids.resolve';
......@@ -27,6 +42,6 @@ export class EventDidsResolve extends BaseEvent<DidDocument> {
}
public static fromEvent(e: EventDidsResolve) {
return new EventDidsResolve(e.data, e.id, e.type, e.timestamp);
return new EventDidsResolve(e.data, e.tenantId, e.id, e.type, e.timestamp);
}
}
import type { BaseEventInput } from './baseEvents.js';
import { JsonTransformer } from '@aries-framework/core';
import { TenantRecord } from '@aries-framework/tenants';
import { BaseEvent } from './baseEvents.js';
export type EventTenantsCreateInput = BaseEventInput<
{ label: string },
undefined
>;
export class EventTenantsCreate extends BaseEvent<TenantRecord, undefined> {
public static token = 'tenants.create';
public get instance() {
return JsonTransformer.fromJSON(this.data, TenantRecord);
}
public static fromEvent(e: EventTenantsCreate) {
return new EventTenantsCreate(e.data, undefined, e.id, e.type, e.timestamp);
}
}
......@@ -6,3 +6,4 @@ export * from './logging/logAxiosError.js';
export * from './events/connectionEvents.js';
export * from './events/didEvents.js';
export * from './events/tenantEvents.js';
......@@ -26,6 +26,7 @@
"@aries-framework/core": "0.4.2",
"@aries-framework/indy-vdr": "0.4.2",
"@aries-framework/node": "0.4.2",
"@aries-framework/tenants": "^0.4.2",
"@elastic/ecs-winston-format": "^1.5.0",
"@hyperledger/anoncreds-nodejs": "^0.1.0",
"@hyperledger/aries-askar-nodejs": "^0.1.0",
......@@ -45,13 +46,13 @@
"winston": "^3.11.0"
},
"devDependencies": {
"@nestjs/cli": "^10.2.1",
"@nestjs/schematics": "^10.0.3",
"@nestjs/testing": "^10.2.10",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.9",
"@types/node": "^20.9.3",
"@types/supertest": "^2.0.16",
"@nestjs/cli": "^10.2.1",
"@nestjs/schematics": "^10.0.3",
"@nestjs/testing": "^10.2.10",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"eslint": "^8.54.0",
......
import { DidDocument } from '@aries-framework/core';
import { Test } from '@nestjs/testing';
import { mockConfigModule } from '../../config/__tests__/mockConfig.js';
import { AgentController } from '../agent.controller.js';
import { AgentService } from '../agent.service.js';
describe('AgentController', () => {
let agentController: AgentController;
let agentService: AgentService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [mockConfigModule()],
controllers: [AgentController],
providers: [AgentService],
}).compile();
agentService = moduleRef.get(AgentService);
agentController = moduleRef.get(AgentController);
});
describe('public did', () => {
it('should get the public did information of the agent', async () => {
const result = new DidDocument({ id: 'did:key:123' });
jest.spyOn(agentService, 'getPublicDid').mockResolvedValue(result);
const event = await agentController.publicDid();
expect(event.data).toMatchObject(result);
});
});
});
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { EventInfoPublicDid } from '@ocm/shared';
import { AgentService } from './agent.service.js';
@Controller('agent')
export class AgentController {
public constructor(private agent: AgentService) {}
@MessagePattern(EventInfoPublicDid.token)
public async publicDid() {
const didDocument = await this.agent.getPublicDid();
return new EventInfoPublicDid(didDocument);
}
}
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AgentController } from './agent.controller.js';
import { AgentService } from './agent.service.js';
import { WithTenantService } from './withTenantService.js';
@Module({
imports: [ConfigModule],
providers: [AgentService],
controllers: [AgentController],
exports: [AgentService],
providers: [AgentService, WithTenantService],
exports: [AgentService, WithTenantService],
})
export class AgentModule {}
......@@ -30,6 +30,7 @@ import {
IndyVdrSovDidResolver,
} from '@aries-framework/indy-vdr';
import { agentDependencies, HttpInboundTransport } from '@aries-framework/node';
import { TenantsModule } from '@aries-framework/tenants';
import { anoncreds } from '@hyperledger/anoncreds-nodejs';
import { ariesAskar } from '@hyperledger/aries-askar-nodejs';
import { indyVdr } from '@hyperledger/indy-vdr-nodejs';
......@@ -113,10 +114,16 @@ export class AgentService implements OnApplicationShutdown {
new JwkDidResolver(),
new WebDidResolver(),
],
registrars: [new PeerDidRegistrar(), new KeyDidRegistrar(), new JwkDidRegistrar()],
registrars: [
new PeerDidRegistrar(),
new KeyDidRegistrar(),
new JwkDidRegistrar(),
],
}),
askar: new AskarModule({ ariesAskar }),
tenants: new TenantsModule(),
};
}
......@@ -178,27 +185,6 @@ export class AgentService implements OnApplicationShutdown {
}
}
public async getPublicDid() {
const dids = await this.agent.dids.getCreatedDids({ method: 'indy' });
if (dids.length === 0) {
throw new Error('No registered public DIDs');
}
if (dids.length > 1) {
throw new Error('Multiple public DIDs found');
}
const didRecord = dids[0];
if (!didRecord.didDocument) {
throw new Error(
'A public DID was found, but did not include a DID Document',
);
}
return didRecord.didDocument;
}
public async onModuleInit() {
await this.agent.initialize();
await this.registerPublicDid();
......
......@@ -30,7 +30,9 @@ describe('ConnectionsController', () => {
const result: Array<ConnectionRecord> = [];
jest.spyOn(connectionsService, 'getAll').mockResolvedValue(result);
const connectionsEvent = await connectionsController.getAll();
const connectionsEvent = await connectionsController.getAll({
tenantId: 'some-id',
});
expect(connectionsEvent.data).toStrictEqual(result);
});
......@@ -43,6 +45,7 @@ describe('ConnectionsController', () => {
const connectionsEvent = await connectionsController.getById({
id: 'id',
tenantId: 'some-id',
});
expect(connectionsEvent.data).toStrictEqual(result);
......@@ -61,7 +64,9 @@ describe('ConnectionsController', () => {
.mockResolvedValue(result);
const connectionsEvent =
await connectionsController.createConnectionWithSelf();
await connectionsController.createConnectionWithSelf({
tenantId: 'some-id',
});
expect(connectionsEvent.data).toStrictEqual(result);
});
......
......@@ -5,6 +5,10 @@ import {
EventDidcommConnectionsGetAll,
EventDidcommConnectionsCreateWithSelf,
EventDidcommConnectionsBlock,
EventDidcommConnectionsGetAllInput,
EventDidcommConnectionsGetByIdInput,
EventDidcommConnectionsCreateWithSelfInput,
EventDidcommConnectionsBlockInput,
} from '@ocm/shared';
import { ConnectionsService } from './connections.service.js';
......@@ -14,38 +18,42 @@ export class ConnectionsController {
public constructor(private connectionsService: ConnectionsService) {}
@MessagePattern(EventDidcommConnectionsGetAll.token)
public async getAll(): Promise<EventDidcommConnectionsGetAll> {
public async getAll(
options: EventDidcommConnectionsGetAllInput,
): Promise<EventDidcommConnectionsGetAll> {
return new EventDidcommConnectionsGetAll(
await this.connectionsService.getAll(),
await this.connectionsService.getAll(options),
options.tenantId,
);
}
@MessagePattern(EventDidcommConnectionsGetById.token)
public async getById({
id,
}: {
id: string;
}): Promise<EventDidcommConnectionsGetById> {
public async getById(
options: EventDidcommConnectionsGetByIdInput,
): Promise<EventDidcommConnectionsGetById> {
return new EventDidcommConnectionsGetById(
await this.connectionsService.getById(id),
await this.connectionsService.getById(options),
options.tenantId,
);
}
@MessagePattern(EventDidcommConnectionsCreateWithSelf.token)
public async createConnectionWithSelf(): Promise<EventDidcommConnectionsCreateWithSelf> {
public async createConnectionWithSelf(
options: EventDidcommConnectionsCreateWithSelfInput,
): Promise<EventDidcommConnectionsCreateWithSelf> {
return new EventDidcommConnectionsCreateWithSelf(
await this.connectionsService.createConnectionWithSelf(),
await this.connectionsService.createConnectionWithSelf(options),
options.tenantId,
);
}
@MessagePattern(EventDidcommConnectionsBlock.token)
public async blockConnection({
idOrDid,
}: {
idOrDid: string;
}): Promise<EventDidcommConnectionsBlock> {
public async blockConnection(
options: EventDidcommConnectionsBlockInput,
): Promise<EventDidcommConnectionsBlock> {
return new EventDidcommConnectionsBlock(
await this.connectionsService.blockByIdOrDid(idOrDid),
await this.connectionsService.blockByIdOrDid(options),
options.tenantId,
);
}
}
......@@ -3,6 +3,12 @@ import type {
ConnectionRecord,
ConnectionStateChangedEvent,
} from '@aries-framework/core';
import type {
EventDidcommConnectionsBlockInput,
EventDidcommConnectionsCreateWithSelfInput,
EventDidcommConnectionsGetAllInput,
EventDidcommConnectionsGetByIdInput,
} from '@ocm/shared';
import {
ConnectionEventTypes,
......@@ -14,79 +20,100 @@ import { Injectable } from '@nestjs/common';
import { MetadataTokens } from '../../common/constants.js';
import { AgentService } from '../agent.service.js';
import { WithTenantService } from '../withTenantService.js';
@Injectable()
export class ConnectionsService {
public agent: AppAgent;
public withTenantService: WithTenantService;
public constructor(agentService: AgentService) {
public constructor(
agentService: AgentService,
withTenantService: WithTenantService,
) {
this.agent = agentService.agent;
this.withTenantService = withTenantService;
}
public async getAll(): Promise<Array<ConnectionRecord>> {
return await this.agent.connections.getAll();
public async getAll({
tenantId,
}: EventDidcommConnectionsGetAllInput): Promise<Array<ConnectionRecord>> {
return this.withTenantService.invoke(tenantId, (t) =>
t.connections.getAll(),
);
}
public async getById(id: string): Promise<ConnectionRecord | null> {
return await this.agent.connections.findById(id);
public async getById({
tenantId,
id,
}: EventDidcommConnectionsGetByIdInput): Promise<ConnectionRecord | null> {
return this.withTenantService.invoke(tenantId, (t) =>
t.connections.findById(id),
);
}
public async blockByIdOrDid(
idOrDid: string,
): Promise<ConnectionRecord | null> {
if (isDid(idOrDid)) {
const records = await this.agent.connections.findAllByQuery({
theirDid: idOrDid,
});
public async blockByIdOrDid({
tenantId,
idOrDid,
}: EventDidcommConnectionsBlockInput): Promise<ConnectionRecord | null> {
return this.withTenantService.invoke(tenantId, async (t) => {
if (isDid(idOrDid)) {
const records = await t.connections.findAllByQuery({
theirDid: idOrDid,
});
if (records.length === 0) {
return null;
}
if (records.length > 1) {
throw new Error(
'Found multiple records with the same DID. This should not be possible',
);
}
if (records.length === 0) {
return null;
}
await t.connections.deleteById(records[0].id);
if (records.length > 1) {
throw new Error(
'Found multiple records with the same DID. This should not be possible',
);
return records[0];
}
await this.agent.connections.deleteById(records[0].id);
return records[0];
}
const record = await t.connections.findById(idOrDid);
if (!record) return null;
const record = await this.agent.connections.findById(idOrDid);
if (!record) return null;
await t.connections.deleteById(record.id);
await this.agent.connections.deleteById(record.id);
return record;
return record;
});
}
public async createConnectionWithSelf(): Promise<ConnectionRecord> {
const outOfBandRecord = await this.agent.oob.createInvitation();
const invitation = outOfBandRecord.outOfBandInvitation;
void this.agent.oob.receiveInvitation(invitation);
return new Promise((resolve) =>
this.agent.events.on<ConnectionStateChangedEvent>(
ConnectionEventTypes.ConnectionStateChanged,
async ({ payload: { connectionRecord } }) => {
if (connectionRecord.state !== DidExchangeState.Completed) return;
connectionRecord.metadata.set(
MetadataTokens.GAIA_X_CONNECTION_METADATA_KEY,
{
trusted: true,
},
);
const connRepo =
this.agent.dependencyManager.resolve(ConnectionRepository);
await connRepo.update(this.agent.context, connectionRecord);
resolve(connectionRecord);
},
),
);
public async createConnectionWithSelf({
tenantId,
}: EventDidcommConnectionsCreateWithSelfInput): Promise<ConnectionRecord> {
return this.withTenantService.invoke(tenantId, async (t) => {
const outOfBandRecord = await t.oob.createInvitation();
const invitation = outOfBandRecord.outOfBandInvitation;
void t.oob.receiveInvitation(invitation);
return new Promise((resolve) =>
this.agent.events.on<ConnectionStateChangedEvent>(
ConnectionEventTypes.ConnectionStateChanged,
async ({ payload: { connectionRecord } }) => {
if (connectionRecord.state !== DidExchangeState.Completed) return;
connectionRecord.metadata.set(
MetadataTokens.GAIA_X_CONNECTION_METADATA_KEY,
{
trusted: true,
},
);
const connRepo = t.dependencyManager.resolve(ConnectionRepository);
await connRepo.update(t.context, connectionRecord);
resolve(connectionRecord);
},
),
);
});
}
}
......@@ -28,6 +28,7 @@ describe('DidsController', () => {
const event = await didsController.resolve({
did: 'did:key:foo',
tenantId: 'some-id',
});
expect(event.data).toStrictEqual(result);
......
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { EventDidsResolve } from '@ocm/shared';
import {
EventDidsPublicDid,
EventDidsPublicDidInput,
EventDidsResolve,
EventDidsResolveInput,
} from '@ocm/shared';
import { DidsService } from './dids.service.js';
......@@ -8,8 +13,19 @@ import { DidsService } from './dids.service.js';
export class DidsController {
public constructor(private didsService: DidsService) {}
@MessagePattern('dids.resolve')
public async resolve({ did }: { did: string }) {
return new EventDidsResolve(await this.didsService.resolve(did));
@MessagePattern(EventDidsPublicDid.token)
public async publicDid(options: EventDidsPublicDidInput) {
return new EventDidsPublicDid(
await this.didsService.getPublicDid(options),
options.tenantId,
);
}
@MessagePattern(EventDidsResolve.token)
public async resolve(options: EventDidsResolveInput) {
return new EventDidsResolve(
await this.didsService.resolve(options),
options.tenantId,
);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment