Compare commits

...

59 Commits

Author SHA1 Message Date
x032205
7884f312cd feat(docs): Okta App Connection + Secret Rotation 2025-07-19 02:29:50 -04:00
x032205
de2df991d7 Review fixes 2025-07-19 01:40:33 -04:00
x032205
38b9d1f5a5 Merge branch 'main' into ENG-3139 2025-07-19 01:20:14 -04:00
x032205
b7b059bb50 Merge pull request #4117 from Infisical/ENG-3259
feat(app-connection): Gateway support for SQL App Connections + Secret Rotations
2025-07-18 16:59:20 -04:00
Scott Wilson
f3a8e30548 improvement: allow null for non-supported gatewayId 2025-07-18 13:40:42 -07:00
Scott Wilson
b0c93e5c4c Merge pull request #4012 from Infisical/commit-ui-overhaul
improvements(frontend): Revise commit history and commit details UI
2025-07-18 13:24:30 -07:00
x032205
4ab0da6b03 Fix type stuff 2025-07-18 16:22:51 -04:00
carlosmonastyrski
9674b71df8 Merge pull request #4114 from Infisical/fix/improveLicenseApiCache
Removed cloud plan cache as it is handled on the license api
2025-07-18 16:36:32 -03:00
x032205
b7d7b555b2 Only allow gateway for supported connections 2025-07-18 14:42:19 -04:00
Scott Wilson
954ca58e15 chore: revert license 2025-07-18 10:04:57 -07:00
Sid
e4a28ab0f4 fix: render sync fns (#4196)
* fix: improve render-sync fns

* fix: remove double delete
2025-07-18 19:28:03 +05:30
Carlos Monastyrski
4ab8d680c4 Adjust commit diff field highlights to use container width as minimum width 2025-07-18 10:46:21 -03:00
Sid
a3b0d86996 fix: github radar app connection errors and clear aod cookie on signout (#4188)
* fix: clear aod cookie on sign out

* fix: propogate github radar connection errors properly

* chore: add `aod` comment
2025-07-18 13:09:54 +05:30
x032205
1baa40ac8e feat(secret-rotation): Okta Client Secret Rotation 2025-07-18 01:23:37 -04:00
Scott Wilson
0080d5f291 improvement: remove test css 2025-07-17 19:57:05 -07:00
x032205
d321f6386d Lint 2025-07-17 22:53:05 -04:00
x032205
a99e7e24cc feat(app-connection): Okta App Connection 2025-07-17 22:52:56 -04:00
Scott Wilson
a276d27451 improvement: address feedback 2025-07-17 19:51:14 -07:00
Scott Wilson
cec15d6d51 improvement: address feedback 2025-07-17 17:40:18 -07:00
x032205
007e10d409 Remove console log 2025-07-17 20:29:48 -04:00
x032205
a8b448be0f Swap gateway to outer layer 2025-07-17 20:25:10 -04:00
Sid
bc98c42c79 feat(ENG-3247): add auth origin domain cookie on token creation (#4187)
* feat(ENG-3247): add auth origin domain cookie to multiple routers and update Nginx config
2025-07-18 01:18:25 +05:30
x032205
e6bfb6ce2b Merge branch 'main' into ENG-3259 2025-07-17 15:41:19 -04:00
carlosmonastyrski
1c20e4fef0 Merge pull request #4164 from Infisical/fix/listSecretsThrowOnReadPermissionError
Throw when user does not have permission to read the referenced secret value
2025-07-17 14:51:27 -03:00
Carlos Monastyrski
b560cdb0f8 Improve reference error message 2025-07-17 11:56:27 -03:00
carlosmonastyrski
144143b43a Merge pull request #4184 from Infisical/fix/cliExportFileFlag
Updated CLI export doc to document the new --output-file behavior
2025-07-17 11:12:33 -03:00
carlosmonastyrski
b9a05688cd Merge pull request #4185 from Infisical/fix/pkiImportCertToCaIssueWithDn
On importCertToCa use serialNumber instead of dn to get the parentCa
2025-07-17 10:42:01 -03:00
Carlos Monastyrski
c06c6c6c61 On importCertToCa use serialNumber instead of dn to get the parentCa 2025-07-17 10:28:31 -03:00
Carlos Monastyrski
350afee45e Updated cli export doc 2025-07-17 10:00:40 -03:00
Sid
5ae18a691d fix: verify response type (#4182)
Co-authored-by: sidwebworks <xodeveloper@gmail.com>
2025-07-17 17:59:49 +05:30
Carlos Monastyrski
8187b1da91 Updated CLI export doc to document the new --output-file behavior 2025-07-17 06:58:34 -03:00
Carlos Monastyrski
0174d36136 Throw a meaningful error message when secret reference process fails 2025-07-16 22:41:08 -03:00
Carlos Monastyrski
968d7420c6 Fix typo 2025-07-16 21:58:07 -03:00
Scott Wilson
fd761df8e5 Merge pull request #4178 from Infisical/access-request-env-view
improvement(access-requests): add access requests to single env view + general UI improvements
2025-07-16 16:25:44 -07:00
Scott Wilson
61ca617616 improvement: address feedback 2025-07-16 16:20:10 -07:00
Daniel Hougaard
6ce6c276cd Merge pull request #4180 from Infisical/daniel/tls-auth-docs
docs: document use of port 8433 for TLS certificate auth
2025-07-17 00:45:08 +04:00
Daniel Hougaard
32b2f7b0fe fix typo 2025-07-17 00:20:02 +04:00
Daniel Hougaard
4c2823c480 Update login.mdx 2025-07-17 00:09:56 +04:00
Daniel Hougaard
60438694e4 Update tls-cert-auth.mdx 2025-07-17 00:08:34 +04:00
Maidul Islam
fdaf8f9a87 Merge pull request #4179 from Infisical/doc/added-section-about-sales-approval-design-doc
doc: added section about sales approval
2025-07-16 16:07:36 -04:00
Scott Wilson
3fe41f81fe improvement: address feedback 2025-07-16 12:52:05 -07:00
Sid
c1798d37be fix: propogate Github app connection errors to the client properly (#4177)
* fix: propogate github errors to the client properly
2025-07-17 01:14:06 +05:30
Sheen Capadngan
01c6d3192d doc: added section about sales approval 2025-07-17 03:31:58 +08:00
Scott Wilson
621bfe3e60 chore: revert license 2025-07-16 12:17:43 -07:00
Scott Wilson
67ec00d46b feature: add access requests to single env view, with general UI improvements 2025-07-16 12:16:13 -07:00
x032205
d6c2789d46 Merge pull request #4176 from Infisical/ENG-3154
Make certificate collection required
2025-07-16 14:29:42 -04:00
carlosmonastyrski
58ba0c8ed4 Merge pull request #4175 from Infisical/fix/samlNotVerifiedEmailFix
Add isEmailVerified to isUserCompleted flag on samlLogin
2025-07-16 15:23:52 -03:00
x032205
f38c574030 Address review 2025-07-16 14:01:55 -04:00
x032205
c330d8ca8a Make certificate collection required 2025-07-16 13:53:52 -04:00
Carlos Monastyrski
59c0f1ff08 List secrets, throw when user does not have permission to read the referenced secret value 2025-07-16 09:14:06 -03:00
x032205
cd84d57025 Lint fixes 2025-07-16 03:19:37 -04:00
x032205
19cb220107 A few tweaks 2025-07-16 03:05:32 -04:00
x032205
fce6738562 Make sql secret rotation use gateway 2025-07-16 02:24:16 -04:00
x032205
aab204a68a feat(app-connection): Gateway support for SQL connections 2025-07-16 01:33:22 -04:00
Scott Wilson
49afaa4d2d improvements: address feedback 2025-07-15 20:03:45 -07:00
Carlos Monastyrski
2f9baee210 Removed cloud plan cache as it is handled on the license api 2025-07-15 18:21:30 -03:00
Scott Wilson
bd7947c04e improvements: missed changes 2025-07-14 14:25:33 -07:00
Scott Wilson
7ff8a19518 improvement: address feedback 2025-07-14 14:08:30 -07:00
Scott Wilson
221de8beb4 improvements: revise commit history and commit details UI 2025-07-11 20:07:53 -07:00
143 changed files with 3055 additions and 658 deletions

View File

@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.AppConnection, "gatewayId"))) {
await knex.schema.alterTable(TableName.AppConnection, (t) => {
t.uuid("gatewayId").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.AppConnection, "gatewayId")) {
await knex.schema.alterTable(TableName.AppConnection, (t) => {
t.dropColumn("gatewayId");
});
}
}

View File

@@ -20,7 +20,8 @@ export const AppConnectionsSchema = z.object({
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
isPlatformManagedCredentials: z.boolean().default(false).nullable().optional()
isPlatformManagedCredentials: z.boolean().default(false).nullable().optional(),
gatewayId: z.string().uuid().nullable().optional()
});
export type TAppConnections = z.infer<typeof AppConnectionsSchema>;

View File

@@ -6,6 +6,7 @@ import { registerAzureClientSecretRotationRouter } from "./azure-client-secret-r
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rotation-router";
import { registerOktaClientSecretRotationRouter } from "./okta-client-secret-rotation-router";
import { registerOracleDBCredentialsRotationRouter } from "./oracledb-credentials-rotation-router";
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
@@ -22,5 +23,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
[SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter,
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter,
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
[SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter
};

View File

@@ -0,0 +1,19 @@
import {
CreateOktaClientSecretRotationSchema,
OktaClientSecretRotationGeneratedCredentialsSchema,
OktaClientSecretRotationSchema,
UpdateOktaClientSecretRotationSchema
} from "@app/ee/services/secret-rotation-v2/okta-client-secret";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
export const registerOktaClientSecretRotationRouter = async (server: FastifyZodProvider) =>
registerSecretRotationEndpoints({
type: SecretRotation.OktaClientSecret,
server,
responseSchema: OktaClientSecretRotationSchema,
createSchema: CreateOktaClientSecretRotationSchema,
updateSchema: UpdateOktaClientSecretRotationSchema,
generatedCredentialsSchema: OktaClientSecretRotationGeneratedCredentialsSchema
});

View File

@@ -7,6 +7,7 @@ import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials";
import { OktaClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
import { OracleDBCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
@@ -23,7 +24,8 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
Auth0ClientSecretRotationListItemSchema,
AzureClientSecretRotationListItemSchema,
AwsIamUserSecretRotationListItemSchema,
LdapPasswordRotationListItemSchema
LdapPasswordRotationListItemSchema,
OktaClientSecretRotationListItemSchema
]);
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {

View File

@@ -45,7 +45,10 @@ export const ValidateOracleDBConnectionCredentialsSchema = z.discriminatedUnion(
]);
export const CreateOracleDBConnectionSchema = ValidateOracleDBConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.OracleDB, { supportsPlatformManagedCredentials: true })
GenericCreateAppConnectionFieldsSchema(AppConnection.OracleDB, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const UpdateOracleDBConnectionSchema = z
@@ -54,7 +57,12 @@ export const UpdateOracleDBConnectionSchema = z
AppConnections.UPDATE(AppConnection.OracleDB).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OracleDB, { supportsPlatformManagedCredentials: true }));
.and(
GenericUpdateAppConnectionFieldsSchema(AppConnection.OracleDB, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const OracleDBConnectionListItemSchema = z.object({
name: z.literal("OracleDB"),

View File

@@ -8,7 +8,6 @@ import { ForbiddenError } from "@casl/ability";
import { CronJob } from "cron";
import { Knex } from "knex";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { verifyOfflineLicense } from "@app/lib/crypto";
import { NotFoundError } from "@app/lib/errors";
@@ -47,7 +46,6 @@ type TLicenseServiceFactoryDep = {
orgDAL: Pick<TOrgDALFactory, "findOrgById" | "countAllOrgMembers">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseDAL: TLicenseDALFactory;
keyStore: Pick<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem">;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
projectDAL: TProjectDALFactory;
};
@@ -57,14 +55,10 @@ export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login";
const LICENSE_SERVER_CLOUD_PLAN_TTL = 5 * 60; // 5 mins
const FEATURE_CACHE_KEY = (orgId: string) => `infisical-cloud-plan-${orgId}`;
export const licenseServiceFactory = ({
orgDAL,
permissionService,
licenseDAL,
keyStore,
identityOrgMembershipDAL,
projectDAL
}: TLicenseServiceFactoryDep) => {
@@ -178,12 +172,6 @@ export const licenseServiceFactory = ({
logger.info(`getPlan: attempting to fetch plan for [orgId=${orgId}] [projectId=${projectId}]`);
try {
if (instanceType === InstanceType.Cloud) {
const cachedPlan = await keyStore.getItem(FEATURE_CACHE_KEY(orgId));
if (cachedPlan) {
logger.info(`getPlan: plan fetched from cache [orgId=${orgId}] [projectId=${projectId}]`);
return JSON.parse(cachedPlan) as TFeatureSet;
}
const org = await orgDAL.findOrgById(orgId);
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
const {
@@ -199,23 +187,12 @@ export const licenseServiceFactory = ({
const identityUsed = await licenseDAL.countOrgUsersAndIdentities(orgId);
currentPlan.identitiesUsed = identityUsed;
await keyStore.setItemWithExpiry(
FEATURE_CACHE_KEY(org.id),
LICENSE_SERVER_CLOUD_PLAN_TTL,
JSON.stringify(currentPlan)
);
return currentPlan;
}
} catch (error) {
logger.error(
error,
`getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`
);
await keyStore.setItemWithExpiry(
FEATURE_CACHE_KEY(orgId),
LICENSE_SERVER_CLOUD_PLAN_TTL,
JSON.stringify(onPremFeatures)
`getPlan: encountered an error when fetching plan [orgId=${orgId}] [projectId=${projectId}] [error]`
);
return onPremFeatures;
} finally {
@@ -226,7 +203,6 @@ export const licenseServiceFactory = ({
const refreshPlan = async (orgId: string) => {
if (instanceType === InstanceType.Cloud) {
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
await getPlan(orgId);
}
};
@@ -264,7 +240,6 @@ export const licenseServiceFactory = ({
quantityIdentities
});
}
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
} else if (instanceType === InstanceType.EnterpriseOnPrem) {
const usedSeats = await licenseDAL.countOfOrgMembers(null, tx);
const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx);
@@ -328,7 +303,6 @@ export const licenseServiceFactory = ({
`/api/license-server/v1/customers/${organization.customerId}/session/trial`,
{ success_url }
);
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
return { url };
};
@@ -705,10 +679,6 @@ export const licenseServiceFactory = ({
return licenses;
};
const invalidateGetPlan = async (orgId: string) => {
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
};
return {
generateOrgCustomerId,
removeOrgCustomer,
@@ -723,7 +693,6 @@ export const licenseServiceFactory = ({
return onPremFeatures;
},
getPlan,
invalidateGetPlan,
updateSubscriptionOrgMemberCount,
refreshPlan,
getOrgPlan,

View File

@@ -0,0 +1,3 @@
export * from "./okta-client-secret-rotation-constants";
export * from "./okta-client-secret-rotation-schemas";
export * from "./okta-client-secret-rotation-types";

View File

@@ -0,0 +1,15 @@
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
name: "Okta Client Secret",
type: SecretRotation.OktaClientSecret,
connection: AppConnection.Okta,
template: {
secretsMapping: {
clientId: "OKTA_CLIENT_ID",
clientSecret: "OKTA_CLIENT_SECRET"
}
}
};

View File

@@ -0,0 +1,273 @@
/* eslint-disable no-await-in-loop */
import { AxiosError } from "axios";
import {
TRotationFactory,
TRotationFactoryGetSecretsPayload,
TRotationFactoryIssueCredentials,
TRotationFactoryRevokeCredentials,
TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { request } from "@app/lib/config/request";
import { delay as delayMs } from "@app/lib/delay";
import { BadRequestError } from "@app/lib/errors";
import { getOktaInstanceUrl } from "@app/services/app-connection/okta";
import {
TOktaClientSecret,
TOktaClientSecretRotationGeneratedCredentials,
TOktaClientSecretRotationWithConnection
} from "./okta-client-secret-rotation-types";
type OktaErrorResponse = { errorCode: string; errorSummary: string; errorCauses?: { errorSummary: string }[] };
const isOktaErrorResponse = (data: unknown): data is OktaErrorResponse => {
return (
typeof data === "object" &&
data !== null &&
"errorSummary" in data &&
typeof (data as OktaErrorResponse).errorSummary === "string"
);
};
const createErrorMessage = (error: unknown) => {
if (error instanceof AxiosError) {
if (error.response?.data && isOktaErrorResponse(error.response.data)) {
const oktaError = error.response.data;
if (oktaError.errorCauses && oktaError.errorCauses.length > 0) {
return oktaError.errorCauses[0].errorSummary;
}
return oktaError.errorSummary;
}
if (error.message) {
return error.message;
}
}
return "Unknown error";
};
// Delay between each revocation call in revokeCredentials
const DELAY_MS = 1000;
export const oktaClientSecretRotationFactory: TRotationFactory<
TOktaClientSecretRotationWithConnection,
TOktaClientSecretRotationGeneratedCredentials
> = (secretRotation) => {
const {
connection,
parameters: { clientId },
secretsMapping
} = secretRotation;
/**
* Creates a new client secret for the Okta app.
*/
const $rotateClientSecret = async () => {
const instanceUrl = await getOktaInstanceUrl(connection);
try {
const { data } = await request.post<TOktaClientSecret>(
`${instanceUrl}/api/v1/apps/${clientId}/credentials/secrets`,
{},
{
headers: {
Accept: "application/json",
Authorization: `SSWS ${connection.credentials.apiToken}`
}
}
);
if (!data.client_secret || !data.id) {
throw new Error("Invalid response from Okta: missing 'client_secret' or secret 'id'.");
}
return {
clientSecret: data.client_secret,
secretId: data.id,
clientId
};
} catch (error: unknown) {
if (
error instanceof AxiosError &&
error.response?.data &&
isOktaErrorResponse(error.response.data) &&
error.response.data.errorCode === "E0000001"
) {
// Okta has a maximum of 2 secrets per app, thus we must warn the users in case they already have 2
throw new BadRequestError({
message: `Failed to add client secret to Okta app ${clientId}: You must have only a single secret for the Okta app prior to creating this secret rotation.`
});
}
throw new BadRequestError({
message: `Failed to add client secret to Okta app ${clientId}: ${createErrorMessage(error)}`
});
}
};
/**
* List client secrets.
*/
const $listClientSecrets = async () => {
const instanceUrl = await getOktaInstanceUrl(connection);
try {
const { data } = await request.get<TOktaClientSecret[]>(
`${instanceUrl}/api/v1/apps/${clientId}/credentials/secrets`,
{
headers: {
Accept: "application/json",
Authorization: `SSWS ${connection.credentials.apiToken}`
}
}
);
return data;
} catch (error: unknown) {
throw new BadRequestError({
message: `Failed to list client secrets for Okta app ${clientId}: ${createErrorMessage(error)}`
});
}
};
/**
* Checks if a credential with the given secretId exists.
*/
const credentialExists = async (secretId: string): Promise<boolean> => {
const instanceUrl = await getOktaInstanceUrl(connection);
try {
const { data } = await request.get<TOktaClientSecret>(
`${instanceUrl}/api/v1/apps/${clientId}/credentials/secrets/${secretId}`,
{
headers: {
Accept: "application/json",
Authorization: `SSWS ${connection.credentials.apiToken}`
}
}
);
return data.id === secretId;
} catch (_) {
return false;
}
};
/**
* Revokes a client secret from the Okta app using its secretId.
* First checks if the credential exists before attempting revocation.
*/
const revokeCredential = async (secretId: string) => {
// Check if credential exists before attempting revocation
const exists = await credentialExists(secretId);
if (!exists) {
return; // Credential doesn't exist, nothing to revoke
}
const instanceUrl = await getOktaInstanceUrl(connection);
try {
// First deactivate the secret
await request.post(
`${instanceUrl}/api/v1/apps/${clientId}/credentials/secrets/${secretId}/lifecycle/deactivate`,
undefined,
{
headers: {
Authorization: `SSWS ${connection.credentials.apiToken}`
}
}
);
// Then delete it
await request.delete(`${instanceUrl}/api/v1/apps/${clientId}/credentials/secrets/${secretId}`, {
headers: {
Authorization: `SSWS ${connection.credentials.apiToken}`
}
});
} catch (error: unknown) {
if (
error instanceof AxiosError &&
error.response?.data &&
isOktaErrorResponse(error.response.data) &&
error.response.data.errorCode === "E0000001"
) {
// If this is the last secret, we cannot revoke it
return;
}
throw new BadRequestError({
message: `Failed to remove client secret with secretId ${secretId} from app ${clientId}: ${createErrorMessage(error)}`
});
}
};
/**
* Issues a new set of credentials.
*/
const issueCredentials: TRotationFactoryIssueCredentials<TOktaClientSecretRotationGeneratedCredentials> = async (
callback
) => {
const credentials = await $rotateClientSecret();
return callback(credentials);
};
/**
* Revokes a list of credentials.
*/
const revokeCredentials: TRotationFactoryRevokeCredentials<TOktaClientSecretRotationGeneratedCredentials> = async (
credentials,
callback
) => {
if (!credentials?.length) return callback();
for (const { secretId } of credentials) {
await revokeCredential(secretId);
await delayMs(DELAY_MS);
}
return callback();
};
/**
* Rotates credentials by issuing new ones and revoking the old.
*/
const rotateCredentials: TRotationFactoryRotateCredentials<TOktaClientSecretRotationGeneratedCredentials> = async (
oldCredentials,
callback,
activeCredentials
) => {
// Since in Okta you can only have a maximum of 2 secrets at a time, we must delete any other secret besides the current one PRIOR to generating the second secret
if (oldCredentials?.secretId) {
await revokeCredential(oldCredentials.secretId);
} else if (activeCredentials) {
// On the first rotation oldCredentials won't be set so we must find the second secret manually
const secrets = await $listClientSecrets();
if (secrets.length > 1) {
const nonActiveSecret = secrets.find((secret) => secret.id !== activeCredentials.secretId);
if (nonActiveSecret) {
await revokeCredential(nonActiveSecret.id);
}
}
}
const newCredentials = await $rotateClientSecret();
return callback(newCredentials);
};
/**
* Maps the generated credentials into the secret payload format.
*/
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TOktaClientSecretRotationGeneratedCredentials> = ({
clientSecret
}) => [
{ key: secretsMapping.clientId, value: clientId },
{ key: secretsMapping.clientSecret, value: clientSecret }
];
return {
issueCredentials,
revokeCredentials,
rotateCredentials,
getSecretsPayload
};
};

View File

@@ -0,0 +1,68 @@
import { z } from "zod";
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
import {
BaseCreateSecretRotationSchema,
BaseSecretRotationSchema,
BaseUpdateSecretRotationSchema
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas";
import { SecretRotations } from "@app/lib/api-docs";
import { SecretNameSchema } from "@app/server/lib/schemas";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
export const OktaClientSecretRotationGeneratedCredentialsSchema = z
.object({
clientId: z.string(),
clientSecret: z.string(),
secretId: z.string()
})
.array()
.min(1)
.max(2);
const OktaClientSecretRotationParametersSchema = z.object({
clientId: z
.string()
.trim()
.min(1, "Client ID Required")
.describe(SecretRotations.PARAMETERS.OKTA_CLIENT_SECRET.clientId)
});
const OktaClientSecretRotationSecretsMappingSchema = z.object({
clientId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.OKTA_CLIENT_SECRET.clientId),
clientSecret: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.OKTA_CLIENT_SECRET.clientSecret)
});
export const OktaClientSecretRotationTemplateSchema = z.object({
secretsMapping: z.object({
clientId: z.string(),
clientSecret: z.string()
})
});
export const OktaClientSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.OktaClientSecret).extend({
type: z.literal(SecretRotation.OktaClientSecret),
parameters: OktaClientSecretRotationParametersSchema,
secretsMapping: OktaClientSecretRotationSecretsMappingSchema
});
export const CreateOktaClientSecretRotationSchema = BaseCreateSecretRotationSchema(
SecretRotation.OktaClientSecret
).extend({
parameters: OktaClientSecretRotationParametersSchema,
secretsMapping: OktaClientSecretRotationSecretsMappingSchema
});
export const UpdateOktaClientSecretRotationSchema = BaseUpdateSecretRotationSchema(
SecretRotation.OktaClientSecret
).extend({
parameters: OktaClientSecretRotationParametersSchema.optional(),
secretsMapping: OktaClientSecretRotationSecretsMappingSchema.optional()
});
export const OktaClientSecretRotationListItemSchema = z.object({
name: z.literal("Okta Client Secret"),
connection: z.literal(AppConnection.Okta),
type: z.literal(SecretRotation.OktaClientSecret),
template: OktaClientSecretRotationTemplateSchema
});

View File

@@ -0,0 +1,40 @@
import { z } from "zod";
import { TOktaConnection } from "@app/services/app-connection/okta";
import {
CreateOktaClientSecretRotationSchema,
OktaClientSecretRotationGeneratedCredentialsSchema,
OktaClientSecretRotationListItemSchema,
OktaClientSecretRotationSchema
} from "./okta-client-secret-rotation-schemas";
export type TOktaClientSecretRotation = z.infer<typeof OktaClientSecretRotationSchema>;
export type TOktaClientSecretRotationInput = z.infer<typeof CreateOktaClientSecretRotationSchema>;
export type TOktaClientSecretRotationListItem = z.infer<typeof OktaClientSecretRotationListItemSchema>;
export type TOktaClientSecretRotationWithConnection = TOktaClientSecretRotation & {
connection: TOktaConnection;
};
export type TOktaClientSecretRotationGeneratedCredentials = z.infer<
typeof OktaClientSecretRotationGeneratedCredentialsSchema
>;
export interface TOktaClientSecretRotationParameters {
clientId: string;
secretId: string;
}
export interface TOktaClientSecretRotationSecretsMapping {
clientId: string;
clientSecret: string;
secretId: string;
}
export interface TOktaClientSecret {
id: string;
client_secret: string;
}

View File

@@ -6,7 +6,8 @@ export enum SecretRotation {
Auth0ClientSecret = "auth0-client-secret",
AzureClientSecret = "azure-client-secret",
AwsIamUserSecret = "aws-iam-user-secret",
LdapPassword = "ldap-password"
LdapPassword = "ldap-password",
OktaClientSecret = "okta-client-secret"
}
export enum SecretRotationStatus {

View File

@@ -10,6 +10,7 @@ import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret"
import { LDAP_PASSWORD_ROTATION_LIST_OPTION, TLdapPasswordRotation } from "./ldap-password";
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
import { MYSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mysql-credentials";
import { OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./okta-client-secret";
import { ORACLEDB_CREDENTIALS_ROTATION_LIST_OPTION } from "./oracledb-credentials";
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
import { SecretRotation, SecretRotationStatus } from "./secret-rotation-v2-enums";
@@ -30,7 +31,8 @@ const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2List
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.AzureClientSecret]: AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION,
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
[SecretRotation.OktaClientSecret]: OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION
};
export const listSecretRotationOptions = () => {

View File

@@ -9,7 +9,8 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
[SecretRotation.AzureClientSecret]: "Azure Client Secret",
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret",
[SecretRotation.LdapPassword]: "LDAP Password"
[SecretRotation.LdapPassword]: "LDAP Password",
[SecretRotation.OktaClientSecret]: "Okta Client Secret"
};
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
@@ -20,5 +21,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnectio
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
[SecretRotation.LdapPassword]: AppConnection.LDAP
[SecretRotation.LdapPassword]: AppConnection.LDAP,
[SecretRotation.OktaClientSecret]: AppConnection.Okta
};

View File

@@ -4,6 +4,7 @@ import isEqual from "lodash.isequal";
import { SecretType, TableName } from "@app/db/schemas";
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
@@ -82,6 +83,7 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns";
import { oktaClientSecretRotationFactory } from "./okta-client-secret/okta-client-secret-rotation-fns";
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
export type TSecretRotationV2ServiceFactoryDep = {
@@ -107,6 +109,7 @@ export type TSecretRotationV2ServiceFactoryDep = {
queueService: Pick<TQueueServiceFactory, "queuePg">;
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
};
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
@@ -126,7 +129,8 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation,
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
[SecretRotation.OktaClientSecret]: oktaClientSecretRotationFactory as TRotationFactoryImplementation
};
export const secretRotationV2ServiceFactory = ({
@@ -148,7 +152,8 @@ export const secretRotationV2ServiceFactory = ({
keyStore,
queueService,
folderCommitService,
appConnectionDAL
appConnectionDAL,
gatewayService
}: TSecretRotationV2ServiceFactoryDep) => {
const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => {
const appCfg = getConfig();
@@ -461,7 +466,8 @@ export const secretRotationV2ServiceFactory = ({
rotationInterval: payload.rotationInterval
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService
kmsService,
gatewayService
);
// even though we have a db constraint we want to check before any rotation of credentials is attempted
@@ -824,7 +830,8 @@ export const secretRotationV2ServiceFactory = ({
connection: appConnection
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService
kmsService,
gatewayService
);
const generatedCredentials = await decryptSecretRotationCredentials({
@@ -907,7 +914,8 @@ export const secretRotationV2ServiceFactory = ({
connection: appConnection
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService
kmsService,
gatewayService
);
const updatedRotation = await rotationFactory.rotateCredentials(

View File

@@ -1,4 +1,5 @@
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
import { OrderByDirection } from "@app/lib/types";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
@@ -45,6 +46,13 @@ import {
TMySqlCredentialsRotationListItem,
TMySqlCredentialsRotationWithConnection
} from "./mysql-credentials";
import {
TOktaClientSecretRotation,
TOktaClientSecretRotationGeneratedCredentials,
TOktaClientSecretRotationInput,
TOktaClientSecretRotationListItem,
TOktaClientSecretRotationWithConnection
} from "./okta-client-secret";
import {
TOracleDBCredentialsRotation,
TOracleDBCredentialsRotationInput,
@@ -68,7 +76,8 @@ export type TSecretRotationV2 =
| TAuth0ClientSecretRotation
| TAzureClientSecretRotation
| TLdapPasswordRotation
| TAwsIamUserSecretRotation;
| TAwsIamUserSecretRotation
| TOktaClientSecretRotation;
export type TSecretRotationV2WithConnection =
| TPostgresCredentialsRotationWithConnection
@@ -78,14 +87,16 @@ export type TSecretRotationV2WithConnection =
| TAuth0ClientSecretRotationWithConnection
| TAzureClientSecretRotationWithConnection
| TLdapPasswordRotationWithConnection
| TAwsIamUserSecretRotationWithConnection;
| TAwsIamUserSecretRotationWithConnection
| TOktaClientSecretRotationWithConnection;
export type TSecretRotationV2GeneratedCredentials =
| TSqlCredentialsRotationGeneratedCredentials
| TAuth0ClientSecretRotationGeneratedCredentials
| TAzureClientSecretRotationGeneratedCredentials
| TLdapPasswordRotationGeneratedCredentials
| TAwsIamUserSecretRotationGeneratedCredentials;
| TAwsIamUserSecretRotationGeneratedCredentials
| TOktaClientSecretRotationGeneratedCredentials;
export type TSecretRotationV2Input =
| TPostgresCredentialsRotationInput
@@ -95,7 +106,8 @@ export type TSecretRotationV2Input =
| TAuth0ClientSecretRotationInput
| TAzureClientSecretRotationInput
| TLdapPasswordRotationInput
| TAwsIamUserSecretRotationInput;
| TAwsIamUserSecretRotationInput
| TOktaClientSecretRotationInput;
export type TSecretRotationV2ListItem =
| TPostgresCredentialsRotationListItem
@@ -105,7 +117,8 @@ export type TSecretRotationV2ListItem =
| TAuth0ClientSecretRotationListItem
| TAzureClientSecretRotationListItem
| TLdapPasswordRotationListItem
| TAwsIamUserSecretRotationListItem;
| TAwsIamUserSecretRotationListItem
| TOktaClientSecretRotationListItem;
export type TSecretRotationV2TemporaryParameters = TLdapPasswordRotationInput["temporaryParameters"] | undefined;
@@ -239,7 +252,8 @@ export type TRotationFactory<
> = (
secretRotation: T,
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
issueCredentials: TRotationFactoryIssueCredentials<C, P>;
revokeCredentials: TRotationFactoryRevokeCredentials<C>;

View File

@@ -6,6 +6,7 @@ import { AzureClientSecretRotationSchema } from "@app/ee/services/secret-rotatio
import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
import { MySqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials";
import { OktaClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret";
import { OracleDBCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/oracledb-credentials";
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
@@ -17,5 +18,6 @@ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
Auth0ClientSecretRotationSchema,
AzureClientSecretRotationSchema,
LdapPasswordRotationSchema,
AwsIamUserSecretRotationSchema
AwsIamUserSecretRotationSchema,
OktaClientSecretRotationSchema
]);

View File

@@ -1,3 +1,5 @@
import { Knex } from "knex";
import {
TRotationFactory,
TRotationFactoryGetSecretsPayload,
@@ -5,7 +7,10 @@ import {
TRotationFactoryRevokeCredentials,
TRotationFactoryRotateCredentials
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql";
import {
executeWithPotentialGateway,
SQL_CONNECTION_ALTER_LOGIN_STATEMENT
} from "@app/services/app-connection/shared/sql";
import { generatePassword } from "../utils";
import {
@@ -30,7 +35,7 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat
export const sqlCredentialsRotationFactory: TRotationFactory<
TSqlCredentialsRotationWithConnection,
TSqlCredentialsRotationGeneratedCredentials
> = (secretRotation) => {
> = (secretRotation, _appConnectionDAL, _kmsService, gatewayService) => {
const {
connection,
parameters: { username1, username2 },
@@ -38,29 +43,38 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
secretsMapping
} = secretRotation;
const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
const client = await getSqlConnectionClient({
...connection,
credentials: {
...connection.credentials,
...credentials
}
});
const executeOperation = <T>(
operation: (client: Knex) => Promise<T>,
credentialsOverride?: TSqlCredentialsRotationGeneratedCredentials[number]
) => {
const finalCredentials = {
...connection.credentials,
...credentialsOverride
};
return executeWithPotentialGateway(
{
...connection,
credentials: finalCredentials
},
gatewayService,
(client) => operation(client)
);
};
const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
try {
await client.raw("SELECT 1");
await executeOperation(async (client) => {
await client.raw("SELECT 1");
}, credentials);
} catch (error) {
throw new Error(redactPasswords(error, [credentials]));
} finally {
await client.destroy();
}
};
const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
callback
) => {
const client = await getSqlConnectionClient(connection);
// For SQL, since we get existing users, we change both their passwords
// on issue to invalidate their existing passwords
const credentialsSet = [
@@ -69,15 +83,15 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
];
try {
await client.transaction(async (tx) => {
for await (const credentials of credentialsSet) {
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
}
await executeOperation(async (client) => {
await client.transaction(async (tx) => {
for await (const credentials of credentialsSet) {
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
}
});
});
} catch (error) {
throw new Error(redactPasswords(error, credentialsSet));
} finally {
await client.destroy();
}
for await (const credentials of credentialsSet) {
@@ -91,21 +105,19 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
credentialsToRevoke,
callback
) => {
const client = await getSqlConnectionClient(connection);
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() }));
try {
await client.transaction(async (tx) => {
for await (const credentials of revokedCredentials) {
// invalidate previous passwords
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
}
await executeOperation(async (client) => {
await client.transaction(async (tx) => {
for await (const credentials of revokedCredentials) {
// invalidate previous passwords
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
}
});
});
} catch (error) {
throw new Error(redactPasswords(error, revokedCredentials));
} finally {
await client.destroy();
}
return callback();
@@ -115,17 +127,15 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
_,
callback
) => {
const client = await getSqlConnectionClient(connection);
// generate new password for the next active user
const credentials = { username: activeIndex === 0 ? username2 : username1, password: generatePassword() };
try {
await client.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
await executeOperation(async (client) => {
await client.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
});
} catch (error) {
throw new Error(redactPasswords(error, [credentials]));
} finally {
await client.destroy();
}
await $validateCredentials(credentials);

View File

@@ -2289,6 +2289,10 @@ export const AppConnections = {
SUPABASE: {
accessKey: "The Key used to access Supabase.",
instanceUrl: "The URL used to access Supabase."
},
OKTA: {
instanceUrl: "The URL used to access your Okta organization.",
apiToken: "The API token used to authenticate with Okta."
}
}
};
@@ -2594,6 +2598,9 @@ export const SecretRotations = {
AWS_IAM_USER_SECRET: {
userName: "The name of the client to rotate credentials for.",
region: "The AWS region the client is present in."
},
OKTA_CLIENT_SECRET: {
clientId: "The ID of the Okta Application to rotate the client secret for."
}
},
SECRETS_MAPPING: {
@@ -2616,6 +2623,10 @@ export const SecretRotations = {
AWS_IAM_USER_SECRET: {
accessKeyId: "The name of the secret that the access key ID will be mapped to.",
secretAccessKey: "The name of the secret that the rotated secret access key will be mapped to."
},
OKTA_CLIENT_SECRET: {
clientId: "The name of the secret that the client ID will be mapped to.",
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
}
}
};

View File

@@ -0,0 +1,43 @@
import { FastifyReply } from "fastify";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger";
/**
* `aod` (Auth Origin Domain) cookie is used to store the origin domain of the application when user was last authenticated.
* This is useful for determining the target domain for authentication redirects, especially in cloud deployments.
* It is set only in cloud mode to ensure that the cookie is shared across subdomains.
*/
export function addAuthOriginDomainCookie(res: FastifyReply) {
try {
const appCfg = getConfig();
// Only set the cookie if the app is running in cloud mode
if (!appCfg.isCloud) return;
const siteUrl = appCfg.SITE_URL!;
let domain: string;
const { hostname } = new URL(siteUrl);
const parts = hostname.split(".");
if (parts.length >= 2) {
// For `app.infisical.com` => `.infisical.com`
domain = `.${parts.slice(-2).join(".")}`;
} else {
// If somehow only "example", fallback to itself
domain = `.${hostname}`;
}
void res.setCookie("aod", siteUrl, {
domain,
path: "/",
sameSite: "strict",
httpOnly: false,
secure: appCfg.HTTPS_ENABLED
});
} catch (error) {
logger.error(error, "Failed to set auth origin domain cookie");
}
}

View File

@@ -500,7 +500,6 @@ export const registerRoutes = async (
permissionService,
orgDAL,
licenseDAL,
keyStore,
identityOrgMembershipDAL,
projectDAL
});
@@ -1706,7 +1705,9 @@ export const registerRoutes = async (
appConnectionDAL,
permissionService,
kmsService,
licenseService
licenseService,
gatewayService,
gatewayDAL
});
const secretSyncService = secretSyncServiceFactory({
@@ -1804,7 +1805,8 @@ export const registerRoutes = async (
snapshotService,
secretQueueService,
queueService,
appConnectionDAL
appConnectionDAL,
gatewayService
});
const certificateAuthorityService = certificateAuthorityServiceFactory({

View File

@@ -12,6 +12,7 @@ import { getConfig, overridableKeys } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -593,6 +594,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return {
message: "Successfully set up admin account",
user: user.user,

View File

@@ -25,12 +25,14 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
credentials: I["credentials"];
description?: string | null;
isPlatformManagedCredentials?: boolean;
gatewayId?: string | null;
}>;
updateSchema: z.ZodType<{
name?: string;
credentials?: I["credentials"];
description?: string | null;
isPlatformManagedCredentials?: boolean;
gatewayId?: string | null;
}>;
sanitizedResponseSchema: z.ZodTypeAny;
}) => {
@@ -224,10 +226,10 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, method, credentials, description, isPlatformManagedCredentials } = req.body;
const { name, method, credentials, description, isPlatformManagedCredentials, gatewayId } = req.body;
const appConnection = (await server.services.appConnection.createAppConnection(
{ name, method, app, credentials, description, isPlatformManagedCredentials },
{ name, method, app, credentials, description, isPlatformManagedCredentials, gatewayId },
req.permission
)) as T;
@@ -270,11 +272,11 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, credentials, description, isPlatformManagedCredentials } = req.body;
const { name, credentials, description, isPlatformManagedCredentials, gatewayId } = req.body;
const { connectionId } = req.params;
const appConnection = (await server.services.appConnection.updateAppConnection(
{ name, credentials, connectionId, description, isPlatformManagedCredentials },
{ name, credentials, connectionId, description, isPlatformManagedCredentials, gatewayId },
req.permission
)) as T;

View File

@@ -71,6 +71,7 @@ import {
import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap";
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import { MySqlConnectionListItemSchema, SanitizedMySqlConnectionSchema } from "@app/services/app-connection/mysql";
import { OktaConnectionListItemSchema, SanitizedOktaConnectionSchema } from "@app/services/app-connection/okta";
import {
PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema
@@ -138,7 +139,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedZabbixConnectionSchema.options,
...SanitizedRailwayConnectionSchema.options,
...SanitizedChecklyConnectionSchema.options,
...SanitizedSupabaseConnectionSchema.options
...SanitizedSupabaseConnectionSchema.options,
...SanitizedOktaConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -175,7 +177,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
ZabbixConnectionListItemSchema,
RailwayConnectionListItemSchema,
ChecklyConnectionListItemSchema,
SupabaseConnectionListItemSchema
SupabaseConnectionListItemSchema,
OktaConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -25,6 +25,7 @@ import { registerHumanitecConnectionRouter } from "./humanitec-connection-router
import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerMySqlConnectionRouter } from "./mysql-connection-router";
import { registerOktaConnectionRouter } from "./okta-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRailwayConnectionRouter } from "./railway-connection-router";
import { registerRenderConnectionRouter } from "./render-connection-router";
@@ -72,5 +73,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Zabbix]: registerZabbixConnectionRouter,
[AppConnection.Railway]: registerRailwayConnectionRouter,
[AppConnection.Checkly]: registerChecklyConnectionRouter,
[AppConnection.Supabase]: registerSupabaseConnectionRouter
[AppConnection.Supabase]: registerSupabaseConnectionRouter,
[AppConnection.Okta]: registerOktaConnectionRouter
};

View File

@@ -0,0 +1,52 @@
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 {
CreateOktaConnectionSchema,
SanitizedOktaConnectionSchema,
UpdateOktaConnectionSchema
} from "@app/services/app-connection/okta";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerOktaConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Okta,
server,
sanitizedResponseSchema: SanitizedOktaConnectionSchema,
createSchema: CreateOktaConnectionSchema,
updateSchema: UpdateOktaConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/apps`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
apps: z.object({ id: z.string(), label: z.string() }).array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const {
params: { connectionId }
} = req;
const apps = await server.services.appConnection.okta.listApps(connectionId, req.permission);
return { apps };
}
});
};

View File

@@ -42,6 +42,14 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
maxAge: 0
});
void res.cookie("aod", "", {
httpOnly: false,
path: "/",
sameSite: "lax",
secure: appCfg.HTTPS_ENABLED,
maxAge: 0
});
return { message: "Successfully logged out" };
}
});

View File

@@ -22,6 +22,7 @@ import { logger } from "@app/lib/logger";
import { ms } from "@app/lib/ms";
import { fetchGithubEmails, fetchGithubUser } from "@app/lib/requests/github";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { AuthMethod } from "@app/services/auth/auth-type";
import { OrgAuthMethod } from "@app/services/org/org-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -475,6 +476,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return {
encryptionVersion: data.user.encryptionVersion,
token: data.token.access,

View File

@@ -4,6 +4,7 @@ import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { mfaRateLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { AuthModeMfaJwtTokenPayload, AuthTokenType, MfaMethod } from "@app/services/auth/auth-type";
export const registerMfaRouter = async (server: FastifyZodProvider) => {
@@ -131,6 +132,8 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return {
...user,
token: token.access,

View File

@@ -10,6 +10,7 @@ import {
import { ApiDocsTags, ORGANIZATIONS } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
@@ -396,6 +397,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
secure: cfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return { organization, accessToken: tokens.accessToken };
}
});

View File

@@ -3,6 +3,7 @@ import { z } from "zod";
import { INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN } from "@app/lib/config/const";
import { getConfig } from "@app/lib/config/env";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
export const registerLoginRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -93,6 +94,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
secure: cfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
void res.cookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",
@@ -155,6 +158,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
void res.cookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",

View File

@@ -4,6 +4,7 @@ import { UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError } from "@app/lib/errors";
import { authRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
@@ -170,6 +171,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
return { message: "Successfully set up account", user, token: accessToken, organizationId };
}
});
@@ -239,6 +242,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
});
// TODO(akhilmhdh-pg): add telemetry service
addAuthOriginDomainCookie(res);
return { message: "Successfully set up account", user, token: accessToken };
}
});

View File

@@ -31,12 +31,16 @@ export const validateOnePassConnectionCredentials = async (config: TOnePassConne
const { apiToken } = config.credentials;
try {
await request.get(`${instanceUrl}/v1/vaults`, {
const res = await request.get(`${instanceUrl}/v1/vaults`, {
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
});
if (!Array.isArray(res.data)) {
throw new AxiosError("Invalid response from 1Password API");
}
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({

View File

@@ -32,7 +32,8 @@ export enum AppConnection {
Railway = "railway",
Bitbucket = "bitbucket",
Checkly = "checkly",
Supabase = "supabase"
Supabase = "supabase",
Okta = "okta"
}
export enum AWSRegion {

View File

@@ -5,6 +5,7 @@ import {
validateOCIConnectionCredentials
} from "@app/ee/services/app-connections/oci";
import { getOracleDBConnectionListItem, OracleDBConnectionMethod } from "@app/ee/services/app-connections/oracledb";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
@@ -91,6 +92,7 @@ import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnection
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns";
import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
import { RenderConnectionMethod } from "./render/render-connection-enums";
@@ -154,7 +156,8 @@ export const listAppConnectionOptions = () => {
getRailwayConnectionListItem(),
getBitbucketConnectionListItem(),
getChecklyConnectionListItem(),
getSupabaseConnectionListItem()
getSupabaseConnectionListItem(),
getOktaConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -201,7 +204,8 @@ export const decryptAppConnectionCredentials = async ({
};
export const validateAppConnectionCredentials = async (
appConnection: TAppConnectionConfig
appConnection: TAppConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
): Promise<TAppConnection["credentials"]> => {
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
@@ -239,10 +243,11 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator
};
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService);
};
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
@@ -278,6 +283,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case CloudflareConnectionMethod.APIToken:
case BitbucketConnectionMethod.ApiToken:
case ZabbixConnectionMethod.ApiToken:
case OktaConnectionMethod.ApiToken:
return "API Token";
case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword:
@@ -365,7 +371,8 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Railway]: platformManagedCredentialsNotSupported,
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
[AppConnection.Checkly]: platformManagedCredentialsNotSupported,
[AppConnection.Supabase]: platformManagedCredentialsNotSupported
[AppConnection.Supabase]: platformManagedCredentialsNotSupported,
[AppConnection.Okta]: platformManagedCredentialsNotSupported
};
export const enterpriseAppCheck = async (

View File

@@ -34,7 +34,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Railway]: "Railway",
[AppConnection.Bitbucket]: "Bitbucket",
[AppConnection.Checkly]: "Checkly",
[AppConnection.Supabase]: "Supabase"
[AppConnection.Supabase]: "Supabase",
[AppConnection.Okta]: "Okta"
};
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@@ -71,5 +72,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Railway]: AppConnectionPlanType.Regular,
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
[AppConnection.Checkly]: AppConnectionPlanType.Regular,
[AppConnection.Supabase]: AppConnectionPlanType.Regular
[AppConnection.Supabase]: AppConnectionPlanType.Regular,
[AppConnection.Okta]: AppConnectionPlanType.Regular
};

View File

@@ -18,7 +18,7 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
export const GenericCreateAppConnectionFieldsSchema = (
app: AppConnection,
{ supportsPlatformManagedCredentials = false }: TAppConnectionBaseConfig = {}
{ supportsPlatformManagedCredentials = false, supportsGateways = false }: TAppConnectionBaseConfig = {}
) =>
z.object({
name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name),
@@ -30,12 +30,23 @@ export const GenericCreateAppConnectionFieldsSchema = (
.describe(AppConnections.CREATE(app).description),
isPlatformManagedCredentials: supportsPlatformManagedCredentials
? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials)
: z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
: z
.literal(false, {
errorMap: () => ({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })
})
.optional()
.describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`),
gatewayId: supportsGateways
? z.string().uuid().nullish().describe("The Gateway ID to use for this connection.")
: z
.undefined({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })
.or(z.null({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }))
.describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
});
export const GenericUpdateAppConnectionFieldsSchema = (
app: AppConnection,
{ supportsPlatformManagedCredentials = false }: TAppConnectionBaseConfig = {}
{ supportsPlatformManagedCredentials = false, supportsGateways = false }: TAppConnectionBaseConfig = {}
) =>
z.object({
name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(),
@@ -47,5 +58,16 @@ export const GenericUpdateAppConnectionFieldsSchema = (
.describe(AppConnections.UPDATE(app).description),
isPlatformManagedCredentials: supportsPlatformManagedCredentials
? z.boolean().optional().describe(AppConnections.UPDATE(app).isPlatformManagedCredentials)
: z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
: z
.literal(false, {
errorMap: () => ({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })
})
.optional()
.describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`),
gatewayId: supportsGateways
? z.string().uuid().nullish().describe("The Gateway ID to use for this connection.")
: z
.undefined({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })
.or(z.null({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }))
.describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
});

View File

@@ -3,8 +3,14 @@ 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 { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb";
import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import {
OrgPermissionAppConnectionActions,
OrgPermissionGatewayActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { crypto } from "@app/lib/crypto/cryptography";
import { DatabaseErrorCode } from "@app/lib/error-codes";
@@ -73,6 +79,8 @@ import { humanitecConnectionService } from "./humanitec/humanitec-connection-ser
import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
import { ValidateOktaConnectionCredentialsSchema } from "./okta";
import { oktaConnectionService } from "./okta/okta-connection-service";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateRailwayConnectionCredentialsSchema } from "./railway";
import { railwayConnectionService } from "./railway/railway-connection-service";
@@ -96,6 +104,8 @@ export type TAppConnectionServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
gatewayDAL: Pick<TGatewayDALFactory, "find">;
};
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
@@ -134,14 +144,17 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
[AppConnection.Checkly]: ValidateChecklyConnectionCredentialsSchema,
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
appConnectionDAL,
permissionService,
kmsService,
licenseService
licenseService,
gatewayService,
gatewayDAL
}: TAppConnectionServiceFactoryDep) => {
const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => {
const { permission } = await permissionService.getOrgPermission(
@@ -222,7 +235,7 @@ export const appConnectionServiceFactory = ({
};
const createAppConnection = async (
{ method, app, credentials, ...params }: TCreateAppConnectionDTO,
{ method, app, credentials, gatewayId, ...params }: TCreateAppConnectionDTO,
actor: OrgServiceActor
) => {
const { permission } = await permissionService.getOrgPermission(
@@ -238,6 +251,20 @@ export const appConnectionServiceFactory = ({
OrgPermissionSubjects.AppConnections
);
if (gatewayId) {
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actor.orgId });
if (!gateway) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found for org`
});
}
}
await enterpriseAppCheck(
licenseService,
app,
@@ -245,12 +272,16 @@ export const appConnectionServiceFactory = ({
"Failed to create app connection due to plan restriction. Upgrade plan to access enterprise app connections."
);
const validatedCredentials = await validateAppConnectionCredentials({
app,
credentials,
method,
orgId: actor.orgId
} as TAppConnectionConfig);
const validatedCredentials = await validateAppConnectionCredentials(
{
app,
credentials,
method,
orgId: actor.orgId,
gatewayId
} as TAppConnectionConfig,
gatewayService
);
try {
const createConnection = async (connectionCredentials: TAppConnection["credentials"]) => {
@@ -265,6 +296,7 @@ export const appConnectionServiceFactory = ({
encryptedCredentials,
method,
app,
gatewayId,
...params
});
};
@@ -277,9 +309,11 @@ export const appConnectionServiceFactory = ({
app,
orgId: actor.orgId,
credentials: validatedCredentials,
method
method,
gatewayId
} as TAppConnectionConfig,
(platformCredentials) => createConnection(platformCredentials)
(platformCredentials) => createConnection(platformCredentials),
gatewayService
);
} else {
connection = await createConnection(validatedCredentials);
@@ -300,7 +334,7 @@ export const appConnectionServiceFactory = ({
};
const updateAppConnection = async (
{ connectionId, credentials, ...params }: TUpdateAppConnectionDTO,
{ connectionId, credentials, gatewayId, ...params }: TUpdateAppConnectionDTO,
actor: OrgServiceActor
) => {
const appConnection = await appConnectionDAL.findById(connectionId);
@@ -327,6 +361,22 @@ export const appConnectionServiceFactory = ({
OrgPermissionSubjects.AppConnections
);
if (gatewayId !== appConnection.gatewayId) {
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
if (gatewayId) {
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actor.orgId });
if (!gateway) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found for org`
});
}
}
}
// prevent updating credentials or management status if platform managed
if (appConnection.isPlatformManagedCredentials && (params.isPlatformManagedCredentials === false || credentials)) {
throw new BadRequestError({
@@ -351,12 +401,16 @@ export const appConnectionServiceFactory = ({
} Connection with method ${getAppConnectionMethodName(method)}`
});
updatedCredentials = await validateAppConnectionCredentials({
app,
orgId: actor.orgId,
credentials,
method
} as TAppConnectionConfig);
updatedCredentials = await validateAppConnectionCredentials(
{
app,
orgId: actor.orgId,
credentials,
method,
gatewayId
} as TAppConnectionConfig,
gatewayService
);
if (!updatedCredentials)
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
@@ -375,6 +429,7 @@ export const appConnectionServiceFactory = ({
return appConnectionDAL.updateById(connectionId, {
orgId: actor.orgId,
encryptedCredentials,
gatewayId,
...params
});
};
@@ -391,9 +446,11 @@ export const appConnectionServiceFactory = ({
app,
orgId: actor.orgId,
credentials: updatedCredentials,
method
method,
gatewayId
} as TAppConnectionConfig,
(platformCredentials) => updateConnection(platformCredentials)
(platformCredentials) => updateConnection(platformCredentials),
gatewayService
);
} else {
updatedConnection = await updateConnection(updatedCredentials);
@@ -549,6 +606,7 @@ export const appConnectionServiceFactory = ({
railway: railwayConnectionService(connectAppConnectionById),
bitbucket: bitbucketConnectionService(connectAppConnectionById),
checkly: checklyConnectionService(connectAppConnectionById),
supabase: supabaseConnectionService(connectAppConnectionById)
supabase: supabaseConnectionService(connectAppConnectionById),
okta: oktaConnectionService(connectAppConnectionById)
};
};

View File

@@ -9,6 +9,7 @@ import {
TOracleDBConnectionInput,
TValidateOracleDBConnectionCredentialsSchema
} from "@app/ee/services/app-connections/oracledb";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
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";
@@ -142,6 +143,12 @@ import {
} from "./ldap";
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { TMySqlConnection, TMySqlConnectionInput, TValidateMySqlConnectionCredentialsSchema } from "./mysql";
import {
TOktaConnection,
TOktaConnectionConfig,
TOktaConnectionInput,
TValidateOktaConnectionCredentialsSchema
} from "./okta";
import {
TPostgresConnection,
TPostgresConnectionInput,
@@ -231,6 +238,7 @@ export type TAppConnection = { id: string } & (
| TRailwayConnection
| TChecklyConnection
| TSupabaseConnection
| TOktaConnection
);
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -272,6 +280,7 @@ export type TAppConnectionInput = { id: string } & (
| TRailwayConnectionInput
| TChecklyConnectionInput
| TSupabaseConnectionInput
| TOktaConnectionInput
);
export type TSqlConnectionInput =
@@ -282,7 +291,7 @@ export type TSqlConnectionInput =
export type TCreateAppConnectionDTO = Pick<
TAppConnectionInput,
"credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials"
"credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials" | "gatewayId"
>;
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
@@ -320,7 +329,8 @@ export type TAppConnectionConfig =
| TZabbixConnectionConfig
| TRailwayConnectionConfig
| TChecklyConnectionConfig
| TSupabaseConnectionConfig;
| TSupabaseConnectionConfig
| TOktaConnectionConfig;
export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema
@@ -356,7 +366,8 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateZabbixConnectionCredentialsSchema
| TValidateRailwayConnectionCredentialsSchema
| TValidateChecklyConnectionCredentialsSchema
| TValidateSupabaseConnectionCredentialsSchema;
| TValidateSupabaseConnectionCredentialsSchema
| TValidateOktaConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = {
connectionId: string;
@@ -369,14 +380,17 @@ export type TListAwsConnectionIamUsers = {
};
export type TAppConnectionCredentialsValidator = (
appConnection: TAppConnectionConfig
appConnection: TAppConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => Promise<TAppConnection["credentials"]>;
export type TAppConnectionTransitionCredentialsToPlatform = (
appConnection: TAppConnectionConfig,
callback: (credentials: TAppConnection["credentials"]) => Promise<TAppConnectionRaw>
callback: (credentials: TAppConnection["credentials"]) => Promise<TAppConnectionRaw>,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => Promise<TAppConnectionRaw>;
export type TAppConnectionBaseConfig = {
supportsPlatformManagedCredentials?: boolean;
supportsGateways?: boolean;
};

View File

@@ -9,6 +9,7 @@ import { getAppConnectionMethodName } from "@app/services/app-connection/app-con
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { AppConnection } from "../app-connection-enums";
import { GithubTokenRespData, isGithubErrorResponse } from "../github/github-connection-fns";
import { GitHubRadarConnectionMethod } from "./github-radar-connection-enums";
import {
TGitHubRadarConnection,
@@ -71,13 +72,6 @@ export const listGitHubRadarRepositories = async (appConnection: TGitHubRadarCon
return repositories;
};
type TokenRespData = {
access_token: string;
scope: string;
token_type: string;
error?: string;
};
export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRadarConnectionConfig) => {
const { credentials, method } = config;
@@ -93,10 +87,10 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa
});
}
let tokenResp: AxiosResponse<TokenRespData>;
let tokenResp: AxiosResponse<GithubTokenRespData>;
try {
tokenResp = await request.get<TokenRespData>("https://github.com/login/oauth/access_token", {
tokenResp = await request.get<GithubTokenRespData>("https://github.com/login/oauth/access_token", {
params: {
client_id: INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID,
client_secret: INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET,
@@ -108,19 +102,27 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa
"Accept-Encoding": "application/json"
}
});
if (isGithubErrorResponse(tokenResp?.data)) {
throw new BadRequestError({
message: `Unable to validate credentials: GitHub responded with an error: ${tokenResp.data.error} - ${tokenResp.data.error_description}`
});
}
} catch (e: unknown) {
if (e instanceof BadRequestError) {
throw e;
}
throw new BadRequestError({
message: `Unable to validate connection: verify credentials`
});
}
if (tokenResp.status !== 200) {
throw new BadRequestError({
message: `Unable to validate credentials: GitHub responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.`
});
}
if (method === GitHubRadarConnectionMethod.App) {
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
const installationsResp = await request.get<{
installations: {
id: number;
@@ -149,10 +151,6 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa
}
}
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
switch (method) {
case GitHubRadarConnectionMethod.App:
return {

View File

@@ -144,13 +144,21 @@ export const getGitHubEnvironments = async (appConnection: TGitHubConnection, ow
}
};
type TokenRespData = {
access_token: string;
export type GithubTokenRespData = {
access_token?: string;
scope: string;
token_type: string;
error?: string;
};
export function isGithubErrorResponse(data: GithubTokenRespData): data is GithubTokenRespData & {
error: string;
error_description: string;
error_uri: string;
} {
return "error" in data;
}
export const validateGitHubConnectionCredentials = async (config: TGitHubConnectionConfig) => {
const { credentials, method } = config;
@@ -183,10 +191,10 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
});
}
let tokenResp: AxiosResponse<TokenRespData>;
let tokenResp: AxiosResponse<GithubTokenRespData>;
try {
tokenResp = await request.get<TokenRespData>("https://github.com/login/oauth/access_token", {
tokenResp = await request.get<GithubTokenRespData>("https://github.com/login/oauth/access_token", {
params: {
client_id: clientId,
client_secret: clientSecret,
@@ -198,7 +206,17 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
"Accept-Encoding": "application/json"
}
});
if (isGithubErrorResponse(tokenResp?.data)) {
throw new BadRequestError({
message: `Unable to validate credentials: GitHub responded with an error: ${tokenResp.data.error} - ${tokenResp.data.error_description}`
});
}
} catch (e: unknown) {
if (e instanceof BadRequestError) {
throw e;
}
throw new BadRequestError({
message: `Unable to validate connection: verify credentials`
});
@@ -211,6 +229,10 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
}
if (method === GitHubConnectionMethod.App) {
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
const installationsResp = await request.get<{
installations: {
id: number;
@@ -239,10 +261,6 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
}
}
if (!tokenResp.data.access_token) {
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
}
switch (method) {
case GitHubConnectionMethod.App:
return {

View File

@@ -49,7 +49,10 @@ export const ValidateMsSqlConnectionCredentialsSchema = z.discriminatedUnion("me
]);
export const CreateMsSqlConnectionSchema = ValidateMsSqlConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagedCredentials: true })
GenericCreateAppConnectionFieldsSchema(AppConnection.MsSql, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const UpdateMsSqlConnectionSchema = z
@@ -58,7 +61,12 @@ export const UpdateMsSqlConnectionSchema = z
AppConnections.UPDATE(AppConnection.MsSql).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagedCredentials: true }));
.and(
GenericUpdateAppConnectionFieldsSchema(AppConnection.MsSql, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const MsSqlConnectionListItemSchema = z.object({
name: z.literal("Microsoft SQL Server"),

View File

@@ -47,7 +47,10 @@ export const ValidateMySqlConnectionCredentialsSchema = z.discriminatedUnion("me
]);
export const CreateMySqlConnectionSchema = ValidateMySqlConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.MySql, { supportsPlatformManagedCredentials: true })
GenericCreateAppConnectionFieldsSchema(AppConnection.MySql, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const UpdateMySqlConnectionSchema = z
@@ -56,7 +59,12 @@ export const UpdateMySqlConnectionSchema = z
AppConnections.UPDATE(AppConnection.MySql).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.MySql, { supportsPlatformManagedCredentials: true }));
.and(
GenericUpdateAppConnectionFieldsSchema(AppConnection.MySql, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const MySqlConnectionListItemSchema = z.object({
name: z.literal("MySQL"),

View File

@@ -0,0 +1,4 @@
export * from "./okta-connection-enums";
export * from "./okta-connection-fns";
export * from "./okta-connection-schemas";
export * from "./okta-connection-types";

View File

@@ -0,0 +1,3 @@
export enum OktaConnectionMethod {
ApiToken = "api-token"
}

View File

@@ -0,0 +1,57 @@
import { request } from "@app/lib/config/request";
import { UnauthorizedError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { OktaConnectionMethod } from "./okta-connection-enums";
import { TOktaApp, TOktaConnection, TOktaConnectionConfig } from "./okta-connection-types";
export const getOktaConnectionListItem = () => {
return {
name: "Okta" as const,
app: AppConnection.Okta as const,
methods: Object.values(OktaConnectionMethod) as [OktaConnectionMethod.ApiToken]
};
};
export const getOktaInstanceUrl = async (config: TOktaConnectionConfig) => {
const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl);
await blockLocalAndPrivateIpAddresses(instanceUrl);
return instanceUrl;
};
export const validateOktaConnectionCredentials = async (config: TOktaConnectionConfig) => {
const { apiToken } = config.credentials;
const instanceUrl = await getOktaInstanceUrl(config);
try {
await request.get(`${instanceUrl}/api/v1/users/me`, {
headers: {
Accept: "application/json",
Authorization: `SSWS ${apiToken}`
},
validateStatus: (status) => status === 200
});
} catch (error: unknown) {
throw new UnauthorizedError({
message: "Unable to validate connection: invalid credentials"
});
}
return config.credentials;
};
export const listOktaApps = async (appConnection: TOktaConnection) => {
const { apiToken } = appConnection.credentials;
const instanceUrl = await getOktaInstanceUrl(appConnection);
const { data } = await request.get<TOktaApp[]>(`${instanceUrl}/api/v1/apps`, {
headers: {
Accept: "application/json",
Authorization: `SSWS ${apiToken}`
}
});
return data.filter((app) => app.status === "ACTIVE" && app.name === "oidc_client");
};

View File

@@ -0,0 +1,69 @@
import RE2 from "re2";
import z from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { OktaConnectionMethod } from "./okta-connection-enums";
export const OktaConnectionApiTokenCredentialsSchema = z.object({
instanceUrl: z
.string()
.trim()
.url("Invalid Instance URL")
.min(1, "Instance URL required")
.max(255)
.describe(AppConnections.CREDENTIALS.OKTA.instanceUrl),
apiToken: z
.string()
.trim()
.min(1, "API Token required")
.refine((value) => new RE2("^00[a-zA-Z0-9_-]{40}$").test(value), "Invalid Okta API Token format")
.describe(AppConnections.CREDENTIALS.OKTA.apiToken)
});
const BaseOktaConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Okta) });
export const OktaConnectionSchema = BaseOktaConnectionSchema.extend({
method: z.literal(OktaConnectionMethod.ApiToken),
credentials: OktaConnectionApiTokenCredentialsSchema
});
export const SanitizedOktaConnectionSchema = z.discriminatedUnion("method", [
BaseOktaConnectionSchema.extend({
method: z.literal(OktaConnectionMethod.ApiToken),
credentials: OktaConnectionApiTokenCredentialsSchema.pick({
instanceUrl: true
})
})
]);
export const ValidateOktaConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(OktaConnectionMethod.ApiToken).describe(AppConnections.CREATE(AppConnection.Okta).method),
credentials: OktaConnectionApiTokenCredentialsSchema.describe(AppConnections.CREATE(AppConnection.Okta).credentials)
})
]);
export const CreateOktaConnectionSchema = ValidateOktaConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Okta)
);
export const UpdateOktaConnectionSchema = z
.object({
credentials: OktaConnectionApiTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Okta).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Okta));
export const OktaConnectionListItemSchema = z.object({
name: z.literal("Okta"),
app: z.literal(AppConnection.Okta),
methods: z.nativeEnum(OktaConnectionMethod).array()
});

View File

@@ -0,0 +1,23 @@
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listOktaApps } from "./okta-connection-fns";
import { TOktaConnection } from "./okta-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TOktaConnection>;
export const oktaConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listApps = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Okta, connectionId, actor);
const apps = await listOktaApps(appConnection);
return apps;
};
return {
listApps
};
};

View File

@@ -0,0 +1,29 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateOktaConnectionSchema,
OktaConnectionSchema,
ValidateOktaConnectionCredentialsSchema
} from "./okta-connection-schemas";
export type TOktaConnection = z.infer<typeof OktaConnectionSchema>;
export type TOktaConnectionInput = z.infer<typeof CreateOktaConnectionSchema> & {
app: AppConnection.Okta;
};
export type TValidateOktaConnectionCredentialsSchema = typeof ValidateOktaConnectionCredentialsSchema;
export type TOktaConnectionConfig = DiscriminativePick<TOktaConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TOktaApp = {
id: string;
label: string;
status: "ACTIVE" | "INACTIVE";
name: "oidc_client"; // "oidc_client" or other types
};

View File

@@ -47,7 +47,10 @@ export const ValidatePostgresConnectionCredentialsSchema = z.discriminatedUnion(
]);
export const CreatePostgresConnectionSchema = ValidatePostgresConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagedCredentials: true })
GenericCreateAppConnectionFieldsSchema(AppConnection.Postgres, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const UpdatePostgresConnectionSchema = z
@@ -56,7 +59,12 @@ export const UpdatePostgresConnectionSchema = z
AppConnections.UPDATE(AppConnection.Postgres).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagedCredentials: true }));
.and(
GenericUpdateAppConnectionFieldsSchema(AppConnection.Postgres, {
supportsPlatformManagedCredentials: true,
supportsGateways: true
})
);
export const PostgresConnectionListItemSchema = z.object({
name: z.literal("PostgreSQL"),

View File

@@ -1,11 +1,13 @@
import knex, { Knex } from "knex";
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import {
TSqlCredentialsRotationGeneratedCredentials,
TSqlCredentialsRotationWithConnection
} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { TAppConnectionRaw, TSqlConnection } from "@app/services/app-connection/app-connection-types";
@@ -98,25 +100,80 @@ export const getSqlConnectionClient = async (appConnection: Pick<TSqlConnection,
return client;
};
export const validateSqlConnectionCredentials = async (config: TSqlConnectionConfig) => {
const { credentials, app } = config;
export const executeWithPotentialGateway = async <T>(
config: TSqlConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
operation: (client: Knex) => Promise<T>
): Promise<T> => {
const { credentials, app, gatewayId } = config;
let client: Knex | undefined;
if (gatewayId && gatewayService) {
const [targetHost] = await verifyHostInputValidity(credentials.host, true);
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
return withGatewayProxy(
async (proxyPort) => {
const client = knex({
client: SQL_CONNECTION_CLIENT_MAP[app],
connection: {
database: credentials.database,
port: proxyPort,
host: "localhost",
user: credentials.username,
password: credentials.password,
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
...getConnectionConfig({ app, credentials })
}
});
try {
return await operation(client);
} finally {
await client.destroy();
}
},
{
protocol: GatewayProxyProtocol.Tcp,
targetHost,
targetPort: credentials.port,
relayHost,
relayPort: Number(relayPort),
identityId: relayDetails.identityId,
orgId: relayDetails.orgId,
tlsOptions: {
ca: relayDetails.certChain,
cert: relayDetails.certificate,
key: relayDetails.privateKey.toString()
}
}
);
}
// Non-gateway path
const client = await getSqlConnectionClient({ app, credentials });
try {
client = await getSqlConnectionClient({ app, credentials });
return await operation(client);
} finally {
await client.destroy();
}
};
await client.raw(`Select 1`);
return credentials;
export const validateSqlConnectionCredentials = async (
config: TSqlConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
try {
await executeWithPotentialGateway(config, gatewayService, async (client) => {
await client.raw(`Select 1`);
});
return config.credentials;
} catch (error) {
throw new BadRequestError({
message: `Unable to validate connection: ${
(error as Error)?.message?.replaceAll(credentials.password, "********************") ?? "verify credentials"
(error as Error)?.message?.replaceAll(config.credentials.password, "********************") ??
"verify credentials"
}`
});
} finally {
await client?.destroy();
}
};
@@ -132,22 +189,23 @@ export const SQL_CONNECTION_ALTER_LOGIN_STATEMENT: Record<
export const transferSqlConnectionCredentialsToPlatform = async (
config: TSqlConnectionConfig,
callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise<TAppConnectionRaw>
callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise<TAppConnectionRaw>,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
const { credentials, app } = config;
const client = await getSqlConnectionClient({ app, credentials });
const newPassword = alphaNumericNanoId(32);
try {
return await client.transaction(async (tx) => {
await tx.raw(
...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[app]({ username: credentials.username, password: newPassword })
);
return callback({
...credentials,
password: newPassword
return await executeWithPotentialGateway(config, gatewayService, (client) => {
return client.transaction(async (tx) => {
await tx.raw(
...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[app]({ username: credentials.username, password: newPassword })
);
return callback({
...credentials,
password: newPassword
});
});
});
} catch (error) {
@@ -161,7 +219,5 @@ export const transferSqlConnectionCredentialsToPlatform = async (
(error as Error)?.message?.replaceAll(newPassword, "********************") ??
"Encountered an error transferring credentials to platform"
});
} finally {
await client.destroy();
}
};

View File

@@ -1,6 +1,9 @@
import { DiscriminativePick } from "@app/lib/types";
import { TSqlConnectionInput } from "@app/services/app-connection/app-connection-types";
export type TSqlConnectionConfig = DiscriminativePick<TSqlConnectionInput, "method" | "app" | "credentials"> & {
export type TSqlConnectionConfig = DiscriminativePick<
TSqlConnectionInput,
"method" | "app" | "credentials" | "gatewayId"
> & {
orgId: string;
};

View File

@@ -218,7 +218,7 @@ export const certificateAuthorityDALFactory = (db: TDbClient) => {
};
const findWithAssociatedCa = async (
filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string },
filter: Parameters<(typeof caOrm)["find"]>[0] & { dn?: string; type?: string; serialNumber?: string },
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TCertificateAuthorities> = {},
tx?: Knex
) => {

View File

@@ -1068,11 +1068,11 @@ export const internalCertificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "Invalid certificate chain" });
const parentCertObj = chainItems[1];
const parentCertSubject = parentCertObj.subject;
const parentSerialNumber = parentCertObj.serialNumber;
const [parentCa] = await certificateAuthorityDAL.findWithAssociatedCa({
[`${TableName.CertificateAuthority}.projectId` as "projectId"]: ca.projectId,
[`${TableName.InternalCertificateAuthority}.dn` as "dn"]: parentCertSubject
[`${TableName.InternalCertificateAuthority}.serialNumber` as "serialNumber"]: parentSerialNumber
});
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({

View File

@@ -161,8 +161,8 @@ type TProjectServiceFactoryDep = {
sshHostGroupDAL: Pick<TSshHostGroupDALFactory, "find" | "findSshHostGroupsWithLoginMappings">;
permissionService: TPermissionServiceFactory;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "invalidateGetPlan">;
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
smtpService: Pick<TSmtpService, "sendMail">;
orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
@@ -489,10 +489,6 @@ export const projectServiceFactory = ({
);
}
// no need to invalidate if there was no limit
if (plan.workspaceLimit) {
await licenseService.invalidateGetPlan(organization.id);
}
return {
...project,
environments: envs,

View File

@@ -174,6 +174,7 @@ export const fnSecretsV2FromImports = async ({
skipMultilineEncoding?: boolean | null;
secretPath: string;
environment: string;
secretKey: string;
}) => Promise<string | undefined>;
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
}) => {
@@ -293,7 +294,8 @@ export const fnSecretsV2FromImports = async ({
value: decryptedSecret.secretValue,
secretPath: processedImport.secretPath,
environment: processedImport.environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding,
secretKey: decryptedSecret.secretKey
});
// eslint-disable-next-line no-param-reassign
processedImport.secrets[index].secretValue = expandedSecretValue || "";

View File

@@ -1,4 +1,6 @@
/* eslint-disable no-await-in-loop */
import { isAxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
@@ -71,7 +73,7 @@ const putEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secr
);
};
const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: TRenderSecret) => {
const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: Pick<TRenderSecret, "key">) => {
const {
destinationConfig,
connection: {
@@ -79,15 +81,24 @@ const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, s
}
} = secretSync;
await request.delete(
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
try {
await request.delete(
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
}
);
} catch (error) {
if (isAxiosError(error) && error.response?.status === 404) {
// If the secret does not exist, we can ignore this error
return;
}
);
throw error;
}
};
const sleep = async () =>
@@ -99,6 +110,11 @@ export const RenderSyncFns = {
syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
for await (const key of Object.keys(secretMap)) {
// If value is empty skip it as render does not allow empty variables
if (secretMap[key].value === "") {
// eslint-disable-next-line no-continue
continue;
}
await putEnvironmentSecret(secretSync, secretMap, key);
await sleep();
}

View File

@@ -231,7 +231,8 @@ export const secretSyncQueueFactory = ({
environment: environment.slug,
secretPath: folder.path,
skipMultilineEncoding: secret.skipMultilineEncoding,
value: secretValue
value: secretValue,
secretKey
});
secretMap[secretKey] = { value: expandedSecretValue || "" };

View File

@@ -614,6 +614,7 @@ export const expandSecretReferencesFactory = ({
secretPath: string;
environment: string;
shouldStackTrace?: boolean;
secretKey: string;
}) => {
const stackTrace = { ...dto, key: "root", children: [] } as TSecretReferenceTraceNode;
@@ -656,7 +657,7 @@ export const expandSecretReferencesFactory = ({
const referredValue = await fetchSecret(environment, secretPath, secretKey);
if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags))
throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to read value on.`
message: `You do not have permission to read secret '${secretKey}' in environment '${environment}' at path '${secretPath}', which is referenced by secret '${dto.secretKey}' in environment '${dto.environment}' at path '${dto.secretPath}'.`
});
const cacheKey = getCacheUniqueKey(environment, secretPath);
@@ -675,7 +676,7 @@ export const expandSecretReferencesFactory = ({
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags))
throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to read value on.`
message: `You do not have permission to read secret '${secretReferenceKey}' in environment '${secretReferenceEnvironment}' at path '${secretReferencePath}', which is referenced by secret '${dto.secretKey}' in environment '${dto.environment}' at path '${dto.secretPath}'.`
});
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
@@ -692,6 +693,7 @@ export const expandSecretReferencesFactory = ({
secretPath: referencedSecretPath,
environment: referencedSecretEnvironmentSlug,
depth: depth + 1,
secretKey: referencedSecretKey,
trace
};
@@ -726,6 +728,7 @@ export const expandSecretReferencesFactory = ({
skipMultilineEncoding?: boolean | null;
secretPath: string;
environment: string;
secretKey: string;
}) => {
if (!inputSecret.value) return inputSecret.value;
@@ -741,6 +744,7 @@ export const expandSecretReferencesFactory = ({
value?: string;
secretPath: string;
environment: string;
secretKey: string;
}) => {
const { stackTrace, expandedValue } = await recursivelyExpandSecret({ ...inputSecret, shouldStackTrace: true });
return { stackTrace, expandedValue };

View File

@@ -1105,7 +1105,7 @@ export const secretV2BridgeServiceFactory = ({
if (shouldExpandSecretReferences) {
const secretsGroupByPath = groupBy(decryptedSecrets, (i) => i.secretPath);
await Promise.allSettled(
const settledPromises = await Promise.allSettled(
Object.keys(secretsGroupByPath).map((groupedPath) =>
Promise.allSettled(
secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => {
@@ -1113,7 +1113,8 @@ export const secretV2BridgeServiceFactory = ({
value: decryptedSecret.secretValue,
secretPath: groupedPath,
environment,
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding
skipMultilineEncoding: decryptedSecret.skipMultilineEncoding,
secretKey: decryptedSecret.secretKey
});
// eslint-disable-next-line no-param-reassign
secretsGroupByPath[groupedPath][index].secretValue = expandedSecretValue || "";
@@ -1121,6 +1122,35 @@ export const secretV2BridgeServiceFactory = ({
)
)
);
const errors: { path: string; error: string }[] = [];
settledPromises.forEach((outerResult: PromiseSettledResult<PromiseSettledResult<void>[]>, outerIndex) => {
const groupedPath = Object.keys(secretsGroupByPath)[outerIndex];
if (outerResult.status === "rejected") {
errors.push({
path: groupedPath,
error: `Failed to process secret group: ${outerResult.reason}`
});
} else {
// Check inner promise results
outerResult.value.forEach((innerResult: PromiseSettledResult<void>) => {
if (innerResult.status === "rejected") {
const reason = innerResult.reason as ForbiddenRequestError;
errors.push({
path: groupedPath,
error: reason.message
});
}
});
}
});
if (errors.length > 0) {
throw new ForbiddenRequestError({
message: "Failed to expand one or more secret references",
details: errors.map((err) => err.error)
});
}
}
if (!includeImports) {
@@ -1424,7 +1454,8 @@ export const secretV2BridgeServiceFactory = ({
environment,
secretPath: path,
value: secretValue,
skipMultilineEncoding: secret.skipMultilineEncoding
skipMultilineEncoding: secret.skipMultilineEncoding,
secretKey: secret.key
});
secretValue = expandedSecretValue || "";
@@ -2722,7 +2753,8 @@ export const secretV2BridgeServiceFactory = ({
const { expandedValue, stackTrace } = await getExpandedSecretStackTrace({
environment,
secretPath,
value: decryptedSecretValue
value: decryptedSecretValue,
secretKey: secretName
});
return { tree: stackTrace, value: expandedValue };

View File

@@ -426,7 +426,8 @@ export const secretQueueFactory = ({
environment: dto.environment,
secretPath: dto.secretPath,
skipMultilineEncoding: secret.skipMultilineEncoding,
value: secretValue
value: secretValue,
secretKey
});
content[secretKey] = { value: expandedSecretValue || "" };

View File

@@ -33,6 +33,7 @@ Every feature/problem is unique, but your design docs should generally include t
- A high-level summary of the problem and proposed solution. Keep it brief (max 3 paragraphs).
3. **Context**
- Explain the problem's background, why it's important to solve now, and any constraints (e.g., technical, sales, or timeline-related). What do we get out of solving this problem? (needed to close a deal, scale, performance, etc.).
- Consider whether this feature has notable sales implications (e.g., affects pricing, customer commitments, go-to-market strategy, or competitive positioning) that would require Sales team input and approval.
4. **Solution**
- Provide a big-picture explanation of the solution, followed by detailed technical architecture.
@@ -76,3 +77,11 @@ Before sharing your design docs with others, review your design doc as if you we
- Ask a relevant engineer(s) to review your document. Their role is to identify blind spots, challenge assumptions, and ensure everything is clear. Once you and the reviewer are on the same page on the approach, update the document with any missing details they brought up.
4. **Team Review and Feedback**
- Invite the relevant engineers to a design doc review meeting and give them 10-15 minutes to read through the document. After everyone has had a chance to review it, open the floor up for discussion. Address any feedback or concerns raised during this meeting. If significant points were overlooked during your initial planning, you may need to revisit the drawing board. Your goal is to think about the feature holistically and minimize the need for drastic changes to your design doc later on.
5. **Sales Approval (When Applicable)**
- If your design document has notable sales implications, get explicit approval from the Sales team before proceeding to implementation. This includes features that:
- Affect pricing models or billing structures
- Impact customer commitments or contractual obligations
- Change core product functionality that's actively being sold
- Introduce new capabilities that could affect competitive positioning
- Modify user experience in ways that could impact customer acquisition or retention
- Share the design document with the Sales team to ensure alignment between the proposed technical approach and sales strategy, pricing models, and market positioning.

View File

@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/okta/available"
---

View File

@@ -0,0 +1,8 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/okta"
---
<Note>
Check out the configuration docs for [Okta Connections](/integrations/app-connections/okta) to learn how to obtain the required credentials.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/okta/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/okta/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/okta/connection-name/{connectionName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/okta"
---

View File

@@ -0,0 +1,8 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/okta/{connectionId}"
---
<Note>
Check out the configuration docs for [Okta Connections](/integrations/app-connections/okta) to learn how to obtain the required credentials.
</Note>

View File

@@ -0,0 +1,8 @@
---
title: "Create"
openapi: "POST /api/v2/secret-rotations/okta-client-secret"
---
<Note>
Check out the configuration docs for [Okta Client Secret Rotations](/documentation/platform/secret-rotation/okta-client-secret) to learn how to obtain the required parameters.
</Note>

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v2/secret-rotations/okta-client-secret/{rotationId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v2/secret-rotations/okta-client-secret/{rotationId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v2/secret-rotations/okta-client-secret/rotation-name/{rotationName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get Credentials by ID"
openapi: "GET /api/v2/secret-rotations/okta-client-secret/{rotationId}/generated-credentials"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v2/secret-rotations/okta-client-secret"
---

View File

@@ -0,0 +1,4 @@
---
title: "Rotate Secrets"
openapi: "POST /api/v2/secret-rotations/okta-client-secret/{rotationId}/rotate-secrets"
---

View File

@@ -0,0 +1,8 @@
---
title: "Update"
openapi: "PATCH /api/v2/secret-rotations/okta-client-secret/{rotationId}"
---
<Note>
Check out the configuration docs for [Okta Client Secret Rotations](/documentation/platform/secret-rotation/okta-client-secret) to learn how to obtain the required parameters.
</Note>

View File

@@ -2,3 +2,8 @@
title: "Login"
openapi: "POST /api/v1/auth/tls-cert-auth/login"
---
<Warning>
Infisical US/EU and dedicated instances are deployed with AWS ALB. TLS Certificate Auth must flow through our ALB mTLS pass-through in order to authenticate.
When you are authenticating with TLS Certificate Auth, you must use the port `8443` instead of the default `443`. Example: `https://app.infisical.com:8443/api/v1/auth/tls-cert-auth/login`
</Warning>

View File

@@ -9,7 +9,7 @@ infisical export [options]
## Description
Export environment variables from the platform into a file format.
Export environment variables from the platform into a file format. By default, output is sent to stdout (standard output), but you can use the `--output-file` flag to save directly to a file.
## Subcommands & flags
@@ -21,18 +21,19 @@ $ infisical export
# Export variables to a .env file
infisical export > .env
infisical export --output-file=./.env
# Export variables to a .env file (with export keyword)
infisical export --format=dotenv-export > .env
# Export variables to a CSV file
infisical export --format=csv > secrets.csv
infisical export --format=dotenv-export --output-file=./.env
# Export variables to a JSON file
infisical export --format=json > secrets.json
infisical export --format=json --output-file=./secrets.json
# Export variables to a YAML file
infisical export --format=yaml > secrets.yaml
infisical export --format=yaml --output-file=./secrets.yaml
# Render secrets using a custom template file
infisical export --template=<path to template>
@@ -73,6 +74,34 @@ infisical export --template=<path to template>
### flags
<Accordion title="--output-file">
The path to write the output file to. Can be a full file path, directory, or filename.
```bash
# Export to specific file
infisical export --format=json --output-file=./secrets.json
# Export to directory (uses default filename based on format)
infisical export --format=yaml --output-file=./
```
**When `--output-file` is specified:**
- Secrets are saved directly to the specified file
- A success message is displayed showing the file path
- For directories: adds default filename `secrets.{format}` (e.g., `secrets.json`, `secrets.yaml`)
- For dotenv formats in directories: uses `.env` as the filename
**When `--output-file` is NOT specified (default behavior):**
- Output is sent to stdout (standard output)
- You can use shell redirection like `infisical export > secrets.json`
- Maintains backwards compatibility with existing scripts
<Warning>
If you're using shell redirection and your token expires, re-authentication will fail because the prompt can't display properly due to the redirection.
</Warning>
</Accordion>
<Accordion title="--template">
The `--template` flag specifies the path to the template file used for rendering secrets. When using templates, you can omit the other format flags.
@@ -94,6 +123,7 @@ infisical export --template=<path to template>
```
</Accordion>
<Accordion title="--env">
Used to set the environment that secrets are pulled from.
@@ -162,7 +192,7 @@ infisical export --template=<path to template>
```bash
# Example
infisical run --tags=tag1,tag2,tag3 -- npm run dev
infisical export --tags=tag1,tag2,tag3 --env=dev
```
Note: you must reference the tag by its slug name not its fully qualified name. Go to project settings to view all tag slugs.
@@ -171,4 +201,4 @@ infisical export --template=<path to template>
</Accordion>
</Accordion>
</Accordion>

View File

@@ -78,7 +78,10 @@
},
{
"group": "Infisical SSH",
"pages": ["documentation/platform/ssh/overview", "documentation/platform/ssh/host-groups"]
"pages": [
"documentation/platform/ssh/overview",
"documentation/platform/ssh/host-groups"
]
},
{
"group": "Key Management (KMS)",
@@ -146,6 +149,7 @@
"documentation/platform/secret-rotation/ldap-password",
"documentation/platform/secret-rotation/mssql-credentials",
"documentation/platform/secret-rotation/mysql-credentials",
"documentation/platform/secret-rotation/okta-client-secret",
"documentation/platform/secret-rotation/oracledb-credentials",
"documentation/platform/secret-rotation/postgres-credentials"
]
@@ -375,7 +379,10 @@
},
{
"group": "Architecture",
"pages": ["internals/architecture/components", "internals/architecture/cloud"]
"pages": [
"internals/architecture/components",
"internals/architecture/cloud"
]
},
"internals/security",
"internals/service-tokens"
@@ -481,6 +488,7 @@
"integrations/app-connections/mssql",
"integrations/app-connections/mysql",
"integrations/app-connections/oci",
"integrations/app-connections/okta",
"integrations/app-connections/oracledb",
"integrations/app-connections/postgres",
"integrations/app-connections/railway",
@@ -551,7 +559,10 @@
"integrations/cloud/gcp-secret-manager",
{
"group": "Cloudflare",
"pages": ["integrations/cloud/cloudflare-pages", "integrations/cloud/cloudflare-workers"]
"pages": [
"integrations/cloud/cloudflare-pages",
"integrations/cloud/cloudflare-workers"
]
},
"integrations/cloud/terraform-cloud",
"integrations/cloud/databricks",
@@ -663,7 +674,11 @@
"cli/commands/reset",
{
"group": "infisical scan",
"pages": ["cli/commands/scan", "cli/commands/scan-git-changes", "cli/commands/scan-install"]
"pages": [
"cli/commands/scan",
"cli/commands/scan-git-changes",
"cli/commands/scan-install"
]
}
]
},
@@ -987,7 +1002,9 @@
"pages": [
{
"group": "Kubernetes",
"pages": ["api-reference/endpoints/dynamic-secrets/kubernetes/create-lease"]
"pages": [
"api-reference/endpoints/dynamic-secrets/kubernetes/create-lease"
]
},
"api-reference/endpoints/dynamic-secrets/create",
"api-reference/endpoints/dynamic-secrets/update",
@@ -1093,6 +1110,19 @@
"api-reference/endpoints/secret-rotations/mysql-credentials/update"
]
},
{
"group": "Okta Client Secret",
"pages": [
"api-reference/endpoints/secret-rotations/okta-client-secret/create",
"api-reference/endpoints/secret-rotations/okta-client-secret/delete",
"api-reference/endpoints/secret-rotations/okta-client-secret/get-by-id",
"api-reference/endpoints/secret-rotations/okta-client-secret/get-by-name",
"api-reference/endpoints/secret-rotations/okta-client-secret/get-generated-credentials-by-id",
"api-reference/endpoints/secret-rotations/okta-client-secret/list",
"api-reference/endpoints/secret-rotations/okta-client-secret/rotate-secrets",
"api-reference/endpoints/secret-rotations/okta-client-secret/update"
]
},
{
"group": "OracleDB Credentials",
"pages": [
@@ -1496,6 +1526,18 @@
"api-reference/endpoints/app-connections/oci/delete"
]
},
{
"group": "Okta",
"pages": [
"api-reference/endpoints/app-connections/okta/list",
"api-reference/endpoints/app-connections/okta/available",
"api-reference/endpoints/app-connections/okta/get-by-id",
"api-reference/endpoints/app-connections/okta/get-by-name",
"api-reference/endpoints/app-connections/okta/create",
"api-reference/endpoints/app-connections/okta/update",
"api-reference/endpoints/app-connections/okta/delete"
]
},
{
"group": "OracleDB",
"pages": [

View File

@@ -42,10 +42,14 @@ To be more specific:
Most of the time, the Infisical server will be behind a load balancer or
proxy. To propagate the TLS certificate from the load balancer to the
instance, you can configure the TLS to send the client certificate as a header
that is set as an [environment
variable](/self-hosting/configuration/envars#param-identity-tls-cert-auth-client-certificate-header-key).
that is set as an [environment variable](/self-hosting/configuration/envars#param-identity-tls-cert-auth-client-certificate-header-key).
</Accordion>
<Note>
Infisical US/EU and dedicated instances are deployed with AWS ALB. TLS Certificate Auth must flow through our ALB mTLS pass-through in order to authenticate.
When you are authenticating with TLS Certificate Auth, you must use the port `8443` instead of the default `443`. Example: `https://app.infisical.com:8443/api/v1/auth/tls-cert-auth/login`
</Note>
## Guide
In the following steps, we explore how to create and use identities for your workloads and applications on TLS Certificate to
@@ -123,7 +127,7 @@ try {
const clientCertificate = fs.readFileSync("client-cert.pem", "utf8");
const clientKeyCertificate = fs.readFileSync("client-key.pem", "utf8");
const infisicalUrl = "https://app.infisical.com"; // or your self-hosted Infisical URL
const infisicalUrl = "https://app.infisical.com:8443"; // or your self-hosted Infisical URL
const identityId = "<your-identity-id>";
// Create HTTPS agent with client certificate and key

View File

@@ -0,0 +1,145 @@
---
title: "Okta Client Secret"
description: "Learn how to automatically rotate Okta Client Secrets."
---
## Prerequisites
- Create an [Okta Connection](/integrations/app-connections/okta).
## Create an Okta Client Secret Rotation in Infisical
<Tabs>
<Tab title="Infisical UI">
1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown.
![Secret Manager Dashboard](/images/secret-rotations-v2/generic/add-secret-rotation.png)
2. Select the **Okta Client Secret** option.
![Select Okta Client Secret](/images/secret-rotations-v2/okta-client-secret/select-okta.png)
3. Configure the rotation behavior, then click **Next**.
![Rotation Configuration](/images/secret-rotations-v2/okta-client-secret/configuration.png)
- **Okta Connection** - the connection that will perform the rotation of the specified application's Client Secret.
- **Rotation Interval** - the interval, in days, that once elapsed will trigger a rotation.
- **Rotate At** - the local time of day when rotation should occur once the interval has elapsed.
- **Auto-Rotation Enabled** - whether secrets should automatically be rotated once the rotation interval has elapsed. Disable this option to manually rotate secrets or pause secret rotation.
4. Select the Okta application whose Client Secret you want to rotate. Then click **Next**.
![Rotation Parameters](/images/secret-rotations-v2/okta-client-secret/parameters.png)
5. Specify the secret names that the client credentials should be mapped to. Then click **Next**.
![Rotation Secrets Mapping](/images/secret-rotations-v2/okta-client-secret/mappings.png)
- **Client ID** - the name of the secret that the application Client ID will be mapped to.
- **Client Secret** - the name of the secret that the rotated Client Secret will be mapped to.
6. Give your rotation a name and description (optional). Then click **Next**.
![Rotation Details](/images/secret-rotations-v2/okta-client-secret/details.png)
- **Name** - the name of the secret rotation configuration. Must be slug-friendly.
- **Description** (optional) - a description of this rotation configuration.
7. Review your configuration, then click **Create Secret Rotation**.
![Rotation Review](/images/secret-rotations-v2/okta-client-secret/review.png)
8. Your **Okta Client Secret** credentials are now available for use via the mapped secrets.
![Rotation Created](/images/secret-rotations-v2/okta-client-secret/created.png)
</Tab>
<Tab title="API">
To create an Okta Client Secret Rotation, make an API request to the [Create Okta Client Secret Rotation](/api-reference/endpoints/secret-rotations/okta-client-secret/create) API endpoint.
You will first need the **Client ID** of the Okta application you want to rotate the secret for. This can be obtained from the applications dashboard.
![Okta Client ID](/images/secret-rotations-v2/okta-client-secret/client-id.png)
### Sample request
```bash Request
curl --request POST \
--url https://us.infisical.com/api/v2/secret-rotations/okta-client-secret \
--header 'Content-Type: application/json' \
--data '{
"name": "my-okta-rotation",
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "my client secret rotation",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"environment": "dev",
"secretPath": "/",
"isAutoRotationEnabled": true,
"rotationInterval": 30,
"rotateAtUtc": {
"hours": 0,
"minutes": 0
},
"parameters": {
"clientId": "...",
},
"secretsMapping": {
"clientId": "OKTA_CLIENT_ID",
"clientSecret": "OKTA_CLIENT_SECRET"
}
}'
```
### Sample response
```bash Response
{
"secretRotation": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "my-okta-rotation",
"description": "my client secret rotation",
"secretsMapping": {
"clientId": "OKTA_CLIENT_ID",
"clientSecret": "OKTA_CLIENT_SECRET"
},
"isAutoRotationEnabled": true,
"activeIndex": 0,
"folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"createdAt": "2023-11-07T05:31:56Z",
"updatedAt": "2023-11-07T05:31:56Z",
"rotationInterval": 30,
"rotationStatus": "success",
"lastRotationAttemptedAt": "2023-11-07T05:31:56Z",
"lastRotatedAt": "2023-11-07T05:31:56Z",
"lastRotationJobId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"nextRotationAt": "2023-11-07T05:31:56Z",
"connection": {
"app": "okta",
"name": "my-okta-connection",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"environment": {
"slug": "dev",
"name": "Development",
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
},
"projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"folder": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"path": "/"
},
"rotateAtUtc": {
"hours": 0,
"minutes": 0
},
"lastRotationMessage": null,
"type": "okta-client-secret",
"parameters": {
"clientId": "..."
}
}
}
```
</Tab>
</Tabs>

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

View File

@@ -0,0 +1,99 @@
---
title: "Okta Connection"
description: "Learn how to configure an Okta Connection for Infisical."
---
Infisical supports the use of [API Tokens](https://developer.okta.com/docs/guides/create-an-api-token/main/) to connect with Okta.
## Create Okta API Token
<Steps>
<Step title="Create API Token">
From the Okta admin dashboard, navigate to **Security > API > Tokens** and click **Create token**.
![Create API Token](/images/app-connections/okta/step-1.png)
</Step>
<Step title="Provide Info">
Enter the token name and select **Any IP** for the second dropdown, then click **Create token**.
![Provide Info](/images/app-connections/okta/step-2.png)
</Step>
<Step title="Copy Token">
Copy the token from the modal for later steps.
![Copy Token](/images/app-connections/okta/step-3.png)
</Step>
</Steps>
## Create Okta Connection in Infisical
<Tabs>
<Tab title="Infisical UI">
<Steps>
<Step title="Navigate to App Connections">
In your Infisical dashboard, go to **Organization Settings** and select the [**App Connections**](https://app.infisical.com/organization/app-connections) tab.
![App Connections Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Select Okta Connection">
Click the **Add Connection** button and select **Okta** from the list of available connections.
</Step>
<Step title="Fill out Connection Modal">
Complete the Okta Connection form by entering:
- A descriptive name for the connection
- An optional description for future reference
- Your Okta instance URL
- The API Token from earlier steps
![Connection Modal](/images/app-connections/okta/step-4.png)
</Step>
<Step title="Connection Created">
After clicking Create, your **Okta Connection** is established and ready to use with your Infisical projects.
![Connection Created](/images/app-connections/okta/step-5.png)
</Step>
</Steps>
</Tab>
<Tab title="API">
To create a Okta Connection, make an API request to the [Create Okta Connection](/api-reference/endpoints/app-connections/okta/create) API endpoint.
### Sample request
```bash Request
curl --request POST \
--url https://app.infisical.com/api/v1/app-connections/okta \
--header 'Content-Type: application/json' \
--data '{
"name": "my-okta-connection",
"method": "api-token",
"credentials": {
"instanceUrl": "https://example.okta.com",
"apiToken": "<YOUR-API-TOKEN>"
}
}'
```
### Sample response
```bash Response
{
"appConnection": {
"id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6",
"name": "my-okta-connection",
"description": null,
"version": 1,
"orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c",
"createdAt": "2025-04-23T19:46:34.831Z",
"updatedAt": "2025-04-23T19:46:34.831Z",
"isPlatformManagedCredentials": false,
"credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f",
"app": "okta",
"method": "api-token",
"credentials": {
"instanceUrl": "https://example.okta.com"
}
}
}
```
</Tab>
</Tabs>

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -0,0 +1,38 @@
import { CredentialDisplay } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/shared/CredentialDisplay";
import { TOktaClientSecretRotationGeneratedCredentialsResponse } from "@app/hooks/api/secretRotationsV2/types/okta-client-secret-rotation";
import { ViewRotationGeneratedCredentialsDisplay } from "./shared";
type Props = {
generatedCredentialsResponse: TOktaClientSecretRotationGeneratedCredentialsResponse;
};
export const ViewOktaClientSecretRotationGeneratedCredentials = ({
generatedCredentialsResponse: { generatedCredentials, activeIndex }
}: Props) => {
const inactiveIndex = activeIndex === 0 ? 1 : 0;
const activeCredentials = generatedCredentials[activeIndex];
const inactiveCredentials = generatedCredentials[inactiveIndex];
return (
<ViewRotationGeneratedCredentialsDisplay
activeCredentials={
<>
<CredentialDisplay label="Client ID">{activeCredentials?.clientId}</CredentialDisplay>
<CredentialDisplay isSensitive label="Client Secret">
{activeCredentials?.clientSecret}
</CredentialDisplay>
</>
}
inactiveCredentials={
<>
<CredentialDisplay label="Client ID">{inactiveCredentials?.clientId}</CredentialDisplay>
<CredentialDisplay isSensitive label="Client Secret">
{inactiveCredentials?.clientSecret}
</CredentialDisplay>
</>
}
/>
);
};

View File

@@ -22,6 +22,7 @@ import {
import { ViewSqlCredentialsRotationGeneratedCredentials } from "./shared";
import { ViewAwsIamUserSecretRotationGeneratedCredentials } from "./ViewAwsIamUserSecretRotationGeneratedCredentials";
import { ViewOktaClientSecretRotationGeneratedCredentials } from "./ViewOktaClientSecretRotationGeneratedCredentials";
type Props = {
secretRotation?: TSecretRotationV2;
@@ -99,6 +100,13 @@ const Content = ({ secretRotation }: ContentProps) => {
/>
);
break;
case SecretRotation.OktaClientSecret:
Component = (
<ViewOktaClientSecretRotationGeneratedCredentials
generatedCredentialsResponse={generatedCredentialsResponse}
/>
);
break;
default:
throw new Error("Unhandled View Generated Credential Rotation Type");
}

Some files were not shown because too many files have changed in this diff Show More