feat: Stats route changes to return count of setup link & saml federations apps (#2627)

* feat: stats route updates to respond count of setup link & saml federations apps

* chore: Remove unused getCountByProductService method from SetupLinkController

* feat: Add validation for development mode connection limits

* chore: Update import path for validateDevelopmentModeLimits in directory-sync and sso-connection APIs

* refactor: update development mode limits validation in directory-sync and connections APIs

* feat: Update development mode limits validation in directory-sync and connections APIs
This commit is contained in:
Utkarsh Mehta 2024-04-29 19:10:01 +05:30 committed by GitHub
parent 1eb2147802
commit b98ccc68bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 130 additions and 88 deletions

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 });

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 };
}
}
}
}
};

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

@ -622,4 +622,5 @@ export interface ProductConfig {
faviconUrl: string | null;
companyName: string | null;
ory: OryConfig | null;
development?: boolean;
}

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

@ -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

@ -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,
},
});
}