0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-13 21:30:30 -05:00

fix(core): should validate config when creating a new connector instance (#3068)

This commit is contained in:
Darcy Ye 2023-02-08 14:35:03 +08:00 committed by GitHub
parent af81c81a53
commit 3a5ac1edc9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 88 additions and 16 deletions

View file

@ -6,7 +6,7 @@ import type Queries from '#src/tenants/Queries.js';
import assertThat from '#src/utils/assert-that.js';
import { defaultConnectorMethods } from '#src/utils/connectors/consts.js';
import { loadConnectorFactories } from '#src/utils/connectors/factories.js';
import { validateConnectorModule, parseMetadata } from '#src/utils/connectors/index.js';
import { buildRawConnector } from '#src/utils/connectors/index.js';
import type { LogtoConnector } from '#src/utils/connectors/types.js';
export type ConnectorLibrary = ReturnType<typeof createConnectorLibrary>;
@ -39,16 +39,11 @@ export const createConnectorLibrary = (queries: Queries) => {
return;
}
const { createConnector, path: packagePath } = connectorFactory;
try {
const rawConnector = await createConnector({
getConfig: async () => {
return getConnectorConfig(id);
},
});
validateConnectorModule(rawConnector);
const rawMetadata = await parseMetadata(rawConnector.metadata, packagePath);
const { rawConnector, rawMetadata } = await buildRawConnector(
connectorFactory,
async () => getConnectorConfig(id)
);
const connector: AllConnector = {
...defaultConnectorMethods,

View file

@ -1,6 +1,11 @@
/* eslint-disable max-lines */
import type { EmailConnector, SmsConnector } from '@logto/connector-kit';
import { ConnectorPlatform, VerificationCodeType } from '@logto/connector-kit';
import {
ConnectorError,
ConnectorErrorCodes,
ConnectorPlatform,
VerificationCodeType,
} from '@logto/connector-kit';
import type { Connector } from '@logto/schemas';
import { ConnectorType } from '@logto/schemas';
import { pickDefault, createMockUtils } from '@logto/shared/esm';
@ -26,7 +31,7 @@ import type { LogtoConnector } from '#src/utils/connectors/types.js';
import { createRequester } from '#src/utils/test-utils.js';
const { jest } = import.meta;
const { mockEsm } = createMockUtils(jest);
const { mockEsm, mockEsmWithActual } = createMockUtils(jest);
mockEsm('#src/utils/connectors/platform.js', () => ({
checkSocialConnectorTargetAndPlatformUniqueness: jest.fn(),
@ -56,6 +61,14 @@ const { loadConnectorFactories } = mockEsm('#src/utils/connectors/factories.js',
loadConnectorFactories: jest.fn(),
}));
const { buildRawConnector } = await mockEsmWithActual('#src/utils/connectors/index.js', () => ({
buildRawConnector: jest.fn(),
}));
const { validateConfig } = await mockEsmWithActual('@logto/connector-kit', () => ({
validateConfig: jest.fn(),
}));
const tenantContext = new MockTenant(
undefined,
{ connectors: connectorQueries },
@ -178,6 +191,8 @@ describe('connector route', () => {
...mockLogtoConnector,
},
]);
validateConfig.mockReturnValueOnce(null);
buildRawConnector.mockResolvedValueOnce({ rawConnector: { configGuard: any() } });
await connectorRequest.post('/connectors').send({
connectorId: 'connectorId',
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
@ -190,6 +205,33 @@ describe('connector route', () => {
);
});
it('throw when create a new connector record with wrong config', async () => {
loadConnectorFactories.mockResolvedValueOnce([
{
...mockConnectorFactory,
metadata: { ...mockConnectorFactory.metadata, id: 'connectorId' },
},
]);
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
getLogtoConnectors.mockResolvedValueOnce([
{
dbEntry: { ...mockConnector, connectorId: 'id0' },
metadata: { ...mockMetadata, id: 'id0' },
type: ConnectorType.Sms,
...mockLogtoConnector,
},
]);
validateConfig.mockImplementationOnce((config: unknown) => {
throw new ConnectorError(ConnectorErrorCodes.General);
});
buildRawConnector.mockResolvedValueOnce({ rawConnector: { configGuard: any() } });
const response = await connectorRequest.post('/connectors').send({
connectorId: 'connectorId',
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
});
expect(response).toHaveProperty('statusCode', 500);
});
it('throws when connector factory not found', async () => {
loadConnectorFactories.mockResolvedValueOnce([
{
@ -225,6 +267,8 @@ describe('connector route', () => {
...mockLogtoConnector,
},
]);
validateConfig.mockReturnValueOnce(null);
buildRawConnector.mockResolvedValueOnce({ rawConnector: { configGuard: any() } });
await connectorRequest.post('/connectors').send({
connectorId: 'id0',
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
@ -293,6 +337,8 @@ describe('connector route', () => {
},
]);
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
validateConfig.mockReturnValueOnce(null);
buildRawConnector.mockResolvedValueOnce({ rawConnector: { configGuard: any() } });
await connectorRequest.post('/connectors').send({
connectorId: 'id1',
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
@ -335,6 +381,8 @@ describe('connector route', () => {
...mockLogtoConnector,
},
]);
validateConfig.mockReturnValueOnce(null);
buildRawConnector.mockResolvedValueOnce({ rawConnector: { configGuard: any() } });
const response = await connectorRequest.post('/connectors').send({
connectorId: 'id0',
metadata: { target: 'target' },

View file

@ -1,14 +1,15 @@
import { VerificationCodeType } from '@logto/connector-kit';
import { VerificationCodeType, validateConfig } from '@logto/connector-kit';
import { emailRegEx, phoneRegEx, buildIdGenerator } from '@logto/core-kit';
import type { ConnectorResponse, ConnectorFactoryResponse } from '@logto/schemas';
import { arbitraryObjectGuard, Connectors, ConnectorType } from '@logto/schemas';
import cleanDeep from 'clean-deep';
import { object, string } from 'zod';
import { string, object } from 'zod';
import RequestError from '#src/errors/RequestError/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import assertThat from '#src/utils/assert-that.js';
import { loadConnectorFactories } from '#src/utils/connectors/factories.js';
import { buildRawConnector } from '#src/utils/connectors/index.js';
import { checkSocialConnectorTargetAndPlatformUniqueness } from '#src/utils/connectors/platform.js';
import type { LogtoConnector } from '#src/utils/connectors/types.js';
@ -184,6 +185,11 @@ export default function connectorRoutes<T extends AuthedRouter>(
);
}
if (config) {
const { rawConnector } = await buildRawConnector(connectorFactory);
validateConfig(config, rawConnector.configGuard);
}
const insertConnectorId = generateConnectorId();
await insertConnector({
id: insertConnectorId,

View file

@ -2,9 +2,12 @@ import { existsSync } from 'fs';
import { readFile } from 'fs/promises';
import path from 'path';
import type { AllConnector, BaseConnector } from '@logto/connector-kit';
import type { AllConnector, BaseConnector, GetConnectorConfig } from '@logto/connector-kit';
import { ConnectorError, ConnectorErrorCodes, ConnectorType } from '@logto/connector-kit';
import { notImplemented } from './consts.js';
import type { ConnectorFactory } from './types.js';
export function validateConnectorModule(
connector: Partial<BaseConnector<ConnectorType>>
): asserts connector is BaseConnector<ConnectorType> {
@ -47,7 +50,10 @@ export const readUrl = async (
return readFile(path.join(baseUrl, url), 'utf8');
};
export const parseMetadata = async (metadata: AllConnector['metadata'], packagePath: string) => {
export const parseMetadata = async (
metadata: AllConnector['metadata'],
packagePath: string
): Promise<AllConnector['metadata']> => {
return {
...metadata,
logo: await readUrl(metadata.logo, packagePath, 'svg'),
@ -56,3 +62,17 @@ export const parseMetadata = async (metadata: AllConnector['metadata'], packageP
configTemplate: await readUrl(metadata.configTemplate, packagePath, 'text'),
};
};
export const buildRawConnector = async (
connectorFactory: ConnectorFactory,
getConnectorConfig?: GetConnectorConfig
) => {
const { createConnector, path: packagePath } = connectorFactory;
const rawConnector = await createConnector({
getConfig: getConnectorConfig ?? notImplemented,
});
validateConnectorModule(rawConnector);
const rawMetadata = await parseMetadata(rawConnector.metadata, packagePath);
return { rawConnector, rawMetadata };
};

View file

@ -32,6 +32,7 @@
"test:ci": "pnpm test:only"
},
"devDependencies": {
"@logto/connector-kit": "workspace:*",
"@logto/core-kit": "workspace:*",
"@silverhand/eslint-config": "1.3.0",
"@silverhand/ts-config": "1.2.1",

2
pnpm-lock.yaml generated
View file

@ -625,6 +625,7 @@ importers:
packages/shared:
specifiers:
'@logto/connector-kit': workspace:*
'@logto/core-kit': workspace:*
'@logto/schemas': workspace:*
'@silverhand/eslint-config': 1.3.0
@ -647,6 +648,7 @@ importers:
nanoid: 4.0.0
slonik: 30.1.2
devDependencies:
'@logto/connector-kit': link:../toolkit/connector-kit
'@logto/core-kit': link:../toolkit/core-kit
'@silverhand/eslint-config': 1.3.0_k3lfx77tsvurbevhk73p7ygch4
'@silverhand/ts-config': 1.2.1_typescript@4.9.4