Fix merge conflicts
@@ -24,3 +24,4 @@ frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65
|
|||||||
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
|
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
|
||||||
docs/documentation/platform/kms/overview.mdx:generic-api-key:281
|
docs/documentation/platform/kms/overview.mdx:generic-api-key:281
|
||||||
docs/documentation/platform/kms/overview.mdx:generic-api-key:344
|
docs/documentation/platform/kms/overview.mdx:generic-api-key:344
|
||||||
|
frontend/src/pages/secret-manager/OverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx:generic-api-key:85
|
||||||
|
|||||||
@@ -92,10 +92,10 @@
|
|||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
"oracledb": "^6.4.0",
|
"oracledb": "^6.4.0",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
"passport-github": "^1.1.0",
|
|
||||||
"passport-gitlab2": "^5.0.0",
|
"passport-gitlab2": "^5.0.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
"passport-ldapauth": "^3.0.1",
|
"passport-ldapauth": "^3.0.1",
|
||||||
|
"passport-oauth2": "^1.8.0",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"pg-boss": "^10.1.5",
|
"pg-boss": "^10.1.5",
|
||||||
"pg-query-stream": "^4.5.3",
|
"pg-query-stream": "^4.5.3",
|
||||||
@@ -136,7 +136,6 @@
|
|||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/node": "^20.17.30",
|
"@types/node": "^20.17.30",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/passport-github": "^1.1.12",
|
|
||||||
"@types/passport-google-oauth20": "^2.0.14",
|
"@types/passport-google-oauth20": "^2.0.14",
|
||||||
"@types/pg": "^8.10.9",
|
"@types/pg": "^8.10.9",
|
||||||
"@types/picomatch": "^2.3.3",
|
"@types/picomatch": "^2.3.3",
|
||||||
@@ -10124,17 +10123,6 @@
|
|||||||
"@types/express": "*"
|
"@types/express": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/passport-github": {
|
|
||||||
"version": "1.1.12",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/passport-github/-/passport-github-1.1.12.tgz",
|
|
||||||
"integrity": "sha512-VJpMEIH+cOoXB694QgcxuvWy2wPd1Oq3gqrg2Y9DMVBYs9TmH9L14qnqPDZsNMZKBDH+SvqRsGZj9SgHYeDgcA==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@types/express": "*",
|
|
||||||
"@types/passport": "*",
|
|
||||||
"@types/passport-oauth2": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/passport-google-oauth20": {
|
"node_modules/@types/passport-google-oauth20": {
|
||||||
"version": "2.0.14",
|
"version": "2.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.14.tgz",
|
||||||
@@ -18397,9 +18385,10 @@
|
|||||||
"integrity": "sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA=="
|
"integrity": "sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA=="
|
||||||
},
|
},
|
||||||
"node_modules/oauth": {
|
"node_modules/oauth": {
|
||||||
"version": "0.9.15",
|
"version": "0.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
|
||||||
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="
|
"integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
@@ -19082,17 +19071,6 @@
|
|||||||
"url": "https://github.com/sponsors/jaredhanson"
|
"url": "https://github.com/sponsors/jaredhanson"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/passport-github": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-XARXJycE6fFh/dxF+Uut8OjlwbFEXgbPVj/+V+K7cvriRK7VcAOm+NgBmbiLM9Qv3SSxEAV+V6fIk89nYHXa8A==",
|
|
||||||
"dependencies": {
|
|
||||||
"passport-oauth2": "1.x.x"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/passport-gitlab2": {
|
"node_modules/passport-gitlab2": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz",
|
||||||
@@ -19128,12 +19106,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/passport-oauth2": {
|
"node_modules/passport-oauth2": {
|
||||||
"version": "1.7.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
|
||||||
"integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==",
|
"integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base64url": "3.x.x",
|
"base64url": "3.x.x",
|
||||||
"oauth": "0.9.x",
|
"oauth": "0.10.x",
|
||||||
"passport-strategy": "1.x.x",
|
"passport-strategy": "1.x.x",
|
||||||
"uid2": "0.0.x",
|
"uid2": "0.0.x",
|
||||||
"utils-merge": "1.x.x"
|
"utils-merge": "1.x.x"
|
||||||
|
|||||||
@@ -91,7 +91,6 @@
|
|||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/node": "^20.17.30",
|
"@types/node": "^20.17.30",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/passport-github": "^1.1.12",
|
|
||||||
"@types/passport-google-oauth20": "^2.0.14",
|
"@types/passport-google-oauth20": "^2.0.14",
|
||||||
"@types/pg": "^8.10.9",
|
"@types/pg": "^8.10.9",
|
||||||
"@types/picomatch": "^2.3.3",
|
"@types/picomatch": "^2.3.3",
|
||||||
@@ -209,10 +208,10 @@
|
|||||||
"ora": "^7.0.1",
|
"ora": "^7.0.1",
|
||||||
"oracledb": "^6.4.0",
|
"oracledb": "^6.4.0",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
"passport-github": "^1.1.0",
|
|
||||||
"passport-gitlab2": "^5.0.0",
|
"passport-gitlab2": "^5.0.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
"passport-ldapauth": "^3.0.1",
|
"passport-ldapauth": "^3.0.1",
|
||||||
|
"passport-oauth2": "^1.8.0",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"pg-boss": "^10.1.5",
|
"pg-boss": "^10.1.5",
|
||||||
"pg-query-stream": "^4.5.3",
|
"pg-query-stream": "^4.5.3",
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const tokenDuration = appCfg?.JWT_REFRESH_LIFETIME;
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.Organization, "userTokenExpiration"))) {
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
t.string("userTokenExpiration");
|
||||||
|
});
|
||||||
|
if (tokenDuration) {
|
||||||
|
await knex(TableName.Organization).update({ userTokenExpiration: tokenDuration });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.Organization, "userTokenExpiration")) {
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
t.dropColumn("userTokenExpiration");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,8 @@ export const OrganizationsSchema = z.object({
|
|||||||
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
||||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||||
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
|
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
|
||||||
bypassOrgAuthEnabled: z.boolean().default(false)
|
bypassOrgAuthEnabled: z.boolean().default(false),
|
||||||
|
userTokenExpiration: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import {
|
||||||
|
AzureClientSecretRotationGeneratedCredentialsSchema,
|
||||||
|
AzureClientSecretRotationSchema,
|
||||||
|
CreateAzureClientSecretRotationSchema,
|
||||||
|
UpdateAzureClientSecretRotationSchema
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/azure-client-secret";
|
||||||
|
import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
|
|
||||||
|
import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints";
|
||||||
|
|
||||||
|
export const registerAzureClientSecretRotationRouter = async (server: FastifyZodProvider) =>
|
||||||
|
registerSecretRotationEndpoints({
|
||||||
|
type: SecretRotation.AzureClientSecret,
|
||||||
|
server,
|
||||||
|
responseSchema: AzureClientSecretRotationSchema,
|
||||||
|
createSchema: CreateAzureClientSecretRotationSchema,
|
||||||
|
updateSchema: UpdateAzureClientSecretRotationSchema,
|
||||||
|
generatedCredentialsSchema: AzureClientSecretRotationGeneratedCredentialsSchema
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotat
|
|||||||
|
|
||||||
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
|
import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-rotation-router";
|
||||||
import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router";
|
import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router";
|
||||||
|
import { registerAzureClientSecretRotationRouter } from "./azure-client-secret-rotation-router";
|
||||||
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
|
import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router";
|
||||||
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
|
import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router";
|
||||||
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
import { registerPostgresCredentialsRotationRouter } from "./postgres-credentials-rotation-router";
|
||||||
@@ -15,6 +16,7 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record<
|
|||||||
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
|
[SecretRotation.PostgresCredentials]: registerPostgresCredentialsRotationRouter,
|
||||||
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
|
[SecretRotation.MsSqlCredentials]: registerMsSqlCredentialsRotationRouter,
|
||||||
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
|
[SecretRotation.Auth0ClientSecret]: registerAuth0ClientSecretRotationRouter,
|
||||||
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter,
|
[SecretRotation.AzureClientSecret]: registerAzureClientSecretRotationRouter,
|
||||||
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter
|
[SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter,
|
||||||
|
[SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||||
import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
|
import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret";
|
||||||
|
import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret";
|
||||||
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||||
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||||
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
import { PostgresCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||||
@@ -16,8 +17,9 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [
|
|||||||
PostgresCredentialsRotationListItemSchema,
|
PostgresCredentialsRotationListItemSchema,
|
||||||
MsSqlCredentialsRotationListItemSchema,
|
MsSqlCredentialsRotationListItemSchema,
|
||||||
Auth0ClientSecretRotationListItemSchema,
|
Auth0ClientSecretRotationListItemSchema,
|
||||||
LdapPasswordRotationListItemSchema,
|
AzureClientSecretRotationListItemSchema,
|
||||||
AwsIamUserSecretRotationListItemSchema
|
AwsIamUserSecretRotationListItemSchema,
|
||||||
|
LdapPasswordRotationListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
|
export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => {
|
||||||
|
|||||||
@@ -685,10 +685,16 @@ export const oidcConfigServiceFactory = ({
|
|||||||
id_token_signed_response_alg: oidcCfg.jwtSignatureAlgorithm
|
id_token_signed_response_alg: oidcCfg.jwtSignatureAlgorithm
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if the OIDC provider supports PKCE
|
||||||
|
const codeChallengeMethods = client.issuer.metadata.code_challenge_methods_supported;
|
||||||
|
const supportsPKCE = Array.isArray(codeChallengeMethods) && codeChallengeMethods.includes("S256");
|
||||||
|
|
||||||
const strategy = new OpenIdStrategy(
|
const strategy = new OpenIdStrategy(
|
||||||
{
|
{
|
||||||
client,
|
client,
|
||||||
passReqToCallback: true
|
passReqToCallback: true,
|
||||||
|
usePKCE: supportsPKCE,
|
||||||
|
params: supportsPKCE ? { code_challenge_method: "S256" } : undefined
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
(_req: any, tokenSet: TokenSet, cb: any) => {
|
(_req: any, tokenSet: TokenSet, cb: any) => {
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export enum OIDCConfigurationType {
|
|||||||
export enum OIDCJWTSignatureAlgorithm {
|
export enum OIDCJWTSignatureAlgorithm {
|
||||||
RS256 = "RS256",
|
RS256 = "RS256",
|
||||||
HS256 = "HS256",
|
HS256 = "HS256",
|
||||||
RS512 = "RS512"
|
RS512 = "RS512",
|
||||||
|
EDDSA = "EdDSA"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TOidcLoginDTO = {
|
export type TOidcLoginDTO = {
|
||||||
|
|||||||
@@ -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 AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = {
|
||||||
|
name: "Azure Client Secret",
|
||||||
|
type: SecretRotation.AzureClientSecret,
|
||||||
|
connection: AppConnection.AzureClientSecrets,
|
||||||
|
template: {
|
||||||
|
secretsMapping: {
|
||||||
|
clientId: "AZURE_CLIENT_ID",
|
||||||
|
clientSecret: "AZURE_CLIENT_SECRET"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AzureAddPasswordResponse,
|
||||||
|
TAzureClientSecretRotationGeneratedCredentials,
|
||||||
|
TAzureClientSecretRotationWithConnection
|
||||||
|
} from "@app/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-types";
|
||||||
|
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 { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-client-secrets";
|
||||||
|
|
||||||
|
const GRAPH_API_BASE = "https://graph.microsoft.com/v1.0";
|
||||||
|
|
||||||
|
type AzureErrorResponse = { error: { message: string } };
|
||||||
|
|
||||||
|
const sleep = async () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const azureClientSecretRotationFactory: TRotationFactory<
|
||||||
|
TAzureClientSecretRotationWithConnection,
|
||||||
|
TAzureClientSecretRotationGeneratedCredentials
|
||||||
|
> = (secretRotation, appConnectionDAL, kmsService) => {
|
||||||
|
const {
|
||||||
|
connection,
|
||||||
|
parameters: { objectId, clientId: clientIdParam },
|
||||||
|
secretsMapping
|
||||||
|
} = secretRotation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new client secret for the Azure app.
|
||||||
|
*/
|
||||||
|
const $rotateClientSecret = async () => {
|
||||||
|
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
|
||||||
|
const endpoint = `${GRAPH_API_BASE}/applications/${objectId}/addPassword`;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const formattedDate = `${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(
|
||||||
|
2,
|
||||||
|
"0"
|
||||||
|
)}-${now.getFullYear()}`;
|
||||||
|
|
||||||
|
const endDateTime = new Date();
|
||||||
|
endDateTime.setFullYear(now.getFullYear() + 5);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await request.post<AzureAddPasswordResponse>(
|
||||||
|
endpoint,
|
||||||
|
{
|
||||||
|
passwordCredential: {
|
||||||
|
displayName: `Infisical Rotated Secret (${formattedDate})`,
|
||||||
|
endDateTime: endDateTime.toISOString()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data?.secretText || !data?.keyId) {
|
||||||
|
throw new Error("Invalid response from Azure: missing secretText or keyId.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
clientSecret: data.secretText,
|
||||||
|
keyId: data.keyId,
|
||||||
|
clientId: clientIdParam
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
let message;
|
||||||
|
if (
|
||||||
|
error.response?.data &&
|
||||||
|
typeof error.response.data === "object" &&
|
||||||
|
"error" in error.response.data &&
|
||||||
|
typeof (error.response.data as AzureErrorResponse).error.message === "string"
|
||||||
|
) {
|
||||||
|
message = (error.response.data as AzureErrorResponse).error.message;
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to add client secret to Azure app ${objectId}: ${
|
||||||
|
message || error.message || "Unknown error"
|
||||||
|
}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to validate connection: verify credentials"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revokes a client secret from the Azure app using its keyId.
|
||||||
|
*/
|
||||||
|
const revokeCredential = async (keyId: string) => {
|
||||||
|
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
|
||||||
|
const endpoint = `${GRAPH_API_BASE}/applications/${objectId}/removePassword`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await request.post(
|
||||||
|
endpoint,
|
||||||
|
{ keyId },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
let message;
|
||||||
|
if (
|
||||||
|
error.response?.data &&
|
||||||
|
typeof error.response.data === "object" &&
|
||||||
|
"error" in error.response.data &&
|
||||||
|
typeof (error.response.data as AzureErrorResponse).error.message === "string"
|
||||||
|
) {
|
||||||
|
message = (error.response.data as AzureErrorResponse).error.message;
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to remove client secret with keyId ${keyId} from app ${objectId}: ${
|
||||||
|
message || error.message || "Unknown error"
|
||||||
|
}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to validate connection: verify credentials"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issues a new set of credentials.
|
||||||
|
*/
|
||||||
|
const issueCredentials: TRotationFactoryIssueCredentials<TAzureClientSecretRotationGeneratedCredentials> = async (
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
const credentials = await $rotateClientSecret();
|
||||||
|
return callback(credentials);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revokes a list of credentials.
|
||||||
|
*/
|
||||||
|
const revokeCredentials: TRotationFactoryRevokeCredentials<TAzureClientSecretRotationGeneratedCredentials> = async (
|
||||||
|
credentials,
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
if (!credentials?.length) return callback();
|
||||||
|
|
||||||
|
for (const { keyId } of credentials) {
|
||||||
|
await revokeCredential(keyId);
|
||||||
|
await sleep();
|
||||||
|
}
|
||||||
|
return callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates credentials by issuing new ones and revoking the old.
|
||||||
|
*/
|
||||||
|
const rotateCredentials: TRotationFactoryRotateCredentials<TAzureClientSecretRotationGeneratedCredentials> = async (
|
||||||
|
oldCredentials,
|
||||||
|
callback
|
||||||
|
) => {
|
||||||
|
const newCredentials = await $rotateClientSecret();
|
||||||
|
if (oldCredentials?.keyId) {
|
||||||
|
await revokeCredential(oldCredentials.keyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(newCredentials);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the generated credentials into the secret payload format.
|
||||||
|
*/
|
||||||
|
const getSecretsPayload: TRotationFactoryGetSecretsPayload<TAzureClientSecretRotationGeneratedCredentials> = ({
|
||||||
|
clientSecret
|
||||||
|
}) => [
|
||||||
|
{ key: secretsMapping.clientSecret, value: clientSecret },
|
||||||
|
{ key: secretsMapping.clientId, value: clientIdParam }
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
issueCredentials,
|
||||||
|
revokeCredentials,
|
||||||
|
rotateCredentials,
|
||||||
|
getSecretsPayload
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
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 AzureClientSecretRotationGeneratedCredentialsSchema = z
|
||||||
|
.object({
|
||||||
|
clientId: z.string(),
|
||||||
|
clientSecret: z.string(),
|
||||||
|
keyId: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.max(2);
|
||||||
|
|
||||||
|
const AzureClientSecretRotationParametersSchema = z.object({
|
||||||
|
objectId: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Object ID Required")
|
||||||
|
.describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.objectId),
|
||||||
|
appName: z.string().trim().describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.appName).optional(),
|
||||||
|
clientId: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Client ID Required")
|
||||||
|
.describe(SecretRotations.PARAMETERS.AZURE_CLIENT_SECRET.clientId)
|
||||||
|
});
|
||||||
|
|
||||||
|
const AzureClientSecretRotationSecretsMappingSchema = z.object({
|
||||||
|
clientId: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AZURE_CLIENT_SECRET.clientId),
|
||||||
|
clientSecret: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.AZURE_CLIENT_SECRET.clientSecret)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureClientSecretRotationTemplateSchema = z.object({
|
||||||
|
secretsMapping: z.object({
|
||||||
|
clientId: z.string(),
|
||||||
|
clientSecret: z.string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureClientSecretRotationSchema = BaseSecretRotationSchema(SecretRotation.AzureClientSecret).extend({
|
||||||
|
type: z.literal(SecretRotation.AzureClientSecret),
|
||||||
|
parameters: AzureClientSecretRotationParametersSchema,
|
||||||
|
secretsMapping: AzureClientSecretRotationSecretsMappingSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateAzureClientSecretRotationSchema = BaseCreateSecretRotationSchema(
|
||||||
|
SecretRotation.AzureClientSecret
|
||||||
|
).extend({
|
||||||
|
parameters: AzureClientSecretRotationParametersSchema,
|
||||||
|
secretsMapping: AzureClientSecretRotationSecretsMappingSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateAzureClientSecretRotationSchema = BaseUpdateSecretRotationSchema(
|
||||||
|
SecretRotation.AzureClientSecret
|
||||||
|
).extend({
|
||||||
|
parameters: AzureClientSecretRotationParametersSchema.optional(),
|
||||||
|
secretsMapping: AzureClientSecretRotationSecretsMappingSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureClientSecretRotationListItemSchema = z.object({
|
||||||
|
name: z.literal("Azure Client Secret"),
|
||||||
|
connection: z.literal(AppConnection.AzureClientSecrets),
|
||||||
|
type: z.literal(SecretRotation.AzureClientSecret),
|
||||||
|
template: AzureClientSecretRotationTemplateSchema
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TAzureClientSecretsConnection } from "@app/services/app-connection/azure-client-secrets";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AzureClientSecretRotationGeneratedCredentialsSchema,
|
||||||
|
AzureClientSecretRotationListItemSchema,
|
||||||
|
AzureClientSecretRotationSchema,
|
||||||
|
CreateAzureClientSecretRotationSchema
|
||||||
|
} from "./azure-client-secret-rotation-schemas";
|
||||||
|
|
||||||
|
export type TAzureClientSecretRotation = z.infer<typeof AzureClientSecretRotationSchema>;
|
||||||
|
|
||||||
|
export type TAzureClientSecretRotationInput = z.infer<typeof CreateAzureClientSecretRotationSchema>;
|
||||||
|
|
||||||
|
export type TAzureClientSecretRotationListItem = z.infer<typeof AzureClientSecretRotationListItemSchema>;
|
||||||
|
|
||||||
|
export type TAzureClientSecretRotationWithConnection = TAzureClientSecretRotation & {
|
||||||
|
connection: TAzureClientSecretsConnection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAzureClientSecretRotationGeneratedCredentials = z.infer<
|
||||||
|
typeof AzureClientSecretRotationGeneratedCredentialsSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface TAzureClientSecretRotationParameters {
|
||||||
|
appId: string;
|
||||||
|
keyId?: string;
|
||||||
|
displayName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TAzureClientSecretRotationSecretsMapping {
|
||||||
|
appId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
keyId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AzureAddPasswordResponse {
|
||||||
|
secretText: string;
|
||||||
|
keyId: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./azure-client-secret-rotation-constants";
|
||||||
|
export * from "./azure-client-secret-rotation-schemas";
|
||||||
|
export * from "./azure-client-secret-rotation-types";
|
||||||
@@ -2,8 +2,9 @@ export enum SecretRotation {
|
|||||||
PostgresCredentials = "postgres-credentials",
|
PostgresCredentials = "postgres-credentials",
|
||||||
MsSqlCredentials = "mssql-credentials",
|
MsSqlCredentials = "mssql-credentials",
|
||||||
Auth0ClientSecret = "auth0-client-secret",
|
Auth0ClientSecret = "auth0-client-secret",
|
||||||
LdapPassword = "ldap-password",
|
AzureClientSecret = "azure-client-secret",
|
||||||
AwsIamUserSecret = "aws-iam-user-secret"
|
AwsIamUserSecret = "aws-iam-user-secret",
|
||||||
|
LdapPassword = "ldap-password"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SecretRotationStatus {
|
export enum SecretRotationStatus {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
|
|||||||
|
|
||||||
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
|
import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret";
|
||||||
import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret";
|
import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret";
|
||||||
|
import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret";
|
||||||
import { LDAP_PASSWORD_ROTATION_LIST_OPTION } from "./ldap-password";
|
import { LDAP_PASSWORD_ROTATION_LIST_OPTION } from "./ldap-password";
|
||||||
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
|
import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials";
|
||||||
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
import { POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION } from "./postgres-credentials";
|
||||||
@@ -21,8 +22,9 @@ const SECRET_ROTATION_LIST_OPTIONS: Record<SecretRotation, TSecretRotationV2List
|
|||||||
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
|
[SecretRotation.PostgresCredentials]: POSTGRES_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||||
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
|
[SecretRotation.MsSqlCredentials]: MSSQL_CREDENTIALS_ROTATION_LIST_OPTION,
|
||||||
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION,
|
[SecretRotation.Auth0ClientSecret]: AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION,
|
||||||
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION,
|
[SecretRotation.AzureClientSecret]: AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION,
|
||||||
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION
|
[SecretRotation.AwsIamUserSecret]: AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION,
|
||||||
|
[SecretRotation.LdapPassword]: LDAP_PASSWORD_ROTATION_LIST_OPTION
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listSecretRotationOptions = () => {
|
export const listSecretRotationOptions = () => {
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ export const SECRET_ROTATION_NAME_MAP: Record<SecretRotation, string> = {
|
|||||||
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
|
[SecretRotation.PostgresCredentials]: "PostgreSQL Credentials",
|
||||||
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials",
|
[SecretRotation.MsSqlCredentials]: "Microsoft SQL Server Credentials",
|
||||||
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
|
[SecretRotation.Auth0ClientSecret]: "Auth0 Client Secret",
|
||||||
[SecretRotation.LdapPassword]: "LDAP Password",
|
[SecretRotation.AzureClientSecret]: "Azure Client Secret",
|
||||||
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret"
|
[SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret",
|
||||||
|
[SecretRotation.LdapPassword]: "LDAP Password"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
export const SECRET_ROTATION_CONNECTION_MAP: Record<SecretRotation, AppConnection> = {
|
||||||
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
[SecretRotation.PostgresCredentials]: AppConnection.Postgres,
|
||||||
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
|
[SecretRotation.MsSqlCredentials]: AppConnection.MsSql,
|
||||||
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
|
[SecretRotation.Auth0ClientSecret]: AppConnection.Auth0,
|
||||||
[SecretRotation.LdapPassword]: AppConnection.LDAP,
|
[SecretRotation.AzureClientSecret]: AppConnection.AzureClientSecrets,
|
||||||
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS
|
[SecretRotation.AwsIamUserSecret]: AppConnection.AWS,
|
||||||
|
[SecretRotation.LdapPassword]: AppConnection.LDAP
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns";
|
import { auth0ClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/auth0-client-secret/auth0-client-secret-rotation-fns";
|
||||||
|
import { azureClientSecretRotationFactory } from "@app/ee/services/secret-rotation-v2/azure-client-secret/azure-client-secret-rotation-fns";
|
||||||
import { ldapPasswordRotationFactory } from "@app/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns";
|
import { ldapPasswordRotationFactory } from "@app/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns";
|
||||||
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
import { SecretRotation, SecretRotationStatus } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums";
|
||||||
import {
|
import {
|
||||||
@@ -102,7 +103,7 @@ export type TSecretRotationV2ServiceFactoryDep = {
|
|||||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
|
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "removeSecretReminder">;
|
||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
queueService: Pick<TQueueServiceFactory, "queuePg">;
|
queueService: Pick<TQueueServiceFactory, "queuePg">;
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
|
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
|
||||||
@@ -117,8 +118,9 @@ const SECRET_ROTATION_FACTORY_MAP: Record<SecretRotation, TRotationFactoryImplem
|
|||||||
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
[SecretRotation.PostgresCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||||
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
[SecretRotation.MsSqlCredentials]: sqlCredentialsRotationFactory as TRotationFactoryImplementation,
|
||||||
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
|
[SecretRotation.Auth0ClientSecret]: auth0ClientSecretRotationFactory as TRotationFactoryImplementation,
|
||||||
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation,
|
[SecretRotation.AzureClientSecret]: azureClientSecretRotationFactory as TRotationFactoryImplementation,
|
||||||
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation
|
[SecretRotation.AwsIamUserSecret]: awsIamUserSecretRotationFactory as TRotationFactoryImplementation,
|
||||||
|
[SecretRotation.LdapPassword]: ldapPasswordRotationFactory as TRotationFactoryImplementation
|
||||||
};
|
};
|
||||||
|
|
||||||
export const secretRotationV2ServiceFactory = ({
|
export const secretRotationV2ServiceFactory = ({
|
||||||
@@ -447,7 +449,8 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
{
|
{
|
||||||
parameters: payload.parameters,
|
parameters: payload.parameters,
|
||||||
secretsMapping,
|
secretsMapping,
|
||||||
connection
|
connection,
|
||||||
|
rotationInterval: payload.rotationInterval
|
||||||
} as TSecretRotationV2WithConnection,
|
} as TSecretRotationV2WithConnection,
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ import {
|
|||||||
TAwsIamUserSecretRotationListItem,
|
TAwsIamUserSecretRotationListItem,
|
||||||
TAwsIamUserSecretRotationWithConnection
|
TAwsIamUserSecretRotationWithConnection
|
||||||
} from "./aws-iam-user-secret";
|
} from "./aws-iam-user-secret";
|
||||||
|
import {
|
||||||
|
TAzureClientSecretRotation,
|
||||||
|
TAzureClientSecretRotationGeneratedCredentials,
|
||||||
|
TAzureClientSecretRotationInput,
|
||||||
|
TAzureClientSecretRotationListItem,
|
||||||
|
TAzureClientSecretRotationWithConnection
|
||||||
|
} from "./azure-client-secret";
|
||||||
import {
|
import {
|
||||||
TLdapPasswordRotation,
|
TLdapPasswordRotation,
|
||||||
TLdapPasswordRotationGeneratedCredentials,
|
TLdapPasswordRotationGeneratedCredentials,
|
||||||
@@ -45,6 +52,7 @@ export type TSecretRotationV2 =
|
|||||||
| TPostgresCredentialsRotation
|
| TPostgresCredentialsRotation
|
||||||
| TMsSqlCredentialsRotation
|
| TMsSqlCredentialsRotation
|
||||||
| TAuth0ClientSecretRotation
|
| TAuth0ClientSecretRotation
|
||||||
|
| TAzureClientSecretRotation
|
||||||
| TLdapPasswordRotation
|
| TLdapPasswordRotation
|
||||||
| TAwsIamUserSecretRotation;
|
| TAwsIamUserSecretRotation;
|
||||||
|
|
||||||
@@ -52,12 +60,14 @@ export type TSecretRotationV2WithConnection =
|
|||||||
| TPostgresCredentialsRotationWithConnection
|
| TPostgresCredentialsRotationWithConnection
|
||||||
| TMsSqlCredentialsRotationWithConnection
|
| TMsSqlCredentialsRotationWithConnection
|
||||||
| TAuth0ClientSecretRotationWithConnection
|
| TAuth0ClientSecretRotationWithConnection
|
||||||
|
| TAzureClientSecretRotationWithConnection
|
||||||
| TLdapPasswordRotationWithConnection
|
| TLdapPasswordRotationWithConnection
|
||||||
| TAwsIamUserSecretRotationWithConnection;
|
| TAwsIamUserSecretRotationWithConnection;
|
||||||
|
|
||||||
export type TSecretRotationV2GeneratedCredentials =
|
export type TSecretRotationV2GeneratedCredentials =
|
||||||
| TSqlCredentialsRotationGeneratedCredentials
|
| TSqlCredentialsRotationGeneratedCredentials
|
||||||
| TAuth0ClientSecretRotationGeneratedCredentials
|
| TAuth0ClientSecretRotationGeneratedCredentials
|
||||||
|
| TAzureClientSecretRotationGeneratedCredentials
|
||||||
| TLdapPasswordRotationGeneratedCredentials
|
| TLdapPasswordRotationGeneratedCredentials
|
||||||
| TAwsIamUserSecretRotationGeneratedCredentials;
|
| TAwsIamUserSecretRotationGeneratedCredentials;
|
||||||
|
|
||||||
@@ -65,6 +75,7 @@ export type TSecretRotationV2Input =
|
|||||||
| TPostgresCredentialsRotationInput
|
| TPostgresCredentialsRotationInput
|
||||||
| TMsSqlCredentialsRotationInput
|
| TMsSqlCredentialsRotationInput
|
||||||
| TAuth0ClientSecretRotationInput
|
| TAuth0ClientSecretRotationInput
|
||||||
|
| TAzureClientSecretRotationInput
|
||||||
| TLdapPasswordRotationInput
|
| TLdapPasswordRotationInput
|
||||||
| TAwsIamUserSecretRotationInput;
|
| TAwsIamUserSecretRotationInput;
|
||||||
|
|
||||||
@@ -72,6 +83,7 @@ export type TSecretRotationV2ListItem =
|
|||||||
| TPostgresCredentialsRotationListItem
|
| TPostgresCredentialsRotationListItem
|
||||||
| TMsSqlCredentialsRotationListItem
|
| TMsSqlCredentialsRotationListItem
|
||||||
| TAuth0ClientSecretRotationListItem
|
| TAuth0ClientSecretRotationListItem
|
||||||
|
| TAzureClientSecretRotationListItem
|
||||||
| TLdapPasswordRotationListItem
|
| TLdapPasswordRotationListItem
|
||||||
| TAwsIamUserSecretRotationListItem;
|
| TAwsIamUserSecretRotationListItem;
|
||||||
|
|
||||||
@@ -197,7 +209,7 @@ export type TRotationFactory<
|
|||||||
C extends TSecretRotationV2GeneratedCredentials
|
C extends TSecretRotationV2GeneratedCredentials
|
||||||
> = (
|
> = (
|
||||||
secretRotation: T,
|
secretRotation: T,
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
) => {
|
) => {
|
||||||
issueCredentials: TRotationFactoryIssueCredentials<C>;
|
issueCredentials: TRotationFactoryIssueCredentials<C>;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
import { Auth0ClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/auth0-client-secret";
|
||||||
|
import { AzureClientSecretRotationSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret";
|
||||||
import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
import { LdapPasswordRotationSchema } from "@app/ee/services/secret-rotation-v2/ldap-password";
|
||||||
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
import { MsSqlCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials";
|
||||||
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
import { PostgresCredentialsRotationSchema } from "@app/ee/services/secret-rotation-v2/postgres-credentials";
|
||||||
@@ -11,6 +12,7 @@ export const SecretRotationV2Schema = z.discriminatedUnion("type", [
|
|||||||
PostgresCredentialsRotationSchema,
|
PostgresCredentialsRotationSchema,
|
||||||
MsSqlCredentialsRotationSchema,
|
MsSqlCredentialsRotationSchema,
|
||||||
Auth0ClientSecretRotationSchema,
|
Auth0ClientSecretRotationSchema,
|
||||||
|
AzureClientSecretRotationSchema,
|
||||||
LdapPasswordRotationSchema,
|
LdapPasswordRotationSchema,
|
||||||
AwsIamUserSecretRotationSchema
|
AwsIamUserSecretRotationSchema
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1912,6 +1912,10 @@ export const AppConnections = {
|
|||||||
TEAMCITY: {
|
TEAMCITY: {
|
||||||
instanceUrl: "The TeamCity instance URL to connect with.",
|
instanceUrl: "The TeamCity instance URL to connect with.",
|
||||||
accessToken: "The access token to use to connect with TeamCity."
|
accessToken: "The access token to use to connect with TeamCity."
|
||||||
|
},
|
||||||
|
AZURE_CLIENT_SECRETS: {
|
||||||
|
code: "The OAuth code to use to connect with Azure Client Secrets.",
|
||||||
|
tenantId: "The Tenant ID to use to connect with Azure Client Secrets."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2120,6 +2124,11 @@ export const SecretRotations = {
|
|||||||
AUTH0_CLIENT_SECRET: {
|
AUTH0_CLIENT_SECRET: {
|
||||||
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
|
clientId: "The client ID of the Auth0 Application to rotate the client secret for."
|
||||||
},
|
},
|
||||||
|
AZURE_CLIENT_SECRET: {
|
||||||
|
objectId: "The ID of the Azure Application to rotate the client secret for.",
|
||||||
|
appName: "The name of the Azure Application to rotate the client secret for.",
|
||||||
|
clientId: "The client ID of the Azure Application to rotate the client secret for."
|
||||||
|
},
|
||||||
LDAP_PASSWORD: {
|
LDAP_PASSWORD: {
|
||||||
dn: "The Distinguished Name (DN) of the principal to rotate the password for."
|
dn: "The Distinguished Name (DN) of the principal to rotate the password for."
|
||||||
},
|
},
|
||||||
@@ -2150,6 +2159,10 @@ export const SecretRotations = {
|
|||||||
clientId: "The name of the secret that the client ID will be mapped to.",
|
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."
|
clientSecret: "The name of the secret that the rotated client secret will be mapped to."
|
||||||
},
|
},
|
||||||
|
AZURE_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."
|
||||||
|
},
|
||||||
LDAP_PASSWORD: {
|
LDAP_PASSWORD: {
|
||||||
dn: "The name of the secret that the Distinguished Name (DN) of the principal will be mapped to.",
|
dn: "The name of the secret that the Distinguished Name (DN) of the principal will be mapped to.",
|
||||||
password: "The name of the secret that the rotated password will be mapped to."
|
password: "The name of the secret that the rotated password will be mapped to."
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000;
|
|||||||
|
|
||||||
export const secondsToMillis = (seconds: number) => seconds * 1000;
|
export const secondsToMillis = (seconds: number) => seconds * 1000;
|
||||||
|
|
||||||
export const applyJitter = (delayMs: number, jitterMs: number) => {
|
export const applyJitter = (delay: number, jitter: number) => {
|
||||||
const jitter = Math.floor(Math.random() * (2 * jitterMs)) - jitterMs;
|
const jitterTime = Math.floor(Math.random() * (2 * jitter)) - jitter;
|
||||||
return delayMs + jitter;
|
return delay + jitterTime;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export * from "./array";
|
|||||||
export * from "./dates";
|
export * from "./dates";
|
||||||
export * from "./object";
|
export * from "./object";
|
||||||
export * from "./string";
|
export * from "./string";
|
||||||
|
export * from "./time";
|
||||||
export * from "./undefined";
|
export * from "./undefined";
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import ms, { StringValue } from "ms";
|
||||||
|
|
||||||
|
const convertToMilliseconds = (exp: string | number): number => {
|
||||||
|
if (typeof exp === "number") {
|
||||||
|
return exp * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = ms(exp as StringValue);
|
||||||
|
if (typeof result !== "number") {
|
||||||
|
throw new Error(`Invalid expiration format: ${exp}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMinExpiresIn = (exp1: string | number, exp2: string | number): string | number => {
|
||||||
|
const ms1 = convertToMilliseconds(exp1);
|
||||||
|
const ms2 = convertToMilliseconds(exp2);
|
||||||
|
|
||||||
|
return ms1 <= ms2 ? exp1 : exp2;
|
||||||
|
};
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
import { Tables } from "knex/types/tables";
|
import { Tables } from "knex/types/tables";
|
||||||
|
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
|
||||||
import { DatabaseError } from "../errors";
|
import { DatabaseError } from "../errors";
|
||||||
import { buildDynamicKnexQuery, TKnexDynamicOperator } from "./dynamic";
|
import { buildDynamicKnexQuery, TKnexDynamicOperator } from "./dynamic";
|
||||||
|
|
||||||
@@ -25,28 +27,41 @@ export type TFindFilter<R extends object = object> = Partial<R> & {
|
|||||||
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
$search?: Partial<{ [k in keyof R]: R[k] }>;
|
||||||
$complex?: TKnexDynamicOperator<R>;
|
$complex?: TKnexDynamicOperator<R>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildFindFilter =
|
export const buildFindFilter =
|
||||||
<R extends object = object>({ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>) =>
|
<R extends object = object>(
|
||||||
|
{ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>,
|
||||||
|
tableName?: TableName,
|
||||||
|
excludeKeys?: Array<keyof R>
|
||||||
|
) =>
|
||||||
(bd: Knex.QueryBuilder<R, R>) => {
|
(bd: Knex.QueryBuilder<R, R>) => {
|
||||||
void bd.where(filter);
|
const processedFilter = tableName
|
||||||
|
? Object.fromEntries(
|
||||||
|
Object.entries(filter)
|
||||||
|
.filter(([key]) => !excludeKeys || !excludeKeys.includes(key as keyof R))
|
||||||
|
.map(([key, value]) => [`${tableName}.${key}`, value])
|
||||||
|
)
|
||||||
|
: filter;
|
||||||
|
|
||||||
|
void bd.where(processedFilter);
|
||||||
if ($in) {
|
if ($in) {
|
||||||
Object.entries($in).forEach(([key, val]) => {
|
Object.entries($in).forEach(([key, val]) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
void bd.whereIn(key as never, val as never);
|
void bd.whereIn(`${tableName ? `${tableName}.` : ""}${key}`, val as never);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($notNull?.length) {
|
if ($notNull?.length) {
|
||||||
$notNull.forEach((key) => {
|
$notNull.forEach((key) => {
|
||||||
void bd.whereNotNull(key as never);
|
void bd.whereNotNull(`${tableName ? `${tableName}.` : ""}${key as string}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($search) {
|
if ($search) {
|
||||||
Object.entries($search).forEach(([key, val]) => {
|
Object.entries($search).forEach(([key, val]) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
void bd.whereILike(key as never, val as never);
|
void bd.whereILike(`${tableName ? `${tableName}.` : ""}${key}`, val as never);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,3 +16,17 @@ export const fetchGithubEmails = async (accessToken: string) => {
|
|||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TGithubUser = {
|
||||||
|
name?: string;
|
||||||
|
login: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchGithubUser = async (accessToken: string) => {
|
||||||
|
const { data } = await request.get<TGithubUser>(`${INTEGRATION_GITHUB_API_URL}/user`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
|||||||
|
|
||||||
const validUrl = new URL(url);
|
const validUrl = new URL(url);
|
||||||
const inputHostIps: string[] = [];
|
const inputHostIps: string[] = [];
|
||||||
if (isIPv4(validUrl.host)) {
|
if (isIPv4(validUrl.hostname)) {
|
||||||
inputHostIps.push(validUrl.host);
|
inputHostIps.push(validUrl.hostname);
|
||||||
} else {
|
} else {
|
||||||
if (validUrl.host === "localhost" || validUrl.host === "host.docker.internal") {
|
if (validUrl.hostname === "localhost" || validUrl.hostname === "host.docker.internal") {
|
||||||
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||||
}
|
}
|
||||||
const resolvedIps = await dns.resolve4(validUrl.host);
|
const resolvedIps = await dns.resolve4(validUrl.hostname);
|
||||||
inputHostIps.push(...resolvedIps);
|
inputHostIps.push(...resolvedIps);
|
||||||
}
|
}
|
||||||
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||||
|
|||||||
@@ -1559,6 +1559,7 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const secretSyncService = secretSyncServiceFactory({
|
const secretSyncService = secretSyncServiceFactory({
|
||||||
secretSyncDAL,
|
secretSyncDAL,
|
||||||
|
secretImportDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import {
|
|||||||
AzureAppConfigurationConnectionListItemSchema,
|
AzureAppConfigurationConnectionListItemSchema,
|
||||||
SanitizedAzureAppConfigurationConnectionSchema
|
SanitizedAzureAppConfigurationConnectionSchema
|
||||||
} from "@app/services/app-connection/azure-app-configuration";
|
} from "@app/services/app-connection/azure-app-configuration";
|
||||||
|
import {
|
||||||
|
AzureClientSecretsConnectionListItemSchema,
|
||||||
|
SanitizedAzureClientSecretsConnectionSchema
|
||||||
|
} from "@app/services/app-connection/azure-client-secrets";
|
||||||
import {
|
import {
|
||||||
AzureKeyVaultConnectionListItemSchema,
|
AzureKeyVaultConnectionListItemSchema,
|
||||||
SanitizedAzureKeyVaultConnectionSchema
|
SanitizedAzureKeyVaultConnectionSchema
|
||||||
@@ -63,8 +67,9 @@ const SanitizedAppConnectionSchema = z.union([
|
|||||||
...SanitizedPostgresConnectionSchema.options,
|
...SanitizedPostgresConnectionSchema.options,
|
||||||
...SanitizedMsSqlConnectionSchema.options,
|
...SanitizedMsSqlConnectionSchema.options,
|
||||||
...SanitizedCamundaConnectionSchema.options,
|
...SanitizedCamundaConnectionSchema.options,
|
||||||
...SanitizedWindmillConnectionSchema.options,
|
|
||||||
...SanitizedAuth0ConnectionSchema.options,
|
...SanitizedAuth0ConnectionSchema.options,
|
||||||
|
...SanitizedAzureClientSecretsConnectionSchema.options,
|
||||||
|
...SanitizedWindmillConnectionSchema.options,
|
||||||
...SanitizedLdapConnectionSchema.options,
|
...SanitizedLdapConnectionSchema.options,
|
||||||
...SanitizedTeamCityConnectionSchema.options
|
...SanitizedTeamCityConnectionSchema.options
|
||||||
]);
|
]);
|
||||||
@@ -82,8 +87,9 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
|||||||
PostgresConnectionListItemSchema,
|
PostgresConnectionListItemSchema,
|
||||||
MsSqlConnectionListItemSchema,
|
MsSqlConnectionListItemSchema,
|
||||||
CamundaConnectionListItemSchema,
|
CamundaConnectionListItemSchema,
|
||||||
WindmillConnectionListItemSchema,
|
|
||||||
Auth0ConnectionListItemSchema,
|
Auth0ConnectionListItemSchema,
|
||||||
|
AzureClientSecretsConnectionListItemSchema,
|
||||||
|
WindmillConnectionListItemSchema,
|
||||||
LdapConnectionListItemSchema,
|
LdapConnectionListItemSchema,
|
||||||
TeamCityConnectionListItemSchema
|
TeamCityConnectionListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
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 {
|
||||||
|
CreateAzureClientSecretsConnectionSchema,
|
||||||
|
SanitizedAzureClientSecretsConnectionSchema,
|
||||||
|
UpdateAzureClientSecretsConnectionSchema
|
||||||
|
} from "@app/services/app-connection/azure-client-secrets";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
export const registerAzureClientSecretsConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
|
registerAppConnectionEndpoints({
|
||||||
|
app: AppConnection.AzureClientSecrets,
|
||||||
|
server,
|
||||||
|
sanitizedResponseSchema: SanitizedAzureClientSecretsConnectionSchema,
|
||||||
|
createSchema: CreateAzureClientSecretsConnectionSchema,
|
||||||
|
updateSchema: UpdateAzureClientSecretsConnectionSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: `/:connectionId/clients`,
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
connectionId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
clients: z.object({ name: z.string(), id: z.string(), appId: z.string() }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
const clients = await server.services.appConnection.azureClientSecrets.listApps(connectionId, req.permission);
|
||||||
|
|
||||||
|
return { clients };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
|||||||
import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
|
import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
|
||||||
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||||
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
|
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
|
||||||
|
import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router";
|
||||||
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
|
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
|
||||||
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||||
@@ -26,6 +27,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
|||||||
[AppConnection.GCP]: registerGcpConnectionRouter,
|
[AppConnection.GCP]: registerGcpConnectionRouter,
|
||||||
[AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter,
|
[AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter,
|
||||||
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
||||||
|
[AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter,
|
||||||
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
|
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
|
||||||
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
|
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
|
||||||
[AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter,
|
[AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import jwt from "jsonwebtoken";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { getMinExpiresIn } from "@app/lib/fn";
|
||||||
import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
@@ -79,6 +80,18 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
|
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
|
||||||
|
if (decodedToken.organizationId) {
|
||||||
|
const org = await server.services.org.findOrganizationById(
|
||||||
|
decodedToken.userId,
|
||||||
|
decodedToken.organizationId,
|
||||||
|
decodedToken.authMethod,
|
||||||
|
decodedToken.organizationId
|
||||||
|
);
|
||||||
|
if (org && org.userTokenExpiration) {
|
||||||
|
expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{
|
{
|
||||||
@@ -92,7 +105,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
mfaMethod: decodedToken.mfaMethod
|
mfaMethod: decodedToken.mfaMethod
|
||||||
},
|
},
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
{ expiresIn }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { token, organizationId: decodedToken.organizationId };
|
return { token, organizationId: decodedToken.organizationId };
|
||||||
|
|||||||
@@ -154,7 +154,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets: z
|
secrets: z
|
||||||
.object({
|
.object({
|
||||||
secretId: z.string(),
|
secretId: z.string(),
|
||||||
referencedSecretKey: z.string()
|
referencedSecretKey: z.string(),
|
||||||
|
referencedSecretEnv: z.string()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -166,6 +167,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
usedBySecretSyncs: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
destination: z.string(),
|
||||||
|
environment: z.string(),
|
||||||
|
id: z.string(),
|
||||||
|
path: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
totalFolderCount: z.number().optional(),
|
totalFolderCount: z.number().optional(),
|
||||||
totalDynamicSecretCount: z.number().optional(),
|
totalDynamicSecretCount: z.number().optional(),
|
||||||
totalSecretCount: z.number().optional(),
|
totalSecretCount: z.number().optional(),
|
||||||
@@ -500,6 +511,24 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const usedBySecretSyncs: { name: string; destination: string; environment: string; id: string; path: string }[] =
|
||||||
|
[];
|
||||||
|
for await (const environment of environments) {
|
||||||
|
const secretSyncs = await server.services.secretSync.listSecretSyncsBySecretPath(
|
||||||
|
{ projectId, secretPath, environment },
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
secretSyncs.forEach((sync) => {
|
||||||
|
usedBySecretSyncs.push({
|
||||||
|
name: sync.name,
|
||||||
|
destination: sync.destination,
|
||||||
|
environment,
|
||||||
|
id: sync.id,
|
||||||
|
path: sync.folder?.path || "/"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
folders,
|
folders,
|
||||||
dynamicSecrets,
|
dynamicSecrets,
|
||||||
@@ -512,6 +541,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalSecretCount,
|
totalSecretCount,
|
||||||
totalSecretRotationCount,
|
totalSecretRotationCount,
|
||||||
importedByEnvs,
|
importedByEnvs,
|
||||||
|
usedBySecretSyncs,
|
||||||
totalCount:
|
totalCount:
|
||||||
(totalFolderCount ?? 0) +
|
(totalFolderCount ?? 0) +
|
||||||
(totalDynamicSecretCount ?? 0) +
|
(totalDynamicSecretCount ?? 0) +
|
||||||
@@ -611,6 +641,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalFolderCount: z.number().optional(),
|
totalFolderCount: z.number().optional(),
|
||||||
totalDynamicSecretCount: z.number().optional(),
|
totalDynamicSecretCount: z.number().optional(),
|
||||||
totalSecretCount: z.number().optional(),
|
totalSecretCount: z.number().optional(),
|
||||||
|
usedBySecretSyncs: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
destination: z.string(),
|
||||||
|
environment: z.string(),
|
||||||
|
id: z.string(),
|
||||||
|
path: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
importedBy: z
|
importedBy: z
|
||||||
.object({
|
.object({
|
||||||
environment: z.object({
|
environment: z.object({
|
||||||
@@ -624,7 +664,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets: z
|
secrets: z
|
||||||
.object({
|
.object({
|
||||||
secretId: z.string(),
|
secretId: z.string(),
|
||||||
referencedSecretKey: z.string()
|
referencedSecretKey: z.string(),
|
||||||
|
referencedSecretEnv: z.string()
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -904,6 +945,18 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
secrets
|
secrets
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const secretSyncs = await server.services.secretSync.listSecretSyncsBySecretPath(
|
||||||
|
{ projectId, secretPath, environment },
|
||||||
|
req.permission
|
||||||
|
);
|
||||||
|
const usedBySecretSyncs = secretSyncs.map((sync) => ({
|
||||||
|
name: sync.name,
|
||||||
|
destination: sync.destination,
|
||||||
|
environment: sync.environment?.name || environment,
|
||||||
|
id: sync.id,
|
||||||
|
path: sync.folder?.path || "/"
|
||||||
|
}));
|
||||||
|
|
||||||
if (secrets?.length || secretRotations?.length) {
|
if (secrets?.length || secretRotations?.length) {
|
||||||
const secretCount =
|
const secretCount =
|
||||||
(secrets?.length ?? 0) +
|
(secrets?.length ?? 0) +
|
||||||
@@ -950,6 +1003,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
totalSecretCount,
|
totalSecretCount,
|
||||||
totalSecretRotationCount,
|
totalSecretRotationCount,
|
||||||
importedBy,
|
importedBy,
|
||||||
|
usedBySecretSyncs,
|
||||||
totalCount:
|
totalCount:
|
||||||
(totalImportCount ?? 0) +
|
(totalImportCount ?? 0) +
|
||||||
(totalFolderCount ?? 0) +
|
(totalFolderCount ?? 0) +
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -263,7 +264,18 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
enforceMfa: z.boolean().optional(),
|
enforceMfa: z.boolean().optional(),
|
||||||
selectedMfaMethod: z.nativeEnum(MfaMethod).optional(),
|
selectedMfaMethod: z.nativeEnum(MfaMethod).optional(),
|
||||||
allowSecretSharingOutsideOrganization: z.boolean().optional(),
|
allowSecretSharingOutsideOrganization: z.boolean().optional(),
|
||||||
bypassOrgAuthEnabled: z.boolean().optional()
|
bypassOrgAuthEnabled: z.boolean().optional(),
|
||||||
|
userTokenExpiration: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => new RE2(/^\d+[mhdw]$/).test(val), "Must be a number followed by m, h, d, or w")
|
||||||
|
.refine(
|
||||||
|
(val) => {
|
||||||
|
const numericPart = val.slice(0, -1);
|
||||||
|
return parseInt(numericPart, 10) >= 1;
|
||||||
|
},
|
||||||
|
{ message: "Duration value must be at least 1" }
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
import { Authenticator } from "@fastify/passport";
|
import { Authenticator } from "@fastify/passport";
|
||||||
import fastifySession from "@fastify/session";
|
import fastifySession from "@fastify/session";
|
||||||
import RedisStore from "connect-redis";
|
import RedisStore from "connect-redis";
|
||||||
import { Strategy as GitHubStrategy } from "passport-github";
|
|
||||||
import { Strategy as GitLabStrategy } from "passport-gitlab2";
|
import { Strategy as GitLabStrategy } from "passport-gitlab2";
|
||||||
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
|
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
|
||||||
|
import { Strategy as OAuth2Strategy } from "passport-oauth2";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN } from "@app/lib/config/const";
|
import { INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN } from "@app/lib/config/const";
|
||||||
@@ -19,7 +19,7 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { fetchGithubEmails } from "@app/lib/requests/github";
|
import { fetchGithubEmails, fetchGithubUser } from "@app/lib/requests/github";
|
||||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
import { AuthMethod } from "@app/services/auth/auth-type";
|
import { AuthMethod } from "@app/services/auth/auth-type";
|
||||||
import { OrgAuthMethod } from "@app/services/org/org-types";
|
import { OrgAuthMethod } from "@app/services/org/org-types";
|
||||||
@@ -44,6 +44,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
await server.register(passport.initialize());
|
await server.register(passport.initialize());
|
||||||
await server.register(passport.secureSession());
|
await server.register(passport.secureSession());
|
||||||
|
|
||||||
// passport oauth strategy for Google
|
// passport oauth strategy for Google
|
||||||
const isGoogleOauthActive = Boolean(appCfg.CLIENT_ID_GOOGLE_LOGIN && appCfg.CLIENT_SECRET_GOOGLE_LOGIN);
|
const isGoogleOauthActive = Boolean(appCfg.CLIENT_ID_GOOGLE_LOGIN && appCfg.CLIENT_SECRET_GOOGLE_LOGIN);
|
||||||
if (isGoogleOauthActive) {
|
if (isGoogleOauthActive) {
|
||||||
@@ -54,8 +55,9 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
clientID: appCfg.CLIENT_ID_GOOGLE_LOGIN as string,
|
clientID: appCfg.CLIENT_ID_GOOGLE_LOGIN as string,
|
||||||
clientSecret: appCfg.CLIENT_SECRET_GOOGLE_LOGIN as string,
|
clientSecret: appCfg.CLIENT_SECRET_GOOGLE_LOGIN as string,
|
||||||
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/google`,
|
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/google`,
|
||||||
scope: ["profile", " email"],
|
scope: ["profile", "email"],
|
||||||
state: true
|
state: true,
|
||||||
|
pkce: true
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
async (req, _accessToken, _refreshToken, profile, cb) => {
|
async (req, _accessToken, _refreshToken, profile, cb) => {
|
||||||
@@ -91,34 +93,44 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
const isGithubOauthActive = Boolean(appCfg.CLIENT_SECRET_GITHUB_LOGIN && appCfg.CLIENT_ID_GITHUB_LOGIN);
|
const isGithubOauthActive = Boolean(appCfg.CLIENT_SECRET_GITHUB_LOGIN && appCfg.CLIENT_ID_GITHUB_LOGIN);
|
||||||
if (isGithubOauthActive) {
|
if (isGithubOauthActive) {
|
||||||
passport.use(
|
passport.use(
|
||||||
new GitHubStrategy(
|
"github",
|
||||||
|
new OAuth2Strategy(
|
||||||
{
|
{
|
||||||
passReqToCallback: true,
|
authorizationURL: "https://github.com/login/oauth/authorize",
|
||||||
clientID: appCfg.CLIENT_ID_GITHUB_LOGIN as string,
|
tokenURL: "https://github.com/login/oauth/access_token",
|
||||||
clientSecret: appCfg.CLIENT_SECRET_GITHUB_LOGIN as string,
|
clientID: appCfg.CLIENT_ID_GITHUB_LOGIN!,
|
||||||
|
clientSecret: appCfg.CLIENT_SECRET_GITHUB_LOGIN!,
|
||||||
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/github`,
|
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/github`,
|
||||||
scope: ["user:email", "read:org"],
|
scope: ["user:email", "read:org"],
|
||||||
// akhilmhdh: because the ts type for this is outdated by the maintainer
|
state: true,
|
||||||
state: true as unknown as string
|
pkce: true,
|
||||||
|
passReqToCallback: true
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
async (req, accessToken, _refreshToken, profile, cb) => {
|
async (req: any, accessToken: string, _refreshToken: string, _profile: any, done: Function) => {
|
||||||
// @ts-expect-error this is because this is express type and not fastify
|
|
||||||
const callbackPort = req.session.get("callbackPort");
|
|
||||||
try {
|
try {
|
||||||
const ghEmails = await fetchGithubEmails(accessToken);
|
const ghEmails = await fetchGithubEmails(accessToken);
|
||||||
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
|
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
|
||||||
|
|
||||||
|
if (!email) throw new Error("No primary email found");
|
||||||
|
|
||||||
|
// profile does not get automatically populated so we need to manually fetch user info
|
||||||
|
const user = await fetchGithubUser(accessToken);
|
||||||
|
|
||||||
|
const callbackPort = req.session.get("callbackPort");
|
||||||
|
|
||||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||||
email,
|
email,
|
||||||
firstName: profile.displayName || profile.username || "",
|
firstName: user.name || user.login,
|
||||||
lastName: "",
|
lastName: "",
|
||||||
authMethod: AuthMethod.GITHUB,
|
authMethod: AuthMethod.GITHUB,
|
||||||
callbackPort
|
callbackPort
|
||||||
});
|
});
|
||||||
return cb(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken });
|
|
||||||
} catch (error) {
|
done(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken });
|
||||||
logger.error(error);
|
} catch (err) {
|
||||||
cb(error as Error, false);
|
logger.error(err);
|
||||||
|
done(err as Error, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -138,7 +150,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
|||||||
clientSecret: appCfg.CLIENT_SECRET_GITLAB_LOGIN,
|
clientSecret: appCfg.CLIENT_SECRET_GITLAB_LOGIN,
|
||||||
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/gitlab`,
|
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/gitlab`,
|
||||||
baseURL: appCfg.CLIENT_GITLAB_LOGIN_URL,
|
baseURL: appCfg.CLIENT_GITLAB_LOGIN_URL,
|
||||||
state: true
|
state: true,
|
||||||
|
pkce: true
|
||||||
},
|
},
|
||||||
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
|
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export enum AppConnection {
|
|||||||
GCP = "gcp",
|
GCP = "gcp",
|
||||||
AzureKeyVault = "azure-key-vault",
|
AzureKeyVault = "azure-key-vault",
|
||||||
AzureAppConfiguration = "azure-app-configuration",
|
AzureAppConfiguration = "azure-app-configuration",
|
||||||
|
AzureClientSecrets = "azure-client-secrets",
|
||||||
Humanitec = "humanitec",
|
Humanitec = "humanitec",
|
||||||
TerraformCloud = "terraform-cloud",
|
TerraformCloud = "terraform-cloud",
|
||||||
Vercel = "vercel",
|
Vercel = "vercel",
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ import {
|
|||||||
getAzureAppConfigurationConnectionListItem,
|
getAzureAppConfigurationConnectionListItem,
|
||||||
validateAzureAppConfigurationConnectionCredentials
|
validateAzureAppConfigurationConnectionCredentials
|
||||||
} from "./azure-app-configuration";
|
} from "./azure-app-configuration";
|
||||||
|
import {
|
||||||
|
AzureClientSecretsConnectionMethod,
|
||||||
|
getAzureClientSecretsConnectionListItem,
|
||||||
|
validateAzureClientSecretsConnectionCredentials
|
||||||
|
} from "./azure-client-secrets";
|
||||||
import {
|
import {
|
||||||
AzureKeyVaultConnectionMethod,
|
AzureKeyVaultConnectionMethod,
|
||||||
getAzureKeyVaultConnectionListItem,
|
getAzureKeyVaultConnectionListItem,
|
||||||
@@ -76,6 +81,7 @@ export const listAppConnectionOptions = () => {
|
|||||||
getPostgresConnectionListItem(),
|
getPostgresConnectionListItem(),
|
||||||
getMsSqlConnectionListItem(),
|
getMsSqlConnectionListItem(),
|
||||||
getCamundaConnectionListItem(),
|
getCamundaConnectionListItem(),
|
||||||
|
getAzureClientSecretsConnectionListItem(),
|
||||||
getWindmillConnectionListItem(),
|
getWindmillConnectionListItem(),
|
||||||
getAuth0ConnectionListItem(),
|
getAuth0ConnectionListItem(),
|
||||||
getLdapConnectionListItem(),
|
getLdapConnectionListItem(),
|
||||||
@@ -136,6 +142,8 @@ export const validateAppConnectionCredentials = async (
|
|||||||
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.AzureAppConfiguration]:
|
[AppConnection.AzureAppConfiguration]:
|
||||||
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.AzureClientSecrets]:
|
||||||
|
validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
@@ -157,6 +165,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
return "GitHub App";
|
return "GitHub App";
|
||||||
case AzureKeyVaultConnectionMethod.OAuth:
|
case AzureKeyVaultConnectionMethod.OAuth:
|
||||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||||
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
case GitHubConnectionMethod.OAuth:
|
case GitHubConnectionMethod.OAuth:
|
||||||
return "OAuth";
|
return "OAuth";
|
||||||
case AwsConnectionMethod.AccessKey:
|
case AwsConnectionMethod.AccessKey:
|
||||||
@@ -226,6 +235,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
|||||||
[AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported,
|
[AppConnection.TerraformCloud]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
|
[AppConnection.Camunda]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
|
[AppConnection.Vercel]: platformManagedCredentialsNotSupported,
|
||||||
|
[AppConnection.AzureClientSecrets]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
|
[AppConnection.Windmill]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
|
[AppConnection.Auth0]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
|
[AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
|||||||
[AppConnection.GCP]: "GCP",
|
[AppConnection.GCP]: "GCP",
|
||||||
[AppConnection.AzureKeyVault]: "Azure Key Vault",
|
[AppConnection.AzureKeyVault]: "Azure Key Vault",
|
||||||
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
||||||
|
[AppConnection.AzureClientSecrets]: "Azure Client Secrets",
|
||||||
[AppConnection.Databricks]: "Databricks",
|
[AppConnection.Databricks]: "Databricks",
|
||||||
[AppConnection.Humanitec]: "Humanitec",
|
[AppConnection.Humanitec]: "Humanitec",
|
||||||
[AppConnection.TerraformCloud]: "Terraform Cloud",
|
[AppConnection.TerraformCloud]: "Terraform Cloud",
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0";
|
|||||||
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||||
import { awsConnectionService } from "./aws/aws-connection-service";
|
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||||
|
import { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets";
|
||||||
|
import { azureClientSecretsConnectionService } from "./azure-client-secrets/azure-client-secrets-service";
|
||||||
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
||||||
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
|
||||||
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
import { camundaConnectionService } from "./camunda/camunda-connection-service";
|
||||||
@@ -76,6 +78,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
|||||||
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
|
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
|
||||||
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
|
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema,
|
||||||
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
|
[AppConnection.Camunda]: ValidateCamundaConnectionCredentialsSchema,
|
||||||
|
[AppConnection.AzureClientSecrets]: ValidateAzureClientSecretsConnectionCredentialsSchema,
|
||||||
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
|
[AppConnection.Windmill]: ValidateWindmillConnectionCredentialsSchema,
|
||||||
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
|
[AppConnection.Auth0]: ValidateAuth0ConnectionCredentialsSchema,
|
||||||
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
|
[AppConnection.LDAP]: ValidateLdapConnectionCredentialsSchema,
|
||||||
@@ -454,8 +457,9 @@ export const appConnectionServiceFactory = ({
|
|||||||
terraformCloud: terraformCloudConnectionService(connectAppConnectionById),
|
terraformCloud: terraformCloudConnectionService(connectAppConnectionById),
|
||||||
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
camunda: camundaConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
vercel: vercelConnectionService(connectAppConnectionById),
|
vercel: vercelConnectionService(connectAppConnectionById),
|
||||||
windmill: windmillConnectionService(connectAppConnectionById),
|
azureClientSecrets: azureClientSecretsConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
auth0: auth0ConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
|
windmill: windmillConnectionService(connectAppConnectionById),
|
||||||
teamcity: teamcityConnectionService(connectAppConnectionById)
|
teamcity: teamcityConnectionService(connectAppConnectionById)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ import {
|
|||||||
TAzureAppConfigurationConnectionInput,
|
TAzureAppConfigurationConnectionInput,
|
||||||
TValidateAzureAppConfigurationConnectionCredentialsSchema
|
TValidateAzureAppConfigurationConnectionCredentialsSchema
|
||||||
} from "./azure-app-configuration";
|
} from "./azure-app-configuration";
|
||||||
|
import {
|
||||||
|
TAzureClientSecretsConnection,
|
||||||
|
TAzureClientSecretsConnectionConfig,
|
||||||
|
TAzureClientSecretsConnectionInput,
|
||||||
|
TValidateAzureClientSecretsConnectionCredentialsSchema
|
||||||
|
} from "./azure-client-secrets";
|
||||||
import {
|
import {
|
||||||
TAzureKeyVaultConnection,
|
TAzureKeyVaultConnection,
|
||||||
TAzureKeyVaultConnectionConfig,
|
TAzureKeyVaultConnectionConfig,
|
||||||
@@ -107,6 +113,7 @@ export type TAppConnection = { id: string } & (
|
|||||||
| TPostgresConnection
|
| TPostgresConnection
|
||||||
| TMsSqlConnection
|
| TMsSqlConnection
|
||||||
| TCamundaConnection
|
| TCamundaConnection
|
||||||
|
| TAzureClientSecretsConnection
|
||||||
| TWindmillConnection
|
| TWindmillConnection
|
||||||
| TAuth0Connection
|
| TAuth0Connection
|
||||||
| TLdapConnection
|
| TLdapConnection
|
||||||
@@ -130,6 +137,7 @@ export type TAppConnectionInput = { id: string } & (
|
|||||||
| TPostgresConnectionInput
|
| TPostgresConnectionInput
|
||||||
| TMsSqlConnectionInput
|
| TMsSqlConnectionInput
|
||||||
| TCamundaConnectionInput
|
| TCamundaConnectionInput
|
||||||
|
| TAzureClientSecretsConnectionInput
|
||||||
| TWindmillConnectionInput
|
| TWindmillConnectionInput
|
||||||
| TAuth0ConnectionInput
|
| TAuth0ConnectionInput
|
||||||
| TLdapConnectionInput
|
| TLdapConnectionInput
|
||||||
@@ -153,12 +161,13 @@ export type TAppConnectionConfig =
|
|||||||
| TGcpConnectionConfig
|
| TGcpConnectionConfig
|
||||||
| TAzureKeyVaultConnectionConfig
|
| TAzureKeyVaultConnectionConfig
|
||||||
| TAzureAppConfigurationConnectionConfig
|
| TAzureAppConfigurationConnectionConfig
|
||||||
|
| TAzureClientSecretsConnectionConfig
|
||||||
| TDatabricksConnectionConfig
|
| TDatabricksConnectionConfig
|
||||||
| THumanitecConnectionConfig
|
| THumanitecConnectionConfig
|
||||||
| TTerraformCloudConnectionConfig
|
| TTerraformCloudConnectionConfig
|
||||||
| TVercelConnectionConfig
|
|
||||||
| TSqlConnectionConfig
|
| TSqlConnectionConfig
|
||||||
| TCamundaConnectionConfig
|
| TCamundaConnectionConfig
|
||||||
|
| TVercelConnectionConfig
|
||||||
| TWindmillConnectionConfig
|
| TWindmillConnectionConfig
|
||||||
| TAuth0ConnectionConfig
|
| TAuth0ConnectionConfig
|
||||||
| TLdapConnectionConfig
|
| TLdapConnectionConfig
|
||||||
@@ -170,13 +179,14 @@ export type TValidateAppConnectionCredentialsSchema =
|
|||||||
| TValidateGcpConnectionCredentialsSchema
|
| TValidateGcpConnectionCredentialsSchema
|
||||||
| TValidateAzureKeyVaultConnectionCredentialsSchema
|
| TValidateAzureKeyVaultConnectionCredentialsSchema
|
||||||
| TValidateAzureAppConfigurationConnectionCredentialsSchema
|
| TValidateAzureAppConfigurationConnectionCredentialsSchema
|
||||||
|
| TValidateAzureClientSecretsConnectionCredentialsSchema
|
||||||
| TValidateDatabricksConnectionCredentialsSchema
|
| TValidateDatabricksConnectionCredentialsSchema
|
||||||
| TValidateHumanitecConnectionCredentialsSchema
|
| TValidateHumanitecConnectionCredentialsSchema
|
||||||
| TValidatePostgresConnectionCredentialsSchema
|
| TValidatePostgresConnectionCredentialsSchema
|
||||||
| TValidateMsSqlConnectionCredentialsSchema
|
| TValidateMsSqlConnectionCredentialsSchema
|
||||||
| TValidateCamundaConnectionCredentialsSchema
|
| TValidateCamundaConnectionCredentialsSchema
|
||||||
| TValidateTerraformCloudConnectionCredentialsSchema
|
|
||||||
| TValidateVercelConnectionCredentialsSchema
|
| TValidateVercelConnectionCredentialsSchema
|
||||||
|
| TValidateTerraformCloudConnectionCredentialsSchema
|
||||||
| TValidateWindmillConnectionCredentialsSchema
|
| TValidateWindmillConnectionCredentialsSchema
|
||||||
| TValidateAuth0ConnectionCredentialsSchema
|
| TValidateAuth0ConnectionCredentialsSchema
|
||||||
| TValidateLdapConnectionCredentialsSchema
|
| TValidateLdapConnectionCredentialsSchema
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export enum AzureClientSecretsConnectionMethod {
|
||||||
|
OAuth = "oauth"
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import { AxiosError, AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import {
|
||||||
|
decryptAppConnectionCredentials,
|
||||||
|
encryptAppConnectionCredentials,
|
||||||
|
getAppConnectionMethodName
|
||||||
|
} from "@app/services/app-connection/app-connection-fns";
|
||||||
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
|
import { TAppConnectionDALFactory } from "../app-connection-dal";
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
|
||||||
|
import {
|
||||||
|
ExchangeCodeAzureResponse,
|
||||||
|
TAzureClientSecretsConnectionConfig,
|
||||||
|
TAzureClientSecretsConnectionCredentials
|
||||||
|
} from "./azure-client-secrets-connection-types";
|
||||||
|
|
||||||
|
export const getAzureClientSecretsConnectionListItem = () => {
|
||||||
|
const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig();
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "Azure Client Secrets" as const,
|
||||||
|
app: AppConnection.AzureClientSecrets as const,
|
||||||
|
methods: Object.values(AzureClientSecretsConnectionMethod) as [AzureClientSecretsConnectionMethod.OAuth],
|
||||||
|
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAzureConnectionAccessToken = async (
|
||||||
|
connectionId: string,
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "updateById">,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
|
) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Azure environment variables have not been configured`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
|
|
||||||
|
if (!appConnection) {
|
||||||
|
throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appConnection.app !== AppConnection.AzureClientSecrets) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Connection with ID '${connectionId}' is not an Azure Client Secrets connection`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = (await decryptAppConnectionCredentials({
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService,
|
||||||
|
encryptedCredentials: appConnection.encryptedCredentials
|
||||||
|
})) as TAzureClientSecretsConnectionCredentials;
|
||||||
|
|
||||||
|
const { refreshToken } = credentials;
|
||||||
|
const currentTime = Date.now();
|
||||||
|
|
||||||
|
const { data } = await request.post<ExchangeCodeAzureResponse>(
|
||||||
|
IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"),
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
scope: `openid offline_access https://graph.microsoft.com/.default`,
|
||||||
|
client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||||
|
client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||||
|
refresh_token: refreshToken
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedCredentials = {
|
||||||
|
...credentials,
|
||||||
|
accessToken: data.access_token,
|
||||||
|
expiresAt: currentTime + data.expires_in * 1000,
|
||||||
|
refreshToken: data.refresh_token
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: updatedCredentials,
|
||||||
|
orgId: appConnection.orgId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
|
await appConnectionDAL.updateById(appConnection.id, { encryptedCredentials });
|
||||||
|
|
||||||
|
return data.access_token;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateAzureClientSecretsConnectionCredentials = async (config: TAzureClientSecretsConnectionConfig) => {
|
||||||
|
const { credentials: inputCredentials, method } = config;
|
||||||
|
|
||||||
|
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
|
||||||
|
|
||||||
|
if (!SITE_URL) {
|
||||||
|
throw new InternalServerError({ message: "SITE_URL env var is required to complete Azure OAuth flow" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
|
||||||
|
let tokenError: AxiosError | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||||
|
IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"),
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
code: inputCredentials.code,
|
||||||
|
scope: `openid offline_access https://graph.microsoft.com/.default`,
|
||||||
|
client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||||
|
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||||
|
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof AxiosError) {
|
||||||
|
tokenError = e;
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate connection: verify credentials`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenError) {
|
||||||
|
if (tokenError instanceof AxiosError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to get access token: ${
|
||||||
|
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||||
|
}`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: "Failed to get access token"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tokenResp) {
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Failed to get access token: Token was empty with no error`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case AzureClientSecretsConnectionMethod.OAuth:
|
||||||
|
return {
|
||||||
|
tenantId: inputCredentials.tenantId,
|
||||||
|
accessToken: tokenResp.data.access_token,
|
||||||
|
refreshToken: tokenResp.data.refresh_token,
|
||||||
|
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
throw new InternalServerError({
|
||||||
|
message: `Unhandled Azure connection method: ${method as AzureClientSecretsConnectionMethod}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
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 { AzureClientSecretsConnectionMethod } from "./azure-client-secrets-connection-enums";
|
||||||
|
|
||||||
|
export const AzureClientSecretsConnectionOAuthInputCredentialsSchema = z.object({
|
||||||
|
code: z.string().trim().min(1, "OAuth code required").describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.code),
|
||||||
|
tenantId: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Tenant ID required")
|
||||||
|
.describe(AppConnections.CREDENTIALS.AZURE_CLIENT_SECRETS.tenantId)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureClientSecretsConnectionOAuthOutputCredentialsSchema = z.object({
|
||||||
|
tenantId: z.string(),
|
||||||
|
accessToken: z.string(),
|
||||||
|
refreshToken: z.string(),
|
||||||
|
expiresAt: z.number()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ValidateAzureClientSecretsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z
|
||||||
|
.literal(AzureClientSecretsConnectionMethod.OAuth)
|
||||||
|
.describe(AppConnections.CREATE(AppConnection.AzureClientSecrets).method),
|
||||||
|
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.AzureClientSecrets).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateAzureClientSecretsConnectionSchema = ValidateAzureClientSecretsConnectionCredentialsSchema.and(
|
||||||
|
GenericCreateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateAzureClientSecretsConnectionSchema = z
|
||||||
|
.object({
|
||||||
|
credentials: AzureClientSecretsConnectionOAuthInputCredentialsSchema.optional().describe(
|
||||||
|
AppConnections.UPDATE(AppConnection.AzureClientSecrets).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureClientSecrets));
|
||||||
|
|
||||||
|
const BaseAzureClientSecretsConnectionSchema = BaseAppConnectionSchema.extend({
|
||||||
|
app: z.literal(AppConnection.AzureClientSecrets)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AzureClientSecretsConnectionSchema = z.intersection(
|
||||||
|
BaseAzureClientSecretsConnectionSchema,
|
||||||
|
z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AzureClientSecretsConnectionMethod.OAuth),
|
||||||
|
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SanitizedAzureClientSecretsConnectionSchema = z.discriminatedUnion("method", [
|
||||||
|
BaseAzureClientSecretsConnectionSchema.extend({
|
||||||
|
method: z.literal(AzureClientSecretsConnectionMethod.OAuth),
|
||||||
|
credentials: AzureClientSecretsConnectionOAuthOutputCredentialsSchema.pick({
|
||||||
|
tenantId: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const AzureClientSecretsConnectionListItemSchema = z.object({
|
||||||
|
name: z.literal("Azure Client Secrets"),
|
||||||
|
app: z.literal(AppConnection.AzureClientSecrets),
|
||||||
|
methods: z.nativeEnum(AzureClientSecretsConnectionMethod).array(),
|
||||||
|
oauthClientId: z.string().optional()
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import {
|
||||||
|
AzureClientSecretsConnectionOAuthOutputCredentialsSchema,
|
||||||
|
AzureClientSecretsConnectionSchema,
|
||||||
|
CreateAzureClientSecretsConnectionSchema,
|
||||||
|
ValidateAzureClientSecretsConnectionCredentialsSchema
|
||||||
|
} from "./azure-client-secrets-connection-schemas";
|
||||||
|
|
||||||
|
export type TAzureClientSecretsConnection = z.infer<typeof AzureClientSecretsConnectionSchema>;
|
||||||
|
|
||||||
|
export type TAzureClientSecretsConnectionInput = z.infer<typeof CreateAzureClientSecretsConnectionSchema> & {
|
||||||
|
app: AppConnection.AzureClientSecrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TValidateAzureClientSecretsConnectionCredentialsSchema =
|
||||||
|
typeof ValidateAzureClientSecretsConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
export type TAzureClientSecretsConnectionConfig = DiscriminativePick<
|
||||||
|
TAzureClientSecretsConnectionInput,
|
||||||
|
"method" | "app" | "credentials"
|
||||||
|
> & {
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAzureClientSecretsConnectionCredentials = z.infer<
|
||||||
|
typeof AzureClientSecretsConnectionOAuthOutputCredentialsSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface ExchangeCodeAzureResponse {
|
||||||
|
token_type: string;
|
||||||
|
scope: string;
|
||||||
|
expires_in: number;
|
||||||
|
ext_expires_in: number;
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
id_token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TAzureRegisteredApp {
|
||||||
|
id: string;
|
||||||
|
appId: string;
|
||||||
|
displayName: string;
|
||||||
|
description?: string;
|
||||||
|
createdDateTime: string;
|
||||||
|
identifierUris?: string[];
|
||||||
|
signInAudience?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TAzureListRegisteredAppsResponse {
|
||||||
|
"@odata.context": string;
|
||||||
|
"@odata.nextLink"?: string;
|
||||||
|
value: TAzureRegisteredApp[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TAzureClientSecret {
|
||||||
|
keyId: string;
|
||||||
|
displayName?: string;
|
||||||
|
startDateTime: string;
|
||||||
|
endDateTime: string;
|
||||||
|
secretText?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
import { OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-client-secrets/azure-client-secrets-connection-fns";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TAzureClientSecretsConnection,
|
||||||
|
TAzureListRegisteredAppsResponse,
|
||||||
|
TAzureRegisteredApp
|
||||||
|
} from "./azure-client-secrets-connection-types";
|
||||||
|
|
||||||
|
type TGetAppConnectionFunc = (
|
||||||
|
app: AppConnection,
|
||||||
|
connectionId: string,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => Promise<TAzureClientSecretsConnection>;
|
||||||
|
|
||||||
|
const listAzureRegisteredApps = async (
|
||||||
|
appConnection: TAzureClientSecretsConnection,
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
|
) => {
|
||||||
|
const accessToken = await getAzureConnectionAccessToken(appConnection.id, appConnectionDAL, kmsService);
|
||||||
|
|
||||||
|
const graphEndpoint = `https://graph.microsoft.com/v1.0/applications`;
|
||||||
|
|
||||||
|
const apps: TAzureRegisteredApp[] = [];
|
||||||
|
let nextLink = graphEndpoint;
|
||||||
|
|
||||||
|
while (nextLink) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const { data: appsPage } = await request.get<TAzureListRegisteredAppsResponse>(nextLink, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
apps.push(...appsPage.value);
|
||||||
|
nextLink = appsPage["@odata.nextLink"] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const azureClientSecretsConnectionService = (
|
||||||
|
getAppConnection: TGetAppConnectionFunc,
|
||||||
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||||
|
) => {
|
||||||
|
const listApps = async (connectionId: string, actor: OrgServiceActor) => {
|
||||||
|
const appConnection = await getAppConnection(AppConnection.AzureClientSecrets, connectionId, actor);
|
||||||
|
|
||||||
|
const apps = await listAzureRegisteredApps(appConnection, appConnectionDAL, kmsService);
|
||||||
|
|
||||||
|
return apps.map((app) => ({
|
||||||
|
id: app.id,
|
||||||
|
name: app.displayName,
|
||||||
|
appId: app.appId
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
listApps
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./azure-client-secrets-connection-enums";
|
||||||
|
export * from "./azure-client-secrets-connection-fns";
|
||||||
|
export * from "./azure-client-secrets-connection-schemas";
|
||||||
|
export * from "./azure-client-secrets-connection-types";
|
||||||
@@ -38,8 +38,12 @@ export const getAzureConnectionAccessToken = async (
|
|||||||
throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` });
|
throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appConnection.app !== AppConnection.AzureKeyVault && appConnection.app !== AppConnection.AzureAppConfiguration) {
|
if (
|
||||||
throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not an Azure Key Vault connection` });
|
appConnection.app !== AppConnection.AzureKeyVault &&
|
||||||
|
appConnection.app !== AppConnection.AzureAppConfiguration &&
|
||||||
|
appConnection.app !== AppConnection.AzureClientSecrets
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not a valid Azure connection` });
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentials = (await decryptAppConnectionCredentials({
|
const credentials = (await decryptAppConnectionCredentials({
|
||||||
|
|||||||
@@ -69,6 +69,5 @@ export const listTeamCityProjects = async (appConnection: TTeamCityConnection) =
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter out the root project. Should not be seen by users.
|
return resp.data.project;
|
||||||
return resp.data.project.filter((proj) => proj.id !== "_Root");
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
|||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { getMinExpiresIn, removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
@@ -143,6 +143,17 @@ export const authLoginServiceFactory = ({
|
|||||||
);
|
);
|
||||||
if (!tokenSession) throw new Error("Failed to create token");
|
if (!tokenSession) throw new Error("Failed to create token");
|
||||||
|
|
||||||
|
let tokenSessionExpiresIn: string | number = cfg.JWT_AUTH_LIFETIME;
|
||||||
|
let refreshTokenExpiresIn: string | number = cfg.JWT_REFRESH_LIFETIME;
|
||||||
|
|
||||||
|
if (organizationId) {
|
||||||
|
const org = await orgDAL.findById(organizationId);
|
||||||
|
if (org && org.userTokenExpiration) {
|
||||||
|
tokenSessionExpiresIn = getMinExpiresIn(cfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
||||||
|
refreshTokenExpiresIn = org.userTokenExpiration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const accessToken = jwt.sign(
|
const accessToken = jwt.sign(
|
||||||
{
|
{
|
||||||
authMethod,
|
authMethod,
|
||||||
@@ -155,7 +166,7 @@ export const authLoginServiceFactory = ({
|
|||||||
mfaMethod
|
mfaMethod
|
||||||
},
|
},
|
||||||
cfg.AUTH_SECRET,
|
cfg.AUTH_SECRET,
|
||||||
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
|
{ expiresIn: tokenSessionExpiresIn }
|
||||||
);
|
);
|
||||||
|
|
||||||
const refreshToken = jwt.sign(
|
const refreshToken = jwt.sign(
|
||||||
@@ -170,7 +181,7 @@ export const authLoginServiceFactory = ({
|
|||||||
mfaMethod
|
mfaMethod
|
||||||
},
|
},
|
||||||
cfg.AUTH_SECRET,
|
cfg.AUTH_SECRET,
|
||||||
{ expiresIn: cfg.JWT_REFRESH_LIFETIME }
|
{ expiresIn: refreshTokenExpiresIn }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { access: accessToken, refresh: refreshToken };
|
return { access: accessToken, refresh: refreshToken };
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { getMinExpiresIn } from "@app/lib/fn";
|
||||||
import { isDisposableEmail } from "@app/lib/validator";
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
@@ -46,7 +47,7 @@ type TAuthSignupDep = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">;
|
||||||
orgDAL: TOrgDALFactory;
|
orgDAL: TOrgDALFactory;
|
||||||
tokenService: TAuthTokenServiceFactory;
|
tokenService: TAuthTokenServiceFactory;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
@@ -320,6 +321,17 @@ export const authSignupServiceFactory = ({
|
|||||||
projectBotDAL
|
projectBotDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let tokenSessionExpiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
|
||||||
|
let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME;
|
||||||
|
|
||||||
|
if (organizationId) {
|
||||||
|
const org = await orgService.findOrganizationById(user.id, organizationId, authMethod, organizationId);
|
||||||
|
if (org && org.userTokenExpiration) {
|
||||||
|
tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
||||||
|
refreshTokenExpiresIn = org.userTokenExpiration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tokenSession = await tokenService.getUserTokenSession({
|
const tokenSession = await tokenService.getUserTokenSession({
|
||||||
userAgent,
|
userAgent,
|
||||||
ip,
|
ip,
|
||||||
@@ -337,7 +349,7 @@ export const authSignupServiceFactory = ({
|
|||||||
organizationId
|
organizationId
|
||||||
},
|
},
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
{ expiresIn: tokenSessionExpiresIn }
|
||||||
);
|
);
|
||||||
|
|
||||||
const refreshToken = jwt.sign(
|
const refreshToken = jwt.sign(
|
||||||
@@ -350,7 +362,7 @@ export const authSignupServiceFactory = ({
|
|||||||
organizationId
|
organizationId
|
||||||
},
|
},
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{ expiresIn: appCfg.JWT_REFRESH_LIFETIME }
|
{ expiresIn: refreshTokenExpiresIn }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { user: updateduser.info, accessToken, refreshToken, organizationId };
|
return { user: updateduser.info, accessToken, refreshToken, organizationId };
|
||||||
|
|||||||
@@ -17,5 +17,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
|||||||
shouldUseNewPrivilegeSystem: true,
|
shouldUseNewPrivilegeSystem: true,
|
||||||
privilegeUpgradeInitiatedByUsername: true,
|
privilegeUpgradeInitiatedByUsername: true,
|
||||||
privilegeUpgradeInitiatedAt: true,
|
privilegeUpgradeInitiatedAt: true,
|
||||||
bypassOrgAuthEnabled: true
|
bypassOrgAuthEnabled: true,
|
||||||
|
userTokenExpiration: true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -170,8 +170,12 @@ export const orgServiceFactory = ({
|
|||||||
actorOrgId: string | undefined
|
actorOrgId: string | undefined
|
||||||
) => {
|
) => {
|
||||||
await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
const appCfg = getConfig();
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||||
|
if (!org.userTokenExpiration) {
|
||||||
|
return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME };
|
||||||
|
}
|
||||||
return org;
|
return org;
|
||||||
};
|
};
|
||||||
/*
|
/*
|
||||||
@@ -350,7 +354,8 @@ export const orgServiceFactory = ({
|
|||||||
enforceMfa,
|
enforceMfa,
|
||||||
selectedMfaMethod,
|
selectedMfaMethod,
|
||||||
allowSecretSharingOutsideOrganization,
|
allowSecretSharingOutsideOrganization,
|
||||||
bypassOrgAuthEnabled
|
bypassOrgAuthEnabled,
|
||||||
|
userTokenExpiration
|
||||||
}
|
}
|
||||||
}: TUpdateOrgDTO) => {
|
}: TUpdateOrgDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -451,7 +456,8 @@ export const orgServiceFactory = ({
|
|||||||
enforceMfa,
|
enforceMfa,
|
||||||
selectedMfaMethod,
|
selectedMfaMethod,
|
||||||
allowSecretSharingOutsideOrganization,
|
allowSecretSharingOutsideOrganization,
|
||||||
bypassOrgAuthEnabled
|
bypassOrgAuthEnabled,
|
||||||
|
userTokenExpiration
|
||||||
});
|
});
|
||||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||||
return org;
|
return org;
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export type TUpdateOrgDTO = {
|
|||||||
selectedMfaMethod: MfaMethod;
|
selectedMfaMethod: MfaMethod;
|
||||||
allowSecretSharingOutsideOrganization: boolean;
|
allowSecretSharingOutsideOrganization: boolean;
|
||||||
bypassOrgAuthEnabled: boolean;
|
bypassOrgAuthEnabled: boolean;
|
||||||
|
userTokenExpiration: string;
|
||||||
}>;
|
}>;
|
||||||
} & TOrgPermission;
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,19 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFolderImports = async (secretPath: string, environmentId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const folderImports = await (tx || db.replicaNode())(TableName.SecretImport)
|
||||||
|
.where({ importPath: secretPath, importEnv: environmentId })
|
||||||
|
.join(TableName.SecretFolder, `${TableName.SecretImport}.folderId`, `${TableName.SecretFolder}.id`)
|
||||||
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
|
.select(db.ref("id").withSchema(TableName.SecretFolder).as("folderId"));
|
||||||
|
return folderImports;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "get secret imports" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getFolderIsImportedBy = async (
|
const getFolderIsImportedBy = async (
|
||||||
secretPath: string,
|
secretPath: string,
|
||||||
environmentId: string,
|
environmentId: string,
|
||||||
@@ -203,7 +216,8 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("name").withSchema(TableName.Environment).as("envName"),
|
db.ref("name").withSchema(TableName.Environment).as("envName"),
|
||||||
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
|
||||||
db.ref("id").withSchema(TableName.SecretFolder).as("folderId"),
|
db.ref("id").withSchema(TableName.SecretFolder).as("folderId"),
|
||||||
db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey")
|
db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey"),
|
||||||
|
db.ref("environment").withSchema(TableName.SecretReferenceV2).as("referencedSecretEnv")
|
||||||
);
|
);
|
||||||
|
|
||||||
const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({
|
const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({
|
||||||
@@ -214,13 +228,14 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const secretResults = secretReferences.map(
|
const secretResults = secretReferences.map(
|
||||||
({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey }) => ({
|
({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey, referencedSecretEnv }) => ({
|
||||||
envName,
|
envName,
|
||||||
envSlug,
|
envSlug,
|
||||||
secretId,
|
secretId,
|
||||||
folderName,
|
folderName,
|
||||||
folderId,
|
folderId,
|
||||||
referencedSecretKey
|
referencedSecretKey,
|
||||||
|
referencedSecretEnv
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -235,6 +250,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
secrets: {
|
secrets: {
|
||||||
secretId: string;
|
secretId: string;
|
||||||
referencedSecretKey: string;
|
referencedSecretKey: string;
|
||||||
|
referencedSecretEnv: string;
|
||||||
}[];
|
}[];
|
||||||
folderId: string;
|
folderId: string;
|
||||||
folderImported: boolean;
|
folderImported: boolean;
|
||||||
@@ -264,7 +280,11 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
if ("secretId" in item && item.secretId) {
|
if ("secretId" in item && item.secretId) {
|
||||||
updatedAcc[env].folders[folder].secrets = [
|
updatedAcc[env].folders[folder].secrets = [
|
||||||
...updatedAcc[env].folders[folder].secrets,
|
...updatedAcc[env].folders[folder].secrets,
|
||||||
{ secretId: item.secretId, referencedSecretKey: item.referencedSecretKey }
|
{
|
||||||
|
secretId: item.secretId,
|
||||||
|
referencedSecretKey: item.referencedSecretKey,
|
||||||
|
referencedSecretEnv: item.referencedSecretEnv
|
||||||
|
}
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
updatedAcc[env].folders[folder].folderImported = true;
|
updatedAcc[env].folders[folder].folderImported = true;
|
||||||
@@ -309,6 +329,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
|||||||
findLastImportPosition,
|
findLastImportPosition,
|
||||||
updateAllPosition,
|
updateAllPosition,
|
||||||
getProjectImportCount,
|
getProjectImportCount,
|
||||||
getFolderIsImportedBy
|
getFolderIsImportedBy,
|
||||||
|
getFolderImports
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -808,7 +808,7 @@ export const secretImportServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
secrets
|
secrets
|
||||||
}: TGetSecretImportsDTO & {
|
}: TGetSecretImportsDTO & {
|
||||||
secrets: { secretKey: string; secretValue: string }[] | undefined;
|
secrets: { secretKey: string; secretValue: string; id: string }[] | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
@@ -877,7 +877,8 @@ export const secretImportServiceFactory = ({
|
|||||||
)
|
)
|
||||||
.map((otherSecret) => ({
|
.map((otherSecret) => ({
|
||||||
secretId: secret.secretKey,
|
secretId: secret.secretKey,
|
||||||
referencedSecretKey: otherSecret.secretKey
|
referencedSecretKey: otherSecret.secretKey,
|
||||||
|
referencedSecretEnv: environment
|
||||||
}));
|
}));
|
||||||
}) || [];
|
}) || [];
|
||||||
if (locallyReferenced.length > 0) {
|
if (locallyReferenced.length > 0) {
|
||||||
|
|||||||
@@ -56,11 +56,12 @@ export type FolderResult = {
|
|||||||
export type SecretResult = {
|
export type SecretResult = {
|
||||||
secretId: string;
|
secretId: string;
|
||||||
referencedSecretKey: string;
|
referencedSecretKey: string;
|
||||||
|
referencedSecretEnv: string;
|
||||||
} & FolderResult;
|
} & FolderResult;
|
||||||
|
|
||||||
export type FolderInfo = {
|
export type FolderInfo = {
|
||||||
folderName: string;
|
folderName: string;
|
||||||
secrets?: { secretId: string; referencedSecretKey: string }[];
|
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
|
||||||
folderId: string;
|
folderId: string;
|
||||||
folderImported: boolean;
|
folderImported: boolean;
|
||||||
envSlug?: string;
|
envSlug?: string;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
TDeleteSecretSyncDTO,
|
TDeleteSecretSyncDTO,
|
||||||
TFindSecretSyncByIdDTO,
|
TFindSecretSyncByIdDTO,
|
||||||
TFindSecretSyncByNameDTO,
|
TFindSecretSyncByNameDTO,
|
||||||
|
TListSecretSyncsByFolderId,
|
||||||
TListSecretSyncsByProjectId,
|
TListSecretSyncsByProjectId,
|
||||||
TSecretSync,
|
TSecretSync,
|
||||||
TTriggerSecretSyncImportSecretsByIdDTO,
|
TTriggerSecretSyncImportSecretsByIdDTO,
|
||||||
@@ -31,12 +32,14 @@ import {
|
|||||||
TUpdateSecretSyncDTO
|
TUpdateSecretSyncDTO
|
||||||
} from "@app/services/secret-sync/secret-sync-types";
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
|
||||||
|
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
|
||||||
import { TSecretSyncDALFactory } from "./secret-sync-dal";
|
import { TSecretSyncDALFactory } from "./secret-sync-dal";
|
||||||
import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps";
|
import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps";
|
||||||
import { TSecretSyncQueueFactory } from "./secret-sync-queue";
|
import { TSecretSyncQueueFactory } from "./secret-sync-queue";
|
||||||
|
|
||||||
type TSecretSyncServiceFactoryDep = {
|
type TSecretSyncServiceFactoryDep = {
|
||||||
secretSyncDAL: TSecretSyncDALFactory;
|
secretSyncDAL: TSecretSyncDALFactory;
|
||||||
|
secretImportDAL: TSecretImportDALFactory;
|
||||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
@@ -53,6 +56,7 @@ export type TSecretSyncServiceFactory = ReturnType<typeof secretSyncServiceFacto
|
|||||||
export const secretSyncServiceFactory = ({
|
export const secretSyncServiceFactory = ({
|
||||||
secretSyncDAL,
|
secretSyncDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
|
secretImportDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
@@ -85,6 +89,37 @@ export const secretSyncServiceFactory = ({
|
|||||||
return secretSyncs as TSecretSync[];
|
return secretSyncs as TSecretSync[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listSecretSyncsBySecretPath = async (
|
||||||
|
{ projectId, secretPath, environment }: TListSecretSyncsByFolderId,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.SecretManager,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (permission.cannot(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
|
if (!folder) return [];
|
||||||
|
|
||||||
|
const folderImports = await secretImportDAL.getFolderImports(secretPath, folder.envId);
|
||||||
|
|
||||||
|
const secretSyncs = await secretSyncDAL.find({
|
||||||
|
$in: {
|
||||||
|
folderId: folderImports.map((folderImport) => folderImport.folderId).concat(folder.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return secretSyncs as TSecretSync[];
|
||||||
|
};
|
||||||
|
|
||||||
const findSecretSyncById = async ({ destination, syncId }: TFindSecretSyncByIdDTO, actor: OrgServiceActor) => {
|
const findSecretSyncById = async ({ destination, syncId }: TFindSecretSyncByIdDTO, actor: OrgServiceActor) => {
|
||||||
const secretSync = await secretSyncDAL.findById(syncId);
|
const secretSync = await secretSyncDAL.findById(syncId);
|
||||||
|
|
||||||
@@ -518,6 +553,7 @@ export const secretSyncServiceFactory = ({
|
|||||||
return {
|
return {
|
||||||
listSecretSyncOptions,
|
listSecretSyncOptions,
|
||||||
listSecretSyncsByProjectId,
|
listSecretSyncsByProjectId,
|
||||||
|
listSecretSyncsBySecretPath,
|
||||||
findSecretSyncById,
|
findSecretSyncById,
|
||||||
findSecretSyncByName,
|
findSecretSyncByName,
|
||||||
createSecretSync,
|
createSecretSync,
|
||||||
|
|||||||
@@ -144,6 +144,13 @@ export type TListSecretSyncsByProjectId = {
|
|||||||
destination?: SecretSync;
|
destination?: SecretSync;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TListSecretSyncsByFolderId = {
|
||||||
|
projectId: string;
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
destination?: SecretSync;
|
||||||
|
};
|
||||||
|
|
||||||
export type TFindSecretSyncByIdDTO = {
|
export type TFindSecretSyncByIdDTO = {
|
||||||
syncId: string;
|
syncId: string;
|
||||||
destination: SecretSync;
|
destination: SecretSync;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
TTeamCitySyncWithCredentials
|
TTeamCitySyncWithCredentials
|
||||||
} from "@app/services/secret-sync/teamcity/teamcity-sync-types";
|
} from "@app/services/secret-sync/teamcity/teamcity-sync-types";
|
||||||
|
|
||||||
// Note: Most variables won't be returned with a value due to them being a "password" type (starting with "env.").
|
// Note: Most variables won't be returned with a value due to them being a "password" type.
|
||||||
// TeamCity API returns empty string for password-type variables for security reasons.
|
// TeamCity API returns empty string for password-type variables for security reasons.
|
||||||
const listTeamCityVariables = async ({ instanceUrl, accessToken, project, buildConfig }: TTeamCityListVariables) => {
|
const listTeamCityVariables = async ({ instanceUrl, accessToken, project, buildConfig }: TTeamCityListVariables) => {
|
||||||
const { data } = await request.get<TTeamCityListVariablesResponse>(
|
const { data } = await request.get<TTeamCityListVariablesResponse>(
|
||||||
@@ -25,12 +25,16 @@ const listTeamCityVariables = async ({ instanceUrl, accessToken, project, buildC
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Filters for only non-inherited environment variables
|
||||||
// Strips out "env." from map key, but the "name" field still has the original unaltered key.
|
// Strips out "env." from map key, but the "name" field still has the original unaltered key.
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
data.property.map((variable) => [
|
data.property
|
||||||
variable.name.startsWith("env.") ? variable.name.substring(4) : variable.name,
|
.filter((variable) => !variable.inherited)
|
||||||
{ ...variable, value: variable.value || "" } // Password values will be empty strings from the API for security
|
.filter((variable) => variable.name.startsWith("env."))
|
||||||
])
|
.map((variable) => [
|
||||||
|
variable.name.substring(4),
|
||||||
|
{ ...variable, value: variable.value || "" } // Password values will be empty strings from the API for security
|
||||||
|
])
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import type {
|
|||||||
TFindSecretsByFolderIdsFilter,
|
TFindSecretsByFolderIdsFilter,
|
||||||
TGetSecretsDTO
|
TGetSecretsDTO
|
||||||
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
||||||
|
import { applyJitter } from "@app/lib/dates";
|
||||||
|
|
||||||
export const SecretServiceCacheKeys = {
|
export const SecretServiceCacheKeys = {
|
||||||
get productKey() {
|
get productKey() {
|
||||||
@@ -48,7 +49,7 @@ interface TSecretV2DalArg {
|
|||||||
keyStore: TKeyStoreFactory;
|
keyStore: TKeyStoreFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SECRET_DAL_TTL = 5 * 60;
|
export const SECRET_DAL_TTL = () => applyJitter(10 * 60, 2 * 60);
|
||||||
export const SECRET_DAL_VERSION_TTL = 15 * 60;
|
export const SECRET_DAL_VERSION_TTL = 15 * 60;
|
||||||
export const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024;
|
export const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024;
|
||||||
export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||||
@@ -63,7 +64,8 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
|||||||
const findOne = async (filter: Partial<TSecretsV2>, tx?: Knex) => {
|
const findOne = async (filter: Partial<TSecretsV2>, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const docs = await (tx || db)(TableName.SecretV2)
|
const docs = await (tx || db)(TableName.SecretV2)
|
||||||
.where(filter)
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter(filter, TableName.SecretV2))
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.SecretV2JnTag,
|
TableName.SecretV2JnTag,
|
||||||
`${TableName.SecretV2}.id`,
|
`${TableName.SecretV2}.id`,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "node:path";
|
|||||||
|
|
||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
|
|
||||||
import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas";
|
import { SecretType, TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas";
|
||||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
@@ -720,7 +720,7 @@ export const reshapeBridgeSecret = (
|
|||||||
secretReminderRecipients: secret.secretReminderRecipients || [],
|
secretReminderRecipients: secret.secretReminderRecipients || [],
|
||||||
...(secretValueHidden
|
...(secretValueHidden
|
||||||
? {
|
? {
|
||||||
secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK,
|
secretValue: secret.type === SecretType.Personal ? secret.value : INFISICAL_SECRET_VALUE_HIDDEN_MASK,
|
||||||
secretValueHidden: true
|
secretValueHidden: true
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
|
|||||||
@@ -962,7 +962,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
const encryptedCachedSecrets = await keyStore.getItem(cacheKey);
|
const encryptedCachedSecrets = await keyStore.getItem(cacheKey);
|
||||||
if (encryptedCachedSecrets) {
|
if (encryptedCachedSecrets) {
|
||||||
try {
|
try {
|
||||||
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL);
|
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL());
|
||||||
const cachedSecrets = secretManagerDecryptor({ cipherTextBlob: Buffer.from(encryptedCachedSecrets, "base64") });
|
const cachedSecrets = secretManagerDecryptor({ cipherTextBlob: Buffer.from(encryptedCachedSecrets, "base64") });
|
||||||
const { secrets, imports = [] } = JSON.parse(cachedSecrets.toString("utf8")) as {
|
const { secrets, imports = [] } = JSON.parse(cachedSecrets.toString("utf8")) as {
|
||||||
secrets: typeof decryptedSecrets;
|
secrets: typeof decryptedSecrets;
|
||||||
@@ -1132,7 +1132,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
plainText: Buffer.from(JSON.stringify(payload))
|
plainText: Buffer.from(JSON.stringify(payload))
|
||||||
}).cipherTextBlob;
|
}).cipherTextBlob;
|
||||||
if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) {
|
if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) {
|
||||||
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64"));
|
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL(), encryptedUpdatedCachedSecrets.toString("base64"));
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
@@ -1179,7 +1179,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
plainText: Buffer.from(JSON.stringify(payload))
|
plainText: Buffer.from(JSON.stringify(payload))
|
||||||
}).cipherTextBlob;
|
}).cipherTextBlob;
|
||||||
if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) {
|
if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) {
|
||||||
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64"));
|
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL(), encryptedUpdatedCachedSecrets.toString("base64"));
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Available"
|
||||||
|
openapi: "GET /api/v1/app-connections/azure-client-secrets/available"
|
||||||
|
---
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v1/app-connections/azure-client-secrets"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Azure Client Secret Connections must be created through the Infisical UI.
|
||||||
|
Check out the configuration docs for [Azure Client Secret Connections](/integrations/app-connections/azure-client-secrets) for a step-by-step
|
||||||
|
guide.
|
||||||
|
</Note>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/app-connections/azure-client-secrets/{connectionId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by ID"
|
||||||
|
openapi: "GET /api/v1/app-connections/azure-client-secrets/{connectionId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by Name"
|
||||||
|
openapi: "GET /api/v1/app-connections/azure-client-secrets/connection-name/{connectionName}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/app-connections/azure-client-secrets"
|
||||||
|
---
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v1/app-connections/azure-client-secrets/{connectionId}"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Azure Client Secret Connections must be updated through the Infisical UI.
|
||||||
|
Check out the configuration docs for [Azure Client Secret Connections](/integrations/app-connections/azure-client-secrets) for a step-by-step
|
||||||
|
guide.
|
||||||
|
</Note>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v2/secret-rotations/azure-client-secret"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Check out the configuration docs for [Azure Client Secret Rotations](/documentation/platform/secret-rotation/azure-client-secret) to learn how to obtain the
|
||||||
|
required parameters.
|
||||||
|
</Note>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v2/secret-rotations/azure-client-secret/{rotationId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by ID"
|
||||||
|
openapi: "GET /api/v2/secret-rotations/azure-client-secret/{rotationId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get by Name"
|
||||||
|
openapi: "GET /api/v2/secret-rotations/azure-client-secret/rotation-name/{rotationName}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get Credentials by ID"
|
||||||
|
openapi: "GET /api/v2/secret-rotations/azure-client-secret/{rotationId}/generated-credentials"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v2/secret-rotations/azure-client-secret"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Rotate Secrets"
|
||||||
|
openapi: "POST /api/v2/secret-rotations/azure-client-secret/{rotationId}/rotate-secrets"
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v2/secret-rotations/azure-client-secret/{rotationId}"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Check out the configuration docs for [Azure Client Secret Rotations](/documentation/platform/secret-rotation/azure-client-secret) to learn how to obtain the
|
||||||
|
required parameters.
|
||||||
|
</Note>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: "Assume Privileges"
|
||||||
|
description: "Learn how to temporarily assume the privileges of a user or machine identity within a project."
|
||||||
|
---
|
||||||
|
|
||||||
|
This feature allows authorized users to temporarily take on the permissions of another user or identity. It helps administrators and access managers test and verify permissions before granting access, ensuring everything is set up correctly.
|
||||||
|
It also reduces back-and-forth with end users when troubleshooting permission-related issues.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
When an authorized user activates assume privileges mode, they temporarily inherit the target user or identity’s permissions for up to one hour.
|
||||||
|
During this time, they can perform actions within the system with the same level of access as the target user.
|
||||||
|
|
||||||
|
- **Permission-based**: Only permissions are inherited, not the full identity
|
||||||
|
- **Time-limited**: Access automatically expires after one hour
|
||||||
|
- **Audited**: All actions are logged under the original user's account. This means any action taken during the session will be recorded under the entity assuming the privileges, not the target entity.
|
||||||
|
- **Authorization required**: Only users with the specific **assume privilege** permission can use this feature
|
||||||
|
- **Scoped to a single project**: You can only assume privileges for one project at a time
|
||||||
|
|
||||||
|
## How to Assume Privileges
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Go to Project Access">
|
||||||
|
Click on the user or identity you want to assume.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Click Assume Privilege">
|
||||||
|
Click **Assume Privilege**, then type `assume` to confirm and start your session.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Session is Active">
|
||||||
|
You will see a yellow banner indicating that your assume privilege session is active. You can exit at any time by clicking **Exit**.
|
||||||
|
|
||||||
|

|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: "GitHub Organization Sync"
|
title: "GitHub Team Sync"
|
||||||
description: "Learn how to automatically synchronize your GitHub teams with Infisical Groups."
|
description: "Learn how to automatically synchronize your GitHub teams with Infisical Groups."
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ This guide will walk you through the steps needed to configure external KMS supp
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
|
- An AWS KMS Key configured as a `Symmetric` key and with `Encrypt and Decrypt` key usage.
|
||||||
|

|
||||||
|
|
||||||
Before you begin, you'll first need to choose a method of authentication with AWS from below.
|
Before you begin, you'll first need to choose a method of authentication with AWS from below.
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
|
|||||||
@@ -268,11 +268,11 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
|||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Create HSM client folder">
|
<Step title="Create HSM client folder">
|
||||||
When using Kubernetes, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Kubernetes.
|
When using Kubernetes, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Kubernetes. In this example, we are going to be using `/etc/luna-docker`.
|
||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir /etc/hsm-client
|
mkdir /etc/luna-docker
|
||||||
```
|
```
|
||||||
|
|
||||||
After [setting up your Luna Cloud HSM client](https://thalesdocs.com/gphsm/luna/7/docs/network/Content/install/client_install/add_dpod.htm), you should have a set of files, referred to as the HSM client. You don't need all the files, but for simplicity we recommend copying all the files from the client.
|
After [setting up your Luna Cloud HSM client](https://thalesdocs.com/gphsm/luna/7/docs/network/Content/install/client_install/add_dpod.htm), you should have a set of files, referred to as the HSM client. You don't need all the files, but for simplicity we recommend copying all the files from the client.
|
||||||
@@ -306,20 +306,60 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
|||||||
The most important parts of the client folder is the `Chrystoki.conf` file, and the `libs`, `plugins`, and `jsp` folders. You need to copy these files to the folder you created in the first step.
|
The most important parts of the client folder is the `Chrystoki.conf` file, and the `libs`, `plugins`, and `jsp` folders. You need to copy these files to the folder you created in the first step.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp -r /<path-to-where-your-hsm-client-is-located> /etc/hsm-client
|
cp -r /<path-to-where-your-luna-client-is-located>/* /etc/luna-docker
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
The `/*` wildcard will copy all files and folders within the HSM client. The wildcard is important to ensure that the file structure is inline with the rest of this guide.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
After copying the files, the `/etc/luna-docker` directory should have the following file structure:
|
||||||
|
```bash
|
||||||
|
$ ls -R /etc/luna-docker
|
||||||
|
Chrystoki.conf etc lock server-certificate.pem
|
||||||
|
Chrystoki.conf.tmp2E jsp partition-ca-certificate.pem setenv
|
||||||
|
lch-support-linux-64bit partition-certificate.pem
|
||||||
|
bin libs plugins
|
||||||
|
|
||||||
|
/etc/luna-docker/bin:
|
||||||
|
64
|
||||||
|
|
||||||
|
/etc/luna-docker/bin/64:
|
||||||
|
ckdemo cmu lunacm multitoken vtl
|
||||||
|
|
||||||
|
/etc/luna-docker/etc:
|
||||||
|
openssl.cnf
|
||||||
|
|
||||||
|
/etc/luna-docker/jsp:
|
||||||
|
64 LunaProvider.jar
|
||||||
|
|
||||||
|
/etc/luna-docker/jsp/64:
|
||||||
|
libLunaAPI.so
|
||||||
|
|
||||||
|
/etc/luna-docker/libs:
|
||||||
|
64
|
||||||
|
|
||||||
|
/etc/luna-docker/libs/64:
|
||||||
|
libCryptoki2.so
|
||||||
|
|
||||||
|
/etc/luna-docker/lock:
|
||||||
|
|
||||||
|
/etc/luna-docker/plugins:
|
||||||
|
libcloud.plugin
|
||||||
|
```
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Update Chrystoki.conf">
|
<Step title="Update Chrystoki.conf">
|
||||||
The `Chrystoki.conf` file is used to configure the HSM client. You need to update the `Chrystoki.conf` file to point to the correct file paths.
|
The `Chrystoki.conf` file is used to configure the HSM client. You need to update the `Chrystoki.conf` file to point to the correct file paths.
|
||||||
|
|
||||||
In this example, we will be mounting the `/etc/hsm-client` folder from the host to containers in our deployment's pods at the path `/hsm-client`. This means the contents of `/etc/hsm-client` on the host will be accessible at `/hsm-client` within the containers.
|
In this example, we will be mounting the `/etc/luna-docker` folder from the host to containers in our deployment's pods at the path `/usr/safenet/lunaclient`. This means the contents of `/etc/luna-docker` on the host will be accessible at `/usr/safenet/lunaclient` within the containers.
|
||||||
|
|
||||||
An example config file will look like this:
|
An example config file will look like this:
|
||||||
|
|
||||||
```Chrystoki.conf
|
```Chrystoki.conf
|
||||||
Chrystoki2 = {
|
Chrystoki2 = {
|
||||||
# This path points to the mounted path, /hsm-client
|
# This path points to the mounted path, /usr/safenet/lunaclient
|
||||||
LibUNIX64 = /hsm-client/libs/64/libCryptoki2.so;
|
LibUNIX64 = /usr/safenet/lunaclient/libs/64/libCryptoki2.so;
|
||||||
}
|
}
|
||||||
|
|
||||||
Luna = {
|
Luna = {
|
||||||
@@ -339,8 +379,8 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
|||||||
|
|
||||||
Misc = {
|
Misc = {
|
||||||
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
|
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
|
||||||
PluginModuleDir = /hsm-client/plugins;
|
PluginModuleDir = /usr/safenet/lunaclient/plugins;
|
||||||
MutexFolder = /hsm-client/lock;
|
MutexFolder = /usr/safenet/lunaclient/lock;
|
||||||
PE1746Enabled = 1;
|
PE1746Enabled = 1;
|
||||||
ToolsDir = /usr/bin;
|
ToolsDir = /usr/bin;
|
||||||
|
|
||||||
@@ -353,7 +393,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
|||||||
LunaSA Client = {
|
LunaSA Client = {
|
||||||
ReceiveTimeout = 20000;
|
ReceiveTimeout = 20000;
|
||||||
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
|
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
|
||||||
SSLConfigFile = /hsm-client/etc/openssl.cnf;
|
SSLConfigFile = /usr/safenet/lunaclient/etc/openssl.cnf;
|
||||||
ClientPrivKeyFile = ./etc/ClientNameKey.pem;
|
ClientPrivKeyFile = ./etc/ClientNameKey.pem;
|
||||||
ClientCertFile = ./etc/ClientNameCert.pem;
|
ClientCertFile = ./etc/ClientNameCert.pem;
|
||||||
ServerCAFile = ./etc/CAFile.pem;
|
ServerCAFile = ./etc/CAFile.pem;
|
||||||
@@ -441,7 +481,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
kubectl exec hsm-setup-pod -- mkdir -p /data/ # Create the data directory
|
kubectl exec hsm-setup-pod -- mkdir -p /data/ # Create the data directory
|
||||||
kubectl cp ./hsm-client/ hsm-setup-pod:/data/ # Copy the HSM client files into the PVC
|
kubectl cp /etc/luna-docker/. hsm-setup-pod:/data/ # Copy the HSM client files into the PVC
|
||||||
kubectl exec hsm-setup-pod -- chmod -R 755 /data/ # Set the correct permissions for the HSM client files
|
kubectl exec hsm-setup-pod -- chmod -R 755 /data/ # Set the correct permissions for the HSM client files
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -456,7 +496,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
|||||||
Next we need to update the environment variables used for the deployment. If you followed the [setup instructions for Kubernetes deployments](/self-hosting/deployment-options/kubernetes-helm), you should have a Kubernetes secret called `infisical-secrets`.
|
Next we need to update the environment variables used for the deployment. If you followed the [setup instructions for Kubernetes deployments](/self-hosting/deployment-options/kubernetes-helm), you should have a Kubernetes secret called `infisical-secrets`.
|
||||||
We need to update the secret with the following environment variables:
|
We need to update the secret with the following environment variables:
|
||||||
|
|
||||||
- `HSM_LIB_PATH` - The path to the HSM client library _(mapped to `/hsm-client/libs/64/libCryptoki2.so`)_
|
- `HSM_LIB_PATH` - The path to the HSM client library _(mapped to `/usr/safenet/lunaclient/libs/64/libCryptoki2.so`)_
|
||||||
- `HSM_PIN` - The PIN for the HSM device that you created when setting up your Luna Cloud HSM client
|
- `HSM_PIN` - The PIN for the HSM device that you created when setting up your Luna Cloud HSM client
|
||||||
- `HSM_SLOT` - The slot number for the HSM device that you selected when setting up your Luna Cloud HSM client
|
- `HSM_SLOT` - The slot number for the HSM device that you selected when setting up your Luna Cloud HSM client
|
||||||
- `HSM_KEY_LABEL` - The label for the HSM key. If no key is found with the provided key label, the HSM will create a new key with the provided label.
|
- `HSM_KEY_LABEL` - The label for the HSM key. If no key is found with the provided key label, the HSM will create a new key with the provided label.
|
||||||
@@ -471,7 +511,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
|||||||
type: Opaque
|
type: Opaque
|
||||||
stringData:
|
stringData:
|
||||||
# ... Other environment variables ...
|
# ... Other environment variables ...
|
||||||
HSM_LIB_PATH: "/hsm-client/libs/64/libCryptoki2.so" # If you followed this guide, this will be the path of the Luna Cloud HSM client
|
HSM_LIB_PATH: "/usr/safenet/lunaclient/libs/64/libCryptoki2.so" # If you followed this guide, this will be the path of the Luna Cloud HSM client
|
||||||
HSM_PIN: "<your-hsm-device-pin>"
|
HSM_PIN: "<your-hsm-device-pin>"
|
||||||
HSM_SLOT: "<hsm-device-slot>"
|
HSM_SLOT: "<hsm-device-slot>"
|
||||||
HSM_KEY_LABEL: "<your-key-label>"
|
HSM_KEY_LABEL: "<your-key-label>"
|
||||||
@@ -487,7 +527,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
|||||||
<Step title="Updating the Deployment">
|
<Step title="Updating the Deployment">
|
||||||
After we've successfully configured the PVC and updated our environment variables, we are ready to update the deployment configuration so that the pods it creates can access the HSM client files.
|
After we've successfully configured the PVC and updated our environment variables, we are ready to update the deployment configuration so that the pods it creates can access the HSM client files.
|
||||||
|
|
||||||
We need to update the Docker image of the deployment to use `infisical/infisical-fips`. The `infisical/infisical-fips` image is a functionally identical image to the `infisical/infisical` image, but it is built with support for HSM encryption.
|
We need to update the Docker image of the deployment to use `infisical/infisical-fips`. The `infisical/infisical-fips` image is a functionally identical image to the `infisical/infisical` image, but it is built with HSM support.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# ... The rest of the values.yaml file ...
|
# ... The rest of the values.yaml file ...
|
||||||
@@ -499,8 +539,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
|
|||||||
|
|
||||||
extraVolumeMounts:
|
extraVolumeMounts:
|
||||||
- name: hsm-data
|
- name: hsm-data
|
||||||
mountPath: /hsm-client # The path we will mount the HSM client files to
|
mountPath: /usr/safenet/lunaclient # The path we will mount the HSM client files to
|
||||||
subPath: ./hsm-client
|
|
||||||
|
|
||||||
extraVolumes:
|
extraVolumes:
|
||||||
- name: hsm-data
|
- name: hsm-data
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ The **Settings** page lets you manage information about your organization includ
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
You can adjust the maximum time a user token will remain valid for your organization. After this period, users will be required to re-authenticate. This helps improve security by enforcing regular sign-ins.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
## Access Control
|
## Access Control
|
||||||
|
|
||||||
The **Access Control** page is where you can manage identities (both people and machines) that are part of your organization.
|
The **Access Control** page is where you can manage identities (both people and machines) that are part of your organization.
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
---
|
||||||
|
title: "Azure Client Secret"
|
||||||
|
description: "Learn how to automatically rotate Azure Client Secrets."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Create an [Azure Client Secret Connection](/integrations/app-connections/azure-client-secrets).
|
||||||
|
|
||||||
|
## Create an Azure 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.
|
||||||
|

|
||||||
|
|
||||||
|
2. Select the **Azure Client Secret** option.
|
||||||
|

|
||||||
|
|
||||||
|
3. Select the **Azure Connection** to use and configure the rotation behavior. Then click **Next**.
|
||||||
|

|
||||||
|
|
||||||
|
- **Azure 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 Azure application whose Client Secret you want to rotate. Then click **Next**.
|
||||||
|

|
||||||
|
|
||||||
|
5. Specify the secret names that the client credentials should be mapped to. Then click **Next**.
|
||||||
|

|
||||||
|
|
||||||
|
- **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**.
|
||||||
|

|
||||||
|
|
||||||
|
- **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**.
|
||||||
|

|
||||||
|
|
||||||
|
8. Your **Azure Client Secret** credentials are now available for use via the mapped secrets.
|
||||||
|

|
||||||
|
</Tab>
|
||||||
|
<Tab title="API">
|
||||||
|
To create an Azure Client Secret Rotation, make an API request to the [Create Azure
|
||||||
|
Client Secret Rotation](/api-reference/endpoints/secret-rotations/azure-client-secret/create) API endpoint.
|
||||||
|
|
||||||
|
You will first need the **Client ID** and **Object ID** of the Azure application you want to rotate the secret for. This can be obtained from the Applications dashboard.
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
### Sample request
|
||||||
|
|
||||||
|
```bash Request
|
||||||
|
curl --request POST \
|
||||||
|
--url https://us.infisical.com/api/v2/secret-rotations/azure-client-secret \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"name": "my-azure-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": {
|
||||||
|
"objectId": "...",
|
||||||
|
"clientId": "...",
|
||||||
|
"appName": "..."
|
||||||
|
},
|
||||||
|
"secretsMapping": {
|
||||||
|
"clientId": "AZURE_CLIENT_ID",
|
||||||
|
"clientSecret": "AZURE_CLIENT_SECRET"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample response
|
||||||
|
|
||||||
|
```bash Response
|
||||||
|
{
|
||||||
|
"secretRotation": {
|
||||||
|
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
|
||||||
|
"name": "my-azure-rotation",
|
||||||
|
"description": "my client secret rotation",
|
||||||
|
"secretsMapping": {
|
||||||
|
"clientId": "AZURE_CLIENT_ID",
|
||||||
|
"clientSecret": "AZURE_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": "azure",
|
||||||
|
"name": "my-azure-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": "azure-client-secret",
|
||||||
|
"parameters": {
|
||||||
|
"objectId": "...",
|
||||||
|
"appName": "...",
|
||||||
|
"clientId": "..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
After Width: | Height: | Size: 566 KiB |
|
After Width: | Height: | Size: 578 KiB |
|
After Width: | Height: | Size: 857 KiB |
|
After Width: | Height: | Size: 580 KiB |
|
After Width: | Height: | Size: 600 KiB |
|
After Width: | Height: | Size: 335 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 519 KiB After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 531 KiB |
|
After Width: | Height: | Size: 866 KiB |
|
After Width: | Height: | Size: 500 KiB |
|
After Width: | Height: | Size: 509 KiB |
|
After Width: | Height: | Size: 504 KiB |