Merge branch 'main' of https://github.com/Infisical/infisical into feat/pagination-in-resource-overview-tables
@@ -75,7 +75,7 @@ class RailwayPublicClient {
|
|||||||
async send<T extends TRailwayResponse>(
|
async send<T extends TRailwayResponse>(
|
||||||
query: string,
|
query: string,
|
||||||
options: RailwaySendReqOptions,
|
options: RailwaySendReqOptions,
|
||||||
variables: Record<string, string | Record<string, string>> = {},
|
variables: Record<string, unknown> = {},
|
||||||
retryAttempt: number = 0
|
retryAttempt: number = 0
|
||||||
): Promise<T["data"] | undefined> {
|
): Promise<T["data"] | undefined> {
|
||||||
const body = {
|
const body = {
|
||||||
@@ -117,6 +117,25 @@ class RailwayPublicClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDeployments(
|
||||||
|
config: RailwaySendReqOptions,
|
||||||
|
variables: { input: { serviceId: string; environmentId: string }; first?: number }
|
||||||
|
) {
|
||||||
|
return this.send<TRailwayResponse<{ deployments: { edges: { node: { id: string } }[] } }>>(
|
||||||
|
`query deployments($input: DeploymentListInput!, $first: Int) { deployments(first: $first, input: $input) { edges { node { id } } } }`,
|
||||||
|
config,
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async redeployDeployment(config: RailwaySendReqOptions, variables: { input: { deploymentId: string } }) {
|
||||||
|
return this.send<TRailwayResponse<{ deploymentRedeploy: { id: string } }>>(
|
||||||
|
`mutation deploymentRedeploy($deploymentId: String!) { deploymentRedeploy(id: $deploymentId) { id } }`,
|
||||||
|
config,
|
||||||
|
{ deploymentId: variables.input.deploymentId }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getSubscriptionType(config: RailwaySendReqOptions & { projectId: string }) {
|
async getSubscriptionType(config: RailwaySendReqOptions & { projectId: string }) {
|
||||||
const res = await this.send(
|
const res = await this.send(
|
||||||
`query project($projectId: String!) { project(id: $projectId) { subscriptionType }}`,
|
`query project($projectId: String!) { project(id: $projectId) { subscriptionType }}`,
|
||||||
@@ -213,7 +232,9 @@ class RailwayPublicClient {
|
|||||||
|
|
||||||
async deleteVariable(
|
async deleteVariable(
|
||||||
config: RailwaySendReqOptions,
|
config: RailwaySendReqOptions,
|
||||||
variables: { input: { projectId: string; environmentId: string; name: string; serviceId?: string } }
|
variables: {
|
||||||
|
input: { projectId: string; environmentId: string; name: string; skipDeploys?: boolean; serviceId?: string };
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
await this.send<TRailwayResponse<{ variables: Record<string, string> }>>(
|
await this.send<TRailwayResponse<{ variables: Record<string, string> }>>(
|
||||||
`mutation variableDelete($input: VariableDeleteInput!) { variableDelete(input: $input) }`,
|
`mutation variableDelete($input: VariableDeleteInput!) { variableDelete(input: $input) }`,
|
||||||
@@ -222,6 +243,26 @@ class RailwayPublicClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async upsertCollection(
|
||||||
|
config: RailwaySendReqOptions,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
projectId: string;
|
||||||
|
environmentId: string;
|
||||||
|
variables: Record<string, string>;
|
||||||
|
skipDeploys?: boolean;
|
||||||
|
serviceId?: string;
|
||||||
|
replace?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return this.send<TRailwayResponse<boolean>>(
|
||||||
|
`mutation variableCollectionUpsert($input: VariableCollectionUpsertInput!) { variableCollectionUpsert(input: $input) }`,
|
||||||
|
config,
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async upsertVariable(
|
async upsertVariable(
|
||||||
config: RailwaySendReqOptions,
|
config: RailwaySendReqOptions,
|
||||||
variables: { input: { projectId: string; environmentId: string; name: string; value: string; serviceId?: string } }
|
variables: { input: { projectId: string; environmentId: string; name: string; value: string; serviceId?: string } }
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export const RailwaySyncFns = {
|
|||||||
async getSecrets(secretSync: TRailwaySyncWithCredentials): Promise<TSecretMap> {
|
async getSecrets(secretSync: TRailwaySyncWithCredentials): Promise<TSecretMap> {
|
||||||
try {
|
try {
|
||||||
const config = secretSync.destinationConfig;
|
const config = secretSync.destinationConfig;
|
||||||
|
const { keySchema } = secretSync.syncOptions;
|
||||||
|
const { environment } = secretSync;
|
||||||
|
|
||||||
const variables = await RailwayPublicAPI.getVariables(secretSync.connection, {
|
const variables = await RailwayPublicAPI.getVariables(secretSync.connection, {
|
||||||
projectId: config.projectId,
|
projectId: config.projectId,
|
||||||
@@ -26,6 +28,10 @@ export const RailwaySyncFns = {
|
|||||||
// eslint-disable-next-line no-continue
|
// eslint-disable-next-line no-continue
|
||||||
if (key.startsWith("RAILWAY_")) continue;
|
if (key.startsWith("RAILWAY_")) continue;
|
||||||
|
|
||||||
|
// Check if key matches the schema
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
||||||
|
|
||||||
entries[key] = {
|
entries[key] = {
|
||||||
value
|
value
|
||||||
};
|
};
|
||||||
@@ -40,60 +46,73 @@ export const RailwaySyncFns = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs secrets to Railway and redeploys the service if needed.
|
||||||
|
*
|
||||||
|
* Gets existing Railway vars, merges with new secrets (keeping Railway vars if deletion is disabled),
|
||||||
|
* then replaces every variable with the new values, if variable is not in the secretMap, it is deleted.
|
||||||
|
* If there's a service, triggers a redeploy to pick up the changes.
|
||||||
|
*/
|
||||||
async syncSecrets(secretSync: TRailwaySyncWithCredentials, secretMap: TSecretMap) {
|
async syncSecrets(secretSync: TRailwaySyncWithCredentials, secretMap: TSecretMap) {
|
||||||
const {
|
try {
|
||||||
environment,
|
const {
|
||||||
syncOptions: { disableSecretDeletion, keySchema }
|
syncOptions: { disableSecretDeletion }
|
||||||
} = secretSync;
|
} = secretSync;
|
||||||
const railwaySecrets = await this.getSecrets(secretSync);
|
const railwaySecrets = await this.getSecrets(secretSync);
|
||||||
const config = secretSync.destinationConfig;
|
const config = secretSync.destinationConfig;
|
||||||
|
|
||||||
for await (const key of Object.keys(secretMap)) {
|
const railwaySecretsMap = Object.fromEntries(
|
||||||
try {
|
Object.entries(railwaySecrets).map(([key, secret]) => [key, secret.value])
|
||||||
const existing = railwaySecrets[key];
|
);
|
||||||
|
const secretMapMap = Object.fromEntries(Object.entries(secretMap).map(([key, secret]) => [key, secret.value]));
|
||||||
|
|
||||||
if (existing === undefined || existing.value !== secretMap[key].value) {
|
const toReplace = disableSecretDeletion ? { ...railwaySecretsMap, ...secretMapMap } : secretMapMap;
|
||||||
await RailwayPublicAPI.upsertVariable(secretSync.connection, {
|
|
||||||
input: {
|
const upserted = await RailwayPublicAPI.upsertCollection(secretSync.connection, {
|
||||||
projectId: config.projectId,
|
input: {
|
||||||
environmentId: config.environmentId,
|
projectId: config.projectId,
|
||||||
serviceId: config.serviceId || undefined,
|
environmentId: config.environmentId,
|
||||||
name: key,
|
serviceId: config.serviceId || undefined,
|
||||||
value: secretMap[key].value ?? ""
|
skipDeploys: true,
|
||||||
}
|
variables: toReplace,
|
||||||
});
|
replace: true
|
||||||
}
|
}
|
||||||
} catch (error) {
|
});
|
||||||
|
|
||||||
|
if (!upserted)
|
||||||
throw new SecretSyncError({
|
throw new SecretSyncError({
|
||||||
error,
|
message: "Failed to upsert secrets to Railway"
|
||||||
secretKey: key
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disableSecretDeletion) return;
|
if (!config.serviceId) return;
|
||||||
|
|
||||||
for await (const key of Object.keys(railwaySecrets)) {
|
const latestDeployment = await RailwayPublicAPI.getDeployments(secretSync.connection, {
|
||||||
try {
|
input: {
|
||||||
// eslint-disable-next-line no-continue
|
serviceId: config.serviceId,
|
||||||
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
environmentId: config.environmentId
|
||||||
|
},
|
||||||
|
first: 1
|
||||||
|
});
|
||||||
|
|
||||||
if (!secretMap[key]) {
|
const latestDeploymentId = latestDeployment?.deployments.edges[0].node.id;
|
||||||
await RailwayPublicAPI.deleteVariable(secretSync.connection, {
|
|
||||||
input: {
|
if (!latestDeploymentId)
|
||||||
projectId: config.projectId,
|
throw new SecretSyncError({
|
||||||
environmentId: config.environmentId,
|
message: "Failed to get latest deployment from Railway"
|
||||||
serviceId: config.serviceId || undefined,
|
});
|
||||||
name: key
|
|
||||||
}
|
await RailwayPublicAPI.redeployDeployment(secretSync.connection, {
|
||||||
});
|
input: {
|
||||||
|
deploymentId: latestDeploymentId
|
||||||
}
|
}
|
||||||
} catch (error) {
|
});
|
||||||
throw new SecretSyncError({
|
} catch (error) {
|
||||||
error,
|
if (error instanceof SecretSyncError) throw error;
|
||||||
secretKey: key
|
|
||||||
});
|
throw new SecretSyncError({
|
||||||
}
|
error,
|
||||||
|
message: "Failed to sync secrets to Railway"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -101,24 +120,37 @@ export const RailwaySyncFns = {
|
|||||||
const existing = await this.getSecrets(secretSync);
|
const existing = await this.getSecrets(secretSync);
|
||||||
const config = secretSync.destinationConfig;
|
const config = secretSync.destinationConfig;
|
||||||
|
|
||||||
for await (const secret of Object.keys(existing)) {
|
// Create a new variables object excluding secrets that exist in secretMap
|
||||||
try {
|
const remainingVariables = Object.fromEntries(
|
||||||
if (secret in secretMap) {
|
Object.entries(existing)
|
||||||
await RailwayPublicAPI.deleteVariable(secretSync.connection, {
|
.filter(([key]) => !(key in secretMap))
|
||||||
input: {
|
.map(([key, secret]) => [key, secret.value])
|
||||||
projectId: config.projectId,
|
);
|
||||||
environmentId: config.environmentId,
|
|
||||||
serviceId: config.serviceId || undefined,
|
try {
|
||||||
name: secret
|
const upserted = await RailwayPublicAPI.upsertCollection(secretSync.connection, {
|
||||||
}
|
input: {
|
||||||
});
|
projectId: config.projectId,
|
||||||
|
environmentId: config.environmentId,
|
||||||
|
serviceId: config.serviceId || undefined,
|
||||||
|
skipDeploys: true,
|
||||||
|
variables: remainingVariables,
|
||||||
|
replace: true
|
||||||
}
|
}
|
||||||
} catch (error) {
|
});
|
||||||
|
|
||||||
|
if (!upserted) {
|
||||||
throw new SecretSyncError({
|
throw new SecretSyncError({
|
||||||
error,
|
message: "Failed to remove secrets from Railway"
|
||||||
secretKey: secret
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof SecretSyncError) throw error;
|
||||||
|
|
||||||
|
throw new SecretSyncError({
|
||||||
|
error,
|
||||||
|
message: "Failed to remove secrets from Railway"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -221,6 +221,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"documentation/platform/sso/auth0-oidc",
|
"documentation/platform/sso/auth0-oidc",
|
||||||
|
"documentation/platform/sso/pingone-oidc",
|
||||||
{
|
{
|
||||||
"group": "General OIDC",
|
"group": "General OIDC",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ To successfully deploy an Infisical Gateway for use, follow these steps in order
|
|||||||
<Step title="Set Up a Relay Server">
|
<Step title="Set Up a Relay Server">
|
||||||
Ensure a relay server is running and accessible before you deploy any gateways. You have two options:
|
Ensure a relay server is running and accessible before you deploy any gateways. You have two options:
|
||||||
- **Managed relay (Infisical Cloud, US/EU only):** Managed relays are only available for Infisical Cloud instances in the US and EU regions. If you are using Infisical Cloud in these regions, you can use the provided managed relay.
|
- **Managed relay (Infisical Cloud, US/EU only):** Managed relays are only available for Infisical Cloud instances in the US and EU regions. If you are using Infisical Cloud in these regions, you can use the provided managed relay.
|
||||||
- **Self-hosted relay:** For all other cases, including all self-hosted and dedicated enterprise instances of Infisical, you must deploy your own relay server. You can also choose to deploy your own relay server when using Infisical Cloud if you require reduced geographic proximity to your target resources for lower latency or to reduce network congestion. For setup instructions, see the <a href="/documentation/platform/gateways/relay-deployment">Relay Deployment Guide</a>.
|
- **Self-hosted relay:** For all other cases, including all self-hosted and dedicated enterprise instances of Infisical, you must deploy your own relay server. You can also choose to deploy your own relay server when using Infisical Cloud if you require reduced geographic proximity to your target resources for lower latency or to reduce network congestion. For setup instructions, see the [Relay Deployment Guide](/documentation/platform/gateways/relay-deployment).
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Install the Infisical CLI">
|
<Step title="Install the Infisical CLI">
|
||||||
Make sure the Infisical CLI is installed on the machine or environment where you plan to deploy the gateway. The CLI is required for gateway installation and management.
|
Make sure the Infisical CLI is installed on the machine or environment where you plan to deploy the gateway. The CLI is required for gateway installation and management.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO."
|
|||||||
<Info>
|
<Info>
|
||||||
Auth0 OIDC SSO is a paid feature. If you're using Infisical Cloud, then it is
|
Auth0 OIDC 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
|
available under the **Pro Tier**. If you're self-hosting Infisical, then you
|
||||||
should contact [email protected] to purchase an enterprise license to use
|
should contact [email protected] to purchase a self-hosted license to use
|
||||||
it.
|
it.
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ description: "Learn how to configure Auth0 OIDC for Infisical SSO."
|
|||||||
<Step title="Enable OIDC in Infisical">
|
<Step title="Enable OIDC in Infisical">
|
||||||
Enabling OIDC allows members in your organization to log into Infisical via Auth0.
|
Enabling OIDC allows members in your organization to log into Infisical via Auth0.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Enforce OIDC SSO in Infisical">
|
<Step title="Enforce OIDC SSO in Infisical">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ description: "Learn how to configure OIDC for Infisical SSO with any OIDC-compli
|
|||||||
<Info>
|
<Info>
|
||||||
OIDC SSO is a paid feature. If you're using Infisical Cloud, then it is
|
OIDC 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
|
available under the **Pro Tier**. If you're self-hosting Infisical, then you
|
||||||
should contact [email protected] to purchase an enterprise license to use
|
should contact [email protected] to purchase a self-hosted license to use
|
||||||
it.
|
it.
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO."
|
|||||||
<Info>
|
<Info>
|
||||||
Keycloak OIDC SSO is a paid feature. If you're using Infisical Cloud, then it
|
Keycloak OIDC 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
|
is available under the **Pro Tier**. If you're self-hosting Infisical, then
|
||||||
you should contact [email protected] to purchase an enterprise license to
|
you should contact [email protected] to purchase a self-hosted license to
|
||||||
use it.
|
use it.
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ description: "Learn how to configure Keycloak OIDC for Infisical SSO."
|
|||||||
<Step title="Enable OIDC SSO in Infisical">
|
<Step title="Enable OIDC SSO in Infisical">
|
||||||
Enabling OIDC SSO allows members in your organization to log into Infisical via Keycloak.
|
Enabling OIDC SSO allows members in your organization to log into Infisical via Keycloak.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Enforce OIDC SSO in Infisical">
|
<Step title="Enforce OIDC SSO in Infisical">
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
title: "PingOne OIDC"
|
||||||
|
description: "Learn how to configure PingOne OIDC for Infisical SSO."
|
||||||
|
---
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
PingOne OIDC 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 [email protected] to purchase a self-hosted license to use
|
||||||
|
it.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Setup application in PingOne">
|
||||||
|
1.1. From the Application's Page, create a new OIDC Web App application.
|
||||||
|

|
||||||
|
|
||||||
|
1.2. Enable the application by pressing the "Enable" toggle.
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
1.3. In the Application "Configuration" tab, press the "Edit" pencil icon to configure the application callback URI.
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
1.4 Set the Redirect URL to `https://app.infisical.com/api/v1/sso/oidc/callback` and press the "Save" button.
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
If you're self-hosting Infisical, then you will want to replace https://app.infisical.com with your own domain.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
|
||||||
|
1.5 After configuring the redirect URL, go to the "Attribute Mappings" tab and press the "Edit" pencil icon to configure the attribute mappings.
|
||||||
|

|
||||||
|
|
||||||
|
1.6 Map the following attributes:
|
||||||
|
- `email` -> `Email Address`
|
||||||
|
- `name` -> `Username`
|
||||||
|

|
||||||
|
|
||||||
|
Once done, press the "Save" button.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Retrieve Identity Provider (IdP) Information from PingOne">
|
||||||
|
2.1. Open the "Overview" tab and copy the **Client ID** and **Client Secret**.
|
||||||
|

|
||||||
|
|
||||||
|
2.2. Still in the "Overview" tab, scroll down to the Connection Details section and retrieve the **OIDC Discovery Endpoint**.
|
||||||
|

|
||||||
|
|
||||||
|
Keep these values handy as we will need them in the next steps.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Finish configuring OIDC in Infisical">
|
||||||
|
3.1. Back in Infisical, head to the **Single Sign-On (SSO)** page and select the **General** tab. Click **Connect** for **OIDC**.
|
||||||
|

|
||||||
|
|
||||||
|
3.2. For configuration type, select **Discovery URL**. Then, set **Discovery Document URL**, **Client ID**, and **Client Secret** from step 2.1 and 2.2.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Currently, the following JWT signature algorithms are supported: RS256, RS512, HS256, and EdDSA
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
Once you've done that, press **Update** to complete the required configuration.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Enable OIDC in Infisical">
|
||||||
|
Enabling OIDC allows members in your organization to log into Infisical via PingOne
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Enforce OIDC SSO in Infisical">
|
||||||
|
Enforcing OIDC SSO ensures that members in your organization can only access Infisical
|
||||||
|
by logging into the organization via PingOne.
|
||||||
|
|
||||||
|
To enforce OIDC SSO, you're required to test out the OpenID connection by successfully authenticating at least one PingOne user with Infisical.
|
||||||
|
Once you've completed this requirement, you can toggle the **Enforce OIDC SSO** button to enforce OIDC SSO.
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
We recommend ensuring that your account is provisioned using the application in PingOne
|
||||||
|
prior to enforcing OIDC SSO to prevent any unintended issues.
|
||||||
|
</Warning>
|
||||||
|
<Info>
|
||||||
|
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.
|
||||||
|
</Info>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
If you are only using one organization on your Infisical instance, you can configure a default organization in the [Server Admin Console](../admin-panel/server-admin#default-organization) to expedite OIDC login.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
If you're configuring OIDC SSO on a self-hosted instance of Infisical, make
|
||||||
|
sure to set the `AUTH_SECRET` and `SITE_URL` environment variable for it to
|
||||||
|
work:
|
||||||
|
<div class="height:1px;"/>
|
||||||
|
- `AUTH_SECRET`: A secret key used for signing and verifying JWT. This
|
||||||
|
can be a random 32-byte base64 string generated with `openssl rand -base64
|
||||||
|
32`.
|
||||||
|
<div class="height:1px;"/>
|
||||||
|
- `SITE_URL`: The absolute URL of your self-hosted instance of Infisical including the protocol (e.g. https://app.infisical.com)
|
||||||
|
</Note>
|
||||||
|
Before Width: | Height: | Size: 797 KiB After Width: | Height: | Size: 797 KiB |
|
Before Width: | Height: | Size: 797 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -1,9 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import axios from "axios";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { useSendVerificationEmail } from "@app/hooks/api";
|
import { useSendVerificationEmail } from "@app/hooks/api";
|
||||||
|
|
||||||
import { Button, Input } from "../v2";
|
import { Button, Input } from "../v2";
|
||||||
@@ -35,11 +34,10 @@ export default function EnterEmailStep({
|
|||||||
* Verifies if the entered email "looks" correct
|
* Verifies if the entered email "looks" correct
|
||||||
*/
|
*/
|
||||||
const emailCheck = async () => {
|
const emailCheck = async () => {
|
||||||
|
const isValid = z.string().email().safeParse(email);
|
||||||
|
|
||||||
let emailCheckBool = false;
|
let emailCheckBool = false;
|
||||||
if (!email) {
|
if (!isValid.success) {
|
||||||
setEmailError(true);
|
|
||||||
emailCheckBool = true;
|
|
||||||
} else if (!email.includes("@") || !email.includes(".") || !/[a-z]/.test(email)) {
|
|
||||||
setEmailError(true);
|
setEmailError(true);
|
||||||
emailCheckBool = true;
|
emailCheckBool = true;
|
||||||
} else {
|
} else {
|
||||||
@@ -48,19 +46,9 @@ export default function EnterEmailStep({
|
|||||||
|
|
||||||
// If everything is correct, go to the next step
|
// If everything is correct, go to the next step
|
||||||
if (!emailCheckBool) {
|
if (!emailCheckBool) {
|
||||||
try {
|
await mutateAsync({ email: email.toLowerCase() });
|
||||||
await mutateAsync({ email: email.toLowerCase() });
|
setEmail(email.toLowerCase());
|
||||||
setEmail(email.toLowerCase());
|
incrementStep();
|
||||||
incrementStep();
|
|
||||||
} catch (e) {
|
|
||||||
if (axios.isAxiosError(e)) {
|
|
||||||
const { message = "Something went wrong" } = e.response?.data as { message: string };
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export const AUDIT_LOG_STREAM_PROVIDER_MAP: Record<
|
|||||||
[LogProvider.Cribl]: { name: "Cribl", image: "Cribl.png", size: 60 },
|
[LogProvider.Cribl]: { name: "Cribl", image: "Cribl.png", size: 60 },
|
||||||
[LogProvider.Custom]: { name: "Custom", icon: faCode },
|
[LogProvider.Custom]: { name: "Custom", icon: faCode },
|
||||||
[LogProvider.Datadog]: { name: "Datadog", image: "Datadog.png" },
|
[LogProvider.Datadog]: { name: "Datadog", image: "Datadog.png" },
|
||||||
[LogProvider.Splunk]: { name: "Splunk", image: "Splunk.png", size: 65 }
|
[LogProvider.Splunk]: { name: "Splunk", image: "Splunk.png", size: 65 },
|
||||||
|
[LogProvider.QRadar]: { name: "IBM QRadar", image: "IBM.png" }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Strictly for showing to the client in the front-end
|
// Strictly for showing to the client in the front-end
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ export enum LogProvider {
|
|||||||
Cribl = "cribl",
|
Cribl = "cribl",
|
||||||
Custom = "custom",
|
Custom = "custom",
|
||||||
Datadog = "datadog",
|
Datadog = "datadog",
|
||||||
Splunk = "splunk"
|
Splunk = "splunk",
|
||||||
|
QRadar = "qradar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { TAzureProviderLogStream } from "./providers/azure-provider";
|
|||||||
import { TCriblProviderLogStream } from "./providers/cribl-provider";
|
import { TCriblProviderLogStream } from "./providers/cribl-provider";
|
||||||
import { TCustomProviderLogStream } from "./providers/custom-provider";
|
import { TCustomProviderLogStream } from "./providers/custom-provider";
|
||||||
import { TDatadogProviderLogStream } from "./providers/datadog-provider";
|
import { TDatadogProviderLogStream } from "./providers/datadog-provider";
|
||||||
|
import { TQRadarProviderLogStream } from "./providers/qradar-provider";
|
||||||
import { TSplunkProviderLogStream } from "./providers/splunk-provider";
|
import { TSplunkProviderLogStream } from "./providers/splunk-provider";
|
||||||
|
|
||||||
export type TAuditLogStream =
|
export type TAuditLogStream =
|
||||||
@@ -18,6 +19,7 @@ export type TAuditLogStreamProviderMap = {
|
|||||||
[LogProvider.Custom]: TCustomProviderLogStream;
|
[LogProvider.Custom]: TCustomProviderLogStream;
|
||||||
[LogProvider.Datadog]: TDatadogProviderLogStream;
|
[LogProvider.Datadog]: TDatadogProviderLogStream;
|
||||||
[LogProvider.Splunk]: TSplunkProviderLogStream;
|
[LogProvider.Splunk]: TSplunkProviderLogStream;
|
||||||
|
[LogProvider.QRadar]: TQRadarProviderLogStream;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateAuditLogStreamDTO = Pick<TAuditLogStream, "provider" | "credentials">;
|
export type TCreateAuditLogStreamDTO = Pick<TAuditLogStream, "provider" | "credentials">;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { LogProvider } from "../../enums";
|
||||||
|
import { TRootProviderLogStream } from "./root-provider";
|
||||||
|
|
||||||
|
export type TQRadarProviderLogStream = TRootProviderLogStream & {
|
||||||
|
provider: LogProvider.QRadar;
|
||||||
|
// credentials: {};
|
||||||
|
};
|
||||||
@@ -2,9 +2,10 @@ import { useMemo } from "react";
|
|||||||
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { EmptyState, Spinner } from "@app/components/v2";
|
import { EmptyState, Spinner } from "@app/components/v2";
|
||||||
import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams";
|
import { AUDIT_LOG_STREAM_PROVIDER_MAP } from "@app/helpers/auditLogStreams";
|
||||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||||
import { useGetAuditLogStreamOptions } from "@app/hooks/api";
|
import { useGetAuditLogStreamOptions } from "@app/hooks/api";
|
||||||
import { LogProvider } from "@app/hooks/api/auditLogStreams/enums";
|
import { LogProvider } from "@app/hooks/api/auditLogStreams/enums";
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ type Props = {
|
|||||||
// TODO: When we have more than 1 page of providers, uncomment the search components
|
// TODO: When we have more than 1 page of providers, uncomment the search components
|
||||||
|
|
||||||
export const LogStreamProviderSelect = ({ onSelect }: Props) => {
|
export const LogStreamProviderSelect = ({ onSelect }: Props) => {
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"]);
|
||||||
|
|
||||||
const { isPending, data: logStreamOptions } = useGetAuditLogStreamOptions();
|
const { isPending, data: logStreamOptions } = useGetAuditLogStreamOptions();
|
||||||
|
|
||||||
const { search, setPage, page, perPage, offset } = usePagination("", {
|
const { search, setPage, page, perPage, offset } = usePagination("", {
|
||||||
@@ -23,7 +26,11 @@ export const LogStreamProviderSelect = ({ onSelect }: Props) => {
|
|||||||
|
|
||||||
const filteredOptions = useMemo(
|
const filteredOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(logStreamOptions || [])
|
[
|
||||||
|
...(logStreamOptions || []),
|
||||||
|
// QRadar is a planned provider
|
||||||
|
{ name: "IBM QRadar", provider: LogProvider.QRadar }
|
||||||
|
]
|
||||||
.filter(
|
.filter(
|
||||||
({ name, provider }) =>
|
({ name, provider }) =>
|
||||||
name.toLowerCase().includes(search.trim().toLowerCase()) ||
|
name.toLowerCase().includes(search.trim().toLowerCase()) ||
|
||||||
@@ -54,13 +61,6 @@ export const LogStreamProviderSelect = ({ onSelect }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{/* <Input
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
|
||||||
placeholder="Search options..."
|
|
||||||
className="bg-mineshaft-800 placeholder:text-mineshaft-400"
|
|
||||||
/> */}
|
|
||||||
<div className="grid h-[29.5rem] grid-cols-4 content-start gap-2">
|
<div className="grid h-[29.5rem] grid-cols-4 content-start gap-2">
|
||||||
{filteredOptions.slice(offset, perPage * page)?.map((option) => {
|
{filteredOptions.slice(offset, perPage * page)?.map((option) => {
|
||||||
const { image, icon, name, size = 50 } = AUDIT_LOG_STREAM_PROVIDER_MAP[option.provider];
|
const { image, icon, name, size = 50 } = AUDIT_LOG_STREAM_PROVIDER_MAP[option.provider];
|
||||||
@@ -68,7 +68,13 @@ export const LogStreamProviderSelect = ({ onSelect }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSelect(option.provider)}
|
onClick={() => {
|
||||||
|
if (option.provider === LogProvider.QRadar) {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
} else {
|
||||||
|
onSelect(option.provider);
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={`group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 ${option.provider === LogProvider.Custom ? "bg-mineshaft-700/30 hover:bg-mineshaft-600/30" : "bg-mineshaft-700 hover:bg-mineshaft-600"} p-4 duration-200`}
|
className={`group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 ${option.provider === LogProvider.Custom ? "bg-mineshaft-700/30 hover:bg-mineshaft-600/30" : "bg-mineshaft-700 hover:bg-mineshaft-600"} p-4 duration-200`}
|
||||||
>
|
>
|
||||||
{image && (
|
{image && (
|
||||||
@@ -100,56 +106,11 @@ export const LogStreamProviderSelect = ({ onSelect }: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* {Boolean(filteredOptions.length) && (
|
<UpgradePlanModal
|
||||||
<Pagination
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
startAdornment={
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
<Tooltip
|
text="This audit log stream provider requires an enterprise license."
|
||||||
side="bottom"
|
/>
|
||||||
className="max-w-sm py-4"
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
<p className="mb-2">Infisical is constantly adding support for more providers.</p>
|
|
||||||
<p>
|
|
||||||
{`If you don't see the third-party
|
|
||||||
provider you're looking for,`}{" "}
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
className="underline hover:text-mineshaft-300"
|
|
||||||
href="https://infisical.com/slack"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
let us know on Slack
|
|
||||||
</a>{" "}
|
|
||||||
or{" "}
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
className="underline hover:text-mineshaft-300"
|
|
||||||
href="https://github.com/Infisical/infisical/discussions"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
make a request on GitHub
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="-ml-3 flex items-center gap-1.5 text-mineshaft-400">
|
|
||||||
<span className="text-xs">
|
|
||||||
Don't see the third-party provider you're looking for?
|
|
||||||
</span>
|
|
||||||
<FontAwesomeIcon size="xs" icon={faInfoCircle} />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
count={filteredOptions.length}
|
|
||||||
page={page}
|
|
||||||
perPage={perPage}
|
|
||||||
onChangePage={setPage}
|
|
||||||
onChangePerPage={setPerPage}
|
|
||||||
perPageList={[16]}
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||