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:
parent
af81c81a53
commit
3a5ac1edc9
6 changed files with 88 additions and 16 deletions
|
@ -6,7 +6,7 @@ import type Queries from '#src/tenants/Queries.js';
|
||||||
import assertThat from '#src/utils/assert-that.js';
|
import assertThat from '#src/utils/assert-that.js';
|
||||||
import { defaultConnectorMethods } from '#src/utils/connectors/consts.js';
|
import { defaultConnectorMethods } from '#src/utils/connectors/consts.js';
|
||||||
import { loadConnectorFactories } from '#src/utils/connectors/factories.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';
|
import type { LogtoConnector } from '#src/utils/connectors/types.js';
|
||||||
|
|
||||||
export type ConnectorLibrary = ReturnType<typeof createConnectorLibrary>;
|
export type ConnectorLibrary = ReturnType<typeof createConnectorLibrary>;
|
||||||
|
@ -39,16 +39,11 @@ export const createConnectorLibrary = (queries: Queries) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { createConnector, path: packagePath } = connectorFactory;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rawConnector = await createConnector({
|
const { rawConnector, rawMetadata } = await buildRawConnector(
|
||||||
getConfig: async () => {
|
connectorFactory,
|
||||||
return getConnectorConfig(id);
|
async () => getConnectorConfig(id)
|
||||||
},
|
);
|
||||||
});
|
|
||||||
validateConnectorModule(rawConnector);
|
|
||||||
const rawMetadata = await parseMetadata(rawConnector.metadata, packagePath);
|
|
||||||
|
|
||||||
const connector: AllConnector = {
|
const connector: AllConnector = {
|
||||||
...defaultConnectorMethods,
|
...defaultConnectorMethods,
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import type { EmailConnector, SmsConnector } from '@logto/connector-kit';
|
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 type { Connector } from '@logto/schemas';
|
||||||
import { ConnectorType } from '@logto/schemas';
|
import { ConnectorType } from '@logto/schemas';
|
||||||
import { pickDefault, createMockUtils } from '@logto/shared/esm';
|
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';
|
import { createRequester } from '#src/utils/test-utils.js';
|
||||||
|
|
||||||
const { jest } = import.meta;
|
const { jest } = import.meta;
|
||||||
const { mockEsm } = createMockUtils(jest);
|
const { mockEsm, mockEsmWithActual } = createMockUtils(jest);
|
||||||
|
|
||||||
mockEsm('#src/utils/connectors/platform.js', () => ({
|
mockEsm('#src/utils/connectors/platform.js', () => ({
|
||||||
checkSocialConnectorTargetAndPlatformUniqueness: jest.fn(),
|
checkSocialConnectorTargetAndPlatformUniqueness: jest.fn(),
|
||||||
|
@ -56,6 +61,14 @@ const { loadConnectorFactories } = mockEsm('#src/utils/connectors/factories.js',
|
||||||
loadConnectorFactories: jest.fn(),
|
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(
|
const tenantContext = new MockTenant(
|
||||||
undefined,
|
undefined,
|
||||||
{ connectors: connectorQueries },
|
{ connectors: connectorQueries },
|
||||||
|
@ -178,6 +191,8 @@ describe('connector route', () => {
|
||||||
...mockLogtoConnector,
|
...mockLogtoConnector,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
validateConfig.mockReturnValueOnce(null);
|
||||||
|
buildRawConnector.mockResolvedValueOnce({ rawConnector: { configGuard: any() } });
|
||||||
await connectorRequest.post('/connectors').send({
|
await connectorRequest.post('/connectors').send({
|
||||||
connectorId: 'connectorId',
|
connectorId: 'connectorId',
|
||||||
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
|
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 () => {
|
it('throws when connector factory not found', async () => {
|
||||||
loadConnectorFactories.mockResolvedValueOnce([
|
loadConnectorFactories.mockResolvedValueOnce([
|
||||||
{
|
{
|
||||||
|
@ -225,6 +267,8 @@ describe('connector route', () => {
|
||||||
...mockLogtoConnector,
|
...mockLogtoConnector,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
validateConfig.mockReturnValueOnce(null);
|
||||||
|
buildRawConnector.mockResolvedValueOnce({ rawConnector: { configGuard: any() } });
|
||||||
await connectorRequest.post('/connectors').send({
|
await connectorRequest.post('/connectors').send({
|
||||||
connectorId: 'id0',
|
connectorId: 'id0',
|
||||||
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
|
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
|
||||||
|
@ -293,6 +337,8 @@ describe('connector route', () => {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
|
||||||
|
validateConfig.mockReturnValueOnce(null);
|
||||||
|
buildRawConnector.mockResolvedValueOnce({ rawConnector: { configGuard: any() } });
|
||||||
await connectorRequest.post('/connectors').send({
|
await connectorRequest.post('/connectors').send({
|
||||||
connectorId: 'id1',
|
connectorId: 'id1',
|
||||||
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
|
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
|
||||||
|
@ -335,6 +381,8 @@ describe('connector route', () => {
|
||||||
...mockLogtoConnector,
|
...mockLogtoConnector,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
validateConfig.mockReturnValueOnce(null);
|
||||||
|
buildRawConnector.mockResolvedValueOnce({ rawConnector: { configGuard: any() } });
|
||||||
const response = await connectorRequest.post('/connectors').send({
|
const response = await connectorRequest.post('/connectors').send({
|
||||||
connectorId: 'id0',
|
connectorId: 'id0',
|
||||||
metadata: { target: 'target' },
|
metadata: { target: 'target' },
|
||||||
|
|
|
@ -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 { emailRegEx, phoneRegEx, buildIdGenerator } from '@logto/core-kit';
|
||||||
import type { ConnectorResponse, ConnectorFactoryResponse } from '@logto/schemas';
|
import type { ConnectorResponse, ConnectorFactoryResponse } from '@logto/schemas';
|
||||||
import { arbitraryObjectGuard, Connectors, ConnectorType } from '@logto/schemas';
|
import { arbitraryObjectGuard, Connectors, ConnectorType } from '@logto/schemas';
|
||||||
import cleanDeep from 'clean-deep';
|
import cleanDeep from 'clean-deep';
|
||||||
import { object, string } from 'zod';
|
import { string, object } from 'zod';
|
||||||
|
|
||||||
import RequestError from '#src/errors/RequestError/index.js';
|
import RequestError from '#src/errors/RequestError/index.js';
|
||||||
import koaGuard from '#src/middleware/koa-guard.js';
|
import koaGuard from '#src/middleware/koa-guard.js';
|
||||||
import assertThat from '#src/utils/assert-that.js';
|
import assertThat from '#src/utils/assert-that.js';
|
||||||
import { loadConnectorFactories } from '#src/utils/connectors/factories.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 { checkSocialConnectorTargetAndPlatformUniqueness } from '#src/utils/connectors/platform.js';
|
||||||
import type { LogtoConnector } from '#src/utils/connectors/types.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();
|
const insertConnectorId = generateConnectorId();
|
||||||
await insertConnector({
|
await insertConnector({
|
||||||
id: insertConnectorId,
|
id: insertConnectorId,
|
||||||
|
|
|
@ -2,9 +2,12 @@ import { existsSync } from 'fs';
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
import path from 'path';
|
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 { ConnectorError, ConnectorErrorCodes, ConnectorType } from '@logto/connector-kit';
|
||||||
|
|
||||||
|
import { notImplemented } from './consts.js';
|
||||||
|
import type { ConnectorFactory } from './types.js';
|
||||||
|
|
||||||
export function validateConnectorModule(
|
export function validateConnectorModule(
|
||||||
connector: Partial<BaseConnector<ConnectorType>>
|
connector: Partial<BaseConnector<ConnectorType>>
|
||||||
): asserts connector is BaseConnector<ConnectorType> {
|
): asserts connector is BaseConnector<ConnectorType> {
|
||||||
|
@ -47,7 +50,10 @@ export const readUrl = async (
|
||||||
return readFile(path.join(baseUrl, url), 'utf8');
|
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 {
|
return {
|
||||||
...metadata,
|
...metadata,
|
||||||
logo: await readUrl(metadata.logo, packagePath, 'svg'),
|
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'),
|
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 };
|
||||||
|
};
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
"test:ci": "pnpm test:only"
|
"test:ci": "pnpm test:only"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@logto/connector-kit": "workspace:*",
|
||||||
"@logto/core-kit": "workspace:*",
|
"@logto/core-kit": "workspace:*",
|
||||||
"@silverhand/eslint-config": "1.3.0",
|
"@silverhand/eslint-config": "1.3.0",
|
||||||
"@silverhand/ts-config": "1.2.1",
|
"@silverhand/ts-config": "1.2.1",
|
||||||
|
|
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
|
@ -625,6 +625,7 @@ importers:
|
||||||
|
|
||||||
packages/shared:
|
packages/shared:
|
||||||
specifiers:
|
specifiers:
|
||||||
|
'@logto/connector-kit': workspace:*
|
||||||
'@logto/core-kit': workspace:*
|
'@logto/core-kit': workspace:*
|
||||||
'@logto/schemas': workspace:*
|
'@logto/schemas': workspace:*
|
||||||
'@silverhand/eslint-config': 1.3.0
|
'@silverhand/eslint-config': 1.3.0
|
||||||
|
@ -647,6 +648,7 @@ importers:
|
||||||
nanoid: 4.0.0
|
nanoid: 4.0.0
|
||||||
slonik: 30.1.2
|
slonik: 30.1.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@logto/connector-kit': link:../toolkit/connector-kit
|
||||||
'@logto/core-kit': link:../toolkit/core-kit
|
'@logto/core-kit': link:../toolkit/core-kit
|
||||||
'@silverhand/eslint-config': 1.3.0_k3lfx77tsvurbevhk73p7ygch4
|
'@silverhand/eslint-config': 1.3.0_k3lfx77tsvurbevhk73p7ygch4
|
||||||
'@silverhand/ts-config': 1.2.1_typescript@4.9.4
|
'@silverhand/ts-config': 1.2.1_typescript@4.9.4
|
||||||
|
|
Loading…
Add table
Reference in a new issue