From 69747e1071334c49da67160666c580362c68691c Mon Sep 17 00:00:00 2001 From: Emma Casolin Date: Fri, 4 Feb 2022 12:30:24 +1100 Subject: [PATCH] Fixed `identities search` command to search connected identities Unattended background discovery for Discovery domain Adding trusted nodes/identities to the Gestalt Graph --- src/PolykeyAgent.ts | 12 +- src/bin/identities/CommandDiscover.ts | 4 +- src/bin/identities/CommandSearch.ts | 88 ++- src/bin/identities/CommandTrust.ts | 23 +- src/bin/utils/parsers.ts | 28 + src/client/GRPCClientClient.ts | 25 +- .../service/gestaltsDiscoveryByIdentity.ts | 10 +- src/client/service/gestaltsDiscoveryByNode.ts | 10 +- .../service/gestaltsGestaltTrustByIdentity.ts | 78 +++ .../service/gestaltsGestaltTrustByNode.ts | 66 ++ .../service/identitiesInfoConnectedGet.ts | 103 +++ src/client/service/identitiesInfoGet.ts | 96 ++- .../service/identitiesInfoGetConnected.ts | 75 -- src/client/service/index.ts | 8 +- src/discovery/Discovery.ts | 512 +++++++++----- src/discovery/errors.ts | 11 +- src/discovery/types.ts | 11 + src/discovery/utils.ts | 19 + src/gestalts/GestaltGraph.ts | 15 +- .../providers/github/GitHubProvider.ts | 43 +- src/identities/utils.ts | 37 +- .../js/polykey/v1/client_service_grpc_pb.d.ts | 74 +- .../js/polykey/v1/client_service_grpc_pb.js | 40 +- .../polykey/v1/identities/identities_pb.d.ts | 20 +- .../js/polykey/v1/identities/identities_pb.js | 152 +++- .../schemas/polykey/v1/client_service.proto | 6 +- .../polykey/v1/identities/identities.proto | 7 +- tests/bin/identities/identities.test.ts | 149 ++-- tests/client/rpcGestalts.test.ts | 36 +- .../gestaltsGestaltTrustByIdentity.test.ts | 450 ++++++++++++ .../gestaltsGestaltTrustByNode.test.ts | 332 +++++++++ .../identitiesInfoConnectedGet.test.ts | 655 ++++++++++++++++++ .../client/service/identitiesInfoGet.test.ts | 397 ++++++++++- .../identitiesInfoGetConnected.test.ts | 161 ----- tests/discovery/Discovery.test.ts | 295 ++++++-- tests/gestalts/GestaltGraph.test.ts | 32 +- tests/identities/IdentitiesManager.test.ts | 5 +- tests/identities/TestProvider.ts | 32 +- 38 files changed, 3348 insertions(+), 769 deletions(-) create mode 100644 src/client/service/gestaltsGestaltTrustByIdentity.ts create mode 100644 src/client/service/gestaltsGestaltTrustByNode.ts create mode 100644 src/client/service/identitiesInfoConnectedGet.ts delete mode 100644 src/client/service/identitiesInfoGetConnected.ts create mode 100644 src/discovery/types.ts create mode 100644 src/discovery/utils.ts create mode 100644 tests/client/service/gestaltsGestaltTrustByIdentity.test.ts create mode 100644 tests/client/service/gestaltsGestaltTrustByNode.test.ts create mode 100644 tests/client/service/identitiesInfoConnectedGet.test.ts delete mode 100644 tests/client/service/identitiesInfoGetConnected.test.ts diff --git a/src/PolykeyAgent.ts b/src/PolykeyAgent.ts index 192cb02ee5..866a33287b 100644 --- a/src/PolykeyAgent.ts +++ b/src/PolykeyAgent.ts @@ -2,7 +2,6 @@ import type { FileSystem } from './types'; import type { PolykeyWorkerManagerInterface } from './workers/types'; import type { Host, Port } from './network/types'; import type { NodeMapping } from './nodes/types'; - import type { RootKeyPairChangeData } from './keys/types'; import path from 'path'; import process from 'process'; @@ -266,11 +265,10 @@ class PolykeyAgent { logger: logger.getChild(NodeManager.name), fresh, })); - // Discovery uses in-memory CreateDestroy pattern - // Therefore it should be destroyed during stop discovery = discovery ?? (await Discovery.createDiscovery({ + db, gestaltGraph, identitiesManager, nodeManager, @@ -323,7 +321,7 @@ class PolykeyAgent { await sessionManager?.stop(); await notificationsManager?.stop(); await vaultManager?.stop(); - await discovery?.destroy(); + await discovery?.stop(); await nodeManager?.stop(); await revProxy?.stop(); await fwdProxy?.stop(); @@ -578,6 +576,7 @@ class PolykeyAgent { await this.nodeManager.start({ fresh }); await this.nodeManager.getConnectionsToSeedNodes(); await this.nodeManager.syncNodeGraph(); + await this.discovery.start({ fresh }); await this.vaultManager.start({ fresh }); await this.notificationsManager.start({ fresh }); await this.sessionManager.start({ fresh }); @@ -596,7 +595,7 @@ class PolykeyAgent { await this.sessionManager?.stop(); await this.notificationsManager?.stop(); await this.vaultManager?.stop(); - await this.discovery?.destroy(); + await this.discovery?.stop(); await this.nodeManager?.stop(); await this.revProxy?.stop(); await this.fwdProxy?.stop(); @@ -624,7 +623,7 @@ class PolykeyAgent { await this.sessionManager.stop(); await this.notificationsManager.stop(); await this.vaultManager.stop(); - await this.discovery.destroy(); + await this.discovery.stop(); await this.nodeManager.stop(); await this.revProxy.stop(); await this.fwdProxy.stop(); @@ -649,6 +648,7 @@ class PolykeyAgent { await this.sessionManager.destroy(); await this.notificationsManager.destroy(); await this.vaultManager.destroy(); + await this.discovery.destroy(); await this.nodeManager.destroy(); await this.gestaltGraph.destroy(); await this.acl.destroy(); diff --git a/src/bin/identities/CommandDiscover.ts b/src/bin/identities/CommandDiscover.ts index 9d08ca1051..c005a22607 100644 --- a/src/bin/identities/CommandDiscover.ts +++ b/src/bin/identities/CommandDiscover.ts @@ -10,9 +10,7 @@ class CommandDiscover extends CommandPolykey { constructor(...args: ConstructorParameters) { super(...args); this.name('discover'); - this.description( - 'Starts Discovery Process using Node or Identity as a Starting Point', - ); + this.description('Adds a Node or Identity to the Discovery Queue'); this.argument( '', 'Node ID or `Provider ID:Identity ID`', diff --git a/src/bin/identities/CommandSearch.ts b/src/bin/identities/CommandSearch.ts index 03b5a43939..67192de9b6 100644 --- a/src/bin/identities/CommandSearch.ts +++ b/src/bin/identities/CommandSearch.ts @@ -1,7 +1,9 @@ import type PolykeyClient from '../../PolykeyClient'; +import type { IdentityId, ProviderId } from '../../identities/types'; import CommandPolykey from '../CommandPolykey'; import * as binOptions from '../utils/options'; import * as binUtils from '../utils'; +import * as parsers from '../utils/parsers'; import * as binProcessors from '../utils/processors'; class CommandSearch extends CommandPolykey { @@ -10,13 +12,32 @@ class CommandSearch extends CommandPolykey { this.name('search'); this.description('Searches a Provider for any Connected Identities'); this.argument( - '', - 'Name of the digital identity provider to search on', + '[searchTerms...]', + 'Search parameters to apply to connected identities', + ); + this.option( + '-pi, --provider-id [providerId...]', + 'Digital identity provider(s) to search on', + parsers.parseProviderIdList, + ); + this.option( + '-ii, --identity-id [identityId]', + 'Name of the digital identity to search for', + parsers.parseIdentityId, + ); + this.option( + '-d, --disconnected', + 'Include disconnected identities in search', + ); + this.option( + '-l, --limit [number]', + 'Limit the number of search results to display to a specific number', + parsers.parseInteger, ); this.addOption(binOptions.nodeId); this.addOption(binOptions.clientHost); this.addOption(binOptions.clientPort); - this.action(async (providerId, options) => { + this.action(async (searchTerms, options) => { const { default: PolykeyClient } = await import('../../PolykeyClient'); const identitiesPB = await import( '../../proto/js/polykey/v1/identities/identities_pb' @@ -34,7 +55,11 @@ class CommandSearch extends CommandPolykey { this.fs, ); let pkClient: PolykeyClient; + let genReadable: ReturnType< + typeof pkClient.grpcClient.identitiesInfoConnectedGet + >; this.exitHandlers.handlers.push(async () => { + if (genReadable != null) genReadable.stream.cancel(); if (pkClient != null) await pkClient.stop(); }); try { @@ -45,25 +70,48 @@ class CommandSearch extends CommandPolykey { port: clientOptions.clientPort, logger: this.logger.getChild(PolykeyClient.name), }); - const providerMessage = new identitiesPB.Provider(); - providerMessage.setProviderId(providerId); - const res = await binUtils.retryAuthentication( - (auth) => - pkClient.grpcClient.identitiesInfoGet(providerMessage, auth), - meta, - ); - let output = ''; - if (res.getIdentityId() && res.getProviderId()) { - output = `${res.getProviderId()}:${res.getIdentityId()}`; + const providerSearchMessage = new identitiesPB.ProviderSearch(); + providerSearchMessage.setSearchTermList(searchTerms); + if (options.providerId) { + providerSearchMessage.setProviderIdList(options.providerId); + } + if (options.disconnected) { + providerSearchMessage.setDisconnected(true); } else { - this.logger.info('No Connected Identities found for Provider'); + providerSearchMessage.setDisconnected(false); + } + if (options.limit) { + providerSearchMessage.setLimit(options.limit); } - process.stdout.write( - binUtils.outputFormatter({ - type: options.format === 'json' ? 'json' : 'list', - data: [output], - }), - ); + await binUtils.retryAuthentication(async (auth) => { + if (options.identity) { + providerSearchMessage.setIdentityId(options.identity); + genReadable = pkClient.grpcClient.identitiesInfoGet( + providerSearchMessage, + auth, + ); + } else { + genReadable = pkClient.grpcClient.identitiesInfoConnectedGet( + providerSearchMessage, + auth, + ); + } + for await (const val of genReadable) { + const output = { + providerId: val.getProvider()!.getProviderId() as ProviderId, + identityId: val.getProvider()!.getIdentityId() as IdentityId, + name: val.getName(), + email: val.getEmail(), + url: val.getUrl(), + }; + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'dict', + data: output, + }), + ); + } + }, meta); } finally { if (pkClient! != null) await pkClient.stop(); } diff --git a/src/bin/identities/CommandTrust.ts b/src/bin/identities/CommandTrust.ts index 284f32de78..0fd432a811 100644 --- a/src/bin/identities/CommandTrust.ts +++ b/src/bin/identities/CommandTrust.ts @@ -24,9 +24,6 @@ class CommandTrust extends CommandPolykey { const identitiesPB = await import( '../../proto/js/polykey/v1/identities/identities_pb' ); - const permissionsPB = await import( - '../../proto/js/polykey/v1/permissions/permissions_pb' - ); const nodesPB = await import('../../proto/js/polykey/v1/nodes/nodes_pb'); const clientOptions = await binProcessors.processClientOptions( options.nodePath, @@ -52,32 +49,24 @@ class CommandTrust extends CommandPolykey { port: clientOptions.clientPort, logger: this.logger.getChild(PolykeyClient.name), }); - const action = 'notify'; - const setActionMessage = new permissionsPB.ActionSet(); - setActionMessage.setAction(action); if (gestaltId.type === 'node') { - // Setting by Node + // Setting by Node. const nodeMessage = new nodesPB.Node(); nodeMessage.setNodeId(gestaltId.nodeId); - setActionMessage.setNode(nodeMessage); await binUtils.retryAuthentication( (auth) => - pkClient.grpcClient.gestaltsActionsSetByNode( - setActionMessage, - auth, - ), + pkClient.grpcClient.gestaltsGestaltTrustByNode(nodeMessage, auth), meta, ); } else { // Setting by Identity const providerMessage = new identitiesPB.Provider(); - providerMessage.setProviderId(gestaltId.providerId!); - providerMessage.setIdentityId(gestaltId.identityId!); - setActionMessage.setIdentity(providerMessage); + providerMessage.setProviderId(gestaltId.providerId); + providerMessage.setIdentityId(gestaltId.identityId); await binUtils.retryAuthentication( (auth) => - pkClient.grpcClient.gestaltsActionsSetByIdentity( - setActionMessage, + pkClient.grpcClient.gestaltsGestaltTrustByIdentity( + providerMessage, auth, ), meta, diff --git a/src/bin/utils/parsers.ts b/src/bin/utils/parsers.ts index 132dbf62a0..298b827eab 100644 --- a/src/bin/utils/parsers.ts +++ b/src/bin/utils/parsers.ts @@ -28,6 +28,25 @@ function validateParserToArgParser( }; } +/** + * Converts a validation parser to commander variadic argument parser + */ +function validateParserToArgListParser( + validate: (data: string) => T, +): (data: string) => Array { + return (data: string) => { + try { + return data.split(' ').map(validate); + } catch (e) { + if (e instanceof validationErrors.ErrorParse) { + throw new commander.InvalidArgumentError(e.message); + } else { + throw e; + } + } + }; +} + const parseInteger = validateParserToArgParser(validationUtils.parseInteger); const parseNumber = validateParserToArgParser(validationUtils.parseNumber); const parseNodeId = validateParserToArgParser(validationUtils.parseNodeId); @@ -40,6 +59,13 @@ const parseHostOrHostname = validateParserToArgParser( validationUtils.parseHostOrHostname, ); const parsePort = validateParserToArgParser(validationUtils.parsePort); +const parseIdentityId = validateParserToArgParser( + validationUtils.parseIdentityId, +); + +const parseProviderIdList = validateParserToArgListParser( + validationUtils.parseProviderId, +); function parseCoreCount(v: string): number | undefined { if (v === 'all') { @@ -159,4 +185,6 @@ export { getDefaultSeedNodes, parseSeedNodes, parseNetwork, + parseProviderIdList, + parseIdentityId, }; diff --git a/src/client/GRPCClientClient.ts b/src/client/GRPCClientClient.ts index 0006ca5456..819a780302 100644 --- a/src/client/GRPCClientClient.ts +++ b/src/client/GRPCClientClient.ts @@ -19,7 +19,8 @@ import Logger from '@matrixai/logger'; import * as clientErrors from './errors'; import * as clientUtils from './utils'; import { ClientServiceClient } from '../proto/js/polykey/v1/client_service_grpc_pb'; -import { GRPCClient, utils as grpcUtils } from '../grpc'; +import { GRPCClient } from '../grpc'; +import * as grpcUtils from '../grpc/utils'; interface GRPCClientClient extends CreateDestroy {} @CreateDestroy() @@ -486,6 +487,22 @@ class GRPCClientClient extends GRPCClient { )(...args); } + @ready(new clientErrors.ErrorClientClientDestroyed()) + public gestaltsGestaltTrustByNode(...args) { + return grpcUtils.promisifyUnaryCall( + this.client, + this.client.gestaltsGestaltTrustByNode, + )(...args); + } + + @ready(new clientErrors.ErrorClientClientDestroyed()) + public gestaltsGestaltTrustByIdentity(...args) { + return grpcUtils.promisifyUnaryCall( + this.client, + this.client.gestaltsGestaltTrustByIdentity, + )(...args); + } + @ready(new clientErrors.ErrorClientClientDestroyed()) public identitiesTokenPut(...args) { return grpcUtils.promisifyUnaryCall( @@ -559,16 +576,16 @@ class GRPCClientClient extends GRPCClient { } @ready(new clientErrors.ErrorClientClientDestroyed()) - public identitiesInfoGetConnected(...args) { + public identitiesInfoConnectedGet(...args) { return grpcUtils.promisifyReadableStreamCall( this.client, - this.client.identitiesInfoGetConnected, + this.client.identitiesInfoConnectedGet, )(...args); } @ready(new clientErrors.ErrorClientClientDestroyed()) public identitiesInfoGet(...args) { - return grpcUtils.promisifyUnaryCall( + return grpcUtils.promisifyReadableStreamCall( this.client, this.client.identitiesInfoGet, )(...args); diff --git a/src/client/service/gestaltsDiscoveryByIdentity.ts b/src/client/service/gestaltsDiscoveryByIdentity.ts index 75199dfb79..4ebeae0ce8 100644 --- a/src/client/service/gestaltsDiscoveryByIdentity.ts +++ b/src/client/service/gestaltsDiscoveryByIdentity.ts @@ -3,9 +3,10 @@ import type { Authenticate } from '../types'; import type { Discovery } from '../../discovery'; import type { IdentityId, ProviderId } from '../../identities/types'; import type * as identitiesPB from '../../proto/js/polykey/v1/identities/identities_pb'; -import { utils as grpcUtils } from '../../grpc'; -import { validateSync, utils as validationUtils } from '../../validation'; +import { validateSync } from '../../validation'; import { matchSync } from '../../utils'; +import * as grpcUtils from '../../grpc/utils'; +import * as validationUtils from '../../validation/utils'; import * as utilsPB from '../../proto/js/polykey/v1/utils/utils_pb'; function gestaltsDiscoveryByIdentity({ @@ -42,10 +43,7 @@ function gestaltsDiscoveryByIdentity({ identityId: call.request.getIdentityId(), }, ); - const gen = discovery.discoverGestaltByIdentity(providerId, identityId); - for await (const _ of gen) { - // Empty - } + await discovery.queueDiscoveryByIdentity(providerId, identityId); callback(null, response); return; } catch (e) { diff --git a/src/client/service/gestaltsDiscoveryByNode.ts b/src/client/service/gestaltsDiscoveryByNode.ts index 8cb8910ae0..c8f9001412 100644 --- a/src/client/service/gestaltsDiscoveryByNode.ts +++ b/src/client/service/gestaltsDiscoveryByNode.ts @@ -3,9 +3,10 @@ import type { Authenticate } from '../types'; import type { Discovery } from '../../discovery'; import type { NodeId } from '../../nodes/types'; import type * as nodesPB from '../../proto/js/polykey/v1/nodes/nodes_pb'; -import { utils as grpcUtils } from '../../grpc'; -import { validateSync, utils as validationUtils } from '../../validation'; +import { validateSync } from '../../validation'; import { matchSync } from '../../utils'; +import * as grpcUtils from '../../grpc/utils'; +import * as validationUtils from '../../validation/utils'; import * as utilsPB from '../../proto/js/polykey/v1/utils/utils_pb'; function gestaltsDiscoveryByNode({ @@ -38,10 +39,7 @@ function gestaltsDiscoveryByNode({ nodeId: call.request.getNodeId(), }, ); - const gen = discovery.discoverGestaltByNode(nodeId); - for await (const _ of gen) { - // Empty - } + await discovery.queueDiscoveryByNode(nodeId); callback(null, response); return; } catch (e) { diff --git a/src/client/service/gestaltsGestaltTrustByIdentity.ts b/src/client/service/gestaltsGestaltTrustByIdentity.ts new file mode 100644 index 0000000000..ee4078c197 --- /dev/null +++ b/src/client/service/gestaltsGestaltTrustByIdentity.ts @@ -0,0 +1,78 @@ +import type * as grpc from '@grpc/grpc-js'; +import type { Authenticate } from '../types'; +import type { GestaltGraph } from '../../gestalts'; +import type * as identitiesPB from '../../proto/js/polykey/v1/identities/identities_pb'; +import type { IdentityId, ProviderId } from '../../identities/types'; +import type { Discovery } from '../../discovery'; +import { validateSync } from '../../validation'; +import { matchSync } from '../../utils'; +import * as grpcUtils from '../../grpc/utils'; +import * as validationUtils from '../../validation/utils'; +import * as utilsPB from '../../proto/js/polykey/v1/utils/utils_pb'; + +function gestaltsGestaltTrustByIdentity({ + authenticate, + gestaltGraph, + discovery, +}: { + authenticate: Authenticate; + gestaltGraph: GestaltGraph; + discovery: Discovery; +}) { + return async ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData, + ): Promise => { + try { + const response = new utilsPB.EmptyMessage(); + const metadata = await authenticate(call.metadata); + call.sendMetadata(metadata); + const { + providerId, + identityId, + }: { + providerId: ProviderId; + identityId: IdentityId; + } = validateSync( + (keyPath, value) => { + return matchSync(keyPath)( + [['providerId'], () => validationUtils.parseProviderId(value)], + [['identityId'], () => validationUtils.parseIdentityId(value)], + () => value, + ); + }, + { + providerId: call.request.getProviderId(), + identityId: call.request.getIdentityId(), + }, + ); + // Set the identity in the gestalt graph if not already + if ( + (await gestaltGraph.getGestaltByIdentity(providerId, identityId)) == + null + ) { + // Queue the new identity for discovery + // This will only add the identity to the GG if it is connected to a + // node (required to set permissions for it) + await discovery.queueDiscoveryByIdentity(providerId, identityId); + } + // We can currently only set permissions for identities that are + // connected to at least one node. If these conditions are not met, this + // will throw an error. Since discovery can take time, you may need to + // reattempt this command if it fails on the first attempt and you expect + // there to be a linked node for the identity. + await gestaltGraph.setGestaltActionByIdentity( + providerId, + identityId, + 'notify', + ); + callback(null, response); + return; + } catch (e) { + callback(grpcUtils.fromError(e)); + return; + } + }; +} + +export default gestaltsGestaltTrustByIdentity; diff --git a/src/client/service/gestaltsGestaltTrustByNode.ts b/src/client/service/gestaltsGestaltTrustByNode.ts new file mode 100644 index 0000000000..1b8497ee4a --- /dev/null +++ b/src/client/service/gestaltsGestaltTrustByNode.ts @@ -0,0 +1,66 @@ +import type * as grpc from '@grpc/grpc-js'; +import type { Authenticate } from '../types'; +import type { GestaltGraph } from '../../gestalts'; +import type { Discovery } from '../../discovery'; +import type * as nodesPB from '../../proto/js/polykey/v1/nodes/nodes_pb'; +import type { NodeId } from '../../nodes/types'; +import { validateSync } from '../../validation'; +import { matchSync } from '../../utils'; +import * as grpcUtils from '../../grpc/utils'; +import * as validationUtils from '../../validation/utils'; +import * as utilsPB from '../../proto/js/polykey/v1/utils/utils_pb'; +import * as nodesUtils from '../../nodes/utils'; + +function gestaltsGestaltTrustByIdentity({ + authenticate, + gestaltGraph, + discovery, +}: { + authenticate: Authenticate; + gestaltGraph: GestaltGraph; + discovery: Discovery; +}) { + return async ( + call: grpc.ServerUnaryCall, + callback: grpc.sendUnaryData, + ): Promise => { + try { + const response = new utilsPB.EmptyMessage(); + const metadata = await authenticate(call.metadata); + call.sendMetadata(metadata); + const { + nodeId, + }: { + nodeId: NodeId; + } = validateSync( + (keyPath, value) => { + return matchSync(keyPath)( + [['nodeId'], () => validationUtils.parseNodeId(value)], + () => value, + ); + }, + { + nodeId: call.request.getNodeId(), + }, + ); + // Set the node in the gestalt graph if not already + if ((await gestaltGraph.getGestaltByNode(nodeId)) == null) { + await gestaltGraph.setNode({ + id: nodesUtils.encodeNodeId(nodeId), + chain: {}, + }); + // Queue the new node for discovery + await discovery.queueDiscoveryByNode(nodeId); + } + // Set notify permission + await gestaltGraph.setGestaltActionByNode(nodeId, 'notify'); + callback(null, response); + return; + } catch (e) { + callback(grpcUtils.fromError(e)); + return; + } + }; +} + +export default gestaltsGestaltTrustByIdentity; diff --git a/src/client/service/identitiesInfoConnectedGet.ts b/src/client/service/identitiesInfoConnectedGet.ts new file mode 100644 index 0000000000..f195743985 --- /dev/null +++ b/src/client/service/identitiesInfoConnectedGet.ts @@ -0,0 +1,103 @@ +import type * as grpc from '@grpc/grpc-js'; +import type { Authenticate } from '../types'; +import type { IdentitiesManager } from '../../identities'; +import type { IdentityData, ProviderId } from '../../identities/types'; +import { validateSync } from '../../validation'; +import { matchSync } from '../../utils'; +import * as grpcUtils from '../../grpc/utils'; +import * as validationUtils from '../../validation/utils'; +import * as identitiesErrors from '../../identities/errors'; +import * as identitiesPB from '../../proto/js/polykey/v1/identities/identities_pb'; + +function identitiesInfoConnectedGet({ + identitiesManager, + authenticate, +}: { + identitiesManager: IdentitiesManager; + authenticate: Authenticate; +}) { + return async ( + call: grpc.ServerWritableStream< + identitiesPB.ProviderSearch, + identitiesPB.Info + >, + ): Promise => { + const genWritable = grpcUtils.generatorWritable(call); + try { + const metadata = await authenticate(call.metadata); + call.sendMetadata(metadata); + const { + providerIds, + }: { + providerIds: Array; + } = validateSync( + (keyPath, value) => { + return matchSync(keyPath)( + [['providerIds'], () => value.map(validationUtils.parseProviderId)], + () => value, + ); + }, + { + providerIds: call.request.getProviderIdList(), + }, + ); + // Process options that were set + if (providerIds.length === 0) { + Object.keys(identitiesManager.getProviders()).forEach((id) => + providerIds.push(id as ProviderId), + ); + } + const getDisconnected = call.request.getDisconnected(); + if (getDisconnected) { + // Can only get connected identities at this stage + throw new identitiesErrors.ErrorProviderUnimplemented(); + } + const identities: Array> = []; + for (const providerId of providerIds) { + // Get provider from id + const provider = identitiesManager.getProvider(providerId); + if (provider === undefined) + throw new identitiesErrors.ErrorProviderMissing(); + // Get our own authenticated identity in order to query, skip provider + // if not authenticated + const authIdentities = await provider.getAuthIdentityIds(); + if (authIdentities.length === 0) { + break; + } + identities.push( + provider.getConnectedIdentityDatas( + authIdentities[0], + call.request.getSearchTermList(), + ), + ); + } + let limit: number | undefined; + if (call.request.getLimit() !== '') { + limit = parseInt(call.request.getLimit()); + } + let count = 0; + for (const gen of identities) { + for await (const identity of gen) { + if (limit !== undefined && count >= limit) break; + const identityInfoMessage = new identitiesPB.Info(); + const providerMessage = new identitiesPB.Provider(); + providerMessage.setProviderId(identity.providerId); + providerMessage.setIdentityId(identity.identityId); + identityInfoMessage.setProvider(providerMessage); + identityInfoMessage.setName(identity.name ?? ''); + identityInfoMessage.setEmail(identity.email ?? ''); + identityInfoMessage.setUrl(identity.url ?? ''); + await genWritable.next(identityInfoMessage); + count++; + } + } + await genWritable.next(null); + return; + } catch (e) { + await genWritable.throw(e); + return; + } + }; +} + +export default identitiesInfoConnectedGet; diff --git a/src/client/service/identitiesInfoGet.ts b/src/client/service/identitiesInfoGet.ts index dcbaac80dd..c24503573a 100644 --- a/src/client/service/identitiesInfoGet.ts +++ b/src/client/service/identitiesInfoGet.ts @@ -1,15 +1,19 @@ import type * as grpc from '@grpc/grpc-js'; import type { Authenticate } from '../types'; import type { IdentitiesManager } from '../../identities'; -import type { ProviderId } from '../../identities/types'; -import { utils as grpcUtils } from '../../grpc'; -import { validateSync, utils as validationUtils } from '../../validation'; +import type { + IdentityData, + IdentityId, + ProviderId, +} from '../../identities/types'; +import { validateSync } from '../../validation'; import { matchSync } from '../../utils'; +import * as grpcUtils from '../../grpc/utils'; +import * as validationUtils from '../../validation/utils'; +import * as identitiesUtils from '../../identities/utils'; +import * as identitiesErrors from '../../identities/errors'; import * as identitiesPB from '../../proto/js/polykey/v1/identities/identities_pb'; -/** - * Gets the first identityId of the local keynode. - */ function identitiesInfoGet({ identitiesManager, authenticate, @@ -18,40 +22,90 @@ function identitiesInfoGet({ authenticate: Authenticate; }) { return async ( - call: grpc.ServerUnaryCall, - callback: grpc.sendUnaryData, + call: grpc.ServerWritableStream< + identitiesPB.ProviderSearch, + identitiesPB.Info + >, ): Promise => { + const genWritable = grpcUtils.generatorWritable(call); try { - const response = new identitiesPB.Provider(); const metadata = await authenticate(call.metadata); call.sendMetadata(metadata); const { - providerId, + providerIds, + identityId, }: { - providerId: ProviderId; + providerIds: Array; + identityId: IdentityId; } = validateSync( (keyPath, value) => { return matchSync(keyPath)( - [['providerId'], () => validationUtils.parseProviderId(value)], + [['providerIds'], () => value.map(validationUtils.parseProviderId)], + [['identityId'], () => validationUtils.parseIdentityId(value)], () => value, ); }, { - providerId: call.request.getProviderId(), + providerIds: call.request.getProviderIdList(), + identityId: call.request.getIdentityId(), }, ); - const provider = identitiesManager.getProvider(providerId); - if (provider !== undefined) { - const identities = await provider.getAuthIdentityIds(); - response.setProviderId(providerId); - if (identities.length !== 0) { - response.setIdentityId(identities[0]); + // Process options that were set + if (providerIds.length === 0) { + Object.keys(identitiesManager.getProviders()).forEach((id) => + providerIds.push(id as ProviderId), + ); + } + const searchTerms = call.request.getSearchTermList(); + const getDisconnected = call.request.getDisconnected(); + if (getDisconnected) { + // Currently this command performs the same way regardless of whether + // this option is set (i.e. always disconnected) + } + const identities: Array = []; + for (const providerId of providerIds) { + // Get provider from id + const provider = identitiesManager.getProvider(providerId); + if (provider === undefined) + throw new identitiesErrors.ErrorProviderMissing(); + // Get our own authenticated identity in order to query, skip provider + // if not authenticated + const authIdentities = await provider.getAuthIdentityIds(); + if (authIdentities.length === 0) { + break; + } + // Get identity data + identities.push( + await provider.getIdentityData(authIdentities[0], identityId), + ); + } + let limit: number | undefined; + if (call.request.getLimit() !== '') { + limit = parseInt(call.request.getLimit()); + } + if (limit === undefined || limit > identities.length) { + limit = identities.length; + } + for (let i = 0; i < limit; i++) { + const identity = identities[i]; + if (identity !== undefined) { + if (identitiesUtils.matchIdentityData(identity, searchTerms)) { + const identityInfoMessage = new identitiesPB.Info(); + const providerMessage = new identitiesPB.Provider(); + providerMessage.setProviderId(identity.providerId); + providerMessage.setIdentityId(identity.identityId); + identityInfoMessage.setProvider(providerMessage); + identityInfoMessage.setName(identity.name ?? ''); + identityInfoMessage.setEmail(identity.email ?? ''); + identityInfoMessage.setUrl(identity.url ?? ''); + await genWritable.next(identityInfoMessage); + } } } - callback(null, response); + await genWritable.next(null); return; } catch (e) { - callback(grpcUtils.fromError(e)); + await genWritable.throw(e); return; } }; diff --git a/src/client/service/identitiesInfoGetConnected.ts b/src/client/service/identitiesInfoGetConnected.ts deleted file mode 100644 index 22ab3124a3..0000000000 --- a/src/client/service/identitiesInfoGetConnected.ts +++ /dev/null @@ -1,75 +0,0 @@ -import type * as grpc from '@grpc/grpc-js'; -import type { Authenticate } from '../types'; -import type { IdentitiesManager } from '../../identities'; -import type { IdentityId, ProviderId } from '../../identities/types'; -import { utils as grpcUtils } from '../../grpc'; -import { errors as identitiesErrors } from '../../identities'; -import { validateSync, utils as validationUtils } from '../../validation'; -import { matchSync } from '../../utils'; -import * as identitiesPB from '../../proto/js/polykey/v1/identities/identities_pb'; - -function identitiesInfoGetConnected({ - identitiesManager, - authenticate, -}: { - identitiesManager: IdentitiesManager; - authenticate: Authenticate; -}) { - return async ( - call: grpc.ServerWritableStream< - identitiesPB.ProviderSearch, - identitiesPB.Info - >, - ): Promise => { - const genWritable = grpcUtils.generatorWritable(call); - try { - const metadata = await authenticate(call.metadata); - call.sendMetadata(metadata); - const { - providerId, - identityId, - }: { - providerId: ProviderId; - identityId: IdentityId; - } = validateSync( - (keyPath, value) => { - return matchSync(keyPath)( - [['providerId'], () => validationUtils.parseProviderId(value)], - [['identityId'], () => validationUtils.parseIdentityId(value)], - () => value, - ); - }, - { - providerId: call.request.getProvider()?.getProviderId(), - identityId: call.request.getProvider()?.getIdentityId(), - }, - ); - const provider = identitiesManager.getProvider(providerId); - if (provider == null) { - throw new identitiesErrors.ErrorProviderMissing(); - } - const identities = provider.getConnectedIdentityDatas( - identityId, - call.request.getSearchTermList(), - ); - for await (const identity of identities) { - const identityInfoMessage = new identitiesPB.Info(); - const providerMessage = new identitiesPB.Provider(); - providerMessage.setProviderId(identity.providerId); - providerMessage.setIdentityId(identity.identityId); - identityInfoMessage.setProvider(providerMessage); - identityInfoMessage.setName(identity.name ?? ''); - identityInfoMessage.setEmail(identity.email ?? ''); - identityInfoMessage.setUrl(identity.url ?? ''); - await genWritable.next(identityInfoMessage); - } - await genWritable.next(null); - return; - } catch (e) { - await genWritable.throw(e); - return; - } - }; -} - -export default identitiesInfoGetConnected; diff --git a/src/client/service/index.ts b/src/client/service/index.ts index 88a6ca8610..91ced4423f 100644 --- a/src/client/service/index.ts +++ b/src/client/service/index.ts @@ -28,10 +28,12 @@ import gestaltsDiscoveryByNode from './gestaltsDiscoveryByNode'; import gestaltsGestaltGetByIdentity from './gestaltsGestaltGetByIdentity'; import gestaltsGestaltGetByNode from './gestaltsGestaltGetByNode'; import gestaltsGestaltList from './gestaltsGestaltList'; +import gestaltsGestaltTrustByIdentity from './gestaltsGestaltTrustByIdentity'; +import gestaltsGestaltTrustByNode from './gestaltsGestaltTrustByNode'; import identitiesAuthenticate from './identitiesAuthenticate'; import identitiesClaim from './identitiesClaim'; import identitiesInfoGet from './identitiesInfoGet'; -import identitiesInfoGetConnected from './identitiesInfoGetConnected'; +import identitiesInfoConnectedGet from './identitiesInfoConnectedGet'; import identitiesProvidersList from './identitiesProvidersList'; import identitiesTokenDelete from './identitiesTokenDelete'; import identitiesTokenGet from './identitiesTokenGet'; @@ -126,10 +128,12 @@ function createService({ gestaltsGestaltGetByIdentity: gestaltsGestaltGetByIdentity(container), gestaltsGestaltGetByNode: gestaltsGestaltGetByNode(container), gestaltsGestaltList: gestaltsGestaltList(container), + gestaltsGestaltTrustByIdentity: gestaltsGestaltTrustByIdentity(container), + gestaltsGestaltTrustByNode: gestaltsGestaltTrustByNode(container), identitiesAuthenticate: identitiesAuthenticate(container), identitiesClaim: identitiesClaim(container), identitiesInfoGet: identitiesInfoGet(container), - identitiesInfoGetConnected: identitiesInfoGetConnected(container), + identitiesInfoConnectedGet: identitiesInfoConnectedGet(container), identitiesProvidersList: identitiesProvidersList(container), identitiesTokenDelete: identitiesTokenDelete(container), identitiesTokenGet: identitiesTokenGet(container), diff --git a/src/discovery/Discovery.ts b/src/discovery/Discovery.ts index 16985e8b24..091bc58726 100644 --- a/src/discovery/Discovery.ts +++ b/src/discovery/Discovery.ts @@ -1,3 +1,4 @@ +import type { DiscoveryQueueId, DiscoveryQueueIdGenerator } from './types'; import type { NodeId, NodeInfo } from '../nodes/types'; import type { GestaltGraph } from '../gestalts'; import type { GestaltKey } from '../gestalts/types'; @@ -11,252 +12,400 @@ import type { import type { NodeManager } from '../nodes'; import type { Provider, IdentitiesManager } from '../identities'; import type { Claim, ClaimIdEncoded, ClaimLinkIdentity } from '../claims/types'; - import type { ChainData } from '../sigchain/types'; +import type { DB, DBLevel } from '@matrixai/db'; +import type { ResourceAcquire } from '../utils'; +import type { MutexInterface } from 'async-mutex'; import Logger from '@matrixai/logger'; -import { CreateDestroy, ready } from '@matrixai/async-init/dist/CreateDestroy'; +import { + CreateDestroyStartStop, + ready, + status, +} from '@matrixai/async-init/dist/CreateDestroyStartStop'; +import { IdInternal } from '@matrixai/id'; +import { Mutex } from 'async-mutex'; +import * as idUtils from '@matrixai/id/dist/utils'; +import * as discoveryUtils from './utils'; import * as discoveryErrors from './errors'; +import * as utils from '../utils'; import * as gestaltsUtils from '../gestalts/utils'; import * as claimsUtils from '../claims/utils'; -import { utils as nodesUtils } from '../nodes'; +import * as nodesUtils from '../nodes/utils'; -interface Discovery extends CreateDestroy {} -@CreateDestroy() +interface Discovery extends CreateDestroyStartStop {} +@CreateDestroyStartStop( + new discoveryErrors.ErrorDiscoveryRunning(), + new discoveryErrors.ErrorDiscoveryDestroyed(), +) class Discovery { - protected gestaltGraph: GestaltGraph; - protected identitiesManager: IdentitiesManager; - protected nodeManager: NodeManager; - protected logger: Logger; - static async createDiscovery({ + db, gestaltGraph, identitiesManager, nodeManager, logger = new Logger(this.name), + fresh = false, }: { + db: DB; gestaltGraph: GestaltGraph; identitiesManager: IdentitiesManager; nodeManager: NodeManager; logger?: Logger; + fresh?: boolean; }): Promise { logger.info(`Creating ${this.name}`); const discovery = new Discovery({ + db, gestaltGraph, identitiesManager, - logger: logger, nodeManager, + logger, }); + await discovery.start({ fresh }); logger.info(`Created ${this.name}`); return discovery; } - constructor({ + protected logger: Logger; + protected db: DB; + protected gestaltGraph: GestaltGraph; + protected identitiesManager: IdentitiesManager; + protected nodeManager: NodeManager; + protected discoveryDbDomain: string = this.constructor.name; + protected discoveryQueueDbDomain: Array = [ + this.discoveryDbDomain, + 'queue', + ]; + protected discoveryDb: DBLevel; + protected discoveryQueueDb: DBLevel; + protected lock: Mutex = new Mutex(); + protected discoveryQueueIdGenerator: DiscoveryQueueIdGenerator; + protected visitedVertices = new Set(); + protected discoveryQueue: AsyncGenerator; + protected discoveryProcess: Promise; + protected queuePlug: Mutex = new Mutex(); + protected queuePlugRelease: MutexInterface.Releaser | undefined; + + public constructor({ + db, gestaltGraph, identitiesManager, nodeManager, logger, }: { + db: DB; gestaltGraph: GestaltGraph; identitiesManager: IdentitiesManager; nodeManager: NodeManager; logger: Logger; }) { + this.logger = logger; + this.db = db; this.gestaltGraph = gestaltGraph; this.identitiesManager = identitiesManager; this.nodeManager = nodeManager; - this.logger = logger; + } + + public async start({ + fresh = false, + }: { + fresh?: boolean; + } = {}): Promise { + this.logger.info(`Starting ${this.constructor.name}`); + const discoveryDb = await this.db.level(this.discoveryDbDomain); + // Queue stores DiscoveryQueueId -> GestaltKey + const discoveryQueueDb = await this.db.level( + this.discoveryQueueDbDomain[1], + discoveryDb, + ); + if (fresh) { + await discoveryDb.clear(); + } + this.discoveryDb = discoveryDb; + this.discoveryQueueDb = discoveryQueueDb; + // Getting latest ID and creating ID generator + let latestId: DiscoveryQueueId | undefined; + const keyStream = this.discoveryQueueDb.createKeyStream({ + limit: 1, + reverse: true, + }); + for await (const o of keyStream) { + latestId = IdInternal.fromBuffer(o); + } + this.discoveryQueueIdGenerator = + discoveryUtils.createDiscoveryQueueIdGenerator(latestId); + this.discoveryQueue = this.setupDiscoveryQueue(); + this.discoveryProcess = this.runDiscoveryQueue(); + this.logger.info(`Started ${this.constructor.name}`); + } + + public async stop(): Promise { + this.logger.info(`Stopping ${this.constructor.name}`); + if (this.queuePlugRelease != null) { + this.queuePlugRelease(); + } + await this.discoveryProcess; + this.logger.info(`Stopped ${this.constructor.name}`); } public async destroy() { this.logger.info(`Destroying ${this.constructor.name}`); + const discoveryDb = await this.db.level(this.discoveryDbDomain); + await discoveryDb.clear(); this.logger.info(`Destroyed ${this.constructor.name}`); } - @ready(new discoveryErrors.ErrorDiscoveryDestroyed()) - public discoverGestaltByNode(nodeId: NodeId) { + + public transaction: ResourceAcquire = async () => { + const release = await this.lock.acquire(); + return [async () => release(), this]; + }; + + /** + * Queues a node for discovery. Internally calls `pushKeyToDiscoveryQueue`. + */ + @ready(new discoveryErrors.ErrorDiscoveryNotRunning()) + public async queueDiscoveryByNode(nodeId: NodeId) { const nodeKey = gestaltsUtils.keyFromNode(nodeId); - return this.discoverGestalt(nodeKey); + await this.pushKeyToDiscoveryQueue(nodeKey); } - @ready(new discoveryErrors.ErrorDiscoveryDestroyed()) - public discoverGestaltByIdentity( + /** + * Queues an identity for discovery. Internally calls + * `pushKeyToDiscoveryQueue`. + */ + @ready(new discoveryErrors.ErrorDiscoveryNotRunning()) + public async queueDiscoveryByIdentity( providerId: ProviderId, identityId: IdentityId, - ): AsyncGenerator { + ) { const identityKey = gestaltsUtils.keyFromIdentity(providerId, identityId); - return this.discoverGestalt(identityKey); + await this.pushKeyToDiscoveryQueue(identityKey); } - protected async *discoverGestalt( - gK: GestaltKey, - ): AsyncGenerator { - const vertexQueue = [gK]; - const visitedVertices = new Set(); - + /** + * Generator for the logic of iterating through the Discovery Queue. + */ + public async *setupDiscoveryQueue(): AsyncGenerator { while (true) { - // Get the next vertex discovered to be in the gestalt - const vertex = vertexQueue.shift(); - if (vertex == null) { - break; - } - - const vertexGId = gestaltsUtils.ungestaltKey(vertex); - if (vertexGId.type === 'node') { - // If the next vertex is a node, find its cryptolinks - // const linkInfos = await this.nodeManager.getCryptolinks(nodeInfo.id); - // need some public API to: - // 1. create connection - // 2. get certificate chain (eventually sigchain) - // 3. get the cryptolinks - // If cannot create connection, then throw some kind of exception - // User display = "Cannot crawl gestalt graph. Please try again later" - - // The sigchain data of the vertex (containing all cryptolinks) - let vertexChainData: ChainData = {}; - // If the vertex we've found is our own node, we simply get our own chain - const nodeId = nodesUtils.decodeNodeId(vertexGId.nodeId)!; - if (nodeId.equals(this.nodeManager.getNodeId())) { - const vertexChainDataEncoded = await this.nodeManager.getChainData(); - // Decode all our claims - no need to verify (on our own sigchain) - for (const c in vertexChainDataEncoded) { - const claimId = c as ClaimIdEncoded; - vertexChainData[claimId] = claimsUtils.decodeClaim( - vertexChainDataEncoded[claimId], - ); - } - // Otherwise, request the verified chain data from the node - } else { - vertexChainData = await this.nodeManager.requestChainData(nodeId); - } - - // TODO: for now, the chain data is treated as a 'disjoint' set of - // cryptolink claims from a node to another node/identity - // That is, we have no notion of revokations, or multiple claims to the - // same node/identity. Thus, we simply iterate over this chain of - // cryptolinks. - - // Now have the NodeInfo of this vertex - const vertexNodeInfo: NodeInfo = { - id: nodesUtils.encodeNodeId(nodeId), - chain: vertexChainData, - }; - - // Iterate over each of the claims in the chain (already verified) - // TODO: because we're iterating over keys in a record, I don't believe - // that this will iterate in lexicographical order of keys. For now, - // this doesn't matter though (because of the previous comment). - for (const claimId in vertexChainData) { - const claim: Claim = vertexChainData[claimId as ClaimIdEncoded]; - - // If the claim is to a node - if (claim.payload.data.type === 'node') { - // Get the chain data of the linked node - const linkedVertexNodeId = nodesUtils.decodeNodeId( - claim.payload.data.node2, - )!; - const linkedVertexChainData = - await this.nodeManager.requestChainData(linkedVertexNodeId); - // With this verified chain, we can link - const linkedVertexNodeInfo: NodeInfo = { - id: nodesUtils.encodeNodeId(linkedVertexNodeId), - chain: linkedVertexChainData, + if (!(await this.queueIsEmpty())) { + for await (const o of this.discoveryQueueDb.createReadStream()) { + const vertexId = IdInternal.fromBuffer(o.key) as DiscoveryQueueId; + const data = o.value as Buffer; + const vertex = await this.db.deserializeDecrypt( + data, + false, + ); + const vertexGId = gestaltsUtils.ungestaltKey(vertex); + if (vertexGId.type === 'node') { + // The sigchain data of the vertex (containing all cryptolinks) + let vertexChainData: ChainData = {}; + // If the vertex we've found is our own node, we simply get our own chain + const nodeId = nodesUtils.decodeNodeId(vertexGId.nodeId)!; + if (nodeId.equals(this.nodeManager.getNodeId())) { + const vertexChainDataEncoded = + await this.nodeManager.getChainData(); + // Decode all our claims - no need to verify (on our own sigchain) + for (const c in vertexChainDataEncoded) { + const claimId = c as ClaimIdEncoded; + vertexChainData[claimId] = claimsUtils.decodeClaim( + vertexChainDataEncoded[claimId], + ); + } + // Otherwise, request the verified chain data from the node + } else { + vertexChainData = await this.nodeManager.requestChainData(nodeId); + } + // TODO: for now, the chain data is treated as a 'disjoint' set of + // cryptolink claims from a node to another node/identity + // That is, we have no notion of revocations, or multiple claims to the + // same node/identity. Thus, we simply iterate over this chain of + // cryptolinks. + // Now have the NodeInfo of this vertex + const vertexNodeInfo: NodeInfo = { + id: nodesUtils.encodeNodeId(nodeId), + chain: vertexChainData, }; - await this.gestaltGraph.linkNodeAndNode( - vertexNodeInfo, - linkedVertexNodeInfo, - ); - - // Add this vertex to the queue if it hasn't already been visited - const linkedVertexGK = - gestaltsUtils.keyFromNode(linkedVertexNodeId); - if (!visitedVertices.has(linkedVertexGK)) { - vertexQueue.push(linkedVertexGK); + // Iterate over each of the claims in the chain (already verified) + // TODO: because we're iterating over keys in a record, I don't believe + // that this will iterate in lexicographical order of keys. For now, + // this doesn't matter though (because of the previous comment). + for (const claimId in vertexChainData) { + const claim: Claim = vertexChainData[claimId as ClaimIdEncoded]; + // If the claim is to a node + if (claim.payload.data.type === 'node') { + // Get the chain data of the linked node + // Could be node1 or node2 in the claim so get the one that's + // not equal to nodeId from above + const node1Id = nodesUtils.decodeNodeId( + claim.payload.data.node1, + )!; + const node2Id = nodesUtils.decodeNodeId( + claim.payload.data.node2, + )!; + const linkedVertexNodeId = node1Id.equals(nodeId) + ? node2Id + : node1Id; + const linkedVertexChainData = + await this.nodeManager.requestChainData(linkedVertexNodeId); + // With this verified chain, we can link + const linkedVertexNodeInfo: NodeInfo = { + id: nodesUtils.encodeNodeId(linkedVertexNodeId), + chain: linkedVertexChainData, + }; + await this.gestaltGraph.linkNodeAndNode( + vertexNodeInfo, + linkedVertexNodeInfo, + ); + // Add this vertex to the queue if it hasn't already been visited + const linkedVertexGK = + gestaltsUtils.keyFromNode(linkedVertexNodeId); + if (!this.visitedVertices.has(linkedVertexGK)) { + await this.pushKeyToDiscoveryQueue(linkedVertexGK); + } + } + // Else the claim is to an identity + if (claim.payload.data.type === 'identity') { + // Attempt to get the identity info on the identity provider + const identityInfo = await this.getIdentityInfo( + claim.payload.data.provider, + claim.payload.data.identity, + ); + // If we can't get identity info, simply skip this claim + if (identityInfo == null) { + continue; + } + // Link the node to the found identity info + await this.gestaltGraph.linkNodeAndIdentity( + vertexNodeInfo, + identityInfo, + ); + // Add this identity vertex to the queue if it is not present + const linkedIdentityGK = gestaltsUtils.keyFromIdentity( + claim.payload.data.provider, + claim.payload.data.identity, + ); + if (!this.visitedVertices.has(linkedIdentityGK)) { + await this.pushKeyToDiscoveryQueue(linkedIdentityGK); + } + } } - } - - // If the claim is to an identity - if (claim.payload.data.type === 'identity') { - // Attempt to get the identity info on the identity provider - const identityInfo = await this.getIdentityInfo( - claim.payload.data.provider, - claim.payload.data.identity, - claim.payload.data.identity, + } else if (vertexGId.type === 'identity') { + // If the next vertex is an identity, perform a social discovery + // Firstly get the identity info of this identity + const vertexIdentityInfo = await this.getIdentityInfo( + vertexGId.providerId, + vertexGId.identityId, ); - // If we can't get identity info, simply skip this claim - if (identityInfo == null) { + // If we don't have identity info, simply skip this vertex + if (vertexIdentityInfo == null) { continue; } - // Link the node to the found identity info - await this.gestaltGraph.linkNodeAndIdentity( - vertexNodeInfo, - identityInfo, - ); - - // Add this identity vertex to the queue if it hasn't already been visited - const linkedIdentityGK = gestaltsUtils.keyFromIdentity( - claim.payload.data.provider, - claim.payload.data.identity, - ); - if (!visitedVertices.has(linkedIdentityGK)) { - vertexQueue.push(linkedIdentityGK); + // Link the identity with each node from its claims on the provider + // Iterate over each of the claims + for (const id in vertexIdentityInfo.claims) { + const identityClaimId = id as IdentityClaimId; + const claim = vertexIdentityInfo.claims[identityClaimId]; + // Claims on an identity provider will always be node -> identity + // So just cast payload data as such + const data = claim.payload.data as ClaimLinkIdentity; + const linkedVertexNodeId = nodesUtils.decodeNodeId(data.node)!; + // Get the chain data of this claimed node (so that we can link in GG) + const linkedVertexChainData = + await this.nodeManager.requestChainData(linkedVertexNodeId); + // With this verified chain, we can link + const linkedVertexNodeInfo: NodeInfo = { + id: nodesUtils.encodeNodeId(linkedVertexNodeId), + chain: linkedVertexChainData, + }; + await this.gestaltGraph.linkNodeAndIdentity( + linkedVertexNodeInfo, + vertexIdentityInfo, + ); + // Add this vertex to the queue if it is not present + const linkedVertexGK = + gestaltsUtils.keyFromNode(linkedVertexNodeId); + if (!this.visitedVertices.has(linkedVertexGK)) { + await this.pushKeyToDiscoveryQueue(linkedVertexGK); + } } } + this.visitedVertices.add(vertex); + await this.removeKeyFromDiscoveryQueue(vertexId); + yield; } - // Add this node vertex to the visited - visitedVertices.add(gestaltsUtils.keyFromNode(nodeId)); - } else if (vertexGId.type === 'identity') { - // If the next vertex is an identity, perform a social discovery - // Firstly get the identity info of this identity - const vertexIdentityInfo = await this.getIdentityInfo( - vertexGId.providerId, - vertexGId.identityId, - vertexGId.identityId, - ); - // If we don't have identity info, simply skip this vertex - if (vertexIdentityInfo == null) { - continue; + } else { + if (!(this[status] === 'stopping')) { + this.queuePlugRelease = await this.queuePlug.acquire(); } + await this.queuePlug.waitForUnlock(); + } + if (this[status] === 'stopping') { + break; + } + } + } - // Link the identity with each node from its claims on the provider - // Iterate over each of the claims - for (const id in vertexIdentityInfo.claims) { - const identityClaimId = id as IdentityClaimId; - const claim = vertexIdentityInfo.claims[identityClaimId]; - // Claims on an identity provider will always be node -> identity - // So just cast payload data as such - const data = claim.payload.data as ClaimLinkIdentity; - const linkedVertexNodeId = nodesUtils.decodeNodeId(data.node)!; - // Get the chain data of this claimed node (so that we can link in GG) - const linkedVertexChainData = await this.nodeManager.requestChainData( - linkedVertexNodeId, - ); - // With this verified chain, we can link - const linkedVertexNodeInfo: NodeInfo = { - id: nodesUtils.encodeNodeId(linkedVertexNodeId), - chain: linkedVertexChainData, - }; - await this.gestaltGraph.linkNodeAndIdentity( - linkedVertexNodeInfo, - vertexIdentityInfo, - ); + /** + * Used for iterating over the discovery queue. This method should run + * continuously whenever the Discovery module is started and should be exited + * only during stopping. + */ + protected async runDiscoveryQueue() { + for await (const _ of this.discoveryQueue) { + // Empty + } + } - // Add this vertex to the queue if it hasn't already been visited - const linkedVertexGK = gestaltsUtils.keyFromNode(linkedVertexNodeId); - if (!visitedVertices.has(linkedVertexGK)) { - vertexQueue.push(linkedVertexGK); - } - } - // Add this identity vertex to the visited - visitedVertices.add( - gestaltsUtils.keyFromIdentity( - vertexGId.providerId, - vertexGId.identityId, - ), - ); + /** + * Simple check for whether the Discovery Queue is empty. Uses a + * transaction lock to ensure consistency. + */ + protected async queueIsEmpty(): Promise { + return await utils.withF([this.transaction], async () => { + let nextDiscoveryQueueId: DiscoveryQueueId | undefined; + const keyStream = this.discoveryQueueDb.createKeyStream({ + limit: 1, + }); + for await (const o of keyStream) { + nextDiscoveryQueueId = IdInternal.fromBuffer(o); + } + if (nextDiscoveryQueueId == null) { + return true; } - yield; + return false; + }); + } + + /** + * Push a Gestalt Key to the Discovery Queue. This process also unlocks + * the queue if it was previously locked (due to being empty). + */ + protected async pushKeyToDiscoveryQueue(gk: GestaltKey) { + await utils.withF([this.transaction], async () => { + const discoveryQueueId = this.discoveryQueueIdGenerator(); + await this.db.put( + this.discoveryQueueDbDomain, + idUtils.toBuffer(discoveryQueueId), + gk, + ); + }); + if (this.queuePlugRelease != null) { + this.queuePlugRelease(); + this.queuePlugRelease = undefined; } } + /** + * Remove a Gestalt Key from the Discovery Queue by its QueueId. This should + * only be done after a Key has been discovered in order to remove it from + * the beginning of the queue. + */ + protected async removeKeyFromDiscoveryQueue(keyId: DiscoveryQueueId) { + await utils.withF([this.transaction], async () => { + await this.db.del(this.discoveryQueueDbDomain, idUtils.toBuffer(keyId)); + }); + } + /** * Helper function to retrieve the IdentityInfo of an identity on a provider. * All claims in the returned IdentityInfo are verified by the node it claims @@ -267,15 +416,24 @@ class Discovery { protected async getIdentityInfo( providerId: ProviderId, identityId: IdentityId, - authIdentityId: IdentityId, ): Promise { const provider = this.identitiesManager.getProvider(providerId); // If we don't have this provider, no identity info to find if (provider == null) { return undefined; } + // Get our own auth identity id + const authIdentityIds = await provider.getAuthIdentityIds(); + // If we don't have one then we can't request data so just skip + if (authIdentityIds === [] || authIdentityIds[0] == null) { + return undefined; + } + const authIdentityId = authIdentityIds[0]; // Get the identity data - const identityData = await provider.getIdentityData(identityId, identityId); + const identityData = await provider.getIdentityData( + authIdentityId, + identityId, + ); // If we don't have identity data, no identity info to find if (identityData == null) { return undefined; diff --git a/src/discovery/errors.ts b/src/discovery/errors.ts index ee359227c1..cadf810485 100644 --- a/src/discovery/errors.ts +++ b/src/discovery/errors.ts @@ -2,6 +2,15 @@ import { ErrorPolykey } from '../errors'; class ErrorDiscovery extends ErrorPolykey {} +class ErrorDiscoveryRunning extends ErrorDiscovery {} + class ErrorDiscoveryDestroyed extends ErrorDiscovery {} -export { ErrorDiscovery, ErrorDiscoveryDestroyed }; +class ErrorDiscoveryNotRunning extends ErrorDiscovery {} + +export { + ErrorDiscovery, + ErrorDiscoveryDestroyed, + ErrorDiscoveryRunning, + ErrorDiscoveryNotRunning, +}; diff --git a/src/discovery/types.ts b/src/discovery/types.ts new file mode 100644 index 0000000000..9c32ed947a --- /dev/null +++ b/src/discovery/types.ts @@ -0,0 +1,11 @@ +import type { Opaque } from '../types'; +import type { Id } from '../GenericIdTypes'; + +/** + * Used to preserve order in the Discovery Queue. + */ +type DiscoveryQueueId = Opaque<'DiscoveryQueueId', Id>; + +type DiscoveryQueueIdGenerator = () => DiscoveryQueueId; + +export type { DiscoveryQueueId, DiscoveryQueueIdGenerator }; diff --git a/src/discovery/utils.ts b/src/discovery/utils.ts new file mode 100644 index 0000000000..b8a9f98089 --- /dev/null +++ b/src/discovery/utils.ts @@ -0,0 +1,19 @@ +import type { DiscoveryQueueId, DiscoveryQueueIdGenerator } from './types'; +import { IdSortable } from '@matrixai/id'; +import { makeId } from '../GenericIdTypes'; + +function makeDiscoveryQueueId(arg: any) { + return makeId(arg); +} + +function createDiscoveryQueueIdGenerator( + lastId?: DiscoveryQueueId, +): DiscoveryQueueIdGenerator { + const idSortableGenerator = new IdSortable({ + lastId, + }); + return (): DiscoveryQueueId => + makeDiscoveryQueueId(idSortableGenerator.get()); +} + +export { makeDiscoveryQueueId, createDiscoveryQueueIdGenerator }; diff --git a/src/gestalts/GestaltGraph.ts b/src/gestalts/GestaltGraph.ts index e9ff881bed..266d411161 100644 --- a/src/gestalts/GestaltGraph.ts +++ b/src/gestalts/GestaltGraph.ts @@ -1,4 +1,3 @@ -import type { Buffer } from 'buffer'; import type { Gestalt, GestaltAction, @@ -13,7 +12,6 @@ import type { IdentityId, IdentityInfo, ProviderId } from '../identities/types'; import type { Permission } from '../acl/types'; import type { DB, DBLevel, DBOp } from '@matrixai/db'; import type { ACL } from '../acl'; - import { Mutex } from 'async-mutex'; import Logger from '@matrixai/logger'; import { @@ -22,9 +20,9 @@ import { } from '@matrixai/async-init/dist/CreateDestroyStartStop'; import * as gestaltsUtils from './utils'; import * as gestaltsErrors from './errors'; -import { utils as aclUtils } from '../acl'; +import * as aclUtils from '../acl/utils'; import * as utils from '../utils'; -import { utils as nodesUtils } from '../nodes'; +import * as nodesUtils from '../nodes/utils'; interface GestaltGraph extends CreateDestroyStartStop {} @CreateDestroyStartStop( @@ -156,12 +154,13 @@ class GestaltGraph { } const gestalts: Array = []; let gestalt: Gestalt; - for (const [gK, gKs] of unvisited) { + for (const gKSet of unvisited) { gestalt = { matrix: {}, nodes: {}, identities: {}, }; + const gK = gKSet[0]; const queue = [gK]; while (true) { const vertex = queue.shift(); @@ -170,7 +169,11 @@ class GestaltGraph { break; } const gId = gestaltsUtils.ungestaltKey(vertex); - const vertexKeys = gKs; + const vertexKeys = unvisited.get(vertex); + if (vertexKeys == null) { + // This should not happen + break; + } gestalt.matrix[vertex] = vertexKeys; if (gId.type === 'node') { const nodeInfo = await this.db.get( diff --git a/src/identities/providers/github/GitHubProvider.ts b/src/identities/providers/github/GitHubProvider.ts index 39f199f32c..21bdfa2ecf 100644 --- a/src/identities/providers/github/GitHubProvider.ts +++ b/src/identities/providers/github/GitHubProvider.ts @@ -7,14 +7,13 @@ import type { } from '../../types'; import type { Claim } from '../../../claims/types'; import type { IdentityClaim, IdentityClaimId } from '../../../identities/types'; - import { fetch, Request, Headers } from 'cross-fetch'; -import { Searcher } from 'fast-fuzzy'; import cheerio from 'cheerio'; import Logger from '@matrixai/logger'; import { sleep } from '../../../utils'; import Provider from '../../Provider'; import * as identitiesErrors from '../../errors'; +import * as identitiesUtils from '../../utils'; class GitHubProvider extends Provider { public readonly id = 'github.com' as ProviderId; @@ -304,7 +303,10 @@ class GitHubProvider extends Provider { authIdentityId, item.login, ); - if (identityData && this.matchIdentityData(identityData, searchTerms)) { + if ( + identityData && + identitiesUtils.matchIdentityData(identityData, searchTerms) + ) { yield identityData; } } @@ -347,7 +349,10 @@ class GitHubProvider extends Provider { authIdentityId, item.login, ); - if (identityData && this.matchIdentityData(identityData, searchTerms)) { + if ( + identityData && + identitiesUtils.matchIdentityData(identityData, searchTerms) + ) { yield identityData; } } @@ -533,36 +538,6 @@ class GitHubProvider extends Provider { }) as Request; } - protected matchIdentityData( - identityData: IdentityData, - searchTerms: Array, - ): boolean { - if (searchTerms.length < 1) { - return true; - } - const searcher = new Searcher([identityData], { - keySelector: (obj) => [ - obj.identityId, - obj.name || '', - obj.email || '', - obj.url || '', - ], - threshold: 0.8, - }); - let matched = false; - for (const searchTerm of searchTerms) { - if (searcher.search(searchTerm).length > 0) { - matched = true; - break; - } - } - if (matched) { - return true; - } else { - return false; - } - } - protected extractClaimIds(html: string): Array { const claimIds: Array = []; const $ = cheerio.load(html); diff --git a/src/identities/utils.ts b/src/identities/utils.ts index 9bb59c4412..dfdc119065 100644 --- a/src/identities/utils.ts +++ b/src/identities/utils.ts @@ -1,6 +1,8 @@ +import type { IdentityData } from './types'; import os from 'os'; import process from 'process'; import spawn from 'cross-spawn'; +import { Searcher } from 'fast-fuzzy'; function browser(url: string): void { let platform = process.platform; @@ -47,4 +49,37 @@ function browser(url: string): void { browserProcess.unref(); } -export { browser }; +/** + * Check whether a given identity matches at least one search term from a list. + */ +function matchIdentityData( + identityData: IdentityData, + searchTerms: Array, +): boolean { + if (searchTerms.length < 1) { + return true; + } + const searcher = new Searcher([identityData], { + keySelector: (obj) => [ + obj.identityId, + obj.name || '', + obj.email || '', + obj.url || '', + ], + threshold: 0.8, + }); + let matched = false; + for (const searchTerm of searchTerms) { + if (searcher.search(searchTerm).length > 0) { + matched = true; + break; + } + } + if (matched) { + return true; + } else { + return false; + } +} + +export { browser, matchIdentityData }; diff --git a/src/proto/js/polykey/v1/client_service_grpc_pb.d.ts b/src/proto/js/polykey/v1/client_service_grpc_pb.d.ts index dc52efc66c..c24147d643 100644 --- a/src/proto/js/polykey/v1/client_service_grpc_pb.d.ts +++ b/src/proto/js/polykey/v1/client_service_grpc_pb.d.ts @@ -64,11 +64,13 @@ interface IClientServiceService extends grpc.ServiceDefinition; responseDeserialize: grpc.deserialize; } -interface IClientServiceService_IIdentitiesInfoGet extends grpc.MethodDefinition { +interface IClientServiceService_IIdentitiesInfoGet extends grpc.MethodDefinition { path: "/polykey.v1.ClientService/IdentitiesInfoGet"; requestStream: false; - responseStream: false; - requestSerialize: grpc.serialize; - requestDeserialize: grpc.deserialize; - responseSerialize: grpc.serialize; - responseDeserialize: grpc.deserialize; + responseStream: true; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; } -interface IClientServiceService_IIdentitiesInfoGetConnected extends grpc.MethodDefinition { - path: "/polykey.v1.ClientService/IdentitiesInfoGetConnected"; +interface IClientServiceService_IIdentitiesInfoConnectedGet extends grpc.MethodDefinition { + path: "/polykey.v1.ClientService/IdentitiesInfoConnectedGet"; requestStream: false; responseStream: true; requestSerialize: grpc.serialize; @@ -532,6 +534,24 @@ interface IClientServiceService_IGestaltsGestaltGetByIdentity extends grpc.Metho responseSerialize: grpc.serialize; responseDeserialize: grpc.deserialize; } +interface IClientServiceService_IGestaltsGestaltTrustByNode extends grpc.MethodDefinition { + path: "/polykey.v1.ClientService/GestaltsGestaltTrustByNode"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} +interface IClientServiceService_IGestaltsGestaltTrustByIdentity extends grpc.MethodDefinition { + path: "/polykey.v1.ClientService/GestaltsGestaltTrustByIdentity"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} interface IClientServiceService_IGestaltsDiscoveryByNode extends grpc.MethodDefinition { path: "/polykey.v1.ClientService/GestaltsDiscoveryByNode"; requestStream: false; @@ -679,12 +699,14 @@ export interface IClientServiceServer extends grpc.UntypedServiceImplementation identitiesTokenGet: grpc.handleUnaryCall; identitiesTokenDelete: grpc.handleUnaryCall; identitiesProvidersList: grpc.handleUnaryCall; - identitiesInfoGet: grpc.handleUnaryCall; - identitiesInfoGetConnected: grpc.handleServerStreamingCall; + identitiesInfoGet: grpc.handleServerStreamingCall; + identitiesInfoConnectedGet: grpc.handleServerStreamingCall; identitiesClaim: grpc.handleUnaryCall; gestaltsGestaltList: grpc.handleServerStreamingCall; gestaltsGestaltGetByNode: grpc.handleUnaryCall; gestaltsGestaltGetByIdentity: grpc.handleUnaryCall; + gestaltsGestaltTrustByNode: grpc.handleUnaryCall; + gestaltsGestaltTrustByIdentity: grpc.handleUnaryCall; gestaltsDiscoveryByNode: grpc.handleUnaryCall; gestaltsDiscoveryByIdentity: grpc.handleUnaryCall; gestaltsActionsGetByNode: grpc.handleUnaryCall; @@ -824,11 +846,10 @@ export interface IClientServiceClient { identitiesProvidersList(request: polykey_v1_utils_utils_pb.EmptyMessage, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Provider) => void): grpc.ClientUnaryCall; identitiesProvidersList(request: polykey_v1_utils_utils_pb.EmptyMessage, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Provider) => void): grpc.ClientUnaryCall; identitiesProvidersList(request: polykey_v1_utils_utils_pb.EmptyMessage, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Provider) => void): grpc.ClientUnaryCall; - identitiesInfoGet(request: polykey_v1_identities_identities_pb.Provider, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Provider) => void): grpc.ClientUnaryCall; - identitiesInfoGet(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Provider) => void): grpc.ClientUnaryCall; - identitiesInfoGet(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Provider) => void): grpc.ClientUnaryCall; - identitiesInfoGetConnected(request: polykey_v1_identities_identities_pb.ProviderSearch, options?: Partial): grpc.ClientReadableStream; - identitiesInfoGetConnected(request: polykey_v1_identities_identities_pb.ProviderSearch, metadata?: grpc.Metadata, options?: Partial): grpc.ClientReadableStream; + identitiesInfoGet(request: polykey_v1_identities_identities_pb.ProviderSearch, options?: Partial): grpc.ClientReadableStream; + identitiesInfoGet(request: polykey_v1_identities_identities_pb.ProviderSearch, metadata?: grpc.Metadata, options?: Partial): grpc.ClientReadableStream; + identitiesInfoConnectedGet(request: polykey_v1_identities_identities_pb.ProviderSearch, options?: Partial): grpc.ClientReadableStream; + identitiesInfoConnectedGet(request: polykey_v1_identities_identities_pb.ProviderSearch, metadata?: grpc.Metadata, options?: Partial): grpc.ClientReadableStream; identitiesClaim(request: polykey_v1_identities_identities_pb.Provider, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Claim) => void): grpc.ClientUnaryCall; identitiesClaim(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Claim) => void): grpc.ClientUnaryCall; identitiesClaim(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Claim) => void): grpc.ClientUnaryCall; @@ -840,6 +861,12 @@ export interface IClientServiceClient { gestaltsGestaltGetByIdentity(request: polykey_v1_identities_identities_pb.Provider, callback: (error: grpc.ServiceError | null, response: polykey_v1_gestalts_gestalts_pb.Graph) => void): grpc.ClientUnaryCall; gestaltsGestaltGetByIdentity(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: polykey_v1_gestalts_gestalts_pb.Graph) => void): grpc.ClientUnaryCall; gestaltsGestaltGetByIdentity(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: polykey_v1_gestalts_gestalts_pb.Graph) => void): grpc.ClientUnaryCall; + gestaltsGestaltTrustByNode(request: polykey_v1_nodes_nodes_pb.Node, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; + gestaltsGestaltTrustByNode(request: polykey_v1_nodes_nodes_pb.Node, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; + gestaltsGestaltTrustByNode(request: polykey_v1_nodes_nodes_pb.Node, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; + gestaltsGestaltTrustByIdentity(request: polykey_v1_identities_identities_pb.Provider, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; + gestaltsGestaltTrustByIdentity(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; + gestaltsGestaltTrustByIdentity(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; gestaltsDiscoveryByNode(request: polykey_v1_nodes_nodes_pb.Node, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; gestaltsDiscoveryByNode(request: polykey_v1_nodes_nodes_pb.Node, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; gestaltsDiscoveryByNode(request: polykey_v1_nodes_nodes_pb.Node, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; @@ -1002,11 +1029,10 @@ export class ClientServiceClient extends grpc.Client implements IClientServiceCl public identitiesProvidersList(request: polykey_v1_utils_utils_pb.EmptyMessage, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Provider) => void): grpc.ClientUnaryCall; public identitiesProvidersList(request: polykey_v1_utils_utils_pb.EmptyMessage, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Provider) => void): grpc.ClientUnaryCall; public identitiesProvidersList(request: polykey_v1_utils_utils_pb.EmptyMessage, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Provider) => void): grpc.ClientUnaryCall; - public identitiesInfoGet(request: polykey_v1_identities_identities_pb.Provider, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Provider) => void): grpc.ClientUnaryCall; - public identitiesInfoGet(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Provider) => void): grpc.ClientUnaryCall; - public identitiesInfoGet(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Provider) => void): grpc.ClientUnaryCall; - public identitiesInfoGetConnected(request: polykey_v1_identities_identities_pb.ProviderSearch, options?: Partial): grpc.ClientReadableStream; - public identitiesInfoGetConnected(request: polykey_v1_identities_identities_pb.ProviderSearch, metadata?: grpc.Metadata, options?: Partial): grpc.ClientReadableStream; + public identitiesInfoGet(request: polykey_v1_identities_identities_pb.ProviderSearch, options?: Partial): grpc.ClientReadableStream; + public identitiesInfoGet(request: polykey_v1_identities_identities_pb.ProviderSearch, metadata?: grpc.Metadata, options?: Partial): grpc.ClientReadableStream; + public identitiesInfoConnectedGet(request: polykey_v1_identities_identities_pb.ProviderSearch, options?: Partial): grpc.ClientReadableStream; + public identitiesInfoConnectedGet(request: polykey_v1_identities_identities_pb.ProviderSearch, metadata?: grpc.Metadata, options?: Partial): grpc.ClientReadableStream; public identitiesClaim(request: polykey_v1_identities_identities_pb.Provider, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Claim) => void): grpc.ClientUnaryCall; public identitiesClaim(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Claim) => void): grpc.ClientUnaryCall; public identitiesClaim(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: polykey_v1_identities_identities_pb.Claim) => void): grpc.ClientUnaryCall; @@ -1018,6 +1044,12 @@ export class ClientServiceClient extends grpc.Client implements IClientServiceCl public gestaltsGestaltGetByIdentity(request: polykey_v1_identities_identities_pb.Provider, callback: (error: grpc.ServiceError | null, response: polykey_v1_gestalts_gestalts_pb.Graph) => void): grpc.ClientUnaryCall; public gestaltsGestaltGetByIdentity(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: polykey_v1_gestalts_gestalts_pb.Graph) => void): grpc.ClientUnaryCall; public gestaltsGestaltGetByIdentity(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: polykey_v1_gestalts_gestalts_pb.Graph) => void): grpc.ClientUnaryCall; + public gestaltsGestaltTrustByNode(request: polykey_v1_nodes_nodes_pb.Node, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; + public gestaltsGestaltTrustByNode(request: polykey_v1_nodes_nodes_pb.Node, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; + public gestaltsGestaltTrustByNode(request: polykey_v1_nodes_nodes_pb.Node, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; + public gestaltsGestaltTrustByIdentity(request: polykey_v1_identities_identities_pb.Provider, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; + public gestaltsGestaltTrustByIdentity(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; + public gestaltsGestaltTrustByIdentity(request: polykey_v1_identities_identities_pb.Provider, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; public gestaltsDiscoveryByNode(request: polykey_v1_nodes_nodes_pb.Node, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; public gestaltsDiscoveryByNode(request: polykey_v1_nodes_nodes_pb.Node, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; public gestaltsDiscoveryByNode(request: polykey_v1_nodes_nodes_pb.Node, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: polykey_v1_utils_utils_pb.EmptyMessage) => void): grpc.ClientUnaryCall; diff --git a/src/proto/js/polykey/v1/client_service_grpc_pb.js b/src/proto/js/polykey/v1/client_service_grpc_pb.js index 1db781fc86..fe2b2eae81 100644 --- a/src/proto/js/polykey/v1/client_service_grpc_pb.js +++ b/src/proto/js/polykey/v1/client_service_grpc_pb.js @@ -981,16 +981,16 @@ identitiesAuthenticate: { identitiesInfoGet: { path: '/polykey.v1.ClientService/IdentitiesInfoGet', requestStream: false, - responseStream: false, - requestType: polykey_v1_identities_identities_pb.Provider, - responseType: polykey_v1_identities_identities_pb.Provider, - requestSerialize: serialize_polykey_v1_identities_Provider, - requestDeserialize: deserialize_polykey_v1_identities_Provider, - responseSerialize: serialize_polykey_v1_identities_Provider, - responseDeserialize: deserialize_polykey_v1_identities_Provider, + responseStream: true, + requestType: polykey_v1_identities_identities_pb.ProviderSearch, + responseType: polykey_v1_identities_identities_pb.Info, + requestSerialize: serialize_polykey_v1_identities_ProviderSearch, + requestDeserialize: deserialize_polykey_v1_identities_ProviderSearch, + responseSerialize: serialize_polykey_v1_identities_Info, + responseDeserialize: deserialize_polykey_v1_identities_Info, }, - identitiesInfoGetConnected: { - path: '/polykey.v1.ClientService/IdentitiesInfoGetConnected', + identitiesInfoConnectedGet: { + path: '/polykey.v1.ClientService/IdentitiesInfoConnectedGet', requestStream: false, responseStream: true, requestType: polykey_v1_identities_identities_pb.ProviderSearch, @@ -1045,6 +1045,28 @@ gestaltsGestaltList: { responseSerialize: serialize_polykey_v1_gestalts_Graph, responseDeserialize: deserialize_polykey_v1_gestalts_Graph, }, + gestaltsGestaltTrustByNode: { + path: '/polykey.v1.ClientService/GestaltsGestaltTrustByNode', + requestStream: false, + responseStream: false, + requestType: polykey_v1_nodes_nodes_pb.Node, + responseType: polykey_v1_utils_utils_pb.EmptyMessage, + requestSerialize: serialize_polykey_v1_nodes_Node, + requestDeserialize: deserialize_polykey_v1_nodes_Node, + responseSerialize: serialize_polykey_v1_utils_EmptyMessage, + responseDeserialize: deserialize_polykey_v1_utils_EmptyMessage, + }, + gestaltsGestaltTrustByIdentity: { + path: '/polykey.v1.ClientService/GestaltsGestaltTrustByIdentity', + requestStream: false, + responseStream: false, + requestType: polykey_v1_identities_identities_pb.Provider, + responseType: polykey_v1_utils_utils_pb.EmptyMessage, + requestSerialize: serialize_polykey_v1_identities_Provider, + requestDeserialize: deserialize_polykey_v1_identities_Provider, + responseSerialize: serialize_polykey_v1_utils_EmptyMessage, + responseDeserialize: deserialize_polykey_v1_utils_EmptyMessage, + }, gestaltsDiscoveryByNode: { path: '/polykey.v1.ClientService/GestaltsDiscoveryByNode', requestStream: false, diff --git a/src/proto/js/polykey/v1/identities/identities_pb.d.ts b/src/proto/js/polykey/v1/identities/identities_pb.d.ts index 449a7e310a..14220875fa 100644 --- a/src/proto/js/polykey/v1/identities/identities_pb.d.ts +++ b/src/proto/js/polykey/v1/identities/identities_pb.d.ts @@ -182,15 +182,20 @@ export namespace AuthenticationResponse { } export class ProviderSearch extends jspb.Message { - - hasProvider(): boolean; - clearProvider(): void; - getProvider(): Provider | undefined; - setProvider(value?: Provider): ProviderSearch; + getIdentityId(): string; + setIdentityId(value: string): ProviderSearch; + getDisconnected(): boolean; + setDisconnected(value: boolean): ProviderSearch; + getLimit(): string; + setLimit(value: string): ProviderSearch; clearSearchTermList(): void; getSearchTermList(): Array; setSearchTermList(value: Array): ProviderSearch; addSearchTerm(value: string, index?: number): string; + clearProviderIdList(): void; + getProviderIdList(): Array; + setProviderIdList(value: Array): ProviderSearch; + addProviderId(value: string, index?: number): string; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): ProviderSearch.AsObject; @@ -204,8 +209,11 @@ export class ProviderSearch extends jspb.Message { export namespace ProviderSearch { export type AsObject = { - provider?: Provider.AsObject, + identityId: string, + disconnected: boolean, + limit: string, searchTermList: Array, + providerIdList: Array, } } diff --git a/src/proto/js/polykey/v1/identities/identities_pb.js b/src/proto/js/polykey/v1/identities/identities_pb.js index 64290d1a4c..6b22d58786 100644 --- a/src/proto/js/polykey/v1/identities/identities_pb.js +++ b/src/proto/js/polykey/v1/identities/identities_pb.js @@ -1371,7 +1371,7 @@ proto.polykey.v1.identities.AuthenticationResponse.prototype.setIdentityId = fun * @private {!Array} * @const */ -proto.polykey.v1.identities.ProviderSearch.repeatedFields_ = [2]; +proto.polykey.v1.identities.ProviderSearch.repeatedFields_ = [4,5]; @@ -1404,8 +1404,11 @@ proto.polykey.v1.identities.ProviderSearch.prototype.toObject = function(opt_inc */ proto.polykey.v1.identities.ProviderSearch.toObject = function(includeInstance, msg) { var f, obj = { - provider: (f = msg.getProvider()) && proto.polykey.v1.identities.Provider.toObject(includeInstance, f), - searchTermList: (f = jspb.Message.getRepeatedField(msg, 2)) == null ? undefined : f + identityId: jspb.Message.getFieldWithDefault(msg, 1, ""), + disconnected: jspb.Message.getBooleanFieldWithDefault(msg, 2, false), + limit: jspb.Message.getFieldWithDefault(msg, 3, ""), + searchTermList: (f = jspb.Message.getRepeatedField(msg, 4)) == null ? undefined : f, + providerIdList: (f = jspb.Message.getRepeatedField(msg, 5)) == null ? undefined : f }; if (includeInstance) { @@ -1443,14 +1446,25 @@ proto.polykey.v1.identities.ProviderSearch.deserializeBinaryFromReader = functio var field = reader.getFieldNumber(); switch (field) { case 1: - var value = new proto.polykey.v1.identities.Provider; - reader.readMessage(value,proto.polykey.v1.identities.Provider.deserializeBinaryFromReader); - msg.setProvider(value); + var value = /** @type {string} */ (reader.readString()); + msg.setIdentityId(value); break; case 2: + var value = /** @type {boolean} */ (reader.readBool()); + msg.setDisconnected(value); + break; + case 3: + var value = /** @type {string} */ (reader.readString()); + msg.setLimit(value); + break; + case 4: var value = /** @type {string} */ (reader.readString()); msg.addSearchTerm(value); break; + case 5: + var value = /** @type {string} */ (reader.readString()); + msg.addProviderId(value); + break; default: reader.skipField(); break; @@ -1480,18 +1494,38 @@ proto.polykey.v1.identities.ProviderSearch.prototype.serializeBinary = function( */ proto.polykey.v1.identities.ProviderSearch.serializeBinaryToWriter = function(message, writer) { var f = undefined; - f = message.getProvider(); - if (f != null) { - writer.writeMessage( + f = message.getIdentityId(); + if (f.length > 0) { + writer.writeString( 1, - f, - proto.polykey.v1.identities.Provider.serializeBinaryToWriter + f + ); + } + f = message.getDisconnected(); + if (f) { + writer.writeBool( + 2, + f + ); + } + f = message.getLimit(); + if (f.length > 0) { + writer.writeString( + 3, + f ); } f = message.getSearchTermList(); if (f.length > 0) { writer.writeRepeatedString( - 2, + 4, + f + ); + } + f = message.getProviderIdList(); + if (f.length > 0) { + writer.writeRepeatedString( + 5, f ); } @@ -1499,48 +1533,65 @@ proto.polykey.v1.identities.ProviderSearch.serializeBinaryToWriter = function(me /** - * optional Provider provider = 1; - * @return {?proto.polykey.v1.identities.Provider} + * optional string identity_id = 1; + * @return {string} */ -proto.polykey.v1.identities.ProviderSearch.prototype.getProvider = function() { - return /** @type{?proto.polykey.v1.identities.Provider} */ ( - jspb.Message.getWrapperField(this, proto.polykey.v1.identities.Provider, 1)); +proto.polykey.v1.identities.ProviderSearch.prototype.getIdentityId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); }; /** - * @param {?proto.polykey.v1.identities.Provider|undefined} value + * @param {string} value * @return {!proto.polykey.v1.identities.ProviderSearch} returns this -*/ -proto.polykey.v1.identities.ProviderSearch.prototype.setProvider = function(value) { - return jspb.Message.setWrapperField(this, 1, value); + */ +proto.polykey.v1.identities.ProviderSearch.prototype.setIdentityId = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); }; /** - * Clears the message field making it undefined. + * optional bool disconnected = 2; + * @return {boolean} + */ +proto.polykey.v1.identities.ProviderSearch.prototype.getDisconnected = function() { + return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 2, false)); +}; + + +/** + * @param {boolean} value * @return {!proto.polykey.v1.identities.ProviderSearch} returns this */ -proto.polykey.v1.identities.ProviderSearch.prototype.clearProvider = function() { - return this.setProvider(undefined); +proto.polykey.v1.identities.ProviderSearch.prototype.setDisconnected = function(value) { + return jspb.Message.setProto3BooleanField(this, 2, value); }; /** - * Returns whether this field is set. - * @return {boolean} + * optional string limit = 3; + * @return {string} */ -proto.polykey.v1.identities.ProviderSearch.prototype.hasProvider = function() { - return jspb.Message.getField(this, 1) != null; +proto.polykey.v1.identities.ProviderSearch.prototype.getLimit = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); }; /** - * repeated string search_term = 2; + * @param {string} value + * @return {!proto.polykey.v1.identities.ProviderSearch} returns this + */ +proto.polykey.v1.identities.ProviderSearch.prototype.setLimit = function(value) { + return jspb.Message.setProto3StringField(this, 3, value); +}; + + +/** + * repeated string search_term = 4; * @return {!Array} */ proto.polykey.v1.identities.ProviderSearch.prototype.getSearchTermList = function() { - return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 2)); + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 4)); }; @@ -1549,7 +1600,7 @@ proto.polykey.v1.identities.ProviderSearch.prototype.getSearchTermList = functio * @return {!proto.polykey.v1.identities.ProviderSearch} returns this */ proto.polykey.v1.identities.ProviderSearch.prototype.setSearchTermList = function(value) { - return jspb.Message.setField(this, 2, value || []); + return jspb.Message.setField(this, 4, value || []); }; @@ -1559,7 +1610,7 @@ proto.polykey.v1.identities.ProviderSearch.prototype.setSearchTermList = functio * @return {!proto.polykey.v1.identities.ProviderSearch} returns this */ proto.polykey.v1.identities.ProviderSearch.prototype.addSearchTerm = function(value, opt_index) { - return jspb.Message.addToRepeatedField(this, 2, value, opt_index); + return jspb.Message.addToRepeatedField(this, 4, value, opt_index); }; @@ -1572,6 +1623,43 @@ proto.polykey.v1.identities.ProviderSearch.prototype.clearSearchTermList = funct }; +/** + * repeated string provider_id = 5; + * @return {!Array} + */ +proto.polykey.v1.identities.ProviderSearch.prototype.getProviderIdList = function() { + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 5)); +}; + + +/** + * @param {!Array} value + * @return {!proto.polykey.v1.identities.ProviderSearch} returns this + */ +proto.polykey.v1.identities.ProviderSearch.prototype.setProviderIdList = function(value) { + return jspb.Message.setField(this, 5, value || []); +}; + + +/** + * @param {string} value + * @param {number=} opt_index + * @return {!proto.polykey.v1.identities.ProviderSearch} returns this + */ +proto.polykey.v1.identities.ProviderSearch.prototype.addProviderId = function(value, opt_index) { + return jspb.Message.addToRepeatedField(this, 5, value, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.polykey.v1.identities.ProviderSearch} returns this + */ +proto.polykey.v1.identities.ProviderSearch.prototype.clearProviderIdList = function() { + return this.setProviderIdList([]); +}; + + diff --git a/src/proto/schemas/polykey/v1/client_service.proto b/src/proto/schemas/polykey/v1/client_service.proto index f2dca62c18..f545b376f6 100644 --- a/src/proto/schemas/polykey/v1/client_service.proto +++ b/src/proto/schemas/polykey/v1/client_service.proto @@ -68,14 +68,16 @@ service ClientService { rpc IdentitiesTokenGet(polykey.v1.identities.Provider) returns (polykey.v1.identities.Token); rpc IdentitiesTokenDelete(polykey.v1.identities.Provider) returns (polykey.v1.utils.EmptyMessage); rpc IdentitiesProvidersList(polykey.v1.utils.EmptyMessage) returns (polykey.v1.identities.Provider); - rpc IdentitiesInfoGet(polykey.v1.identities.Provider) returns (polykey.v1.identities.Provider); - rpc IdentitiesInfoGetConnected(polykey.v1.identities.ProviderSearch) returns (stream polykey.v1.identities.Info); + rpc IdentitiesInfoGet(polykey.v1.identities.ProviderSearch) returns (stream polykey.v1.identities.Info); + rpc IdentitiesInfoConnectedGet(polykey.v1.identities.ProviderSearch) returns (stream polykey.v1.identities.Info); rpc IdentitiesClaim(polykey.v1.identities.Provider) returns (polykey.v1.identities.Claim); // Gestalts rpc GestaltsGestaltList(polykey.v1.utils.EmptyMessage) returns (stream polykey.v1.gestalts.Gestalt); rpc GestaltsGestaltGetByNode(polykey.v1.nodes.Node) returns (polykey.v1.gestalts.Graph); rpc GestaltsGestaltGetByIdentity(polykey.v1.identities.Provider) returns (polykey.v1.gestalts.Graph); + rpc GestaltsGestaltTrustByNode(polykey.v1.nodes.Node) returns (polykey.v1.utils.EmptyMessage); + rpc GestaltsGestaltTrustByIdentity(polykey.v1.identities.Provider) returns (polykey.v1.utils.EmptyMessage); rpc GestaltsDiscoveryByNode(polykey.v1.nodes.Node) returns (polykey.v1.utils.EmptyMessage); rpc GestaltsDiscoveryByIdentity(polykey.v1.identities.Provider) returns (polykey.v1.utils.EmptyMessage); rpc GestaltsActionsGetByNode(polykey.v1.nodes.Node) returns (polykey.v1.permissions.Actions); diff --git a/src/proto/schemas/polykey/v1/identities/identities.proto b/src/proto/schemas/polykey/v1/identities/identities.proto index 0df20034f8..f0a305b9eb 100644 --- a/src/proto/schemas/polykey/v1/identities/identities.proto +++ b/src/proto/schemas/polykey/v1/identities/identities.proto @@ -38,8 +38,11 @@ message AuthenticationResponse { } message ProviderSearch { - Provider provider = 1; - repeated string search_term = 2; + string identity_id = 1; + bool disconnected = 2; + string limit = 3; + repeated string search_term = 4; + repeated string provider_id = 5; } message Info { diff --git a/tests/bin/identities/identities.test.ts b/tests/bin/identities/identities.test.ts index ab9ab0ffa5..d2599d4d10 100644 --- a/tests/bin/identities/identities.test.ts +++ b/tests/bin/identities/identities.test.ts @@ -1,14 +1,16 @@ import type { IdentityId, IdentityInfo, ProviderId } from '@/identities/types'; import type { NodeIdEncoded, NodeInfo } from '@/nodes/types'; -import type { ClaimLinkIdentity, ClaimLinkNode } from '@/claims/types'; +import type { ClaimLinkIdentity } from '@/claims/types'; +import type { Gestalt } from '@/gestalts/types'; import os from 'os'; import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { PolykeyAgent } from '@'; +import { poll } from '@/utils'; import * as claimsUtils from '@/claims/utils'; import * as identitiesUtils from '@/identities/utils'; -import { utils as nodesUtils } from '@/nodes'; +import * as nodesUtils from '@/nodes/utils'; import * as testBinUtils from '../utils'; import * as testNodesUtils from '../../nodes/utils'; import TestProvider from '../../identities/TestProvider'; @@ -403,17 +405,8 @@ describe('CLI Identities', () => { expect(actionKeys).toContain('notify'); }); test('Should fail on invalid inputs.', async () => { - let result; - // Invalid node. - result = await testBinUtils.pkStdio( - genCommands(['trust', invaldNode.id]), - {}, - dataDir, - ); - expect(result.exitCode === 0).toBeFalsy(); // Fails.. - - // invalid identity - result = await testBinUtils.pkStdio( + // Invalid identity + const result = await testBinUtils.pkStdio( genCommands([ 'trust', identityString( @@ -477,7 +470,7 @@ describe('CLI Identities', () => { let result; // Invalid node. result = await testBinUtils.pkStdio( - genCommands(['trust', invaldNode.id]), + genCommands(['untrust', invaldNode.id]), {}, dataDir, ); @@ -623,9 +616,19 @@ describe('CLI Identities', () => { }); describe('commandSearchIdentities', () => { test('Should find a connected identity.', async () => { - // Create an identity + const provider = new TestProvider('provider' as ProviderId); + const identity = { + providerId: provider.id, + identityId: 'connected_user' as IdentityId, + name: 'User', + email: 'user@test.com', + url: 'test.com/user', + }; + provider.users['connected_user'] = identity; + polykeyAgent.identitiesManager.registerProvider(provider); + // Need an authenticated identity await polykeyAgent.identitiesManager.putToken( - testToken.providerId, + provider.id, testToken.identityId, testToken.tokenData, ); @@ -634,21 +637,28 @@ describe('CLI Identities', () => { 'search', '-np', nodePath, - testToken.providerId, + '--provider-id', + 'provider', + '--format', + 'json', ]; const result = await testBinUtils.pkStdio(commands, {}, dataDir); expect(result.exitCode).toBe(0); // Succeeds. - expect(result.stdout).toContain(testToken.providerId); - expect(result.stdout).toContain(testToken.identityId); + expect(JSON.parse(result.stdout)).toEqual(identity); + await polykeyAgent.identitiesManager.delToken( + provider.id, + testToken.identityId, + ); + polykeyAgent.identitiesManager.unregisterProvider(provider.id); }); }); describe('commandDiscoverGestalts', () => { let rootDataDir; // Test variables + const testProvider = new TestProvider('discovery-provider' as ProviderId); + const identityId = 'connected-identity' as IdentityId; let nodeB: PolykeyAgent; let nodeC: PolykeyAgent; - // Let testProvider: TestProvider; - let identityId: IdentityId; beforeAll(async () => { rootDataDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'polykey-test-'), @@ -677,26 +687,17 @@ describe('CLI Identities', () => { // Adding connection details. await testNodesUtils.nodesConnect(polykeyAgent, nodeB); await testNodesUtils.nodesConnect(nodeB, nodeC); + await testNodesUtils.nodesConnect(polykeyAgent, nodeC); // Adding sigchain details. - const claimBtoC: ClaimLinkNode = { - type: 'node', - node1: nodesUtils.encodeNodeId(nodeB.nodeManager.getNodeId()), - node2: nodesUtils.encodeNodeId(nodeC.nodeManager.getNodeId()), - }; - const claimCtoB: ClaimLinkNode = { - type: 'node', - node1: nodesUtils.encodeNodeId(nodeC.nodeManager.getNodeId()), - node2: nodesUtils.encodeNodeId(nodeB.nodeManager.getNodeId()), - }; - await nodeB.sigchain.addClaim(claimBtoC); - await nodeB.sigchain.addClaim(claimCtoB); - await nodeC.sigchain.addClaim(claimCtoB); - await nodeC.sigchain.addClaim(claimBtoC); + await nodeB.nodeManager.claimNode(nodeC.nodeManager.getNodeId()); // Setting up identtiy. - const gen = testProvider.authenticate(); - await gen.next(); - identityId = (await gen.next()).value as IdentityId; + testProvider.users[identityId] = {}; + polykeyAgent.identitiesManager.registerProvider(testProvider); + nodeB.identitiesManager.registerProvider(testProvider); + await nodeB.identitiesManager.putToken(testProvider.id, identityId, { + accessToken: 'def456', + }); const claimIdentToB: ClaimLinkIdentity = { type: 'identity', @@ -707,7 +708,7 @@ describe('CLI Identities', () => { const [, claimEncoded] = await nodeB.sigchain.addClaim(claimIdentToB); const claim = claimsUtils.decodeClaim(claimEncoded); await testProvider.publishClaim(identityId, claim); - }, global.polykeyStartupTimeout * 2); + }, global.defaultTimeout * 3); afterAll(async () => { await nodeC.stop(); await nodeB.stop(); @@ -719,6 +720,9 @@ describe('CLI Identities', () => { recursive: true, }); }); + beforeEach(async () => { + await polykeyAgent.gestaltGraph.clearDB(); + }); afterEach(async () => { // Clean the local nodes gestalt graph here. await polykeyAgent.gestaltGraph.clearDB(); @@ -728,8 +732,8 @@ describe('CLI Identities', () => { test('Should start discovery by Node', async () => { // Authenticate identity await polykeyAgent.identitiesManager.putToken( - testToken.providerId, - identityId, + testProvider.id, + testToken.identityId, testToken.tokenData, ); @@ -743,10 +747,28 @@ describe('CLI Identities', () => { ]; const result = await testBinUtils.pkStdio(commands); expect(result.exitCode).toBe(0); - - // We expect to find a gestalt now. - const gestalt = await polykeyAgent.gestaltGraph.getGestalts(); - expect(gestalt.length).not.toBe(0); + // Should eventually discover entire gestalt + const gestalt = await poll( + async () => { + const gestalts = await poll>( + async () => { + return await polykeyAgent.gestaltGraph.getGestalts(); + }, + (_, result) => { + if (result.length === 1) return true; + return false; + }, + 100, + ); + return gestalts[0]; + }, + (_, result) => { + if (result === undefined) return false; + if (Object.keys(result.matrix).length === 3) return true; + return false; + }, + 100, + ); const gestaltString = JSON.stringify(gestalt); expect(gestaltString).toContain( nodesUtils.encodeNodeId(nodeB.nodeManager.getNodeId()), @@ -757,15 +779,15 @@ describe('CLI Identities', () => { expect(gestaltString).toContain(identityId); // Unauthenticate identity await polykeyAgent.identitiesManager.delToken( - testToken.providerId, - identityId, + testProvider.id, + testToken.identityId, ); }); test('Should start discovery by Identity', async () => { // Authenticate identity await polykeyAgent.identitiesManager.putToken( - testToken.providerId, - identityId, + testProvider.id, + testToken.identityId, testToken.tokenData, ); const commands = [ @@ -777,9 +799,28 @@ describe('CLI Identities', () => { ]; const result = await testBinUtils.pkStdio(commands, {}, dataDir); expect(result.exitCode).toBe(0); - // We expect to find a gestalt now. - const gestalt = await polykeyAgent.gestaltGraph.getGestalts(); - expect(gestalt.length).not.toBe(0); + // Should eventually discover entire gestalt + const gestalt = await poll( + async () => { + const gestalts = await poll>( + async () => { + return await polykeyAgent.gestaltGraph.getGestalts(); + }, + (_, result) => { + if (result.length === 1) return true; + return false; + }, + 100, + ); + return gestalts[0]; + }, + (_, result) => { + if (result === undefined) return false; + if (Object.keys(result.matrix).length === 3) return true; + return false; + }, + 100, + ); const gestaltString = JSON.stringify(gestalt); expect(gestaltString).toContain( nodesUtils.encodeNodeId(nodeB.nodeManager.getNodeId()), @@ -790,8 +831,8 @@ describe('CLI Identities', () => { expect(gestaltString).toContain(identityId); // Unauthenticate identity await polykeyAgent.identitiesManager.delToken( - testToken.providerId, - identityId, + testProvider.id, + testToken.identityId, ); }); }); diff --git a/tests/client/rpcGestalts.test.ts b/tests/client/rpcGestalts.test.ts index b314d16326..67dfa02373 100644 --- a/tests/client/rpcGestalts.test.ts +++ b/tests/client/rpcGestalts.test.ts @@ -1,25 +1,25 @@ import type * as grpc from '@grpc/grpc-js'; import type { IdentitiesManager } from '@/identities'; import type { GestaltGraph } from '@/gestalts'; -import type { NodeManager } from '@/nodes'; import type { IdentityId, IdentityInfo, ProviderId } from '@/identities/types'; import type { NodeIdEncoded, NodeInfo } from '@/nodes/types'; import type * as gestaltsPB from '@/proto/js/polykey/v1/gestalts/gestalts_pb'; import type { ClientServiceClient } from '@/proto/js/polykey/v1/client_service_grpc_pb'; +import type { Discovery } from '@/discovery'; import os from 'os'; import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { NodeManager } from '@/nodes'; import { PolykeyAgent } from '@'; +import { KeyManager } from '@/keys'; +import { ForwardProxy } from '@/network'; import * as utilsPB from '@/proto/js/polykey/v1/utils/utils_pb'; import * as nodesPB from '@/proto/js/polykey/v1/nodes/nodes_pb'; import * as identitiesPB from '@/proto/js/polykey/v1/identities/identities_pb'; import * as permissionsPB from '@/proto/js/polykey/v1/permissions/permissions_pb'; -import { KeyManager } from '@/keys'; -import { ForwardProxy } from '@/network'; import * as grpcUtils from '@/grpc/utils'; import * as gestaltsUtils from '@/gestalts/utils'; -import * as nodesErrors from '@/nodes/errors'; import * as nodesUtils from '@/nodes/utils'; import * as testUtils from './utils'; import TestProvider from '../identities/TestProvider'; @@ -44,6 +44,7 @@ describe('Client service', () => { let nodeManager: NodeManager; let gestaltGraph: GestaltGraph; let identitiesManager: IdentitiesManager; + let discovery: Discovery; let passwordFile: string; let callCredentials: grpc.Metadata; @@ -107,6 +108,7 @@ describe('Client service', () => { nodeManager = pkAgent.nodeManager; gestaltGraph = pkAgent.gestaltGraph; identitiesManager = pkAgent.identitiesManager; + discovery = pkAgent.discovery; // Adding provider const testProvider = new TestProvider(); @@ -245,6 +247,10 @@ describe('Client service', () => { expect(jsonString).toContain(nodesUtils.encodeNodeId(nodeId2)); // Contains NodeId }); test('should discover gestalt via Node.', async () => { + const mockedRequestChainData = jest + .spyOn(NodeManager.prototype, 'requestChainData') + .mockResolvedValue({}); + const gestaltsDiscoverNode = grpcUtils.promisifyUnaryCall( client, @@ -253,12 +259,20 @@ describe('Client service', () => { const nodeMessage = new nodesPB.Node(); nodeMessage.setNodeId(nodesUtils.encodeNodeId(nodeId2)); - // I have no idea how to test this. so we just check for expected error for now - await expect(() => - gestaltsDiscoverNode(nodeMessage, callCredentials), - ).rejects.toThrow(nodesErrors.ErrorNodeGraphEmptyDatabase); + expect( + await gestaltsDiscoverNode(nodeMessage, callCredentials), + ).toBeInstanceOf(utilsPB.EmptyMessage); + + // Revert side-effects + await discovery.stop(); + await discovery.start({ fresh: true }); + mockedRequestChainData.mockRestore(); }); test('should discover gestalt via Identity.', async () => { + const mockedRequestChainData = jest + .spyOn(NodeManager.prototype, 'requestChainData') + .mockResolvedValue({}); + const gestaltsDiscoverIdentity = grpcUtils.promisifyUnaryCall( client, @@ -274,10 +288,14 @@ describe('Client service', () => { const providerMessage = new identitiesPB.Provider(); providerMessage.setProviderId(testToken.providerId); providerMessage.setIdentityId(testToken.identityId); - // Technically contains a node, but no other thing, will succeed with no results expect( await gestaltsDiscoverIdentity(providerMessage, callCredentials), ).toBeInstanceOf(utilsPB.EmptyMessage); + + // Revert side-effects + await discovery.stop(); + await discovery.start({ fresh: true }); + mockedRequestChainData.mockRestore(); }); test('should get gestalt permissions by node.', async () => { const gestaltsGetActionsByNode = diff --git a/tests/client/service/gestaltsGestaltTrustByIdentity.test.ts b/tests/client/service/gestaltsGestaltTrustByIdentity.test.ts new file mode 100644 index 0000000000..8c86f4cbdd --- /dev/null +++ b/tests/client/service/gestaltsGestaltTrustByIdentity.test.ts @@ -0,0 +1,450 @@ +import type { Host, Port } from '@/network/types'; +import type { NodeIdEncoded } from '@/nodes/types'; +import type { IdentityId } from '@/identities/types'; +import type { ClaimLinkIdentity } from '@/claims/types'; +import type { ChainData } from '@/sigchain/types'; +import type { Gestalt } from '@/gestalts/types'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { Metadata } from '@grpc/grpc-js'; +import { DB } from '@matrixai/db'; +import { KeyManager } from '@/keys'; +import { GestaltGraph } from '@/gestalts'; +import { ACL } from '@/acl'; +import { GRPCServer } from '@/grpc'; +import { Discovery } from '@/discovery'; +import { IdentitiesManager } from '@/identities'; +import { NodeManager } from '@/nodes'; +import { Sigchain } from '@/sigchain'; +import { ForwardProxy, ReverseProxy } from '@/network'; +import { GRPCClientClient, ClientServiceService } from '@/client'; +import gestaltsGestaltTrustByIdentity from '@/client/service/gestaltsGestaltTrustByIdentity'; +import { PolykeyAgent } from '@'; +import { poll } from '@/utils'; +import * as identitiesPB from '@/proto/js/polykey/v1/identities/identities_pb'; +import * as utilsPB from '@/proto/js/polykey/v1/utils/utils_pb'; +import * as nodesUtils from '@/nodes/utils'; +import * as claimsUtils from '@/claims/utils'; +import * as gestaltsErrors from '@/gestalts/errors'; +import * as clientUtils from '@/client/utils'; +import * as keysUtils from '@/keys/utils'; +import * as testUtils from '../../utils'; +import TestProvider from '../../identities/TestProvider'; + +describe('gestaltsGestaltTrustByIdentity', () => { + const logger = new Logger( + 'gestaltsGestaltTrustByIdentity test', + LogLevel.WARN, + [new StreamHandler()], + ); + const password = 'helloworld'; + const authenticate = async (metaClient, metaServer = new Metadata()) => + metaServer; + const testProvider = new TestProvider(); + // Create node to trust + const connectedIdentity = 'trusted-node' as IdentityId; + let nodeDataDir: string; + let node: PolykeyAgent; + let nodeId: NodeIdEncoded; + const nodeChainData: ChainData = {}; + let mockedRequestChainData: jest.SpyInstance; + let mockedGenerateKeyPair: jest.SpyInstance; + let mockedGenerateDeterministicKeyPair: jest.SpyInstance; + beforeAll(async () => { + const globalKeyPair = await testUtils.setupGlobalKeypair(); + const nodeKeyPair = await keysUtils.generateKeyPair(2048); + mockedRequestChainData = jest + .spyOn(NodeManager.prototype, 'requestChainData') + .mockResolvedValue(nodeChainData); + mockedGenerateKeyPair = jest + .spyOn(keysUtils, 'generateKeyPair') + .mockResolvedValueOnce(nodeKeyPair) + .mockResolvedValue(globalKeyPair); + mockedGenerateDeterministicKeyPair = jest + .spyOn(keysUtils, 'generateDeterministicKeyPair') + .mockResolvedValueOnce(nodeKeyPair) + .mockResolvedValue(globalKeyPair); + nodeDataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'trusted-node-'), + ); + const nodePath = path.join(nodeDataDir, 'polykey'); + node = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + logger, + }); + nodeId = nodesUtils.encodeNodeId(node.nodeManager.getNodeId()); + node.identitiesManager.registerProvider(testProvider); + await node.identitiesManager.putToken(testProvider.id, connectedIdentity, { + accessToken: 'abc123', + }); + testProvider.users['trusted-node'] = {}; + const identityClaim: ClaimLinkIdentity = { + type: 'identity', + node: nodesUtils.encodeNodeId(node.nodeManager.getNodeId()), + provider: testProvider.id, + identity: connectedIdentity, + }; + const [claimId, claimEncoded] = await node.sigchain.addClaim(identityClaim); + const claim = claimsUtils.decodeClaim(claimEncoded); + nodeChainData[claimId] = claim; + await testProvider.publishClaim(connectedIdentity, claim); + }, global.maxTimeout); + afterAll(async () => { + await node.stop(); + await fs.promises.rm(nodeDataDir, { + force: true, + recursive: true, + }); + mockedGenerateKeyPair.mockRestore(); + mockedGenerateDeterministicKeyPair.mockRestore(); + mockedRequestChainData.mockRestore(); + }); + const authToken = 'abc123'; + let dataDir: string; + let discovery: Discovery; + let gestaltGraph: GestaltGraph; + let identitiesManager: IdentitiesManager; + let nodeManager: NodeManager; + let sigchain: Sigchain; + let fwdProxy: ForwardProxy; + let revProxy: ReverseProxy; + let acl: ACL; + let db: DB; + let keyManager: KeyManager; + let grpcServer: GRPCServer; + let grpcClient: GRPCClientClient; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const keysPath = path.join(dataDir, 'keys'); + keyManager = await KeyManager.createKeyManager({ + password, + keysPath, + logger, + }); + const dbPath = path.join(dataDir, 'db'); + db = await DB.createDB({ + dbPath, + logger, + crypto: { + key: keyManager.dbKey, + ops: { + encrypt: keysUtils.encryptWithKey, + decrypt: keysUtils.decryptWithKey, + }, + }, + }); + acl = await ACL.createACL({ + db, + logger, + }); + gestaltGraph = await GestaltGraph.createGestaltGraph({ + db, + acl, + logger, + }); + identitiesManager = await IdentitiesManager.createIdentitiesManager({ + db, + logger, + }); + identitiesManager.registerProvider(testProvider); + await identitiesManager.putToken( + testProvider.id, + 'test-user' as IdentityId, + { + accessToken: 'def456', + }, + ); + fwdProxy = new ForwardProxy({ + authToken, + logger, + }); + await fwdProxy.start({ + tlsConfig: { + keyPrivatePem: keyManager.getRootKeyPairPem().privateKey, + certChainPem: await keyManager.getRootCertChainPem(), + }, + }); + revProxy = new ReverseProxy({ logger }); + await revProxy.start({ + serverHost: '1.1.1.1' as Host, + serverPort: 1 as Port, + tlsConfig: { + keyPrivatePem: keyManager.getRootKeyPairPem().privateKey, + certChainPem: await keyManager.getRootCertChainPem(), + }, + }); + sigchain = await Sigchain.createSigchain({ + db, + keyManager, + logger, + }); + nodeManager = await NodeManager.createNodeManager({ + db, + keyManager, + sigchain, + fwdProxy, + revProxy, + logger, + }); + await nodeManager.setNode(nodesUtils.decodeNodeId(nodeId)!, { + host: node.revProxy.getIngressHost(), + port: node.revProxy.getIngressPort(), + }); + discovery = await Discovery.createDiscovery({ + db, + gestaltGraph, + identitiesManager, + nodeManager, + logger, + }); + const clientService = { + gestaltsGestaltTrustByIdentity: gestaltsGestaltTrustByIdentity({ + authenticate, + gestaltGraph, + discovery, + }), + }; + grpcServer = new GRPCServer({ logger }); + await grpcServer.start({ + services: [[ClientServiceService, clientService]], + host: '127.0.0.1' as Host, + port: 0 as Port, + }); + grpcClient = await GRPCClientClient.createGRPCClientClient({ + nodeId: keyManager.getNodeId(), + host: '127.0.0.1' as Host, + port: grpcServer.port, + logger, + }); + }); + afterEach(async () => { + await grpcClient.destroy(); + await grpcServer.stop(); + await discovery.stop(); + await nodeManager.stop(); + await sigchain.stop(); + await revProxy.stop(); + await fwdProxy.stop(); + await identitiesManager.stop(); + await gestaltGraph.stop(); + await acl.stop(); + await db.stop(); + await keyManager.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + test('trusts an identity (already set in gestalt graph)', async () => { + testProvider.users['disconnected-user'] = {}; + await gestaltGraph.linkNodeAndIdentity( + { + id: nodeId, + chain: {}, + }, + { + providerId: testProvider.id, + identityId: connectedIdentity, + claims: {}, + }, + ); + const request = new identitiesPB.Provider(); + request.setProviderId(testProvider.id); + request.setIdentityId(connectedIdentity); + const response = await grpcClient.gestaltsGestaltTrustByIdentity( + request, + clientUtils.encodeAuthFromPassword(password), + ); + expect(response).toBeInstanceOf(utilsPB.EmptyMessage); + expect( + await gestaltGraph.getGestaltActionsByIdentity( + testProvider.id, + connectedIdentity, + ), + ).toEqual({ + notify: null, + }); + // Reverse side effects + await gestaltGraph.unsetNode(nodesUtils.decodeNodeId(nodeId)!); + await gestaltGraph.unsetIdentity(testProvider.id, connectedIdentity); + }); + test('trusts an identity (new identity)', async () => { + const request = new identitiesPB.Provider(); + request.setProviderId(testProvider.id); + request.setIdentityId(connectedIdentity); + // Should fail on first attempt - need to allow time for the identity to be + // linked to a node via discovery + await expect( + grpcClient.gestaltsGestaltTrustByIdentity( + request, + clientUtils.encodeAuthFromPassword(password), + ), + ).rejects.toThrow(gestaltsErrors.ErrorGestaltsGraphIdentityIdMissing); + // Wait for both identity and node to be set in GG + await poll( + async () => { + const gestalts = await poll>( + async () => { + return await gestaltGraph.getGestalts(); + }, + (_, result) => { + if (result.length === 1) return true; + return false; + }, + 100, + ); + return gestalts[0]; + }, + (_, result) => { + if (result === undefined) return false; + if (Object.keys(result.matrix).length === 2) return true; + return false; + }, + 100, + ); + const response = await grpcClient.gestaltsGestaltTrustByIdentity( + request, + clientUtils.encodeAuthFromPassword(password), + ); + expect(response).toBeInstanceOf(utilsPB.EmptyMessage); + expect( + await gestaltGraph.getGestaltActionsByIdentity( + testProvider.id, + connectedIdentity, + ), + ).toEqual({ + notify: null, + }); + // Reverse side effects + await gestaltGraph.unsetNode(nodesUtils.decodeNodeId(nodeId)!); + await gestaltGraph.unsetIdentity(testProvider.id, connectedIdentity); + }); + test('cannot trust a disconnected identity', async () => { + testProvider.users['disconnected-user'] = {}; + const request = new identitiesPB.Provider(); + request.setProviderId(testProvider.id); + request.setIdentityId('disconnected-user'); + // Should fail on first attempt - attempt to find a connected node + await expect( + grpcClient.gestaltsGestaltTrustByIdentity( + request, + clientUtils.encodeAuthFromPassword(password), + ), + ).rejects.toThrow(gestaltsErrors.ErrorGestaltsGraphIdentityIdMissing); + // Wait and try again - should fail again because the identity has no + // linked nodes we can trust + await expect( + grpcClient.gestaltsGestaltTrustByIdentity( + request, + clientUtils.encodeAuthFromPassword(password), + ), + ).rejects.toThrow(gestaltsErrors.ErrorGestaltsGraphIdentityIdMissing); + }); + test('trust extends to entire gestalt', async () => { + await gestaltGraph.linkNodeAndIdentity( + { + id: nodeId, + chain: {}, + }, + { + providerId: testProvider.id, + identityId: connectedIdentity, + claims: {}, + }, + ); + const request = new identitiesPB.Provider(); + request.setProviderId(testProvider.id); + request.setIdentityId(connectedIdentity); + const response = await grpcClient.gestaltsGestaltTrustByIdentity( + request, + clientUtils.encodeAuthFromPassword(password), + ); + expect(response).toBeInstanceOf(utilsPB.EmptyMessage); + expect( + await gestaltGraph.getGestaltActionsByIdentity( + testProvider.id, + connectedIdentity, + ), + ).toEqual({ + notify: null, + }); + expect( + await gestaltGraph.getGestaltActionsByNode( + nodesUtils.decodeNodeId(nodeId)!, + ), + ).toEqual({ + notify: null, + }); + // Reverse side effects + await gestaltGraph.unsetNode(nodesUtils.decodeNodeId(nodeId)!); + await gestaltGraph.unsetIdentity(testProvider.id, connectedIdentity); + }); + test('links trusted identity to an existing node', async () => { + await gestaltGraph.setNode({ + id: nodeId, + chain: {}, + }); + const request = new identitiesPB.Provider(); + request.setProviderId(testProvider.id); + request.setIdentityId(connectedIdentity); + // Should fail on first attempt - need to allow time for the identity to be + // linked to a node via discovery + await expect( + grpcClient.gestaltsGestaltTrustByIdentity( + request, + clientUtils.encodeAuthFromPassword(password), + ), + ).rejects.toThrow(gestaltsErrors.ErrorGestaltsGraphIdentityIdMissing); + // Wait and try again - should succeed second time + // Wait for both identity and node to be set in GG + await poll( + async () => { + const gestalts = await poll>( + async () => { + return await gestaltGraph.getGestalts(); + }, + (_, result) => { + if (result.length === 1) return true; + return false; + }, + 100, + ); + return gestalts[0]; + }, + (_, result) => { + if (result === undefined) return false; + if (Object.keys(result.matrix).length === 2) return true; + return false; + }, + 100, + ); + const response = await grpcClient.gestaltsGestaltTrustByIdentity( + request, + clientUtils.encodeAuthFromPassword(password), + ); + expect(response).toBeInstanceOf(utilsPB.EmptyMessage); + expect( + await gestaltGraph.getGestaltActionsByIdentity( + testProvider.id, + connectedIdentity, + ), + ).toEqual({ + notify: null, + }); + expect( + await gestaltGraph.getGestaltActionsByNode( + nodesUtils.decodeNodeId(nodeId)!, + ), + ).toEqual({ + notify: null, + }); + // Reverse side effects + await gestaltGraph.unsetNode(nodesUtils.decodeNodeId(nodeId)!); + await gestaltGraph.unsetIdentity(testProvider.id, connectedIdentity); + }); +}); diff --git a/tests/client/service/gestaltsGestaltTrustByNode.test.ts b/tests/client/service/gestaltsGestaltTrustByNode.test.ts new file mode 100644 index 0000000000..adf14f19c4 --- /dev/null +++ b/tests/client/service/gestaltsGestaltTrustByNode.test.ts @@ -0,0 +1,332 @@ +import type { Host, Port } from '@/network/types'; +import type { NodeIdEncoded } from '@/nodes/types'; +import type { IdentityId } from '@/identities/types'; +import type { ClaimLinkIdentity } from '@/claims/types'; +import type { ChainData } from '@/sigchain/types'; +import type { Gestalt } from '@/gestalts/types'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { Metadata } from '@grpc/grpc-js'; +import { DB } from '@matrixai/db'; +import { KeyManager } from '@/keys'; +import { GestaltGraph } from '@/gestalts'; +import { ACL } from '@/acl'; +import { GRPCServer } from '@/grpc'; +import { Discovery } from '@/discovery'; +import { IdentitiesManager } from '@/identities'; +import { NodeManager } from '@/nodes'; +import { Sigchain } from '@/sigchain'; +import { ForwardProxy, ReverseProxy } from '@/network'; +import { GRPCClientClient, ClientServiceService } from '@/client'; +import gestaltsGestaltTrustByNode from '@/client/service/gestaltsGestaltTrustByNode'; +import { PolykeyAgent } from '@'; +import { poll } from '@/utils'; +import * as nodesPB from '@/proto/js/polykey/v1/nodes/nodes_pb'; +import * as utilsPB from '@/proto/js/polykey/v1/utils/utils_pb'; +import * as nodesUtils from '@/nodes/utils'; +import * as claimsUtils from '@/claims/utils'; +import * as keysUtils from '@/keys/utils'; +import * as clientUtils from '@/client/utils'; +import * as testUtils from '../../utils'; +import TestProvider from '../../identities/TestProvider'; + +describe('gestaltsGestaltTrustByNode', () => { + const logger = new Logger('gestaltsGestaltTrustByNode test', LogLevel.WARN, [ + new StreamHandler(), + ]); + const password = 'helloworld'; + const authenticate = async (metaClient, metaServer = new Metadata()) => + metaServer; + const testProvider = new TestProvider(); + // Create node to trust + const connectedIdentity = 'trusted-node' as IdentityId; + let nodeDataDir: string; + let node: PolykeyAgent; + let nodeId: NodeIdEncoded; + const nodeChainData: ChainData = {}; + let mockedRequestChainData: jest.SpyInstance; + let mockedGenerateKeyPair: jest.SpyInstance; + let mockedGenerateDeterministicKeyPair: jest.SpyInstance; + beforeAll(async () => { + const globalKeyPair = await testUtils.setupGlobalKeypair(); + const nodeKeyPair = await keysUtils.generateKeyPair(2048); + mockedRequestChainData = jest + .spyOn(NodeManager.prototype, 'requestChainData') + .mockResolvedValue(nodeChainData); + mockedGenerateKeyPair = jest + .spyOn(keysUtils, 'generateKeyPair') + .mockResolvedValueOnce(nodeKeyPair) + .mockResolvedValue(globalKeyPair); + mockedGenerateDeterministicKeyPair = jest + .spyOn(keysUtils, 'generateDeterministicKeyPair') + .mockResolvedValueOnce(nodeKeyPair) + .mockResolvedValue(globalKeyPair); + nodeDataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'trusted-node-'), + ); + const nodePath = path.join(nodeDataDir, 'polykey'); + node = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + logger, + }); + nodeId = nodesUtils.encodeNodeId(node.nodeManager.getNodeId()); + node.identitiesManager.registerProvider(testProvider); + await node.identitiesManager.putToken(testProvider.id, connectedIdentity, { + accessToken: 'abc123', + }); + testProvider.users['trusted-node'] = {}; + const identityClaim: ClaimLinkIdentity = { + type: 'identity', + node: nodesUtils.encodeNodeId(node.nodeManager.getNodeId()), + provider: testProvider.id, + identity: connectedIdentity, + }; + const [claimId, claimEncoded] = await node.sigchain.addClaim(identityClaim); + const claim = claimsUtils.decodeClaim(claimEncoded); + nodeChainData[claimId] = claim; + await testProvider.publishClaim(connectedIdentity, claim); + }, global.maxTimeout); + afterAll(async () => { + await node.stop(); + await fs.promises.rm(nodeDataDir, { + force: true, + recursive: true, + }); + mockedGenerateKeyPair.mockRestore(); + mockedGenerateDeterministicKeyPair.mockRestore(); + mockedRequestChainData.mockRestore(); + }); + const authToken = 'abc123'; + let dataDir: string; + let discovery: Discovery; + let gestaltGraph: GestaltGraph; + let identitiesManager: IdentitiesManager; + let nodeManager: NodeManager; + let sigchain: Sigchain; + let fwdProxy: ForwardProxy; + let revProxy: ReverseProxy; + let acl: ACL; + let db: DB; + let keyManager: KeyManager; + let grpcServer: GRPCServer; + let grpcClient: GRPCClientClient; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const keysPath = path.join(dataDir, 'keys'); + keyManager = await KeyManager.createKeyManager({ + password, + keysPath, + logger, + }); + const dbPath = path.join(dataDir, 'db'); + db = await DB.createDB({ + dbPath, + logger, + crypto: { + key: keyManager.dbKey, + ops: { + encrypt: keysUtils.encryptWithKey, + decrypt: keysUtils.decryptWithKey, + }, + }, + }); + acl = await ACL.createACL({ + db, + logger, + }); + gestaltGraph = await GestaltGraph.createGestaltGraph({ + db, + acl, + logger, + }); + identitiesManager = await IdentitiesManager.createIdentitiesManager({ + db, + logger, + }); + identitiesManager.registerProvider(testProvider); + await identitiesManager.putToken( + testProvider.id, + 'test-user' as IdentityId, + { + accessToken: 'def456', + }, + ); + fwdProxy = new ForwardProxy({ + authToken, + logger, + }); + await fwdProxy.start({ + tlsConfig: { + keyPrivatePem: keyManager.getRootKeyPairPem().privateKey, + certChainPem: await keyManager.getRootCertChainPem(), + }, + }); + revProxy = new ReverseProxy({ logger }); + await revProxy.start({ + serverHost: '1.1.1.1' as Host, + serverPort: 1 as Port, + tlsConfig: { + keyPrivatePem: keyManager.getRootKeyPairPem().privateKey, + certChainPem: await keyManager.getRootCertChainPem(), + }, + }); + sigchain = await Sigchain.createSigchain({ + db, + keyManager, + logger, + }); + nodeManager = await NodeManager.createNodeManager({ + db, + keyManager, + sigchain, + fwdProxy, + revProxy, + logger, + }); + await nodeManager.setNode(nodesUtils.decodeNodeId(nodeId)!, { + host: node.revProxy.getIngressHost(), + port: node.revProxy.getIngressPort(), + }); + discovery = await Discovery.createDiscovery({ + db, + gestaltGraph, + identitiesManager, + nodeManager, + logger, + }); + const clientService = { + gestaltsGestaltTrustByNode: gestaltsGestaltTrustByNode({ + authenticate, + gestaltGraph, + discovery, + }), + }; + grpcServer = new GRPCServer({ logger }); + await grpcServer.start({ + services: [[ClientServiceService, clientService]], + host: '127.0.0.1' as Host, + port: 0 as Port, + }); + grpcClient = await GRPCClientClient.createGRPCClientClient({ + nodeId: keyManager.getNodeId(), + host: '127.0.0.1' as Host, + port: grpcServer.port, + logger, + }); + }); + afterEach(async () => { + await grpcClient.destroy(); + await grpcServer.stop(); + await discovery.stop(); + await nodeManager.stop(); + await sigchain.stop(); + await revProxy.stop(); + await fwdProxy.stop(); + await identitiesManager.stop(); + await gestaltGraph.stop(); + await acl.stop(); + await db.stop(); + await keyManager.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + test('trusts a node (already set in gestalt graph)', async () => { + await gestaltGraph.setNode({ + id: nodeId, + chain: {}, + }); + const request = new nodesPB.Node(); + request.setNodeId(nodeId); + const response = await grpcClient.gestaltsGestaltTrustByNode( + request, + clientUtils.encodeAuthFromPassword(password), + ); + expect(response).toBeInstanceOf(utilsPB.EmptyMessage); + expect( + await gestaltGraph.getGestaltActionsByNode( + nodesUtils.decodeNodeId(nodeId)!, + ), + ).toEqual({ + notify: null, + }); + // Reverse side effects + await gestaltGraph.unsetNode(nodesUtils.decodeNodeId(nodeId)!); + await gestaltGraph.unsetIdentity(testProvider.id, connectedIdentity); + }); + test('trusts a node (new node)', async () => { + const request = new nodesPB.Node(); + request.setNodeId(nodeId); + const response = await grpcClient.gestaltsGestaltTrustByNode( + request, + clientUtils.encodeAuthFromPassword(password), + ); + expect(response).toBeInstanceOf(utilsPB.EmptyMessage); + expect( + await gestaltGraph.getGestaltActionsByNode( + nodesUtils.decodeNodeId(nodeId)!, + ), + ).toEqual({ + notify: null, + }); + // Reverse side effects + await gestaltGraph.unsetNode(nodesUtils.decodeNodeId(nodeId)!); + await gestaltGraph.unsetIdentity(testProvider.id, connectedIdentity); + }); + test('trust extends to entire gestalt', async () => { + const request = new nodesPB.Node(); + request.setNodeId(nodeId); + const response = await grpcClient.gestaltsGestaltTrustByNode( + request, + clientUtils.encodeAuthFromPassword(password), + ); + expect(response).toBeInstanceOf(utilsPB.EmptyMessage); + expect( + await gestaltGraph.getGestaltActionsByNode( + nodesUtils.decodeNodeId(nodeId)!, + ), + ).toEqual({ + notify: null, + }); + // Give discovery process time to complete before checking identity actions + // Wait for both identity and node to be set in GG + await poll( + async () => { + const gestalts = await poll>( + async () => { + return await gestaltGraph.getGestalts(); + }, + (_, result) => { + if (result.length === 1) return true; + return false; + }, + 100, + ); + return gestalts[0]; + }, + (_, result) => { + if (result === undefined) return false; + if (Object.keys(result.matrix).length === 2) return true; + return false; + }, + 100, + ); + expect( + await gestaltGraph.getGestaltActionsByIdentity( + testProvider.id, + connectedIdentity, + ), + ).toEqual({ + notify: null, + }); + // Reverse side effects + await gestaltGraph.unsetNode(nodesUtils.decodeNodeId(nodeId)!); + await gestaltGraph.unsetIdentity(testProvider.id, connectedIdentity); + }); +}); diff --git a/tests/client/service/identitiesInfoConnectedGet.test.ts b/tests/client/service/identitiesInfoConnectedGet.test.ts new file mode 100644 index 0000000000..c71a7e0ee5 --- /dev/null +++ b/tests/client/service/identitiesInfoConnectedGet.test.ts @@ -0,0 +1,655 @@ +import type { IdentityId, ProviderId } from '@/identities/types'; +import type { Host, Port } from '@/network/types'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { Metadata } from '@grpc/grpc-js'; +import { DB } from '@matrixai/db'; +import { GRPCServer } from '@/grpc'; +import { IdentitiesManager } from '@/identities'; +import { GRPCClientClient, ClientServiceService } from '@/client'; +import identitiesInfoConnectedGet from '@/client/service/identitiesInfoConnectedGet'; +import * as identitiesPB from '@/proto/js/polykey/v1/identities/identities_pb'; +import * as nodesUtils from '@/nodes/utils'; +import * as identitiesErrors from '@/identities/errors'; +import * as clientUtils from '@/client/utils'; +import TestProvider from '../../identities/TestProvider'; + +describe('identitiesInfoConnectedGet', () => { + const logger = new Logger('identitiesInfoConnectedGet test', LogLevel.WARN, [ + new StreamHandler(), + ]); + const password = 'helloworld'; + const authenticate = async (metaClient, metaServer = new Metadata()) => + metaServer; + const testToken = { + identityId: 'test_user' as IdentityId, + tokenData: { + accessToken: 'abc123', + }, + }; + let dataDir: string; + let identitiesManager: IdentitiesManager; + let db: DB; + let grpcServer: GRPCServer; + let grpcClient: GRPCClientClient; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const dbPath = path.join(dataDir, 'db'); + db = await DB.createDB({ + dbPath, + logger, + }); + identitiesManager = await IdentitiesManager.createIdentitiesManager({ + db, + logger, + }); + const clientService = { + identitiesInfoConnectedGet: identitiesInfoConnectedGet({ + authenticate, + identitiesManager, + }), + }; + grpcServer = new GRPCServer({ logger }); + await grpcServer.start({ + services: [[ClientServiceService, clientService]], + host: '127.0.0.1' as Host, + port: 0 as Port, + }); + grpcClient = await GRPCClientClient.createGRPCClientClient({ + nodeId: nodesUtils.decodeNodeId( + 'vrcacp9vsb4ht25hds6s4lpp2abfaso0mptcfnh499n35vfcn2gkg', + )!, + host: '127.0.0.1' as Host, + port: grpcServer.port, + logger, + }); + }); + afterEach(async () => { + await grpcClient.destroy(); + await grpcServer.stop(); + await identitiesManager.stop(); + await db.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + test('gets connected identities from a single provider', async () => { + // Setup provider + const provider = new TestProvider(); + const user1 = { + providerId: provider.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider.users['user1'] = user1; + const user2 = { + providerId: provider.id, + identityId: 'user2' as IdentityId, + name: 'User2', + email: 'user2@test.com', + url: 'test.com/user2', + }; + provider.users['user2'] = user2; + identitiesManager.registerProvider(provider); + await identitiesManager.putToken( + provider.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setProviderIdList([provider.id]); + const response = grpcClient.identitiesInfoConnectedGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(2); + expect(output[0]).toEqual({ + email: user1.email, + name: user1.name, + provider: { + identityId: user1.identityId, + providerId: user1.providerId, + }, + url: user1.url, + }); + expect(output[1]).toEqual({ + email: user2.email, + name: user2.name, + provider: { + identityId: user2.identityId, + providerId: user2.providerId, + }, + url: user2.url, + }); + }); + test('gets connected identities from multiple providers', async () => { + // Setup providers + const provider1 = new TestProvider('provider1' as ProviderId); + const provider2 = new TestProvider('provider2' as ProviderId); + const user1 = { + providerId: provider1.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider1.users['user1'] = user1; + const user2 = { + providerId: provider2.id, + identityId: 'user2' as IdentityId, + name: 'User2', + email: 'user2@test.com', + url: 'test.com/user2', + }; + provider2.users['user2'] = user2; + identitiesManager.registerProvider(provider1); + identitiesManager.registerProvider(provider2); + await identitiesManager.putToken( + provider1.id, + testToken.identityId, + testToken.tokenData, + ); + await identitiesManager.putToken( + provider2.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setProviderIdList([provider1.id, provider2.id]); + const response = grpcClient.identitiesInfoConnectedGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(2); + expect(output[0]).toEqual({ + email: user1.email, + name: user1.name, + provider: { + identityId: user1.identityId, + providerId: user1.providerId, + }, + url: user1.url, + }); + expect(output[1]).toEqual({ + email: user2.email, + name: user2.name, + provider: { + identityId: user2.identityId, + providerId: user2.providerId, + }, + url: user2.url, + }); + }); + test('gets connected identities from all providers', async () => { + // Setup providers + const provider1 = new TestProvider('provider1' as ProviderId); + const provider2 = new TestProvider('provider2' as ProviderId); + const user1 = { + providerId: provider1.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider1.users['user1'] = user1; + const user2 = { + providerId: provider2.id, + identityId: 'user2' as IdentityId, + name: 'User2', + email: 'user2@test.com', + url: 'test.com/user2', + }; + provider2.users['user2'] = user2; + identitiesManager.registerProvider(provider1); + identitiesManager.registerProvider(provider2); + await identitiesManager.putToken( + provider1.id, + testToken.identityId, + testToken.tokenData, + ); + await identitiesManager.putToken( + provider2.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setProviderIdList([]); + const response = grpcClient.identitiesInfoConnectedGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(2); + expect(output[0]).toEqual({ + email: user1.email, + name: user1.name, + provider: { + identityId: user1.identityId, + providerId: user1.providerId, + }, + url: user1.url, + }); + expect(output[1]).toEqual({ + email: user2.email, + name: user2.name, + provider: { + identityId: user2.identityId, + providerId: user2.providerId, + }, + url: user2.url, + }); + }); + test('searches for identities matching a search term', async () => { + // Setup provider + const provider = new TestProvider(); + const user1 = { + providerId: provider.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider.users['user1'] = user1; + const user2 = { + providerId: provider.id, + identityId: 'user2' as IdentityId, + name: 'User2', + email: 'user2@test.com', + url: 'test.com/user2', + }; + provider.users['user2'] = user2; + identitiesManager.registerProvider(provider); + await identitiesManager.putToken( + provider.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setSearchTermList(['1']); + const response = grpcClient.identitiesInfoConnectedGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(1); + expect(output[0]).toEqual({ + email: user1.email, + name: user1.name, + provider: { + identityId: user1.identityId, + providerId: user1.providerId, + }, + url: user1.url, + }); + }); + test('searches for identities matching multiple search terms', async () => { + // Setup providers + const provider = new TestProvider(); + const user1 = { + providerId: provider.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider.users['user1'] = user1; + const user2 = { + providerId: provider.id, + identityId: 'user2' as IdentityId, + name: 'User2', + email: 'user2@test.com', + url: 'test.com/user2', + }; + provider.users['user2'] = user2; + identitiesManager.registerProvider(provider); + await identitiesManager.putToken( + provider.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setSearchTermList(['1', '2']); + const response = grpcClient.identitiesInfoConnectedGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(2); + expect(output[0]).toEqual({ + email: user1.email, + name: user1.name, + provider: { + identityId: user1.identityId, + providerId: user1.providerId, + }, + url: user1.url, + }); + expect(output[1]).toEqual({ + email: user2.email, + name: user2.name, + provider: { + identityId: user2.identityId, + providerId: user2.providerId, + }, + url: user2.url, + }); + }); + test('searches for identities matching a search term across multiple providers', async () => { + // Setup providers + const provider1 = new TestProvider('provider1' as ProviderId); + const provider2 = new TestProvider('provider2' as ProviderId); + const user1 = { + providerId: provider1.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider1.users['user1'] = user1; + const user2 = { + providerId: provider2.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider2.users['user1'] = user2; + identitiesManager.registerProvider(provider1); + identitiesManager.registerProvider(provider2); + await identitiesManager.putToken( + provider1.id, + testToken.identityId, + testToken.tokenData, + ); + await identitiesManager.putToken( + provider2.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setSearchTermList(['1']); + const response = grpcClient.identitiesInfoConnectedGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(2); + expect(output[0]).toEqual({ + email: user1.email, + name: user1.name, + provider: { + identityId: user1.identityId, + providerId: user1.providerId, + }, + url: user1.url, + }); + expect(output[1]).toEqual({ + email: user2.email, + name: user2.name, + provider: { + identityId: user2.identityId, + providerId: user2.providerId, + }, + url: user2.url, + }); + }); + test('gets no connected identities', async () => { + // Setup provider + const provider = new TestProvider(); + const user1 = { + providerId: provider.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider.users['user1'] = user1; + const user2 = { + providerId: provider.id, + identityId: 'user2' as IdentityId, + name: 'User2', + email: 'user2@test.com', + url: 'test.com/user2', + }; + provider.users['user2'] = user2; + identitiesManager.registerProvider(provider); + await identitiesManager.putToken( + provider.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setLimit('0'); + const response = grpcClient.identitiesInfoConnectedGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(0); + }); + test('gets one connected identity', async () => { + // Setup providers + const provider1 = new TestProvider('provider1' as ProviderId); + const provider2 = new TestProvider('provider2' as ProviderId); + const user1 = { + providerId: provider1.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider1.users['user1'] = user1; + const user2 = { + providerId: provider2.id, + identityId: 'user2' as IdentityId, + name: 'User2', + email: 'user2@test.com', + url: 'test.com/user2', + }; + provider2.users['user2'] = user2; + identitiesManager.registerProvider(provider1); + identitiesManager.registerProvider(provider2); + await identitiesManager.putToken( + provider1.id, + testToken.identityId, + testToken.tokenData, + ); + await identitiesManager.putToken( + provider2.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setLimit('1'); + const response = grpcClient.identitiesInfoConnectedGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(1); + expect(output[0]).toEqual({ + email: user1.email, + name: user1.name, + provider: { + identityId: user1.identityId, + providerId: user1.providerId, + }, + url: user1.url, + }); + }); + test('cannot get more identities than available', async () => { + // Setup providers + const provider1 = new TestProvider('provider1' as ProviderId); + const provider2 = new TestProvider('provider2' as ProviderId); + const user1 = { + providerId: provider1.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider1.users['user1'] = user1; + const user2 = { + providerId: provider2.id, + identityId: 'user2' as IdentityId, + name: 'User2', + email: 'user2@test.com', + url: 'test.com/user2', + }; + provider2.users['user2'] = user2; + identitiesManager.registerProvider(provider1); + identitiesManager.registerProvider(provider2); + await identitiesManager.putToken( + provider1.id, + testToken.identityId, + testToken.tokenData, + ); + await identitiesManager.putToken( + provider2.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setLimit('3'); + const response = grpcClient.identitiesInfoConnectedGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(2); + expect(output[0]).toEqual({ + email: user1.email, + name: user1.name, + provider: { + identityId: user1.identityId, + providerId: user1.providerId, + }, + url: user1.url, + }); + expect(output[1]).toEqual({ + email: user2.email, + name: user2.name, + provider: { + identityId: user2.identityId, + providerId: user2.providerId, + }, + url: user2.url, + }); + }); + test('can only get from authenticated providers', async () => { + // Setup providers + const provider1 = new TestProvider('provider1' as ProviderId); + const provider2 = new TestProvider('provider2' as ProviderId); + const user1 = { + providerId: provider1.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider1.users['user1'] = user1; + const user2 = { + providerId: provider2.id, + identityId: 'user2' as IdentityId, + name: 'User2', + email: 'user2@test.com', + url: 'test.com/user2', + }; + provider2.users['user2'] = user2; + identitiesManager.registerProvider(provider1); + identitiesManager.registerProvider(provider2); + await identitiesManager.putToken( + provider1.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + const response = grpcClient.identitiesInfoConnectedGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(1); + expect(output[0]).toEqual({ + email: user1.email, + name: user1.name, + provider: { + identityId: user1.identityId, + providerId: user1.providerId, + }, + url: user1.url, + }); + }); + test('gets disconnected identities', async () => { + // This feature is not implemented yet - should throw error + const request = new identitiesPB.ProviderSearch(); + request.setDisconnected(true); + await expect( + grpcClient + .identitiesInfoConnectedGet( + request, + clientUtils.encodeAuthFromPassword(password), + ) + .next(), + ).rejects.toThrow(identitiesErrors.ErrorProviderUnimplemented); + }); +}); diff --git a/tests/client/service/identitiesInfoGet.test.ts b/tests/client/service/identitiesInfoGet.test.ts index 344444210e..a6eaaef7ab 100644 --- a/tests/client/service/identitiesInfoGet.test.ts +++ b/tests/client/service/identitiesInfoGet.test.ts @@ -8,14 +8,11 @@ import { Metadata } from '@grpc/grpc-js'; import { DB } from '@matrixai/db'; import { GRPCServer } from '@/grpc'; import { IdentitiesManager } from '@/identities'; -import { - GRPCClientClient, - ClientServiceService, - utils as clientUtils, -} from '@/client'; +import { GRPCClientClient, ClientServiceService } from '@/client'; import identitiesInfoGet from '@/client/service/identitiesInfoGet'; import * as identitiesPB from '@/proto/js/polykey/v1/identities/identities_pb'; -import { utils as nodesUtils } from '@/nodes'; +import * as nodesUtils from '@/nodes/utils'; +import * as clientUtils from '@/client/utils'; import TestProvider from '../../identities/TestProvider'; describe('identitiesInfoGet', () => { @@ -26,23 +23,12 @@ describe('identitiesInfoGet', () => { const authenticate = async (metaClient, metaServer = new Metadata()) => metaServer; const testToken = { - providerId: 'test-provider' as ProviderId, identityId: 'test_user' as IdentityId, tokenData: { accessToken: 'abc123', }, }; - let mockedGetAuthIdentityIds: jest.SpyInstance; - beforeAll(async () => { - mockedGetAuthIdentityIds = jest - .spyOn(TestProvider.prototype, 'getAuthIdentityIds') - .mockResolvedValue([testToken.identityId]); - }); - afterAll(async () => { - mockedGetAuthIdentityIds.mockRestore(); - }); let dataDir: string; - let testProvider: TestProvider; let identitiesManager: IdentitiesManager; let db: DB; let grpcServer: GRPCServer; @@ -60,8 +46,6 @@ describe('identitiesInfoGet', () => { db, logger, }); - testProvider = new TestProvider(); - identitiesManager.registerProvider(testProvider); const clientService = { identitiesInfoGet: identitiesInfoGet({ authenticate, @@ -93,16 +77,375 @@ describe('identitiesInfoGet', () => { recursive: true, }); }); - test('gets identity', async () => { - const request = new identitiesPB.Provider(); - request.setProviderId(testToken.providerId); - request.setIdentityId(testToken.identityId); - const response = await grpcClient.identitiesInfoGet( + test('gets an identity', async () => { + // Setup provider + const provider = new TestProvider(); + const user1 = { + providerId: provider.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider.users['user1'] = user1; + identitiesManager.registerProvider(provider); + await identitiesManager.putToken( + provider.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setIdentityId(user1.identityId); + const response = grpcClient.identitiesInfoGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(1); + expect(output[0]).toEqual({ + email: user1.email, + name: user1.name, + provider: { + identityId: user1.identityId, + providerId: user1.providerId, + }, + url: user1.url, + }); + }); + test('searches for a handle across providers', async () => { + // Setup providers + const provider1 = new TestProvider('provider1' as ProviderId); + const provider2 = new TestProvider('provider2' as ProviderId); + const user1 = { + providerId: provider1.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider1.users['user1'] = user1; + const user2 = { + providerId: provider2.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider2.users['user1'] = user2; + identitiesManager.registerProvider(provider1); + identitiesManager.registerProvider(provider2); + await identitiesManager.putToken( + provider1.id, + testToken.identityId, + testToken.tokenData, + ); + await identitiesManager.putToken( + provider2.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setIdentityId('user1'); + const response = grpcClient.identitiesInfoGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(2); + expect(output[0]).toEqual({ + email: user1.email, + name: user1.name, + provider: { + identityId: user1.identityId, + providerId: user1.providerId, + }, + url: user1.url, + }); + expect(output[1]).toEqual({ + email: user2.email, + name: user2.name, + provider: { + identityId: user2.identityId, + providerId: user2.providerId, + }, + url: user2.url, + }); + }); + test('searches for identities matching a search term', async () => { + // Setup providers + const provider1 = new TestProvider('provider1' as ProviderId); + const provider2 = new TestProvider('provider2' as ProviderId); + const user1 = { + providerId: provider1.id, + identityId: 'user1' as IdentityId, + name: 'abc', + email: 'abc@test.com', + url: 'provider1.com/user1', + }; + provider1.users['user1'] = user1; + const user2 = { + providerId: provider2.id, + identityId: 'user1' as IdentityId, + name: 'def', + email: 'def@test.com', + url: 'provider2.com/user1', + }; + provider2.users['user1'] = user2; + identitiesManager.registerProvider(provider1); + identitiesManager.registerProvider(provider2); + await identitiesManager.putToken( + provider1.id, + testToken.identityId, + testToken.tokenData, + ); + await identitiesManager.putToken( + provider2.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setIdentityId('user1'); + request.setSearchTermList(['abc']); + const response = grpcClient.identitiesInfoGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(1); + expect(output[0]).toEqual({ + email: user1.email, + name: user1.name, + provider: { + identityId: user1.identityId, + providerId: user1.providerId, + }, + url: user1.url, + }); + }); + test('gets no connected identities', async () => { + // Setup provider + const provider = new TestProvider(); + const user1 = { + providerId: provider.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider.users['user1'] = user1; + const user2 = { + providerId: provider.id, + identityId: 'user2' as IdentityId, + name: 'User2', + email: 'user2@test.com', + url: 'test.com/user2', + }; + provider.users['user2'] = user2; + identitiesManager.registerProvider(provider); + await identitiesManager.putToken( + provider.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setIdentityId('user1'); + request.setLimit('0'); + const response = grpcClient.identitiesInfoGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(0); + }); + test('gets one connected identity', async () => { + // Setup providers + const provider1 = new TestProvider('provider1' as ProviderId); + const provider2 = new TestProvider('provider2' as ProviderId); + const user1 = { + providerId: provider1.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider1.users['user1'] = user1; + const user2 = { + providerId: provider2.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider2.users['user1'] = user2; + identitiesManager.registerProvider(provider1); + identitiesManager.registerProvider(provider2); + await identitiesManager.putToken( + provider1.id, + testToken.identityId, + testToken.tokenData, + ); + await identitiesManager.putToken( + provider2.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setIdentityId('user1'); + request.setLimit('1'); + const response = grpcClient.identitiesInfoGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(1); + expect(output[0]).toEqual({ + email: user1.email, + name: user1.name, + provider: { + identityId: user1.identityId, + providerId: user1.providerId, + }, + url: user1.url, + }); + }); + test('cannot get more identities than available', async () => { + // Setup providers + const provider1 = new TestProvider('provider1' as ProviderId); + const provider2 = new TestProvider('provider2' as ProviderId); + const user1 = { + providerId: provider1.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider1.users['user1'] = user1; + const user2 = { + providerId: provider2.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider2.users['user1'] = user2; + identitiesManager.registerProvider(provider1); + identitiesManager.registerProvider(provider2); + await identitiesManager.putToken( + provider1.id, + testToken.identityId, + testToken.tokenData, + ); + await identitiesManager.putToken( + provider2.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setIdentityId('user1'); + request.setLimit('3'); + const response = grpcClient.identitiesInfoGet( + request, + clientUtils.encodeAuthFromPassword(password), + ); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(2); + expect(output[0]).toEqual({ + email: user1.email, + name: user1.name, + provider: { + identityId: user1.identityId, + providerId: user1.providerId, + }, + url: user1.url, + }); + expect(output[1]).toEqual({ + email: user2.email, + name: user2.name, + provider: { + identityId: user2.identityId, + providerId: user2.providerId, + }, + url: user2.url, + }); + }); + test('can only get from authenticated providers', async () => { + // Setup providers + const provider1 = new TestProvider('provider1' as ProviderId); + const provider2 = new TestProvider('provider2' as ProviderId); + const user1 = { + providerId: provider1.id, + identityId: 'user1' as IdentityId, + name: 'User1', + email: 'user1@test.com', + url: 'test.com/user1', + }; + provider1.users['user1'] = user1; + const user2 = { + providerId: provider2.id, + identityId: 'user1' as IdentityId, + name: 'User2', + email: 'user2@test.com', + url: 'test.com/user2', + }; + provider2.users['user1'] = user2; + identitiesManager.registerProvider(provider1); + identitiesManager.registerProvider(provider2); + await identitiesManager.putToken( + provider1.id, + testToken.identityId, + testToken.tokenData, + ); + const request = new identitiesPB.ProviderSearch(); + request.setIdentityId('user1'); + const response = grpcClient.identitiesInfoGet( request, clientUtils.encodeAuthFromPassword(password), ); - expect(response).toBeInstanceOf(identitiesPB.Provider); - expect(response.getProviderId()).toBe(testToken.providerId); - expect(response.getIdentityId()).toBe(testToken.identityId); + const output = Array(); + for await (const identityInfoMessage of response) { + expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); + const obj = identityInfoMessage.toObject(); + output.push(obj); + } + expect(output).toHaveLength(1); + expect(output[0]).toEqual({ + email: user1.email, + name: user1.name, + provider: { + identityId: user1.identityId, + providerId: user1.providerId, + }, + url: user1.url, + }); }); }); diff --git a/tests/client/service/identitiesInfoGetConnected.test.ts b/tests/client/service/identitiesInfoGetConnected.test.ts deleted file mode 100644 index 7a40b97a41..0000000000 --- a/tests/client/service/identitiesInfoGetConnected.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import type { IdentityData, IdentityId, ProviderId } from '@/identities/types'; -import type { Host, Port } from '@/network/types'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import { Metadata } from '@grpc/grpc-js'; -import { DB } from '@matrixai/db'; -import { GRPCServer } from '@/grpc'; -import { IdentitiesManager } from '@/identities'; -import { - GRPCClientClient, - ClientServiceService, - utils as clientUtils, -} from '@/client'; -import identitiesInfoGetConnected from '@/client/service/identitiesInfoGetConnected'; -import * as identitiesPB from '@/proto/js/polykey/v1/identities/identities_pb'; -import { utils as nodesUtils } from '@/nodes'; -import TestProvider from '../../identities/TestProvider'; - -describe('identitiesInfoGetConnected', () => { - const logger = new Logger('identitiesInfoGetConnected test', LogLevel.WARN, [ - new StreamHandler(), - ]); - const password = 'helloworld'; - const authenticate = async (metaClient, metaServer = new Metadata()) => - metaServer; - const testToken = { - providerId: 'test-provider' as ProviderId, - identityId: 'test_user' as IdentityId, - tokenData: { - accessToken: 'abc123', - }, - }; - const user1: IdentityData = { - providerId: testToken.providerId, - identityId: 'user1' as IdentityId, - name: 'User1', - email: 'user1@test.com', - url: 'test.com/user1', - }; - const user2: IdentityData = { - providerId: testToken.providerId, - identityId: 'user2' as IdentityId, - name: 'User2', - email: 'user2@test.com', - url: 'test.com/user2', - }; - const getConnectedInfos = async function* (): AsyncGenerator { - yield user1; - yield user2; - }; - let mockedGetConnectedIdentityDatas: jest.SpyInstance; - beforeAll(async () => { - mockedGetConnectedIdentityDatas = jest - .spyOn(TestProvider.prototype, 'getConnectedIdentityDatas') - .mockImplementation(getConnectedInfos); - }); - afterAll(async () => { - mockedGetConnectedIdentityDatas.mockRestore(); - }); - let dataDir: string; - let testProvider: TestProvider; - let identitiesManager: IdentitiesManager; - let db: DB; - let grpcServer: GRPCServer; - let grpcClient: GRPCClientClient; - beforeEach(async () => { - dataDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'polykey-test-'), - ); - const dbPath = path.join(dataDir, 'db'); - db = await DB.createDB({ - dbPath, - logger, - }); - identitiesManager = await IdentitiesManager.createIdentitiesManager({ - db, - logger, - }); - testProvider = new TestProvider(); - identitiesManager.registerProvider(testProvider); - const clientService = { - identitiesInfoGetConnected: identitiesInfoGetConnected({ - authenticate, - identitiesManager, - }), - }; - grpcServer = new GRPCServer({ logger }); - await grpcServer.start({ - services: [[ClientServiceService, clientService]], - host: '127.0.0.1' as Host, - port: 0 as Port, - }); - grpcClient = await GRPCClientClient.createGRPCClientClient({ - nodeId: nodesUtils.decodeNodeId( - 'vrcacp9vsb4ht25hds6s4lpp2abfaso0mptcfnh499n35vfcn2gkg', - )!, - host: '127.0.0.1' as Host, - port: grpcServer.port, - logger, - }); - }); - afterEach(async () => { - await grpcClient.destroy(); - await grpcServer.stop(); - await identitiesManager.stop(); - await db.stop(); - await fs.promises.rm(dataDir, { - force: true, - recursive: true, - }); - }); - test('gets connected identities', async () => { - // Needs an authenticated identity - await identitiesManager.putToken( - testToken.providerId, - testToken.identityId, - testToken.tokenData, - ); - const request = new identitiesPB.ProviderSearch(); - const provider = new identitiesPB.Provider(); - provider.setProviderId(testToken.providerId); - provider.setIdentityId(testToken.identityId); - request.setProvider(provider); - request.setSearchTermList([]); - const response = grpcClient.identitiesInfoGetConnected( - request, - clientUtils.encodeAuthFromPassword(password), - ); - const output = Array(); - for await (const identityInfoMessage of response) { - expect(identityInfoMessage).toBeInstanceOf(identitiesPB.Info); - const obj = identityInfoMessage.toObject(); - output.push(obj); - } - expect(output[0]).toEqual({ - email: user1.email, - name: user1.name, - provider: { - identityId: user1.identityId, - providerId: user1.providerId, - }, - url: user1.url, - }); - expect(output[1]).toEqual({ - email: user2.email, - name: user2.name, - provider: { - identityId: user2.identityId, - providerId: user2.providerId, - }, - url: user2.url, - }); - // Unauthenticate - await identitiesManager.delToken( - testToken.providerId, - testToken.identityId, - ); - }); -}); diff --git a/tests/discovery/Discovery.test.ts b/tests/discovery/Discovery.test.ts index 114a8a5982..e889fcef43 100644 --- a/tests/discovery/Discovery.test.ts +++ b/tests/discovery/Discovery.test.ts @@ -1,26 +1,29 @@ import type { ClaimLinkIdentity } from '@/claims/types'; import type { IdentityId, ProviderId } from '@/identities/types'; import type { Host, Port } from '@/network/types'; +import type { Gestalt } from '@/gestalts/types'; import fs from 'fs'; import path from 'path'; import os from 'os'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import { destroyed } from '@matrixai/async-init'; import { DB } from '@matrixai/db'; import { PolykeyAgent } from '@'; -import { utils as claimsUtils } from '@/claims'; -import { Discovery, errors as discoveryErrors } from '@/discovery'; +import { Discovery } from '@/discovery'; import { GestaltGraph } from '@/gestalts'; import { IdentitiesManager } from '@/identities'; import { NodeManager } from '@/nodes'; -import { KeyManager, utils as keysUtils } from '@/keys'; +import { KeyManager } from '@/keys'; import { ACL } from '@/acl'; import { Sigchain } from '@/sigchain'; import { ForwardProxy, ReverseProxy } from '@/network'; -import { utils as nodesUtils } from '@/nodes'; -import TestProvider from '../identities/TestProvider'; -import * as testUtils from '../utils'; +import { poll } from '@/utils'; +import * as nodesUtils from '@/nodes/utils'; +import * as claimsUtils from '@/claims/utils'; +import * as discoveryErrors from '@/discovery/errors'; +import * as keysUtils from '@/keys/utils'; import * as testNodesUtils from '../nodes/utils'; +import * as testUtils from '../utils'; +import TestProvider from '../identities/TestProvider'; describe('Discovery', () => { const password = 'password'; @@ -97,6 +100,11 @@ describe('Discovery', () => { logger: logger.getChild('identities'), }); identitiesManager.registerProvider(testProvider); + await identitiesManager.putToken( + testToken.providerId, + testToken.identityId, + testToken.tokenData, + ); sigchain = await Sigchain.createSigchain({ db, keyManager, @@ -157,6 +165,7 @@ describe('Discovery', () => { await nodeA.identitiesManager.putToken(testToken.providerId, identityId, { accessToken: 'def456', }); + testProvider.users[identityId] = {}; const identityClaim: ClaimLinkIdentity = { type: 'identity', node: nodesUtils.encodeNodeId(nodeB.nodeManager.getNodeId()), @@ -169,7 +178,9 @@ describe('Discovery', () => { }, global.maxTimeout); afterAll(async () => { await nodeA.stop(); + await nodeA.destroy(); await nodeB.stop(); + await nodeB.destroy(); await nodeManager.stop(); await revProxy.stop(); await fwdProxy.stop(); @@ -188,36 +199,61 @@ describe('Discovery', () => { }); test('discovery readiness', async () => { const discovery = await Discovery.createDiscovery({ + db, gestaltGraph, identitiesManager, nodeManager, logger, }); - expect(discovery[destroyed]).toBeFalsy(); + await expect(discovery.destroy()).rejects.toThrow( + discoveryErrors.ErrorDiscoveryRunning, + ); + await discovery.start(); + await discovery.stop(); await discovery.destroy(); - expect(discovery[destroyed]).toBeTruthy(); - expect(() => { - discovery.discoverGestaltByIdentity('' as ProviderId, '' as IdentityId); - }).toThrow(discoveryErrors.ErrorDiscoveryDestroyed); - expect(() => { - discovery.discoverGestaltByNode(testUtils.generateRandomNodeId()); - }).toThrow(discoveryErrors.ErrorDiscoveryDestroyed); + await expect( + discovery.queueDiscoveryByIdentity('' as ProviderId, '' as IdentityId), + ).rejects.toThrow(discoveryErrors.ErrorDiscoveryNotRunning); + await expect( + discovery.queueDiscoveryByNode(testUtils.generateRandomNodeId()), + ).rejects.toThrow(discoveryErrors.ErrorDiscoveryNotRunning); }); test('discovery by node', async () => { const discovery = await Discovery.createDiscovery({ + db, gestaltGraph, identitiesManager, nodeManager, logger, }); - const discoverProcess = discovery.discoverGestaltByNode( - nodeA.nodeManager.getNodeId(), + await discovery.queueDiscoveryByNode(nodeA.nodeManager.getNodeId()); + const gestalt = await poll( + async () => { + const gestalts = await poll>( + async () => { + return await gestaltGraph.getGestalts(); + }, + (_, result) => { + if (result.length === 1) return true; + return false; + }, + 100, + ); + return gestalts[0]; + }, + (_, result) => { + if (result === undefined) return false; + if (Object.keys(result.matrix).length === 3) return true; + return false; + }, + 100, ); - for await (const _step of discoverProcess) { - // Waiting for the discovery process to finish. - } - const gestalt = await gestaltGraph.getGestalts(); - expect(gestalt.length).not.toBe(0); + const gestaltMatrix = gestalt.matrix; + const gestaltNodes = gestalt.nodes; + const gestaltIdentities = gestalt.identities; + expect(Object.keys(gestaltMatrix)).toHaveLength(3); + expect(Object.keys(gestaltNodes)).toHaveLength(2); + expect(Object.keys(gestaltIdentities)).toHaveLength(1); const gestaltString = JSON.stringify(gestalt); expect(gestaltString).toContain( nodesUtils.encodeNodeId(nodeA.nodeManager.getNodeId()), @@ -226,30 +262,49 @@ describe('Discovery', () => { nodesUtils.encodeNodeId(nodeB.nodeManager.getNodeId()), ); expect(gestaltString).toContain(identityId); + // Reverse side-effects + await gestaltGraph.unsetNode(nodeA.nodeManager.getNodeId()); + await gestaltGraph.unsetNode(nodeB.nodeManager.getNodeId()); + await gestaltGraph.unsetIdentity(testToken.providerId, identityId); + await discovery.stop(); await discovery.destroy(); - await gestaltGraph.stop(); - await gestaltGraph.destroy(); - gestaltGraph = await GestaltGraph.createGestaltGraph({ - db, - acl, - logger, - }); }); test('discovery by identity', async () => { const discovery = await Discovery.createDiscovery({ + db, gestaltGraph, identitiesManager, nodeManager, logger, }); - const discoverProcess = discovery.discoverGestaltByNode( - nodeA.nodeManager.getNodeId(), + await discovery.queueDiscoveryByIdentity(testToken.providerId, identityId); + const gestalt = await poll( + async () => { + const gestalts = await poll>( + async () => { + return await gestaltGraph.getGestalts(); + }, + (_, result) => { + if (result.length === 1) return true; + return false; + }, + 100, + ); + return gestalts[0]; + }, + (_, result) => { + if (result === undefined) return false; + if (Object.keys(result.matrix).length === 3) return true; + return false; + }, + 100, ); - for await (const _step of discoverProcess) { - // Waiting for the discovery process to finish. - } - const gestalt = await gestaltGraph.getGestalts(); - expect(gestalt.length).not.toBe(0); + const gestaltMatrix = gestalt.matrix; + const gestaltNodes = gestalt.nodes; + const gestaltIdentities = gestalt.identities; + expect(Object.keys(gestaltMatrix)).toHaveLength(3); + expect(Object.keys(gestaltNodes)).toHaveLength(2); + expect(Object.keys(gestaltIdentities)).toHaveLength(1); const gestaltString = JSON.stringify(gestalt); expect(gestaltString).toContain( nodesUtils.encodeNodeId(nodeA.nodeManager.getNodeId()), @@ -258,13 +313,173 @@ describe('Discovery', () => { nodesUtils.encodeNodeId(nodeB.nodeManager.getNodeId()), ); expect(gestaltString).toContain(identityId); + // Reverse side-effects + await gestaltGraph.unsetNode(nodeA.nodeManager.getNodeId()); + await gestaltGraph.unsetNode(nodeB.nodeManager.getNodeId()); + await gestaltGraph.unsetIdentity(testToken.providerId, identityId); + await discovery.stop(); await discovery.destroy(); - await gestaltGraph.stop(); - await gestaltGraph.destroy(); - gestaltGraph = await GestaltGraph.createGestaltGraph({ + }); + test('updates previously discovered gestalts', async () => { + const discovery = await Discovery.createDiscovery({ db, - acl, + gestaltGraph, + identitiesManager, + nodeManager, + logger, + }); + await discovery.queueDiscoveryByNode(nodeA.nodeManager.getNodeId()); + const gestalt1 = await poll( + async () => { + const gestalts = await poll>( + async () => { + return await gestaltGraph.getGestalts(); + }, + (_, result) => { + if (result.length === 1) return true; + return false; + }, + 100, + ); + return gestalts[0]; + }, + (_, result) => { + if (result === undefined) return false; + if (Object.keys(result.matrix).length === 3) return true; + return false; + }, + 100, + ); + const gestaltMatrix1 = gestalt1.matrix; + const gestaltNodes1 = gestalt1.nodes; + const gestaltIdentities1 = gestalt1.identities; + expect(Object.keys(gestaltMatrix1)).toHaveLength(3); + expect(Object.keys(gestaltNodes1)).toHaveLength(2); + expect(Object.keys(gestaltIdentities1)).toHaveLength(1); + const gestaltString1 = JSON.stringify(gestalt1); + expect(gestaltString1).toContain( + nodesUtils.encodeNodeId(nodeA.nodeManager.getNodeId()), + ); + expect(gestaltString1).toContain( + nodesUtils.encodeNodeId(nodeB.nodeManager.getNodeId()), + ); + expect(gestaltString1).toContain(identityId); + // Add another linked identity + const identityId2 = 'other-gestalt2' as IdentityId; + await nodeA.identitiesManager.putToken(testToken.providerId, identityId2, { + accessToken: 'ghi789', + }); + testProvider.users[identityId2] = {}; + const identityClaim: ClaimLinkIdentity = { + type: 'identity', + node: nodesUtils.encodeNodeId(nodeA.nodeManager.getNodeId()), + provider: testProvider.id, + identity: identityId2, + }; + const [, claimEncoded] = await nodeA.sigchain.addClaim(identityClaim); + const claim = claimsUtils.decodeClaim(claimEncoded); + await testProvider.publishClaim(identityId2, claim); + // Note that eventually we would like to add in a system of revisiting + // already discovered vertices, however for now we must do this manually. + await discovery.queueDiscoveryByNode(nodeA.nodeManager.getNodeId()); + const gestalt2 = await poll( + async () => { + const gestalts = await poll>( + async () => { + return await gestaltGraph.getGestalts(); + }, + (_, result) => { + if (result.length === 1) return true; + return false; + }, + 100, + ); + return gestalts[0]; + }, + (_, result) => { + if (result === undefined) return false; + if (Object.keys(result.matrix).length === 4) return true; + return false; + }, + 100, + ); + const gestaltMatrix2 = gestalt2.matrix; + const gestaltNodes2 = gestalt2.nodes; + const gestaltIdentities2 = gestalt2.identities; + expect(Object.keys(gestaltMatrix2)).toHaveLength(4); + expect(Object.keys(gestaltNodes2)).toHaveLength(2); + expect(Object.keys(gestaltIdentities2)).toHaveLength(2); + const gestaltString2 = JSON.stringify(gestalt2); + expect(gestaltString2).toContain( + nodesUtils.encodeNodeId(nodeA.nodeManager.getNodeId()), + ); + expect(gestaltString2).toContain( + nodesUtils.encodeNodeId(nodeB.nodeManager.getNodeId()), + ); + expect(gestaltString2).toContain(identityId); + expect(gestaltString2).toContain(identityId2); + // Reverse side-effects + await gestaltGraph.unsetNode(nodeA.nodeManager.getNodeId()); + await gestaltGraph.unsetNode(nodeB.nodeManager.getNodeId()); + await gestaltGraph.unsetIdentity(testToken.providerId, identityId); + await gestaltGraph.unsetIdentity(testToken.providerId, identityId2); + // Can just remove the user that the claim is for as this will cause the + // claim to be dropped during discovery + delete testProvider.users[identityId2]; + await discovery.stop(); + await discovery.destroy(); + }); + test('discovery persistence across restarts', async () => { + const discovery = await Discovery.createDiscovery({ + db, + gestaltGraph, + identitiesManager, + nodeManager, logger, }); + await discovery.queueDiscoveryByNode(nodeA.nodeManager.getNodeId()); + await discovery.stop(); + await discovery.start(); + const gestalt = await poll( + async () => { + const gestalts = await poll>( + async () => { + return await gestaltGraph.getGestalts(); + }, + (_, result) => { + if (result.length === 1) return true; + return false; + }, + 100, + ); + return gestalts[0]; + }, + (_, result) => { + if (result === undefined) return false; + if (Object.keys(result.matrix).length === 3) return true; + return false; + }, + 100, + ); + const gestaltMatrix = gestalt.matrix; + const gestaltNodes = gestalt.nodes; + const gestaltIdentities = gestalt.identities; + expect(Object.keys(gestaltMatrix)).toHaveLength(3); + expect(Object.keys(gestaltNodes)).toHaveLength(2); + expect(Object.keys(gestaltIdentities)).toHaveLength(1); + const gestaltString = JSON.stringify(gestalt); + expect(gestaltString).toContain( + nodesUtils.encodeNodeId(nodeA.nodeManager.getNodeId()), + ); + expect(gestaltString).toContain( + nodesUtils.encodeNodeId(nodeB.nodeManager.getNodeId()), + ); + expect(gestaltString).toContain(identityId); + // Reverse side-effects + await gestaltGraph.unsetNode(nodeA.nodeManager.getNodeId()); + await gestaltGraph.unsetNode(nodeB.nodeManager.getNodeId()); + await gestaltGraph.unsetIdentity(testToken.providerId, identityId); + await discovery.stop(); + await discovery.destroy(); }); }); diff --git a/tests/gestalts/GestaltGraph.test.ts b/tests/gestalts/GestaltGraph.test.ts index 7cd3ab7a44..fa30c86bdc 100644 --- a/tests/gestalts/GestaltGraph.test.ts +++ b/tests/gestalts/GestaltGraph.test.ts @@ -14,15 +14,12 @@ import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { DB } from '@matrixai/db'; - -import { - GestaltGraph, - utils as gestaltsUtils, - errors as gestaltErrors, -} from '@/gestalts'; +import { GestaltGraph } from '@/gestalts'; import { ACL } from '@/acl'; +import * as gestaltsErrors from '@/gestalts/errors'; +import * as gestaltsUtils from '@/gestalts/utils'; import * as keysUtils from '@/keys/utils'; -import { utils as nodesUtils } from '@/nodes'; +import * as nodesUtils from '@/nodes/utils'; import * as testUtils from '../utils'; describe('GestaltGraph', () => { @@ -152,17 +149,17 @@ describe('GestaltGraph', () => { logger, }); await expect(gestaltGraph.destroy()).rejects.toThrow( - gestaltErrors.ErrorGestaltsGraphRunning, + gestaltsErrors.ErrorGestaltsGraphRunning, ); // Should be a noop await gestaltGraph.start(); await gestaltGraph.stop(); await gestaltGraph.destroy(); await expect(gestaltGraph.start()).rejects.toThrow( - gestaltErrors.ErrorGestaltsGraphDestroyed, + gestaltsErrors.ErrorGestaltsGraphDestroyed, ); await expect(gestaltGraph.getGestalts()).rejects.toThrow( - gestaltErrors.ErrorGestaltsGraphNotRunning, + gestaltsErrors.ErrorGestaltsGraphNotRunning, ); }); test('get, set and unset node', async () => { @@ -536,17 +533,23 @@ describe('GestaltGraph', () => { acl, logger, }); - const nodeInfo: NodeInfo = { + const nodeInfo1: NodeInfo = { id: nodeIdABCEncoded, chain: {}, }; - await gestaltGraph.setNode(nodeInfo); + const nodeInfo2: NodeInfo = { + id: nodeIdDEFEncoded, + chain: {}, + }; const identityInfo: IdentityInfo = { providerId: 'github.com' as ProviderId, identityId: 'abc' as IdentityId, claims: {}, }; + await gestaltGraph.setNode(nodeInfo1); + await gestaltGraph.setNode(nodeInfo2); await gestaltGraph.setIdentity(identityInfo); + await gestaltGraph.linkNodeAndIdentity(nodeInfo1, identityInfo); const gestalts = await gestaltGraph.getGestalts(); const identityGestalt = await gestaltGraph.getGestaltByIdentity( identityInfo.providerId, @@ -558,11 +561,12 @@ describe('GestaltGraph', () => { expect(gestalts).toHaveLength(2); // Check if the two combine after linking. - await gestaltGraph.linkNodeAndIdentity(nodeInfo, identityInfo); + await gestaltGraph.linkNodeAndNode(nodeInfo1, nodeInfo2); const gestalts2 = await gestaltGraph.getGestalts(); expect(gestalts2).toHaveLength(1); const gestalts2String = JSON.stringify(gestalts2[0]); - expect(gestalts2String).toContain(nodeInfo.id); + expect(gestalts2String).toContain(nodeInfo1.id); + expect(gestalts2String).toContain(nodeInfo2.id); expect(gestalts2String).toContain(identityInfo.providerId); expect(gestalts2String).toContain(identityInfo.identityId); diff --git a/tests/identities/IdentitiesManager.test.ts b/tests/identities/IdentitiesManager.test.ts index d1933b6580..170767cfc5 100644 --- a/tests/identities/IdentitiesManager.test.ts +++ b/tests/identities/IdentitiesManager.test.ts @@ -12,11 +12,10 @@ import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { DB } from '@matrixai/db'; - import { IdentitiesManager, providers } from '@/identities'; import * as identitiesErrors from '@/identities/errors'; import * as keysUtils from '@/keys/utils'; -import { utils as nodesUtils } from '@/nodes'; +import * as nodesUtils from '@/nodes/utils'; import TestProvider from './TestProvider'; import * as testUtils from '../utils'; @@ -208,6 +207,8 @@ describe('IdentitiesManager', () => { expect(identityData).toBeDefined(); expect(identityData).toHaveProperty('providerId', testProvider.id); expect(identityData).toHaveProperty('identityId', identityId); + // Give the provider a connected identity to discover + testProvider.users['some-user'] = {}; const identityDatas: Array = []; for await (const identityData_ of testProvider.getConnectedIdentityDatas( identityId, diff --git a/tests/identities/TestProvider.ts b/tests/identities/TestProvider.ts index a26567eb5c..e85d3c1636 100644 --- a/tests/identities/TestProvider.ts +++ b/tests/identities/TestProvider.ts @@ -8,14 +8,15 @@ import type { } from '@/identities/types'; import type { Claim } from '@/claims/types'; import type { IdentityClaim, IdentityClaimId } from '@/identities/types'; - -import { Provider, errors as identitiesErrors } from '@/identities'; +import { Provider } from '@/identities'; +import * as identitiesUtils from '@/identities/utils'; +import * as identitiesErrors from '@/identities/errors'; class TestProvider extends Provider { - public readonly id = 'test-provider' as ProviderId; + public readonly id: ProviderId; public linkIdCounter: number = 0; - protected users: Record; // FIXME: the string union on VaultId is to prevent some false errors. + public users: Record; // FIXME: the string union on VaultId is to prevent some false errors. public links: Record; // FIXME: the string union on VaultId is to prevent some false errors. protected userLinks: Record< IdentityId | string, @@ -23,15 +24,13 @@ class TestProvider extends Provider { >; // FIXME: the string union on VaultId is to prevent some false errors. protected userTokens: Record; - public constructor() { + public constructor(providerId: ProviderId = 'test-provider' as ProviderId) { super(); + this.id = providerId; this.users = { test_user: { email: 'test_user@test.com', }, - test_user2: { - email: 'test_user2@test.com', - }, }; this.userTokens = { abc123: 'test_user' as IdentityId, @@ -91,12 +90,15 @@ class TestProvider extends Provider { return { providerId: this.id, identityId: identityId, + name: user.name ?? undefined, email: user.email ?? undefined, + url: user.url ?? undefined, }; } public async *getConnectedIdentityDatas( authIdentityId: IdentityId, + searchTerms: Array = [], ): AsyncGenerator { let tokenData = await this.getToken(authIdentityId); if (!tokenData) { @@ -106,16 +108,21 @@ class TestProvider extends Provider { } tokenData = await this.checkToken(tokenData, authIdentityId); for (const [k, v] of Object.entries(this.users) as Array< - [IdentityId, { email: string }] + [IdentityId, { name: string; email: string; url: string }] >) { if (k === authIdentityId) { continue; } - yield { + const data: IdentityData = { providerId: this.id, identityId: k, + name: v.name ?? undefined, email: v.email ?? undefined, + url: v.url ?? undefined, }; + if (identitiesUtils.matchIdentityData(data, searchTerms)) { + yield data; + } } return; } @@ -134,7 +141,10 @@ class TestProvider extends Provider { const linkId = this.linkIdCounter.toString() as IdentityClaimId; this.linkIdCounter++; this.links[linkId] = JSON.stringify(identityClaim); - const links = this.userLinks[authIdentityId] ?? []; + this.userLinks[authIdentityId] = this.userLinks[authIdentityId] + ? this.userLinks[authIdentityId] + : []; + const links = this.userLinks[authIdentityId]; links.push(linkId); return { ...identityClaim,