Merge branch 'main' into terminus-json-model

This commit is contained in:
Deepak Prabhakara 2024-05-04 23:24:07 +01:00
commit 3c667c94f0
29 changed files with 3431 additions and 4821 deletions

View File

@ -1,4 +1,4 @@
ARG NODEJS_IMAGE=node:20.12.1-alpine3.19
ARG NODEJS_IMAGE=node:20.12.2-alpine3.19
FROM --platform=$BUILDPLATFORM $NODEJS_IMAGE AS base
# Install dependencies only when needed

View File

@ -136,7 +136,12 @@ const Branding = ({ hasValidLicense }: { hasValidLicense: boolean }) => {
<label className='label'>
<span className='label-text'>{t('bui-shared-primary-color')}</span>
</label>
<input type='color' id='primaryColor' onChange={onChange} value={branding.primaryColor || ''} />
<input
type='color'
id='primaryColor'
onChange={onChange}
value={branding.primaryColor || '#25c2a0'}
/>
<label className='label'>
<span className='label-text-alt'>{t('bui-shared-primary-color-desc')}</span>
</label>

View File

@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import jackson from '@lib/jackson';
import { defaultHandler } from '@lib/api';
import { parsePaginateApiParams } from '@lib/utils';
import { validateDevelopmentModeLimits } from '@lib/development-mode';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await defaultHandler(req, res, {
@ -15,6 +16,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
const { samlFederatedController } = await jackson();
await validateDevelopmentModeLimits(
req.body.product,
'samlFederation',
'Maximum number of federation apps reached'
);
const app = await samlFederatedController.app.create(req.body);
res.status(201).json({ data: app });

View File

@ -2,36 +2,28 @@ import { AppRequestParams } from '@boxyhq/saml-jackson';
import { NextApiRequest, NextApiResponse } from 'next';
import jackson from '@lib/jackson';
import { validateDevelopmentModeLimits } from '@lib/development-mode';
import { defaultHandler } from '@lib/api';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
switch (req.method) {
case 'POST':
await handlePOST(req, res);
break;
case 'GET':
await handleGET(req, res);
break;
case 'PATCH':
await handlePATCH(req, res);
break;
case 'DELETE':
await handleDELETE(req, res);
break;
default:
res.setHeader('Allow', 'POST, GET, PATCH, DELETE');
res.status(405).json({ error: { message: `Method ${req.method} Not Allowed` } });
}
} catch (error: any) {
const { message, statusCode = 500 } = error;
res.status(statusCode).json({ error: { message } });
}
await defaultHandler(req, res, {
POST: handlePOST,
GET: handleGET,
PATCH: handlePATCH,
DELETE: handleDELETE,
});
}
// Create a SAML federated app
const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
const { samlFederatedController } = await jackson();
await validateDevelopmentModeLimits(
req.body.product,
'samlFederation',
'Maximum number of federation apps reached'
);
const app = await samlFederatedController.app.create(req.body);
res.status(201).json({ data: app });

File diff suppressed because it is too large Load Diff

View File

@ -26,21 +26,21 @@
},
"devDependencies": {
"@rollup/plugin-typescript": "11.1.6",
"@types/node": "20.12.7",
"@types/react": "18.2.79",
"@typescript-eslint/eslint-plugin": "7.7.1",
"@typescript-eslint/parser": "7.7.1",
"@types/node": "20.12.8",
"@types/react": "18.3.1",
"@typescript-eslint/eslint-plugin": "7.8.0",
"@typescript-eslint/parser": "7.8.0",
"@vitejs/plugin-react": "4.2.1",
"eslint": "8.57.0",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-react-refresh": "0.4.6",
"prettier": "3.2.5",
"react-daisyui": "5.0.0",
"typescript": "5.4.5",
"vite": "5.2.10"
"vite": "5.2.11"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.16.4"
"@rollup/rollup-linux-x64-gnu": "4.17.2"
},
"peerDependencies": {
"@boxyhq/react-ui": ">=3.3.42",

View File

@ -22,4 +22,4 @@ patches:
images:
- name: boxyhq/jackson
newTag: 1.22.1
newTag: 1.23.5

View File

@ -22,4 +22,4 @@ patches:
images:
- name: boxyhq/jackson
newTag: 1.22.1
newTag: 1.23.5

44
lib/development-mode.ts Normal file
View File

@ -0,0 +1,44 @@
import jackson from './jackson';
import { IndexNames } from 'npm/src/controller/utils';
type Module = 'sso' | 'dsync' | 'samlFederation';
export const validateDevelopmentModeLimits = async (
productId: string,
type: Module,
message: string = 'Maximum number of connections reached'
) => {
if (productId) {
const { productController, connectionAPIController, directorySyncController, samlFederatedController } =
await jackson();
const getController = async (type: Module) => {
switch (type) {
case 'sso':
return connectionAPIController;
case 'dsync':
return directorySyncController.directories;
case 'samlFederation':
return samlFederatedController.app;
default:
return {
getCount: () => null,
};
}
};
const product = await productController.get(productId);
if (product?.development) {
const controller = await getController(type);
const count = await controller.getCount({
name: IndexNames.Product,
value: productId,
});
if (count) {
if (count >= 3) {
throw { message, statusCode: 400 };
}
}
}
}
};

1558
npm/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -39,19 +39,19 @@
"coverage-map": "map.js"
},
"dependencies": {
"@aws-sdk/client-dynamodb": "3.556.0",
"@aws-sdk/credential-providers": "3.556.0",
"@aws-sdk/util-dynamodb": "3.556.0",
"@aws-sdk/client-dynamodb": "3.569.0",
"@aws-sdk/credential-providers": "3.569.0",
"@aws-sdk/util-dynamodb": "3.569.0",
"@boxyhq/error-code-mnemonic": "0.1.1",
"@boxyhq/metrics": "0.2.6",
"@boxyhq/saml20": "1.5.0",
"@googleapis/admin": "16.0.0",
"@boxyhq/metrics": "0.2.7",
"@boxyhq/saml20": "1.5.1",
"@googleapis/admin": "18.0.0",
"axios": "1.6.8",
"encoding": "0.1.13",
"jose": "5.2.4",
"lodash": "4.17.21",
"mixpanel": "0.18.0",
"mongodb": "6.5.0",
"mongodb": "6.6.0",
"mssql": "10.0.2",
"mysql2": "3.9.7",
"node-forge": "1.3.1",
@ -64,8 +64,8 @@
},
"devDependencies": {
"@faker-js/faker": "8.4.1",
"@types/lodash": "4.17.0",
"@types/node": "20.12.7",
"@types/lodash": "4.17.1",
"@types/node": "20.12.8",
"@types/sinon": "17.0.3",
"@types/tap": "15.0.11",
"cross-env": "7.0.3",

View File

@ -65,4 +65,12 @@ export class AdminController implements IAdminController {
) {
return await this.ssoTracer.getTracesByProduct({ product, pageOffset, pageLimit, pageToken });
}
public async deleteTracesByProduct(product: string) {
return await this.ssoTracer.deleteTracesByProduct(product);
}
public async countByProduct(product: string) {
return await this.ssoTracer.countByProduct(product);
}
}

