mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 11:29:42 +00:00
Merge pull request #4192 from Infisical/ENG-3139
feat(app-connection, secret-rotation): Okta App Connection + Okta Client Secret Rotation
This commit is contained in:
@@ -32,7 +32,8 @@ export enum AppConnection {
|
||||
Railway = "railway",
|
||||
Bitbucket = "bitbucket",
|
||||
Checkly = "checkly",
|
||||
Supabase = "supabase"
|
||||
Supabase = "supabase",
|
||||
Okta = "okta"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
||||
@@ -92,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";
|
||||
@@ -155,7 +156,8 @@ export const listAppConnectionOptions = () => {
|
||||
getRailwayConnectionListItem(),
|
||||
getBitbucketConnectionListItem(),
|
||||
getChecklyConnectionListItem(),
|
||||
getSupabaseConnectionListItem()
|
||||
getSupabaseConnectionListItem(),
|
||||
getOktaConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
@@ -241,7 +243,8 @@ 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, gatewayService);
|
||||
@@ -280,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:
|
||||
@@ -367,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 (
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -79,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";
|
||||
@@ -142,7 +144,8 @@ 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 = ({
|
||||
@@ -603,6 +606,7 @@ export const appConnectionServiceFactory = ({
|
||||
railway: railwayConnectionService(connectAppConnectionById),
|
||||
bitbucket: bitbucketConnectionService(connectAppConnectionById),
|
||||
checkly: checklyConnectionService(connectAppConnectionById),
|
||||
supabase: supabaseConnectionService(connectAppConnectionById)
|
||||
supabase: supabaseConnectionService(connectAppConnectionById),
|
||||
okta: oktaConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -143,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,
|
||||
@@ -232,6 +238,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TRailwayConnection
|
||||
| TChecklyConnection
|
||||
| TSupabaseConnection
|
||||
| TOktaConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -273,6 +280,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TRailwayConnectionInput
|
||||
| TChecklyConnectionInput
|
||||
| TSupabaseConnectionInput
|
||||
| TOktaConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput =
|
||||
@@ -321,7 +329,8 @@ export type TAppConnectionConfig =
|
||||
| TZabbixConnectionConfig
|
||||
| TRailwayConnectionConfig
|
||||
| TChecklyConnectionConfig
|
||||
| TSupabaseConnectionConfig;
|
||||
| TSupabaseConnectionConfig
|
||||
| TOktaConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
@@ -357,7 +366,8 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateZabbixConnectionCredentialsSchema
|
||||
| TValidateRailwayConnectionCredentialsSchema
|
||||
| TValidateChecklyConnectionCredentialsSchema
|
||||
| TValidateSupabaseConnectionCredentialsSchema;
|
||||
| TValidateSupabaseConnectionCredentialsSchema
|
||||
| TValidateOktaConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./okta-connection-enums";
|
||||
export * from "./okta-connection-fns";
|
||||
export * from "./okta-connection-schemas";
|
||||
export * from "./okta-connection-types";
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum OktaConnectionMethod {
|
||||
ApiToken = "api-token"
|
||||
}
|
||||
@@ -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");
|
||||
};
|
||||
@@ -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()
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
Reference in New Issue
Block a user