Merge branch 'main' of https://github.com/Infisical/infisical into feat/chef-data-bag-app-connection-secret-sync

This commit is contained in:
Piyush Gupta
2025-11-01 03:09:48 +05:30
145 changed files with 4657 additions and 1490 deletions
@@ -135,7 +135,7 @@ jobs:
TAG_NAME="${{ github.ref_name }}" TAG_NAME="${{ github.ref_name }}"
echo "Checking for tag: $TAG_NAME" echo "Checking for tag: $TAG_NAME"
EXACT_MATCH=$(gh api repos/Infisical/infisical-omnibus/git/refs/tags/$TAG_NAME | jq -r 'if type == "array" then .[].ref else .ref end' | grep -x "refs/tags/$TAG_NAME") EXACT_MATCH=$(gh api repos/Infisical/infisical-omnibus/git/refs/tags/$TAG_NAME 2>/dev/null | jq -r 'if type == "array" then .[].ref else .ref end' | grep -x "refs/tags/$TAG_NAME" || true)
if [ "$EXACT_MATCH" == "refs/tags/$TAG_NAME" ]; then if [ "$EXACT_MATCH" == "refs/tags/$TAG_NAME" ]; then
echo "Tag $TAG_NAME already exists, skipping..." echo "Tag $TAG_NAME already exists, skipping..."
+14
View File
@@ -135,9 +135,23 @@ import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integ
declare module "@fastify/request-context" { declare module "@fastify/request-context" {
interface RequestContextData { interface RequestContextData {
reqId: string; reqId: string;
ip?: string;
userAgent?: string;
orgId?: string; orgId?: string;
orgName?: string;
userAuthInfo?: {
userId: string;
email: string;
};
projectDetails?: {
id: string;
name: string;
slug: string;
};
identityAuthInfo?: { identityAuthInfo?: {
identityId: string; identityId: string;
identityName: string;
authMethod: string;
oidc?: { oidc?: {
claims: Record<string, string>; claims: Record<string, string>;
}; };
@@ -0,0 +1,68 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
// Fix for 20250722152841_add-policies-environments-table.ts migration.
// 20250722152841_add-policies-environments-table.ts introduced a bug where you can no longer delete a project if it has any approval policy environments.
export async function up(knex: Knex): Promise<void> {
// Fix SecretApprovalPolicyEnvironment to cascade delete when environment is deleted
// note: this won't actually happen, as we prevent deletion of environments with active approval policies
// in the old migration it was ON DELETE SET NULL, which doesn't work because envId is not a nullable col
await knex.schema.alterTable(TableName.SecretApprovalPolicyEnvironment, (t) => {
t.dropForeign(["envId"]);
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
});
// Fix AccessApprovalPolicyEnvironment to cascade delete when environment is deleted
// note: this won't actually happen, as we prevent deletion of environments with active approval policies
// in the old migration it was ON DELETE SET NULL, which doesn't work because envId is not a nullable col
await knex.schema.alterTable(TableName.AccessApprovalPolicyEnvironment, (t) => {
t.dropForeign(["envId"]);
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
});
// Fix SecretApprovalPolicy to CASCADE instead of SET NULL
// in the old migration it was ON DELETE SET NULL, which doesn't work because envId is not a nullable col
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
t.dropForeign(["envId"]);
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
});
// Fix AccessApprovalPolicy to CASCADE instead of SET NULL
// in the old migration it was ON DELETE SET NULL, which doesn't work because envId is not a nullable col
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
t.dropForeign(["envId"]);
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
});
}
export async function down(knex: Knex): Promise<void> {
// Revert SecretApprovalPolicyEnvironment
await knex.schema.alterTable(TableName.SecretApprovalPolicyEnvironment, (t) => {
t.dropForeign(["envId"]);
t.foreign("envId").references("id").inTable(TableName.Environment);
});
// Revert AccessApprovalPolicyEnvironment
await knex.schema.alterTable(TableName.AccessApprovalPolicyEnvironment, (t) => {
t.dropForeign(["envId"]);
t.foreign("envId").references("id").inTable(TableName.Environment);
});
// Revert SecretApprovalPolicy back to SET NULL
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
t.dropForeign(["envId"]);
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("SET NULL");
});
// Revert AccessApprovalPolicy back to SET NULL
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
t.dropForeign(["envId"]);
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("SET NULL");
});
}
@@ -0,0 +1,29 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.PamAccount, "rotationStatus"))) {
await knex.schema.alterTable(TableName.PamAccount, (t) => {
t.string("rotationStatus").nullable();
});
}
if (!(await knex.schema.hasColumn(TableName.PamAccount, "encryptedLastRotationMessage"))) {
await knex.schema.alterTable(TableName.PamAccount, (t) => {
t.binary("encryptedLastRotationMessage").nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.PamAccount, "rotationStatus")) {
await knex.schema.alterTable(TableName.PamAccount, (t) => {
t.dropColumn("rotationStatus");
});
}
if (await knex.schema.hasColumn(TableName.PamAccount, "encryptedLastRotationMessage")) {
await knex.schema.alterTable(TableName.PamAccount, (t) => {
t.dropColumn("encryptedLastRotationMessage");
});
}
}
+3 -1
View File
@@ -21,7 +21,9 @@ export const PamAccountsSchema = z.object({
updatedAt: z.date(), updatedAt: z.date(),
rotationEnabled: z.boolean().default(false), rotationEnabled: z.boolean().default(false),
rotationIntervalSeconds: z.number().nullable().optional(), rotationIntervalSeconds: z.number().nullable().optional(),
lastRotatedAt: z.date().nullable().optional() lastRotatedAt: z.date().nullable().optional(),
rotationStatus: z.string().nullable().optional(),
encryptedLastRotationMessage: zodBuffer.nullable().optional()
}); });
export type TPamAccounts = z.infer<typeof PamAccountsSchema>; export type TPamAccounts = z.infer<typeof PamAccountsSchema>;
@@ -1,14 +1,14 @@
import {
CreateMySQLResourceSchema,
MySQLResourceSchema,
UpdateMySQLResourceSchema
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
import { import {
CreatePostgresResourceSchema, CreatePostgresResourceSchema,
SanitizedPostgresResourceSchema, SanitizedPostgresResourceSchema,
UpdatePostgresResourceSchema UpdatePostgresResourceSchema
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
import {
CreateMySQLResourceSchema,
MySQLResourceSchema,
UpdateMySQLResourceSchema
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import { registerPamResourceEndpoints } from "./pam-resource-endpoints"; import { registerPamResourceEndpoints } from "./pam-resource-endpoints";
+28 -2
View File
@@ -7,6 +7,7 @@
// All the any rules are disabled because passport typesense with fastify is really poor // All the any rules are disabled because passport typesense with fastify is really poor
import { Authenticator } from "@fastify/passport"; import { Authenticator } from "@fastify/passport";
import { requestContext } from "@fastify/request-context";
import fastifySession from "@fastify/session"; import fastifySession from "@fastify/session";
import { MultiSamlStrategy } from "@node-saml/passport-saml"; import { MultiSamlStrategy } from "@node-saml/passport-saml";
import { FastifyRequest } from "fastify"; import { FastifyRequest } from "fastify";
@@ -17,6 +18,7 @@ import { ApiDocsTags, SamlSso } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, 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 { SanitizedSamlConfigSchema } from "@app/server/routes/sanitizedSchema/directory-config"; import { SanitizedSamlConfigSchema } from "@app/server/routes/sanitizedSchema/directory-config";
@@ -102,7 +104,6 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
}, },
// eslint-disable-next-line // eslint-disable-next-line
async (req, profile, cb) => { async (req, profile, cb) => {
try {
if (!profile) throw new BadRequestError({ message: "Missing profile" }); if (!profile) throw new BadRequestError({ message: "Missing profile" });
const email = const email =
@@ -111,6 +112,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
(profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email"] as string) ?? (profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email"] as string) ??
(profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved\ (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved\
try {
const firstName = (profile.firstName ?? const firstName = (profile.firstName ??
// entra sends data in this format // entra sends data in this format
profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/firstName"]) as string; profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/firstName"]) as string;
@@ -144,7 +146,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
}) })
.filter((el) => el.key && !["email", "firstName", "lastName"].includes(el.key)); .filter((el) => el.key && !["email", "firstName", "lastName"].includes(el.key));
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({ const { isUserCompleted, providerAuthToken, user, organization } = await server.services.saml.samlLogin({
externalId: profile.nameID, externalId: profile.nameID,
email: email.toLowerCase(), email: email.toLowerCase(),
firstName, firstName,
@@ -154,8 +156,32 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId, orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId,
metadata: userMetadata metadata: userMetadata
}); });
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email.toLowerCase(),
"infisical.user.id": user.id,
"infisical.organization.id": organization.id,
"infisical.organization.name": organization.name,
"infisical.auth.method": AuthAttemptAuthMethod.SAML,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
cb(null, { isUserCompleted, providerAuthToken }); cb(null, { isUserCompleted, providerAuthToken });
} catch (error) { } catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email.toLowerCase(),
"infisical.auth.method": AuthAttemptAuthMethod.SAML,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
logger.error(error); logger.error(error);
cb(error as Error); cb(error as Error);
} }
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client"; import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
import { AccessScope, OrganizationActionScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas"; import { AccessScope, OrganizationActionScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas";
@@ -15,6 +16,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto"; import { crypto } from "@app/lib/crypto";
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
@@ -471,7 +473,7 @@ export const oidcConfigServiceFactory = ({
}); });
} }
return { isUserCompleted, providerAuthToken }; return { isUserCompleted, providerAuthToken, user };
}; };
const updateOidcCfg = async ({ const updateOidcCfg = async ({
@@ -754,10 +756,35 @@ export const oidcConfigServiceFactory = ({
callbackPort, callbackPort,
manageGroupMemberships: oidcCfg.manageGroupMemberships manageGroupMemberships: oidcCfg.manageGroupMemberships
}) })
.then(({ isUserCompleted, providerAuthToken }) => { .then(({ isUserCompleted, providerAuthToken, user }) => {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": claims?.email?.toLowerCase(),
"infisical.user.id": user.id,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.auth.method": AuthAttemptAuthMethod.OIDC,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
cb(null, { isUserCompleted, providerAuthToken }); cb(null, { isUserCompleted, providerAuthToken });
}) })
.catch((error) => { .catch((error) => {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": claims?.email?.toLowerCase(),
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.auth.method": AuthAttemptAuthMethod.OIDC,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
cb(error); cb(error);
}); });
} }
@@ -45,17 +45,47 @@ export const decryptAccountCredentials = async ({
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamAccountCredentials; return JSON.parse(decryptedPlainTextBlob.toString()) as TPamAccountCredentials;
}; };
export const decryptAccount = async <T extends { encryptedCredentials: Buffer }>( export const decryptAccountMessage = async ({
projectId,
encryptedMessage,
kmsService
}: {
projectId: string;
encryptedMessage: Buffer;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedPlainTextBlob = decryptor({
cipherTextBlob: encryptedMessage
});
return decryptedPlainTextBlob.toString();
};
export const decryptAccount = async <
T extends { encryptedCredentials: Buffer; encryptedLastRotationMessage?: Buffer | null }
>(
account: T, account: T,
projectId: string, projectId: string,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey"> kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
): Promise<T & { credentials: TPamAccountCredentials }> => { ): Promise<T & { credentials: TPamAccountCredentials; lastRotationMessage: string | null }> => {
return { return {
...account, ...account,
credentials: await decryptAccountCredentials({ credentials: await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials, encryptedCredentials: account.encryptedCredentials,
projectId, projectId,
kmsService kmsService
}),
lastRotationMessage: account.encryptedLastRotationMessage
? await decryptAccountMessage({
encryptedMessage: account.encryptedLastRotationMessage,
projectId,
kmsService
}) })
} as T & { credentials: TPamAccountCredentials }; : null
};
}; };
@@ -15,6 +15,7 @@ import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type"; import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
@@ -353,6 +354,7 @@ export const pamAccountServiceFactory = ({
TPamAccounts & { TPamAccounts & {
resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean }; resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean };
credentials: TPamAccountCredentials; credentials: TPamAccountCredentials;
lastRotationMessage: string | null;
} }
> = []; > = [];
@@ -376,6 +378,7 @@ export const pamAccountServiceFactory = ({
) { ) {
// Decrypt the account only if the user has permission to read it // Decrypt the account only if the user has permission to read it
const decryptedAccount = await decryptAccount(account, account.projectId, kmsService); const decryptedAccount = await decryptAccount(account, account.projectId, kmsService);
decryptedAndPermittedAccounts.push({ decryptedAndPermittedAccounts.push({
...decryptedAccount, ...decryptedAccount,
resource: { resource: {
@@ -575,10 +578,10 @@ export const pamAccountServiceFactory = ({
for (let i = 0; i < accounts.length; i += ROTATION_CONCURRENCY_LIMIT) { for (let i = 0; i < accounts.length; i += ROTATION_CONCURRENCY_LIMIT) {
const batch = accounts.slice(i, i + ROTATION_CONCURRENCY_LIMIT); const batch = accounts.slice(i, i + ROTATION_CONCURRENCY_LIMIT);
const rotationPromises = batch.map(async (account) => const rotationPromises = batch.map(async (account) => {
pamAccountDAL.transaction(async (tx) => {
let logResourceType = "unknown"; let logResourceType = "unknown";
try { try {
await pamAccountDAL.transaction(async (tx) => {
const resource = await pamResourceDAL.findById(account.resourceId, tx); const resource = await pamResourceDAL.findById(account.resourceId, tx);
if (!resource || !resource.encryptedRotationAccountCredentials) return; if (!resource || !resource.encryptedRotationAccountCredentials) return;
logResourceType = resource.resourceType; logResourceType = resource.resourceType;
@@ -619,7 +622,9 @@ export const pamAccountServiceFactory = ({
account.id, account.id,
{ {
encryptedCredentials, encryptedCredentials,
lastRotatedAt: new Date() lastRotatedAt: new Date(),
rotationStatus: "success",
encryptedLastRotationMessage: null
}, },
tx tx
); );
@@ -640,11 +645,26 @@ export const pamAccountServiceFactory = ({
} }
} }
}); });
});
} catch (error) { } catch (error) {
logger.error(error, `Failed to rotate credentials for account [accountId=${account.id}]`); logger.error(error, `Failed to rotate credentials for account [accountId=${account.id}]`);
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: account.projectId
});
const { cipherTextBlob: encryptedMessage } = encryptor({
plainText: Buffer.from(errorMessage)
});
await pamAccountDAL.updateById(account.id, {
rotationStatus: "failed",
encryptedLastRotationMessage: encryptedMessage
});
await auditLogService.createAuditLog({ await auditLogService.createAuditLog({
projectId: account.projectId, projectId: account.projectId,
actor: { actor: {
@@ -662,10 +682,8 @@ export const pamAccountServiceFactory = ({
} }
} }
}); });
throw error; // Rollback transaction
} }
}) });
);
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await Promise.all(rotationPromises); await Promise.all(rotationPromises);
@@ -33,7 +33,9 @@ export const BasePamAccountSchemaWithResource = BasePamAccountSchema.extend({
resourceType: true resourceType: true
}).extend({ }).extend({
rotationCredentialsConfigured: z.boolean() rotationCredentialsConfigured: z.boolean()
}) }),
lastRotationMessage: z.string().nullable().optional(),
rotationStatus: z.string().nullable().optional()
}); });
export const BaseCreatePamAccountSchema = z.object({ export const BaseCreatePamAccountSchema = z.object({
@@ -41,7 +41,10 @@ export interface SqlResourceConnection {
* *
* @returns Promise to be resolved with the new credentials * @returns Promise to be resolved with the new credentials
*/ */
rotateCredentials: (currentCredentials: TSqlAccountCredentials) => Promise<TSqlAccountCredentials>; rotateCredentials: (
currentCredentials: TSqlAccountCredentials,
newPassword: string
) => Promise<TSqlAccountCredentials>;
/** /**
* Close the connection. * Close the connection.
@@ -113,8 +116,7 @@ const makeSqlConnection = (
}); });
} }
}, },
rotateCredentials: async (currentCredentials) => { rotateCredentials: async (currentCredentials, newPassword) => {
const newPassword = alphaNumericNanoId(32);
// Note: The generated random password is not really going to make SQL Injection possible. // Note: The generated random password is not really going to make SQL Injection possible.
// The reason we are not using parameters binding is that the "ALTER USER" syntax is DDL, // The reason we are not using parameters binding is that the "ALTER USER" syntax is DDL,
// parameters binding is not supported. But just in case if the this code got copied // parameters binding is not supported. But just in case if the this code got copied
@@ -295,6 +297,7 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
rotationAccountCredentials, rotationAccountCredentials,
currentCredentials currentCredentials
) => { ) => {
const newPassword = alphaNumericNanoId(32);
try { try {
return await executeWithGateway( return await executeWithGateway(
{ {
@@ -305,7 +308,7 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
password: rotationAccountCredentials.password password: rotationAccountCredentials.password
}, },
gatewayV2Service, gatewayV2Service,
(client) => client.rotateCredentials(currentCredentials) (client) => client.rotateCredentials(currentCredentials, newPassword)
); );
} catch (error) { } catch (error) {
if (error instanceof BadRequestError) { if (error instanceof BadRequestError) {
@@ -328,8 +331,10 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
} }
} }
const sanitizedErrorMessage = ((error as Error).message || String(error)).replaceAll(newPassword, "REDACTED");
throw new BadRequestError({ throw new BadRequestError({
message: `Unable to rotate account credentials for ${resourceType}: ${(error as Error).message || String(error)}` message: `Unable to rotate account credentials for ${resourceType}: ${sanitizedErrorMessage}`
}); });
} }
}; };
@@ -337,6 +337,12 @@ export const permissionServiceFactory = ({
throw new NotFoundError({ message: `Project with ${projectId} not found` }); throw new NotFoundError({ message: `Project with ${projectId} not found` });
} }
requestContext.set("projectDetails", {
id: projectDetails.id,
name: projectDetails.name,
slug: projectDetails.slug
});
if (projectDetails.orgId !== actorOrgId) { if (projectDetails.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
} }
@@ -769,7 +769,7 @@ export const samlConfigServiceFactory = ({
}); });
} }
return { isUserCompleted, providerAuthToken }; return { isUserCompleted, providerAuthToken, user, organization };
}; };
return { return {
@@ -1,4 +1,4 @@
import { TSamlConfigs } from "@app/db/schemas"; import { TOrganizations, TSamlConfigs, TUsers } from "@app/db/schemas";
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type"; import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
@@ -78,5 +78,7 @@ export type TSamlConfigServiceFactory = {
samlLogin: (arg: TSamlLoginDTO) => Promise<{ samlLogin: (arg: TSamlLoginDTO) => Promise<{
isUserCompleted: boolean; isUserCompleted: boolean;
providerAuthToken: string; providerAuthToken: string;
user: TUsers;
organization: TOrganizations;
}>; }>;
}; };
+9
View File
@@ -2348,6 +2348,9 @@ export const AppConnections = {
RAILWAY: { RAILWAY: {
apiToken: "The API token used to authenticate with Railway." apiToken: "The API token used to authenticate with Railway."
}, },
NORTHFLANK: {
apiToken: "The API token used to authenticate with Northflank."
},
CHECKLY: { CHECKLY: {
apiKey: "The API key used to authenticate with Checkly." apiKey: "The API key used to authenticate with Checkly."
}, },
@@ -2630,6 +2633,12 @@ export const SecretSyncs = {
CHEF: { CHEF: {
dataBagName: "The name of the Chef data bag to sync secrets to.", dataBagName: "The name of the Chef data bag to sync secrets to.",
dataBagItemName: "The name of the Chef data bag item to sync secrets to." dataBagItemName: "The name of the Chef data bag item to sync secrets to."
},
NORTHFLANK: {
projectId: "The ID of the Northflank project to sync secrets to.",
projectName: "The name of the Northflank project to sync secrets to.",
secretGroupId: "The ID of the Northflank secret group to sync secrets to.",
secretGroupName: "The name of the Northflank secret group to sync secrets to."
} }
} }
}; };
+100
View File
@@ -0,0 +1,100 @@
import { requestContext } from "@fastify/request-context";
import opentelemetry from "@opentelemetry/api";
import { getConfig } from "../config/env";
const infisicalMeter = opentelemetry.metrics.getMeter("Infisical");
export enum AuthAttemptAuthMethod {
EMAIL = "email",
SAML = "saml",
OIDC = "oidc",
GOOGLE = "google",
GITHUB = "github",
GITLAB = "gitlab",
TOKEN_AUTH = "token-auth",
UNIVERSAL_AUTH = "universal-auth",
KUBERNETES_AUTH = "kubernetes-auth",
GCP_AUTH = "gcp-auth",
ALICLOUD_AUTH = "alicloud-auth",
AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth",
TLS_CERT_AUTH = "tls-cert-auth",
OCI_AUTH = "oci-auth",
OIDC_AUTH = "oidc-auth",
JWT_AUTH = "jwt-auth",
LDAP_AUTH = "ldap-auth"
}
export enum AuthAttemptAuthResult {
SUCCESS = "success",
FAILURE = "failure"
}
export const authAttemptCounter = infisicalMeter.createCounter("infisical.auth.attempt.count", {
description: "Authentication attempts (both successful and failed)",
unit: "{attempt}"
});
export const secretReadCounter = infisicalMeter.createCounter("infisical.secret.read.count", {
description: "Number of secret read operations",
unit: "{operation}"
});
export const recordSecretReadMetric = (params: { environment: string; secretPath: string; name?: string }) => {
const appCfg = getConfig();
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
const attributes: Record<string, string> = {
"infisical.environment": params.environment,
"infisical.secret.path": params.secretPath,
...(params.name ? { "infisical.secret.name": params.name } : {})
};
const orgId = requestContext.get("orgId");
if (orgId) {
attributes["infisical.organization.id"] = orgId;
}
const orgName = requestContext.get("orgName");
if (orgName) {
attributes["infisical.organization.name"] = orgName;
}
const projectDetails = requestContext.get("projectDetails");
if (projectDetails?.id) {
attributes["infisical.project.id"] = projectDetails.id;
}
if (projectDetails?.name) {
attributes["infisical.project.name"] = projectDetails.name;
}
const userAuthInfo = requestContext.get("userAuthInfo");
if (userAuthInfo?.userId) {
attributes["infisical.user.id"] = userAuthInfo.userId;
}
if (userAuthInfo?.email) {
attributes["infisical.user.email"] = userAuthInfo.email;
}
const identityAuthInfo = requestContext.get("identityAuthInfo");
if (identityAuthInfo?.identityId) {
attributes["infisical.identity.id"] = identityAuthInfo.identityId;
}
if (identityAuthInfo?.identityName) {
attributes["infisical.identity.name"] = identityAuthInfo.identityName;
}
const userAgent = requestContext.get("userAgent");
if (userAgent) {
attributes["user_agent.original"] = userAgent;
}
const ip = requestContext.get("ip");
if (ip) {
attributes["client.address"] = ip;
}
secretReadCounter.add(1, attributes);
}
};
+3 -1
View File
@@ -141,7 +141,9 @@ export const main = async ({
await server.register(fastifyRequestContext, { await server.register(fastifyRequestContext, {
defaultStoreValues: (req) => ({ defaultStoreValues: (req) => ({
reqId: req.id, reqId: req.id,
log: req.log.child({ reqId: req.id }) log: req.log.child({ reqId: req.id }),
ip: req.realIp,
userAgent: req.headers["user-agent"]
}) })
}); });
+77 -1
View File
@@ -1,12 +1,26 @@
import { requestContext } from "@fastify/request-context";
import opentelemetry from "@opentelemetry/api"; import opentelemetry from "@opentelemetry/api";
import fp from "fastify-plugin"; import fp from "fastify-plugin";
export const apiMetrics = fp(async (fastify) => {
const apiMeter = opentelemetry.metrics.getMeter("API"); const apiMeter = opentelemetry.metrics.getMeter("API");
const latencyHistogram = apiMeter.createHistogram("API_latency", { const latencyHistogram = apiMeter.createHistogram("API_latency", {
unit: "ms" unit: "ms"
}); });
const infisicalMeter = opentelemetry.metrics.getMeter("Infisical");
const requestCounter = infisicalMeter.createCounter("infisical.http.server.request.count", {
description: "Total number of API requests to Infisical (covers both human users and machine identities)",
unit: "{request}"
});
const requestDurationHistogram = infisicalMeter.createHistogram("infisical.http.server.request.duration", {
description: "API request latency",
unit: "s"
});
export const apiMetrics = fp(async (fastify) => {
fastify.addHook("onResponse", async (request, reply) => { fastify.addHook("onResponse", async (request, reply) => {
const { method } = request; const { method } = request;
const route = request.routerPath; const route = request.routerPath;
@@ -17,5 +31,67 @@ export const apiMetrics = fp(async (fastify) => {
method, method,
statusCode statusCode
}); });
const orgId = requestContext.get("orgId");
const orgName = requestContext.get("orgName");
const userAuthInfo = requestContext.get("userAuthInfo");
const identityAuthInfo = requestContext.get("identityAuthInfo");
const projectDetails = requestContext.get("projectDetails");
const userAgent = requestContext.get("userAgent");
const ip = requestContext.get("ip");
const attributes: Record<string, string | number> = {
"http.request.method": method,
"http.route": route,
"http.response.status_code": statusCode
};
if (orgId) {
attributes["infisical.organization.id"] = orgId;
}
if (orgName) {
attributes["infisical.organization.name"] = orgName;
}
if (userAuthInfo) {
if (userAuthInfo.userId) {
attributes["infisical.user.id"] = userAuthInfo.userId;
}
if (userAuthInfo.email) {
attributes["infisical.user.email"] = userAuthInfo.email;
}
}
if (identityAuthInfo) {
if (identityAuthInfo.identityId) {
attributes["infisical.identity.id"] = identityAuthInfo.identityId;
}
if (identityAuthInfo.identityName) {
attributes["infisical.identity.name"] = identityAuthInfo.identityName;
}
if (identityAuthInfo.authMethod) {
attributes["infisical.auth.method"] = identityAuthInfo.authMethod;
}
}
if (projectDetails) {
if (projectDetails.id) {
attributes["infisical.project.id"] = projectDetails.id;
}
if (projectDetails.name) {
attributes["infisical.project.name"] = projectDetails.name;
}
}
if (userAgent) {
attributes["user_agent.original"] = userAgent;
}
if (ip) {
attributes["client.address"] = ip;
}
requestCounter.add(1, attributes);
requestDurationHistogram.record(reply.elapsedTime / 1000, attributes);
}); });
}); });
@@ -1,4 +1,4 @@
import { requestContext } from "@fastify/request-context"; import { requestContext, RequestContextData } from "@fastify/request-context";
import { FastifyRequest } from "fastify"; import { FastifyRequest } from "fastify";
import fp from "fastify-plugin"; import fp from "fastify-plugin";
import type { JwtPayload } from "jsonwebtoken"; import type { JwtPayload } from "jsonwebtoken";
@@ -159,10 +159,11 @@ export const injectIdentity = fp(
switch (authMode) { switch (authMode) {
case AuthMode.JWT: { case AuthMode.JWT: {
const { user, tokenVersionId, orgId, rootOrgId, parentOrgId } = const { user, tokenVersionId, orgId, orgName, rootOrgId, parentOrgId } =
await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector); await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector);
requestContext.set("orgId", orgId); requestContext.set("orgId", orgId);
requestContext.set("orgName", orgName);
requestContext.set("userAuthInfo", { userId: user.id, email: user.email || "" });
req.auth = { req.auth = {
authMode: AuthMode.JWT, authMode: AuthMode.JWT,
user, user,
@@ -186,6 +187,7 @@ export const injectIdentity = fp(
); );
const serverCfg = await getServerCfg(); const serverCfg = await getServerCfg();
requestContext.set("orgId", identity.orgId); requestContext.set("orgId", identity.orgId);
requestContext.set("orgName", identity.orgName);
req.auth = { req.auth = {
authMode: AuthMode.IDENTITY_ACCESS_TOKEN, authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
actor, actor,
@@ -198,24 +200,23 @@ export const injectIdentity = fp(
isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId), isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId),
token token
}; };
if (token?.identityAuth?.oidc) { const identityAuthInfo: RequestContextData["identityAuthInfo"] = {
requestContext.set("identityAuthInfo", {
identityId: identity.identityId, identityId: identity.identityId,
oidc: token?.identityAuth?.oidc identityName: identity.name,
}); authMethod: identity.authMethod
};
if (token?.identityAuth?.oidc) {
identityAuthInfo.oidc = token?.identityAuth?.oidc;
} }
if (token?.identityAuth?.kubernetes) { if (token?.identityAuth?.kubernetes) {
requestContext.set("identityAuthInfo", { identityAuthInfo.kubernetes = token?.identityAuth?.kubernetes;
identityId: identity.identityId,
kubernetes: token?.identityAuth?.kubernetes
});
} }
if (token?.identityAuth?.aws) { if (token?.identityAuth?.aws) {
requestContext.set("identityAuthInfo", { identityAuthInfo.aws = token?.identityAuth?.aws;
identityId: identity.identityId,
aws: token?.identityAuth?.aws
});
} }
requestContext.set("identityAuthInfo", identityAuthInfo);
break; break;
} }
case AuthMode.SERVICE_TOKEN: { case AuthMode.SERVICE_TOKEN: {
@@ -1,4 +1,5 @@
import { ForbiddenError, PureAbility } from "@casl/ability"; import { ForbiddenError, PureAbility } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import opentelemetry from "@opentelemetry/api"; import opentelemetry from "@opentelemetry/api";
import fastifyPlugin from "fastify-plugin"; import fastifyPlugin from "fastify-plugin";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
@@ -47,6 +48,12 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
unit: "1" unit: "1"
}); });
const infisicalMeter = opentelemetry.metrics.getMeter("Infisical");
const errorCounter = infisicalMeter.createCounter("infisical.http.server.error.count", {
description: "Total number of API errors in Infisical (covers both human users and machine identities)",
unit: "{error}"
});
server.setErrorHandler((error, req, res) => { server.setErrorHandler((error, req, res) => {
req.log.error(error); req.log.error(error);
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
@@ -61,6 +68,67 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
type: errorType, type: errorType,
name: error.name name: error.name
}); });
const orgId = requestContext.get("orgId");
const orgName = requestContext.get("orgName");
const userAuthInfo = requestContext.get("userAuthInfo");
const identityAuthInfo = requestContext.get("identityAuthInfo");
const projectDetails = requestContext.get("projectDetails");
const attributes: Record<string, string | number> = {
"http.request.method": method,
"http.route": route,
"error.type": errorType,
"error.name": error.name
};
if (orgId) {
attributes["infisical.organization.id"] = orgId;
}
if (orgName) {
attributes["infisical.organization.name"] = orgName;
}
if (userAuthInfo) {
if (userAuthInfo.userId) {
attributes["infisical.user.id"] = userAuthInfo.userId;
}
if (userAuthInfo.email) {
attributes["infisical.user.email"] = userAuthInfo.email;
}
}
if (identityAuthInfo) {
if (identityAuthInfo.identityId) {
attributes["infisical.identity.id"] = identityAuthInfo.identityId;
}
if (identityAuthInfo.identityName) {
attributes["infisical.identity.name"] = identityAuthInfo.identityName;
}
if (identityAuthInfo.authMethod) {
attributes["infisical.auth.method"] = identityAuthInfo.authMethod;
}
}
if (projectDetails) {
if (projectDetails.id) {
attributes["infisical.project.id"] = projectDetails.id;
}
if (projectDetails.name) {
attributes["infisical.project.name"] = projectDetails.name;
}
}
const userAgent = req.headers["user-agent"];
if (userAgent) {
attributes["user_agent.original"] = userAgent;
}
if (req.realIp) {
attributes["client.address"] = req.realIp;
}
errorCounter.add(1, attributes);
} }
if (error instanceof BadRequestError) { if (error instanceof BadRequestError) {
+6 -1
View File
@@ -608,6 +608,10 @@ export const registerRoutes = async (
const membershipGroupService = membershipGroupServiceFactory({ const membershipGroupService = membershipGroupServiceFactory({
membershipGroupDAL, membershipGroupDAL,
membershipRoleDAL, membershipRoleDAL,
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
secretApprovalPolicyDAL,
secretApprovalPolicyApproverDAL: sapApproverDAL,
roleDAL, roleDAL,
permissionService, permissionService,
orgDAL orgDAL
@@ -1705,7 +1709,8 @@ export const registerRoutes = async (
licenseService, licenseService,
permissionService, permissionService,
kmsService, kmsService,
membershipIdentityDAL membershipIdentityDAL,
orgDAL
}); });
const identityAwsAuthService = identityAwsAuthServiceFactory({ const identityAwsAuthService = identityAwsAuthServiceFactory({
@@ -89,6 +89,10 @@ import {
NetlifyConnectionListItemSchema, NetlifyConnectionListItemSchema,
SanitizedNetlifyConnectionSchema SanitizedNetlifyConnectionSchema
} from "@app/services/app-connection/netlify"; } from "@app/services/app-connection/netlify";
import {
NorthflankConnectionListItemSchema,
SanitizedNorthflankConnectionSchema
} from "@app/services/app-connection/northflank";
import { OktaConnectionListItemSchema, SanitizedOktaConnectionSchema } from "@app/services/app-connection/okta"; import { OktaConnectionListItemSchema, SanitizedOktaConnectionSchema } from "@app/services/app-connection/okta";
import { import {
PostgresConnectionListItemSchema, PostgresConnectionListItemSchema,
@@ -161,6 +165,7 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedSupabaseConnectionSchema.options, ...SanitizedSupabaseConnectionSchema.options,
...SanitizedDigitalOceanConnectionSchema.options, ...SanitizedDigitalOceanConnectionSchema.options,
...SanitizedNetlifyConnectionSchema.options, ...SanitizedNetlifyConnectionSchema.options,
...SanitizedNorthflankConnectionSchema.options,
...SanitizedOktaConnectionSchema.options, ...SanitizedOktaConnectionSchema.options,
...SanitizedAzureADCSConnectionSchema.options, ...SanitizedAzureADCSConnectionSchema.options,
...SanitizedRedisConnectionSchema.options, ...SanitizedRedisConnectionSchema.options,
@@ -205,6 +210,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
SupabaseConnectionListItemSchema, SupabaseConnectionListItemSchema,
DigitalOceanConnectionListItemSchema, DigitalOceanConnectionListItemSchema,
NetlifyConnectionListItemSchema, NetlifyConnectionListItemSchema,
NorthflankConnectionListItemSchema,
OktaConnectionListItemSchema, OktaConnectionListItemSchema,
AzureADCSConnectionListItemSchema, AzureADCSConnectionListItemSchema,
RedisConnectionListItemSchema, RedisConnectionListItemSchema,
@@ -30,6 +30,7 @@ import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerMySqlConnectionRouter } from "./mysql-connection-router"; import { registerMySqlConnectionRouter } from "./mysql-connection-router";
import { registerNetlifyConnectionRouter } from "./netlify-connection-router"; import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
import { registerNorthflankConnectionRouter } from "./northflank-connection-router";
import { registerOktaConnectionRouter } from "./okta-connection-router"; import { registerOktaConnectionRouter } from "./okta-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRailwayConnectionRouter } from "./railway-connection-router"; import { registerRailwayConnectionRouter } from "./railway-connection-router";
@@ -84,6 +85,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Supabase]: registerSupabaseConnectionRouter, [AppConnection.Supabase]: registerSupabaseConnectionRouter,
[AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter, [AppConnection.DigitalOcean]: registerDigitalOceanConnectionRouter,
[AppConnection.Netlify]: registerNetlifyConnectionRouter, [AppConnection.Netlify]: registerNetlifyConnectionRouter,
[AppConnection.Northflank]: registerNorthflankConnectionRouter,
[AppConnection.Okta]: registerOktaConnectionRouter, [AppConnection.Okta]: registerOktaConnectionRouter,
[AppConnection.Redis]: registerRedisConnectionRouter, [AppConnection.Redis]: registerRedisConnectionRouter,
[AppConnection.Chef]: registerChefConnectionRouter [AppConnection.Chef]: registerChefConnectionRouter
@@ -0,0 +1,87 @@
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 {
CreateNorthflankConnectionSchema,
SanitizedNorthflankConnectionSchema,
UpdateNorthflankConnectionSchema
} from "@app/services/app-connection/northflank";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerNorthflankConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Northflank,
server,
sanitizedResponseSchema: SanitizedNorthflankConnectionSchema,
createSchema: CreateNorthflankConnectionSchema,
updateSchema: UpdateNorthflankConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/projects`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z.object({
projects: z
.object({
name: z.string(),
id: z.string()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const projects = await server.services.appConnection.northflank.listProjects(connectionId, req.permission);
return { projects };
}
});
server.route({
method: "GET",
url: `/:connectionId/projects/:projectId/secret-groups`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid(),
projectId: z.string()
}),
response: {
200: z.object({
secretGroups: z
.object({
name: z.string(),
id: z.string()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId, projectId } = req.params;
const secretGroups = await server.services.appConnection.northflank.listSecretGroups(
connectionId,
projectId,
req.permission
);
return { secretGroups };
}
});
};
@@ -24,6 +24,7 @@ import { registerHerokuSyncRouter } from "./heroku-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerLaravelForgeSyncRouter } from "./laravel-forge-sync-router"; import { registerLaravelForgeSyncRouter } from "./laravel-forge-sync-router";
import { registerNetlifySyncRouter } from "./netlify-sync-router"; import { registerNetlifySyncRouter } from "./netlify-sync-router";
import { registerNorthflankSyncRouter } from "./northflank-sync-router";
import { registerRailwaySyncRouter } from "./railway-sync-router"; import { registerRailwaySyncRouter } from "./railway-sync-router";
import { registerRenderSyncRouter } from "./render-sync-router"; import { registerRenderSyncRouter } from "./render-sync-router";
import { registerSupabaseSyncRouter } from "./supabase-sync-router"; import { registerSupabaseSyncRouter } from "./supabase-sync-router";
@@ -65,6 +66,7 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.Checkly]: registerChecklySyncRouter, [SecretSync.Checkly]: registerChecklySyncRouter,
[SecretSync.DigitalOceanAppPlatform]: registerDigitalOceanAppPlatformSyncRouter, [SecretSync.DigitalOceanAppPlatform]: registerDigitalOceanAppPlatformSyncRouter,
[SecretSync.Netlify]: registerNetlifySyncRouter, [SecretSync.Netlify]: registerNetlifySyncRouter,
[SecretSync.Northflank]: registerNorthflankSyncRouter,
[SecretSync.Bitbucket]: registerBitbucketSyncRouter, [SecretSync.Bitbucket]: registerBitbucketSyncRouter,
[SecretSync.LaravelForge]: registerLaravelForgeSyncRouter, [SecretSync.LaravelForge]: registerLaravelForgeSyncRouter,
[SecretSync.Chef]: registerChefSyncRouter [SecretSync.Chef]: registerChefSyncRouter
@@ -0,0 +1,17 @@
import {
CreateNorthflankSyncSchema,
NorthflankSyncSchema,
UpdateNorthflankSyncSchema
} from "@app/services/secret-sync/northflank";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerNorthflankSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Northflank,
server,
responseSchema: NorthflankSyncSchema,
createSchema: CreateNorthflankSyncSchema,
updateSchema: UpdateNorthflankSyncSchema
});
@@ -47,6 +47,7 @@ import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { LaravelForgeSyncListItemSchema, LaravelForgeSyncSchema } from "@app/services/secret-sync/laravel-forge"; import { LaravelForgeSyncListItemSchema, LaravelForgeSyncSchema } from "@app/services/secret-sync/laravel-forge";
import { NetlifySyncListItemSchema, NetlifySyncSchema } from "@app/services/secret-sync/netlify"; import { NetlifySyncListItemSchema, NetlifySyncSchema } from "@app/services/secret-sync/netlify";
import { NorthflankSyncListItemSchema, NorthflankSyncSchema } from "@app/services/secret-sync/northflank";
import { RailwaySyncListItemSchema, RailwaySyncSchema } from "@app/services/secret-sync/railway/railway-sync-schemas"; import { RailwaySyncListItemSchema, RailwaySyncSchema } from "@app/services/secret-sync/railway/railway-sync-schemas";
import { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas"; import { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas";
import { SupabaseSyncListItemSchema, SupabaseSyncSchema } from "@app/services/secret-sync/supabase"; import { SupabaseSyncListItemSchema, SupabaseSyncSchema } from "@app/services/secret-sync/supabase";
@@ -86,6 +87,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
ChecklySyncSchema, ChecklySyncSchema,
DigitalOceanAppPlatformSyncSchema, DigitalOceanAppPlatformSyncSchema,
NetlifySyncSchema, NetlifySyncSchema,
NorthflankSyncSchema,
BitbucketSyncSchema, BitbucketSyncSchema,
LaravelForgeSyncSchema, LaravelForgeSyncSchema,
ChefSyncSchema ChefSyncSchema
@@ -121,6 +123,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
ChecklySyncListItemSchema, ChecklySyncListItemSchema,
SupabaseSyncListItemSchema, SupabaseSyncListItemSchema,
NetlifySyncListItemSchema, NetlifySyncListItemSchema,
NorthflankSyncListItemSchema,
BitbucketSyncListItemSchema, BitbucketSyncListItemSchema,
LaravelForgeSyncListItemSchema, LaravelForgeSyncListItemSchema,
ChefSyncListItemSchema ChefSyncListItemSchema
+82 -8
View File
@@ -7,6 +7,7 @@
// All the any rules are disabled because passport typesense with fastify is really poor // All the any rules are disabled because passport typesense with fastify is really poor
import { Authenticator } from "@fastify/passport"; import { Authenticator } from "@fastify/passport";
import { requestContext } from "@fastify/request-context";
import fastifySession from "@fastify/session"; import fastifySession from "@fastify/session";
import RedisStore from "connect-redis"; import RedisStore from "connect-redis";
import { CronJob } from "cron"; import { CronJob } from "cron";
@@ -21,6 +22,7 @@ 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, fetchGithubUser } from "@app/lib/requests/github"; import { fetchGithubEmails, fetchGithubUser } from "@app/lib/requests/github";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { authRateLimit } from "@app/server/config/rateLimiter"; import { authRateLimit } from "@app/server/config/rateLimiter";
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie"; import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { AuthMethod } from "@app/services/auth/auth-type"; import { AuthMethod } from "@app/services/auth/auth-type";
@@ -51,7 +53,6 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
}, },
// eslint-disable-next-line // eslint-disable-next-line
async (req, _accessToken, _refreshToken, profile, cb) => { async (req, _accessToken, _refreshToken, profile, cb) => {
try {
// @ts-expect-error this is because this is express type and not fastify // @ts-expect-error this is because this is express type and not fastify
const callbackPort = req.session.get("callbackPort"); const callbackPort = req.session.get("callbackPort");
// @ts-expect-error this is because this is express type and not fastify // @ts-expect-error this is because this is express type and not fastify
@@ -64,7 +65,9 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
name: "OauthGoogleRegister" name: "OauthGoogleRegister"
}); });
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({ try {
const { isUserCompleted, providerAuthToken, user, orgId, orgName } =
await server.services.login.oauth2Login({
email, email,
firstName: profile?.name?.givenName || "", firstName: profile?.name?.givenName || "",
lastName: profile?.name?.familyName || "", lastName: profile?.name?.familyName || "",
@@ -72,9 +75,32 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
callbackPort, callbackPort,
orgSlug orgSlug
}); });
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.user.id": user.id,
"infisical.organization.id": orgId,
"infisical.organization.name": orgName,
"infisical.auth.method": AuthAttemptAuthMethod.GOOGLE,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
cb(null, { isUserCompleted, providerAuthToken }); cb(null, { isUserCompleted, providerAuthToken });
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.auth.method": AuthAttemptAuthMethod.GOOGLE,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
cb(error as Error, false); cb(error as Error, false);
} }
} }
@@ -101,27 +127,50 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
}, },
// eslint-disable-next-line // eslint-disable-next-line
async (req: any, accessToken: string, _refreshToken: string, _profile: any, done: Function) => { async (req: any, accessToken: string, _refreshToken: string, _profile: any, done: Function) => {
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"); if (!email) throw new Error("No primary email found");
try {
// profile does not get automatically populated so we need to manually fetch user info // profile does not get automatically populated so we need to manually fetch user info
const user = await fetchGithubUser(accessToken); const githubUser = await fetchGithubUser(accessToken);
const callbackPort = req.session.get("callbackPort"); const callbackPort = req.session.get("callbackPort");
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({ const { isUserCompleted, providerAuthToken, user, orgId, orgName } =
await server.services.login.oauth2Login({
email, email,
firstName: user.name || user.login, firstName: githubUser.name || githubUser.login,
lastName: "", lastName: "",
authMethod: AuthMethod.GITHUB, authMethod: AuthMethod.GITHUB,
callbackPort callbackPort
}); });
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.user.id": user.id,
"infisical.organization.id": orgId,
"infisical.organization.name": orgName,
"infisical.auth.method": AuthAttemptAuthMethod.GITHUB,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
done(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken }); done(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken });
} catch (err) { } catch (err) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.auth.method": AuthAttemptAuthMethod.GITHUB,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
logger.error(err); logger.error(err);
done(err as Error, false); done(err as Error, false);
} }
@@ -147,11 +196,13 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
pkce: 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) => {
const email = profile.emails[0].value;
try { try {
const callbackPort = req.session.get("callbackPort"); const callbackPort = req.session.get("callbackPort");
const email = profile.emails[0].value; const { isUserCompleted, providerAuthToken, user, orgId, orgName } =
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({ await server.services.login.oauth2Login({
email, email,
firstName: profile.displayName || profile.username || "", firstName: profile.displayName || profile.username || "",
lastName: "", lastName: "",
@@ -159,8 +210,31 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
callbackPort callbackPort
}); });
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.user.id": user.id,
"infisical.organization.id": orgId,
"infisical.organization.name": orgName,
"infisical.auth.method": AuthAttemptAuthMethod.GITLAB,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return cb(null, { isUserCompleted, providerAuthToken }); return cb(null, { isUserCompleted, providerAuthToken });
} catch (error) { } catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.auth.method": AuthAttemptAuthMethod.GITLAB,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
logger.error(error); logger.error(error);
cb(error as Error, false); cb(error as Error, false);
} }
@@ -39,7 +39,8 @@ export enum AppConnection {
Okta = "okta", Okta = "okta",
Redis = "redis", Redis = "redis",
LaravelForge = "laravel-forge", LaravelForge = "laravel-forge",
Chef = "chef" Chef = "chef",
Northflank = "northflank"
} }
export enum AWSRegion { export enum AWSRegion {
@@ -68,6 +68,7 @@ import {
} from "./bitbucket"; } from "./bitbucket";
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda"; import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
import { ChecklyConnectionMethod, getChecklyConnectionListItem, validateChecklyConnectionCredentials } from "./checkly"; import { ChecklyConnectionMethod, getChecklyConnectionListItem, validateChecklyConnectionCredentials } from "./checkly";
import { ChefConnectionMethod, getChefConnectionListItem, validateChefConnectionCredentials } from "./chef";
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum"; import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
import { import {
getCloudflareConnectionListItem, getCloudflareConnectionListItem,
@@ -113,6 +114,11 @@ import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums"; import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns"; import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns";
import { getNetlifyConnectionListItem, validateNetlifyConnectionCredentials } from "./netlify"; import { getNetlifyConnectionListItem, validateNetlifyConnectionCredentials } from "./netlify";
import {
getNorthflankConnectionListItem,
NorthflankConnectionMethod,
validateNorthflankConnectionCredentials
} from "./northflank";
import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta"; import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway"; import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
@@ -142,7 +148,6 @@ import {
WindmillConnectionMethod WindmillConnectionMethod
} from "./windmill"; } from "./windmill";
import { getZabbixConnectionListItem, validateZabbixConnectionCredentials, ZabbixConnectionMethod } from "./zabbix"; import { getZabbixConnectionListItem, validateZabbixConnectionCredentials, ZabbixConnectionMethod } from "./zabbix";
import { ChefConnectionMethod, getChefConnectionListItem, validateChefConnectionCredentials } from "./chef";
const SECRET_SYNC_APP_CONNECTION_MAP = Object.fromEntries( const SECRET_SYNC_APP_CONNECTION_MAP = Object.fromEntries(
Object.entries(SECRET_SYNC_CONNECTION_MAP).map(([key, value]) => [value, key]) Object.entries(SECRET_SYNC_CONNECTION_MAP).map(([key, value]) => [value, key])
@@ -204,6 +209,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
getSupabaseConnectionListItem(), getSupabaseConnectionListItem(),
getDigitalOceanConnectionListItem(), getDigitalOceanConnectionListItem(),
getNetlifyConnectionListItem(), getNetlifyConnectionListItem(),
getNorthflankConnectionListItem(),
getOktaConnectionListItem(), getOktaConnectionListItem(),
getRedisConnectionListItem(), getRedisConnectionListItem(),
getChefConnectionListItem() getChefConnectionListItem()
@@ -334,10 +340,11 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Northflank]: validateNorthflankConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator [AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator
}; };
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service); return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service);
@@ -379,6 +386,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case BitbucketConnectionMethod.ApiToken: case BitbucketConnectionMethod.ApiToken:
case ZabbixConnectionMethod.ApiToken: case ZabbixConnectionMethod.ApiToken:
case DigitalOceanConnectionMethod.ApiToken: case DigitalOceanConnectionMethod.ApiToken:
case NorthflankConnectionMethod.ApiToken:
case OktaConnectionMethod.ApiToken: case OktaConnectionMethod.ApiToken:
case LaravelForgeConnectionMethod.ApiToken: case LaravelForgeConnectionMethod.ApiToken:
return "API Token"; return "API Token";
@@ -477,6 +485,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Supabase]: platformManagedCredentialsNotSupported, [AppConnection.Supabase]: platformManagedCredentialsNotSupported,
[AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported, [AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported,
[AppConnection.Netlify]: platformManagedCredentialsNotSupported, [AppConnection.Netlify]: platformManagedCredentialsNotSupported,
[AppConnection.Northflank]: platformManagedCredentialsNotSupported,
[AppConnection.Okta]: platformManagedCredentialsNotSupported, [AppConnection.Okta]: platformManagedCredentialsNotSupported,
[AppConnection.Redis]: platformManagedCredentialsNotSupported, [AppConnection.Redis]: platformManagedCredentialsNotSupported,
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported, [AppConnection.LaravelForge]: platformManagedCredentialsNotSupported,
@@ -41,7 +41,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Netlify]: "Netlify", [AppConnection.Netlify]: "Netlify",
[AppConnection.Okta]: "Okta", [AppConnection.Okta]: "Okta",
[AppConnection.Redis]: "Redis", [AppConnection.Redis]: "Redis",
[AppConnection.Chef]: "Chef" [AppConnection.Chef]: "Chef",
[AppConnection.Northflank]: "Northflank"
}; };
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = { export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@@ -85,5 +86,6 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Netlify]: AppConnectionPlanType.Regular, [AppConnection.Netlify]: AppConnectionPlanType.Regular,
[AppConnection.Okta]: AppConnectionPlanType.Regular, [AppConnection.Okta]: AppConnectionPlanType.Regular,
[AppConnection.Redis]: AppConnectionPlanType.Regular, [AppConnection.Redis]: AppConnectionPlanType.Regular,
[AppConnection.Chef]: AppConnectionPlanType.Regular [AppConnection.Chef]: AppConnectionPlanType.Regular,
[AppConnection.Northflank]: AppConnectionPlanType.Regular
}; };
@@ -98,6 +98,8 @@ import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql"; import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
import { ValidateNetlifyConnectionCredentialsSchema } from "./netlify"; import { ValidateNetlifyConnectionCredentialsSchema } from "./netlify";
import { netlifyConnectionService } from "./netlify/netlify-connection-service"; import { netlifyConnectionService } from "./netlify/netlify-connection-service";
import { ValidateNorthflankConnectionCredentialsSchema } from "./northflank";
import { northflankConnectionService } from "./northflank/northflank-connection-service";
import { ValidateOktaConnectionCredentialsSchema } from "./okta"; import { ValidateOktaConnectionCredentialsSchema } from "./okta";
import { oktaConnectionService } from "./okta/okta-connection-service"; import { oktaConnectionService } from "./okta/okta-connection-service";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
@@ -172,6 +174,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema, [AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
[AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema, [AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema,
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema, [AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
[AppConnection.Northflank]: ValidateNorthflankConnectionCredentialsSchema,
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema, [AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema, [AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema,
[AppConnection.Chef]: ValidateChefConnectionCredentialsSchema [AppConnection.Chef]: ValidateChefConnectionCredentialsSchema
@@ -879,6 +882,7 @@ export const appConnectionServiceFactory = ({
supabase: supabaseConnectionService(connectAppConnectionById), supabase: supabaseConnectionService(connectAppConnectionById),
digitalOcean: digitalOceanAppPlatformConnectionService(connectAppConnectionById), digitalOcean: digitalOceanAppPlatformConnectionService(connectAppConnectionById),
netlify: netlifyConnectionService(connectAppConnectionById), netlify: netlifyConnectionService(connectAppConnectionById),
northflank: northflankConnectionService(connectAppConnectionById),
okta: oktaConnectionService(connectAppConnectionById), okta: oktaConnectionService(connectAppConnectionById),
laravelForge: laravelForgeConnectionService(connectAppConnectionById), laravelForge: laravelForgeConnectionService(connectAppConnectionById),
chef: chefConnectionService(connectAppConnectionById) chef: chefConnectionService(connectAppConnectionById)
@@ -168,6 +168,12 @@ import {
TNetlifyConnectionInput, TNetlifyConnectionInput,
TValidateNetlifyConnectionCredentialsSchema TValidateNetlifyConnectionCredentialsSchema
} from "./netlify"; } from "./netlify";
import {
TNorthflankConnection,
TNorthflankConnectionConfig,
TNorthflankConnectionInput,
TValidateNorthflankConnectionCredentialsSchema
} from "./northflank";
import { import {
TOktaConnection, TOktaConnection,
TOktaConnectionConfig, TOktaConnectionConfig,
@@ -279,6 +285,7 @@ export type TAppConnection = { id: string } & (
| TSupabaseConnection | TSupabaseConnection
| TDigitalOceanConnection | TDigitalOceanConnection
| TNetlifyConnection | TNetlifyConnection
| TNorthflankConnection
| TOktaConnection | TOktaConnection
| TRedisConnection | TRedisConnection
| TChefConnection | TChefConnection
@@ -327,6 +334,7 @@ export type TAppConnectionInput = { id: string } & (
| TSupabaseConnectionInput | TSupabaseConnectionInput
| TDigitalOceanConnectionInput | TDigitalOceanConnectionInput
| TNetlifyConnectionInput | TNetlifyConnectionInput
| TNorthflankConnectionInput
| TOktaConnectionInput | TOktaConnectionInput
| TRedisConnectionInput | TRedisConnectionInput
| TChefConnectionInput | TChefConnectionInput
@@ -393,6 +401,7 @@ export type TAppConnectionConfig =
| TSupabaseConnectionConfig | TSupabaseConnectionConfig
| TDigitalOceanConnectionConfig | TDigitalOceanConnectionConfig
| TNetlifyConnectionConfig | TNetlifyConnectionConfig
| TNorthflankConnectionConfig
| TOktaConnectionConfig | TOktaConnectionConfig
| TRedisConnectionConfig | TRedisConnectionConfig
| TChefConnectionConfig; | TChefConnectionConfig;
@@ -436,6 +445,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateSupabaseConnectionCredentialsSchema | TValidateSupabaseConnectionCredentialsSchema
| TValidateDigitalOceanCredentialsSchema | TValidateDigitalOceanCredentialsSchema
| TValidateNetlifyConnectionCredentialsSchema | TValidateNetlifyConnectionCredentialsSchema
| TValidateNorthflankConnectionCredentialsSchema
| TValidateOktaConnectionCredentialsSchema | TValidateOktaConnectionCredentialsSchema
| TValidateRedisConnectionCredentialsSchema | TValidateRedisConnectionCredentialsSchema
| TValidateChefConnectionCredentialsSchema; | TValidateChefConnectionCredentialsSchema;
@@ -0,0 +1,5 @@
export * from "./northflank-connection-enums";
export * from "./northflank-connection-fns";
export * from "./northflank-connection-schemas";
export * from "./northflank-connection-service";
export * from "./northflank-connection-types";
@@ -0,0 +1,3 @@
export enum NorthflankConnectionMethod {
ApiToken = "api-token"
}
@@ -0,0 +1,114 @@
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { NorthflankConnectionMethod } from "./northflank-connection-enums";
import {
TNorthflankConnection,
TNorthflankConnectionConfig,
TNorthflankProject,
TNorthflankSecretGroup
} from "./northflank-connection-types";
const NORTHFLANK_API_URL = "https://api.northflank.com";
export const getNorthflankConnectionListItem = () => {
return {
name: "Northflank" as const,
app: AppConnection.Northflank as const,
methods: Object.values(NorthflankConnectionMethod)
};
};
export const validateNorthflankConnectionCredentials = async (config: TNorthflankConnectionConfig) => {
const { credentials } = config;
try {
await request.get(`${NORTHFLANK_API_URL}/v1/projects`, {
headers: {
Authorization: `Bearer ${credentials.apiToken}`,
Accept: "application/json"
}
});
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to validate Northflank credentials: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: `Failed to validate Northflank credentials - verify API token is correct`
});
}
return credentials;
};
export const listProjects = async (appConnection: TNorthflankConnection): Promise<TNorthflankProject[]> => {
const { credentials } = appConnection;
try {
const {
data: {
data: { projects }
}
} = await request.get<{ data: { projects: TNorthflankProject[] } }>(`${NORTHFLANK_API_URL}/v1/projects`, {
headers: {
Authorization: `Bearer ${credentials.apiToken}`,
Accept: "application/json"
}
});
return projects;
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to list Northflank projects: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to list Northflank projects",
error
});
}
};
export const listSecretGroups = async (
appConnection: TNorthflankConnection,
projectId: string
): Promise<TNorthflankSecretGroup[]> => {
const { credentials } = appConnection;
try {
const {
data: {
data: { secrets }
}
} = await request.get<{ data: { secrets: TNorthflankSecretGroup[] } }>(
`${NORTHFLANK_API_URL}/v1/projects/${projectId}/secrets`,
{
headers: {
Authorization: `Bearer ${credentials.apiToken}`,
Accept: "application/json"
}
}
);
return secrets;
} catch (error: unknown) {
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to list Northflank secret groups: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to list Northflank secret groups",
error
});
}
};
@@ -0,0 +1,60 @@
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 { NorthflankConnectionMethod } from "./northflank-connection-enums";
export const NorthflankConnectionApiTokenCredentialsSchema = z.object({
apiToken: z.string().trim().min(1, "API Token required").describe(AppConnections.CREDENTIALS.NORTHFLANK.apiToken)
});
const BaseNorthflankConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.Northflank)
});
export const NorthflankConnectionSchema = BaseNorthflankConnectionSchema.extend({
method: z.literal(NorthflankConnectionMethod.ApiToken),
credentials: NorthflankConnectionApiTokenCredentialsSchema
});
export const SanitizedNorthflankConnectionSchema = z.discriminatedUnion("method", [
BaseNorthflankConnectionSchema.extend({
method: z.literal(NorthflankConnectionMethod.ApiToken),
credentials: NorthflankConnectionApiTokenCredentialsSchema.pick({})
})
]);
export const ValidateNorthflankConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z
.literal(NorthflankConnectionMethod.ApiToken)
.describe(AppConnections.CREATE(AppConnection.Northflank).method),
credentials: NorthflankConnectionApiTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Northflank).credentials
)
})
]);
export const CreateNorthflankConnectionSchema = ValidateNorthflankConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Northflank)
);
export const UpdateNorthflankConnectionSchema = z
.object({
credentials: NorthflankConnectionApiTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Northflank).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Northflank));
export const NorthflankConnectionListItemSchema = z.object({
name: z.literal("Northflank"),
app: z.literal(AppConnection.Northflank),
methods: z.nativeEnum(NorthflankConnectionMethod).array()
});
@@ -0,0 +1,50 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
listProjects as getNorthflankProjects,
listSecretGroups as getNorthflankSecretGroups
} from "./northflank-connection-fns";
import { TNorthflankConnection, TNorthflankSecretGroup } from "./northflank-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TNorthflankConnection>;
export const northflankConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listProjects = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Northflank, connectionId, actor);
try {
const projects = await getNorthflankProjects(appConnection);
return projects;
} catch (error) {
logger.error({ error, connectionId, actor: actor.type }, "Failed to establish connection with Northflank");
return [];
}
};
const listSecretGroups = async (
connectionId: string,
projectId: string,
actor: OrgServiceActor
): Promise<TNorthflankSecretGroup[]> => {
const appConnection = await getAppConnection(AppConnection.Northflank, connectionId, actor);
try {
const secretGroups = await getNorthflankSecretGroups(appConnection, projectId);
return secretGroups;
} catch (error) {
logger.error({ error, connectionId, projectId, actor: actor.type }, "Failed to list Northflank secret groups");
return [];
}
};
return {
listProjects,
listSecretGroups
};
};
@@ -0,0 +1,35 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateNorthflankConnectionSchema,
NorthflankConnectionSchema,
ValidateNorthflankConnectionCredentialsSchema
} from "./northflank-connection-schemas";
export type TNorthflankConnection = z.infer<typeof NorthflankConnectionSchema>;
export type TNorthflankConnectionInput = z.infer<typeof CreateNorthflankConnectionSchema> & {
app: AppConnection.Northflank;
};
export type TValidateNorthflankConnectionCredentialsSchema = typeof ValidateNorthflankConnectionCredentialsSchema;
export type TNorthflankConnectionConfig = DiscriminativePick<
TNorthflankConnection,
"method" | "app" | "credentials"
> & {
orgId: string;
};
export type TNorthflankProject = {
id: string;
name: string;
};
export type TNorthflankSecretGroup = {
id: string;
name: string;
};
@@ -210,6 +210,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` }); if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` });
let orgId = ""; let orgId = "";
let orgName = "";
let rootOrgId = ""; let rootOrgId = "";
let parentOrgId = ""; let parentOrgId = "";
if (token.organizationId) { if (token.organizationId) {
@@ -235,9 +236,11 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
throw new ForbiddenRequestError({ message: "User organization membership is inactive" }); throw new ForbiddenRequestError({ message: "User organization membership is inactive" });
} }
orgId = subOrganization.id; orgId = subOrganization.id;
orgName = subOrganization.name;
rootOrgId = token.organizationId; rootOrgId = token.organizationId;
parentOrgId = subOrganization.parentOrgId as string; parentOrgId = subOrganization.parentOrgId as string;
} else { } else {
const organization = await orgDAL.findOne({ id: token.organizationId });
const orgMembership = await membershipUserDAL.findOne({ const orgMembership = await membershipUserDAL.findOne({
actorUserId: user.id, actorUserId: user.id,
scopeOrgId: token.organizationId, scopeOrgId: token.organizationId,
@@ -253,12 +256,13 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
} }
orgId = token.organizationId; orgId = token.organizationId;
orgName = organization.name;
rootOrgId = token.organizationId; rootOrgId = token.organizationId;
parentOrgId = token.organizationId; parentOrgId = token.organizationId;
} }
} }
return { user, tokenVersionId: token.tokenVersionId, orgId, rootOrgId, parentOrgId }; return { user, tokenVersionId: token.tokenVersionId, orgId, orgName, rootOrgId, parentOrgId };
}; };
return { return {
@@ -16,6 +16,7 @@ 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 { getMinExpiresIn, 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 { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
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";
@@ -385,6 +386,9 @@ export const authLoginServiceFactory = ({
providerAuthToken?: string; providerAuthToken?: string;
captchaToken?: string; captchaToken?: string;
}) => { }) => {
const appCfg = getConfig();
try {
const usersByUsername = await userDAL.findUserEncKeyByUsername({ const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email username: email
}); });
@@ -394,7 +398,10 @@ export const authLoginServiceFactory = ({
if (!userEnc) throw new BadRequestError({ message: "User not found" }); if (!userEnc) throw new BadRequestError({ message: "User not found" });
if (userEnc.encryptionVersion !== UserEncryption.V2) { if (userEnc.encryptionVersion !== UserEncryption.V2) {
throw new BadRequestError({ message: "Legacy encryption scheme not supported", name: "LegacyEncryptionScheme" }); throw new BadRequestError({
message: "Legacy encryption scheme not supported",
name: "LegacyEncryptionScheme"
});
} }
if (!userEnc.hashedPassword) { if (!userEnc.hashedPassword) {
@@ -435,6 +442,18 @@ export const authLoginServiceFactory = ({
organizationId organizationId
}); });
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.organization.id": organizationId,
"infisical.user.email": email,
"infisical.user.id": userEnc.userId,
"infisical.auth.method": AuthAttemptAuthMethod.EMAIL,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": ip,
"user_agent.original": userAgent
});
}
return { return {
tokens: { tokens: {
accessToken: token.access, accessToken: token.access,
@@ -442,6 +461,19 @@ export const authLoginServiceFactory = ({
}, },
user: userEnc user: userEnc
} as const; } as const;
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.auth.method": AuthAttemptAuthMethod.EMAIL,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": ip,
"user_agent.original": userAgent
});
}
throw error;
}
}; };
const selectOrganization = async ({ const selectOrganization = async ({
@@ -965,7 +997,8 @@ export const authLoginServiceFactory = ({
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
} }
); );
return { isUserCompleted, providerAuthToken };
return { isUserCompleted, providerAuthToken, user, orgId, orgName };
}; };
/** /**
@@ -210,6 +210,7 @@ export const identityAccessTokenServiceFactory = ({
}); });
} }
let orgId = ""; let orgId = "";
let orgName = "";
let parentOrgId = ""; let parentOrgId = "";
const identityOrgDetails = await orgDAL.findOne({ id: identityAccessToken.identityScopeOrgId }); const identityOrgDetails = await orgDAL.findOne({ id: identityAccessToken.identityScopeOrgId });
const rootOrgId = identityOrgDetails.rootOrgId || identityOrgDetails.id; const rootOrgId = identityOrgDetails.rootOrgId || identityOrgDetails.id;
@@ -229,8 +230,12 @@ export const identityAccessTokenServiceFactory = ({
throw new BadRequestError({ message: "Identity does not belong to any organization" }); throw new BadRequestError({ message: "Identity does not belong to any organization" });
} }
orgId = subOrganization.id; orgId = subOrganization.id;
orgName = subOrganization.name;
parentOrgId = subOrganization.parentOrgId as string; parentOrgId = subOrganization.parentOrgId as string;
} else { } else {
const organization = await orgDAL.findOne({ id: rootOrgId });
const identityOrgMembership = await membershipIdentityDAL.findOne({ const identityOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization, scope: AccessScope.Organization,
actorIdentityId: identityAccessToken.identityId, actorIdentityId: identityAccessToken.identityId,
@@ -242,6 +247,7 @@ export const identityAccessTokenServiceFactory = ({
} }
orgId = rootOrgId; orgId = rootOrgId;
orgName = organization.name;
parentOrgId = rootOrgId; parentOrgId = rootOrgId;
} }
@@ -253,7 +259,7 @@ export const identityAccessTokenServiceFactory = ({
await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses }); await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses });
await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1); await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1);
return { ...identityAccessToken, orgId, rootOrgId, parentOrgId }; return { ...identityAccessToken, orgId, rootOrgId, parentOrgId, orgName };
}; };
return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken }; return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken };
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
@@ -22,6 +23,7 @@ import {
} from "@app/lib/errors"; } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -65,6 +67,7 @@ export const identityAliCloudAuthServiceFactory = ({
orgDAL orgDAL
}: TIdentityAliCloudAuthServiceFactoryDep) => { }: TIdentityAliCloudAuthServiceFactoryDep) => {
const login = async ({ identityId, ...params }: TLoginAliCloudAuthDTO) => { const login = async ({ identityId, ...params }: TLoginAliCloudAuthDTO) => {
const appCfg = getConfig();
const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId }); const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId });
if (!identityAliCloudAuth) { if (!identityAliCloudAuth) {
throw new NotFoundError({ throw new NotFoundError({
@@ -75,6 +78,9 @@ export const identityAliCloudAuthServiceFactory = ({
const identity = await identityDAL.findById(identityAliCloudAuth.identityId); const identity = await identityDAL.findById(identityAliCloudAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
try {
const requestUrl = new URL("https://sts.aliyuncs.com"); const requestUrl = new URL("https://sts.aliyuncs.com");
for (const key of Object.keys(params)) { for (const key of Object.keys(params)) {
@@ -121,7 +127,6 @@ export const identityAliCloudAuthServiceFactory = ({
return newToken; return newToken;
}); });
const appCfg = getConfig();
const accessToken = crypto.jwt().sign( const accessToken = crypto.jwt().sign(
{ {
identityId: identityAliCloudAuth.identityId, identityId: identityAliCloudAuth.identityId,
@@ -136,12 +141,40 @@ export const identityAliCloudAuthServiceFactory = ({
} }
); );
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityAliCloudAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.ALICLOUD_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { return {
identityAliCloudAuth, identityAliCloudAuth,
accessToken, accessToken,
identityAccessToken, identityAccessToken,
identity identity
}; };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityAliCloudAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.ALICLOUD_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
}; };
const attachAliCloudAuth = async ({ const attachAliCloudAuth = async ({
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import axios from "axios"; import axios from "axios";
import RE2 from "re2"; import RE2 from "re2";
@@ -22,6 +23,7 @@ import {
} from "@app/lib/errors"; } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -98,6 +100,7 @@ export const identityAwsAuthServiceFactory = ({
orgDAL orgDAL
}: TIdentityAwsAuthServiceFactoryDep) => { }: TIdentityAwsAuthServiceFactoryDep) => {
const login = async ({ identityId, iamHttpRequestMethod, iamRequestBody, iamRequestHeaders }: TLoginAwsAuthDTO) => { const login = async ({ identityId, iamHttpRequestMethod, iamRequestBody, iamRequestHeaders }: TLoginAwsAuthDTO) => {
const appCfg = getConfig();
const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId }); const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId });
if (!identityAwsAuth) { if (!identityAwsAuth) {
throw new NotFoundError({ message: "AWS auth method not found for identity, did you configure AWS auth?" }); throw new NotFoundError({ message: "AWS auth method not found for identity, did you configure AWS auth?" });
@@ -106,6 +109,8 @@ export const identityAwsAuthServiceFactory = ({
const identity = await identityDAL.findById(identityAwsAuth.identityId); const identity = await identityDAL.findById(identityAwsAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
try {
const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString());
const body: string = Buffer.from(iamRequestBody, "base64").toString(); const body: string = Buffer.from(iamRequestBody, "base64").toString();
@@ -196,7 +201,6 @@ export const identityAwsAuthServiceFactory = ({
return newToken; return newToken;
}); });
const appCfg = getConfig();
const splitArn = extractPrincipalArnEntity(Arn); const splitArn = extractPrincipalArnEntity(Arn);
const accessToken = crypto.jwt().sign( const accessToken = crypto.jwt().sign(
{ {
@@ -226,7 +230,35 @@ export const identityAwsAuthServiceFactory = ({
} }
); );
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityAwsAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.AWS_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { accessToken, identityAwsAuth, identityAccessToken, identity }; return { accessToken, identityAwsAuth, identityAccessToken, identity };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityAwsAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.AWS_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
}; };
const attachAwsAuth = async ({ const attachAwsAuth = async ({
@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -18,6 +19,7 @@ import {
UnauthorizedError UnauthorizedError
} from "@app/lib/errors"; } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -61,6 +63,7 @@ export const identityAzureAuthServiceFactory = ({
orgDAL orgDAL
}: TIdentityAzureAuthServiceFactoryDep) => { }: TIdentityAzureAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: azureJwt }: TLoginAzureAuthDTO) => { const login = async ({ identityId, jwt: azureJwt }: TLoginAzureAuthDTO) => {
const appCfg = getConfig();
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId }); const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
if (!identityAzureAuth) { if (!identityAzureAuth) {
throw new NotFoundError({ message: "Azure auth method not found for identity, did you configure Azure Auth?" }); throw new NotFoundError({ message: "Azure auth method not found for identity, did you configure Azure Auth?" });
@@ -69,6 +72,9 @@ export const identityAzureAuthServiceFactory = ({
const identity = await identityDAL.findById(identityAzureAuth.identityId); const identity = await identityDAL.findById(identityAzureAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
try {
const azureIdentity = await validateAzureIdentity({ const azureIdentity = await validateAzureIdentity({
tenantId: identityAzureAuth.tenantId, tenantId: identityAzureAuth.tenantId,
resource: identityAzureAuth.resource, resource: identityAzureAuth.resource,
@@ -115,7 +121,6 @@ export const identityAzureAuthServiceFactory = ({
return newToken; return newToken;
}); });
const appCfg = getConfig();
const accessToken = crypto.jwt().sign( const accessToken = crypto.jwt().sign(
{ {
identityId: identityAzureAuth.identityId, identityId: identityAzureAuth.identityId,
@@ -131,7 +136,35 @@ export const identityAzureAuthServiceFactory = ({
} }
); );
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityAzureAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.AZURE_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { accessToken, identityAzureAuth, identityAccessToken, identity }; return { accessToken, identityAzureAuth, identityAccessToken, identity };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityAzureAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.AZURE_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
}; };
const attachAzureAuth = async ({ const attachAzureAuth = async ({
@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -18,6 +19,7 @@ import {
UnauthorizedError UnauthorizedError
} from "@app/lib/errors"; } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -59,6 +61,7 @@ export const identityGcpAuthServiceFactory = ({
orgDAL orgDAL
}: TIdentityGcpAuthServiceFactoryDep) => { }: TIdentityGcpAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: gcpJwt }: TLoginGcpAuthDTO) => { const login = async ({ identityId, jwt: gcpJwt }: TLoginGcpAuthDTO) => {
const appCfg = getConfig();
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
if (!identityGcpAuth) { if (!identityGcpAuth) {
throw new NotFoundError({ message: "GCP auth method not found for identity, did you configure GCP auth?" }); throw new NotFoundError({ message: "GCP auth method not found for identity, did you configure GCP auth?" });
@@ -67,6 +70,8 @@ export const identityGcpAuthServiceFactory = ({
const identity = await identityDAL.findById(identityGcpAuth.identityId); const identity = await identityDAL.findById(identityGcpAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
try {
let gcpIdentityDetails: TGcpIdentityDetails; let gcpIdentityDetails: TGcpIdentityDetails;
switch (identityGcpAuth.type) { switch (identityGcpAuth.type) {
case "gce": { case "gce": {
@@ -102,7 +107,11 @@ export const identityGcpAuthServiceFactory = ({
}); });
} }
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedProjects && gcpIdentityDetails.computeEngineDetails) { if (
identityGcpAuth.type === "gce" &&
identityGcpAuth.allowedProjects &&
gcpIdentityDetails.computeEngineDetails
) {
// validate if the project that the service account belongs to is in the list of allowed projects // validate if the project that the service account belongs to is in the list of allowed projects
const isProjectAllowed = identityGcpAuth.allowedProjects const isProjectAllowed = identityGcpAuth.allowedProjects
@@ -151,8 +160,6 @@ export const identityGcpAuthServiceFactory = ({
); );
return newToken; return newToken;
}); });
const appCfg = getConfig();
const accessToken = crypto.jwt().sign( const accessToken = crypto.jwt().sign(
{ {
identityId: identityGcpAuth.identityId, identityId: identityGcpAuth.identityId,
@@ -168,7 +175,35 @@ export const identityGcpAuthServiceFactory = ({
} }
); );
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityGcpAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.GCP_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { accessToken, identityGcpAuth, identityAccessToken, identity }; return { accessToken, identityGcpAuth, identityAccessToken, identity };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityGcpAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.GCP_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
}; };
const attachGcpAuth = async ({ const attachGcpAuth = async ({
@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import https from "https"; import https from "https";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { JwksClient } from "jwks-rsa"; import { JwksClient } from "jwks-rsa";
@@ -21,6 +22,7 @@ import {
UnauthorizedError UnauthorizedError
} from "@app/lib/errors"; } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { getValueByDot } from "@app/lib/template/dot-access"; import { getValueByDot } from "@app/lib/template/dot-access";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -67,6 +69,7 @@ export const identityJwtAuthServiceFactory = ({
orgDAL orgDAL
}: TIdentityJwtAuthServiceFactoryDep) => { }: TIdentityJwtAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => { const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => {
const appCfg = getConfig();
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId }); const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
if (!identityJwtAuth) { if (!identityJwtAuth) {
throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" }); throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" });
@@ -75,6 +78,8 @@ export const identityJwtAuthServiceFactory = ({
const identity = await identityDAL.findById(identityJwtAuth.identityId); const identity = await identityDAL.findById(identityJwtAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
try {
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: identity.orgId orgId: identity.orgId
@@ -228,7 +233,6 @@ export const identityJwtAuthServiceFactory = ({
return newToken; return newToken;
}); });
const appCfg = getConfig();
const accessToken = crypto.jwt().sign( const accessToken = crypto.jwt().sign(
{ {
identityId: identityJwtAuth.identityId, identityId: identityJwtAuth.identityId,
@@ -244,7 +248,35 @@ export const identityJwtAuthServiceFactory = ({
} }
); );
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityJwtAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.JWT_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { accessToken, identityJwtAuth, identityAccessToken, identity }; return { accessToken, identityJwtAuth, identityAccessToken, identity };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityJwtAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.JWT_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
}; };
const attachJwtAuth = async ({ const attachJwtAuth = async ({
@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import axios, { AxiosError } from "axios"; import axios, { AxiosError } from "axios";
import https from "https"; import https from "https";
import RE2 from "re2"; import RE2 from "re2";
@@ -37,6 +38,7 @@ import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -182,6 +184,7 @@ export const identityKubernetesAuthServiceFactory = ({
}; };
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => { const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => {
const appCfg = getConfig();
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
if (!identityKubernetesAuth) { if (!identityKubernetesAuth) {
throw new NotFoundError({ throw new NotFoundError({
@@ -192,6 +195,9 @@ export const identityKubernetesAuthServiceFactory = ({
const identity = await identityDAL.findById(identityKubernetesAuth.identityId); const identity = await identityDAL.findById(identityKubernetesAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
try {
const { decryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: identity.orgId orgId: identity.orgId
@@ -242,7 +248,9 @@ export const identityKubernetesAuthServiceFactory = ({
kind: "TokenReview", kind: "TokenReview",
spec: { spec: {
token: serviceAccountJwt, token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {}) ...(identityKubernetesAuth.allowedAudience
? { audiences: [identityKubernetesAuth.allowedAudience] }
: {})
} }
}, },
{ {
@@ -295,7 +303,9 @@ export const identityKubernetesAuthServiceFactory = ({
kind: "TokenReview", kind: "TokenReview",
spec: { spec: {
token: serviceAccountJwt, token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {}) ...(identityKubernetesAuth.allowedAudience
? { audiences: [identityKubernetesAuth.allowedAudience] }
: {})
} }
}, },
{ {
@@ -457,7 +467,6 @@ export const identityKubernetesAuthServiceFactory = ({
return newToken; return newToken;
}); });
const appCfg = getConfig();
const accessToken = crypto.jwt().sign( const accessToken = crypto.jwt().sign(
{ {
identityId: identityKubernetesAuth.identityId, identityId: identityKubernetesAuth.identityId,
@@ -479,7 +488,35 @@ export const identityKubernetesAuthServiceFactory = ({
} }
); );
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityKubernetesAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.KUBERNETES_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { accessToken, identityKubernetesAuth, identityAccessToken, identity }; return { accessToken, identityKubernetesAuth, identityAccessToken, identity };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityKubernetesAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.KUBERNETES_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
}; };
const attachKubernetesAuth = async ({ const attachKubernetesAuth = async ({
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
@@ -29,6 +30,7 @@ import {
} from "@app/lib/errors"; } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -151,6 +153,7 @@ export const identityLdapAuthServiceFactory = ({
}; };
const login = async ({ identityId }: TLoginLdapAuthDTO) => { const login = async ({ identityId }: TLoginLdapAuthDTO) => {
const appCfg = getConfig();
const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId }); const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
if (!identityLdapAuth) { if (!identityLdapAuth) {
@@ -162,6 +165,7 @@ export const identityLdapAuthServiceFactory = ({
const identity = await identityDAL.findById(identityLdapAuth.identityId); const identity = await identityDAL.findById(identityLdapAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
const plan = await licenseService.getPlan(identity.orgId); const plan = await licenseService.getPlan(identity.orgId);
if (!plan.ldap) { if (!plan.ldap) {
throw new BadRequestError({ throw new BadRequestError({
@@ -170,6 +174,7 @@ export const identityLdapAuthServiceFactory = ({
}); });
} }
try {
const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => { const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update( await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
@@ -191,7 +196,6 @@ export const identityLdapAuthServiceFactory = ({
return newToken; return newToken;
}); });
const appCfg = getConfig();
const accessToken = crypto.jwt().sign( const accessToken = crypto.jwt().sign(
{ {
identityId: identityLdapAuth.identityId, identityId: identityLdapAuth.identityId,
@@ -207,7 +211,35 @@ export const identityLdapAuthServiceFactory = ({
} }
); );
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityLdapAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.LDAP_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { accessToken, identityLdapAuth, identityAccessToken, identity }; return { accessToken, identityLdapAuth, identityAccessToken, identity };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityLdapAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.LDAP_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
}; };
const attachLdapAuth = async ({ const attachLdapAuth = async ({
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import RE2 from "re2"; import RE2 from "re2";
@@ -23,6 +24,7 @@ import {
} from "@app/lib/errors"; } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -63,6 +65,7 @@ export const identityOciAuthServiceFactory = ({
orgDAL orgDAL
}: TIdentityOciAuthServiceFactoryDep) => { }: TIdentityOciAuthServiceFactoryDep) => {
const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => { const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => {
const appCfg = getConfig();
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId }); const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
if (!identityOciAuth) { if (!identityOciAuth) {
throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" }); throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" });
@@ -71,6 +74,8 @@ export const identityOciAuthServiceFactory = ({
const identity = await identityDAL.findById(identityOciAuth.identityId); const identity = await identityDAL.findById(identityOciAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
try {
// Validate OCI host format. Ensures that the host is in "identity.<region>.oraclecloud.com" format. // Validate OCI host format. Ensures that the host is in "identity.<region>.oraclecloud.com" format.
if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) { if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) {
throw new BadRequestError({ throw new BadRequestError({
@@ -124,7 +129,6 @@ export const identityOciAuthServiceFactory = ({
return newToken; return newToken;
}); });
const appCfg = getConfig();
const accessToken = crypto.jwt().sign( const accessToken = crypto.jwt().sign(
{ {
identityId: identityOciAuth.identityId, identityId: identityOciAuth.identityId,
@@ -139,12 +143,40 @@ export const identityOciAuthServiceFactory = ({
} }
); );
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityOciAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.OCI_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { return {
identityOciAuth, identityOciAuth,
accessToken, accessToken,
identityAccessToken, identityAccessToken,
identity identity
}; };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityOciAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.OCI_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
}; };
const attachOciAuth = async ({ const attachOciAuth = async ({
@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import axios from "axios"; import axios from "axios";
import https from "https"; import https from "https";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
@@ -22,6 +23,7 @@ import {
UnauthorizedError UnauthorizedError
} from "@app/lib/errors"; } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { getValueByDot } from "@app/lib/template/dot-access"; import { getValueByDot } from "@app/lib/template/dot-access";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -67,6 +69,7 @@ export const identityOidcAuthServiceFactory = ({
orgDAL orgDAL
}: TIdentityOidcAuthServiceFactoryDep) => { }: TIdentityOidcAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => { const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => {
const appCfg = getConfig();
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
if (!identityOidcAuth) { if (!identityOidcAuth) {
throw new NotFoundError({ message: "OIDC auth method not found for identity, did you configure OIDC auth?" }); throw new NotFoundError({ message: "OIDC auth method not found for identity, did you configure OIDC auth?" });
@@ -75,6 +78,8 @@ export const identityOidcAuthServiceFactory = ({
const identity = await identityDAL.findById(identityOidcAuth.identityId); const identity = await identityDAL.findById(identityOidcAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
try {
const { decryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: identity.orgId orgId: identity.orgId
@@ -198,7 +203,6 @@ export const identityOidcAuthServiceFactory = ({
return newToken; return newToken;
}); });
const appCfg = getConfig();
const accessToken = crypto.jwt().sign( const accessToken = crypto.jwt().sign(
{ {
identityId: identityOidcAuth.identityId, identityId: identityOidcAuth.identityId,
@@ -219,7 +223,35 @@ export const identityOidcAuthServiceFactory = ({
} }
); );
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityOidcAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.OIDC_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { accessToken, identityOidcAuth, identityAccessToken, identity, oidcTokenData: tokenData }; return { accessToken, identityOidcAuth, identityAccessToken, identity, oidcTokenData: tokenData };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityOidcAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.OIDC_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
}; };
const attachOidcAuth = async ({ const attachOidcAuth = async ({
@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -19,6 +20,7 @@ import {
UnauthorizedError UnauthorizedError
} from "@app/lib/errors"; } from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -27,6 +29,7 @@ import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identit
import { TKmsServiceFactory } from "../kms/kms-service"; import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types"; import { KmsDataKey } from "../kms/kms-types";
import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal";
import { TOrgDALFactory } from "../org/org-dal";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal"; import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal";
import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types"; import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types";
@@ -42,6 +45,7 @@ type TIdentityTlsCertAuthServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
orgDAL: Pick<TOrgDALFactory, "findById">;
}; };
const parseSubjectDetails = (data: string) => { const parseSubjectDetails = (data: string) => {
@@ -60,9 +64,11 @@ export const identityTlsCertAuthServiceFactory = ({
membershipIdentityDAL, membershipIdentityDAL,
licenseService, licenseService,
permissionService, permissionService,
kmsService kmsService,
orgDAL
}: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => { }: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => {
const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ identityId, clientCertificate }) => { const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ identityId, clientCertificate }) => {
const appCfg = getConfig();
const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId }); const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId });
if (!identityTlsCertAuth) { if (!identityTlsCertAuth) {
throw new NotFoundError({ throw new NotFoundError({
@@ -73,6 +79,9 @@ export const identityTlsCertAuthServiceFactory = ({
const identity = await identityDAL.findById(identityTlsCertAuth.identityId); const identity = await identityDAL.findById(identityTlsCertAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
try {
const { decryptor } = await kmsService.createCipherPairWithDataKey({ const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: identity.orgId orgId: identity.orgId
@@ -140,7 +149,6 @@ export const identityTlsCertAuthServiceFactory = ({
return newToken; return newToken;
}); });
const appCfg = getConfig();
const accessToken = crypto.jwt().sign( const accessToken = crypto.jwt().sign(
{ {
identityId: identityTlsCertAuth.identityId, identityId: identityTlsCertAuth.identityId,
@@ -155,12 +163,40 @@ export const identityTlsCertAuthServiceFactory = ({
} }
); );
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityTlsCertAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.TLS_CERT_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { return {
identityTlsCertAuth, identityTlsCertAuth,
accessToken, accessToken,
identityAccessToken, identityAccessToken,
identity identity
}; };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityTlsCertAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.TLS_CERT_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
}; };
const attachTlsCertAuth: TIdentityTlsCertAuthServiceFactory["attachTlsCertAuth"] = async ({ const attachTlsCertAuth: TIdentityTlsCertAuthServiceFactory["attachTlsCertAuth"] = async ({
@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -21,6 +22,7 @@ import {
} from "@app/lib/errors"; } from "@app/lib/errors";
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip"; import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -77,6 +79,7 @@ export const identityUaServiceFactory = ({
identityDAL identityDAL
}: TIdentityUaServiceFactoryDep) => { }: TIdentityUaServiceFactoryDep) => {
const login = async (clientId: string, clientSecret: string, ip: string) => { const login = async (clientId: string, clientSecret: string, ip: string) => {
const appCfg = getConfig();
const identityUa = await identityUaDAL.findOne({ clientId }); const identityUa = await identityUaDAL.findOne({ clientId });
if (!identityUa) { if (!identityUa) {
throw new UnauthorizedError({ throw new UnauthorizedError({
@@ -84,6 +87,10 @@ export const identityUaServiceFactory = ({
}); });
} }
const identity = await identityDAL.findById(identityUa.identityId);
const org = await orgDAL.findById(identity.orgId);
try {
checkIPAgainstBlocklist({ checkIPAgainstBlocklist({
ipAddress: ip, ipAddress: ip,
trustedIps: identityUa.clientSecretTrustedIps as TIp[] trustedIps: identityUa.clientSecretTrustedIps as TIp[]
@@ -221,7 +228,6 @@ export const identityUaServiceFactory = ({
accessTokenMaxTTL: 1000000000 accessTokenMaxTTL: 1000000000
}; };
const identity = await identityDAL.findById(identityUa.identityId);
const identityAccessToken = await identityUaDAL.transaction(async (tx) => { const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx); const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
await membershipIdentityDAL.update( await membershipIdentityDAL.update(
@@ -249,7 +255,6 @@ export const identityUaServiceFactory = ({
return newToken; return newToken;
}); });
const appCfg = getConfig();
const accessToken = crypto.jwt().sign( const accessToken = crypto.jwt().sign(
{ {
identityId: identityUa.identityId, identityId: identityUa.identityId,
@@ -266,6 +271,19 @@ export const identityUaServiceFactory = ({
} }
); );
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityUa.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.UNIVERSAL_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { return {
accessToken, accessToken,
identityUa, identityUa,
@@ -274,6 +292,21 @@ export const identityUaServiceFactory = ({
identity, identity,
...accessTokenTTLParams ...accessTokenTTLParams
}; };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityUa.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.UNIVERSAL_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
}; };
const attachUniversalAuth = async ({ const attachUniversalAuth = async ({
-7
View File
@@ -401,13 +401,6 @@ export const kmsServiceFactory = ({
const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256); const cipher = symmetricCipherService(SymmetricKeyAlgorithm.AES_GCM_256);
const expectedByteLength = getByteLengthForSymmetricEncryptionAlgorithm(algorithm as SymmetricKeyAlgorithm);
if (key.byteLength !== expectedByteLength) {
throw new BadRequestError({
message: `Invalid key length for ${algorithm}. Expected ${expectedByteLength} bytes but got ${key.byteLength} bytes`
});
}
const encryptedKeyMaterial = cipher.encrypt(key, ROOT_ENCRYPTION_KEY); const encryptedKeyMaterial = cipher.encrypt(key, ROOT_ENCRYPTION_KEY);
const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase()); const sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
const dbQuery = async (db: Knex) => { const dbQuery = async (db: Knex) => {
@@ -1,5 +1,15 @@
import { AccessScope, ProjectMembershipRole, TemporaryPermissionMode, TMembershipRolesInsert } from "@app/db/schemas"; import {
AccessScope,
ProjectMembershipRole,
TableName,
TemporaryPermissionMode,
TMembershipRolesInsert
} from "@app/db/schemas";
import { TAccessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { TSecretApprovalPolicyApproverDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
@@ -23,6 +33,10 @@ import { newProjectMembershipGroupFactory } from "./project/project-membership-g
type TMembershipGroupServiceFactoryDep = { type TMembershipGroupServiceFactoryDep = {
membershipGroupDAL: TMembershipGroupDALFactory; membershipGroupDAL: TMembershipGroupDALFactory;
membershipRoleDAL: Pick<TMembershipRoleDALFactory, "insertMany" | "delete">; membershipRoleDAL: Pick<TMembershipRoleDALFactory, "insertMany" | "delete">;
accessApprovalPolicyDAL: Pick<TAccessApprovalPolicyDALFactory, "find">;
accessApprovalPolicyApproverDAL: Pick<TAccessApprovalPolicyApproverDALFactory, "find">;
secretApprovalPolicyDAL: Pick<TSecretApprovalPolicyDALFactory, "find">;
secretApprovalPolicyApproverDAL: Pick<TSecretApprovalPolicyApproverDALFactory, "find">;
roleDAL: Pick<TRoleDALFactory, "find">; roleDAL: Pick<TRoleDALFactory, "find">;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
orgDAL: TOrgDALFactory; orgDAL: TOrgDALFactory;
@@ -33,6 +47,10 @@ export type TMembershipGroupServiceFactory = ReturnType<typeof membershipGroupSe
export const membershipGroupServiceFactory = ({ export const membershipGroupServiceFactory = ({
membershipGroupDAL, membershipGroupDAL,
roleDAL, roleDAL,
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
secretApprovalPolicyDAL,
secretApprovalPolicyApproverDAL,
membershipRoleDAL, membershipRoleDAL,
orgDAL, orgDAL,
permissionService permissionService
@@ -268,6 +286,48 @@ export const membershipGroupServiceFactory = ({
message: "You can't delete your own membership" message: "You can't delete your own membership"
}); });
const accessApprovalPolicyApprovers = await accessApprovalPolicyApproverDAL.find({
approverGroupId: dto.selector.groupId
});
// check if group is assigned to any access approval policy
const accessApprovalPolicyApproverGroupIds = accessApprovalPolicyApprovers.map(({ policyId }) => policyId);
if (accessApprovalPolicyApprovers.length > 0) {
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({
$in: {
[`${TableName.AccessApprovalPolicy}.id` as "id"]: [...new Set(accessApprovalPolicyApproverGroupIds)]
},
projectId: existingMembership.scopeProjectId ?? undefined,
deletedAt: null
});
if (accessApprovalPolicies.length > 0) {
throw new BadRequestError({
message: "This group is assigned to an approval policy and cannot be deleted"
});
}
}
// check if group is assigned to any secret approval policy
const secretApprovalPolicyApprovers = await secretApprovalPolicyApproverDAL.find({
approverGroupId: dto.selector.groupId
});
const secretApprovalPolicyApproverGroupIds = secretApprovalPolicyApprovers.map(({ policyId }) => policyId);
if (secretApprovalPolicyApprovers.length > 0) {
const secretApprovalPolicies = await secretApprovalPolicyDAL.find({
$in: {
[`${TableName.SecretApprovalPolicy}.id` as "id"]: [...new Set(secretApprovalPolicyApproverGroupIds)]
},
projectId: existingMembership.scopeProjectId ?? undefined,
deletedAt: null
});
if (secretApprovalPolicies.length > 0) {
throw new BadRequestError({
message: "This group is assigned to a secret approval policy and cannot be deleted"
});
}
}
const membershipDoc = await membershipGroupDAL.transaction(async (tx) => { const membershipDoc = await membershipGroupDAL.transaction(async (tx) => {
await membershipRoleDAL.delete({ membershipId: existingMembership.id }, tx); await membershipRoleDAL.delete({ membershipId: existingMembership.id }, tx);
const doc = await membershipGroupDAL.deleteById(existingMembership.id, tx); const doc = await membershipGroupDAL.deleteById(existingMembership.id, tx);
@@ -0,0 +1,4 @@
export * from "./northflank-sync-constants";
export * from "./northflank-sync-fns";
export * from "./northflank-sync-schemas";
export * from "./northflank-sync-types";
@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const NORTHFLANK_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Northflank",
destination: SecretSync.Northflank,
connection: AppConnection.Northflank,
canImportSecrets: true
};
@@ -0,0 +1,165 @@
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { SecretSyncError } from "../secret-sync-errors";
import { TNorthflankSyncWithCredentials } from "./northflank-sync-types";
const NORTHFLANK_API_URL = "https://api.northflank.com";
const buildNorthflankAPIErrorMessage = (error: unknown): string => {
let errorMessage = "Northflank API returned an error.";
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as AxiosError;
if (axiosError.response?.data) {
// This is the shape of the error response from the Northflank API
const responseData = axiosError.response.data as {
error?: { message?: string; details?: Record<string, string[]> };
message?: string;
};
const errorParts = [];
if (responseData.error?.message) {
errorParts.push(responseData.error.message);
} else if (responseData.message) {
errorParts.push(responseData.message);
}
if (responseData.error?.details) {
const { details } = responseData.error;
// Flatten the details object into a string
Object.entries(details).forEach(([field, fieldErrors]) => {
if (Array.isArray(fieldErrors)) {
fieldErrors.forEach((fieldError) => errorParts.push(`${field}: ${fieldError}`));
} else {
errorParts.push(`${field}: ${String(fieldErrors)}`);
}
});
}
errorMessage += ` ${errorParts.join(". ")}`;
}
}
return errorMessage;
};
const getNorthflankSecrets = async (secretSync: TNorthflankSyncWithCredentials): Promise<Record<string, string>> => {
const {
destinationConfig: { projectId, secretGroupId },
connection: {
credentials: { apiToken }
}
} = secretSync;
try {
const {
data: {
data: {
secrets: { variables }
}
}
} = await request.get<{
data: {
secrets: {
variables: Record<string, string>;
};
};
}>(`${NORTHFLANK_API_URL}/v1/projects/${projectId}/secrets/${secretGroupId}/details`, {
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
});
return variables;
} catch (error: unknown) {
throw new SecretSyncError({
error,
message: `Failed to fetch Northflank secrets. ${buildNorthflankAPIErrorMessage(error)}`
});
}
};
const updateNorthflankSecrets = async (
secretSync: TNorthflankSyncWithCredentials,
variables: Record<string, string>
): Promise<void> => {
const {
destinationConfig: { projectId, secretGroupId },
connection: {
credentials: { apiToken }
}
} = secretSync;
try {
await request.patch(
`${NORTHFLANK_API_URL}/v1/projects/${projectId}/secrets/${secretGroupId}`,
{
secrets: {
variables
}
},
{
headers: {
Authorization: `Bearer ${apiToken}`,
Accept: "application/json"
}
}
);
} catch (error: unknown) {
throw new SecretSyncError({
error,
message: `Failed to update Northflank secrets. ${buildNorthflankAPIErrorMessage(error)}`
});
}
};
export const NorthflankSyncFns = {
syncSecrets: async (secretSync: TNorthflankSyncWithCredentials, secretMap: TSecretMap): Promise<void> => {
const northflankSecrets = await getNorthflankSecrets(secretSync);
const updatedVariables: Record<string, string> = {};
for (const [key, value] of Object.entries(northflankSecrets)) {
const shouldKeep =
!secretMap[key] && // this prevents duplicates from infisical secrets, because we add all of them to the updateVariables in the next loop
(secretSync.syncOptions.disableSecretDeletion ||
!matchesSchema(key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema));
if (shouldKeep) {
updatedVariables[key] = value;
}
}
for (const [key, { value }] of Object.entries(secretMap)) {
updatedVariables[key] = value;
}
await updateNorthflankSecrets(secretSync, updatedVariables);
},
getSecrets: async (secretSync: TNorthflankSyncWithCredentials): Promise<TSecretMap> => {
const northflankSecrets = await getNorthflankSecrets(secretSync);
return Object.fromEntries(Object.entries(northflankSecrets).map(([key, value]) => [key, { value }]));
},
removeSecrets: async (secretSync: TNorthflankSyncWithCredentials, secretMap: TSecretMap): Promise<void> => {
const northflankSecrets = await getNorthflankSecrets(secretSync);
const updatedVariables: Record<string, string> = {};
for (const [key, value] of Object.entries(northflankSecrets)) {
if (!(key in secretMap)) {
updatedVariables[key] = value;
}
}
await updateNorthflankSecrets(secretSync, updatedVariables);
}
};
@@ -0,0 +1,54 @@
import { z } from "zod";
import { SecretSyncs } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import {
BaseSecretSyncSchema,
GenericCreateSecretSyncFieldsSchema,
GenericUpdateSecretSyncFieldsSchema
} from "@app/services/secret-sync/secret-sync-schemas";
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
const NorthflankSyncDestinationConfigSchema = z.object({
projectId: z
.string()
.trim()
.min(1, "Project ID is required")
.describe(SecretSyncs.DESTINATION_CONFIG.NORTHFLANK.projectId),
projectName: z.string().trim().optional().describe(SecretSyncs.DESTINATION_CONFIG.NORTHFLANK.projectName),
secretGroupId: z
.string()
.trim()
.min(1, "Secret Group ID is required")
.describe(SecretSyncs.DESTINATION_CONFIG.NORTHFLANK.secretGroupId),
secretGroupName: z.string().trim().optional().describe(SecretSyncs.DESTINATION_CONFIG.NORTHFLANK.secretGroupName)
});
const NorthflankSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const NorthflankSyncSchema = BaseSecretSyncSchema(SecretSync.Northflank, NorthflankSyncOptionsConfig).extend({
destination: z.literal(SecretSync.Northflank),
destinationConfig: NorthflankSyncDestinationConfigSchema
});
export const CreateNorthflankSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.Northflank,
NorthflankSyncOptionsConfig
).extend({
destinationConfig: NorthflankSyncDestinationConfigSchema
});
export const UpdateNorthflankSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.Northflank,
NorthflankSyncOptionsConfig
).extend({
destinationConfig: NorthflankSyncDestinationConfigSchema.optional()
});
export const NorthflankSyncListItemSchema = z.object({
name: z.literal("Northflank"),
connection: z.literal(AppConnection.Northflank),
destination: z.literal(SecretSync.Northflank),
canImportSecrets: z.literal(true)
});
@@ -0,0 +1,19 @@
import { z } from "zod";
import { TNorthflankConnection } from "@app/services/app-connection/northflank";
import {
CreateNorthflankSyncSchema,
NorthflankSyncListItemSchema,
NorthflankSyncSchema
} from "./northflank-sync-schemas";
export type TNorthflankSyncListItem = z.infer<typeof NorthflankSyncListItemSchema>;
export type TNorthflankSync = z.infer<typeof NorthflankSyncSchema>;
export type TNorthflankSyncInput = z.infer<typeof CreateNorthflankSyncSchema>;
export type TNorthflankSyncWithCredentials = TNorthflankSync & {
connection: TNorthflankConnection;
};
@@ -28,6 +28,7 @@ export enum SecretSync {
Checkly = "checkly", Checkly = "checkly",
DigitalOceanAppPlatform = "digital-ocean-app-platform", DigitalOceanAppPlatform = "digital-ocean-app-platform",
Netlify = "netlify", Netlify = "netlify",
Northflank = "northflank",
Bitbucket = "bitbucket", Bitbucket = "bitbucket",
LaravelForge = "laravel-forge", LaravelForge = "laravel-forge",
Chef = "chef" Chef = "chef"
@@ -52,6 +52,7 @@ import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
import { LARAVEL_FORGE_SYNC_LIST_OPTION, LaravelForgeSyncFns } from "./laravel-forge"; import { LARAVEL_FORGE_SYNC_LIST_OPTION, LaravelForgeSyncFns } from "./laravel-forge";
import { NETLIFY_SYNC_LIST_OPTION, NetlifySyncFns } from "./netlify"; import { NETLIFY_SYNC_LIST_OPTION, NetlifySyncFns } from "./netlify";
import { NORTHFLANK_SYNC_LIST_OPTION, NorthflankSyncFns } from "./northflank";
import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants"; import { RAILWAY_SYNC_LIST_OPTION } from "./railway/railway-sync-constants";
import { RailwaySyncFns } from "./railway/railway-sync-fns"; import { RailwaySyncFns } from "./railway/railway-sync-fns";
import { RENDER_SYNC_LIST_OPTION, RenderSyncFns } from "./render"; import { RENDER_SYNC_LIST_OPTION, RenderSyncFns } from "./render";
@@ -93,6 +94,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.Checkly]: CHECKLY_SYNC_LIST_OPTION, [SecretSync.Checkly]: CHECKLY_SYNC_LIST_OPTION,
[SecretSync.DigitalOceanAppPlatform]: DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION, [SecretSync.DigitalOceanAppPlatform]: DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION,
[SecretSync.Netlify]: NETLIFY_SYNC_LIST_OPTION, [SecretSync.Netlify]: NETLIFY_SYNC_LIST_OPTION,
[SecretSync.Northflank]: NORTHFLANK_SYNC_LIST_OPTION,
[SecretSync.Bitbucket]: BITBUCKET_SYNC_LIST_OPTION, [SecretSync.Bitbucket]: BITBUCKET_SYNC_LIST_OPTION,
[SecretSync.LaravelForge]: LARAVEL_FORGE_SYNC_LIST_OPTION, [SecretSync.LaravelForge]: LARAVEL_FORGE_SYNC_LIST_OPTION,
[SecretSync.Chef]: CHEF_SYNC_LIST_OPTION [SecretSync.Chef]: CHEF_SYNC_LIST_OPTION
@@ -279,6 +281,8 @@ export const SecretSyncFns = {
return DigitalOceanAppPlatformSyncFns.syncSecrets(secretSync, schemaSecretMap); return DigitalOceanAppPlatformSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Netlify: case SecretSync.Netlify:
return NetlifySyncFns.syncSecrets(secretSync, schemaSecretMap); return NetlifySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Northflank:
return NorthflankSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Bitbucket: case SecretSync.Bitbucket:
return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap); return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.LaravelForge: case SecretSync.LaravelForge:
@@ -398,6 +402,9 @@ export const SecretSyncFns = {
case SecretSync.Netlify: case SecretSync.Netlify:
secretMap = await NetlifySyncFns.getSecrets(secretSync); secretMap = await NetlifySyncFns.getSecrets(secretSync);
break; break;
case SecretSync.Northflank:
secretMap = await NorthflankSyncFns.getSecrets(secretSync);
break;
case SecretSync.Bitbucket: case SecretSync.Bitbucket:
secretMap = await BitbucketSyncFns.getSecrets(secretSync); secretMap = await BitbucketSyncFns.getSecrets(secretSync);
break; break;
@@ -498,6 +505,8 @@ export const SecretSyncFns = {
return DigitalOceanAppPlatformSyncFns.removeSecrets(secretSync, schemaSecretMap); return DigitalOceanAppPlatformSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Netlify: case SecretSync.Netlify:
return NetlifySyncFns.removeSecrets(secretSync, schemaSecretMap); return NetlifySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Northflank:
return NorthflankSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Bitbucket: case SecretSync.Bitbucket:
return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap); return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.LaravelForge: case SecretSync.LaravelForge:
@@ -32,6 +32,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.Checkly]: "Checkly", [SecretSync.Checkly]: "Checkly",
[SecretSync.DigitalOceanAppPlatform]: "Digital Ocean App Platform", [SecretSync.DigitalOceanAppPlatform]: "Digital Ocean App Platform",
[SecretSync.Netlify]: "Netlify", [SecretSync.Netlify]: "Netlify",
[SecretSync.Northflank]: "Northflank",
[SecretSync.Bitbucket]: "Bitbucket", [SecretSync.Bitbucket]: "Bitbucket",
[SecretSync.LaravelForge]: "Laravel Forge", [SecretSync.LaravelForge]: "Laravel Forge",
[SecretSync.Chef]: "Chef" [SecretSync.Chef]: "Chef"
@@ -67,6 +68,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Checkly]: AppConnection.Checkly, [SecretSync.Checkly]: AppConnection.Checkly,
[SecretSync.DigitalOceanAppPlatform]: AppConnection.DigitalOcean, [SecretSync.DigitalOceanAppPlatform]: AppConnection.DigitalOcean,
[SecretSync.Netlify]: AppConnection.Netlify, [SecretSync.Netlify]: AppConnection.Netlify,
[SecretSync.Northflank]: AppConnection.Northflank,
[SecretSync.Bitbucket]: AppConnection.Bitbucket, [SecretSync.Bitbucket]: AppConnection.Bitbucket,
[SecretSync.LaravelForge]: AppConnection.LaravelForge, [SecretSync.LaravelForge]: AppConnection.LaravelForge,
[SecretSync.Chef]: AppConnection.Chef [SecretSync.Chef]: AppConnection.Chef
@@ -102,6 +104,7 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.Checkly]: SecretSyncPlanType.Regular, [SecretSync.Checkly]: SecretSyncPlanType.Regular,
[SecretSync.DigitalOceanAppPlatform]: SecretSyncPlanType.Regular, [SecretSync.DigitalOceanAppPlatform]: SecretSyncPlanType.Regular,
[SecretSync.Netlify]: SecretSyncPlanType.Regular, [SecretSync.Netlify]: SecretSyncPlanType.Regular,
[SecretSync.Northflank]: SecretSyncPlanType.Regular,
[SecretSync.Bitbucket]: SecretSyncPlanType.Regular, [SecretSync.Bitbucket]: SecretSyncPlanType.Regular,
[SecretSync.LaravelForge]: SecretSyncPlanType.Regular, [SecretSync.LaravelForge]: SecretSyncPlanType.Regular,
[SecretSync.Chef]: SecretSyncPlanType.Regular [SecretSync.Chef]: SecretSyncPlanType.Regular
@@ -146,6 +149,7 @@ export const SECRET_SYNC_SKIP_FIELDS_MAP: Record<SecretSync, string[]> = {
[SecretSync.Checkly]: ["groupName", "accountName"], [SecretSync.Checkly]: ["groupName", "accountName"],
[SecretSync.DigitalOceanAppPlatform]: ["appName"], [SecretSync.DigitalOceanAppPlatform]: ["appName"],
[SecretSync.Netlify]: ["accountName", "siteName"], [SecretSync.Netlify]: ["accountName", "siteName"],
[SecretSync.Northflank]: [],
[SecretSync.Bitbucket]: [], [SecretSync.Bitbucket]: [],
[SecretSync.LaravelForge]: [], [SecretSync.LaravelForge]: [],
[SecretSync.Chef]: [] [SecretSync.Chef]: []
@@ -207,6 +211,7 @@ export const DESTINATION_DUPLICATE_CHECK_MAP: Record<SecretSync, DestinationDupl
[SecretSync.Checkly]: defaultDuplicateCheck, [SecretSync.Checkly]: defaultDuplicateCheck,
[SecretSync.DigitalOceanAppPlatform]: defaultDuplicateCheck, [SecretSync.DigitalOceanAppPlatform]: defaultDuplicateCheck,
[SecretSync.Netlify]: defaultDuplicateCheck, [SecretSync.Netlify]: defaultDuplicateCheck,
[SecretSync.Northflank]: defaultDuplicateCheck,
[SecretSync.Bitbucket]: defaultDuplicateCheck, [SecretSync.Bitbucket]: defaultDuplicateCheck,
[SecretSync.LaravelForge]: defaultDuplicateCheck, [SecretSync.LaravelForge]: defaultDuplicateCheck,
[SecretSync.Chef]: defaultDuplicateCheck [SecretSync.Chef]: defaultDuplicateCheck
@@ -125,6 +125,12 @@ import {
TLaravelForgeSyncWithCredentials TLaravelForgeSyncWithCredentials
} from "./laravel-forge"; } from "./laravel-forge";
import { TNetlifySync, TNetlifySyncInput, TNetlifySyncListItem, TNetlifySyncWithCredentials } from "./netlify"; import { TNetlifySync, TNetlifySyncInput, TNetlifySyncListItem, TNetlifySyncWithCredentials } from "./netlify";
import {
TNorthflankSync,
TNorthflankSyncInput,
TNorthflankSyncListItem,
TNorthflankSyncWithCredentials
} from "./northflank";
import { import {
TRailwaySync, TRailwaySync,
TRailwaySyncInput, TRailwaySyncInput,
@@ -189,6 +195,7 @@ export type TSecretSync =
| TChecklySync | TChecklySync
| TSupabaseSync | TSupabaseSync
| TNetlifySync | TNetlifySync
| TNorthflankSync
| TBitbucketSync; | TBitbucketSync;
export type TSecretSyncWithCredentials = export type TSecretSyncWithCredentials =
@@ -222,6 +229,7 @@ export type TSecretSyncWithCredentials =
| TSupabaseSyncWithCredentials | TSupabaseSyncWithCredentials
| TDigitalOceanAppPlatformSyncWithCredentials | TDigitalOceanAppPlatformSyncWithCredentials
| TNetlifySyncWithCredentials | TNetlifySyncWithCredentials
| TNorthflankSyncWithCredentials
| TBitbucketSyncWithCredentials | TBitbucketSyncWithCredentials
| TLaravelForgeSyncWithCredentials; | TLaravelForgeSyncWithCredentials;
@@ -256,6 +264,7 @@ export type TSecretSyncInput =
| TSupabaseSyncInput | TSupabaseSyncInput
| TDigitalOceanAppPlatformSyncInput | TDigitalOceanAppPlatformSyncInput
| TNetlifySyncInput | TNetlifySyncInput
| TNorthflankSyncInput
| TBitbucketSyncInput | TBitbucketSyncInput
| TLaravelForgeSyncInput; | TLaravelForgeSyncInput;
@@ -291,6 +300,7 @@ export type TSecretSyncListItem =
| TSupabaseSyncListItem | TSupabaseSyncListItem
| TDigitalOceanAppPlatformSyncListItem | TDigitalOceanAppPlatformSyncListItem
| TNetlifySyncListItem | TNetlifySyncListItem
| TNorthflankSyncListItem
| TBitbucketSyncListItem; | TBitbucketSyncListItem;
export type TSyncOptionsConfig = { export type TSyncOptionsConfig = {
@@ -34,6 +34,7 @@ import { diff, groupBy } from "@app/lib/fn";
import { setKnexStringValue } from "@app/lib/knex"; import { setKnexStringValue } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { recordSecretReadMetric } from "@app/lib/telemetry/metrics";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service"; import { TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
@@ -1052,6 +1053,11 @@ export const secretV2BridgeServiceFactory = ({
}); });
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
recordSecretReadMetric({
environment,
secretPath: path
});
const cachedSecretDalVersion = await keyStore.pgGetIntItem(SecretServiceCacheKeys.getSecretDalVersion(projectId)); const cachedSecretDalVersion = await keyStore.pgGetIntItem(SecretServiceCacheKeys.getSecretDalVersion(projectId));
const secretDalVersion = Number(cachedSecretDalVersion || 0); const secretDalVersion = Number(cachedSecretDalVersion || 0);
const cacheKey = SecretServiceCacheKeys.getSecretsOfServiceLayer(projectId, secretDalVersion, { const cacheKey = SecretServiceCacheKeys.getSecretsOfServiceLayer(projectId, secretDalVersion, {
@@ -1482,6 +1488,12 @@ export const secretV2BridgeServiceFactory = ({
secretTags: (secret?.tags || []).map((el) => el.slug) secretTags: (secret?.tags || []).map((el) => el.slug)
}); });
recordSecretReadMetric({
environment,
secretPath: path,
name: secretName
});
// this will throw if the user doesn't have read value permission no matter what // this will throw if the user doesn't have read value permission no matter what
// because if its an expansion, it will fully depend on the value. // because if its an expansion, it will fully depend on the value.
const { expandSecretReferences } = expandSecretReferencesFactory({ const { expandSecretReferences } = expandSecretReferencesFactory({
@@ -0,0 +1,4 @@
---
title: "Available"
openapi: "GET /api/v1/app-connections/northflank/available"
---
@@ -0,0 +1,8 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/northflank"
---
<Note>
Check out the configuration docs for [Northflank Connections](/integrations/app-connections/northflank) to learn how to obtain the required credentials.
</Note>
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/app-connections/northflank/{connectionId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/app-connections/northflank/{connectionId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/app-connections/northflank/connection-name/{connectionName}"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/app-connections/northflank"
---
@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/northflank/{connectionId}"
---
@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/northflank"
---
@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/secret-syncs/northflank/{syncId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/secret-syncs/northflank/{syncId}"
---
@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/secret-syncs/northflank/sync-name/{syncName}"
---
@@ -0,0 +1,4 @@
---
title: "Import Secrets"
openapi: "POST /api/v1/secret-syncs/northflank/{syncId}/import-secrets"
---
@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/secret-syncs/northflank"
---
@@ -0,0 +1,4 @@
---
title: "Remove Secrets"
openapi: "POST /api/v1/secret-syncs/northflank/{syncId}/remove-secrets"
---
@@ -0,0 +1,4 @@
---
title: "Sync Secrets"
openapi: "POST /api/v1/secret-syncs/northflank/{syncId}/sync-secrets"
---
@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/secret-syncs/northflank/{syncId}"
---
+28
View File
@@ -131,6 +131,7 @@
"integrations/app-connections/mssql", "integrations/app-connections/mssql",
"integrations/app-connections/mysql", "integrations/app-connections/mysql",
"integrations/app-connections/netlify", "integrations/app-connections/netlify",
"integrations/app-connections/northflank",
"integrations/app-connections/oci", "integrations/app-connections/oci",
"integrations/app-connections/okta", "integrations/app-connections/okta",
"integrations/app-connections/oracledb", "integrations/app-connections/oracledb",
@@ -554,6 +555,7 @@
"integrations/secret-syncs/humanitec", "integrations/secret-syncs/humanitec",
"integrations/secret-syncs/laravel-forge", "integrations/secret-syncs/laravel-forge",
"integrations/secret-syncs/netlify", "integrations/secret-syncs/netlify",
"integrations/secret-syncs/northflank",
"integrations/secret-syncs/oci-vault", "integrations/secret-syncs/oci-vault",
"integrations/secret-syncs/railway", "integrations/secret-syncs/railway",
"integrations/secret-syncs/render", "integrations/secret-syncs/render",
@@ -1862,6 +1864,18 @@
"api-reference/endpoints/app-connections/netlify/delete" "api-reference/endpoints/app-connections/netlify/delete"
] ]
}, },
{
"group": "Northflank",
"pages": [
"api-reference/endpoints/app-connections/northflank/list",
"api-reference/endpoints/app-connections/northflank/available",
"api-reference/endpoints/app-connections/northflank/get-by-id",
"api-reference/endpoints/app-connections/northflank/get-by-name",
"api-reference/endpoints/app-connections/northflank/create",
"api-reference/endpoints/app-connections/northflank/update",
"api-reference/endpoints/app-connections/northflank/delete"
]
},
{ {
"group": "OCI", "group": "OCI",
"pages": [ "pages": [
@@ -2335,6 +2349,20 @@
"api-reference/endpoints/secret-syncs/netlify/remove-secrets" "api-reference/endpoints/secret-syncs/netlify/remove-secrets"
] ]
}, },
{
"group": "Northflank",
"pages": [
"api-reference/endpoints/secret-syncs/northflank/list",
"api-reference/endpoints/secret-syncs/northflank/get-by-id",
"api-reference/endpoints/secret-syncs/northflank/get-by-name",
"api-reference/endpoints/secret-syncs/northflank/create",
"api-reference/endpoints/secret-syncs/northflank/update",
"api-reference/endpoints/secret-syncs/northflank/delete",
"api-reference/endpoints/secret-syncs/northflank/sync-secrets",
"api-reference/endpoints/secret-syncs/northflank/import-secrets",
"api-reference/endpoints/secret-syncs/northflank/remove-secrets"
]
},
{ {
"group": "OCI", "group": "OCI",
"pages": [ "pages": [
Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

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