diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index b098368c4..1882964bb 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -110,6 +110,7 @@ import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integ declare module "@fastify/request-context" { interface RequestContextData { reqId: string; + orgId?: string; identityAuthInfo?: { identityId: string; oidc?: { diff --git a/backend/src/server/routes/v1/app-connection-routers/oci-connection-router.ts b/backend/src/ee/routes/v1/app-connection-routers/oci-connection-router.ts similarity index 94% rename from backend/src/server/routes/v1/app-connection-routers/oci-connection-router.ts rename to backend/src/ee/routes/v1/app-connection-routers/oci-connection-router.ts index d78eee3d9..e87e5b69e 100644 --- a/backend/src/server/routes/v1/app-connection-routers/oci-connection-router.ts +++ b/backend/src/ee/routes/v1/app-connection-routers/oci-connection-router.ts @@ -1,16 +1,16 @@ import z from "zod"; -import { readLimit } from "@app/server/config/rateLimiter"; -import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { CreateOCIConnectionSchema, SanitizedOCIConnectionSchema, UpdateOCIConnectionSchema -} from "@app/services/app-connection/oci"; +} from "@app/ee/services/app-connections/oci"; +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { AuthMode } from "@app/services/auth/auth-type"; -import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; +import { registerAppConnectionEndpoints } from "../../../../server/routes/v1/app-connection-routers/app-connection-endpoints"; export const registerOCIConnectionRouter = async (server: FastifyZodProvider) => { registerAppConnectionEndpoints({ diff --git a/backend/src/server/routes/v1/secret-sync-routers/oci-vault-sync-router.ts b/backend/src/ee/routes/v1/secret-sync-routers/oci-vault-sync-router.ts similarity index 73% rename from backend/src/server/routes/v1/secret-sync-routers/oci-vault-sync-router.ts rename to backend/src/ee/routes/v1/secret-sync-routers/oci-vault-sync-router.ts index b46f27a50..2efe3e3f5 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/oci-vault-sync-router.ts +++ b/backend/src/ee/routes/v1/secret-sync-routers/oci-vault-sync-router.ts @@ -2,11 +2,10 @@ import { CreateOCIVaultSyncSchema, OCIVaultSyncSchema, UpdateOCIVaultSyncSchema -} from "@app/services/secret-sync/oci-vault"; +} from "@app/ee/services/secret-sync/oci-vault"; +import { registerSyncSecretsEndpoints } from "@app/server/routes/v1/secret-sync-routers/secret-sync-endpoints"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; -import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints"; - export const registerOCIVaultSyncRouter = async (server: FastifyZodProvider) => registerSyncSecretsEndpoints({ destination: SecretSync.OCIVault, diff --git a/backend/src/services/app-connection/oci/index.ts b/backend/src/ee/services/app-connections/oci/index.ts similarity index 100% rename from backend/src/services/app-connection/oci/index.ts rename to backend/src/ee/services/app-connections/oci/index.ts diff --git a/backend/src/services/app-connection/oci/oci-connection-enums.ts b/backend/src/ee/services/app-connections/oci/oci-connection-enums.ts similarity index 100% rename from backend/src/services/app-connection/oci/oci-connection-enums.ts rename to backend/src/ee/services/app-connections/oci/oci-connection-enums.ts diff --git a/backend/src/services/app-connection/oci/oci-connection-fns.ts b/backend/src/ee/services/app-connections/oci/oci-connection-fns.ts similarity index 100% rename from backend/src/services/app-connection/oci/oci-connection-fns.ts rename to backend/src/ee/services/app-connections/oci/oci-connection-fns.ts diff --git a/backend/src/services/app-connection/oci/oci-connection-schemas.ts b/backend/src/ee/services/app-connections/oci/oci-connection-schemas.ts similarity index 100% rename from backend/src/services/app-connection/oci/oci-connection-schemas.ts rename to backend/src/ee/services/app-connections/oci/oci-connection-schemas.ts diff --git a/backend/src/services/app-connection/oci/oci-connection-service.ts b/backend/src/ee/services/app-connections/oci/oci-connection-service.ts similarity index 68% rename from backend/src/services/app-connection/oci/oci-connection-service.ts rename to backend/src/ee/services/app-connections/oci/oci-connection-service.ts index 2d72135e5..c2e60399c 100644 --- a/backend/src/services/app-connection/oci/oci-connection-service.ts +++ b/backend/src/ee/services/app-connections/oci/oci-connection-service.ts @@ -1,7 +1,9 @@ +import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; -import { AppConnection } from "../app-connection-enums"; +import { AppConnection } from "../../../../services/app-connection/app-connection-enums"; +import { TLicenseServiceFactory } from "../../license/license-service"; import { listOCICompartments, listOCIVaultKeys, listOCIVaults } from "./oci-connection-fns"; import { TOCIConnection } from "./oci-connection-types"; @@ -22,8 +24,23 @@ type TListOCIVaultKeysDTO = { vaultOcid: string; }; -export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) => { +// Enterprise check +export const checkPlan = async (licenseService: Pick, orgId: string) => { + const plan = await licenseService.getPlan(orgId); + if (!plan.enterpriseAppConnections) + throw new BadRequestError({ + message: + "Failed to use app connection due to plan restriction. Upgrade plan to access enterprise app connections." + }); +}; + +export const ociConnectionService = ( + getAppConnection: TGetAppConnectionFunc, + licenseService: Pick +) => { const listCompartments = async (connectionId: string, actor: OrgServiceActor) => { + await checkPlan(licenseService, actor.orgId); + const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor); try { @@ -36,6 +53,8 @@ export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) => }; const listVaults = async ({ connectionId, compartmentOcid }: TListOCIVaultsDTO, actor: OrgServiceActor) => { + await checkPlan(licenseService, actor.orgId); + const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor); try { @@ -51,6 +70,8 @@ export const ociConnectionService = (getAppConnection: TGetAppConnectionFunc) => { connectionId, compartmentOcid, vaultOcid }: TListOCIVaultKeysDTO, actor: OrgServiceActor ) => { + await checkPlan(licenseService, actor.orgId); + const appConnection = await getAppConnection(AppConnection.OCI, connectionId, actor); try { diff --git a/backend/src/services/app-connection/oci/oci-connection-types.ts b/backend/src/ee/services/app-connections/oci/oci-connection-types.ts similarity index 87% rename from backend/src/services/app-connection/oci/oci-connection-types.ts rename to backend/src/ee/services/app-connections/oci/oci-connection-types.ts index 74ddfe0c8..e07554f29 100644 --- a/backend/src/services/app-connection/oci/oci-connection-types.ts +++ b/backend/src/ee/services/app-connections/oci/oci-connection-types.ts @@ -2,7 +2,7 @@ import z from "zod"; import { DiscriminativePick } from "@app/lib/types"; -import { AppConnection } from "../app-connection-enums"; +import { AppConnection } from "../../../../services/app-connection/app-connection-enums"; import { CreateOCIConnectionSchema, OCIConnectionSchema, diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 365ada987..e4874619b 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -1,3 +1,4 @@ +import { ProjectType } from "@app/db/schemas"; import { TCreateProjectTemplateDTO, TUpdateProjectTemplateDTO @@ -315,7 +316,6 @@ export enum EventType { CREATE_PROJECT_TEMPLATE = "create-project-template", UPDATE_PROJECT_TEMPLATE = "update-project-template", DELETE_PROJECT_TEMPLATE = "delete-project-template", - APPLY_PROJECT_TEMPLATE = "apply-project-template", GET_APP_CONNECTIONS = "get-app-connections", GET_AVAILABLE_APP_CONNECTIONS_DETAILS = "get-available-app-connections-details", GET_APP_CONNECTION = "get-app-connection", @@ -375,7 +375,13 @@ export enum EventType { MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list", PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start", - PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end" + PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end", + + UPDATE_ORG = "update-org", + + CREATE_PROJECT = "create-project", + UPDATE_PROJECT = "update-project", + DELETE_PROJECT = "delete-project" } export const filterableSecretEvents: EventType[] = [ @@ -2451,14 +2457,6 @@ interface DeleteProjectTemplateEvent { }; } -interface ApplyProjectTemplateEvent { - type: EventType.APPLY_PROJECT_TEMPLATE; - metadata: { - template: string; - projectId: string; - }; -} - interface GetAppConnectionsEvent { type: EventType.GET_APP_CONNECTIONS; metadata: { @@ -2913,6 +2911,59 @@ interface MicrosoftTeamsWorkflowIntegrationUpdateEvent { }; } +interface OrgUpdateEvent { + type: EventType.UPDATE_ORG; + metadata: { + name?: string; + slug?: string; + authEnforced?: boolean; + scimEnabled?: boolean; + defaultMembershipRoleSlug?: string; + enforceMfa?: boolean; + selectedMfaMethod?: string; + allowSecretSharingOutsideOrganization?: boolean; + bypassOrgAuthEnabled?: boolean; + userTokenExpiration?: string; + secretsProductEnabled?: boolean; + pkiProductEnabled?: boolean; + kmsProductEnabled?: boolean; + sshProductEnabled?: boolean; + scannerProductEnabled?: boolean; + shareSecretsProductEnabled?: boolean; + }; +} + +interface ProjectCreateEvent { + type: EventType.CREATE_PROJECT; + metadata: { + name: string; + slug?: string; + type: ProjectType; + }; +} + +interface ProjectUpdateEvent { + type: EventType.UPDATE_PROJECT; + metadata: { + name?: string; + description?: string; + autoCapitalization?: boolean; + hasDeleteProtection?: boolean; + slug?: string; + secretSharing?: boolean; + pitVersionLimit?: number; + auditLogsRetentionDays?: number; + }; +} + +interface ProjectDeleteEvent { + type: EventType.DELETE_PROJECT; + metadata: { + id: string; + name: string; + }; +} + export type Event = | GetSecretsEvent | GetSecretEvent @@ -3117,7 +3168,6 @@ export type Event = | CreateProjectTemplateEvent | UpdateProjectTemplateEvent | DeleteProjectTemplateEvent - | ApplyProjectTemplateEvent | GetAppConnectionsEvent | GetAvailableAppConnectionsDetailsEvent | GetAppConnectionEvent @@ -3179,4 +3229,8 @@ export type Event = | MicrosoftTeamsWorkflowIntegrationGetTeamsEvent | MicrosoftTeamsWorkflowIntegrationGetEvent | MicrosoftTeamsWorkflowIntegrationListEvent - | MicrosoftTeamsWorkflowIntegrationUpdateEvent; + | MicrosoftTeamsWorkflowIntegrationUpdateEvent + | OrgUpdateEvent + | ProjectCreateEvent + | ProjectUpdateEvent + | ProjectDeleteEvent; diff --git a/backend/src/ee/services/license/__mocks__/license-fns.ts b/backend/src/ee/services/license/__mocks__/license-fns.ts index 6a8f807ad..5259d4616 100644 --- a/backend/src/ee/services/license/__mocks__/license-fns.ts +++ b/backend/src/ee/services/license/__mocks__/license-fns.ts @@ -29,7 +29,9 @@ export const getDefaultOnPremFeatures = () => { secretApproval: true, secretRotation: true, caCrl: false, - sshHostGroups: false + sshHostGroups: false, + enterpriseSecretSyncs: false, + enterpriseAppConnections: false }; }; diff --git a/backend/src/ee/services/license/license-dal.ts b/backend/src/ee/services/license/license-dal.ts index cab428e86..88a2dadf6 100644 --- a/backend/src/ee/services/license/license-dal.ts +++ b/backend/src/ee/services/license/license-dal.ts @@ -19,7 +19,7 @@ export const licenseDALFactory = (db: TDbClient) => { .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .where(`${TableName.Users}.isGhost`, false) .count(); - return Number(doc?.[0].count); + return Number(doc?.[0]?.count ?? 0); } catch (error) { throw new DatabaseError({ error, name: "Count of Org Members" }); } diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index 8ef91c6f8..d8ca362bd 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -55,7 +55,9 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ projectTemplates: false, kmip: false, gateway: false, - sshHostGroups: false + sshHostGroups: false, + enterpriseSecretSyncs: false, + enterpriseAppConnections: false }); export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index f5b1f96ec..e9a2ada8b 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -92,6 +92,10 @@ export const licenseServiceFactory = ({ const { data: { currentPlan } } = await licenseServerOnPremApi.request.get<{ currentPlan: TFeatureSet }>("/api/license/v1/plan"); + + const workspacesUsed = await projectDAL.countOfOrgProjects(null); + currentPlan.workspacesUsed = workspacesUsed; + onPremFeatures = currentPlan; logger.info("Successfully synchronized license key features"); } catch (error) { @@ -185,6 +189,9 @@ export const licenseServiceFactory = ({ } = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>( `/api/license-server/v1/customers/${org.customerId}/cloud-plan` ); + const workspacesUsed = await projectDAL.countOfOrgProjects(orgId); + currentPlan.workspacesUsed = workspacesUsed; + await keyStore.setItemWithExpiry( FEATURE_CACHE_KEY(org.id), LICENSE_SERVER_CLOUD_PLAN_TTL, diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 358849fb2..f509c7127 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -27,7 +27,7 @@ export type TFeatureSet = { slug: null; tier: -1; workspaceLimit: null; - workspacesUsed: 0; + workspacesUsed: number; dynamicSecret: false; memberLimit: null; membersUsed: number; @@ -72,6 +72,8 @@ export type TFeatureSet = { kmip: false; gateway: false; sshHostGroups: false; + enterpriseSecretSyncs: false; + enterpriseAppConnections: false; }; export type TOrgPlansTableDTO = { diff --git a/backend/src/services/secret-sync/oci-vault/index.ts b/backend/src/ee/services/secret-sync/oci-vault/index.ts similarity index 100% rename from backend/src/services/secret-sync/oci-vault/index.ts rename to backend/src/ee/services/secret-sync/oci-vault/index.ts diff --git a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-constants.ts b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-constants.ts similarity index 89% rename from backend/src/services/secret-sync/oci-vault/oci-vault-sync-constants.ts rename to backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-constants.ts index 9e2aad056..b864e354b 100644 --- a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-constants.ts +++ b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-constants.ts @@ -6,5 +6,6 @@ export const OCI_VAULT_SYNC_LIST_OPTION: TSecretSyncListItem = { name: "OCI Vault", destination: SecretSync.OCIVault, connection: AppConnection.OCI, - canImportSecrets: true + canImportSecrets: true, + enterprise: true }; diff --git a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-fns.ts similarity index 98% rename from backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts rename to backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-fns.ts index e270f2e02..5b05b2301 100644 --- a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts +++ b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-fns.ts @@ -1,7 +1,6 @@ import { secrets, vault } from "oci-sdk"; -import { delay } from "@app/lib/delay"; -import { getOCIProvider } from "@app/services/app-connection/oci"; +import { getOCIProvider } from "@app/ee/services/app-connections/oci"; import { TCreateOCIVaultVariable, TDeleteOCIVaultVariable, @@ -9,7 +8,8 @@ import { TOCIVaultSyncWithCredentials, TUnmarkOCIVaultVariableFromDeletion, TUpdateOCIVaultVariable -} from "@app/services/secret-sync/oci-vault/oci-vault-sync-types"; +} from "@app/ee/services/secret-sync/oci-vault/oci-vault-sync-types"; +import { delay } from "@app/lib/delay"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; diff --git a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-schemas.ts b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-schemas.ts similarity index 97% rename from backend/src/services/secret-sync/oci-vault/oci-vault-sync-schemas.ts rename to backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-schemas.ts index 84a58bc8a..a0bd29382 100644 --- a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-schemas.ts +++ b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-schemas.ts @@ -66,5 +66,6 @@ export const OCIVaultSyncListItemSchema = z.object({ name: z.literal("OCI Vault"), connection: z.literal(AppConnection.OCI), destination: z.literal(SecretSync.OCIVault), - canImportSecrets: z.literal(true) + canImportSecrets: z.literal(true), + enterprise: z.boolean() }); diff --git a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-types.ts b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-types.ts similarity index 94% rename from backend/src/services/secret-sync/oci-vault/oci-vault-sync-types.ts rename to backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-types.ts index c040cd0c0..8804b1322 100644 --- a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-types.ts +++ b/backend/src/ee/services/secret-sync/oci-vault/oci-vault-sync-types.ts @@ -1,7 +1,7 @@ import { SimpleAuthenticationDetailsProvider } from "oci-sdk"; import { z } from "zod"; -import { TOCIConnection } from "@app/services/app-connection/oci"; +import { TOCIConnection } from "@app/ee/services/app-connections/oci"; import { CreateOCIVaultSyncSchema, OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "./oci-vault-sync-schemas"; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index e38dbcfb5..ae5af701e 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -69,6 +69,9 @@ const envSchema = z SMTP_PASSWORD: zpStr(z.string().optional()), SMTP_FROM_ADDRESS: zpStr(z.string().optional()), SMTP_FROM_NAME: zpStr(z.string().optional().default("Infisical")), + SMTP_CUSTOM_CA_CERT: zpStr( + z.string().optional().describe("Base64 encoded custom CA certificate PEM(s) for the SMTP server") + ), COOKIE_SECRET_SIGN_KEY: z .string() .min(32) @@ -298,6 +301,17 @@ export const initEnvConfig = (logger?: CustomLogger) => { }; export const formatSmtpConfig = () => { + const tlsOptions: { + rejectUnauthorized: boolean; + ca?: string | string[]; + } = { + rejectUnauthorized: envCfg.SMTP_TLS_REJECT_UNAUTHORIZED + }; + + if (envCfg.SMTP_CUSTOM_CA_CERT) { + tlsOptions.ca = Buffer.from(envCfg.SMTP_CUSTOM_CA_CERT, "base64").toString("utf-8"); + } + return { host: envCfg.SMTP_HOST, port: envCfg.SMTP_PORT, @@ -309,8 +323,6 @@ export const formatSmtpConfig = () => { from: `"${envCfg.SMTP_FROM_NAME}" <${envCfg.SMTP_FROM_ADDRESS}>`, ignoreTLS: envCfg.SMTP_IGNORE_TLS, requireTLS: envCfg.SMTP_REQUIRE_TLS, - tls: { - rejectUnauthorized: envCfg.SMTP_TLS_REJECT_UNAUTHORIZED - } + tls: tlsOptions }; }; diff --git a/backend/src/lib/logger/logger.ts b/backend/src/lib/logger/logger.ts index afde8ef97..219b4a9a7 100644 --- a/backend/src/lib/logger/logger.ts +++ b/backend/src/lib/logger/logger.ts @@ -95,11 +95,20 @@ const extractReqId = () => { try { return requestContext.get("reqId") || UNKNOWN_REQUEST_ID; } catch (err) { + // eslint-disable-next-line no-console console.log("failed to get request context", err); return UNKNOWN_REQUEST_ID; } }; +const extractOrgId = () => { + try { + return requestContext.get("orgId"); + } catch { + return ""; + } +}; + export const initLogger = () => { const cfg = loggerConfig.parse(process.env); const targets: pino.TransportMultiOptions["targets"][number][] = [ @@ -135,22 +144,22 @@ export const initLogger = () => { const wrapLogger = (originalLogger: Logger): CustomLogger => { // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any originalLogger.info = (obj: unknown, msg?: string, ...args: any[]) => { - return originalLogger.child({ reqId: extractReqId() }).info(obj, msg, ...args); + return originalLogger.child({ reqId: extractReqId(), orgId: extractOrgId() }).info(obj, msg, ...args); }; // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any originalLogger.error = (obj: unknown, msg?: string, ...args: any[]) => { - return originalLogger.child({ reqId: extractReqId() }).error(obj, msg, ...args); + return originalLogger.child({ reqId: extractReqId(), orgId: extractOrgId() }).error(obj, msg, ...args); }; // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any originalLogger.warn = (obj: unknown, msg?: string, ...args: any[]) => { - return originalLogger.child({ reqId: extractReqId() }).warn(obj, msg, ...args); + return originalLogger.child({ reqId: extractReqId(), orgId: extractOrgId() }).warn(obj, msg, ...args); }; // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any originalLogger.debug = (obj: unknown, msg?: string, ...args: any[]) => { - return originalLogger.child({ reqId: extractReqId() }).debug(obj, msg, ...args); + return originalLogger.child({ reqId: extractReqId(), orgId: extractOrgId() }).debug(obj, msg, ...args); }; return originalLogger; diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 57a1313c6..afea5c9f9 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -123,6 +123,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { switch (authMode) { case AuthMode.JWT: { const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token); + requestContext.set("orgId", orgId); req.auth = { authMode: AuthMode.JWT, user, @@ -138,6 +139,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { case AuthMode.IDENTITY_ACCESS_TOKEN: { const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp); const serverCfg = await getServerCfg(); + requestContext.set("orgId", identity.orgId); req.auth = { authMode: AuthMode.IDENTITY_ACCESS_TOKEN, actor, @@ -157,6 +159,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { } case AuthMode.SERVICE_TOKEN: { const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token); + requestContext.set("orgId", serviceToken.orgId); req.auth = { orgId: serviceToken.orgId, authMode: AuthMode.SERVICE_TOKEN as const, @@ -181,6 +184,7 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { } case AuthMode.SCIM_TOKEN: { const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token); + requestContext.set("orgId", orgId); req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId, authMethod: null }; break; } diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index cb22c8c3c..5e9890ff2 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1014,7 +1014,8 @@ export const registerRoutes = async ( secretVersionV2BridgeDAL, secretVersionTagV2BridgeDAL, resourceMetadataDAL, - appConnectionDAL + appConnectionDAL, + licenseService }); const secretQueueService = secretQueueFactory({ @@ -1631,7 +1632,8 @@ export const registerRoutes = async ( const appConnectionService = appConnectionServiceFactory({ appConnectionDAL, permissionService, - kmsService + kmsService, + licenseService }); const secretSyncService = secretSyncServiceFactory({ @@ -1642,7 +1644,8 @@ export const registerRoutes = async ( folderDAL, secretSyncQueue, projectBotService, - keyStore + keyStore, + licenseService }); const kmipService = kmipServiceFactory({ diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 1d8430601..0fea749c0 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/ee/services/app-connections/oci"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags } from "@app/lib/api-docs"; import { readLimit } from "@app/server/config/rateLimiter"; @@ -42,7 +43,6 @@ import { } from "@app/services/app-connection/humanitec"; import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap"; import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql"; -import { OCIConnectionListItemSchema, SanitizedOCIConnectionSchema } from "@app/services/app-connection/oci"; import { PostgresConnectionListItemSchema, SanitizedPostgresConnectionSchema diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index b0ceac986..1c46b4ea9 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -1,3 +1,4 @@ +import { registerOCIConnectionRouter } from "@app/ee/routes/v1/app-connection-routers/oci-connection-router"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { registerOnePassConnectionRouter } from "./1password-connection-router"; @@ -14,7 +15,6 @@ import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; -import { registerOCIConnectionRouter } from "./oci-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index c489d685d..b3fceb201 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -312,8 +312,17 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { data: req.body }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + event: { + type: EventType.UPDATE_ORG, + metadata: req.body + } + }); + return { - message: "Successfully changed organization name", + message: "Successfully updated organization", organization }; } diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 2e983cb83..651faede4 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -263,6 +263,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorOrgId: req.permission.orgId }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.params.workspaceId, + event: { + type: EventType.DELETE_PROJECT, + metadata: workspace + } + }); + return { workspace }; } }); @@ -297,6 +308,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { projectId: req.params.workspaceId, name: req.body.name }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.params.workspaceId, + event: { + type: EventType.UPDATE_PROJECT, + metadata: req.body + } + }); + return { message: "Successfully changed workspace name", workspace @@ -375,6 +397,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorOrgId: req.permission.orgId }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.params.workspaceId, + event: { + type: EventType.UPDATE_PROJECT, + metadata: req.body + } + }); + return { workspace }; @@ -411,6 +444,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { projectId: req.params.workspaceId, autoCapitalization: req.body.autoCapitalization }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.params.workspaceId, + event: { + type: EventType.UPDATE_PROJECT, + metadata: req.body + } + }); + return { message: "Successfully changed workspace settings", workspace @@ -448,6 +492,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { projectId: req.params.workspaceId, hasDeleteProtection: req.body.hasDeleteProtection }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.params.workspaceId, + event: { + type: EventType.UPDATE_PROJECT, + metadata: req.body + } + }); + return { message: "Successfully changed workspace settings", workspace @@ -486,6 +541,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { workspaceSlug: req.params.workspaceSlug }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: workspace.id, + event: { + type: EventType.UPDATE_PROJECT, + metadata: req.body + } + }); + return { message: "Successfully changed workspace version limit", workspace @@ -524,6 +589,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { auditLogsRetentionDays: req.body.auditLogsRetentionDays }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: workspace.id, + event: { + type: EventType.UPDATE_PROJECT, + metadata: req.body + } + }); + return { message: "Successfully updated project's audit logs retention period", workspace diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index 5004f4f4a..fbc636ffc 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -1,3 +1,4 @@ +import { registerOCIVaultSyncRouter } from "@app/ee/routes/v1/secret-sync-routers/oci-vault-sync-router"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { registerOnePassSyncRouter } from "./1password-sync-router"; @@ -11,7 +12,6 @@ import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; -import { registerOCIVaultSyncRouter } from "./oci-vault-sync-router"; import { registerTeamCitySyncRouter } from "./teamcity-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerVercelSyncRouter } from "./vercel-sync-router"; diff --git a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts index d93b2be2d..64017cebf 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "@app/ee/services/secret-sync/oci-vault"; import { ApiDocsTags, SecretSyncs } from "@app/lib/api-docs"; import { readLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -25,7 +26,6 @@ import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/ import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github"; import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault"; import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; -import { OCIVaultSyncListItemSchema, OCIVaultSyncSchema } from "@app/services/secret-sync/oci-vault"; import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity"; import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud"; import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel"; diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 3d92bfb1a..00cd69329 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -206,19 +206,18 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } }); - if (req.body.template) { - await server.services.auditLog.createAuditLog({ - ...req.auditLogInfo, - orgId: req.permission.orgId, - event: { - type: EventType.APPLY_PROJECT_TEMPLATE, - metadata: { - template: req.body.template, - projectId: project.id - } + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: project.id, + event: { + type: EventType.CREATE_PROJECT, + metadata: { + ...req.body, + name: req.body.projectName } - }); - } + } + }); return { project }; } @@ -262,6 +261,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actor: req.permission.type }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: project.id, + event: { + type: EventType.DELETE_PROJECT, + metadata: project + } + }); + return project; } }); @@ -341,6 +350,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { actorOrgId: req.permission.orgId }); + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: project.id, + event: { + type: EventType.UPDATE_PROJECT, + metadata: req.body + } + }); + return project; } }); diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 8d7920d12..25c6394fa 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -67,3 +67,8 @@ export enum AWSRegion { // South America SA_EAST_1 = "sa-east-1" // Sao Paulo } + +export enum AppConnectionPlanType { + Enterprise = "enterprise", + Regular = "regular" +} diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 061f4a2f7..614c9accc 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -1,7 +1,13 @@ import { TAppConnections } from "@app/db/schemas/app-connections"; +import { + getOCIConnectionListItem, + OCIConnectionMethod, + validateOCIConnectionCredentials +} from "@app/ee/services/app-connections/oci"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { generateHash } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; -import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps"; +import { APP_CONNECTION_NAME_MAP, APP_CONNECTION_PLAN_MAP } from "@app/services/app-connection/app-connection-maps"; import { transferSqlConnectionCredentialsToPlatform, validateSqlConnectionCredentials @@ -13,7 +19,7 @@ import { OnePassConnectionMethod, validateOnePassConnectionCredentials } from "./1password"; -import { AppConnection } from "./app-connection-enums"; +// import { AppConnection, AppConnectionPlanType } from "./app-connection-enums"; import { TAppConnectionServiceFactoryDep } from "./app-connection-service"; import { TAppConnection, @@ -58,7 +64,6 @@ import { } from "./humanitec"; import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; -import { getOCIConnectionListItem, OCIConnectionMethod, validateOCIConnectionCredentials } from "./oci"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { getTeamCityConnectionListItem, @@ -266,3 +271,18 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.OCI]: platformManagedCredentialsNotSupported, [AppConnection.OnePass]: platformManagedCredentialsNotSupported }; + +export const enterpriseAppCheck = async ( + licenseService: Pick, + appConnection: AppConnection, + orgId: string, + errorMessage: string +) => { + if (APP_CONNECTION_PLAN_MAP[appConnection] === AppConnectionPlanType.Enterprise) { + const plan = await licenseService.getPlan(orgId); + if (!plan.enterpriseAppConnections) + throw new BadRequestError({ + message: errorMessage + }); + } +}; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index e48523480..5615623aa 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -1,4 +1,4 @@ -import { AppConnection } from "./app-connection-enums"; +import { AppConnection, AppConnectionPlanType } from "./app-connection-enums"; export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.AWS]: "AWS", @@ -22,3 +22,25 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.OCI]: "OCI", [AppConnection.OnePass]: "1Password" }; + +export const APP_CONNECTION_PLAN_MAP: Record = { + [AppConnection.AWS]: AppConnectionPlanType.Regular, + [AppConnection.GitHub]: AppConnectionPlanType.Regular, + [AppConnection.GCP]: AppConnectionPlanType.Regular, + [AppConnection.AzureKeyVault]: AppConnectionPlanType.Regular, + [AppConnection.AzureAppConfiguration]: AppConnectionPlanType.Regular, + [AppConnection.AzureClientSecrets]: AppConnectionPlanType.Regular, + [AppConnection.Databricks]: AppConnectionPlanType.Regular, + [AppConnection.Humanitec]: AppConnectionPlanType.Regular, + [AppConnection.TerraformCloud]: AppConnectionPlanType.Regular, + [AppConnection.Vercel]: AppConnectionPlanType.Regular, + [AppConnection.Postgres]: AppConnectionPlanType.Regular, + [AppConnection.MsSql]: AppConnectionPlanType.Regular, + [AppConnection.Camunda]: AppConnectionPlanType.Regular, + [AppConnection.Windmill]: AppConnectionPlanType.Regular, + [AppConnection.Auth0]: AppConnectionPlanType.Regular, + [AppConnection.HCVault]: AppConnectionPlanType.Regular, + [AppConnection.LDAP]: AppConnectionPlanType.Regular, + [AppConnection.TeamCity]: AppConnectionPlanType.Regular, + [AppConnection.OCI]: AppConnectionPlanType.Enterprise +}; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index e9e9c28c0..7d7508fc2 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -1,5 +1,8 @@ import { ForbiddenError, subject } from "@casl/ability"; +import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci"; +import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { generateHash } from "@app/lib/crypto/encryption"; @@ -9,6 +12,7 @@ import { DiscriminativePick, OrgServiceActor } from "@app/lib/types"; import { decryptAppConnection, encryptAppConnectionCredentials, + enterpriseAppCheck, getAppConnectionMethodName, listAppConnectionOptions, TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM, @@ -51,8 +55,6 @@ import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec"; import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; import { ValidateLdapConnectionCredentialsSchema } from "./ldap"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; -import { ValidateOCIConnectionCredentialsSchema } from "./oci"; -import { ociConnectionService } from "./oci/oci-connection-service"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity"; import { teamcityConnectionService } from "./teamcity/teamcity-connection-service"; @@ -67,6 +69,7 @@ export type TAppConnectionServiceFactoryDep = { appConnectionDAL: TAppConnectionDALFactory; permissionService: Pick; kmsService: Pick; + licenseService: Pick; }; export type TAppConnectionServiceFactory = ReturnType; @@ -97,7 +100,8 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record { const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => { const { permission } = await permissionService.getOrgPermission( @@ -194,6 +198,13 @@ export const appConnectionServiceFactory = ({ OrgPermissionSubjects.AppConnections ); + await enterpriseAppCheck( + licenseService, + app, + actor.orgId, + "Failed to create app connection due to plan restriction. Upgrade plan to access enterprise app connections." + ); + const validatedCredentials = await validateAppConnectionCredentials({ app, credentials, @@ -256,6 +267,13 @@ export const appConnectionServiceFactory = ({ if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); + await enterpriseAppCheck( + licenseService, + appConnection.app as AppConnection, + actor.orgId, + "Failed to update app connection due to plan restriction. Upgrade plan to access enterprise app connections." + ); + const { permission } = await permissionService.getOrgPermission( actor.type, actor.id, @@ -402,6 +420,13 @@ export const appConnectionServiceFactory = ({ if (!appConnection) throw new NotFoundError({ message: `Could not find App Connection with ID ${connectionId}` }); + await enterpriseAppCheck( + licenseService, + app, + actor.orgId, + "Failed to connect app due to plan restriction. Upgrade plan to access enterprise app connections." + ); + const { permission: orgPermission } = await permissionService.getOrgPermission( actor.type, actor.id, @@ -471,7 +496,7 @@ export const appConnectionServiceFactory = ({ hcvault: hcVaultConnectionService(connectAppConnectionById), windmill: windmillConnectionService(connectAppConnectionById), teamcity: teamcityConnectionService(connectAppConnectionById), - oci: ociConnectionService(connectAppConnectionById), + oci: ociConnectionService(connectAppConnectionById, licenseService), onepass: onePassConnectionService(connectAppConnectionById) }; }; diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts index fbce1a1e8..459096ae5 100644 --- a/backend/src/services/app-connection/app-connection-types.ts +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -1,3 +1,9 @@ +import { + TOCIConnection, + TOCIConnectionConfig, + TOCIConnectionInput, + TValidateOCIConnectionCredentialsSchema +} from "@app/ee/services/app-connections/oci"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; @@ -82,12 +88,6 @@ import { TValidateLdapConnectionCredentialsSchema } from "./ldap"; import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql"; -import { - TOCIConnection, - TOCIConnectionConfig, - TOCIConnectionInput, - TValidateOCIConnectionCredentialsSchema -} from "./oci"; import { TPostgresConnection, TPostgresConnectionInput, diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 43f1d57e4..bdcee1e61 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -425,6 +425,21 @@ export const projectDALFactory = (db: TDbClient) => { return { docs, totalCount: Number(docs?.[0]?.count ?? 0) }; }; + const countOfOrgProjects = async (orgId: string | null, tx?: Knex) => { + try { + const doc = await (tx || db.replicaNode())(TableName.Project) + .andWhere((bd) => { + if (orgId) { + void bd.where({ orgId }); + } + }) + .count(); + return Number(doc?.[0]?.count ?? 0); + } catch (error) { + throw new DatabaseError({ error, name: "Count of Org Projects" }); + } + }; + return { ...projectOrm, findUserProjects, @@ -437,6 +452,7 @@ export const projectDALFactory = (db: TDbClient) => { findProjectWithOrg, checkProjectUpgradeStatus, getProjectFromSplitId, - searchProjects + searchProjects, + countOfOrgProjects }; }; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index dc938b8ae..24f7d05f8 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -27,3 +27,8 @@ export enum SecretSyncImportBehavior { PrioritizeSource = "prioritize-source", PrioritizeDestination = "prioritize-destination" } + +export enum SecretSyncPlanType { + Enterprise = "enterprise", + Regular = "regular" +} diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index cf0773631..dbf3a3699 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -1,6 +1,9 @@ import { AxiosError } from "axios"; import RE2 from "re2"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "@app/ee/services/secret-sync/oci-vault"; +import { BadRequestError } from "@app/lib/errors"; import { AWS_PARAMETER_STORE_SYNC_LIST_OPTION, AwsParameterStoreSyncFns @@ -11,7 +14,7 @@ import { } from "@app/services/secret-sync/aws-secrets-manager"; import { DATABRICKS_SYNC_LIST_OPTION, databricksSyncFactory } from "@app/services/secret-sync/databricks"; import { GITHUB_SYNC_LIST_OPTION, GithubSyncFns } from "@app/services/secret-sync/github"; -import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { SecretSync, SecretSyncPlanType } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { TSecretMap, @@ -30,7 +33,7 @@ import { GcpSyncFns } from "./gcp/gcp-sync-fns"; import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; -import { OCI_VAULT_SYNC_LIST_OPTION, OCIVaultSyncFns } from "./oci-vault"; +import { SECRET_SYNC_PLAN_MAP } from "./secret-sync-maps"; import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity"; import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud"; import { VERCEL_SYNC_LIST_OPTION, VercelSyncFns } from "./vercel"; @@ -336,3 +339,18 @@ export const parseSyncErrorMessage = (err: unknown): string => { ? errorMessage : `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`; }; + +export const enterpriseSyncCheck = async ( + licenseService: Pick, + secretSync: SecretSync, + orgId: string, + errorMessage: string +) => { + if (SECRET_SYNC_PLAN_MAP[secretSync] === SecretSyncPlanType.Enterprise) { + const plan = await licenseService.getPlan(orgId); + if (!plan.enterpriseSecretSyncs) + throw new BadRequestError({ + message: errorMessage + }); + } +}; diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index 7bea7236b..7db830064 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -1,5 +1,5 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums"; -import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { SecretSync, SecretSyncPlanType } from "@app/services/secret-sync/secret-sync-enums"; export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.AWSParameterStore]: "AWS Parameter Store", @@ -38,3 +38,21 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.OCIVault]: AppConnection.OCI, [SecretSync.OnePass]: AppConnection.OnePass }; + +export const SECRET_SYNC_PLAN_MAP: Record = { + [SecretSync.AWSParameterStore]: SecretSyncPlanType.Regular, + [SecretSync.AWSSecretsManager]: SecretSyncPlanType.Regular, + [SecretSync.GitHub]: SecretSyncPlanType.Regular, + [SecretSync.GCPSecretManager]: SecretSyncPlanType.Regular, + [SecretSync.AzureKeyVault]: SecretSyncPlanType.Regular, + [SecretSync.AzureAppConfiguration]: SecretSyncPlanType.Regular, + [SecretSync.Databricks]: SecretSyncPlanType.Regular, + [SecretSync.Humanitec]: SecretSyncPlanType.Regular, + [SecretSync.TerraformCloud]: SecretSyncPlanType.Regular, + [SecretSync.Camunda]: SecretSyncPlanType.Regular, + [SecretSync.Vercel]: SecretSyncPlanType.Regular, + [SecretSync.Windmill]: SecretSyncPlanType.Regular, + [SecretSync.HCVault]: SecretSyncPlanType.Regular, + [SecretSync.TeamCity]: SecretSyncPlanType.Regular, + [SecretSync.OCIVault]: SecretSyncPlanType.Enterprise +}; diff --git a/backend/src/services/secret-sync/secret-sync-queue.ts b/backend/src/services/secret-sync/secret-sync-queue.ts index 62b4ba3cc..6f627c24e 100644 --- a/backend/src/services/secret-sync/secret-sync-queue.ts +++ b/backend/src/services/secret-sync/secret-sync-queue.ts @@ -5,6 +5,7 @@ import { Job } from "bullmq"; import { ProjectMembershipRole, SecretType } from "@app/db/schemas"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { logger } from "@app/lib/logger"; @@ -32,7 +33,7 @@ import { SecretSyncInitialSyncBehavior } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; -import { parseSyncErrorMessage, SecretSyncFns } from "@app/services/secret-sync/secret-sync-fns"; +import { enterpriseSyncCheck, parseSyncErrorMessage, SecretSyncFns } from "@app/services/secret-sync/secret-sync-fns"; import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { SecretSyncAction, @@ -93,6 +94,7 @@ type TSecretSyncQueueFactoryDep = { secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; resourceMetadataDAL: Pick; + licenseService: Pick; }; type SecretSyncActionJob = Job< @@ -133,7 +135,8 @@ export const secretSyncQueueFactory = ({ secretVersionTagDAL, secretVersionV2BridgeDAL, secretVersionTagV2BridgeDAL, - resourceMetadataDAL + resourceMetadataDAL, + licenseService }: TSecretSyncQueueFactoryDep) => { const appCfg = getConfig(); @@ -323,7 +326,20 @@ export const secretSyncQueueFactory = ({ secretSync: TSecretSyncWithCredentials, importBehavior: SecretSyncImportBehavior ): Promise => { - const { projectId, environment, folder } = secretSync; + const { + projectId, + environment, + folder, + destination, + connection: { orgId } + } = secretSync; + + await enterpriseSyncCheck( + licenseService, + destination, + orgId, + "Failed to import secrets due to plan restriction. Upgrade plan to access enterprise secret syncs." + ); if (!environment || !folder) throw new Error( @@ -400,6 +416,13 @@ export const secretSyncQueueFactory = ({ if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`); + await enterpriseSyncCheck( + licenseService, + secretSync.destination as SecretSync, + secretSync.connection.orgId, + "Failed to sync secrets due to plan restriction. Upgrade plan to access enterprise secret syncs." + ); + await secretSyncDAL.updateById(syncId, { syncStatus: SecretSyncStatus.Running }); @@ -659,6 +682,13 @@ export const secretSyncQueueFactory = ({ if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`); + await enterpriseSyncCheck( + licenseService, + secretSync.destination as SecretSync, + secretSync.connection.orgId, + "Failed to remove secrets due to plan restriction. Upgrade plan to access enterprise secret syncs." + ); + await secretSyncDAL.updateById(syncId, { removeStatus: SecretSyncStatus.Running }); diff --git a/backend/src/services/secret-sync/secret-sync-service.ts b/backend/src/services/secret-sync/secret-sync-service.ts index db350f785..e7751d3f9 100644 --- a/backend/src/services/secret-sync/secret-sync-service.ts +++ b/backend/src/services/secret-sync/secret-sync-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { ActionProjectType } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { @@ -16,7 +17,7 @@ import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-c import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; -import { listSecretSyncOptions } from "@app/services/secret-sync/secret-sync-fns"; +import { enterpriseSyncCheck, listSecretSyncOptions } from "@app/services/secret-sync/secret-sync-fns"; import { SecretSyncStatus, TCreateSecretSyncDTO, @@ -49,6 +50,7 @@ type TSecretSyncServiceFactoryDep = { TSecretSyncQueueFactory, "queueSecretSyncSyncSecretsById" | "queueSecretSyncImportSecretsById" | "queueSecretSyncRemoveSecretsById" >; + licenseService: Pick; }; export type TSecretSyncServiceFactory = ReturnType; @@ -61,7 +63,8 @@ export const secretSyncServiceFactory = ({ appConnectionService, projectBotService, secretSyncQueue, - keyStore + keyStore, + licenseService }: TSecretSyncServiceFactoryDep) => { const listSecretSyncsByProjectId = async ( { projectId, destination }: TListSecretSyncsByProjectId, @@ -191,6 +194,13 @@ export const secretSyncServiceFactory = ({ { projectId, secretPath, environment, ...params }: TCreateSecretSyncDTO, actor: OrgServiceActor ) => { + await enterpriseSyncCheck( + licenseService, + params.destination, + actor.orgId, + "Failed to create secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs." + ); + const { permission: projectPermission } = await permissionService.getProjectPermission({ actor: actor.type, actorId: actor.id, @@ -260,6 +270,13 @@ export const secretSyncServiceFactory = ({ message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID ${syncId}` }); + await enterpriseSyncCheck( + licenseService, + secretSync.destination as SecretSync, + actor.orgId, + "Failed to update secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs." + ); + const { permission } = await permissionService.getProjectPermission({ actor: actor.type, actorId: actor.id, @@ -408,6 +425,13 @@ export const secretSyncServiceFactory = ({ message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"` }); + await enterpriseSyncCheck( + licenseService, + secretSync.destination as SecretSync, + actor.orgId, + "Failed to trigger secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs." + ); + const { permission } = await permissionService.getProjectPermission({ actor: actor.type, actorId: actor.id, @@ -463,6 +487,13 @@ export const secretSyncServiceFactory = ({ message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"` }); + await enterpriseSyncCheck( + licenseService, + secretSync.destination as SecretSync, + actor.orgId, + "Failed to trigger secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs." + ); + const { permission } = await permissionService.getProjectPermission({ actor: actor.type, actorId: actor.id, @@ -512,6 +543,13 @@ export const secretSyncServiceFactory = ({ message: `Could not find ${SECRET_SYNC_NAME_MAP[destination]} Sync with ID "${syncId}"` }); + await enterpriseSyncCheck( + licenseService, + secretSync.destination as SecretSync, + actor.orgId, + "Failed to trigger secret sync due to plan restriction. Upgrade plan to access enterprise secret syncs." + ); + const { permission } = await permissionService.getProjectPermission({ actor: actor.type, actorId: actor.id, diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index e83885759..22f7848ad 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -1,6 +1,12 @@ import { Job } from "bullmq"; import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types"; +import { + TOCIVaultSync, + TOCIVaultSyncInput, + TOCIVaultSyncListItem, + TOCIVaultSyncWithCredentials +} from "@app/ee/services/secret-sync/oci-vault"; import { QueueJobs } from "@app/queue"; import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema"; import { @@ -73,7 +79,6 @@ import { THumanitecSyncListItem, THumanitecSyncWithCredentials } from "./humanitec"; -import { TOCIVaultSync, TOCIVaultSyncInput, TOCIVaultSyncListItem, TOCIVaultSyncWithCredentials } from "./oci-vault"; import { TTeamCitySync, TTeamCitySyncInput, diff --git a/backend/src/services/user/user-service.ts b/backend/src/services/user/user-service.ts index 29f6300d6..aae32d91f 100644 --- a/backend/src/services/user/user-service.ts +++ b/backend/src/services/user/user-service.ts @@ -5,6 +5,7 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/pe import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TokenType } from "@app/services/auth-token/auth-token-types"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; @@ -80,6 +81,17 @@ export const userServiceFactory = ({ const verifyEmailVerificationCode = async (username: string, code: string) => { // akhilmhdh: case sensitive email resolution const usersByusername = await userDAL.findUserByUsername(username); + + logger.info( + usersByusername.map((user) => ({ + id: user.id, + email: user.email, + username: user.username, + isEmailVerified: user.isEmailVerified + })), + `Verify email users: [username=${username}]` + ); + const user = usersByusername?.length > 1 ? usersByusername.find((el) => el.username === username) : usersByusername?.[0]; if (!user) throw new NotFoundError({ name: `User with username '${username}' not found` }); diff --git a/docs/documentation/platform/sso/auth0-oidc.mdx b/docs/documentation/platform/sso/auth0-oidc.mdx index 0665a7b30..4b54c053d 100644 --- a/docs/documentation/platform/sso/auth0-oidc.mdx +++ b/docs/documentation/platform/sso/auth0-oidc.mdx @@ -14,7 +14,7 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO." 1.1. From the Application's Page, navigate to the settings tab of the Auth0 application you want to integrate with Infisical. ![OIDC auth0 list of applications](../../../images/sso/auth0-oidc/application-settings.png) - + 1.2. In the Application URIs section, set the **Application Login URI** and **Allowed Web Origins** fields to `https://app.infisical.com` and the **Allowed Callback URL** field to `https://app.infisical.com/api/v1/sso/oidc/callback`. ![OIDC auth0 create application uris](../../../images/sso/auth0-oidc/application-uris.png) ![OIDC auth0 create application origin](../../../images/sso/auth0-oidc/application-origin.png) @@ -70,7 +70,7 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO." prior to enforcing OIDC SSO to prevent any unintended issues. - In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + In case of a lockout, an organization admin can use the [Admin Login Portal](https://infisical.com/docs/documentation/platform/sso/overview#admin-login-portal) in the `/login/admin` path e.g. https://app.infisical.com/login/admin. diff --git a/docs/documentation/platform/sso/auth0-saml.mdx b/docs/documentation/platform/sso/auth0-saml.mdx index 562360ecb..22ef00c89 100644 --- a/docs/documentation/platform/sso/auth0-saml.mdx +++ b/docs/documentation/platform/sso/auth0-saml.mdx @@ -23,30 +23,30 @@ description: "Learn how to configure Auth0 SAML for Infisical SSO." 2.1. In your Auth0 account, head to Applications and create an application. - + ![Auth0 SAML app creation](../../../images/sso/auth0-saml/create-application.png) - + Select **Regular Web Application** and press **Create**. - + ![Auth0 SAML app creation](../../../images/sso/auth0-saml/create-application-2.png) - + 2.2. In the Application head to Settings > Application URIs and add the **Application Callback URL** from step 1 into the **Allowed Callback URLs** field. - + ![Auth0 SAML allowed callback URLs](../../../images/sso/auth0-saml/auth0-config.png) - + 2.3. In the Application head to Addons > SAML2 Web App and copy the **Issuer**, **Identity Provider Login URL**, and **Identity Provider Certificate** from the **Usage** tab. - + ![Auth0 SAML config](../../../images/sso/auth0-saml/auth0-config-2.png) - + 2.4. Back in Infisical, set **Issuer**, **Identity Provider Login URL**, and **Certificate** to the corresponding items from step 2.3. - + ![Auth0 SAML Infisical config](../../../images/sso/auth0-saml/infisical-config.png) - + 2.5. Back in Auth0, in the **Settings** tab, set the **Application Callback URL** to the **Application Callback URL** from step 1 and update the **Settings** field with the JSON under the picture below (replacing `` with the **Audience** from step 1). - + ![Auth0 SAML config](../../../images/sso/auth0-saml/auth0-config-3.png) - + ```json { "audience": "", @@ -76,7 +76,7 @@ description: "Learn how to configure Auth0 SAML for Infisical SSO." Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO. - In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + In case of a lockout, an organization admin can use the [Admin Login Portal](https://infisical.com/docs/documentation/platform/sso/overview#admin-login-portal) in the `/login/admin` path e.g. https://app.infisical.com/login/admin. @@ -96,4 +96,4 @@ description: "Learn how to configure Auth0 SAML for Infisical SSO." 32`.
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com) - \ No newline at end of file + diff --git a/docs/documentation/platform/sso/azure.mdx b/docs/documentation/platform/sso/azure.mdx index 137dc6564..0957dc4d1 100644 --- a/docs/documentation/platform/sso/azure.mdx +++ b/docs/documentation/platform/sso/azure.mdx @@ -5,7 +5,7 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO." Azure SAML SSO is a paid feature. - + If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it. @@ -26,7 +26,7 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO." ![Azure SAML enterprise applications](../../../images/sso/azure/enterprise-applications.png) ![Azure SAML new application](../../../images/sso/azure/new-application.png) - + On the next screen, press the **+ Create your own application** button. Give the application a unique name like Infisical; choose the "Integrate any other application you don't find in the gallery (Non-gallery)" option and hit the **Create** button. @@ -89,9 +89,9 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO." Back in Azure, navigate to the **Users and groups** tab and select **+ Add user/group** to assign access to the login with SSO application on a user or group-level. - + ![Azure SAML assignment](../../../images/sso/azure/assignment.png) - + Enabling SAML SSO allows members in your organization to log into Infisical via Azure. @@ -109,7 +109,7 @@ description: "Learn how to configure Microsoft Entra ID for Infisical SSO." prior to enforcing SAML SSO to prevent any unintended issues. - In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + In case of a lockout, an organization admin can use the [Admin Login Portal](https://infisical.com/docs/documentation/platform/sso/overview#admin-login-portal) in the `/login/admin` path e.g. https://app.infisical.com/login/admin. diff --git a/docs/documentation/platform/sso/general-oidc/overview.mdx b/docs/documentation/platform/sso/general-oidc/overview.mdx index 76ac982f8..07ddaaedd 100644 --- a/docs/documentation/platform/sso/general-oidc/overview.mdx +++ b/docs/documentation/platform/sso/general-oidc/overview.mdx @@ -70,7 +70,7 @@ Prerequisites: We recommend ensuring that your account is provisioned using the identity provider prior to enforcing OIDC SSO to prevent any unintended issues. - In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + In case of a lockout, an organization admin can use the [Admin Login Portal](https://infisical.com/docs/documentation/platform/sso/overview#admin-login-portal) in the `/login/admin` path e.g. https://app.infisical.com/login/admin. diff --git a/docs/documentation/platform/sso/google-saml.mdx b/docs/documentation/platform/sso/google-saml.mdx index 99223c815..84888b2f9 100644 --- a/docs/documentation/platform/sso/google-saml.mdx +++ b/docs/documentation/platform/sso/google-saml.mdx @@ -24,21 +24,21 @@ description: "Learn how to configure Google SAML for Infisical SSO." 2.1. In your [Google Admin console](https://support.google.com/a/answer/182076), head to Menu > Apps > Web and mobile apps and create a **custom SAML app**. - + ![Google SAML app creation](../../../images/sso/google-saml/create-custom-saml-app.png) - + 2.2. In the **App details** tab, give the application a unique name like Infisical. - + ![Google SAML app naming](../../../images/sso/google-saml/name-custom-saml-app.png) - + 2.3. In the **Google Identity Provider details** tab, copy the **SSO URL**, **Entity ID** and **Certificate**. - + ![Google SAML custom app details](../../../images/sso/google-saml/custom-saml-app-config.png) - + 2.4. Back in Infisical, set **SSO URL** and **Certificate** to the corresponding items from step 2.3. - + ![Google SAML Infisical config](../../../images/sso/google-saml/infisical-config.png) - + 2.5. Back in the Google Admin console, in the **Service provider details** tab, set the **ACS URL** and **Entity ID** to the corresponding items from step 1. Also, check the **Signed response** checkbox. @@ -84,7 +84,7 @@ description: "Learn how to configure Google SAML for Infisical SSO." prior to enforcing SAML SSO to prevent any unintended issues. - In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + In case of a lockout, an organization admin can use the [Admin Login Portal](https://infisical.com/docs/documentation/platform/sso/overview#admin-login-portal) in the `/login/admin` path e.g. https://app.infisical.com/login/admin. diff --git a/docs/documentation/platform/sso/jumpcloud.mdx b/docs/documentation/platform/sso/jumpcloud.mdx index 0898c0715..3cad22247 100644 --- a/docs/documentation/platform/sso/jumpcloud.mdx +++ b/docs/documentation/platform/sso/jumpcloud.mdx @@ -5,7 +5,7 @@ description: "Learn how to configure JumpCloud SAML for Infisical SSO." JumpCloud SAML SSO is a paid feature. - + If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it. @@ -83,13 +83,12 @@ description: "Learn how to configure JumpCloud SAML for Infisical SSO." To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one JumpCloud user with Infisical; Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO. - + - We recommend ensuring that your account is provisioned the application in JumpCloud - prior to enforcing SAML SSO to prevent any unintended issues. + We recommend ensuring that your account is provisioned in the application in JumpCloud prior to enforcing SAML SSO to prevent any unintended issues. - In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + In case of a lockout, an organization admin can use the [Admin Login Portal](https://infisical.com/docs/documentation/platform/sso/overview#admin-login-portal) in the `/login/admin` path e.g. https://app.infisical.com/login/admin. diff --git a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx index 06d8dfa43..2c75fc6fe 100644 --- a/docs/documentation/platform/sso/keycloak-oidc/overview.mdx +++ b/docs/documentation/platform/sso/keycloak-oidc/overview.mdx @@ -97,7 +97,7 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO." prior to enforcing OIDC SSO to prevent any unintended issues. - In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + In case of a lockout, an organization admin can use the [Admin Login Portal](https://infisical.com/docs/documentation/platform/sso/overview#admin-login-portal) in the `/login/admin` path e.g. https://app.infisical.com/login/admin. diff --git a/docs/documentation/platform/sso/keycloak-saml.mdx b/docs/documentation/platform/sso/keycloak-saml.mdx index ba6aa0c3a..daca360b4 100644 --- a/docs/documentation/platform/sso/keycloak-saml.mdx +++ b/docs/documentation/platform/sso/keycloak-saml.mdx @@ -5,7 +5,7 @@ description: "Learn how to configure Keycloak SAML for Infisical SSO." Keycloak SAML SSO is a paid feature. - + If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it. @@ -13,36 +13,36 @@ description: "Learn how to configure Keycloak SAML for Infisical SSO." In Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **SAML** under the Connect to an Identity Provider section. Select **Keycloak**, then click **Connect** again. - + ![SSO connect section](../../../images/sso/connect-saml.png) - + Next, copy the **Valid redirect URI** and **SP Entity ID** to use when configuring the Keycloak SAML application. - + ![Keycloak SAML initial configuration](../../../images/sso/keycloak/init-config.png) 2.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application. - + ![SAML keycloak list of clients](../../../images/sso/keycloak/clients-list.png) - + You don’t typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm. - + In the General Settings step, set **Client type** to **SAML**, the **Client ID** field to `https://app.infisical.com`, and the **Name** field to a friendly name like **Infisical**. - + ![SAML keycloak create client general settings](../../../images/sso/keycloak/create-client-general-settings.png) - + If you’re self-hosting Infisical, then you will want to replace https://app.infisical.com with your own domain. - + Next, in the Login Settings step, set both the **Home URL** field and **Valid redirect URIs** field to the **Valid redirect URI** from step 1 and press **Save**. - + ![SAML keycloak create client login settings](../../../images/sso/keycloak/create-client-login-settings.png) - + 2.2. Once you've created the client, under its **Settings** tab, make sure to set the following values: - + - Under **SAML Capabilities**: - Name ID format: email (or username). - Force name ID format: On. @@ -54,59 +54,59 @@ description: "Learn how to configure Keycloak SAML for Infisical SSO." - Signature algorithm: RSA_SHA256. ![SAML keycloak client SAML capabilities](../../../images/sso/keycloak/client-saml-capabilities.png) - + ![SAML keycloak client signature encryption](../../../images/sso/keycloak/client-signature-encryption.png) - + 2.3. Next, navigate to the **Client scopes** tab select the client's dedicated scope. - + ![SAML keycloak client scopes list](../../../images/sso/keycloak/client-scopes-list.png) - + Next click **Add predefined mapper**. - + ![SAML keycloak client mappers empty](../../../images/sso/keycloak/client-mappers-empty.png) - + Select the **X500 email**, **X500 givenName**, and **X500 surname** attributes and click **Add**. - + ![SAML keycloak client mappers predefined](../../../images/sso/keycloak/client-mappers-predefined.png) - - Now click on the **X500 email** mapper and set the **SAML Attribute Name** field to **email**. + + Now click on the **X500 email** mapper and set the **SAML Attribute Name** field to **email**. ![SAML keycloak client mappers email](../../../images/sso/keycloak/client-mappers-email.png) - + Repeat the same for **X500 givenName** and **X500 surname** mappers, setting the **SAML Attribute Name** field to **firstName** and **lastName** respectively. - + Next, back in the client scope's **Mappers**, click **Add mapper** and select **by configuration**. - + ![SAML keycloak client mappers by configuration](../../../images/sso/keycloak/client-mappers-by-configuration.png) - + Select **User Property**. - + ![SAML keycloak client mappers user property](../../../images/sso/keycloak/client-mappers-user-property.png) - Set the the **Name** field to **Username**, the **Property** field to **username**, and the **SAML Attribtue Name** to **username**. - + Set the the **Name** field to **Username**, the **Property** field to **username**, and the **SAML Attribute Name** to **username**. + ![SAML keycloak client mappers username](../../../images/sso/keycloak/client-mappers-username.png) - + Repeat the same for the `id` attribute, setting the **Name** field to **ID**, the **Property** field to **id**, and the **SAML Attribute Name** to **id**. - + ![SAML keycloak client mappers id](../../../images/sso/keycloak/client-mappers-id.png) - + Once you've completed the above steps, the list of mappers should look like this: - + ![SAML keycloak client mappers completed](../../../images/sso/keycloak/client-mappers-completed.png) Back in Keycloak, navigate to Configure > Realm settings > General tab > Endpoints > SAML 2.0 Identity Provider Metadata and copy the IDP URL. This should appear in various places and take the form: `https://keycloak-mysite.com/realms/myrealm/protocol/saml`. - + ![SAML keycloak realm SAML metadata](../../../images/sso/keycloak/realm-saml-metadata.png) - + Also, in the **Keys** tab, locate the RS256 key and copy the certificate to use when finishing configuring Keycloak SAML in Infisical. - + ![SAML keycloak realm settings keys](../../../images/sso/keycloak/realm-settings-keys.png) Back in Infisical, set **IDP URL** and **Certificate** to the items from step 3. Also, set the **Client ID** to the `https://app.infisical.com`. - + Once you've done that, press **Update** to complete the required configuration. ![SAML Okta paste values into Infisical](../../../images/sso/keycloak/idp-values.png) @@ -119,7 +119,7 @@ description: "Learn how to configure Keycloak SAML for Infisical SSO." Enforcing SAML SSO ensures that members in your organization can only access Infisical by logging into the organization via Keycloak. - + To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Keycloak user with Infisical; Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO. @@ -128,7 +128,7 @@ description: "Learn how to configure Keycloak SAML for Infisical SSO." prior to enforcing SAML SSO to prevent any unintended issues. - In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + In case of a lockout, an organization admin can use the [Admin Login Portal](https://infisical.com/docs/documentation/platform/sso/overview#admin-login-portal) in the `/login/admin` path e.g. https://app.infisical.com/login/admin. @@ -147,4 +147,4 @@ description: "Learn how to configure Keycloak SAML for Infisical SSO." 32`.
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com) - \ No newline at end of file + diff --git a/docs/documentation/platform/sso/okta.mdx b/docs/documentation/platform/sso/okta.mdx index 2af689e4c..ecdf6ca39 100644 --- a/docs/documentation/platform/sso/okta.mdx +++ b/docs/documentation/platform/sso/okta.mdx @@ -93,13 +93,12 @@ description: "Learn how to configure Okta SAML 2.0 for Infisical SSO." Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO. - We recommend ensuring that your account is provisioned the application in Okta - prior to enforcing SAML SSO to prevent any unintended issues. + We recommend ensuring that your account is provisioned for the application in Okta prior to enforcing SAML SSO to prevent any unintended issues. - - In case of a lockout, an organization admin can use the admin login portal in the `/login/admin` path e.g. https://app.infisical.com/login/admin. - + + In case of a lockout, an organization admin can use the [Admin Login Portal](https://infisical.com/docs/documentation/platform/sso/overview#admin-login-portal) in the `/login/admin` path e.g. https://app.infisical.com/login/admin. + diff --git a/docs/documentation/platform/sso/overview.mdx b/docs/documentation/platform/sso/overview.mdx index e5d5e5c16..66243f7d8 100644 --- a/docs/documentation/platform/sso/overview.mdx +++ b/docs/documentation/platform/sso/overview.mdx @@ -39,18 +39,30 @@ If your required identity provider is not shown in the list above, please reach For enhanced security, Infisical enforces PKCE (Proof Key for Code Exchange) with the OAuth 2.0-based SSO providers and OIDC. This provides additional protection against authorization code interception attacks and strengthens your authentication flow security. +## SSO Break Glass + +In the event your SSO provider experiences downtime, and you need to access Infisical, Organization Admins can utilize the Admin Login Portal to bypass SSO enforcement. + +This portal is accessible at `/login/admin` (e.g., https://app.infisical.com/login/admin). + + + To bypass SSO for an organization, you must be an **Organization Admin** for that specific organization. This **Organization Admin** role is independent of **Server Admin** status. Being a **Server Admin** alone does not grant permission to use this bypass feature. + + ## FAQ - - By default, Infisical Cloud is configured to not trust emails from external - identity providers to prevent any malicious account takeover attempts via - email spoofing. Accordingly, Infisical creates a new user for anyone provisioned - through an external identity provider and requires an additional email - verification step upon their first login. + + By default, Infisical Cloud is configured to not trust emails from external + identity providers to prevent any malicious account takeover attempts via + email spoofing. Accordingly, Infisical creates a new user for anyone provisioned + through an external identity provider and requires an additional email + verification step upon their first login. - If you're running a self-hosted instance of Infisical and would like it to trust emails from external identity providers, - you can configure this behavior in the Server Admin Console. - - + If you're running a self-hosted instance of Infisical and would like it to trust emails from external identity providers, + you can configure this behavior in the Server Admin Console. + + + You are likely being redirected because you do not have email authentication mode enabled, or you're not an **Organization Admin**. This portal requires **Organization Admin** status and direct credential login (email and password). **Server Admin** status alone is insufficient. + diff --git a/docs/integrations/app-connections/oci.mdx b/docs/integrations/app-connections/oci.mdx index ff51ce1d9..58fb3c1d3 100644 --- a/docs/integrations/app-connections/oci.mdx +++ b/docs/integrations/app-connections/oci.mdx @@ -3,6 +3,13 @@ title: "OCI Connection" description: "Learn how to configure an Oracle Cloud Infrastructure Connection for Infisical." --- + + OCI App Connection is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, + then you should contact team@infisical.com to purchase an enterprise license to use it. + + Infisical supports the use of [API Signing Key Authentication](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm) to connect with OCI. ## Create OCI User diff --git a/docs/integrations/secret-syncs/oci-vault.mdx b/docs/integrations/secret-syncs/oci-vault.mdx index 67a3426aa..00b7120e7 100644 --- a/docs/integrations/secret-syncs/oci-vault.mdx +++ b/docs/integrations/secret-syncs/oci-vault.mdx @@ -3,6 +3,13 @@ title: "OCI Vault Sync" description: "Learn how to configure an Oracle Cloud Infrastructure Vault Sync for Infisical." --- + + OCI Vault Sync is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, + then you should contact team@infisical.com to purchase an enterprise license to use it. + + **Prerequisites:** - Create an [OCI Connection](/integrations/app-connections/oci) with the required **Secret Sync** permissions - [Create](https://docs.oracle.com/en-us/iaas/Content/Identity/compartments/To_create_a_compartment.htm) or use an existing OCI Compartment (which the OCI Connection is authorized to access) diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index b63c58d3a..8da732d6d 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -32,7 +32,7 @@ Used to configure platform-specific security and operational settings Specifies the network interface Infisical will bind to when accepting incoming connections. - By default, Infisical binds to `localhost`, which restricts access to connections from the same machine. + By default, Infisical binds to `localhost`, which restricts access to connections from the same machine. To make the application accessible externally (e.g., for self-hosted deployments), set this to `0.0.0.0`, which tells the server to listen on all network interfaces. @@ -95,9 +95,8 @@ The platform utilizes Postgres to persist all of its data and Redis for caching - Configure the SSL certificate for securing a Postgres connection by first encoding it in base64. - Use the command below to encode your certificate: - `echo "" | base64` + Configure the SSL certificate for securing a Postgres connection by first encoding it in base64. + Use the following command to encode your certificate: `echo "" | base64` @@ -111,10 +110,9 @@ DB_READ_REPLICAS=[{"DB_CONNECTION_URI":""}] Configure the SSL certificate for securing a Postgres replica connection by first encoding it in base64. - Use the command below to encode your certificate: - `echo "" | base64` + Use the following command to encode your certificate: `echo "" | base64` - If not provided it will use master SSL certificate. + If not provided it will use master SSL certificate. @@ -169,6 +167,16 @@ Without email configuration, Infisical's core functions like sign-up/login and s If this is `true`, Infisical will validate the server's SSL/TLS certificate and reject the connection if the certificate is invalid or not trusted. If set to `false`, the client will accept the server's certificate regardless of its validity, which can be useful in development or testing environments but is not recommended for production use. + + + If your SMTP server uses a certificate signed by a custom Certificate Authority, you should set this variable so that Infisical can trust the custom CA. + + This variable **must be a base64 encoded PEM certificate**. Use the following command to encode your certificate: `echo "" | base64` + + Infisical highly encourages the following variables be used alongside this one for maximum security: + - `SMTP_REQUIRE_TLS=true` + - `SMTP_TLS_REJECT_UNAUTHORIZED=true` + @@ -222,7 +230,7 @@ SMTP_FROM_NAME=Infisical This will be used to verify the email you are sending from. ![Create SES identity](../../images/self-hosting/configuration/email/ses-create-identity.png) - If you AWS SES is under sandbox mode, you will only be able to send emails to verified identies. + If you AWS SES is under sandbox mode, you will only be able to send emails to verified identies. @@ -388,9 +396,9 @@ SMTP_FROM_NAME=Infisical - + 1. Create an account and configure [SMTP2Go](https://www.smtp2go.com/) to send emails. -2. Turn on SMTP authentication +2. Turn on SMTP authentication ``` SMTP_HOST=mail.smtp2go.com SMTP_PORT=You can use one of the following ports: 2525, 80, 25, 8025, or 587 @@ -401,7 +409,7 @@ SMTP_FROM_NAME=Infisical ``` {" "} - + Optional (for TLS/SSL): TLS: Available on the same ports (2525, 80, 25, 8025, or 587) diff --git a/frontend/src/components/secret-syncs/SecretSyncSelect.tsx b/frontend/src/components/secret-syncs/SecretSyncSelect.tsx index cbcba4513..62d99544f 100644 --- a/frontend/src/components/secret-syncs/SecretSyncSelect.tsx +++ b/frontend/src/components/secret-syncs/SecretSyncSelect.tsx @@ -2,16 +2,23 @@ import { faWrench } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Spinner, Tooltip } from "@app/components/v2"; +import { useSubscription } from "@app/context"; import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; +import { usePopUp } from "@app/hooks"; import { SecretSync, useSecretSyncOptions } from "@app/hooks/api/secretSyncs"; +import { UpgradePlanModal } from "../license/UpgradePlanModal"; + type Props = { onSelect: (destination: SecretSync) => void; }; export const SecretSyncSelect = ({ onSelect }: Props) => { + const { subscription } = useSubscription(); const { isPending, data: secretSyncOptions } = useSecretSyncOptions(); + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); + if (isPending) { return (
@@ -23,14 +30,17 @@ export const SecretSyncSelect = ({ onSelect }: Props) => { return (
- {secretSyncOptions?.map(({ destination }) => { + {secretSyncOptions?.map(({ destination, enterprise }) => { const { image, name } = SECRET_SYNC_MAP[destination]; return ( ); })} + handlePopUpToggle("upgradePlan", isOpen)} + text="You can use every Secret Sync if you switch to Infisical's Enterprise plan." + /> = { [AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" }, [AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" }, @@ -64,7 +64,7 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.HCVault]: { name: "Hashicorp Vault", image: "Vault.png", size: 65 }, [AppConnection.LDAP]: { name: "LDAP", image: "LDAP.png", size: 65 }, [AppConnection.TeamCity]: { name: "TeamCity", image: "TeamCity.png" }, - [AppConnection.OCI]: { name: "OCI", image: "Oracle.png" }, + [AppConnection.OCI]: { name: "OCI", image: "Oracle.png", enterprise: true }, [AppConnection.OnePass]: { name: "1Password", image: "1Password.png" } }; diff --git a/frontend/src/helpers/userTablePreferences.ts b/frontend/src/helpers/userTablePreferences.ts new file mode 100644 index 000000000..a23438746 --- /dev/null +++ b/frontend/src/helpers/userTablePreferences.ts @@ -0,0 +1,73 @@ +const TABLE_PREFERENCES_KEY = "userTablePreferences"; + +export enum PreferenceKey { + PerPage = "perPage" +} + +interface TableSpecificPreferences { + [preferenceKey: string]: any; +} + +interface UserTablePreferences { + [tableName: string]: TableSpecificPreferences; +} + +// Retrieves all table preferences from localStorage +const getAllTablePreferences = (): UserTablePreferences => { + try { + const preferencesString = localStorage.getItem(TABLE_PREFERENCES_KEY); + if (preferencesString) { + return JSON.parse(preferencesString) as UserTablePreferences; + } + } catch (error) { + console.error("Error reading user table preferences from localStorage:", error); + } + return {}; +}; + +// Saves all table preferences to localStorage +const saveAllTablePreferences = (preferences: UserTablePreferences): void => { + try { + localStorage.setItem(TABLE_PREFERENCES_KEY, JSON.stringify(preferences)); + } catch (error) { + console.error("Error saving user table preferences to localStorage:", error); + } +}; + +// Retrieves a specific preference for a given table +export const getUserTablePreference = ( + tableName: string, + preferenceKey: PreferenceKey, + defaultValue: T +): T => { + const preferences = getAllTablePreferences(); + if ( + preferences && + typeof preferences === "object" && + tableName in preferences && + preferenceKey in preferences[tableName] + ) { + const value = preferences[tableName][preferenceKey]; + + if (value !== undefined && value !== null) { + return value as T; + } + } + return defaultValue; +}; + +// Sets a specific preference for a given table and saves it to localStorage +export const setUserTablePreference = ( + tableName: string, + preferenceKey: PreferenceKey, + value: any +): void => { + const preferences = getAllTablePreferences(); + + if (!preferences[tableName]) { + preferences[tableName] = {}; + } + + preferences[tableName][preferenceKey] = value; + saveAllTablePreferences(preferences); +}; diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index f726566cd..555d66b88 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -123,7 +123,6 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.CREATE_PROJECT_TEMPLATE]: "Create project template", [EventType.UPDATE_PROJECT_TEMPLATE]: "Update project template", [EventType.DELETE_PROJECT_TEMPLATE]: "Delete project template", - [EventType.APPLY_PROJECT_TEMPLATE]: "Apply project template", [EventType.GET_APP_CONNECTIONS]: "List App Connections", [EventType.GET_AVAILABLE_APP_CONNECTIONS_DETAILS]: "List App Connections Details", [EventType.GET_APP_CONNECTION]: "Get App Connection", @@ -189,7 +188,13 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.ADD_IDENTITY_LDAP_AUTH]: "Attached LDAP Auth to identity", [EventType.UPDATE_IDENTITY_LDAP_AUTH]: "Updated LDAP Auth for identity", [EventType.GET_IDENTITY_LDAP_AUTH]: "Retrieved LDAP Auth for identity", - [EventType.REVOKE_IDENTITY_LDAP_AUTH]: "Revoked LDAP Auth for identity" + [EventType.REVOKE_IDENTITY_LDAP_AUTH]: "Revoked LDAP Auth for identity", + + [EventType.UPDATE_ORG]: "Update Organization", + + [EventType.CREATE_PROJECT]: "Create Project", + [EventType.UPDATE_PROJECT]: "Update Project", + [EventType.DELETE_PROJECT]: "Delete Project" }; export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = { diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index b74969d6d..19dfd9522 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -131,7 +131,6 @@ export enum EventType { CREATE_PROJECT_TEMPLATE = "create-project-template", UPDATE_PROJECT_TEMPLATE = "update-project-template", DELETE_PROJECT_TEMPLATE = "delete-project-template", - APPLY_PROJECT_TEMPLATE = "apply-project-template", GET_APP_CONNECTIONS = "get-app-connections", GET_AVAILABLE_APP_CONNECTIONS_DETAILS = "get-available-app-connections-details", GET_APP_CONNECTION = "get-app-connection", @@ -183,5 +182,11 @@ export enum EventType { MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_CHECK_INSTALLATION_STATUS = "microsoft-teams-workflow-integration-check-installation-status", MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET_TEAMS = "microsoft-teams-workflow-integration-get-teams", MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_GET = "microsoft-teams-workflow-integration-get", - MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list" + MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list", + + UPDATE_ORG = "update-org", + + CREATE_PROJECT = "create-project", + UPDATE_PROJECT = "update-project", + DELETE_PROJECT = "delete-project" } diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 087ba1939..f28a0820b 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -22,6 +22,7 @@ export type TSecretSyncOption = { name: string; destination: SecretSync; canImportSecrets: boolean; + enterprise?: boolean; }; export type TSecretSync = diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index ec7b6a2dd..a861c215a 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -50,4 +50,6 @@ export type SubscriptionPlan = { enforceMfa: boolean; projectTemplates: boolean; kmip: boolean; + enterpriseSecretSyncs: boolean; + enterpriseAppConnections: boolean; }; diff --git a/frontend/src/pages/kms/KmipPage/components/KmipClientTable.tsx b/frontend/src/pages/kms/KmipPage/components/KmipClientTable.tsx index a9d29d24a..bcc49b11a 100644 --- a/frontend/src/pages/kms/KmipPage/components/KmipClientTable.tsx +++ b/frontend/src/pages/kms/KmipPage/components/KmipClientTable.tsx @@ -43,6 +43,11 @@ import { useSubscription, useWorkspace } from "@app/context"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { useGetKmipClientsByProjectId } from "@app/hooks/api/kmip"; @@ -71,7 +76,14 @@ export const KmipClientTable = () => { perPage, page, setPerPage - } = usePagination(KmipClientOrderBy.Name); + } = usePagination(KmipClientOrderBy.Name, { + initPerPage: getUserTablePreference("kmipClientTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("kmipClientTable", PreferenceKey.PerPage, newPerPage); + }; const { data, isPending, isFetching } = useGetKmipClientsByProjectId({ projectId, @@ -290,7 +302,7 @@ export const KmipClientTable = () => { page={page} perPage={perPage} onChangePage={(newPage) => setPage(newPage)} - onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + onChangePerPage={handlePerPageChange} /> )} {!isPending && kmipClients.length === 0 && ( diff --git a/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx b/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx index c6902b4fa..e1a2fe789 100644 --- a/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx +++ b/frontend/src/pages/kms/OverviewPage/components/CmekTable.tsx @@ -53,6 +53,11 @@ import { useWorkspace } from "@app/context"; import { kmsKeyUsageOptions } from "@app/helpers/kms"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { usePagination, usePopUp, useResetPageHelper, useTimedReset } from "@app/hooks"; import { useGetCmeksByProjectId, useUpdateCmek } from "@app/hooks/api/cmeks"; import { CmekOrderBy, KmsKeyUsage, TCmek } from "@app/hooks/api/cmeks/types"; @@ -100,7 +105,14 @@ export const CmekTable = () => { perPage, page, setPerPage - } = usePagination(CmekOrderBy.Name); + } = usePagination(CmekOrderBy.Name, { + initPerPage: getUserTablePreference("cmekClientTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("cmekClientTable", PreferenceKey.PerPage, newPerPage); + }; const { data, isPending, isFetching } = useGetCmeksByProjectId({ projectId, @@ -508,7 +520,7 @@ export const CmekTable = () => { page={page} perPage={perPage} onChangePage={(newPage) => setPage(newPage)} - onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + onChangePerPage={handlePerPageChange} /> )} {!isPending && keys.length === 0 && ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx index e7b89c48d..14de28fd6 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx @@ -34,6 +34,11 @@ import { Tr } from "@app/components/v2"; import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { usePagination, useResetPageHelper } from "@app/hooks"; import { useGetOrganizationGroups, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; @@ -103,7 +108,14 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { setOrderBy, setOrderDirection, toggleOrderDirection - } = usePagination(GroupsOrderBy.Name, { initPerPage: 20 }); + } = usePagination(GroupsOrderBy.Name, { + initPerPage: getUserTablePreference("orgGroupsTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("orgGroupsTable", PreferenceKey.PerPage, newPerPage); + }; const filteredGroups = useMemo(() => { const filtered = search @@ -376,7 +388,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { page={page} perPage={perPage} onChangePage={setPage} - onChangePerPage={setPerPage} + onChangePerPage={handlePerPageChange} /> )} {!isPending && !filteredGroups?.length && ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx index ddfcc7a7c..d499a7b36 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx @@ -42,6 +42,11 @@ import { Tr } from "@app/components/v2"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { usePagination, useResetPageHelper } from "@app/hooks"; import { useGetOrgRoles, useSearchIdentities, useUpdateIdentity } from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; @@ -76,7 +81,15 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { perPage, page, setPerPage - } = usePagination(OrgIdentityOrderBy.Name); + } = usePagination(OrgIdentityOrderBy.Name, { + initPerPage: getUserTablePreference("identityTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("identityTable", PreferenceKey.PerPage, newPerPage); + }; + const [filteredRoles, setFilteredRoles] = useState([]); const organizationId = currentOrg?.id || ""; @@ -379,7 +392,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { page={page} perPage={perPage} onChangePage={(newPage) => setPage(newPage)} - onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + onChangePerPage={handlePerPageChange} /> )} {!isPending && data && data?.identities.length === 0 && ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx index a79da885b..e0350741e 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx @@ -42,6 +42,11 @@ import { useSubscription, useUser } from "@app/context"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { usePagination, useResetPageHelper } from "@app/hooks"; import { useFetchServerStatus, @@ -170,7 +175,14 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro setOrderBy, setOrderDirection, toggleOrderDirection - } = usePagination(OrgMembersOrderBy.Name, { initPerPage: 20 }); + } = usePagination(OrgMembersOrderBy.Name, { + initPerPage: getUserTablePreference("orgMembersTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("orgMembersTable", PreferenceKey.PerPage, newPerPage); + }; const filteredUsers = useMemo( () => @@ -513,7 +525,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro page={page} perPage={perPage} onChangePage={setPage} - onChangePerPage={setPerPage} + onChangePerPage={handlePerPageChange} /> )} {!isMembersLoading && !filteredUsers?.length && ( diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx index 5e9f69335..ab6a44f43 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionList.tsx @@ -1,8 +1,11 @@ import { faWrench } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { Spinner, Tooltip } from "@app/components/v2"; +import { useSubscription } from "@app/context"; import { APP_CONNECTION_MAP } from "@app/helpers/appConnections"; +import { usePopUp } from "@app/hooks"; import { useAppConnectionOptions } from "@app/hooks/api/appConnections"; import { AppConnection } from "@app/hooks/api/appConnections/enums"; @@ -11,8 +14,11 @@ type Props = { }; export const AppConnectionsSelect = ({ onSelect }: Props) => { + const { subscription } = useSubscription(); const { isPending, data: appConnectionOptions } = useAppConnectionOptions(); + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); + if (isPending) { return (
@@ -25,13 +31,16 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => { return (
{appConnectionOptions?.map((option) => { - const { image, name, size = 50 } = APP_CONNECTION_MAP[option.app]; + const { image, name, size = 50, enterprise = false } = APP_CONNECTION_MAP[option.app]; return ( ); })} + handlePopUpToggle("upgradePlan", isOpen)} + text="You can use every App Connection if you switch to Infisical's Enterprise plan." + /> { orderBy, setOrderDirection, setOrderBy - } = usePagination(AppConnectionsOrderBy.App, { initPerPage: 20 }); + } = usePagination(AppConnectionsOrderBy.App, { + initPerPage: getUserTablePreference("appConnectionsTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("appConnectionsTable", PreferenceKey.PerPage, newPerPage); + }; const filteredAppConnections = useMemo( () => @@ -282,7 +294,7 @@ export const AppConnectionsTable = () => { page={page} perPage={perPage} onChangePage={setPage} - onChangePerPage={setPerPage} + onChangePerPage={handlePerPageChange} /> )} {!isPending && !filteredAppConnections?.length && ( diff --git a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx index b6af10f2f..6472c34c1 100644 --- a/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx +++ b/frontend/src/pages/organization/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx @@ -25,6 +25,11 @@ import { Tr } from "@app/components/v2"; import { OrgPermissionGroupActions, OrgPermissionSubjects, useOrganization } from "@app/context"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { usePagination, useResetPageHelper } from "@app/hooks"; import { useListGroupUsers, useOidcManageGroupMembershipsEnabled } from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; @@ -57,7 +62,14 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props offset, orderDirection, toggleOrderDirection - } = usePagination(GroupMembersOrderBy.Name, { initPerPage: 10 }); + } = usePagination(GroupMembersOrderBy.Name, { + initPerPage: getUserTablePreference("groupMembersTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("groupMembersTable", PreferenceKey.PerPage, newPerPage); + }; const { currentOrg } = useOrganization(); @@ -163,7 +175,7 @@ export const GroupMembersTable = ({ groupId, groupSlug, handlePopUpOpen }: Props page={page} perPage={perPage} onChangePage={setPage} - onChangePerPage={setPerPage} + onChangePerPage={handlePerPageChange} /> )} {!isPending && !filteredGroupMemberships?.length && ( diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx index 680bb8035..ef70d1f96 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityProjectsSection/IdentityProjectsTable.tsx @@ -21,6 +21,11 @@ import { THead, Tr } from "@app/components/v2"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { usePagination, useResetPageHelper } from "@app/hooks"; import { useGetIdentityProjectMemberships } from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; @@ -53,7 +58,14 @@ export const IdentityProjectsTable = ({ identityId, handlePopUpOpen }: Props) => offset, orderDirection, toggleOrderDirection - } = usePagination(IdentityProjectsOrderBy.Name, { initPerPage: 10 }); + } = usePagination(IdentityProjectsOrderBy.Name, { + initPerPage: getUserTablePreference("identityProjectsTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("identityProjectsTable", PreferenceKey.PerPage, newPerPage); + }; const filteredProjectMemberships = useMemo( () => @@ -132,7 +144,7 @@ export const IdentityProjectsTable = ({ identityId, handlePopUpOpen }: Props) => page={page} perPage={perPage} onChangePage={setPage} - onChangePerPage={setPerPage} + onChangePerPage={handlePerPageChange} /> )} {!isPending && !filteredProjectMemberships?.length && ( diff --git a/frontend/src/pages/organization/SecretManagerOverviewPage/components/AllProjectView.tsx b/frontend/src/pages/organization/SecretManagerOverviewPage/components/AllProjectView.tsx index 5fd4252e3..6be1a4554 100644 --- a/frontend/src/pages/organization/SecretManagerOverviewPage/components/AllProjectView.tsx +++ b/frontend/src/pages/organization/SecretManagerOverviewPage/components/AllProjectView.tsx @@ -28,6 +28,11 @@ import { } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { getProjectHomePage } from "@app/helpers/project"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useRequestProjectAccess, useSearchProjects } from "@app/hooks/api"; import { ProjectType, Workspace } from "@app/hooks/api/workspace/types"; @@ -104,7 +109,15 @@ export const AllProjectView = ({ limit, toggleOrderDirection, orderDirection - } = usePagination("name", { initPerPage: 50 }); + } = usePagination("name", { + initPerPage: getUserTablePreference("allProjectsTable", PreferenceKey.PerPage, 50) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("allProjectsTable", PreferenceKey.PerPage, newPerPage); + }; + const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp([ "requestAccessConfirmation" ] as const); @@ -274,7 +287,7 @@ export const AllProjectView = ({ count={searchedProjects?.totalCount || 0} page={page} onChangePage={setPage} - onChangePerPage={setPerPage} + onChangePerPage={handlePerPageChange} /> )} {!isProjectLoading && !searchedProjects?.totalCount && ( diff --git a/frontend/src/pages/organization/SecretManagerOverviewPage/components/MyProjectView.tsx b/frontend/src/pages/organization/SecretManagerOverviewPage/components/MyProjectView.tsx index e723e1dfe..a04b4e196 100644 --- a/frontend/src/pages/organization/SecretManagerOverviewPage/components/MyProjectView.tsx +++ b/frontend/src/pages/organization/SecretManagerOverviewPage/components/MyProjectView.tsx @@ -19,6 +19,11 @@ import { OrgPermissionCan } from "@app/components/permissions"; import { Button, IconButton, Input, Pagination, Skeleton, Tooltip } from "@app/components/v2"; import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context"; import { getProjectHomePage } from "@app/helpers/project"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { usePagination, useResetPageHelper } from "@app/hooks"; import { useGetUserWorkspaces } from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; @@ -63,7 +68,15 @@ export const MyProjectView = ({ limit, toggleOrderDirection, orderDirection - } = usePagination(ProjectOrderBy.Name, { initPerPage: 24 }); + } = usePagination(ProjectOrderBy.Name, { + initPerPage: getUserTablePreference("myProjectsTable", PreferenceKey.PerPage, 24) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("myProjectsTable", PreferenceKey.PerPage, newPerPage); + }; + const { data: projectFavorites, isPending: isProjectFavoritesLoading } = useGetUserProjectFavorites(currentOrg?.id); @@ -415,7 +428,7 @@ export const MyProjectView = ({ count={filteredWorkspaces.length} page={page} onChangePage={setPage} - onChangePerPage={setPerPage} + onChangePerPage={handlePerPageChange} /> )} {isWorkspaceEmpty && ( diff --git a/frontend/src/pages/organization/SecretScanningPage/SecretScanningPage.tsx b/frontend/src/pages/organization/SecretScanningPage/SecretScanningPage.tsx index 23cdcf800..f0ad03677 100644 --- a/frontend/src/pages/organization/SecretScanningPage/SecretScanningPage.tsx +++ b/frontend/src/pages/organization/SecretScanningPage/SecretScanningPage.tsx @@ -13,6 +13,11 @@ import { useOrganization, useServerConfig } from "@app/context"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { withPermission } from "@app/hoc"; import { usePagination, usePopUp } from "@app/hooks"; import { @@ -28,8 +33,6 @@ import { SecretScanningFilter } from "./components/SecretScanningFilters"; import { SecretScanningFilterFormData, secretScanningFilterFormSchema } from "./components/types"; import { SecretScanningLogsTable } from "./components"; -const PER_PAGE_INIT = 25; - export const SecretScanningPage = withPermission( () => { const queryParams = useSearch({ @@ -49,9 +52,14 @@ export const SecretScanningPage = withPermission( const { offset, limit, orderBy, setPage, perPage, page, setPerPage } = usePagination( SecretScanningOrderBy.CreatedAt, - { initPerPage: PER_PAGE_INIT } + { initPerPage: getUserTablePreference("secretScanningTable", PreferenceKey.PerPage, 20) } ); + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("secretScanningTable", PreferenceKey.PerPage, newPerPage); + }; + const repositoryNames = watch("repositoryNames"); const resolvedStatus = watch("resolved"); @@ -180,7 +188,7 @@ export const SecretScanningPage = withPermission(
)}
-
+
{integrationEnabled && (
)} - {!isPending && - risksData?.totalCount !== undefined && - risksData.totalCount >= PER_PAGE_INIT && ( - setPage(newPage)} - onChangePerPage={(newPerPage) => setPerPage(newPerPage)} - /> - )} + {!isPending && risksData?.totalCount !== undefined && risksData.totalCount >= 10 && ( + setPage(newPage)} + onChangePerPage={handlePerPageChange} + /> + )}
diff --git a/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx index 21c440957..ac8685192 100644 --- a/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx +++ b/frontend/src/pages/organization/SsoPage/components/OrgSsoTab/OrgGeneralAuthSection.tsx @@ -129,7 +129,16 @@ export const OrgGeneralAuthSection = () => { level.

- In case of a lockout, admins can use the admin login portal at{" "} + In case of a lockout, admins can use the{" "} + + Admin Login Portal + {" "} + at{" "} { level.

- In case of a lockout, admins can use the admin login portal at{" "} + In case of a lockout, admins can use the{" "} + + Admin Login Portal + {" "} + at{" "} { offset, orderDirection, toggleOrderDirection - } = usePagination(UserGroupsOrderBy.Name, { initPerPage: 10 }); + } = usePagination(UserGroupsOrderBy.Name, { + initPerPage: getUserTablePreference("userGroupsTable", PreferenceKey.PerPage, 10) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("userGroupsTable", PreferenceKey.PerPage, newPerPage); + }; const filteredGroupMemberships = useMemo( () => @@ -119,7 +131,7 @@ export const UserGroupsTable = ({ handlePopUpOpen, orgMembership }: Props) => { page={page} perPage={perPage} onChangePage={setPage} - onChangePerPage={setPerPage} + onChangePerPage={handlePerPageChange} /> )} {!isPending && !filteredGroupMemberships?.length && ( diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserProjectsTable.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserProjectsTable.tsx index ff9ed9bbc..9e6042c03 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserProjectsTable.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserProjectsSection/UserProjectsTable.tsx @@ -22,6 +22,11 @@ import { Tr } from "@app/components/v2"; import { useOrganization } from "@app/context"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { usePagination, useResetPageHelper } from "@app/hooks"; import { useGetOrgMembershipProjectMemberships } from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; @@ -54,7 +59,14 @@ export const UserProjectsTable = ({ membershipId, handlePopUpOpen }: Props) => { offset, orderDirection, toggleOrderDirection - } = usePagination(UserProjectsOrderBy.Name, { initPerPage: 10 }); + } = usePagination(UserProjectsOrderBy.Name, { + initPerPage: getUserTablePreference("userProjectsTable", PreferenceKey.PerPage, 10) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("userProjectsTable", PreferenceKey.PerPage, newPerPage); + }; const { data: projectMemberships = [], isPending } = useGetOrgMembershipProjectMemberships( orgId, @@ -136,7 +148,7 @@ export const UserProjectsTable = ({ membershipId, handlePopUpOpen }: Props) => { page={page} perPage={perPage} onChangePage={setPage} - onChangePerPage={setPerPage} + onChangePerPage={handlePerPageChange} /> )} {!isPending && !filteredProjectMemberships?.length && ( diff --git a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx index 24f459a93..8f06576e2 100644 --- a/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/GroupsTab/components/GroupsSection/GroupsTable.tsx @@ -27,6 +27,11 @@ import { Tr } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { usePagination, useResetPageHelper } from "@app/hooks"; import { useListWorkspaceGroups } from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; @@ -62,7 +67,14 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => { orderDirection, orderBy, toggleOrderDirection - } = usePagination(GroupsOrderBy.Name, { initPerPage: 20 }); + } = usePagination(GroupsOrderBy.Name, { + initPerPage: getUserTablePreference("projectGroupsTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("projectGroupsTable", PreferenceKey.PerPage, newPerPage); + }; const { data: groupMemberships = [], isPending } = useListWorkspaceGroups( currentWorkspace?.id || "" @@ -183,7 +195,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => { page={page} perPage={perPage} onChangePage={setPage} - onChangePerPage={setPerPage} + onChangePerPage={handlePerPageChange} /> )} {!isPending && !filteredGroupMemberships?.length && ( diff --git a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx index c9e563065..d20beb738 100644 --- a/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/IdentityTab/IdentityTab.tsx @@ -42,6 +42,11 @@ import { } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { formatProjectRoleName } from "@app/helpers/roles"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { withProjectPermission } from "@app/hoc"; import { usePagination, useResetPageHelper } from "@app/hooks"; import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api"; @@ -72,7 +77,14 @@ export const IdentityTab = withProjectPermission( perPage, page, setPerPage - } = usePagination(ProjectIdentityOrderBy.Name); + } = usePagination(ProjectIdentityOrderBy.Name, { + initPerPage: getUserTablePreference("projectIdentityTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("projectIdentityTable", PreferenceKey.PerPage, newPerPage); + }; const workspaceId = currentWorkspace?.id ?? ""; @@ -403,7 +415,7 @@ export const IdentityTab = withProjectPermission( page={page} perPage={perPage} onChangePage={(newPage) => setPage(newPage)} - onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + onChangePerPage={handlePerPageChange} /> )} {!isPending && data && data?.identityMemberships.length === 0 && ( diff --git a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersTable.tsx b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersTable.tsx index c03f7f793..c1251bef2 100644 --- a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersTable.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/MembersTable.tsx @@ -51,6 +51,11 @@ import { useWorkspace } from "@app/context"; import { formatProjectRoleName } from "@app/helpers/roles"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { usePagination, useResetPageHelper } from "@app/hooks"; import { useGetProjectRoles, useGetWorkspaceUsers } from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; @@ -101,12 +106,12 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => { setOrderDirection, toggleOrderDirection } = usePagination(MembersOrderBy.Name, { - initPerPage: parseInt(localStorage.getItem("PROJECT_MEMBERS_TABLE_PER_PAGE") || "20", 10) + initPerPage: getUserTablePreference("projectMembersTable", PreferenceKey.PerPage, 20) }); const handlePerPageChange = (newPerPage: number) => { setPerPage(newPerPage); - localStorage.setItem("PROJECT_MEMBERS_TABLE_PER_PAGE", newPerPage.toString()); + setUserTablePreference("projectMembersTable", PreferenceKey.PerPage, newPerPage); }; const { data: members = [], isPending: isMembersLoading } = useGetWorkspaceUsers( diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/IntegrationsTable.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/IntegrationsTable.tsx index a8d2cccd5..775e42c50 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/IntegrationsTable.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/NativeIntegrationsTab/IntegrationsTable.tsx @@ -32,6 +32,11 @@ import { Tooltip, Tr } from "@app/components/v2"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { usePagination, useResetPageHelper } from "@app/hooks"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { useSyncIntegration } from "@app/hooks/api/integrations/queries"; @@ -110,7 +115,14 @@ export const IntegrationsTable = ({ orderBy, setOrderDirection, setOrderBy - } = usePagination(IntegrationsOrderBy.App, { initPerPage: 20 }); + } = usePagination(IntegrationsOrderBy.App, { + initPerPage: getUserTablePreference("integrationsTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("integrationsTable", PreferenceKey.PerPage, newPerPage); + }; useEffect(() => { if (integrations?.some((integration) => integration.isSynced === false)) @@ -437,7 +449,7 @@ export const IntegrationsTable = ({ page={page} perPage={perPage} onChangePage={setPage} - onChangePerPage={setPerPage} + onChangePerPage={handlePerPageChange} /> )} {!isLoading && !filteredIntegrations?.length && ( diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx index 802e88bc1..fcd42a10f 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncsTable.tsx @@ -38,6 +38,11 @@ import { } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { @@ -119,7 +124,14 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => { orderBy, setOrderDirection, setOrderBy - } = usePagination(SecretSyncsOrderBy.Name, { initPerPage: 20 }); + } = usePagination(SecretSyncsOrderBy.Name, { + initPerPage: getUserTablePreference("secretSyncTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("secretSyncTable", PreferenceKey.PerPage, newPerPage); + }; const filteredSecretSyncs = useMemo( () => @@ -465,7 +477,7 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => { page={page} perPage={perPage} onChangePage={setPage} - onChangePerPage={setPerPage} + onChangePerPage={handlePerPageChange} /> )} {!filteredSecretSyncs?.length && ( diff --git a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx index 5f0d90f6c..c94aef0d5 100644 --- a/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx +++ b/frontend/src/pages/secret-manager/OverviewPage/OverviewPage.tsx @@ -64,6 +64,11 @@ import { useWorkspace } from "@app/context"; import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useCreateFolder, @@ -180,7 +185,14 @@ export const OverviewPage = () => { page, setPerPage, orderBy - } = usePagination(DashboardSecretsOrderBy.Name); + } = usePagination(DashboardSecretsOrderBy.Name, { + initPerPage: getUserTablePreference("secretOverviewTable", PreferenceKey.PerPage, 100) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("secretOverviewTable", PreferenceKey.PerPage, newPerPage); + }; const resetSelectedEntries = useCallback(() => { setSelectedEntries({ @@ -1416,7 +1428,7 @@ export const OverviewPage = () => { page={page} perPage={perPage} onChangePage={(newPage) => setPage(newPage)} - onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + onChangePerPage={handlePerPageChange} /> )}

diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index d28f28392..49bf1a72c 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -29,6 +29,11 @@ import { ProjectPermissionSecretActions, ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types"; +import { + getUserTablePreference, + PreferenceKey, + setUserTablePreference +} from "@app/helpers/userTablePreferences"; import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useGetImportedSecretsSingleEnv, @@ -98,7 +103,14 @@ const Page = () => { page, setPerPage, orderBy - } = usePagination(DashboardSecretsOrderBy.Name); + } = usePagination(DashboardSecretsOrderBy.Name, { + initPerPage: getUserTablePreference("secretDashboardTable", PreferenceKey.PerPage, 100) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("secretDashboardTable", PreferenceKey.PerPage, newPerPage); + }; const [snapshotId, setSnapshotId] = useState(null); const isRollbackMode = Boolean(snapshotId); @@ -558,7 +570,7 @@ const Page = () => { page={page} perPage={perPage} onChangePage={(newPage) => setPage(newPage)} - onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + onChangePerPage={handlePerPageChange} /> )} { offset, orderDirection, toggleOrderDirection - } = usePagination(TagsOrderBy.Slug, { initPerPage: 10 }); + } = usePagination(TagsOrderBy.Slug, { + initPerPage: getUserTablePreference("secretTagsTable", PreferenceKey.PerPage, 20) + }); + + const handlePerPageChange = (newPerPage: number) => { + setPerPage(newPerPage); + setUserTablePreference("secretTagsTable", PreferenceKey.PerPage, newPerPage); + }; const filteredTags = useMemo( () => @@ -151,7 +163,7 @@ export const SecretTagsTable = ({ handlePopUpOpen }: Props) => { page={page} perPage={perPage} onChangePage={setPage} - onChangePerPage={setPerPage} + onChangePerPage={handlePerPageChange} /> )} {!isPending && !filteredTags?.length && (