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 !12
parents f9c6aaac d25cec02
No related branches found
No related tags found
1 merge request!12feat(ssi): multitenancy support
Showing
with 356 additions and 44 deletions
......@@ -6,7 +6,7 @@ import { DidsController } from './dids.controller.js';
import { DidsService } from './dids.service.js';
@Module({
imports: [AgentModule],
imports: [AgentModule ],
providers: [DidsService],
controllers: [DidsController],
})
......
import type { AppAgent } from '../agent.service.js';
import type {
EventDidsPublicDidInput,
EventDidsResolveInput,
} from '@ocm/shared';
import { Injectable } from '@nestjs/common';
import { AgentService } from '../agent.service.js';
import { WithTenantService } from '../withTenantService.js';
@Injectable()
export class DidsService {
public agent: AppAgent;
public withTenantService: WithTenantService;
public constructor(agentService: AgentService) {
this.agent = agentService.agent;
public constructor(withTenantService: WithTenantService) {
this.withTenantService = withTenantService;
}
public async resolve(did: string) {
const {
didDocument,
didResolutionMetadata: { message, error },
} = await this.agent.dids.resolve(did);
if (!didDocument) {
throw new Error(
`Could not resolve did: '${did}'. Error: ${error ?? 'None'} Message: ${
message ?? 'None'
}`,
);
}
return didDocument;
public async resolve({ did, tenantId }: EventDidsResolveInput) {
return this.withTenantService.invoke(tenantId, async (t) => {
const {
didDocument,
didResolutionMetadata: { message, error },
} = await t.dids.resolve(did);
if (!didDocument) {
throw new Error(
`Could not resolve did: '${did}'. Error: ${
error ?? 'None'
} Message: ${message ?? 'None'}`,
);
}
return didDocument;
});
}
public async getPublicDid({ tenantId }: EventDidsPublicDidInput) {
return this.withTenantService.invoke(tenantId, async (t) => {
const dids = await t.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;
});
}
}
import { TenantRecord } from '@aries-framework/tenants';
import { Test } from '@nestjs/testing';
import { mockConfigModule } from '../../../config/__tests__/mockConfig.js';
import { AgentModule } from '../../agent.module.js';
import { TenantsController } from '../tenants.controller.js';
import { TenantsService } from '../tenants.service.js';
describe('TenantsController', () => {
let tenantsController: TenantsController;
let tenantsService: TenantsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [mockConfigModule(), AgentModule],
controllers: [TenantsController],
providers: [TenantsService],
}).compile();
tenantsService = moduleRef.get(TenantsService);
tenantsController = moduleRef.get(TenantsController);
});
describe('resolve', () => {
it('should resolve a basic did', async () => {
const result = new TenantRecord({
config: {
label: 'my-label',
walletConfig: { key: 'some-key', id: 'some-id' },
},
});
jest.spyOn(tenantsService, 'create').mockResolvedValue(result);
const event = await tenantsController.create({
label: 'my-label',
});
expect(event.data).toStrictEqual(result);
});
});
});
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { EventTenantsCreate, EventTenantsCreateInput } from '@ocm/shared';
import { TenantsService } from './tenants.service.js';
@Controller('tenants')
export class TenantsController {
public constructor(private tenantsService: TenantsService) {}
@MessagePattern(EventTenantsCreate.token)
public async create({
label,
}: EventTenantsCreateInput): Promise<EventTenantsCreate> {
return new EventTenantsCreate(
await this.tenantsService.create(label),
undefined,
);
}
}
import { Module } from '@nestjs/common';
import { AgentModule } from '../agent.module.js';
import { TenantsController } from './tenants.controller.js';
import { TenantsService } from './tenants.service.js';
@Module({
imports: [AgentModule],
providers: [TenantsService],
controllers: [TenantsController],
})
export class TenantsModule {}
import type { AppAgent } from '../agent.service.js';
import { Injectable } from '@nestjs/common';
import { AgentService } from '../agent.service.js';
@Injectable()
export class TenantsService {
public agent: AppAgent;
public constructor(agentService: AgentService) {
this.agent = agentService.agent;
}
public async create(label: string) {
return await this.agent.modules.tenants.createTenant({ config: { label } });
}
}
import type { AppAgent } from './agent.service.js';
import { Injectable } from '@nestjs/common';
import { AgentService } from './agent.service.js';
@Injectable()
export class WithTenantService {
private agent: AppAgent;
public constructor(agentService: AgentService) {
this.agent = agentService.agent;
}
public invoke<T>(
tenantId: string,
cb: (tenant: AppAgent) => Promise<T>,
): Promise<T> {
// eslint-disable-next-line no-async-promise-executor
return new Promise<T>(async (resolve, reject) => {
await this.agent.modules.tenants.withTenantAgent(
{ tenantId },
async (tenant) => {
try {
const ret = await cb(tenant as unknown as AppAgent);
resolve(ret);
} catch (e) {
reject(e);
}
},
);
});
}
}
import { DidsModule } from '@aries-framework/core';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TerminusModule } from '@nestjs/terminus';
......@@ -5,6 +6,7 @@ import { HealthController } from '@ocm/shared';
import { AgentModule } from './agent/agent.module.js';
import { ConnectionsModule } from './agent/connections/connections.module.js';
import { TenantsModule } from './agent/tenants/tenants.module.js';
import { config } from './config/config.js';
import { validationSchema } from './config/validation.js';
......@@ -18,6 +20,8 @@ import { validationSchema } from './config/validation.js';
}),
AgentModule,
ConnectionsModule,
DidsModule,
TenantsModule,
],
controllers: [HealthController],
})
......
import type { INestApplication } from '@nestjs/common';
import type { ClientProxy } from '@nestjs/microservices';
import type { Observable } from 'rxjs';
import type {
EventDidcommConnectionsGetAllInput,
EventDidcommConnectionsCreateWithSelfInput,
EventDidcommConnectionsGetByIdInput,
EventDidcommConnectionsBlockInput,
} from '@ocm/shared';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { Test } from '@nestjs/testing';
......@@ -14,6 +19,8 @@ import { firstValueFrom } from 'rxjs';
import { AgentModule } from '../src/agent/agent.module.js';
import { ConnectionsModule } from '../src/agent/connections/connections.module.js';
import { TenantsModule } from '../src/agent/tenants/tenants.module.js';
import { TenantsService } from '../src/agent/tenants/tenants.service.js';
import { MetadataTokens } from '../src/common/constants.js';
import { mockConfigModule } from '../src/config/__tests__/mockConfig.js';
......@@ -21,6 +28,7 @@ describe('Connections', () => {
const TOKEN = 'CONNECTIONS_CLIENT_SERVICE';
let app: INestApplication;
let client: ClientProxy;
let tenantId: string;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
......@@ -28,6 +36,7 @@ describe('Connections', () => {
mockConfigModule(3004),
AgentModule,
ConnectionsModule,
TenantsModule,
ClientsModule.register([{ name: TOKEN, transport: Transport.NATS }]),
],
}).compile();
......@@ -41,6 +50,10 @@ describe('Connections', () => {
client = app.get(TOKEN);
await client.connect();
const ts = app.get(TenantsService);
const { id } = await ts.create(TOKEN);
tenantId = id;
});
afterAll(async () => {
......@@ -49,10 +62,10 @@ describe('Connections', () => {
});
it(EventDidcommConnectionsGetAll.token, async () => {
const response$: Observable<EventDidcommConnectionsGetAll> = client.send(
EventDidcommConnectionsGetAll.token,
{},
);
const response$ = client.send<
EventDidcommConnectionsGetAll,
EventDidcommConnectionsGetAllInput
>(EventDidcommConnectionsGetAll.token, { tenantId });
const response = await firstValueFrom(response$);
const eventInstance = EventDidcommConnectionsGetAll.fromEvent(response);
......@@ -60,10 +73,13 @@ describe('Connections', () => {
});
it(EventDidcommConnectionsGetById.token, async () => {
const response$: Observable<EventDidcommConnectionsGetById> = client.send(
EventDidcommConnectionsGetById.token,
{ id: 'some-id' },
);
const response$ = client.send<
EventDidcommConnectionsGetById,
EventDidcommConnectionsGetByIdInput
>(EventDidcommConnectionsGetById.token, {
id: 'some-id',
tenantId,
});
const response = await firstValueFrom(response$);
const eventInstance = EventDidcommConnectionsGetById.fromEvent(response);
......@@ -71,8 +87,12 @@ describe('Connections', () => {
});
it(EventDidcommConnectionsCreateWithSelf.token, async () => {
const response$: Observable<EventDidcommConnectionsCreateWithSelf> =
client.send(EventDidcommConnectionsCreateWithSelf.token, {});
const response$ = client.send<
EventDidcommConnectionsCreateWithSelf,
EventDidcommConnectionsCreateWithSelfInput
>(EventDidcommConnectionsCreateWithSelf.token, {
tenantId,
});
const response = await firstValueFrom(response$);
const eventInstance =
......@@ -86,10 +106,13 @@ describe('Connections', () => {
});
it(EventDidcommConnectionsBlock.token, async () => {
const response$: Observable<EventDidcommConnectionsBlock> = client.send(
EventDidcommConnectionsBlock.token,
{ idOrDid: 'some-id' },
);
const response$ = client.send<
EventDidcommConnectionsBlock,
EventDidcommConnectionsBlockInput
>(EventDidcommConnectionsBlock.token, {
idOrDid: 'some-id',
tenantId,
});
const response = await firstValueFrom(response$);
const eventInstance = EventDidcommConnectionsBlock.fromEvent(response);
......
import type { INestApplication } from '@nestjs/common';
import type { ClientProxy } from '@nestjs/microservices';
import type { Observable } from 'rxjs';
import type {
EventDidsResolveInput,
EventDidsPublicDidInput,
} from '@ocm/shared';
import { DidDocument } from '@aries-framework/core';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { Test } from '@nestjs/testing';
import {
EventDidsResolve,
} from '@ocm/shared';
import { EventDidsResolve, EventDidsPublicDid } from '@ocm/shared';
import { firstValueFrom } from 'rxjs';
import { AgentModule } from '../src/agent/agent.module.js';
import { DidsModule } from '../src/agent/dids/dids.module.js';
import { DidsService } from '../src/agent/dids/dids.service.js';
import { TenantsModule } from '../src/agent/tenants/tenants.module.js';
import { TenantsService } from '../src/agent/tenants/tenants.service.js';
import { mockConfigModule } from '../src/config/__tests__/mockConfig.js';
const mockDidDocument = {
context: ['https://w3id.org/did/v1'],
id: 'did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM',
verificationMethod: [
{
id: 'did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM#verkey',
type: 'Ed25519VerificationKey2018',
controller: 'did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM',
publicKeyBase58: '4SySYXQUtuK26zfC7RpQpWYMThfbXphUf8LWyXXmxyTX',
},
],
authentication: ['did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM#verkey'],
};
describe('Dids', () => {
const TOKEN = 'DIDS_CLIENT_SERVICE';
let app: INestApplication;
let client: ClientProxy;
let tenantId: string;
beforeAll(async () => {
jest
.spyOn(DidsService.prototype, 'getPublicDid')
.mockResolvedValue(new DidDocument(mockDidDocument));
const moduleRef = await Test.createTestingModule({
imports: [
mockConfigModule(3005),
AgentModule,
DidsModule,
TenantsModule,
ClientsModule.register([{ name: TOKEN, transport: Transport.NATS }]),
],
}).compile();
......@@ -37,6 +61,10 @@ describe('Dids', () => {
client = app.get(TOKEN);
await client.connect();
const ts = app.get(TenantsService);
const { id } = await ts.create(TOKEN);
tenantId = id;
});
afterAll(async () => {
......@@ -44,11 +72,24 @@ describe('Dids', () => {
client.close();
});
it(EventDidsPublicDid.token, async () => {
const response$ = client.send<EventDidsPublicDid, EventDidsPublicDidInput>(
EventDidsPublicDid.token,
{ tenantId },
);
const response = await firstValueFrom(response$);
const eventInstance = EventDidsPublicDid.fromEvent(response);
expect(eventInstance.instance).toMatchObject(mockDidDocument);
});
it(EventDidsResolve.token, async () => {
const response$: Observable<EventDidsResolve> = client.send(
const response$ = client.send<EventDidsResolve, EventDidsResolveInput>(
EventDidsResolve.token,
{
did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH',
tenantId,
},
);
......
......@@ -3,7 +3,7 @@ import config from '../jest.config.js';
/** @type {import('jest').Config} */
export default {
...config,
testTimeout: 12000,
testTimeout: 24000,
rootDir: '.',
testRegex: '.*\\.e2e-spec\\.ts$',
};
import './setEnvVars.js';
import type { INestApplication } from '@nestjs/common';
import type { ClientProxy } from '@nestjs/microservices';
import type { EventTenantsCreateInput } from '@ocm/shared';
import { DidDocument } from '@aries-framework/core';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { Test } from '@nestjs/testing';
import { EventInfoPublicDid } from '@ocm/shared';
import { firstValueFrom, type Observable } from 'rxjs';
import { EventTenantsCreate } from '@ocm/shared';
import { firstValueFrom } from 'rxjs';
import { AgentModule } from '../src/agent/agent.module.js';
import { AgentService } from '../src/agent/agent.service.js';
import { TenantsModule } from '../src/agent/tenants/tenants.module.js';
import { mockConfigModule } from '../src/config/__tests__/mockConfig.js';
const mockDidDocument = {
context: ['https://w3id.org/did/v1'],
id: 'did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM',
verificationMethod: [
{
id: 'did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM#verkey',
type: 'Ed25519VerificationKey2018',
controller: 'did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM',
publicKeyBase58: '4SySYXQUtuK26zfC7RpQpWYMThfbXphUf8LWyXXmxyTX',
},
],
authentication: ['did:indy:bcovrin:test:7KuDTpQh3GJ7Gp6kErpWvM#verkey'],
};
describe('Agent', () => {
const TOKEN = 'AGENT_CLIENT_SERVICE';
describe('Tenants', () => {
const TOKEN = 'TENANTS_CLIENT_SERVICE';
let app: INestApplication;
let client: ClientProxy;
beforeAll(async () => {
jest
.spyOn(AgentService.prototype, 'getPublicDid')
.mockResolvedValue(new DidDocument(mockDidDocument));
const moduleRef = await Test.createTestingModule({
imports: [
mockConfigModule(3000),
mockConfigModule(3005),
AgentModule,
TenantsModule,
ClientsModule.register([{ name: TOKEN, transport: Transport.NATS }]),
],
}).compile();
......@@ -56,20 +37,29 @@ describe('Agent', () => {
await client.connect();
});
it(EventInfoPublicDid.token, async () => {
const response$: Observable<EventInfoPublicDid> = client.send(
EventInfoPublicDid.token,
{},
afterAll(async () => {
await app.close();
client.close();
});
it(EventTenantsCreate.token, async () => {
const response$ = client.send<EventTenantsCreate, EventTenantsCreateInput>(
EventTenantsCreate.token,
{
label: 'my-new-tenant',
},
);
const response = await firstValueFrom(response$);
const eventInstance = EventInfoPublicDid.fromEvent(response);
expect(eventInstance.instance).toMatchObject(mockDidDocument);
});
const eventInstance = EventTenantsCreate.fromEvent(response);
afterAll(async () => {
await app.close();
client.close();
expect(eventInstance.instance.toJSON()).toMatchObject({
config: {
label: 'my-new-tenant',
walletConfig: {
keyDerivationMethod: 'RAW',
},
},
});
});
});
......@@ -682,6 +682,9 @@ importers:
'@aries-framework/core':
specifier: 0.4.2
version: 0.4.2(expo@49.0.18)(react-native@0.72.7)
'@aries-framework/tenants':
specifier: ^0.4.2
version: 0.4.2(expo@49.0.18)(react-native@0.72.7)
'@elastic/ecs-winston-format':
specifier: ^1.5.0
version: 1.5.0
......@@ -758,6 +761,9 @@ importers:
'@aries-framework/node':
specifier: 0.4.2
version: 0.4.2(expo@49.0.18)(react-native@0.72.7)
'@aries-framework/tenants':
specifier: ^0.4.2
version: 0.4.2(expo@49.0.18)(react-native@0.72.7)
'@elastic/ecs-winston-format':
specifier: ^1.5.0
version: 1.5.0
......@@ -1062,6 +1068,19 @@ packages:
- web-streams-polyfill
dev: false
 
/@aries-framework/tenants@0.4.2(expo@49.0.18)(react-native@0.72.7):
resolution: {integrity: sha512-dRgneBY4z6YAn9ieNSeLEqhW+H03aFZwnxcnWhJfSGeHKUl0kMPmjCqvpP3NFhdB/rX92U9OOZDruIv2efM2ig==}
dependencies:
'@aries-framework/core': 0.4.2(expo@49.0.18)(react-native@0.72.7)
async-mutex: 0.4.0
transitivePeerDependencies:
- domexception
- encoding
- expo
- react-native
- web-streams-polyfill
dev: false
/@babel/code-frame@7.10.4:
resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==}
dependencies:
......@@ -6144,6 +6163,12 @@ packages:
dev: false
optional: true
 
/async-mutex@0.4.0:
resolution: {integrity: sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==}
dependencies:
tslib: 2.6.2
dev: false
/async-value-promise@1.1.1:
resolution: {integrity: sha512-c2RFDKjJle1rHa0YxN9Ysu97/QBu3Wa+NOejJxsX+1qVDJrkD3JL/GN1B3gaILAEXJXbu/4Z1lcoCHFESe/APA==}
requiresBuild: true
......@@ -8974,7 +8999,7 @@ packages:
semver: 7.5.4
tapable: 2.2.1
typescript: 5.2.2
webpack: 5.89.0(@swc/core@1.3.96)
webpack: 5.89.0
dev: true
 
/form-data@3.0.1:
......@@ -10313,7 +10338,7 @@ packages:
pretty-format: 29.7.0
slash: 3.0.0
strip-json-comments: 3.1.1
ts-node: 10.9.1(@swc/core@1.3.96)(@types/node@20.9.0)(typescript@5.2.2)
ts-node: 10.9.1(@types/node@20.9.4)(typescript@5.3.2)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
......
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