View File

@ -8,6 +8,7 @@ import type {
Records,
GetByProductParams,
AppRequestParams,
Index,
} from '../../typings';
import { fedAppID, clientIDFederatedPrefix } from '../../controller/utils';
import { JacksonError } from '../../controller/error';
@ -629,4 +630,8 @@ export class App {
x509cert: publicKey,
};
}
public async getCount(idx?: Index) {
return await this.store.getCount(idx);
}
}

View File

@ -206,6 +206,31 @@ class SSOTracer {
return traces;
}
public async deleteTracesByProduct(product: string) {
let pageToken;
do {
const res = await this.getTracesByProduct({
product,
pageOffset: 0,
pageLimit: 50,
});
if (!res.data || !res.data.length) {
break;
}
pageToken = res.pageToken;
// deleting traces in batches of 50
// deleting in the loop right away as we get the traces
await this.tracerStore.deleteMany((res.data || []).map((t) => t.traceId));
} while (pageToken);
}
public async countByProduct(product: string) {
return await this.tracerStore.getCount({
name: IndexNames.Product,
value: product,
});
}
}
export default SSOTracer;

View File

@ -212,6 +212,7 @@ export interface IAdminController {
getAllSSOTraces(pageOffset: number, pageLimit: number, pageToken?: string);
getSSOTraceById(traceId: string);
getTracesByProduct(product: string, pageOffset: number, pageLimit: number, pageToken?: string);
deleteTracesByProduct(product: string);
}
export interface IHealthCheckController {
@ -621,4 +622,5 @@ export interface ProductConfig {
faviconUrl: string | null;
companyName: string | null;
ory: OryConfig | null;
development?: boolean;
}

3872
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "jackson",
"version": "1.23.4",
"version": "1.23.6",
"private": true,
"description": "SAML 2.0 service",
"keywords": [
@ -66,29 +66,29 @@
"@boxyhq/react-ui": "3.3.43",
"@boxyhq/saml-jackson": "file:npm",
"@heroicons/react": "2.1.3",
"@retracedhq/logs-viewer": "2.7.2",
"@retracedhq/retraced": "0.7.9",
"@tailwindcss/typography": "0.5.12",
"@retracedhq/logs-viewer": "2.7.4",
"@retracedhq/retraced": "0.7.10",
"@tailwindcss/typography": "0.5.13",
"axios": "1.6.8",
"blockly": "10.4.3",
"chroma-js": "2.4.2",
"classnames": "2.5.1",
"cors": "2.8.5",
"cross-env": "7.0.3",
"daisyui": "4.10.2",
"formik": "2.4.5",
"i18next": "23.11.2",
"daisyui": "4.10.5",
"formik": "2.4.6",
"i18next": "23.11.3",
"medium-zoom": "1.1.0",
"micromatch": "4.0.5",
"next": "14.2.2",
"next": "14.2.3",
"next-auth": "4.24.7",
"next-i18next": "15.3.0",
"next-mdx-remote": "4.4.1",
"nodemailer": "6.9.13",
"raw-body": "2.5.2",
"react": "18.2.0",
"react": "18.3.1",
"react-daisyui": "5.0.0",
"react-dom": "18.2.0",
"react-dom": "18.3.1",
"react-i18next": "14.1.1",
"react-syntax-highlighter": "15.5.0",
"react-tagsinput": "3.20.3",
@ -100,27 +100,28 @@
"@playwright/test": "1.43.1",
"@types/cors": "2.8.17",
"@types/micromatch": "4.0.7",
"@types/node": "20.11.30",
"@types/react": "18.2.67",
"@typescript-eslint/eslint-plugin": "7.3.1",
"@typescript-eslint/parser": "7.3.1",
"@types/node": "20.12.7",
"@types/react": "18.3.1",
"@typescript-eslint/eslint-plugin": "7.8.0",
"@typescript-eslint/parser": "7.8.0",
"autoprefixer": "10.4.19",
"env-cmd": "10.1.0",
"eslint": "8.57.0",
"eslint-config-next": "14.2.2",
"eslint-config-next": "14.2.3",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-i18next": "6.0.3",
"jose": "5.2.3",
"jose": "5.2.4",
"postcss": "8.4.38",
"prettier": "3.2.5",
"prettier-plugin-tailwindcss": "0.5.14",
"release-it": "17.2.0",
"release-it": "17.2.1",
"swagger-jsdoc": "6.2.8",
"tailwindcss": "3.4.3",
"ts-node": "10.9.2",
"tsconfig-paths": "4.2.0",
"typescript": "5.4.2"
"typescript": "5.4.5"
},
"overrides": {},
"engines": {
"node": ">=18.14.2",
"npm": ">=10"

View File

@ -5,6 +5,7 @@ import { oidcMetadataParse, parsePaginateApiParams, strategyChecker } from '@lib
import { adminPortalSSODefaults } from '@lib/env';
import { defaultHandler } from '@lib/api';
import { ApiError } from '@lib/error';
import { validateDevelopmentModeLimits } from '@lib/development-mode';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await defaultHandler(req, res, {
@ -55,6 +56,8 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
const { isSAML, isOIDC } = strategyChecker(req);
await validateDevelopmentModeLimits(req.body.product, 'sso');
if (!isSAML && !isOIDC) {
throw new ApiError('Missing SSO connection params', 400);
}

View File

@ -4,6 +4,7 @@ import jackson from '@lib/jackson';
import { defaultHandler } from '@lib/api';
import { ApiError } from '@lib/error';
import { parsePaginateApiParams } from '@lib/utils';
import { validateDevelopmentModeLimits } from '@lib/development-mode';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
await defaultHandler(req, res, {
@ -17,6 +18,8 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
const { name, tenant, product, type, webhook_url, webhook_secret, google_domain } = req.body;
await validateDevelopmentModeLimits(product, 'dsync');
const { data, error } = await directorySyncController.directories.create({
name,
tenant,

View File

@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { SetupLink } from '@boxyhq/saml-jackson';
import jackson from '@lib/jackson';
import { validateDevelopmentModeLimits } from '@lib/development-mode';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { setupLinkController } = await jackson();
@ -33,6 +34,8 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse, setupLink:
const { type, google_domain } = req.body;
await validateDevelopmentModeLimits(setupLink.product, 'dsync');
const directory = {
type,
google_domain,

View File

@ -1,39 +1,23 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import jackson from '@lib/jackson';
import { oidcMetadataParse, strategyChecker } from '@lib/utils';
import type { SetupLink } from '@boxyhq/saml-jackson';
import { validateDevelopmentModeLimits } from '@lib/development-mode';
import { defaultHandler } from '@lib/api';
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { setupLinkController } = await jackson();
const { method } = req;
const { token } = req.query as { token: string };
try {
const setupLink = await setupLinkController.getByToken(token);
switch (method) {
case 'GET':
return await handleGET(req, res, setupLink);
case 'POST':
return await handlePOST(req, res, setupLink);
case 'PATCH':
return await handlePATCH(req, res);
case 'DELETE':
return await handleDELETE(req, res);
default:
res.setHeader('Allow', 'GET, POST, PATCH, DELETE');
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } });
}
} catch (error: any) {
const { message, statusCode = 500 } = error;
return res.status(statusCode).json({ error: { message } });
}
await defaultHandler(req, res, {
GET: handleGET,
POST: handlePOST,
PATCH: handlePATCH,
DELETE: handleDELETE,
});
};
const handleGET = async (req: NextApiRequest, res: NextApiResponse, setupLink: SetupLink) => {
const { connectionAPIController } = await jackson();
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
const { token } = req.query as { token: string };
const { connectionAPIController, setupLinkController } = await jackson();
const setupLink = await setupLinkController.getByToken(token);
const connections = await connectionAPIController.getConnections({
tenant: setupLink.tenant,
@ -68,14 +52,19 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse, setupLink: S
res.json(_connections);
};
const handlePOST = async (req: NextApiRequest, res: NextApiResponse, setupLink: SetupLink) => {
const { connectionAPIController } = await jackson();
const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
const { token } = req.query as { token: string };
const { connectionAPIController, setupLinkController } = await jackson();
const setupLink = await setupLinkController.getByToken(token);
const body = {
...req.body,
...setupLink,
};
await validateDevelopmentModeLimits(body.product, 'sso');
const { isSAML, isOIDC } = strategyChecker(req);
if (isSAML) {

View File

@ -55,7 +55,7 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
directoryId: searchParams.directoryId,
});
return res.json({ data: events });
return res.json(events);
};
// Delete webhook events for a directory

View File

@ -41,16 +41,16 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
product = directory.product;
}
const { data, error } = await directorySyncController.groups.setTenantAndProduct(tenant, product).getAll({
const groups = await directorySyncController.groups.setTenantAndProduct(tenant, product).getAll({
pageOffset,
pageLimit,
pageToken,
directoryId: searchParams.directoryId,
});
if (error) {
return res.status(error.code).json({ error });
if (groups.error) {
return res.status(groups.error.code).json({ error: groups.error });
}
return res.status(200).json({ data });
return res.status(200).json(groups);
};

View File

@ -1,18 +1,13 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import jackson from '@lib/jackson';
import { validateDevelopmentModeLimits } from '@lib/development-mode';
import { defaultHandler } from '@lib/api';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req;
switch (method) {
case 'GET':
return await handleGET(req, res);
case 'POST':
return await handlePOST(req, res);
default:
res.setHeader('Allow', 'GET, POST');
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } });
}
await defaultHandler(req, res, {
GET: handleGET,
POST: handlePOST,
});
}
// Get the configuration
@ -41,6 +36,8 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
const { directorySyncController } = await jackson();
await validateDevelopmentModeLimits(req.body.product, 'dsync');
const { data, error } = await directorySyncController.directories.create(req.body);
if (error) {

View File

@ -41,16 +41,16 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
product = directory.product;
}
const { data, error } = await directorySyncController.users.setTenantAndProduct(tenant, product).getAll({
const users = await directorySyncController.users.setTenantAndProduct(tenant, product).getAll({
pageOffset,
pageLimit,
pageToken,
directoryId: searchParams.directoryId,
});
if (error) {
return res.status(error.code).json({ error });
if (users.error) {
return res.status(users.error.code).json({ error: users.error });
}
return res.status(200).json({ data });
return res.status(200).json(users);
};

View File

@ -1,5 +1,4 @@
import jackson from '@lib/jackson';
import { parsePaginateApiParams } from '@lib/utils';
import { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@ -26,9 +25,7 @@ const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
product: string;
};
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
const count = await adminController.countByProduct(product);
const traces = await adminController.getTracesByProduct(product, pageOffset, pageLimit, pageToken);
res.json(traces);
res.json({ count });
};

View File

@ -0,0 +1,49 @@
import jackson from '@lib/jackson';
import { parsePaginateApiParams } from '@lib/utils';
import { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
switch (req.method) {
case 'GET':
await handleGET(req, res);
break;
case 'DELETE':
await handleDelete(req, res);
break;
default:
res.setHeader('Allow', 'GET,DELETE');
res.status(405).json({ error: { message: `Method ${req.method} Not Allowed` } });
}
} catch (error: any) {
const { message, statusCode = 500 } = error;
res.status(statusCode).json({ error: { message } });
}
}
// Get the sso traces filtered by the product
const handleGET = async (req: NextApiRequest, res: NextApiResponse) => {
const { adminController } = await jackson();
const { product } = req.query as {
product: string;
};
const { pageOffset, pageLimit, pageToken } = parsePaginateApiParams(req.query);
const traces = await adminController.getTracesByProduct(product, pageOffset, pageLimit, pageToken);
res.json(traces);
};
const handleDelete = async (req: NextApiRequest, res: NextApiResponse) => {
const { adminController } = await jackson();
const { product } = req.query as {
product: string;
};
await adminController.deleteTracesByProduct(product);
res.status(204).end();
};

View File

@ -2,29 +2,16 @@ import jackson from '@lib/jackson';
import { oidcMetadataParse, strategyChecker } from '@lib/utils';
import { NextApiRequest, NextApiResponse } from 'next';
import type { DelConnectionsQuery } from '@boxyhq/saml-jackson';
import { validateDevelopmentModeLimits } from '@lib/development-mode';
import { defaultHandler } from '@lib/api';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req;
try {
switch (method) {
case 'GET':
return await handleGET(req, res);
case 'POST':
return await handlePOST(req, res);
case 'PATCH':
return await handlePATCH(req, res);
case 'DELETE':
return await handleDELETE(req, res);
default:
res.setHeader('Allow', 'GET, POST, PATCH, DELETE');
res.status(405).json({ error: { message: `Method ${method} Not Allowed` } });
}
} catch (error: any) {
const { message, statusCode = 500 } = error;
return res.status(statusCode).json({ error: { message } });
}
await defaultHandler(req, res, {
GET: handleGET,
POST: handlePOST,
PATCH: handlePATCH,
DELETE: handleDELETE,
});
}
// Get all connections
@ -52,6 +39,8 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
throw { message: 'Missing SSO connection params', statusCode: 400 };
}
await validateDevelopmentModeLimits(req.body.product, 'sso');
// Create SAML connection
if (isSAML) {
const connection = await connectionAPIController.createSAMLConnection(req.body);

View File

@ -21,11 +21,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
const { connectionAPIController, directorySyncController } = await jackson();
const { connectionAPIController, directorySyncController, samlFederatedController } = await jackson();
// Products must be an array of strings
const products = req.body.products as string[];
const type = req.body.type ? (req.body.type as 'sso' | 'dsync') : undefined;
const type = req.body.type ? (req.body.type as 'sso' | 'dsync' | 'samlFederation') : undefined;
// Validate products
if (!products) {
@ -36,9 +36,9 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
throw { message: 'Products must not exceed 50', statusCode: 400 };
} else {
// Get counts for product
// If type is not provided, get counts for both sso and dsync
let sso_connections_count = 0;
let dsync_connections_count = 0;
let saml_federation_count = 0;
for (const product of products) {
if (product) {
@ -54,6 +54,14 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
});
dsync_connections_count += count || 0;
}
if (!type || type === 'samlFederation') {
const count = await samlFederatedController.app.getCount({
name: IndexNames.Product,
value: product,
});
saml_federation_count += count || 0;
}
}
}
@ -61,6 +69,7 @@ const handlePOST = async (req: NextApiRequest, res: NextApiResponse) => {
data: {
sso_connections: sso_connections_count,
dsync_connections: dsync_connections_count,
saml_federation: saml_federation_count,
},
});
}