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

Merge branch 'feat/tenants' into 'main'

Tenant manager

See merge request !47
parents 006dbc69 e458fb0f
No related branches found
No related tags found
3 merge requests!47Tenant manager,!45fix(security): remove dependency on expo-random and react-native-secure-random,!44feat(ssi-abstraction): offer to self fully covered
Pipeline #41077 failed
Showing
with 221 additions and 74 deletions
......@@ -11,7 +11,6 @@
# ... in these directories
!apps/**/src/*
!devtools/**/src/*
# Explicitly ignore these locations
node_modules
......
......@@ -20,7 +20,7 @@ module.exports = {
},
'import/resolver': {
typescript: {
project: ['apps/*/tsconfig.json', 'devtools/tsconfig.json'],
project: ['apps/*/tsconfig.json'],
alwaysTryTypes: true,
},
},
......@@ -88,12 +88,6 @@ module.exports = {
},
],
},
},
{
files: ['devtools/**/*.ts'],
rules: {
'no-console': 'off',
}
}
],
};
......@@ -12,7 +12,6 @@
# ... in these ones
!apps/**/src/*
!devtools/**/src/*
# Explicitly ignore these locations
node_modules
......
......@@ -14,7 +14,6 @@ FROM base AS dependencies
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig*.json .swcrc ./
COPY patches ./patches
COPY apps/shared/package.json ./apps/shared/
COPY devtools/package.json ./devtools/
RUN pnpm install --frozen-lockfile
# Build shared
......@@ -27,33 +26,6 @@ COPY --from=dependencies ${APP_HOME}/apps/shared/node_modules ./apps/shared/node
COPY --from=dependencies ${APP_HOME}/patches ./patches
RUN pnpm --filter shared build
# Build DevTools
FROM base AS build-devtools
COPY --from=dependencies ${APP_HOME}/package.json ${APP_HOME}/pnpm-lock.yaml ${APP_HOME}/pnpm-workspace.yaml ${APP_HOME}/tsconfig*.json ${APP_HOME}/.swcrc ./
COPY --from=dependencies ${APP_HOME}/node_modules ./node_modules
COPY --from=dependencies ${APP_HOME}/devtools/node_modules ./devtools/node_modules
COPY --from=dependencies ${APP_HOME}/patches ./patches
COPY --from=build-shared ${APP_HOME}/apps/shared ./apps/shared
COPY devtools ./devtools
RUN pnpm --filter devtools build && pnpm --filter devtools --prod deploy build
# Final devtools
FROM node:20-slim AS devtools
ARG APP_HOME=/home/node/app
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
WORKDIR ${APP_HOME}
CMD ["node", "dist/server.js"]
COPY --from=build-devtools --chown=node:node ${APP_HOME}/build/dist ./dist
COPY --from=build-devtools --chown=node:node ${APP_HOME}/build/node_modules ./node_modules
COPY --from=build-devtools --chown=node:node ${APP_HOME}/build/package.json .
USER node
# Build service
FROM base AS build-service
......
......@@ -15,9 +15,9 @@
"test": "jest"
},
"dependencies": {
"@credo-ts/anoncreds": "0.5.0-alpha.149",
"@credo-ts/core": "0.5.0-alpha.149",
"@credo-ts/tenants": "0.5.0-alpha.149",
"@credo-ts/anoncreds": "0.5.0-alpha.151",
"@credo-ts/core": "0.5.0-alpha.151",
"@credo-ts/tenants": "0.5.0-alpha.151",
"@elastic/ecs-winston-format": "1.5.2",
"@nestjs/axios": "3.0.2",
"@nestjs/swagger": "7.3.0",
......
......@@ -24,3 +24,8 @@ export * from './modules/tsa/index.js';
export * from './interceptors/response-format.interceptor.js';
export * from './staticStorage.js';
export * from './rxjs/extract-response-data.js';
export * from './rxjs/handle-empty-response.js';
export * from './rxjs/handle-request-timeout.js';
export * from './rxjs/handle-ssi-response.js';
import { map } from 'rxjs';
export const exrtactResponseData = () => map(({ data }) => data);
import { InternalServerErrorException, Logger } from '@nestjs/common';
import { catchError, of } from 'rxjs';
export const handleEmptyResponse = (
message = 'Make sure SSI abstraction is running',
) =>
catchError((error) => {
if (
error instanceof Error &&
error.constructor.name === 'EmptyResponseException'
) {
Logger.error(error.message);
message && Logger.error(message);
throw new InternalServerErrorException();
}
return of(error);
});
import { throwError, timeout } from 'rxjs';
export const handleRequestTimeout = (timeoutMs: number = 10000) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
timeout<any, any>({
each: timeoutMs,
with: () => throwError(() => new Error('Request timed out')),
});
import { pipe } from 'rxjs';
import { exrtactResponseData } from './extract-response-data.js';
import { handleEmptyResponse } from './handle-empty-response.js';
import { handleRequestTimeout } from './handle-request-timeout.js';
export const handleSSIResponse = pipe(
handleRequestTimeout(),
handleEmptyResponse(),
exrtactResponseData(),
);
......@@ -15,12 +15,12 @@
"test:e2e": "pnpm test -- -c=test/jest.config.js --runInBand"
},
"dependencies": {
"@credo-ts/anoncreds": "0.5.0-alpha.149",
"@credo-ts/askar": "0.5.0-alpha.149",
"@credo-ts/core": "0.5.0-alpha.149",
"@credo-ts/indy-vdr": "0.5.0-alpha.149",
"@credo-ts/node": "0.5.0-alpha.149",
"@credo-ts/tenants": "0.5.0-alpha.149",
"@credo-ts/anoncreds": "0.5.0-alpha.151",
"@credo-ts/askar": "0.5.0-alpha.151",
"@credo-ts/core": "0.5.0-alpha.151",
"@credo-ts/indy-vdr": "0.5.0-alpha.151",
"@credo-ts/node": "0.5.0-alpha.151",
"@credo-ts/tenants": "0.5.0-alpha.151",
"@elastic/ecs-winston-format": "1.5.2",
"@hyperledger/anoncreds-nodejs": "^0.2.1",
"@hyperledger/aries-askar-nodejs": "^0.2.0",
......
......@@ -27,6 +27,7 @@ import type {
import {
AutoAcceptCredential,
CredentialEventTypes,
CredentialRole,
CredentialState,
} from '@credo-ts/core';
import { GenericRecord } from '@credo-ts/core/build/modules/generic-records/repository/GenericRecord.js';
......@@ -217,7 +218,7 @@ export class AnonCredsCredentialsService {
throw new Error('Connection with yourself is not ready, yet');
}
const acceptOfferListener = new Promise((resolve) => {
const acceptOfferListener = new Promise<void>((resolve) => {
this.agentService.agent.events.on<CredentialStateChangedEvent>(
CredentialEventTypes.CredentialStateChanged,
async ({ payload: { credentialRecord } }) => {
......@@ -242,7 +243,7 @@ export class AnonCredsCredentialsService {
autoAcceptCredential: AutoAcceptCredential.Always,
});
resolve(connectionRecord);
resolve();
},
);
});
......@@ -252,8 +253,8 @@ export class AnonCredsCredentialsService {
CredentialEventTypes.CredentialStateChanged,
({ payload: { credentialRecord } }) => {
if (
credentialRecord.state === CredentialState.Done ||
credentialRecord.state === CredentialState.CredentialIssued
credentialRecord.state === CredentialState.Done &&
credentialRecord.role === CredentialRole.Holder
)
resolve(credentialRecord);
},
......
......@@ -120,6 +120,22 @@ export class ConnectionsService {
});
}
public async waitUntilComplete({ connectionId }: { connectionId: string }) {
return new Promise<ConnectionRecord>((resolve) =>
this.agent.events.on<ConnectionStateChangedEvent>(
ConnectionEventTypes.ConnectionStateChanged,
({ payload: { connectionRecord } }) => {
if (
connectionRecord.id === connectionId &&
connectionRecord.state === DidExchangeState.Completed
) {
resolve(connectionRecord);
}
},
),
);
}
public async receiveInvitationFromUrl({
tenantId,
invitationUrl,
......@@ -173,7 +189,10 @@ export class ConnectionsService {
);
const connRepo = t.dependencyManager.resolve(ConnectionRepository);
await connRepo.update(t.context, connectionRecord);
// Check whether the connection record actually belongs to the current tenant (t)
if (await connRepo.findById(t.context, connectionRecord.id)) {
await connRepo.update(t.context, connectionRecord);
}
resolve(connectionRecord);
},
......
......@@ -24,7 +24,7 @@ import {
KeyType,
TypedArrayEncoder,
} from '@credo-ts/core';
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { LEDGERS } from '../../config/ledger.js';
......@@ -34,6 +34,8 @@ import { WithTenantService } from '../withTenantService.js';
@Injectable()
export class DidsService {
private readonly logger = new Logger(this.constructor.name);
public constructor(
private agentService: AgentService,
private withTenantService: WithTenantService,
......@@ -137,12 +139,22 @@ export class DidsService {
keyType: KeyType.Ed25519,
};
await this.agentService.agent.wallet.createKey(privKey);
try {
this.logger.log('Registering wallet key for endorser dids');
await this.agentService.agent.wallet.createKey(privKey);
} catch (e) {
if (e instanceof Error && e.constructor.name === 'WalletKeyExistsError') {
this.logger.log('Wallet key already exists');
} else {
throw e;
}
}
for (const indyDid of indyDids) {
await this.agentService.agent.dids.import({
did: indyDid.did,
privateKeys: [privKey],
overwrite: true,
});
}
......
import type { OnApplicationBootstrap } from '@nestjs/common';
import type { ConfigType } from '@nestjs/config';
import { Module } from '@nestjs/common';
import { Inject, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { RouterModule } from '@nestjs/core';
import { HealthModule } from '@ocm/shared';
import { ClientProxy, ClientsModule, Transport } from '@nestjs/microservices';
import { EventDidsRegisterEndorserDid, HealthModule } from '@ocm/shared';
import { firstValueFrom } from 'rxjs';
import { AgentModule } from './agent/agent.module.js';
import { AnonCredsCredentialsModule } from './agent/anoncredsCredentials/anoncredsCredentials.module.js';
......@@ -14,6 +17,7 @@ import { DidsModule } from './agent/dids/dids.module.js';
import { RevocationModule } from './agent/revocation/revocation.module.js';
import { SchemasModule } from './agent/schemas/schemas.module.js';
import { TenantsModule } from './agent/tenants/tenants.module.js';
import { NATS_CLIENT } from './common/constants.js';
import { agentConfig } from './config/agent.config.js';
import { httpConfig } from './config/http.config.js';
import { natsConfig } from './config/nats.config.js';
......@@ -35,6 +39,23 @@ import { validationSchema } from './config/validation.js';
},
}),
ClientsModule.registerAsync({
clients: [
{
name: NATS_CLIENT,
inject: [natsConfig.KEY],
useFactory: (config: ConfigType<typeof natsConfig>) => ({
transport: Transport.NATS,
options: {
servers: [config.url],
user: config.user as string,
pass: config.password as string,
},
}),
},
],
}),
HealthModule.registerAsync({
inject: [natsConfig.KEY],
useFactory: (config: ConfigType<typeof natsConfig>) => {
......@@ -64,4 +85,16 @@ import { validationSchema } from './config/validation.js';
RouterModule.register([{ module: HealthModule, path: '/health' }]),
],
})
export class Application {}
export class Application implements OnApplicationBootstrap {
public constructor(
@Inject(NATS_CLIENT) private readonly natsClient: ClientProxy,
) {}
public async onApplicationBootstrap() {
await this.natsClient.connect();
await firstValueFrom(
this.natsClient.send(EventDidsRegisterEndorserDid.token, {}),
);
}
}
......@@ -9,3 +9,5 @@ export enum MetadataTokens {
export enum GenericRecordTokens {
REVOCATION = 'revocation_generic_record',
}
export const NATS_CLIENT = Symbol('NATS_CLIENT');
......@@ -9,7 +9,6 @@ import type {
EventAnonCredsCredentialsDeleteByIdInput,
EventAnonCredsCredentialsGetAllInput,
EventAnonCredsCredentialsGetByIdInput,
EventDidcommAnonCredsCredentialsAcceptOffer,
EventDidcommAnonCredsCredentialsAcceptOfferInput,
EventDidcommAnonCredsCredentialsOfferInput,
EventDidcommAnonCredsCredentialsOfferToSelfInput,
......@@ -18,11 +17,13 @@ import type {
import {
AutoAcceptCredential,
CredentialExchangeRecord,
CredentialRole,
CredentialState,
} from '@credo-ts/core';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { Test } from '@nestjs/testing';
import {
EventDidcommAnonCredsCredentialsAcceptOffer,
EventAnonCredsCredentialOfferGetAll,
EventAnonCredsCredentialOfferGetById,
EventAnonCredsCredentialRequestGetAll,
......@@ -30,10 +31,10 @@ import {
EventAnonCredsCredentialsDeleteById,
EventAnonCredsCredentialsGetAll,
EventAnonCredsCredentialsGetById,
EventAnonCredsProofsDeleteById,
EventDidcommAnonCredsCredentialsOffer,
EventDidcommAnonCredsCredentialsOfferToSelf,
} from '@ocm/shared';
import assert from 'assert';
import { randomBytes } from 'crypto';
import { firstValueFrom } from 'rxjs';
......@@ -57,6 +58,7 @@ describe('Credentials', () => {
let app: INestApplication;
let client: ClientProxy;
let tenantId: string;
let tenantIdTwo: string;
let issuerDid: string;
let credentialDefinitionId: string;
......@@ -116,6 +118,11 @@ describe('Credentials', () => {
const { id } = await tenantsService.create({ label: TOKEN });
tenantId = id;
const { id: tIdTwo } = await tenantsService.create({
label: `${TOKEN}-two`,
});
tenantIdTwo = tIdTwo;
const connectionsService = app.get(ConnectionsService);
await connectionsService.createConnectionWithSelf({ tenantId });
......@@ -150,15 +157,16 @@ describe('Credentials', () => {
const connectionService = app.get(ConnectionsService);
const { invitationUrl } = await connectionService.createInvitation({
tenantId,
tenantId: tenantIdTwo,
});
const { id: cId } = await connectionService.receiveInvitationFromUrl({
const connectionRecord = await connectionService.receiveInvitationFromUrl({
tenantId,
invitationUrl,
});
connectionId = cId;
connectionId = connectionRecord.id;
await connectionService.waitUntilComplete({ connectionId });
});
afterAll(async () => {
......@@ -166,7 +174,7 @@ describe('Credentials', () => {
client.close();
});
xit(EventAnonCredsCredentialsGetAll.token, async () => {
it(EventAnonCredsCredentialsGetAll.token, async () => {
const response$ = client.send<
EventAnonCredsCredentialsGetAll,
EventAnonCredsCredentialsGetAllInput
......@@ -177,7 +185,7 @@ describe('Credentials', () => {
expect(eventInstance.instance).toEqual(expect.arrayContaining([]));
});
xit(EventAnonCredsCredentialOfferGetAll.token, async () => {
it(EventAnonCredsCredentialOfferGetAll.token, async () => {
const response$ = client.send<
EventAnonCredsCredentialOfferGetAll,
EventAnonCredsCredentialOfferGetAllInput
......@@ -189,7 +197,7 @@ describe('Credentials', () => {
expect(eventInstance.instance).toEqual(expect.arrayContaining([]));
});
xit(EventAnonCredsCredentialOfferGetById.token, async () => {
it(EventAnonCredsCredentialOfferGetById.token, async () => {
const response$ = client.send<
EventAnonCredsCredentialOfferGetById,
EventAnonCredsCredentialOfferGetByIdInput
......@@ -204,7 +212,7 @@ describe('Credentials', () => {
expect(eventInstance.instance).toBeNull();
});
xit(EventAnonCredsCredentialRequestGetAll.token, async () => {
it(EventAnonCredsCredentialRequestGetAll.token, async () => {
const response$ = client.send<
EventAnonCredsCredentialRequestGetAll,
EventAnonCredsCredentialRequestGetAllInput
......@@ -216,7 +224,7 @@ describe('Credentials', () => {
expect(eventInstance.instance).toEqual(expect.arrayContaining([]));
});
xit(EventAnonCredsCredentialRequestGetById.token, async () => {
it(EventAnonCredsCredentialRequestGetById.token, async () => {
const response$ = client.send<
EventAnonCredsCredentialRequestGetById,
EventAnonCredsCredentialRequestGetByIdInput
......@@ -231,7 +239,7 @@ describe('Credentials', () => {
expect(eventInstance.instance).toBeNull();
});
xit(EventAnonCredsCredentialsGetById.token, async () => {
it(EventAnonCredsCredentialsGetById.token, async () => {
const response$ = client.send<
EventAnonCredsCredentialsGetById,
EventAnonCredsCredentialsGetByIdInput
......@@ -245,7 +253,7 @@ describe('Credentials', () => {
expect(eventInstance.instance).toEqual(null);
});
xit(EventDidcommAnonCredsCredentialsOffer.token, async () => {
it(EventDidcommAnonCredsCredentialsOffer.token, async () => {
const attributes = [
{ name: 'Name', value: 'Berend' },
{ name: 'Age', value: '25' },
......@@ -265,23 +273,75 @@ describe('Credentials', () => {
const eventInstance =
EventDidcommAnonCredsCredentialsOffer.fromEvent(response);
await new Promise((r) => setTimeout(r, 2000));
expect(eventInstance.instance).toMatchObject({
state: CredentialState.OfferSent,
});
const getAllOffersResponse$ = client.send<
EventAnonCredsCredentialOfferGetAll,
EventAnonCredsCredentialOfferGetAllInput
>(EventAnonCredsCredentialOfferGetAll.token, {
tenantId: tenantIdTwo,
});
const getAllOffersResponse = await firstValueFrom(getAllOffersResponse$);
const getAllOffersEventInstance =
EventAnonCredsCredentialOfferGetAll.fromEvent(getAllOffersResponse);
const receivedOffer = getAllOffersEventInstance.instance.find(
(o) => o.state === CredentialState.OfferReceived,
);
expect(receivedOffer).toMatchObject({
state: CredentialState.OfferReceived,
});
assert(receivedOffer);
const acceptResponse$ = client.send<
EventDidcommAnonCredsCredentialsAcceptOffer,
EventDidcommAnonCredsCredentialsAcceptOfferInput
>(EventDidcommAnonCredsCredentialsOffer.token, {
tenantId,
credentialId: eventInstance.instance.id,
>(EventDidcommAnonCredsCredentialsAcceptOffer.token, {
tenantId: tenantIdTwo,
credentialId: receivedOffer.id,
});
const acceptResponse = await firstValueFrom(acceptResponse$);
const acceptEventInstance =
EventAnonCredsCredentialsGetById.fromEvent(acceptResponse);
EventDidcommAnonCredsCredentialsOffer.fromEvent(acceptResponse);
expect(acceptEventInstance.instance).toMatchObject({
state: CredentialState.RequestSent,
});
await new Promise((r) => setTimeout(r, 2000));
const getAllCredentialsResponse$ = client.send<
EventAnonCredsCredentialsGetAll,
EventAnonCredsCredentialsGetAllInput
>(EventAnonCredsCredentialsGetAll.token, {
tenantId: tenantIdTwo,
});
const getAllCredentialsResponse = await firstValueFrom(
getAllCredentialsResponse$,
);
const getAllCredentialsEventInstance =
EventAnonCredsCredentialsGetAll.fromEvent(getAllCredentialsResponse);
const credential = getAllCredentialsEventInstance.instance.find(
(c) => c.id === receivedOffer.id,
);
expect(credential).toMatchObject({
id: receivedOffer.id,
state: CredentialState.Done,
role: CredentialRole.Holder,
credentialAttributes: expect.arrayContaining([
expect.objectContaining({ name: 'Name', value: 'Berend' }),
expect.objectContaining({ name: 'Age', value: '25' }),
]),
});
});
it(EventDidcommAnonCredsCredentialsOfferToSelf.token, async () => {
......@@ -305,10 +365,16 @@ describe('Credentials', () => {
expect(eventInstance.instance).toMatchObject({
autoAcceptCredential: AutoAcceptCredential.Always,
role: CredentialRole.Holder,
state: CredentialState.Done,
});
// Sleep is done here so the last message can be received and we do not end up in a broken state.
// Without the sleep the broken state is reached because we still have to send to `ack` but we already shut down the agent when the test is finished.
await new Promise((r) => setTimeout(r, 1000));
});
xit(EventAnonCredsProofsDeleteById.token, async () => {
it(EventAnonCredsCredentialsDeleteById.token, async () => {
let credentialExchangeRecord: CredentialExchangeRecord | undefined =
undefined;
......
HTTP_HOSTNAME=0.0.0.0
HTTP_PORT=4007
NATS_URL=nats://localhost:4222
NATS_USER=nats_user
NATS_PASSWORD=nats_password
NATS_MONITORING_URL=http://localhost:8222
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