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

View File

@@ -135,7 +135,7 @@ jobs:
TAG_NAME="${{ github.ref_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
echo "Tag $TAG_NAME already exists, skipping..."

View File

@@ -135,9 +135,23 @@ import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integ
declare module "@fastify/request-context" {
interface RequestContextData {
reqId: string;
ip?: string;
userAgent?: string;
orgId?: string;
orgName?: string;
userAuthInfo?: {
userId: string;
email: string;
};
projectDetails?: {
id: string;
name: string;
slug: string;
};
identityAuthInfo?: {
identityId: string;
identityName: string;
authMethod: string;
oidc?: {
claims: Record<string, string>;
};

View File

@@ -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");
});
}

View File

@@ -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");
});
}
}

View File

@@ -21,7 +21,9 @@ export const PamAccountsSchema = z.object({
updatedAt: z.date(),
rotationEnabled: z.boolean().default(false),
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>;

View File

@@ -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 {
CreatePostgresResourceSchema,
SanitizedPostgresResourceSchema,
UpdatePostgresResourceSchema
} 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";

View File

@@ -7,6 +7,7 @@
// All the any rules are disabled because passport typesense with fastify is really poor
import { Authenticator } from "@fastify/passport";
import { requestContext } from "@fastify/request-context";
import fastifySession from "@fastify/session";
import { MultiSamlStrategy } from "@node-saml/passport-saml";
import { FastifyRequest } from "fastify";
@@ -17,6 +18,7 @@ import { ApiDocsTags, SamlSso } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedSamlConfigSchema } from "@app/server/routes/sanitizedSchema/directory-config";
@@ -102,7 +104,6 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
},
// eslint-disable-next-line
async (req, profile, cb) => {
try {
if (!profile) throw new BadRequestError({ message: "Missing profile" });
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?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved\
try {
const firstName = (profile.firstName ??
// entra sends data in this format
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));
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
const { isUserCompleted, providerAuthToken, user, organization } = await server.services.saml.samlLogin({
externalId: profile.nameID,
email: email.toLowerCase(),
firstName,
@@ -154,8 +156,32 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId,
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 });
} 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);
cb(error as Error);
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
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 { crypto } from "@app/lib/crypto";
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 { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
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 ({
@@ -754,10 +756,35 @@ export const oidcConfigServiceFactory = ({
callbackPort,
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 });
})
.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);
});
}

View File

@@ -45,17 +45,47 @@ export const decryptAccountCredentials = async ({
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,
projectId: string,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
): Promise<T & { credentials: TPamAccountCredentials }> => {
): Promise<T & { credentials: TPamAccountCredentials; lastRotationMessage: string | null }> => {
return {
...account,
credentials: await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials,
projectId,
kmsService
}),
lastRotationMessage: account.encryptedLastRotationMessage
? await decryptAccountMessage({
encryptedMessage: account.encryptedLastRotationMessage,
projectId,
kmsService
})
} as T & { credentials: TPamAccountCredentials };
: null
};
};

View File

@@ -15,6 +15,7 @@ import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
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 { TUserDALFactory } from "@app/services/user/user-dal";
@@ -353,6 +354,7 @@ export const pamAccountServiceFactory = ({
TPamAccounts & {
resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean };
credentials: TPamAccountCredentials;
lastRotationMessage: string | null;
}
> = [];
@@ -376,6 +378,7 @@ export const pamAccountServiceFactory = ({
) {
// Decrypt the account only if the user has permission to read it
const decryptedAccount = await decryptAccount(account, account.projectId, kmsService);
decryptedAndPermittedAccounts.push({
...decryptedAccount,
resource: {
@@ -575,10 +578,10 @@ export const pamAccountServiceFactory = ({
for (let i = 0; i < accounts.length; i += ROTATION_CONCURRENCY_LIMIT) {
const batch = accounts.slice(i, i + ROTATION_CONCURRENCY_LIMIT);
const rotationPromises = batch.map(async (account) =>
pamAccountDAL.transaction(async (tx) => {
const rotationPromises = batch.map(async (account) => {
let logResourceType = "unknown";
try {
await pamAccountDAL.transaction(async (tx) => {
const resource = await pamResourceDAL.findById(account.resourceId, tx);
if (!resource || !resource.encryptedRotationAccountCredentials) return;
logResourceType = resource.resourceType;
@@ -619,7 +622,9 @@ export const pamAccountServiceFactory = ({
account.id,
{
encryptedCredentials,
lastRotatedAt: new Date()
lastRotatedAt: new Date(),
rotationStatus: "success",
encryptedLastRotationMessage: null
},
tx
);
@@ -640,11 +645,26 @@ export const pamAccountServiceFactory = ({
}
}
});
});
} catch (error) {
logger.error(error, `Failed to rotate credentials for account [accountId=${account.id}]`);
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({
projectId: account.projectId,
actor: {
@@ -662,10 +682,8 @@ export const pamAccountServiceFactory = ({
}
}
});
throw error; // Rollback transaction
}
})
);
});
// eslint-disable-next-line no-await-in-loop
await Promise.all(rotationPromises);

View File

@@ -33,7 +33,9 @@ export const BasePamAccountSchemaWithResource = BasePamAccountSchema.extend({
resourceType: true
}).extend({
rotationCredentialsConfigured: z.boolean()
})
}),
lastRotationMessage: z.string().nullable().optional(),
rotationStatus: z.string().nullable().optional()
});
export const BaseCreatePamAccountSchema = z.object({

View File

@@ -41,7 +41,10 @@ export interface SqlResourceConnection {
*
* @returns Promise to be resolved with the new credentials
*/
rotateCredentials: (currentCredentials: TSqlAccountCredentials) => Promise<TSqlAccountCredentials>;
rotateCredentials: (
currentCredentials: TSqlAccountCredentials,
newPassword: string
) => Promise<TSqlAccountCredentials>;
/**
* Close the connection.
@@ -113,8 +116,7 @@ const makeSqlConnection = (
});
}
},
rotateCredentials: async (currentCredentials) => {
const newPassword = alphaNumericNanoId(32);
rotateCredentials: async (currentCredentials, newPassword) => {
// 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,
// 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,
currentCredentials
) => {
const newPassword = alphaNumericNanoId(32);
try {
return await executeWithGateway(
{
@@ -305,7 +308,7 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
password: rotationAccountCredentials.password
},
gatewayV2Service,
(client) => client.rotateCredentials(currentCredentials)
(client) => client.rotateCredentials(currentCredentials, newPassword)
);
} catch (error) {
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({
message: `Unable to rotate account credentials for ${resourceType}: ${(error as Error).message || String(error)}`
message: `Unable to rotate account credentials for ${resourceType}: ${sanitizedErrorMessage}`
});
}
};

View File

@@ -337,6 +337,12 @@ export const permissionServiceFactory = ({
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) {
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
}

View File

@@ -769,7 +769,7 @@ export const samlConfigServiceFactory = ({
});
}
return { isUserCompleted, providerAuthToken };
return { isUserCompleted, providerAuthToken, user, organization };
};
return {

View File

@@ -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 { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
@@ -78,5 +78,7 @@ export type TSamlConfigServiceFactory = {
samlLogin: (arg: TSamlLoginDTO) => Promise<{
isUserCompleted: boolean;
providerAuthToken: string;
user: TUsers;
organization: TOrganizations;
}>;
};

View File

@@ -2348,6 +2348,9 @@ export const AppConnections = {
RAILWAY: {
apiToken: "The API token used to authenticate with Railway."
},
NORTHFLANK: {
apiToken: "The API token used to authenticate with Northflank."
},
CHECKLY: {
apiKey: "The API key used to authenticate with Checkly."
},
@@ -2630,6 +2633,12 @@ export const SecretSyncs = {
CHEF: {
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."
},
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."
}
}
};

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);
}
};

View File

@@ -141,7 +141,9 @@ export const main = async ({
await server.register(fastifyRequestContext, {
defaultStoreValues: (req) => ({
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"]
})
});

View File

@@ -1,12 +1,26 @@
import { requestContext } from "@fastify/request-context";
import opentelemetry from "@opentelemetry/api";
import fp from "fastify-plugin";
export const apiMetrics = fp(async (fastify) => {
const apiMeter = opentelemetry.metrics.getMeter("API");
const latencyHistogram = apiMeter.createHistogram("API_latency", {
unit: "ms"
});
const apiMeter = opentelemetry.metrics.getMeter("API");
const latencyHistogram = apiMeter.createHistogram("API_latency", {
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) => {
const { method } = request;
const route = request.routerPath;
@@ -17,5 +31,67 @@ export const apiMetrics = fp(async (fastify) => {
method,
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);
});
});

View File

@@ -1,4 +1,4 @@
import { requestContext } from "@fastify/request-context";
import { requestContext, RequestContextData } from "@fastify/request-context";
import { FastifyRequest } from "fastify";
import fp from "fastify-plugin";
import type { JwtPayload } from "jsonwebtoken";
@@ -159,10 +159,11 @@ export const injectIdentity = fp(
switch (authMode) {
case AuthMode.JWT: {
const { user, tokenVersionId, orgId, rootOrgId, parentOrgId } =
const { user, tokenVersionId, orgId, orgName, rootOrgId, parentOrgId } =
await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector);
requestContext.set("orgId", orgId);
requestContext.set("orgName", orgName);
requestContext.set("userAuthInfo", { userId: user.id, email: user.email || "" });
req.auth = {
authMode: AuthMode.JWT,
user,
@@ -186,6 +187,7 @@ export const injectIdentity = fp(
);
const serverCfg = await getServerCfg();
requestContext.set("orgId", identity.orgId);
requestContext.set("orgName", identity.orgName);
req.auth = {
authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
actor,
@@ -198,24 +200,23 @@ export const injectIdentity = fp(
isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId),
token
};
if (token?.identityAuth?.oidc) {
requestContext.set("identityAuthInfo", {
const identityAuthInfo: RequestContextData["identityAuthInfo"] = {
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) {
requestContext.set("identityAuthInfo", {
identityId: identity.identityId,
kubernetes: token?.identityAuth?.kubernetes
});
identityAuthInfo.kubernetes = token?.identityAuth?.kubernetes;
}
if (token?.identityAuth?.aws) {
requestContext.set("identityAuthInfo", {
identityId: identity.identityId,
aws: token?.identityAuth?.aws
});
identityAuthInfo.aws = token?.identityAuth?.aws;
}
requestContext.set("identityAuthInfo", identityAuthInfo);
break;
}
case AuthMode.SERVICE_TOKEN: {

View File

@@ -1,4 +1,5 @@
import { ForbiddenError, PureAbility } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import opentelemetry from "@opentelemetry/api";
import fastifyPlugin from "fastify-plugin";
import jwt from "jsonwebtoken";
@@ -47,6 +48,12 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
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) => {
req.log.error(error);
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
@@ -61,6 +68,67 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
type: errorType,
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) {

View File

@@ -608,6 +608,10 @@ export const registerRoutes = async (
const membershipGroupService = membershipGroupServiceFactory({
membershipGroupDAL,
membershipRoleDAL,
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
secretApprovalPolicyDAL,
secretApprovalPolicyApproverDAL: sapApproverDAL,
roleDAL,
permissionService,
orgDAL
@@ -1705,7 +1709,8 @@ export const registerRoutes = async (
licenseService,
permissionService,
kmsService,
membershipIdentityDAL
membershipIdentityDAL,
orgDAL
});
const identityAwsAuthService = identityAwsAuthServiceFactory({

View File

@@ -89,6 +89,10 @@ import {
NetlifyConnectionListItemSchema,
SanitizedNetlifyConnectionSchema
} 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 {
PostgresConnectionListItemSchema,
@@ -161,6 +165,7 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedSupabaseConnectionSchema.options,
...SanitizedDigitalOceanConnectionSchema.options,
...SanitizedNetlifyConnectionSchema.options,
...SanitizedNorthflankConnectionSchema.options,
...SanitizedOktaConnectionSchema.options,
...SanitizedAzureADCSConnectionSchema.options,
...SanitizedRedisConnectionSchema.options,
@@ -205,6 +210,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
SupabaseConnectionListItemSchema,
DigitalOceanConnectionListItemSchema,
NetlifyConnectionListItemSchema,
NorthflankConnectionListItemSchema,
OktaConnectionListItemSchema,
AzureADCSConnectionListItemSchema,
RedisConnectionListItemSchema,

View File

@@ -30,6 +30,7 @@ import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerMySqlConnectionRouter } from "./mysql-connection-router";
import { registerNetlifyConnectionRouter } from "./netlify-connection-router";
import { registerNorthflankConnectionRouter } from "./northflank-connection-router";
import { registerOktaConnectionRouter } from "./okta-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-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.DigitalOcean]: registerDigitalOceanConnectionRouter,
[AppConnection.Netlify]: registerNetlifyConnectionRouter,
[AppConnection.Northflank]: registerNorthflankConnectionRouter,
[AppConnection.Okta]: registerOktaConnectionRouter,
[AppConnection.Redis]: registerRedisConnectionRouter,
[AppConnection.Chef]: registerChefConnectionRouter

View File

@@ -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 };
}
});
};

View File

@@ -24,6 +24,7 @@ import { registerHerokuSyncRouter } from "./heroku-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerLaravelForgeSyncRouter } from "./laravel-forge-sync-router";
import { registerNetlifySyncRouter } from "./netlify-sync-router";
import { registerNorthflankSyncRouter } from "./northflank-sync-router";
import { registerRailwaySyncRouter } from "./railway-sync-router";
import { registerRenderSyncRouter } from "./render-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.DigitalOceanAppPlatform]: registerDigitalOceanAppPlatformSyncRouter,
[SecretSync.Netlify]: registerNetlifySyncRouter,
[SecretSync.Northflank]: registerNorthflankSyncRouter,
[SecretSync.Bitbucket]: registerBitbucketSyncRouter,
[SecretSync.LaravelForge]: registerLaravelForgeSyncRouter,
[SecretSync.Chef]: registerChefSyncRouter

View File

@@ -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
});

View File

@@ -47,6 +47,7 @@ import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { LaravelForgeSyncListItemSchema, LaravelForgeSyncSchema } from "@app/services/secret-sync/laravel-forge";
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 { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas";
import { SupabaseSyncListItemSchema, SupabaseSyncSchema } from "@app/services/secret-sync/supabase";
@@ -86,6 +87,7 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
ChecklySyncSchema,
DigitalOceanAppPlatformSyncSchema,
NetlifySyncSchema,
NorthflankSyncSchema,
BitbucketSyncSchema,
LaravelForgeSyncSchema,
ChefSyncSchema
@@ -121,6 +123,7 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
ChecklySyncListItemSchema,
SupabaseSyncListItemSchema,
NetlifySyncListItemSchema,
NorthflankSyncListItemSchema,
BitbucketSyncListItemSchema,
LaravelForgeSyncListItemSchema,
ChefSyncListItemSchema

View File

@@ -7,6 +7,7 @@
// All the any rules are disabled because passport typesense with fastify is really poor
import { Authenticator } from "@fastify/passport";
import { requestContext } from "@fastify/request-context";
import fastifySession from "@fastify/session";
import RedisStore from "connect-redis";
import { CronJob } from "cron";
@@ -21,6 +22,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { ms } from "@app/lib/ms";
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 { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
import { AuthMethod } from "@app/services/auth/auth-type";
@@ -51,7 +53,6 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
},
// eslint-disable-next-line
async (req, _accessToken, _refreshToken, profile, cb) => {
try {
// @ts-expect-error this is because this is express type and not fastify
const callbackPort = req.session.get("callbackPort");
// @ts-expect-error this is because this is express type and not fastify
@@ -64,7 +65,9 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
name: "OauthGoogleRegister"
});
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
try {
const { isUserCompleted, providerAuthToken, user, orgId, orgName } =
await server.services.login.oauth2Login({
email,
firstName: profile?.name?.givenName || "",
lastName: profile?.name?.familyName || "",
@@ -72,9 +75,32 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
callbackPort,
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 });
} catch (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);
}
}
@@ -101,27 +127,50 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
},
// eslint-disable-next-line
async (req: any, accessToken: string, _refreshToken: string, _profile: any, done: Function) => {
try {
const ghEmails = await fetchGithubEmails(accessToken);
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
if (!email) throw new Error("No primary email found");
try {
// 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 { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
const { isUserCompleted, providerAuthToken, user, orgId, orgName } =
await server.services.login.oauth2Login({
email,
firstName: user.name || user.login,
firstName: githubUser.name || githubUser.login,
lastName: "",
authMethod: AuthMethod.GITHUB,
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 });
} 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);
done(err as Error, false);
}
@@ -147,11 +196,13 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
pkce: true
},
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
const email = profile.emails[0].value;
try {
const callbackPort = req.session.get("callbackPort");
const email = profile.emails[0].value;
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
const { isUserCompleted, providerAuthToken, user, orgId, orgName } =
await server.services.login.oauth2Login({
email,
firstName: profile.displayName || profile.username || "",
lastName: "",
@@ -159,8 +210,31 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
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 });
} 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);
cb(error as Error, false);
}

View File

@@ -39,7 +39,8 @@ export enum AppConnection {
Okta = "okta",
Redis = "redis",
LaravelForge = "laravel-forge",
Chef = "chef"
Chef = "chef",
Northflank = "northflank"
}
export enum AWSRegion {

View File

@@ -68,6 +68,7 @@ import {
} from "./bitbucket";
import { CamundaConnectionMethod, getCamundaConnectionListItem, validateCamundaConnectionCredentials } from "./camunda";
import { ChecklyConnectionMethod, getChecklyConnectionListItem, validateChecklyConnectionCredentials } from "./checkly";
import { ChefConnectionMethod, getChefConnectionListItem, validateChefConnectionCredentials } from "./chef";
import { CloudflareConnectionMethod } from "./cloudflare/cloudflare-connection-enum";
import {
getCloudflareConnectionListItem,
@@ -113,6 +114,11 @@ import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns";
import { getNetlifyConnectionListItem, validateNetlifyConnectionCredentials } from "./netlify";
import {
getNorthflankConnectionListItem,
NorthflankConnectionMethod,
validateNorthflankConnectionCredentials
} from "./northflank";
import { getOktaConnectionListItem, OktaConnectionMethod, validateOktaConnectionCredentials } from "./okta";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway";
@@ -142,7 +148,6 @@ import {
WindmillConnectionMethod
} from "./windmill";
import { getZabbixConnectionListItem, validateZabbixConnectionCredentials, ZabbixConnectionMethod } from "./zabbix";
import { ChefConnectionMethod, getChefConnectionListItem, validateChefConnectionCredentials } from "./chef";
const SECRET_SYNC_APP_CONNECTION_MAP = Object.fromEntries(
Object.entries(SECRET_SYNC_CONNECTION_MAP).map(([key, value]) => [value, key])
@@ -204,6 +209,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
getSupabaseConnectionListItem(),
getDigitalOceanConnectionListItem(),
getNetlifyConnectionListItem(),
getNorthflankConnectionListItem(),
getOktaConnectionListItem(),
getRedisConnectionListItem(),
getChefConnectionListItem()
@@ -334,10 +340,11 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Checkly]: validateChecklyConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Supabase]: validateSupabaseConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.DigitalOcean]: validateDigitalOceanConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator
[AppConnection.Northflank]: validateNorthflankConnectionCredentials 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);
@@ -379,6 +386,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case BitbucketConnectionMethod.ApiToken:
case ZabbixConnectionMethod.ApiToken:
case DigitalOceanConnectionMethod.ApiToken:
case NorthflankConnectionMethod.ApiToken:
case OktaConnectionMethod.ApiToken:
case LaravelForgeConnectionMethod.ApiToken:
return "API Token";
@@ -477,6 +485,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Supabase]: platformManagedCredentialsNotSupported,
[AppConnection.DigitalOcean]: platformManagedCredentialsNotSupported,
[AppConnection.Netlify]: platformManagedCredentialsNotSupported,
[AppConnection.Northflank]: platformManagedCredentialsNotSupported,
[AppConnection.Okta]: platformManagedCredentialsNotSupported,
[AppConnection.Redis]: platformManagedCredentialsNotSupported,
[AppConnection.LaravelForge]: platformManagedCredentialsNotSupported,

View File

@@ -41,7 +41,8 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Netlify]: "Netlify",
[AppConnection.Okta]: "Okta",
[AppConnection.Redis]: "Redis",
[AppConnection.Chef]: "Chef"
[AppConnection.Chef]: "Chef",
[AppConnection.Northflank]: "Northflank"
};
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.Okta]: AppConnectionPlanType.Regular,
[AppConnection.Redis]: AppConnectionPlanType.Regular,
[AppConnection.Chef]: AppConnectionPlanType.Regular
[AppConnection.Chef]: AppConnectionPlanType.Regular,
[AppConnection.Northflank]: AppConnectionPlanType.Regular
};

View File

@@ -98,6 +98,8 @@ import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
import { ValidateNetlifyConnectionCredentialsSchema } from "./netlify";
import { netlifyConnectionService } from "./netlify/netlify-connection-service";
import { ValidateNorthflankConnectionCredentialsSchema } from "./northflank";
import { northflankConnectionService } from "./northflank/northflank-connection-service";
import { ValidateOktaConnectionCredentialsSchema } from "./okta";
import { oktaConnectionService } from "./okta/okta-connection-service";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
@@ -172,6 +174,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Supabase]: ValidateSupabaseConnectionCredentialsSchema,
[AppConnection.DigitalOcean]: ValidateDigitalOceanConnectionCredentialsSchema,
[AppConnection.Netlify]: ValidateNetlifyConnectionCredentialsSchema,
[AppConnection.Northflank]: ValidateNorthflankConnectionCredentialsSchema,
[AppConnection.Okta]: ValidateOktaConnectionCredentialsSchema,
[AppConnection.Redis]: ValidateRedisConnectionCredentialsSchema,
[AppConnection.Chef]: ValidateChefConnectionCredentialsSchema
@@ -879,6 +882,7 @@ export const appConnectionServiceFactory = ({
supabase: supabaseConnectionService(connectAppConnectionById),
digitalOcean: digitalOceanAppPlatformConnectionService(connectAppConnectionById),
netlify: netlifyConnectionService(connectAppConnectionById),
northflank: northflankConnectionService(connectAppConnectionById),
okta: oktaConnectionService(connectAppConnectionById),
laravelForge: laravelForgeConnectionService(connectAppConnectionById),
chef: chefConnectionService(connectAppConnectionById)

View File

@@ -168,6 +168,12 @@ import {
TNetlifyConnectionInput,
TValidateNetlifyConnectionCredentialsSchema
} from "./netlify";
import {
TNorthflankConnection,
TNorthflankConnectionConfig,
TNorthflankConnectionInput,
TValidateNorthflankConnectionCredentialsSchema
} from "./northflank";
import {
TOktaConnection,
TOktaConnectionConfig,
@@ -279,6 +285,7 @@ export type TAppConnection = { id: string } & (
| TSupabaseConnection
| TDigitalOceanConnection
| TNetlifyConnection
| TNorthflankConnection
| TOktaConnection
| TRedisConnection
| TChefConnection
@@ -327,6 +334,7 @@ export type TAppConnectionInput = { id: string } & (
| TSupabaseConnectionInput
| TDigitalOceanConnectionInput
| TNetlifyConnectionInput
| TNorthflankConnectionInput
| TOktaConnectionInput
| TRedisConnectionInput
| TChefConnectionInput
@@ -393,6 +401,7 @@ export type TAppConnectionConfig =
| TSupabaseConnectionConfig
| TDigitalOceanConnectionConfig
| TNetlifyConnectionConfig
| TNorthflankConnectionConfig
| TOktaConnectionConfig
| TRedisConnectionConfig
| TChefConnectionConfig;
@@ -436,6 +445,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateSupabaseConnectionCredentialsSchema
| TValidateDigitalOceanCredentialsSchema
| TValidateNetlifyConnectionCredentialsSchema
| TValidateNorthflankConnectionCredentialsSchema
| TValidateOktaConnectionCredentialsSchema
| TValidateRedisConnectionCredentialsSchema
| TValidateChefConnectionCredentialsSchema;

View File

@@ -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";

View File

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

View File

@@ -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
});
}
};

View File

@@ -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()
});

View File

@@ -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
};
};

View File

@@ -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;
};

View File

@@ -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` });
let orgId = "";
let orgName = "";
let rootOrgId = "";
let parentOrgId = "";
if (token.organizationId) {
@@ -235,9 +236,11 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
throw new ForbiddenRequestError({ message: "User organization membership is inactive" });
}
orgId = subOrganization.id;
orgName = subOrganization.name;
rootOrgId = token.organizationId;
parentOrgId = subOrganization.parentOrgId as string;
} else {
const organization = await orgDAL.findOne({ id: token.organizationId });
const orgMembership = await membershipUserDAL.findOne({
actorUserId: user.id,
scopeOrgId: token.organizationId,
@@ -253,12 +256,13 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
}
orgId = token.organizationId;
orgName = organization.name;
rootOrgId = token.organizationId;
parentOrgId = token.organizationId;
}
}
return { user, tokenVersionId: token.tokenVersionId, orgId, rootOrgId, parentOrgId };
return { user, tokenVersionId: token.tokenVersionId, orgId, orgName, rootOrgId, parentOrgId };
};
return {

View File

@@ -16,6 +16,7 @@ import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { getMinExpiresIn, removeTrailingSlash } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { getUserAgentType } from "@app/server/plugins/audit-log";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -385,6 +386,9 @@ export const authLoginServiceFactory = ({
providerAuthToken?: string;
captchaToken?: string;
}) => {
const appCfg = getConfig();
try {
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email
});
@@ -394,7 +398,10 @@ export const authLoginServiceFactory = ({
if (!userEnc) throw new BadRequestError({ message: "User not found" });
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) {
@@ -435,6 +442,18 @@ export const authLoginServiceFactory = ({
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 {
tokens: {
accessToken: token.access,
@@ -442,6 +461,19 @@ export const authLoginServiceFactory = ({
},
user: userEnc
} 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 ({
@@ -965,7 +997,8 @@ export const authLoginServiceFactory = ({
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
}
);
return { isUserCompleted, providerAuthToken };
return { isUserCompleted, providerAuthToken, user, orgId, orgName };
};
/**

View File

@@ -210,6 +210,7 @@ export const identityAccessTokenServiceFactory = ({
});
}
let orgId = "";
let orgName = "";
let parentOrgId = "";
const identityOrgDetails = await orgDAL.findOne({ id: identityAccessToken.identityScopeOrgId });
const rootOrgId = identityOrgDetails.rootOrgId || identityOrgDetails.id;
@@ -229,8 +230,12 @@ export const identityAccessTokenServiceFactory = ({
throw new BadRequestError({ message: "Identity does not belong to any organization" });
}
orgId = subOrganization.id;
orgName = subOrganization.name;
parentOrgId = subOrganization.parentOrgId as string;
} else {
const organization = await orgDAL.findOne({ id: rootOrgId });
const identityOrgMembership = await membershipIdentityDAL.findOne({
scope: AccessScope.Organization,
actorIdentityId: identityAccessToken.identityId,
@@ -242,6 +247,7 @@ export const identityAccessTokenServiceFactory = ({
}
orgId = rootOrgId;
orgName = organization.name;
parentOrgId = rootOrgId;
}
@@ -253,7 +259,7 @@ export const identityAccessTokenServiceFactory = ({
await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses });
await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1);
return { ...identityAccessToken, orgId, rootOrgId, parentOrgId };
return { ...identityAccessToken, orgId, rootOrgId, parentOrgId, orgName };
};
return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken };

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AxiosError } from "axios";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
@@ -22,6 +23,7 @@ import {
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -65,6 +67,7 @@ export const identityAliCloudAuthServiceFactory = ({
orgDAL
}: TIdentityAliCloudAuthServiceFactoryDep) => {
const login = async ({ identityId, ...params }: TLoginAliCloudAuthDTO) => {
const appCfg = getConfig();
const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId });
if (!identityAliCloudAuth) {
throw new NotFoundError({
@@ -75,6 +78,9 @@ export const identityAliCloudAuthServiceFactory = ({
const identity = await identityDAL.findById(identityAliCloudAuth.identityId);
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");
for (const key of Object.keys(params)) {
@@ -121,7 +127,6 @@ export const identityAliCloudAuthServiceFactory = ({
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
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 {
identityAliCloudAuth,
accessToken,
identityAccessToken,
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 ({

View File

@@ -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 { requestContext } from "@fastify/request-context";
import axios from "axios";
import RE2 from "re2";
@@ -22,6 +23,7 @@ import {
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -98,6 +100,7 @@ export const identityAwsAuthServiceFactory = ({
orgDAL
}: TIdentityAwsAuthServiceFactoryDep) => {
const login = async ({ identityId, iamHttpRequestMethod, iamRequestBody, iamRequestHeaders }: TLoginAwsAuthDTO) => {
const appCfg = getConfig();
const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId });
if (!identityAwsAuth) {
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);
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 body: string = Buffer.from(iamRequestBody, "base64").toString();
@@ -196,7 +201,6 @@ export const identityAwsAuthServiceFactory = ({
return newToken;
});
const appCfg = getConfig();
const splitArn = extractPrincipalArnEntity(Arn);
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 };
} 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 ({

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -18,6 +19,7 @@ import {
UnauthorizedError
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -61,6 +63,7 @@ export const identityAzureAuthServiceFactory = ({
orgDAL
}: TIdentityAzureAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: azureJwt }: TLoginAzureAuthDTO) => {
const appCfg = getConfig();
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
if (!identityAzureAuth) {
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);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
try {
const azureIdentity = await validateAzureIdentity({
tenantId: identityAzureAuth.tenantId,
resource: identityAzureAuth.resource,
@@ -115,7 +121,6 @@ export const identityAzureAuthServiceFactory = ({
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
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 };
} 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 ({

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -18,6 +19,7 @@ import {
UnauthorizedError
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -59,6 +61,7 @@ export const identityGcpAuthServiceFactory = ({
orgDAL
}: TIdentityGcpAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: gcpJwt }: TLoginGcpAuthDTO) => {
const appCfg = getConfig();
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
if (!identityGcpAuth) {
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);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
try {
let gcpIdentityDetails: TGcpIdentityDetails;
switch (identityGcpAuth.type) {
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
const isProjectAllowed = identityGcpAuth.allowedProjects
@@ -151,8 +160,6 @@ export const identityGcpAuthServiceFactory = ({
);
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
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 };
} 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 ({

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import https from "https";
import jwt from "jsonwebtoken";
import { JwksClient } from "jwks-rsa";
@@ -21,6 +22,7 @@ import {
UnauthorizedError
} from "@app/lib/errors";
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 { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -67,6 +69,7 @@ export const identityJwtAuthServiceFactory = ({
orgDAL
}: TIdentityJwtAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => {
const appCfg = getConfig();
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
if (!identityJwtAuth) {
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);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
try {
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identity.orgId
@@ -228,7 +233,6 @@ export const identityJwtAuthServiceFactory = ({
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
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 };
} 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 ({

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import axios, { AxiosError } from "axios";
import https from "https";
import RE2 from "re2";
@@ -37,6 +38,7 @@ import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -182,6 +184,7 @@ export const identityKubernetesAuthServiceFactory = ({
};
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => {
const appCfg = getConfig();
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
if (!identityKubernetesAuth) {
throw new NotFoundError({
@@ -192,6 +195,9 @@ export const identityKubernetesAuthServiceFactory = ({
const identity = await identityDAL.findById(identityKubernetesAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
try {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identity.orgId
@@ -242,7 +248,9 @@ export const identityKubernetesAuthServiceFactory = ({
kind: "TokenReview",
spec: {
token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
...(identityKubernetesAuth.allowedAudience
? { audiences: [identityKubernetesAuth.allowedAudience] }
: {})
}
},
{
@@ -295,7 +303,9 @@ export const identityKubernetesAuthServiceFactory = ({
kind: "TokenReview",
spec: {
token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
...(identityKubernetesAuth.allowedAudience
? { audiences: [identityKubernetesAuth.allowedAudience] }
: {})
}
},
{
@@ -457,7 +467,6 @@ export const identityKubernetesAuthServiceFactory = ({
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
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 };
} 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 ({

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import slugify from "@sindresorhus/slugify";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
@@ -29,6 +30,7 @@ import {
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -151,6 +153,7 @@ export const identityLdapAuthServiceFactory = ({
};
const login = async ({ identityId }: TLoginLdapAuthDTO) => {
const appCfg = getConfig();
const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
if (!identityLdapAuth) {
@@ -162,6 +165,7 @@ export const identityLdapAuthServiceFactory = ({
const identity = await identityDAL.findById(identityLdapAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
const plan = await licenseService.getPlan(identity.orgId);
if (!plan.ldap) {
throw new BadRequestError({
@@ -170,6 +174,7 @@ export const identityLdapAuthServiceFactory = ({
});
}
try {
const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
@@ -191,7 +196,6 @@ export const identityLdapAuthServiceFactory = ({
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
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 };
} 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 ({

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AxiosError } from "axios";
import RE2 from "re2";
@@ -23,6 +24,7 @@ import {
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -63,6 +65,7 @@ export const identityOciAuthServiceFactory = ({
orgDAL
}: TIdentityOciAuthServiceFactoryDep) => {
const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => {
const appCfg = getConfig();
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
if (!identityOciAuth) {
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);
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.
if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) {
throw new BadRequestError({
@@ -124,7 +129,6 @@ export const identityOciAuthServiceFactory = ({
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
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 {
identityOciAuth,
accessToken,
identityAccessToken,
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 ({

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import axios from "axios";
import https from "https";
import jwt from "jsonwebtoken";
@@ -22,6 +23,7 @@ import {
UnauthorizedError
} from "@app/lib/errors";
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 { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -67,6 +69,7 @@ export const identityOidcAuthServiceFactory = ({
orgDAL
}: TIdentityOidcAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => {
const appCfg = getConfig();
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
if (!identityOidcAuth) {
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);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
try {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identity.orgId
@@ -198,7 +203,6 @@ export const identityOidcAuthServiceFactory = ({
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
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 };
} 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 ({

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -19,6 +20,7 @@ import {
UnauthorizedError
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -27,6 +29,7 @@ import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identit
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal";
import { TOrgDALFactory } from "../org/org-dal";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal";
import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types";
@@ -42,6 +45,7 @@ type TIdentityTlsCertAuthServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
orgDAL: Pick<TOrgDALFactory, "findById">;
};
const parseSubjectDetails = (data: string) => {
@@ -60,9 +64,11 @@ export const identityTlsCertAuthServiceFactory = ({
membershipIdentityDAL,
licenseService,
permissionService,
kmsService
kmsService,
orgDAL
}: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => {
const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ identityId, clientCertificate }) => {
const appCfg = getConfig();
const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId });
if (!identityTlsCertAuth) {
throw new NotFoundError({
@@ -73,6 +79,9 @@ export const identityTlsCertAuthServiceFactory = ({
const identity = await identityDAL.findById(identityTlsCertAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
try {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identity.orgId
@@ -140,7 +149,6 @@ export const identityTlsCertAuthServiceFactory = ({
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
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 {
identityTlsCertAuth,
accessToken,
identityAccessToken,
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 ({

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -21,6 +22,7 @@ import {
} from "@app/lib/errors";
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -77,6 +79,7 @@ export const identityUaServiceFactory = ({
identityDAL
}: TIdentityUaServiceFactoryDep) => {
const login = async (clientId: string, clientSecret: string, ip: string) => {
const appCfg = getConfig();
const identityUa = await identityUaDAL.findOne({ clientId });
if (!identityUa) {
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({
ipAddress: ip,
trustedIps: identityUa.clientSecretTrustedIps as TIp[]
@@ -221,7 +228,6 @@ export const identityUaServiceFactory = ({
accessTokenMaxTTL: 1000000000
};
const identity = await identityDAL.findById(identityUa.identityId);
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
await membershipIdentityDAL.update(
@@ -249,7 +255,6 @@ export const identityUaServiceFactory = ({
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
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 {
accessToken,
identityUa,
@@ -274,6 +292,21 @@ export const identityUaServiceFactory = ({
identity,
...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 ({

View File

@@ -401,13 +401,6 @@ export const kmsServiceFactory = ({
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 sanitizedName = name ? slugify(name) : slugify(alphaNumericNanoId(8).toLowerCase());
const dbQuery = async (db: Knex) => {

View File

@@ -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 { 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 { groupBy } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
@@ -23,6 +33,10 @@ import { newProjectMembershipGroupFactory } from "./project/project-membership-g
type TMembershipGroupServiceFactoryDep = {
membershipGroupDAL: TMembershipGroupDALFactory;
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">;
permissionService: TPermissionServiceFactory;
orgDAL: TOrgDALFactory;
@@ -33,6 +47,10 @@ export type TMembershipGroupServiceFactory = ReturnType<typeof membershipGroupSe
export const membershipGroupServiceFactory = ({
membershipGroupDAL,
roleDAL,
accessApprovalPolicyDAL,
accessApprovalPolicyApproverDAL,
secretApprovalPolicyDAL,
secretApprovalPolicyApproverDAL,
membershipRoleDAL,
orgDAL,
permissionService
@@ -268,6 +286,48 @@ export const membershipGroupServiceFactory = ({
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) => {
await membershipRoleDAL.delete({ membershipId: existingMembership.id }, tx);
const doc = await membershipGroupDAL.deleteById(existingMembership.id, tx);

View File

@@ -0,0 +1,4 @@
export * from "./northflank-sync-constants";
export * from "./northflank-sync-fns";
export * from "./northflank-sync-schemas";
export * from "./northflank-sync-types";

View File

@@ -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
};

View File

@@ -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);
}
};

View File

@@ -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)
});

View File

@@ -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;
};

View File

@@ -28,6 +28,7 @@ export enum SecretSync {
Checkly = "checkly",
DigitalOceanAppPlatform = "digital-ocean-app-platform",
Netlify = "netlify",
Northflank = "northflank",
Bitbucket = "bitbucket",
LaravelForge = "laravel-forge",
Chef = "chef"

View File

@@ -52,6 +52,7 @@ import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
import { LARAVEL_FORGE_SYNC_LIST_OPTION, LaravelForgeSyncFns } from "./laravel-forge";
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 { RailwaySyncFns } from "./railway/railway-sync-fns";
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.DigitalOceanAppPlatform]: DIGITAL_OCEAN_APP_PLATFORM_SYNC_LIST_OPTION,
[SecretSync.Netlify]: NETLIFY_SYNC_LIST_OPTION,
[SecretSync.Northflank]: NORTHFLANK_SYNC_LIST_OPTION,
[SecretSync.Bitbucket]: BITBUCKET_SYNC_LIST_OPTION,
[SecretSync.LaravelForge]: LARAVEL_FORGE_SYNC_LIST_OPTION,
[SecretSync.Chef]: CHEF_SYNC_LIST_OPTION
@@ -279,6 +281,8 @@ export const SecretSyncFns = {
return DigitalOceanAppPlatformSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Netlify:
return NetlifySyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Northflank:
return NorthflankSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Bitbucket:
return BitbucketSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.LaravelForge:
@@ -398,6 +402,9 @@ export const SecretSyncFns = {
case SecretSync.Netlify:
secretMap = await NetlifySyncFns.getSecrets(secretSync);
break;
case SecretSync.Northflank:
secretMap = await NorthflankSyncFns.getSecrets(secretSync);
break;
case SecretSync.Bitbucket:
secretMap = await BitbucketSyncFns.getSecrets(secretSync);
break;
@@ -498,6 +505,8 @@ export const SecretSyncFns = {
return DigitalOceanAppPlatformSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Netlify:
return NetlifySyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Northflank:
return NorthflankSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Bitbucket:
return BitbucketSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.LaravelForge:

View File

@@ -32,6 +32,7 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.Checkly]: "Checkly",
[SecretSync.DigitalOceanAppPlatform]: "Digital Ocean App Platform",
[SecretSync.Netlify]: "Netlify",
[SecretSync.Northflank]: "Northflank",
[SecretSync.Bitbucket]: "Bitbucket",
[SecretSync.LaravelForge]: "Laravel Forge",
[SecretSync.Chef]: "Chef"
@@ -67,6 +68,7 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.Checkly]: AppConnection.Checkly,
[SecretSync.DigitalOceanAppPlatform]: AppConnection.DigitalOcean,
[SecretSync.Netlify]: AppConnection.Netlify,
[SecretSync.Northflank]: AppConnection.Northflank,
[SecretSync.Bitbucket]: AppConnection.Bitbucket,
[SecretSync.LaravelForge]: AppConnection.LaravelForge,
[SecretSync.Chef]: AppConnection.Chef
@@ -102,6 +104,7 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.Checkly]: SecretSyncPlanType.Regular,
[SecretSync.DigitalOceanAppPlatform]: SecretSyncPlanType.Regular,
[SecretSync.Netlify]: SecretSyncPlanType.Regular,
[SecretSync.Northflank]: SecretSyncPlanType.Regular,
[SecretSync.Bitbucket]: SecretSyncPlanType.Regular,
[SecretSync.LaravelForge]: 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.DigitalOceanAppPlatform]: ["appName"],
[SecretSync.Netlify]: ["accountName", "siteName"],
[SecretSync.Northflank]: [],
[SecretSync.Bitbucket]: [],
[SecretSync.LaravelForge]: [],
[SecretSync.Chef]: []
@@ -207,6 +211,7 @@ export const DESTINATION_DUPLICATE_CHECK_MAP: Record<SecretSync, DestinationDupl
[SecretSync.Checkly]: defaultDuplicateCheck,
[SecretSync.DigitalOceanAppPlatform]: defaultDuplicateCheck,
[SecretSync.Netlify]: defaultDuplicateCheck,
[SecretSync.Northflank]: defaultDuplicateCheck,
[SecretSync.Bitbucket]: defaultDuplicateCheck,
[SecretSync.LaravelForge]: defaultDuplicateCheck,
[SecretSync.Chef]: defaultDuplicateCheck

View File

@@ -125,6 +125,12 @@ import {
TLaravelForgeSyncWithCredentials
} from "./laravel-forge";
import { TNetlifySync, TNetlifySyncInput, TNetlifySyncListItem, TNetlifySyncWithCredentials } from "./netlify";
import {
TNorthflankSync,
TNorthflankSyncInput,
TNorthflankSyncListItem,
TNorthflankSyncWithCredentials
} from "./northflank";
import {
TRailwaySync,
TRailwaySyncInput,
@@ -189,6 +195,7 @@ export type TSecretSync =
| TChecklySync
| TSupabaseSync
| TNetlifySync
| TNorthflankSync
| TBitbucketSync;
export type TSecretSyncWithCredentials =
@@ -222,6 +229,7 @@ export type TSecretSyncWithCredentials =
| TSupabaseSyncWithCredentials
| TDigitalOceanAppPlatformSyncWithCredentials
| TNetlifySyncWithCredentials
| TNorthflankSyncWithCredentials
| TBitbucketSyncWithCredentials
| TLaravelForgeSyncWithCredentials;
@@ -256,6 +264,7 @@ export type TSecretSyncInput =
| TSupabaseSyncInput
| TDigitalOceanAppPlatformSyncInput
| TNetlifySyncInput
| TNorthflankSyncInput
| TBitbucketSyncInput
| TLaravelForgeSyncInput;
@@ -291,6 +300,7 @@ export type TSecretSyncListItem =
| TSupabaseSyncListItem
| TDigitalOceanAppPlatformSyncListItem
| TNetlifySyncListItem
| TNorthflankSyncListItem
| TBitbucketSyncListItem;
export type TSyncOptionsConfig = {

View File

@@ -34,6 +34,7 @@ import { diff, groupBy } from "@app/lib/fn";
import { setKnexStringValue } from "@app/lib/knex";
import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { recordSecretReadMetric } from "@app/lib/telemetry/metrics";
import { ActorType } from "../auth/auth-type";
import { TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
@@ -1052,6 +1053,11 @@ export const secretV2BridgeServiceFactory = ({
});
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
recordSecretReadMetric({
environment,
secretPath: path
});
const cachedSecretDalVersion = await keyStore.pgGetIntItem(SecretServiceCacheKeys.getSecretDalVersion(projectId));
const secretDalVersion = Number(cachedSecretDalVersion || 0);
const cacheKey = SecretServiceCacheKeys.getSecretsOfServiceLayer(projectId, secretDalVersion, {
@@ -1482,6 +1488,12 @@ export const secretV2BridgeServiceFactory = ({
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
// because if its an expansion, it will fully depend on the value.
const { expandSecretReferences } = expandSecretReferencesFactory({

View File

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

View File

@@ -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>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/northflank/{connectionId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Create"
openapi: "POST /api/v1/secret-syncs/northflank"
---

View File

@@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/secret-syncs/northflank/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by ID"
openapi: "GET /api/v1/secret-syncs/northflank/{syncId}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Get by Name"
openapi: "GET /api/v1/secret-syncs/northflank/sync-name/{syncName}"
---

View File

@@ -0,0 +1,4 @@
---
title: "Import Secrets"
openapi: "POST /api/v1/secret-syncs/northflank/{syncId}/import-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/secret-syncs/northflank"
---

View File

@@ -0,0 +1,4 @@
---
title: "Remove Secrets"
openapi: "POST /api/v1/secret-syncs/northflank/{syncId}/remove-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Sync Secrets"
openapi: "POST /api/v1/secret-syncs/northflank/{syncId}/sync-secrets"
---

View File

@@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/secret-syncs/northflank/{syncId}"
---

View File

@@ -131,6 +131,7 @@
"integrations/app-connections/mssql",
"integrations/app-connections/mysql",
"integrations/app-connections/netlify",
"integrations/app-connections/northflank",
"integrations/app-connections/oci",
"integrations/app-connections/okta",
"integrations/app-connections/oracledb",
@@ -554,6 +555,7 @@
"integrations/secret-syncs/humanitec",
"integrations/secret-syncs/laravel-forge",
"integrations/secret-syncs/netlify",
"integrations/secret-syncs/northflank",
"integrations/secret-syncs/oci-vault",
"integrations/secret-syncs/railway",
"integrations/secret-syncs/render",
@@ -1862,6 +1864,18 @@
"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",
"pages": [
@@ -2335,6 +2349,20 @@
"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",
"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