diff --git a/.env.example b/.env.example index 53e36449a..05a888db0 100644 --- a/.env.example +++ b/.env.example @@ -23,7 +23,7 @@ REDIS_URL=redis://redis:6379 # Required SITE_URL=http://localhost:8080 -# Mail/SMTP +# Mail/SMTP SMTP_HOST= SMTP_PORT= SMTP_FROM_ADDRESS= @@ -132,3 +132,6 @@ DATADOG_PROFILING_ENABLED= DATADOG_ENV= DATADOG_SERVICE= DATADOG_HOSTNAME= + +# kubernetes +KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN=false diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 01113f019..e07c8a9da 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -34,6 +34,7 @@ ARG INFISICAL_PLATFORM_VERSION ENV VITE_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION ARG CAPTCHA_SITE_KEY ENV VITE_CAPTCHA_SITE_KEY $CAPTCHA_SITE_KEY +ENV NODE_OPTIONS="--max-old-space-size=8192" # Build RUN npm run build @@ -77,6 +78,7 @@ RUN npm ci --only-production COPY /backend . COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh RUN npm i -D tsconfig-paths +ENV NODE_OPTIONS="--max-old-space-size=8192" RUN npm run build # Production stage diff --git a/backend/src/db/migrations/20250710022434_add-index-for-access-token.ts b/backend/src/db/migrations/20250710022434_add-index-for-access-token.ts new file mode 100644 index 000000000..162535c28 --- /dev/null +++ b/backend/src/db/migrations/20250710022434_add-index-for-access-token.ts @@ -0,0 +1,46 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +const MIGRATION_TIMEOUT = 30 * 60 * 1000; // 30 minutes + +export async function up(knex: Knex): Promise { + const result = await knex.raw("SHOW statement_timeout"); + const originalTimeout = result.rows[0].statement_timeout; + + try { + await knex.raw(`SET statement_timeout = ${MIGRATION_TIMEOUT}`); + + // iat means IdentityAccessToken + await knex.raw(` + CREATE INDEX IF NOT EXISTS idx_iat_identity_id + ON ${TableName.IdentityAccessToken} ("identityId") + `); + + await knex.raw(` + CREATE INDEX IF NOT EXISTS idx_iat_ua_client_secret_id + ON ${TableName.IdentityAccessToken} ("identityUAClientSecretId") + `); + } finally { + await knex.raw(`SET statement_timeout = '${originalTimeout}'`); + } +} + +export async function down(knex: Knex): Promise { + const result = await knex.raw("SHOW statement_timeout"); + const originalTimeout = result.rows[0].statement_timeout; + + try { + await knex.raw(`SET statement_timeout = ${MIGRATION_TIMEOUT}`); + + await knex.raw(` + DROP INDEX IF EXISTS idx_iat_identity_id + `); + + await knex.raw(` + DROP INDEX IF EXISTS idx_iat_ua_client_secret_id + `); + } finally { + await knex.raw(`SET statement_timeout = '${originalTimeout}'`); + } +} diff --git a/backend/src/db/migrations/20250710115149_required-path-on-approval-policies.ts b/backend/src/db/migrations/20250710115149_required-path-on-approval-policies.ts new file mode 100644 index 000000000..b5f5abef7 --- /dev/null +++ b/backend/src/db/migrations/20250710115149_required-path-on-approval-policies.ts @@ -0,0 +1,55 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const existingSecretApprovalPolicies = await knex(TableName.SecretApprovalPolicy) + .whereNull("secretPath") + .orWhere("secretPath", ""); + + const existingAccessApprovalPolicies = await knex(TableName.AccessApprovalPolicy) + .whereNull("secretPath") + .orWhere("secretPath", ""); + + // update all the secret approval policies secretPath to be "/**" + if (existingSecretApprovalPolicies.length) { + await knex(TableName.SecretApprovalPolicy) + .whereIn( + "id", + existingSecretApprovalPolicies.map((el) => el.id) + ) + .update({ + secretPath: "/**" + }); + } + + // update all the access approval policies secretPath to be "/**" + if (existingAccessApprovalPolicies.length) { + await knex(TableName.AccessApprovalPolicy) + .whereIn( + "id", + existingAccessApprovalPolicies.map((el) => el.id) + ) + .update({ + secretPath: "/**" + }); + } + + await knex.schema.alterTable(TableName.SecretApprovalPolicy, (table) => { + table.string("secretPath").notNullable().alter(); + }); + + await knex.schema.alterTable(TableName.AccessApprovalPolicy, (table) => { + table.string("secretPath").notNullable().alter(); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(TableName.SecretApprovalPolicy, (table) => { + table.string("secretPath").nullable().alter(); + }); + + await knex.schema.alterTable(TableName.AccessApprovalPolicy, (table) => { + table.string("secretPath").nullable().alter(); + }); +} diff --git a/backend/src/db/schemas/access-approval-policies.ts b/backend/src/db/schemas/access-approval-policies.ts index 19a98675f..ea57c54d2 100644 --- a/backend/src/db/schemas/access-approval-policies.ts +++ b/backend/src/db/schemas/access-approval-policies.ts @@ -11,7 +11,7 @@ export const AccessApprovalPoliciesSchema = z.object({ id: z.string().uuid(), name: z.string(), approvals: z.number().default(1), - secretPath: z.string().nullable().optional(), + secretPath: z.string(), envId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), diff --git a/backend/src/db/schemas/secret-approval-policies.ts b/backend/src/db/schemas/secret-approval-policies.ts index 8b9174456..0273e617c 100644 --- a/backend/src/db/schemas/secret-approval-policies.ts +++ b/backend/src/db/schemas/secret-approval-policies.ts @@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models"; export const SecretApprovalPoliciesSchema = z.object({ id: z.string().uuid(), name: z.string(), - secretPath: z.string().nullable().optional(), + secretPath: z.string(), approvals: z.number().default(1), envId: z.string().uuid(), createdAt: z.date(), diff --git a/backend/src/ee/routes/v1/access-approval-policy-router.ts b/backend/src/ee/routes/v1/access-approval-policy-router.ts index 74545579c..177f5e1fd 100644 --- a/backend/src/ee/routes/v1/access-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts @@ -2,6 +2,7 @@ import { nanoid } from "nanoid"; import { z } from "zod"; import { ApproverType, BypasserType } from "@app/ee/services/access-approval-policy/access-approval-policy-types"; +import { removeTrailingSlash } from "@app/lib/fn"; import { EnforcementLevel } from "@app/lib/types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -19,7 +20,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi body: z.object({ projectSlug: z.string().trim(), name: z.string().optional(), - secretPath: z.string().trim().default("/"), + secretPath: z.string().trim().min(1, { message: "Secret path cannot be empty" }).transform(removeTrailingSlash), environment: z.string(), approvers: z .discriminatedUnion("type", [ @@ -174,8 +175,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi secretPath: z .string() .trim() + .min(1, { message: "Secret path cannot be empty" }) .optional() - .transform((val) => (val === "" ? "/" : val)), + .transform((val) => (val ? removeTrailingSlash(val) : val)), approvers: z .discriminatedUnion("type", [ z.object({ diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts index ebe1345b3..46b2544b2 100644 --- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts @@ -23,10 +23,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi environment: z.string(), secretPath: z .string() - .optional() - .nullable() - .default("/") - .transform((val) => (val ? removeTrailingSlash(val) : val)), + .min(1, { message: "Secret path cannot be empty" }) + .transform((val) => removeTrailingSlash(val)), approvers: z .discriminatedUnion("type", [ z.object({ type: z.literal(ApproverType.Group), id: z.string() }), @@ -100,10 +98,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi approvals: z.number().min(1).default(1), secretPath: z .string() + .trim() + .min(1, { message: "Secret path cannot be empty" }) .optional() - .nullable() - .transform((val) => (val ? removeTrailingSlash(val) : val)) - .transform((val) => (val === "" ? "/" : val)), + .transform((val) => (val ? removeTrailingSlash(val) : undefined)), enforcementLevel: z.nativeEnum(EnforcementLevel).optional(), allowedSelfApprovals: z.boolean().default(true) }), diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts index 9fa48ca15..995534f8f 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts @@ -53,7 +53,7 @@ export interface TAccessApprovalPolicyDALFactory envId: string; enforcementLevel: string; allowedSelfApprovals: boolean; - secretPath?: string | null | undefined; + secretPath: string; deletedAt?: Date | null | undefined; environment: { id: string; @@ -93,7 +93,7 @@ export interface TAccessApprovalPolicyDALFactory envId: string; enforcementLevel: string; allowedSelfApprovals: boolean; - secretPath?: string | null | undefined; + secretPath: string; deletedAt?: Date | null | undefined; environment: { id: string; @@ -116,7 +116,7 @@ export interface TAccessApprovalPolicyDALFactory envId: string; enforcementLevel: string; allowedSelfApprovals: boolean; - secretPath?: string | null | undefined; + secretPath: string; deletedAt?: Date | null | undefined; }>; findLastValidPolicy: ( @@ -138,7 +138,7 @@ export interface TAccessApprovalPolicyDALFactory envId: string; enforcementLevel: string; allowedSelfApprovals: boolean; - secretPath?: string | null | undefined; + secretPath: string; deletedAt?: Date | null | undefined; } | undefined @@ -190,7 +190,7 @@ export interface TAccessApprovalPolicyServiceFactory { envId: string; enforcementLevel: string; allowedSelfApprovals: boolean; - secretPath?: string | null | undefined; + secretPath: string; deletedAt?: Date | null | undefined; }>; deleteAccessApprovalPolicy: ({ @@ -214,7 +214,7 @@ export interface TAccessApprovalPolicyServiceFactory { envId: string; enforcementLevel: string; allowedSelfApprovals: boolean; - secretPath?: string | null | undefined; + secretPath: string; deletedAt?: Date | null | undefined; environment: { id: string; @@ -252,7 +252,7 @@ export interface TAccessApprovalPolicyServiceFactory { envId: string; enforcementLevel: string; allowedSelfApprovals: boolean; - secretPath?: string | null | undefined; + secretPath: string; deletedAt?: Date | null | undefined; }>; getAccessApprovalPolicyByProjectSlug: ({ @@ -286,7 +286,7 @@ export interface TAccessApprovalPolicyServiceFactory { envId: string; enforcementLevel: string; allowedSelfApprovals: boolean; - secretPath?: string | null | undefined; + secretPath: string; deletedAt?: Date | null | undefined; environment: { id: string; @@ -337,7 +337,7 @@ export interface TAccessApprovalPolicyServiceFactory { envId: string; enforcementLevel: string; allowedSelfApprovals: boolean; - secretPath?: string | null | undefined; + secretPath: string; deletedAt?: Date | null | undefined; environment: { id: string; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index 5976f5fff..fa487d0b7 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -60,6 +60,26 @@ export const accessApprovalPolicyServiceFactory = ({ accessApprovalRequestReviewerDAL, orgMembershipDAL }: TAccessApprovalPolicyServiceFactoryDep): TAccessApprovalPolicyServiceFactory => { + const $policyExists = async ({ + envId, + secretPath, + policyId + }: { + envId: string; + secretPath: string; + policyId?: string; + }) => { + const policy = await accessApprovalPolicyDAL + .findOne({ + envId, + secretPath, + deletedAt: null + }) + .catch(() => null); + + return policyId ? policy && policy.id !== policyId : Boolean(policy); + }; + const createAccessApprovalPolicy: TAccessApprovalPolicyServiceFactory["createAccessApprovalPolicy"] = async ({ name, actor, @@ -106,6 +126,12 @@ export const accessApprovalPolicyServiceFactory = ({ const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id }); if (!env) throw new NotFoundError({ message: `Environment with slug '${environment}' not found` }); + if (await $policyExists({ envId: env.id, secretPath })) { + throw new BadRequestError({ + message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'` + }); + } + let approverUserIds = userApprovers; if (userApproverNames.length) { const approverUsersInDB = await userDAL.find({ @@ -279,7 +305,11 @@ export const accessApprovalPolicyServiceFactory = ({ ) as { username: string; sequence?: number }[]; const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId); - if (!accessApprovalPolicy) throw new BadRequestError({ message: "Approval policy not found" }); + if (!accessApprovalPolicy) { + throw new NotFoundError({ + message: `Access approval policy with ID '${policyId}' not found` + }); + } const currentApprovals = approvals || accessApprovalPolicy.approvals; if ( @@ -290,9 +320,18 @@ export const accessApprovalPolicyServiceFactory = ({ throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); } - if (!accessApprovalPolicy) { - throw new NotFoundError({ message: `Secret approval policy with ID '${policyId}' not found` }); + if ( + await $policyExists({ + envId: accessApprovalPolicy.envId, + secretPath: secretPath || accessApprovalPolicy.secretPath, + policyId: accessApprovalPolicy.id + }) + ) { + throw new BadRequestError({ + message: `A policy for secret path '${secretPath}' already exists in environment '${accessApprovalPolicy.environment.slug}'` + }); } + const { permission } = await permissionService.getProjectPermission({ actor, actorId, diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts index 6806c7123..f3f195914 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts @@ -122,7 +122,7 @@ export interface TAccessApprovalPolicyServiceFactory { envId: string; enforcementLevel: string; allowedSelfApprovals: boolean; - secretPath?: string | null | undefined; + secretPath: string; deletedAt?: Date | null | undefined; }>; deleteAccessApprovalPolicy: ({ @@ -146,7 +146,7 @@ export interface TAccessApprovalPolicyServiceFactory { envId: string; enforcementLevel: string; allowedSelfApprovals: boolean; - secretPath?: string | null | undefined; + secretPath: string; deletedAt?: Date | null | undefined; environment: { id: string; @@ -218,7 +218,7 @@ export interface TAccessApprovalPolicyServiceFactory { envId: string; enforcementLevel: string; allowedSelfApprovals: boolean; - secretPath?: string | null | undefined; + secretPath: string; deletedAt?: Date | null | undefined; environment: { id: string; @@ -269,7 +269,7 @@ export interface TAccessApprovalPolicyServiceFactory { envId: string; enforcementLevel: string; allowedSelfApprovals: boolean; - secretPath?: string | null | undefined; + secretPath: string; deletedAt?: Date | null | undefined; environment: { id: string; diff --git a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts index ec75bb2e4..329715941 100644 --- a/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts +++ b/backend/src/ee/services/dynamic-secret/providers/aws-iam.ts @@ -21,7 +21,7 @@ import { randomUUID } from "crypto"; import { z } from "zod"; import { getConfig } from "@app/lib/config/env"; -import { BadRequestError } from "@app/lib/errors"; +import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models"; @@ -81,6 +81,21 @@ export const AwsIamProvider = (): TDynamicProviderFns => { return client; } + if (providerInputs.method === AwsIamAuthType.IRSA) { + // Allow instances to disable automatic service account token fetching (e.g. for shared cloud) + if (!appCfg.KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN) { + throw new UnauthorizedError({ + message: "Failed to get AWS credentials via IRSA: KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN is not enabled." + }); + } + + // The SDK will automatically pick up credentials from the environment + const client = new IAMClient({ + region: providerInputs.region + }); + return client; + } + const client = new IAMClient({ region: providerInputs.region, credentials: { @@ -101,7 +116,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => { .catch((err) => { const message = (err as Error)?.message; if ( - providerInputs.method === AwsIamAuthType.AssumeRole && + (providerInputs.method === AwsIamAuthType.AssumeRole || providerInputs.method === AwsIamAuthType.IRSA) && // assume role will throw an error asking to provider username, but if so this has access in aws correctly message.includes("Must specify userName when calling with non-User credentials") ) { diff --git a/backend/src/ee/services/dynamic-secret/providers/models.ts b/backend/src/ee/services/dynamic-secret/providers/models.ts index f6fa2a4a8..528ea414a 100644 --- a/backend/src/ee/services/dynamic-secret/providers/models.ts +++ b/backend/src/ee/services/dynamic-secret/providers/models.ts @@ -28,7 +28,8 @@ export enum SqlProviders { export enum AwsIamAuthType { AssumeRole = "assume-role", - AccessKey = "access-key" + AccessKey = "access-key", + IRSA = "irsa" } export enum ElasticSearchAuthTypes { @@ -221,6 +222,16 @@ export const DynamicSecretAwsIamSchema = z.preprocess( userGroups: z.string().trim().optional(), policyArns: z.string().trim().optional(), tags: ResourceMetadataSchema.optional() + }), + z.object({ + method: z.literal(AwsIamAuthType.IRSA), + region: z.string().trim().min(1), + awsPath: z.string().trim().optional(), + permissionBoundaryPolicyArn: z.string().trim().optional(), + policyDocument: z.string().trim().optional(), + userGroups: z.string().trim().optional(), + policyArns: z.string().trim().optional(), + tags: ResourceMetadataSchema.optional() }) ]) ); diff --git a/backend/src/ee/services/ldap-config/ldap-config-service.ts b/backend/src/ee/services/ldap-config/ldap-config-service.ts index 426ed132e..f85d88cd3 100644 --- a/backend/src/ee/services/ldap-config/ldap-config-service.ts +++ b/backend/src/ee/services/ldap-config/ldap-config-service.ts @@ -361,13 +361,6 @@ export const ldapConfigServiceFactory = ({ }); } else { const plan = await licenseService.getPlan(orgId); - if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { - // limit imposed on number of members allowed / number of members used exceeds the number of members allowed - throw new BadRequestError({ - message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members." - }); - } - if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed throw new BadRequestError({ diff --git a/backend/src/ee/services/license/licence-enums.ts b/backend/src/ee/services/license/licence-enums.ts index 8812621f2..340fc764f 100644 --- a/backend/src/ee/services/license/licence-enums.ts +++ b/backend/src/ee/services/license/licence-enums.ts @@ -1,5 +1,4 @@ export const BillingPlanRows = { - MemberLimit: { name: "Organization member limit", field: "memberLimit" }, IdentityLimit: { name: "Organization identity limit", field: "identityLimit" }, WorkspaceLimit: { name: "Project limit", field: "workspaceLimit" }, EnvironmentLimit: { name: "Environment limit", field: "environmentLimit" }, diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index b841c5bea..7e784d9ad 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -442,9 +442,7 @@ export const licenseServiceFactory = ({ rows: data.rows.map((el) => { let used = "-"; - if (el.name === BillingPlanRows.MemberLimit.name) { - used = orgMembersUsed.toString(); - } else if (el.name === BillingPlanRows.WorkspaceLimit.name) { + if (el.name === BillingPlanRows.WorkspaceLimit.name) { used = projectCount.toString(); } else if (el.name === BillingPlanRows.IdentityLimit.name) { used = (identityUsed + orgMembersUsed).toString(); @@ -464,12 +462,10 @@ export const licenseServiceFactory = ({ const allowed = onPremFeatures[field as keyof TFeatureSet]; let used = "-"; - if (field === BillingPlanRows.MemberLimit.field) { - used = orgMembersUsed.toString(); - } else if (field === BillingPlanRows.WorkspaceLimit.field) { + if (field === BillingPlanRows.WorkspaceLimit.field) { used = projectCount.toString(); } else if (field === BillingPlanRows.IdentityLimit.field) { - used = identityUsed.toString(); + used = (identityUsed + orgMembersUsed).toString(); } return { diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index 5430b0afc..4ab5c29e3 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -311,13 +311,6 @@ export const samlConfigServiceFactory = ({ }); } else { const plan = await licenseService.getPlan(orgId); - if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { - // limit imposed on number of members allowed / number of members used exceeds the number of members allowed - throw new BadRequestError({ - message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members." - }); - } - if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed throw new BadRequestError({ diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index cb497aa7d..80127c071 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -55,6 +55,26 @@ export const secretApprovalPolicyServiceFactory = ({ licenseService, secretApprovalRequestDAL }: TSecretApprovalPolicyServiceFactoryDep) => { + const $policyExists = async ({ + envId, + secretPath, + policyId + }: { + envId: string; + secretPath: string; + policyId?: string; + }) => { + const policy = await secretApprovalPolicyDAL + .findOne({ + envId, + secretPath, + deletedAt: null + }) + .catch(() => null); + + return policyId ? policy && policy.id !== policyId : Boolean(policy); + }; + const createSecretApprovalPolicy = async ({ name, actor, @@ -106,10 +126,17 @@ export const secretApprovalPolicyServiceFactory = ({ } const env = await projectEnvDAL.findOne({ slug: environment, projectId }); - if (!env) + if (!env) { throw new NotFoundError({ message: `Environment with slug '${environment}' not found in project with ID ${projectId}` }); + } + + if (await $policyExists({ envId: env.id, secretPath })) { + throw new BadRequestError({ + message: `A policy for secret path '${secretPath}' already exists in environment '${environment}'` + }); + } let groupBypassers: string[] = []; let bypasserUserIds: string[] = []; @@ -260,6 +287,18 @@ export const secretApprovalPolicyServiceFactory = ({ }); } + if ( + await $policyExists({ + envId: secretApprovalPolicy.envId, + secretPath: secretPath || secretApprovalPolicy.secretPath, + policyId: secretApprovalPolicy.id + }) + ) { + throw new BadRequestError({ + message: `A policy for secret path '${secretPath}' already exists in environment '${secretApprovalPolicy.environment.slug}'` + }); + } + const { permission } = await permissionService.getProjectPermission({ actor, actorId, diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts index ed074336c..ba5334e5c 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts @@ -4,7 +4,7 @@ import { ApproverType, BypasserType } from "../access-approval-policy/access-app export type TCreateSapDTO = { approvals: number; - secretPath?: string | null; + secretPath: string; environment: string; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[]; bypassers?: ( @@ -20,7 +20,7 @@ export type TCreateSapDTO = { export type TUpdateSapDTO = { secretPolicyId: string; approvals?: number; - secretPath?: string | null; + secretPath?: string; approvers: ({ type: ApproverType.Group; id: string } | { type: ApproverType.User; id?: string; username?: string })[]; bypassers?: ( | { type: BypasserType.Group; id: string } diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts index 417417c74..137034feb 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-queue.ts @@ -37,7 +37,8 @@ import { TQueueSecretScanningDataSourceFullScan, TQueueSecretScanningResourceDiffScan, TQueueSecretScanningSendNotification, - TSecretScanningDataSourceWithConnection + TSecretScanningDataSourceWithConnection, + TSecretScanningFinding } from "./secret-scanning-v2-types"; type TSecretRotationV2QueueServiceFactoryDep = { @@ -459,13 +460,16 @@ export const secretScanningV2QueueServiceFactory = async ({ const newFindings = allFindings.filter((finding) => finding.scanId === scanId); if (newFindings.length) { + const finding = newFindings[0] as TSecretScanningFinding; await queueService.queuePg(QueueJobs.SecretScanningV2SendNotification, { status: SecretScanningScanStatus.Completed, resourceName: resource.name, isDiffScan: true, dataSource, numberOfSecrets: newFindings.length, - scanId + scanId, + authorName: finding?.details?.author, + authorEmail: finding?.details?.email }); } @@ -582,8 +586,8 @@ export const secretScanningV2QueueServiceFactory = async ({ substitutions: payload.status === SecretScanningScanStatus.Completed ? { - authorName: "Jim", - authorEmail: "jim@infisical.com", + authorName: payload.authorName, + authorEmail: payload.authorEmail, resourceName, numberOfSecrets: payload.numberOfSecrets, isDiffScan: payload.isDiffScan, diff --git a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts index 35486bd59..6bd8251b6 100644 --- a/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts +++ b/backend/src/ee/services/secret-scanning-v2/secret-scanning-v2-types.ts @@ -119,7 +119,14 @@ export type TQueueSecretScanningSendNotification = { resourceName: string; } & ( | { status: SecretScanningScanStatus.Failed; errorMessage: string } - | { status: SecretScanningScanStatus.Completed; numberOfSecrets: number; scanId: string; isDiffScan: boolean } + | { + status: SecretScanningScanStatus.Completed; + numberOfSecrets: number; + scanId: string; + isDiffScan: boolean; + authorName?: string; + authorEmail?: string; + } ); export type TCloneRepository = { diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index e604899b6..59734583a 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2279,6 +2279,9 @@ export const AppConnections = { ZABBIX: { apiToken: "The API Token used to access Zabbix.", instanceUrl: "The Zabbix instance URL to connect with." + }, + RAILWAY: { + apiToken: "The API token used to authenticate with Railway." } } }; @@ -2477,6 +2480,14 @@ export const SecretSyncs = { hostId: "The ID of the Zabbix host to sync secrets to.", hostName: "The name of the Zabbix host to sync secrets to.", macroType: "The type of macro to sync secrets to. (0: Text, 1: Secret)" + }, + RAILWAY: { + projectId: "The ID of the Railway project to sync secrets to.", + projectName: "The name of the Railway project to sync secrets to.", + environmentId: "The Railway environment to sync secrets to.", + environmentName: "The Railway environment to sync secrets to.", + serviceId: "The Railway service that secrets should be synced to.", + serviceName: "The Railway service that secrets should be synced to." } } }; @@ -2597,7 +2608,9 @@ export const SecretRotations = { export const SecretScanningDataSources = { LIST: (type?: SecretScanningDataSource) => ({ - projectId: `The ID of the project to list ${type ? SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type] : "Scanning"} Data Sources from.` + projectId: `The ID of the project to list ${ + type ? SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type] : "Scanning" + } Data Sources from.` }), GET_BY_ID: (type: SecretScanningDataSource) => ({ dataSourceId: `The ID of the ${SECRET_SCANNING_DATA_SOURCE_NAME_MAP[type]} Data Source to retrieve.` diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 2523c763c..38b34f488 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -28,6 +28,7 @@ const databaseReadReplicaSchema = z const envSchema = z .object({ INFISICAL_PLATFORM_VERSION: zpStr(z.string().optional()), + KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN: zodStrBool.default("false"), PORT: z.coerce.number().default(IS_PACKAGED ? 8080 : 4000), DISABLE_SECRET_SCANNING: z .enum(["true", "false"]) @@ -373,6 +374,19 @@ export const overwriteSchema: { fields: { key: keyof TEnvConfig; description?: string }[]; }; } = { + aws: { + name: "AWS", + fields: [ + { + key: "INF_APP_CONNECTION_AWS_ACCESS_KEY_ID", + description: "The Access Key ID of your AWS account." + }, + { + key: "INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY", + description: "The Client Secret of your AWS application." + } + ] + }, azure: { name: "Azure", fields: [ @@ -386,16 +400,79 @@ export const overwriteSchema: { } ] }, - google_sso: { - name: "Google SSO", + gcp: { + name: "GCP", fields: [ { - key: "CLIENT_ID_GOOGLE_LOGIN", - description: "The Client ID of your GCP OAuth2 application." + key: "INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL", + description: "The GCP Service Account JSON credentials." + } + ] + }, + github_app: { + name: "GitHub App", + fields: [ + { + key: "INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID", + description: "The Client ID of your GitHub application." }, { - key: "CLIENT_SECRET_GOOGLE_LOGIN", - description: "The Client Secret of your GCP OAuth2 application." + key: "INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET", + description: "The Client Secret of your GitHub application." + }, + { + key: "INF_APP_CONNECTION_GITHUB_APP_SLUG", + description: "The Slug of your GitHub application. This is the one found in the URL." + }, + { + key: "INF_APP_CONNECTION_GITHUB_APP_ID", + description: "The App ID of your GitHub application." + }, + { + key: "INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY", + description: "The Private Key of your GitHub application." + } + ] + }, + github_oauth: { + name: "GitHub OAuth", + fields: [ + { + key: "INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID", + description: "The Client ID of your GitHub OAuth application." + }, + { + key: "INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET", + description: "The Client Secret of your GitHub OAuth application." + } + ] + }, + github_radar_app: { + name: "GitHub Radar App", + fields: [ + { + key: "INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID", + description: "The Client ID of your GitHub application." + }, + { + key: "INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET", + description: "The Client Secret of your GitHub application." + }, + { + key: "INF_APP_CONNECTION_GITHUB_RADAR_APP_SLUG", + description: "The Slug of your GitHub application. This is the one found in the URL." + }, + { + key: "INF_APP_CONNECTION_GITHUB_RADAR_APP_ID", + description: "The App ID of your GitHub application." + }, + { + key: "INF_APP_CONNECTION_GITHUB_RADAR_APP_PRIVATE_KEY", + description: "The Private Key of your GitHub application." + }, + { + key: "INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET", + description: "The Webhook Secret of your GitHub application." } ] }, @@ -412,6 +489,19 @@ export const overwriteSchema: { } ] }, + gitlab_oauth: { + name: "GitLab OAuth", + fields: [ + { + key: "INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_ID", + description: "The Client ID of your GitLab OAuth application." + }, + { + key: "INF_APP_CONNECTION_GITLAB_OAUTH_CLIENT_SECRET", + description: "The Client Secret of your GitLab OAuth application." + } + ] + }, gitlab_sso: { name: "GitLab SSO", fields: [ @@ -429,6 +519,19 @@ export const overwriteSchema: { "The URL of your self-hosted instance of GitLab where the OAuth application is registered. If no URL is passed in, this will default to https://gitlab.com." } ] + }, + google_sso: { + name: "Google SSO", + fields: [ + { + key: "CLIENT_ID_GOOGLE_LOGIN", + description: "The Client ID of your GCP OAuth2 application." + }, + { + key: "CLIENT_SECRET_GOOGLE_LOGIN", + description: "The Client Secret of your GCP OAuth2 application." + } + ] } }; diff --git a/backend/src/lib/config/request.ts b/backend/src/lib/config/request.ts index 8636b7476..aedf5cd44 100644 --- a/backend/src/lib/config/request.ts +++ b/backend/src/lib/config/request.ts @@ -1,11 +1,18 @@ -import axios from "axios"; -import axiosRetry from "axios-retry"; +import axios, { AxiosInstance, CreateAxiosDefaults } from "axios"; +import axiosRetry, { IAxiosRetryConfig } from "axios-retry"; -export const request = axios.create(); +export function createRequestClient(defaults: CreateAxiosDefaults = {}, retry: IAxiosRetryConfig = {}): AxiosInstance { + const client = axios.create(defaults); -axiosRetry(request, { - retries: 3, - // eslint-disable-next-line - retryDelay: axiosRetry.exponentialDelay, - retryCondition: (err) => axiosRetry.isNetworkError(err) || axiosRetry.isRetryableError(err) -}); + axiosRetry(client, { + retries: 3, + // eslint-disable-next-line + retryDelay: axiosRetry.exponentialDelay, + retryCondition: (err) => axiosRetry.isNetworkError(err) || axiosRetry.isRetryableError(err), + ...retry + }); + + return client; +} + +export const request = createRequestClient(); diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 81e911621..c3b204c48 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -49,7 +49,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { defaultAuthOrgSlug: z.string().nullable(), defaultAuthOrgAuthEnforced: z.boolean().nullish(), defaultAuthOrgAuthMethod: z.string().nullish(), - isSecretScanningDisabled: z.boolean() + isSecretScanningDisabled: z.boolean(), + kubernetesAutoFetchServiceAccountToken: z.boolean() }) }) } @@ -61,7 +62,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { config: { ...config, isMigrationModeOn: serverEnvs.MAINTENANCE_MODE, - isSecretScanningDisabled: serverEnvs.DISABLE_SECRET_SCANNING + isSecretScanningDisabled: serverEnvs.DISABLE_SECRET_SCANNING, + kubernetesAutoFetchServiceAccountToken: serverEnvs.KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN } }; } diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 35ec330e8..f692e700f 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -71,6 +71,10 @@ import { PostgresConnectionListItemSchema, SanitizedPostgresConnectionSchema } from "@app/services/app-connection/postgres"; +import { + RailwayConnectionListItemSchema, + SanitizedRailwayConnectionSchema +} from "@app/services/app-connection/railway"; import { RenderConnectionListItemSchema, SanitizedRenderConnectionSchema @@ -123,7 +127,8 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedGitLabConnectionSchema.options, ...SanitizedCloudflareConnectionSchema.options, ...SanitizedBitbucketConnectionSchema.options, - ...SanitizedZabbixConnectionSchema.options + ...SanitizedZabbixConnectionSchema.options, + ...SanitizedRailwayConnectionSchema.options ]); const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ @@ -157,7 +162,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ GitLabConnectionListItemSchema, CloudflareConnectionListItemSchema, BitbucketConnectionListItemSchema, - ZabbixConnectionListItemSchema + ZabbixConnectionListItemSchema, + RailwayConnectionListItemSchema ]); export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 56005beac..524abc18d 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -25,6 +25,7 @@ import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMySqlConnectionRouter } from "./mysql-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router"; +import { registerRailwayConnectionRouter } from "./railway-connection-router"; import { registerRenderConnectionRouter } from "./render-connection-router"; import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; @@ -66,5 +67,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + registerAppConnectionEndpoints({ + app: AppConnection.Railway, + server, + sanitizedResponseSchema: SanitizedRailwayConnectionSchema, + createSchema: CreateRailwayConnectionSchema, + updateSchema: UpdateRailwayConnectionSchema + }); + + // 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(), + services: z.array( + z.object({ + name: z.string(), + id: z.string() + }) + ), + environments: z.array( + 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.railway.listProjects(connectionId, req.permission); + + return { projects }; + } + }); +}; diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index 82e250fa1..4e1d185a8 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -18,6 +18,7 @@ import { registerGitLabSyncRouter } from "./gitlab-sync-router"; import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; import { registerHerokuSyncRouter } from "./heroku-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; +import { registerRailwaySyncRouter } from "./railway-sync-router"; import { registerRenderSyncRouter } from "./render-sync-router"; import { registerTeamCitySyncRouter } from "./teamcity-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; @@ -51,5 +52,7 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record + registerSyncSecretsEndpoints({ + destination: SecretSync.Railway, + server, + responseSchema: RailwaySyncSchema, + createSchema: CreateRailwaySyncSchema, + updateSchema: UpdateRailwaySyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts index fd3b94cb6..ec9eaebc7 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/secret-sync-router.ts @@ -38,6 +38,7 @@ import { GitLabSyncListItemSchema, GitLabSyncSchema } from "@app/services/secret import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault"; import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret-sync/heroku"; import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; +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 { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity"; import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud"; @@ -69,7 +70,9 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [ GitLabSyncSchema, CloudflarePagesSyncSchema, CloudflareWorkersSyncSchema, - ZabbixSyncSchema + + ZabbixSyncSchema, + RailwaySyncSchema ]); const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ @@ -96,7 +99,9 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ GitLabSyncListItemSchema, CloudflarePagesSyncListItemSchema, CloudflareWorkersSyncListItemSchema, - ZabbixSyncListItemSchema + + ZabbixSyncListItemSchema, + RailwaySyncListItemSchema ]); export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index a0f4c7aac..b9c405654 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -28,8 +28,9 @@ export enum AppConnection { Flyio = "flyio", GitLab = "gitlab", Cloudflare = "cloudflare", - Bitbucket = "bitbucket", - Zabbix = "zabbix" + Zabbix = "zabbix", + Railway = "railway", + Bitbucket = "bitbucket" } export enum AWSRegion { diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index c5c2b325f..df40a9eea 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -91,6 +91,7 @@ import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums"; import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; +import { getRailwayConnectionListItem, validateRailwayConnectionCredentials } from "./railway"; import { RenderConnectionMethod } from "./render/render-connection-enums"; import { getRenderConnectionListItem, validateRenderConnectionCredentials } from "./render/render-connection-fns"; import { @@ -143,8 +144,9 @@ export const listAppConnectionOptions = () => { getFlyioConnectionListItem(), getGitLabConnectionListItem(), getCloudflareConnectionListItem(), - getBitbucketConnectionListItem(), - getZabbixConnectionListItem() + getZabbixConnectionListItem(), + getRailwayConnectionListItem(), + getBitbucketConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); }; @@ -225,8 +227,9 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); @@ -345,8 +348,9 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Flyio]: platformManagedCredentialsNotSupported, [AppConnection.GitLab]: platformManagedCredentialsNotSupported, [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported, - [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported, - [AppConnection.Zabbix]: platformManagedCredentialsNotSupported + [AppConnection.Zabbix]: platformManagedCredentialsNotSupported, + [AppConnection.Railway]: platformManagedCredentialsNotSupported, + [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported }; export const enterpriseAppCheck = async ( diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index cf5ed1a42..4f274516c 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -30,8 +30,9 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Flyio]: "Fly.io", [AppConnection.GitLab]: "GitLab", [AppConnection.Cloudflare]: "Cloudflare", - [AppConnection.Bitbucket]: "Bitbucket", - [AppConnection.Zabbix]: "Zabbix" + [AppConnection.Zabbix]: "Zabbix", + [AppConnection.Railway]: "Railway", + [AppConnection.Bitbucket]: "Bitbucket" }; export const APP_CONNECTION_PLAN_MAP: Record = { @@ -64,6 +65,7 @@ export const APP_CONNECTION_PLAN_MAP: Record>>; @@ -248,6 +255,7 @@ export type TAppConnectionInput = { id: string } & ( | TCloudflareConnectionInput | TBitbucketConnectionInput | TZabbixConnectionInput + | TRailwayConnectionInput ); export type TSqlConnectionInput = @@ -293,7 +301,8 @@ export type TAppConnectionConfig = | TGitLabConnectionConfig | TCloudflareConnectionConfig | TBitbucketConnectionConfig - | TZabbixConnectionConfig; + | TZabbixConnectionConfig + | TRailwayConnectionConfig; export type TValidateAppConnectionCredentialsSchema = | TValidateAwsConnectionCredentialsSchema @@ -326,7 +335,8 @@ export type TValidateAppConnectionCredentialsSchema = | TValidateGitLabConnectionCredentialsSchema | TValidateCloudflareConnectionCredentialsSchema | TValidateBitbucketConnectionCredentialsSchema - | TValidateZabbixConnectionCredentialsSchema; + | TValidateZabbixConnectionCredentialsSchema + | TValidateRailwayConnectionCredentialsSchema; export type TListAwsConnectionKmsKeys = { connectionId: string; diff --git a/backend/src/services/app-connection/railway/index.ts b/backend/src/services/app-connection/railway/index.ts new file mode 100644 index 000000000..a282bd9dd --- /dev/null +++ b/backend/src/services/app-connection/railway/index.ts @@ -0,0 +1,4 @@ +export * from "./railway-connection-constants"; +export * from "./railway-connection-fns"; +export * from "./railway-connection-schemas"; +export * from "./railway-connection-types"; diff --git a/backend/src/services/app-connection/railway/railway-connection-constants.ts b/backend/src/services/app-connection/railway/railway-connection-constants.ts new file mode 100644 index 000000000..aabec1027 --- /dev/null +++ b/backend/src/services/app-connection/railway/railway-connection-constants.ts @@ -0,0 +1,5 @@ +export enum RailwayConnectionMethod { + AccountToken = "account-token", + ProjectToken = "project-token", + TeamToken = "team-token" +} diff --git a/backend/src/services/app-connection/railway/railway-connection-fns.ts b/backend/src/services/app-connection/railway/railway-connection-fns.ts new file mode 100644 index 000000000..7aa25b9f6 --- /dev/null +++ b/backend/src/services/app-connection/railway/railway-connection-fns.ts @@ -0,0 +1,66 @@ +/* eslint-disable no-await-in-loop */ +import { AxiosError } from "axios"; + +import { BadRequestError } from "@app/lib/errors"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +import { RailwayConnectionMethod } from "./railway-connection-constants"; +import { RailwayPublicAPI } from "./railway-connection-public-client"; +import { TRailwayConnection, TRailwayConnectionConfig } from "./railway-connection-types"; + +export const getRailwayConnectionListItem = () => { + return { + name: "Railway" as const, + app: AppConnection.Railway as const, + methods: Object.values(RailwayConnectionMethod) + }; +}; + +export const validateRailwayConnectionCredentials = async (config: TRailwayConnectionConfig) => { + const { credentials, method } = config; + + try { + await RailwayPublicAPI.healthcheck({ + method, + credentials + }); + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${error.message || "Unknown error"}` + }); + } + + throw new BadRequestError({ + message: "Unable to validate connection - verify credentials" + }); + } + + return credentials; +}; + +export const listProjects = async (appConnection: TRailwayConnection) => { + const { credentials, method } = appConnection; + + try { + return await RailwayPublicAPI.listProjects({ + method, + credentials + }); + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to list projects: ${error.message || "Unknown error"}` + }); + } + + if (error instanceof BadRequestError) { + throw error; + } + + throw new BadRequestError({ + message: "Unable to list projects", + error + }); + } +}; diff --git a/backend/src/services/app-connection/railway/railway-connection-public-client.ts b/backend/src/services/app-connection/railway/railway-connection-public-client.ts new file mode 100644 index 000000000..1c8bd9cc2 --- /dev/null +++ b/backend/src/services/app-connection/railway/railway-connection-public-client.ts @@ -0,0 +1,237 @@ +/* eslint-disable class-methods-use-this */ +import { AxiosError, AxiosInstance, AxiosResponse } from "axios"; + +import { createRequestClient } from "@app/lib/config/request"; +import { BadRequestError } from "@app/lib/errors"; +import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; + +import { RailwayConnectionMethod } from "./railway-connection-constants"; +import { + RailwayAccountWorkspaceListSchema, + RailwayGetProjectsByProjectTokenSchema, + RailwayGetSubscriptionTypeSchema, + RailwayProjectsListSchema +} from "./railway-connection-schemas"; +import { RailwayProject, TRailwayConnectionConfig, TRailwayResponse } from "./railway-connection-types"; + +type RailwaySendReqOptions = Pick; + +export function getRailwayAuthHeaders(method: RailwayConnectionMethod, token: string): Record { + switch (method) { + case RailwayConnectionMethod.AccountToken: + case RailwayConnectionMethod.TeamToken: + return { + Authorization: token + }; + case RailwayConnectionMethod.ProjectToken: + return { + "Project-Access-Token": token + }; + default: + throw new Error(`Unsupported Railway connection method`); + } +} + +export function getRailwayRatelimiter(headers: AxiosResponse["headers"]): { + isRatelimited: boolean; + maxAttempts: number; + wait: () => Promise; +} { + const retryAfter: number | undefined = headers["Retry-After"] as number | undefined; + const requestsLeft = parseInt(headers["X-RateLimit-Remaining"] as string, 10); + const limitResetAt = headers["X-RateLimit-Reset"] as string; + + const now = +new Date(); + const nextReset = +new Date(limitResetAt); + + const remaining = Math.min(0, nextReset - now); + + const wait = () => { + return new Promise((res) => { + setTimeout(res, remaining); + }); + }; + + return { + isRatelimited: Boolean(retryAfter || requestsLeft === 0), + wait, + maxAttempts: 3 + }; +} + +class RailwayPublicClient { + private client: AxiosInstance; + + constructor() { + this.client = createRequestClient({ + method: "POST", + baseURL: IntegrationUrls.RAILWAY_API_URL, + headers: { + "Content-Type": "application/json" + } + }); + } + + async send( + query: string, + options: RailwaySendReqOptions, + variables: Record> = {}, + retryAttempt: number = 0 + ): Promise { + const body = { + query, + variables + }; + + const response = await this.client.request({ + data: body, + headers: getRailwayAuthHeaders(options.method, options.credentials.apiToken) + }); + + const { errors } = response.data; + + if (Array.isArray(errors) && errors.length > 0) { + throw new AxiosError(errors[0].message); + } + + const limiter = getRailwayRatelimiter(response.headers); + + if (limiter.isRatelimited && retryAttempt <= limiter.maxAttempts) { + await limiter.wait(); + return this.send(query, options, variables, retryAttempt + 1); + } + + return response.data.data; + } + + healthcheck(config: RailwaySendReqOptions) { + switch (config.method) { + case RailwayConnectionMethod.AccountToken: + return this.send(`{ me { teams { edges { node { id } } } } }`, config); + case RailwayConnectionMethod.ProjectToken: + return this.send(`{ projectToken { projectId environmentId project { id } } }`, config); + case RailwayConnectionMethod.TeamToken: + return this.send(`{ projects { edges { node { id name team { id } } } } }`, config); + default: + throw new Error(`Unsupported Railway connection method`); + } + } + + async getSubscriptionType(config: RailwaySendReqOptions & { projectId: string }) { + const res = await this.send( + `query project($projectId: String!) { project(id: $projectId) { subscriptionType }}`, + config, + { + projectId: config.projectId + } + ); + + const data = await RailwayGetSubscriptionTypeSchema.parseAsync(res); + + return data.project.subscriptionType; + } + + async listProjects(config: RailwaySendReqOptions): Promise { + switch (config.method) { + case RailwayConnectionMethod.TeamToken: { + const res = await this.send( + `{ projects { edges { node { id, name, services{ edges{ node { id, name } } } environments { edges { node { name, id } } } } } } }`, + config + ); + + const data = await RailwayProjectsListSchema.parseAsync(res); + + return data.projects.edges.map((p) => ({ + id: p.node.id, + name: p.node.name, + environments: p.node.environments.edges.map((e) => e.node), + services: p.node.services.edges.map((s) => s.node) + })); + } + + case RailwayConnectionMethod.AccountToken: { + const res = await this.send( + `{ me { workspaces { id, name, team{ projects{ edges{ node{ id, name, services{ edges { node { name, id } } } environments { edges { node { name, id } } } } } } } } } }`, + config + ); + + const data = await RailwayAccountWorkspaceListSchema.parseAsync(res); + + return data.me.workspaces.flatMap((w) => + w.team.projects.edges.map((p) => ({ + id: p.node.id, + name: p.node.name, + environments: p.node.environments.edges.map((e) => e.node), + services: p.node.services.edges.map((s) => s.node) + })) + ); + } + + case RailwayConnectionMethod.ProjectToken: { + const res = await this.send( + `query { projectToken { project { id, name, services { edges { node { name, id } } } environments { edges { node { name, id } } } } } }`, + config + ); + + const data = await RailwayGetProjectsByProjectTokenSchema.parseAsync(res); + + const p = data.projectToken.project; + + return [ + { + id: p.id, + name: p.name, + environments: p.environments.edges.map((e) => e.node), + services: p.services.edges.map((s) => s.node) + } + ]; + } + + default: + throw new Error(`Unsupported Railway connection method`); + } + } + + async getVariables( + config: RailwaySendReqOptions, + variables: { projectId: string; environmentId: string; serviceId?: string } + ) { + const res = await this.send }>>( + `query variables($environmentId: String!, $projectId: String!, $serviceId: String) { variables( projectId: $projectId, environmentId: $environmentId, serviceId: $serviceId ) }`, + config, + variables + ); + + if (!res?.variables) { + throw new BadRequestError({ + message: "Failed to get railway variables - empty response" + }); + } + + return res.variables; + } + + async deleteVariable( + config: RailwaySendReqOptions, + variables: { input: { projectId: string; environmentId: string; name: string; serviceId?: string } } + ) { + await this.send }>>( + `mutation variableDelete($input: VariableDeleteInput!) { variableDelete(input: $input) }`, + config, + variables + ); + } + + async upsertVariable( + config: RailwaySendReqOptions, + variables: { input: { projectId: string; environmentId: string; name: string; value: string; serviceId?: string } } + ) { + await this.send }>>( + `mutation variableUpsert($input: VariableUpsertInput!) { variableUpsert(input: $input) }`, + config, + variables + ); + } +} + +export const RailwayPublicAPI = new RailwayPublicClient(); diff --git a/backend/src/services/app-connection/railway/railway-connection-schemas.ts b/backend/src/services/app-connection/railway/railway-connection-schemas.ts new file mode 100644 index 000000000..066258f1e --- /dev/null +++ b/backend/src/services/app-connection/railway/railway-connection-schemas.ts @@ -0,0 +1,117 @@ +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 { RailwayConnectionMethod } from "./railway-connection-constants"; + +export const RailwayConnectionMethodSchema = z + .nativeEnum(RailwayConnectionMethod) + .describe(AppConnections.CREATE(AppConnection.Railway).method); + +export const RailwayConnectionAccessTokenCredentialsSchema = z.object({ + apiToken: z + .string() + .trim() + .min(1, "API Token required") + .max(255) + .describe(AppConnections.CREDENTIALS.RAILWAY.apiToken) +}); + +const BaseRailwayConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.Railway) +}); + +export const RailwayConnectionSchema = BaseRailwayConnectionSchema.extend({ + method: RailwayConnectionMethodSchema, + credentials: RailwayConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedRailwayConnectionSchema = z.discriminatedUnion("method", [ + BaseRailwayConnectionSchema.extend({ + method: RailwayConnectionMethodSchema, + credentials: RailwayConnectionAccessTokenCredentialsSchema.pick({}) + }) +]); + +export const ValidateRailwayConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: RailwayConnectionMethodSchema, + credentials: RailwayConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.Railway).credentials + ) + }) +]); + +export const CreateRailwayConnectionSchema = ValidateRailwayConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.Railway) +); + +export const UpdateRailwayConnectionSchema = z + .object({ + credentials: RailwayConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.Railway).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Railway)); + +export const RailwayConnectionListItemSchema = z.object({ + name: z.literal("Railway"), + app: z.literal(AppConnection.Railway), + methods: z.nativeEnum(RailwayConnectionMethod).array() +}); + +export const RailwayResourceSchema = z.object({ + node: z.object({ + id: z.string(), + name: z.string() + }) +}); + +export const RailwayProjectEdgeSchema = z.object({ + node: z.object({ + id: z.string(), + name: z.string(), + services: z.object({ + edges: z.array(RailwayResourceSchema) + }), + environments: z.object({ + edges: z.array(RailwayResourceSchema) + }) + }) +}); + +export const RailwayProjectsListSchema = z.object({ + projects: z.object({ + edges: z.array(RailwayProjectEdgeSchema) + }) +}); + +export const RailwayAccountWorkspaceListSchema = z.object({ + me: z.object({ + workspaces: z.array( + z.object({ + id: z.string(), + name: z.string(), + team: RailwayProjectsListSchema + }) + ) + }) +}); + +export const RailwayGetProjectsByProjectTokenSchema = z.object({ + projectToken: z.object({ + project: RailwayProjectEdgeSchema.shape.node + }) +}); + +export const RailwayGetSubscriptionTypeSchema = z.object({ + project: z.object({ + subscriptionType: z.enum(["free", "hobby", "pro", "trial"]) + }) +}); diff --git a/backend/src/services/app-connection/railway/railway-connection-service.ts b/backend/src/services/app-connection/railway/railway-connection-service.ts new file mode 100644 index 000000000..379f36456 --- /dev/null +++ b/backend/src/services/app-connection/railway/railway-connection-service.ts @@ -0,0 +1,30 @@ +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listProjects as getRailwayProjects } from "./railway-connection-fns"; +import { TRailwayConnection } from "./railway-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const railwayConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listProjects = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.Railway, connectionId, actor); + try { + const projects = await getRailwayProjects(appConnection); + + return projects; + } catch (error) { + logger.error(error, "Failed to establish connection with Railway"); + return []; + } + }; + + return { + listProjects + }; +}; diff --git a/backend/src/services/app-connection/railway/railway-connection-types.ts b/backend/src/services/app-connection/railway/railway-connection-types.ts new file mode 100644 index 000000000..66b6b549f --- /dev/null +++ b/backend/src/services/app-connection/railway/railway-connection-types.ts @@ -0,0 +1,79 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateRailwayConnectionSchema, + RailwayConnectionSchema, + ValidateRailwayConnectionCredentialsSchema +} from "./railway-connection-schemas"; + +export type TRailwayConnection = z.infer; + +export type TRailwayConnectionInput = z.infer & { + app: AppConnection.Railway; +}; + +export type TValidateRailwayConnectionCredentialsSchema = typeof ValidateRailwayConnectionCredentialsSchema; + +export type TRailwayConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type TRailwayService = { + id: string; + name: string; +}; + +export type TRailwayEnvironment = { + id: string; + name: string; +}; + +export type RailwayProject = { + id: string; + name: string; + services: TRailwayService[]; + environments: TRailwayEnvironment[]; +}; + +export type TRailwayResponse = { + data?: T; + errors?: { + message: string; + }[]; +}; + +export type TAccountProjectListResponse = TRailwayResponse<{ + projects: { + edges: TProjectEdge[]; + }; +}>; + +export interface TProjectEdge { + node: { + id: string; + name: string; + services: { + edges: TServiceEdge[]; + }; + environments: { + edges: TEnvironmentEdge[]; + }; + }; +} + +type TServiceEdge = { + node: { + id: string; + name: string; + }; +}; + +type TEnvironmentEdge = { + node: { + id: string; + name: string; + }; +}; diff --git a/backend/src/services/org-membership/org-membership-dal.ts b/backend/src/services/org-membership/org-membership-dal.ts index ebf1700d0..ed4867025 100644 --- a/backend/src/services/org-membership/org-membership-dal.ts +++ b/backend/src/services/org-membership/org-membership-dal.ts @@ -108,16 +108,16 @@ export const orgMembershipDALFactory = (db: TDbClient) => { const now = new Date(); const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); const oneMonthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); - const threeMonthsAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); + const twelveMonthsAgo = new Date(now.getTime() - 360 * 24 * 60 * 60 * 1000); const memberships = await db .replicaNode()(TableName.OrgMembership) .where("status", "invited") .where((qb) => { - // lastInvitedAt is null AND createdAt is between 1 week and 3 months ago + // lastInvitedAt is null AND createdAt is between 1 week and 12 months ago void qb .whereNull(`${TableName.OrgMembership}.lastInvitedAt`) - .whereBetween(`${TableName.OrgMembership}.createdAt`, [threeMonthsAgo, oneWeekAgo]); + .whereBetween(`${TableName.OrgMembership}.createdAt`, [twelveMonthsAgo, oneWeekAgo]); }) .orWhere((qb) => { // lastInvitedAt is older than 1 week ago AND createdAt is younger than 1 month ago diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 5941eb013..de518c829 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -36,6 +36,8 @@ import { getConfig } from "@app/lib/config/env"; import { generateAsymmetricKeyPair } from "@app/lib/crypto"; import { generateSymmetricKey, infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { generateUserSrpKeys } from "@app/lib/crypto/srp"; +import { applyJitter } from "@app/lib/dates"; +import { delay as delayMs } from "@app/lib/delay"; import { BadRequestError, ForbiddenRequestError, @@ -44,9 +46,10 @@ import { UnauthorizedError } from "@app/lib/errors"; import { groupBy } from "@app/lib/fn"; +import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { isDisposableEmail } from "@app/lib/validator"; -import { TQueueServiceFactory } from "@app/queue"; +import { QueueName, TQueueServiceFactory } from "@app/queue"; import { getDefaultOrgMembershipRoleForUpdateOrg } from "@app/services/org/org-role-fns"; import { TOrgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal"; @@ -909,14 +912,6 @@ export const orgServiceFactory = ({ // if there exist no org membership we set is as given by the request if (!inviteeOrgMembership) { - if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { - // limit imposed on number of members allowed / number of members used exceeds the number of members allowed - throw new BadRequestError({ - name: "InviteUser", - message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members." - }); - } - if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed throw new BadRequestError({ @@ -1438,6 +1433,8 @@ export const orgServiceFactory = ({ * Re-send emails to users who haven't accepted an invite yet */ const notifyInvitedUsers = async () => { + logger.info(`${QueueName.DailyResourceCleanUp}: notify invited users started`); + const invitedUsers = await orgMembershipDAL.findRecentInvitedMemberships(); const appCfg = getConfig(); @@ -1461,24 +1458,32 @@ export const orgServiceFactory = ({ }); if (invitedUser.inviteEmail) { - await smtpService.sendMail({ - template: SmtpTemplates.OrgInvite, - subjectLine: `Reminder: You have been invited to ${org.name} on Infisical`, - recipients: [invitedUser.inviteEmail], - substitutions: { - organizationName: org.name, - email: invitedUser.inviteEmail, - organizationId: org.id.toString(), - token, - callback_url: `${appCfg.SITE_URL}/signupinvite` - } - }); - notifiedUsers.push(invitedUser.id); + await delayMs(Math.max(0, applyJitter(0, 2000))); + + try { + await smtpService.sendMail({ + template: SmtpTemplates.OrgInvite, + subjectLine: `Reminder: You have been invited to ${org.name} on Infisical`, + recipients: [invitedUser.inviteEmail], + substitutions: { + organizationName: org.name, + email: invitedUser.inviteEmail, + organizationId: org.id.toString(), + token, + callback_url: `${appCfg.SITE_URL}/signupinvite` + } + }); + notifiedUsers.push(invitedUser.id); + } catch (err) { + logger.error(err, `${QueueName.DailyResourceCleanUp}: notify invited users failed to send email`); + } } }) ); await orgMembershipDAL.updateLastInvitedAtByIds(notifiedUsers); + + logger.info(`${QueueName.DailyResourceCleanUp}: notify invited users completed`); }; return { diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index e06cfcb01..e8d0b05e5 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -214,7 +214,7 @@ export const secretFolderServiceFactory = ({ } }, message: "Folder created", - folderId: doc.id, + folderId: parentFolder.id, changes: [ { type: CommitType.ADD, diff --git a/backend/src/services/secret-sync/railway/railway-sync-constants.ts b/backend/src/services/secret-sync/railway/railway-sync-constants.ts new file mode 100644 index 000000000..a77311bbf --- /dev/null +++ b/backend/src/services/secret-sync/railway/railway-sync-constants.ts @@ -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 RAILWAY_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Railway", + destination: SecretSync.Railway, + connection: AppConnection.Railway, + canImportSecrets: true +}; diff --git a/backend/src/services/secret-sync/railway/railway-sync-fns.ts b/backend/src/services/secret-sync/railway/railway-sync-fns.ts new file mode 100644 index 000000000..07862aeb5 --- /dev/null +++ b/backend/src/services/secret-sync/railway/railway-sync-fns.ts @@ -0,0 +1,124 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +import { RailwayPublicAPI } from "@app/services/app-connection/railway/railway-connection-public-client"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; + +import { SecretSyncError } from "../secret-sync-errors"; +import { TSecretMap } from "../secret-sync-types"; +import { TRailwaySyncWithCredentials } from "./railway-sync-types"; + +export const RailwaySyncFns = { + async getSecrets(secretSync: TRailwaySyncWithCredentials): Promise { + try { + const config = secretSync.destinationConfig; + + const variables = await RailwayPublicAPI.getVariables(secretSync.connection, { + projectId: config.projectId, + environmentId: config.environmentId, + serviceId: config.serviceId || undefined + }); + + const entries = {} as TSecretMap; + + for (const [key, value] of Object.entries(variables)) { + // Skip importing private railway variables + // eslint-disable-next-line no-continue + if (key.startsWith("RAILWAY_")) continue; + + entries[key] = { + value + }; + } + + return entries; + } catch (error) { + throw new SecretSyncError({ + error, + message: "Failed to import secrets from Railway" + }); + } + }, + + async syncSecrets(secretSync: TRailwaySyncWithCredentials, secretMap: TSecretMap) { + const { + environment, + syncOptions: { disableSecretDeletion, keySchema } + } = secretSync; + const railwaySecrets = await this.getSecrets(secretSync); + const config = secretSync.destinationConfig; + + for await (const key of Object.keys(secretMap)) { + try { + const existing = railwaySecrets[key]; + + if (existing === undefined || existing.value !== secretMap[key].value) { + await RailwayPublicAPI.upsertVariable(secretSync.connection, { + input: { + projectId: config.projectId, + environmentId: config.environmentId, + serviceId: config.serviceId || undefined, + name: key, + value: secretMap[key].value ?? "" + } + }); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + + if (disableSecretDeletion) return; + + for await (const key of Object.keys(railwaySecrets)) { + try { + // eslint-disable-next-line no-continue + if (!matchesSchema(key, environment?.slug || "", keySchema)) continue; + + if (!secretMap[key]) { + await RailwayPublicAPI.deleteVariable(secretSync.connection, { + input: { + projectId: config.projectId, + environmentId: config.environmentId, + serviceId: config.serviceId || undefined, + name: key + } + }); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: key + }); + } + } + }, + + async removeSecrets(secretSync: TRailwaySyncWithCredentials, secretMap: TSecretMap) { + const existing = await this.getSecrets(secretSync); + const config = secretSync.destinationConfig; + + for await (const secret of Object.keys(existing)) { + try { + if (secret in secretMap) { + await RailwayPublicAPI.deleteVariable(secretSync.connection, { + input: { + projectId: config.projectId, + environmentId: config.environmentId, + serviceId: config.serviceId || undefined, + name: secret + } + }); + } + } catch (error) { + throw new SecretSyncError({ + error, + secretKey: secret + }); + } + } + } +}; diff --git a/backend/src/services/secret-sync/railway/railway-sync-schemas.ts b/backend/src/services/secret-sync/railway/railway-sync-schemas.ts new file mode 100644 index 000000000..56cea0408 --- /dev/null +++ b/backend/src/services/secret-sync/railway/railway-sync-schemas.ts @@ -0,0 +1,56 @@ +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 RailwaySyncDestinationConfigSchema = z.object({ + projectId: z + .string() + .trim() + .min(1, "Railway project ID required") + .describe(SecretSyncs.DESTINATION_CONFIG.RAILWAY.projectId), + projectName: z.string().trim().describe(SecretSyncs.DESTINATION_CONFIG.RAILWAY.projectName), + environmentId: z + .string() + .trim() + .min(1, "Railway environment ID required") + .describe(SecretSyncs.DESTINATION_CONFIG.RAILWAY.environmentId), + environmentName: z.string().trim().describe(SecretSyncs.DESTINATION_CONFIG.RAILWAY.environmentName), + serviceId: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.RAILWAY.serviceId), + serviceName: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.RAILWAY.serviceName) +}); + +const RailwaySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const RailwaySyncSchema = BaseSecretSyncSchema(SecretSync.Railway, RailwaySyncOptionsConfig).extend({ + destination: z.literal(SecretSync.Railway), + destinationConfig: RailwaySyncDestinationConfigSchema +}); + +export const CreateRailwaySyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.Railway, + RailwaySyncOptionsConfig +).extend({ + destinationConfig: RailwaySyncDestinationConfigSchema +}); + +export const UpdateRailwaySyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.Railway, + RailwaySyncOptionsConfig +).extend({ + destinationConfig: RailwaySyncDestinationConfigSchema.optional() +}); + +export const RailwaySyncListItemSchema = z.object({ + name: z.literal("Railway"), + connection: z.literal(AppConnection.Railway), + destination: z.literal(SecretSync.Railway), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/railway/railway-sync-types.ts b/backend/src/services/secret-sync/railway/railway-sync-types.ts new file mode 100644 index 000000000..d2165072d --- /dev/null +++ b/backend/src/services/secret-sync/railway/railway-sync-types.ts @@ -0,0 +1,31 @@ +import z from "zod"; + +import { TRailwayConnection } from "@app/services/app-connection/railway"; + +import { CreateRailwaySyncSchema, RailwaySyncListItemSchema, RailwaySyncSchema } from "./railway-sync-schemas"; + +export type TRailwaySyncListItem = z.infer; + +export type TRailwaySync = z.infer; + +export type TRailwaySyncInput = z.infer; + +export type TRailwaySyncWithCredentials = TRailwaySync & { + connection: TRailwayConnection; +}; + +export type TRailwaySecret = { + createdAt: string; + environmentId?: string | null; + id: string; + isSealed: boolean; + name: string; + serviceId?: string | null; + updatedAt: string; +}; + +export type TRailwayVariablesGraphResponse = { + data: { + variables: Record; + }; +}; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 022a7a1b3..64a191a22 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -22,7 +22,9 @@ export enum SecretSync { GitLab = "gitlab", CloudflarePages = "cloudflare-pages", CloudflareWorkers = "cloudflare-workers", - Zabbix = "zabbix" + + Zabbix = "zabbix", + Railway = "railway" } export enum SecretSyncInitialSyncBehavior { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 46ebd3742..7dc19d3e9 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -40,6 +40,8 @@ import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault"; import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; +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"; import { SECRET_SYNC_PLAN_MAP } from "./secret-sync-maps"; import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity"; @@ -72,7 +74,9 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION, [SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION, [SecretSync.CloudflareWorkers]: CLOUDFLARE_WORKERS_SYNC_LIST_OPTION, - [SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION + + [SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION, + [SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION }; export const listSecretSyncOptions = () => { @@ -244,6 +248,8 @@ export const SecretSyncFns = { return CloudflareWorkersSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Zabbix: return ZabbixSyncFns.syncSecrets(secretSync, schemaSecretMap); + case SecretSync.Railway: + return RailwaySyncFns.syncSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -342,6 +348,9 @@ export const SecretSyncFns = { case SecretSync.Zabbix: secretMap = await ZabbixSyncFns.getSecrets(secretSync); break; + case SecretSync.Railway: + secretMap = await RailwaySyncFns.getSecrets(secretSync); + break; default: throw new Error( `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -423,6 +432,8 @@ export const SecretSyncFns = { return CloudflareWorkersSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Zabbix: return ZabbixSyncFns.removeSecrets(secretSync, schemaSecretMap); + case SecretSync.Railway: + return RailwaySyncFns.removeSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index f7cdc9896..dd1d5b146 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -25,7 +25,9 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.GitLab]: "GitLab", [SecretSync.CloudflarePages]: "Cloudflare Pages", [SecretSync.CloudflareWorkers]: "Cloudflare Workers", - [SecretSync.Zabbix]: "Zabbix" + + [SecretSync.Zabbix]: "Zabbix", + [SecretSync.Railway]: "Railway" }; export const SECRET_SYNC_CONNECTION_MAP: Record = { @@ -52,7 +54,9 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.GitLab]: AppConnection.GitLab, [SecretSync.CloudflarePages]: AppConnection.Cloudflare, [SecretSync.CloudflareWorkers]: AppConnection.Cloudflare, - [SecretSync.Zabbix]: AppConnection.Zabbix + + [SecretSync.Zabbix]: AppConnection.Zabbix, + [SecretSync.Railway]: AppConnection.Railway }; export const SECRET_SYNC_PLAN_MAP: Record = { @@ -79,5 +83,7 @@ export const SECRET_SYNC_PLAN_MAP: Record = { [SecretSync.GitLab]: SecretSyncPlanType.Regular, [SecretSync.CloudflarePages]: SecretSyncPlanType.Regular, [SecretSync.CloudflareWorkers]: SecretSyncPlanType.Regular, - [SecretSync.Zabbix]: SecretSyncPlanType.Regular + + [SecretSync.Zabbix]: SecretSyncPlanType.Regular, + [SecretSync.Railway]: SecretSyncPlanType.Regular }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index 272002d05..998328b85 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -100,6 +100,12 @@ import { THumanitecSyncListItem, THumanitecSyncWithCredentials } from "./humanitec"; +import { + TRailwaySync, + TRailwaySyncInput, + TRailwaySyncListItem, + TRailwaySyncWithCredentials +} from "./railway/railway-sync-types"; import { TRenderSync, TRenderSyncInput, @@ -145,7 +151,8 @@ export type TSecretSync = | TGitLabSync | TCloudflarePagesSync | TCloudflareWorkersSync - | TZabbixSync; + | TZabbixSync + | TRailwaySync; export type TSecretSyncWithCredentials = | TAwsParameterStoreSyncWithCredentials @@ -171,7 +178,8 @@ export type TSecretSyncWithCredentials = | TGitLabSyncWithCredentials | TCloudflarePagesSyncWithCredentials | TCloudflareWorkersSyncWithCredentials - | TZabbixSyncWithCredentials; + | TZabbixSyncWithCredentials + | TRailwaySyncWithCredentials; export type TSecretSyncInput = | TAwsParameterStoreSyncInput @@ -197,7 +205,8 @@ export type TSecretSyncInput = | TGitLabSyncInput | TCloudflarePagesSyncInput | TCloudflareWorkersSyncInput - | TZabbixSyncInput; + | TZabbixSyncInput + | TRailwaySyncInput; export type TSecretSyncListItem = | TAwsParameterStoreSyncListItem @@ -223,7 +232,8 @@ export type TSecretSyncListItem = | TGitLabSyncListItem | TCloudflarePagesSyncListItem | TCloudflareWorkersSyncListItem - | TZabbixSyncListItem; + | TZabbixSyncListItem + | TRailwaySyncListItem; export type TSyncOptionsConfig = { canImportSecrets: boolean; diff --git a/docs/Dockerfile b/docs/Dockerfile index 34730d01e..079972544 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -1,6 +1,32 @@ -FROM node:20-alpine +FROM node:20-alpine AS builder + WORKDIR /app -RUN npm install -g mint + +RUN npm install -g mint@4.2.13 + COPY . . + +# Install a local version of our OpenAPI spec +RUN apk add --no-cache wget jq && \ + wget -O spec.json https://app.infisical.com/api/docs/json && \ + jq '.api.openapi = "./spec.json"' docs.json > temp.json && \ + mv temp.json docs.json + +# Run mint dev briefly to download the web client +RUN timeout 30 mint dev || true + +FROM node:20-alpine + +WORKDIR /app + +RUN npm install -g mint@4.2.13 + +COPY . . + +COPY --from=builder /root/.mintlify /root/.mintlify +COPY --from=builder /app/docs.json /app/docs.json +COPY --from=builder /app/spec.json /app/spec.json + EXPOSE 3000 + CMD ["mint", "dev"] diff --git a/docs/api-reference/endpoints/app-connections/railway/available.mdx b/docs/api-reference/endpoints/app-connections/railway/available.mdx new file mode 100644 index 000000000..83190c379 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/railway/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/railway/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/railway/create.mdx b/docs/api-reference/endpoints/app-connections/railway/create.mdx new file mode 100644 index 000000000..96c1c9c53 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/railway/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/railway" +--- + + + Check out the configuration docs for [Railway Connections](/integrations/app-connections/railway) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/railway/delete.mdx b/docs/api-reference/endpoints/app-connections/railway/delete.mdx new file mode 100644 index 000000000..4938f26e8 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/railway/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/railway/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/railway/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/railway/get-by-id.mdx new file mode 100644 index 000000000..844bd2376 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/railway/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/railway/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/railway/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/railway/get-by-name.mdx new file mode 100644 index 000000000..4497cbfca --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/railway/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/railway/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/railway/list.mdx b/docs/api-reference/endpoints/app-connections/railway/list.mdx new file mode 100644 index 000000000..16a6a087e --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/railway/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/railway" +--- diff --git a/docs/api-reference/endpoints/app-connections/railway/update.mdx b/docs/api-reference/endpoints/app-connections/railway/update.mdx new file mode 100644 index 000000000..66fa37a43 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/railway/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/railway/{connectionId}" +--- + + + Check out the configuration docs for [Railway Connections](/integrations/app-connections/railway) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/secret-syncs/railway/create.mdx b/docs/api-reference/endpoints/secret-syncs/railway/create.mdx new file mode 100644 index 000000000..51d23eaf2 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/railway/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/railway" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/railway/delete.mdx b/docs/api-reference/endpoints/secret-syncs/railway/delete.mdx new file mode 100644 index 000000000..786ce05e6 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/railway/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/railway/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/railway/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/railway/get-by-id.mdx new file mode 100644 index 000000000..dbeddee50 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/railway/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/railway/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/railway/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/railway/get-by-name.mdx new file mode 100644 index 000000000..4e4964adc --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/railway/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/railway/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/railway/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/railway/import-secrets.mdx new file mode 100644 index 000000000..2f9a9b017 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/railway/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/railway/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/railway/list.mdx b/docs/api-reference/endpoints/secret-syncs/railway/list.mdx new file mode 100644 index 000000000..f4dc62a45 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/railway/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/railway" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/railway/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/railway/remove-secrets.mdx new file mode 100644 index 000000000..f3e187a11 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/railway/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/railway/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/railway/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/railway/sync-secrets.mdx new file mode 100644 index 000000000..5bccb271a --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/railway/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/railway/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/railway/update.mdx b/docs/api-reference/endpoints/secret-syncs/railway/update.mdx new file mode 100644 index 000000000..104194868 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/railway/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/railway/{syncId}" +--- diff --git a/docs/docs.json b/docs/docs.json index 5b5775fc6..3a516febb 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -488,6 +488,7 @@ "integrations/app-connections/oci", "integrations/app-connections/oracledb", "integrations/app-connections/postgres", + "integrations/app-connections/railway", "integrations/app-connections/render", "integrations/app-connections/teamcity", "integrations/app-connections/terraform-cloud", @@ -523,6 +524,7 @@ "integrations/secret-syncs/heroku", "integrations/secret-syncs/humanitec", "integrations/secret-syncs/oci-vault", + "integrations/secret-syncs/railway", "integrations/secret-syncs/render", "integrations/secret-syncs/teamcity", "integrations/secret-syncs/terraform-cloud", @@ -564,8 +566,8 @@ "integrations/cloud/digital-ocean-app-platform", "integrations/cloud/heroku", "integrations/cloud/netlify", - "integrations/cloud/railway", "integrations/cloud/flyio", + "integrations/cloud/railway", "integrations/cloud/render", "integrations/cloud/laravel-forge", "integrations/cloud/supabase", @@ -1518,6 +1520,18 @@ "api-reference/endpoints/app-connections/postgres/delete" ] }, + { + "group": "Railway", + "pages": [ + "api-reference/endpoints/app-connections/railway/list", + "api-reference/endpoints/app-connections/railway/available", + "api-reference/endpoints/app-connections/railway/get-by-id", + "api-reference/endpoints/app-connections/railway/get-by-name", + "api-reference/endpoints/app-connections/railway/create", + "api-reference/endpoints/app-connections/railway/update", + "api-reference/endpoints/app-connections/railway/delete" + ] + }, { "group": "Render", "pages": [ @@ -1840,6 +1854,20 @@ "api-reference/endpoints/secret-syncs/oci-vault/remove-secrets" ] }, + { + "group": "Railway", + "pages": [ + "api-reference/endpoints/secret-syncs/railway/list", + "api-reference/endpoints/secret-syncs/railway/get-by-id", + "api-reference/endpoints/secret-syncs/railway/get-by-name", + "api-reference/endpoints/secret-syncs/railway/create", + "api-reference/endpoints/secret-syncs/railway/update", + "api-reference/endpoints/secret-syncs/railway/delete", + "api-reference/endpoints/secret-syncs/railway/sync-secrets", + "api-reference/endpoints/secret-syncs/railway/import-secrets", + "api-reference/endpoints/secret-syncs/railway/remove-secrets" + ] + }, { "group": "Render", "pages": [ @@ -2186,7 +2214,7 @@ "api": { "openapi": "https://app.infisical.com/api/docs/json", "mdx": { - "server": ["https://app.infisical.com", "http://localhost:8080"] + "server": ["https://app.infisical.com"] } }, "appearance": { diff --git a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx index 03bc5df84..28b177c5f 100644 --- a/docs/documentation/platform/dynamic-secrets/aws-iam.mdx +++ b/docs/documentation/platform/dynamic-secrets/aws-iam.mdx @@ -3,13 +3,13 @@ title: "AWS IAM" description: "Learn how to dynamically generate AWS IAM Users." --- -The Infisical AWS IAM dynamic secret allows you to generate AWS IAM Users on demand based on configured AWS policy. +The Infisical AWS IAM dynamic secret allows you to generate AWS IAM Users on demand based on a configured AWS policy. Infisical supports several authentication methods to connect to your AWS account, including assuming an IAM Role, using IAM Roles for Service Accounts (IRSA) on EKS, or static Access Keys. ## Prerequisite -Infisical needs an initial AWS IAM user with the required permissions to create sub IAM users. This IAM user will be responsible for managing the lifecycle of new IAM users. +Infisical needs an AWS IAM principal (a user or a role) with the required permissions to create and manage other IAM users. This principal will be responsible for the lifecycle of the dynamically generated users. - + ```json { @@ -235,7 +235,169 @@ Replace **\** with your AWS account id and **\** w ![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png) + + + This method is recommended for self-hosted Infisical instances running on AWS EKS. It uses [IAM Roles for Service Accounts (IRSA)](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) to securely grant permissions to the Infisical pods without managing static credentials. + + In order to use IRSA, the `KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN` environment variable must be set to `true` for your self-hosted Infisical instance. + + + + + If you don't already have one, you need to create an IAM OIDC provider for your EKS cluster. This allows IAM to trust authentication tokens from your Kubernetes cluster. + 1. Find your cluster's OIDC provider URL from the EKS console or by using the AWS CLI: + `aws eks describe-cluster --name --query "cluster.identity.oidc.issuer" --output text` + 2. Navigate to the [IAM Identity Providers](https://console.aws.amazon.com/iam/home#/providers) page in your AWS Console and create a new OpenID Connect provider with the URL and `sts.amazonaws.com` as the audience. + + ![Create OIDC Provider Placeholder](/images/integrations/aws/irsa-create-oidc-provider.png) + + + 1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console. + 2. Select **Web identity** as the **Trusted Entity Type**. + 3. Choose the OIDC provider you created in the previous step. + 4. For the **Audience**, select `sts.amazonaws.com`. + ![IAM Role Creation for IRSA](/images/integrations/aws/irsa-iam-role-creation.png) + 5. Attach the permission policy detailed in the **Prerequisite** section at the top of this page. + 6. After creating the role, edit its **Trust relationship** to specify the service account Infisical is using in your cluster. This ensures only the Infisical pod can assume this role. + + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam:::oidc-provider/oidc.eks..amazonaws.com/id/" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "oidc.eks..amazonaws.com/id/:sub": "system:serviceaccount::", + "oidc.eks..amazonaws.com/id/:aud": "sts.amazonaws.com" + } + } + } + ] + } + ``` + Replace ``, ``, ``, ``, and `` with your specific values. + + + For the IRSA mechanism to work, the Infisical service account in your Kubernetes cluster must be annotated with the ARN of the IAM role you just created. + + Run the following command, replacing the placeholders with your values: + ```bash + kubectl annotate serviceaccount -n \ + eks.amazonaws.com/role-arn=arn:aws:iam:::role/ + ``` + This annotation tells the EKS Pod Identity Webhook to inject the necessary environment variables and tokens into the Infisical pod, allowing it to assume the specified IAM role. + + + Navigate to the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret to. + + + ![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png) + + + ![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal-aws-iam.png) + + + ![Dynamic Secret Setup Modal for IRSA](/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-irsa.png) + + Name by which you want the secret to be referenced + + + Default time-to-live for a generated secret (it is possible to modify this value after a secret is generated) + + + Maximum time-to-live for a generated secret + + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Allowed template variables are + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + - `{{identity.name}}`: Name of the identity that is generating the secret + - `{{random N}}`: Random string of N characters + + Allowed template functions are + - `truncate`: Truncates a string to a specified length + - `replace`: Replaces a substring with another value + + Examples: + ``` + {{randomUsername}} // 3POnzeFyK9gW2nioK0q2gMjr6CZqsRiX + {{unixTimestamp}} // 17490641580 + {{identity.name}} // testuser + {{random-5}} // x9k2m + {{truncate identity.name 4}} // test + {{replace identity.name 'user' 'replace'}} // testreplace + ``` + + + Tags to be added to the created IAM User resource. + + + Select *IRSA* method. + + + The ARN of the AWS IAM Role for the service account to assume. + + + [IAM AWS Path](https://aws.amazon.com/blogs/security/optimize-aws-administration-with-iam-paths/) to scope created IAM User resource access. + + + The AWS data center region. + + + The IAM Policy ARN of the [AWS Permissions Boundary](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html) to attach to IAM users created in the role. + + + The AWS IAM groups that should be assigned to the created users. Multiple values can be provided by separating them with commas + + + The AWS IAM managed policies that should be attached to the created users. Multiple values can be provided by separating them with commas + + + The AWS IAM inline policy that should be attached to the created users. + Multiple values can be provided by separating them with commas + + + Specifies a template for generating usernames. This field allows customization of how usernames are automatically created. + + Allowed template variables are + + - `{{randomUsername}}`: Random username string + - `{{unixTimestamp}}`: Current Unix timestamp + + + + After submitting the form, you will see a dynamic secret created in the dashboard. + ![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png) + + + Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials. + To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item. + Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section. + + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-generate.png) + ![Dynamic Secret](/images/platform/dynamic-secrets/dynamic-secret-lease-empty.png) + + When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for. + + ![Provision Lease](/images/platform/dynamic-secrets/provision-lease.png) + + + Ensure that the TTL for the lease falls within the maximum TTL defined when configuring the dynamic secret in step 4. + + + Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you. + + ![Provision Lease](/images/platform/dynamic-secrets/lease-values-aws-iam.png) + + Infisical will use the provided **Access Key ID** and **Secret Key** to connect to your AWS instance. @@ -263,9 +425,9 @@ Replace **\** with your AWS account id and **\** w Maximum time-to-live for a generated secret - - Select *Access Key* method. - + + Select *Access Key* method. + The managing AWS IAM User Access Key diff --git a/docs/images/app-connections/railway/railway-app-connection-account-settings-tokens.png b/docs/images/app-connections/railway/railway-app-connection-account-settings-tokens.png new file mode 100644 index 000000000..c3a6b014f Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-account-settings-tokens.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-account-settings.png b/docs/images/app-connections/railway/railway-app-connection-account-settings.png new file mode 100644 index 000000000..15f3d9af5 Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-account-settings.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-account-token-create.png b/docs/images/app-connections/railway/railway-app-connection-account-token-create.png new file mode 100644 index 000000000..7c4ac2af7 Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-account-token-create.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-account-token-created.png b/docs/images/app-connections/railway/railway-app-connection-account-token-created.png new file mode 100644 index 000000000..6ba7a83c7 Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-account-token-created.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-account-token-form.png b/docs/images/app-connections/railway/railway-app-connection-account-token-form.png new file mode 100644 index 000000000..200fd1cdc Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-account-token-form.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-form.png b/docs/images/app-connections/railway/railway-app-connection-form.png new file mode 100644 index 000000000..122661426 Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-form.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-generated.png b/docs/images/app-connections/railway/railway-app-connection-generated.png new file mode 100644 index 000000000..e3fc0c5cb Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-generated.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-option.png b/docs/images/app-connections/railway/railway-app-connection-option.png new file mode 100644 index 000000000..cffaa0e85 Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-option.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-project-token-create.png b/docs/images/app-connections/railway/railway-app-connection-project-token-create.png new file mode 100644 index 000000000..839336cdd Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-project-token-create.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-project-token-created.png b/docs/images/app-connections/railway/railway-app-connection-project-token-created.png new file mode 100644 index 000000000..5588f89d5 Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-project-token-created.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-project-token-dashboard.png b/docs/images/app-connections/railway/railway-app-connection-project-token-dashboard.png new file mode 100644 index 000000000..0ec110a91 Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-project-token-dashboard.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-project-token-form.png b/docs/images/app-connections/railway/railway-app-connection-project-token-form.png new file mode 100644 index 000000000..a16c6a2e7 Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-project-token-form.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-project-token-project.png b/docs/images/app-connections/railway/railway-app-connection-project-token-project.png new file mode 100644 index 000000000..b938cbe9e Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-project-token-project.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-project-token-settings.png b/docs/images/app-connections/railway/railway-app-connection-project-token-settings.png new file mode 100644 index 000000000..ae59c588a Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-project-token-settings.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-team-token-create.png b/docs/images/app-connections/railway/railway-app-connection-team-token-create.png new file mode 100644 index 000000000..d5c368e83 Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-team-token-create.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-team-token-created.png b/docs/images/app-connections/railway/railway-app-connection-team-token-created.png new file mode 100644 index 000000000..4c3ef583d Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-team-token-created.png differ diff --git a/docs/images/app-connections/railway/railway-app-connection-team-token-form.png b/docs/images/app-connections/railway/railway-app-connection-team-token-form.png new file mode 100644 index 000000000..be7e10710 Binary files /dev/null and b/docs/images/app-connections/railway/railway-app-connection-team-token-form.png differ diff --git a/docs/images/integrations/aws/irsa-create-oidc-provider.png b/docs/images/integrations/aws/irsa-create-oidc-provider.png new file mode 100644 index 000000000..aff8d600e Binary files /dev/null and b/docs/images/integrations/aws/irsa-create-oidc-provider.png differ diff --git a/docs/images/integrations/aws/irsa-iam-role-creation.png b/docs/images/integrations/aws/irsa-iam-role-creation.png new file mode 100644 index 000000000..8fd725bca Binary files /dev/null and b/docs/images/integrations/aws/irsa-iam-role-creation.png differ diff --git a/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-irsa.png b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-irsa.png new file mode 100644 index 000000000..0051b0cf4 Binary files /dev/null and b/docs/images/platform/dynamic-secrets/dynamic-secret-setup-modal-aws-iam-irsa.png differ diff --git a/docs/images/secret-syncs/railway/railway-sync-created.png b/docs/images/secret-syncs/railway/railway-sync-created.png new file mode 100644 index 000000000..0b965ed5c Binary files /dev/null and b/docs/images/secret-syncs/railway/railway-sync-created.png differ diff --git a/docs/images/secret-syncs/railway/railway-sync-destination.png b/docs/images/secret-syncs/railway/railway-sync-destination.png new file mode 100644 index 000000000..2c401ac3d Binary files /dev/null and b/docs/images/secret-syncs/railway/railway-sync-destination.png differ diff --git a/docs/images/secret-syncs/railway/railway-sync-details.png b/docs/images/secret-syncs/railway/railway-sync-details.png new file mode 100644 index 000000000..9667ac8fb Binary files /dev/null and b/docs/images/secret-syncs/railway/railway-sync-details.png differ diff --git a/docs/images/secret-syncs/railway/railway-sync-options.png b/docs/images/secret-syncs/railway/railway-sync-options.png new file mode 100644 index 000000000..874b4c866 Binary files /dev/null and b/docs/images/secret-syncs/railway/railway-sync-options.png differ diff --git a/docs/images/secret-syncs/railway/railway-sync-review.png b/docs/images/secret-syncs/railway/railway-sync-review.png new file mode 100644 index 000000000..8f381d59e Binary files /dev/null and b/docs/images/secret-syncs/railway/railway-sync-review.png differ diff --git a/docs/images/secret-syncs/railway/railway-sync-source.png b/docs/images/secret-syncs/railway/railway-sync-source.png new file mode 100644 index 000000000..25cfe51d7 Binary files /dev/null and b/docs/images/secret-syncs/railway/railway-sync-source.png differ diff --git a/docs/images/secret-syncs/railway/select-option.png b/docs/images/secret-syncs/railway/select-option.png new file mode 100644 index 000000000..54f68a959 Binary files /dev/null and b/docs/images/secret-syncs/railway/select-option.png differ diff --git a/docs/integrations/app-connections/railway.mdx b/docs/integrations/app-connections/railway.mdx new file mode 100644 index 000000000..7b53d02ad --- /dev/null +++ b/docs/integrations/app-connections/railway.mdx @@ -0,0 +1,164 @@ +--- +title: "Railway Connection" +description: "Learn how to configure a Railway Connection for Infisical." +--- + +Infisical supports the use of [API Tokens](https://docs.railway.com/guides/public-api#creating-a-token) to connect with Railway. + +## Create a Railway API Token + + + + A team token provides access to all resources within a team. It cannot be used to access personal resources in Railway. + + + + ![Dashboard Page](/images/app-connections/railway/railway-app-connection-account-settings.png) + + + ![Account Settings Page](/images/app-connections/railway/railway-app-connection-account-settings-tokens.png) + + + Make sure to provide a descriptive name and select the correct team. + + ![Enter Name and Select Team](/images/app-connections/railway/railway-app-connection-team-token-form.png) + + + ![Create Token](/images/app-connections/railway/railway-app-connection-team-token-create.png) + + + After clicking 'Create', your access token will be displayed. Save it securely for later use. + + ![Copy Token Modal](/images/app-connections/railway/railway-app-connection-team-token-created.png) + + + + + + If no team is selected, the token will be associated with your personal Railway account and will have access to all your individual and team resources. + + + + ![Dashboard Page](/images/app-connections/railway/railway-app-connection-account-settings.png) + + + ![Account Settings Page](/images/app-connections/railway/railway-app-connection-account-settings-tokens.png) + + + Provide a descriptive name and ensure no team is selected. This will create an account-level token. + + ![Enter Name](/images/app-connections/railway/railway-app-connection-account-token-form.png) + + + ![Create Token](/images/app-connections/railway/railway-app-connection-account-token-create.png) + + + After clicking 'Create', your access token will be shown. Save it for future use. + + ![Copy Token Modal](/images/app-connections/railway/railway-app-connection-account-token-created.png) + + + + + + Project tokens are limited to a specific environment within a project and can only be used to authenticate requests to that environment. + + + + ![Dashboard Page](/images/app-connections/railway/railway-app-connection-project-token-dashboard.png) + + + ![Project Settings Page](/images/app-connections/railway/railway-app-connection-project-token-project.png) + + + ![Project Token Settings Page](/images/app-connections/railway/railway-app-connection-project-token-settings.png) + + + Provide a descriptive name and select the appropriate environment for the token. + + ![Enter Name and Select environment](/images/app-connections/railway/railway-app-connection-project-token-form.png) + + + ![Create Token](/images/app-connections/railway/railway-app-connection-project-token-create.png) + + + After clicking 'Create', the access token will be displayed. Be sure to save it for later use. + + ![Copy Token Modal](/images/app-connections/railway/railway-app-connection-project-token-created.png) + + + + + +## Create a Railway Connection in Infisical + + + + + + In your Infisical dashboard, go to **Organization Settings** and open the [**App Connections**](https://app.infisical.com/organization/app-connections) tab. + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click **+ Add Connection** and choose **Railway Connection** from the list of integrations. + + ![Select Railway Connection](/images/app-connections/railway/railway-app-connection-option.png) + + + Complete the form by providing: + - A descriptive name for the connection + - An optional description + - The type of token you created earlier + - The token value from the previous step + + ![Railway Connection Modal](/images/app-connections/railway/railway-app-connection-form.png) + + + After submitting the form, your **Railway Connection** will be successfully created and ready to use with your Infisical projects. + + ![Railway Connection Created](/images/app-connections/railway/railway-app-connection-generated.png) + + + + + + To create a Railway Connection via API, send a request to the [Create Railway Connection](/api-reference/endpoints/app-connections/railway/create) endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/railway \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-railway-connection", + "method": "team-token", + "credentials": { + "apiToken": "[TEAM TOKEN]" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", + "name": "my-railway-connection", + "description": null, + "version": 1, + "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", + "createdAt": "2025-04-23T19:46:34.831Z", + "updatedAt": "2025-04-23T19:46:34.831Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "7c2d371dec195f82a6a0d5b41c970a229cfcaf88e894a5b6395e2dbd0280661f", + "app": "railway", + "method": "team-token", + "credentials": {} + } + } + ``` + + diff --git a/docs/integrations/secret-syncs/railway.mdx b/docs/integrations/secret-syncs/railway.mdx new file mode 100644 index 000000000..d0d546984 --- /dev/null +++ b/docs/integrations/secret-syncs/railway.mdx @@ -0,0 +1,171 @@ +--- +title: "Railway Sync" +description: "Learn how to configure a Railway Sync for Infisical." +--- + +**Prerequisites:** +- Create a [Railway Connection](/integrations/app-connections/railway) + + + + + + Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + + ![Select Railway](/images/secret-syncs/railway/select-option.png) + + + Configure the **Source** from where secrets should be retrieved, then click **Next**. + + ![Configure Source](/images/secret-syncs/railway/railway-sync-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + + Configure the **Destination** to where secrets should be deployed, then click **Next**. + + ![Configure Destination](/images/secret-syncs/railway/railway-sync-destination.png) + + - **Railway Connection**: The Railway Connection to authenticate with. + - **Project**: The Railway project to sync secrets to. + - **Environment**: The Railway environment to sync secrets to. + - **Service**: The Service to sync secrets to. + - **If not provided**: Secrets will be synced as [shared variables](https://docs.railway.com/guides/variables#shared-variables) on Railway. + + + Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + + ![Configure Options](/images/secret-syncs/railway/railway-sync-options.png) + + - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Railway when keys conflict. + - **Import Secrets (Prioritize Railway)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Railway over Infisical when keys conflict. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name and `{{environment}}` for the environment. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. + - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. + + + Configure the **Details** of your Railway Sync, then click **Next**. + + ![Configure Details](/images/secret-syncs/railway/railway-sync-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + + Review your Railway Sync configuration, then click **Create Sync**. + + ![Review Configuration](/images/secret-syncs/railway/railway-sync-review.png) + + + If enabled, your Railway Sync will begin syncing your secrets to the destination endpoint. + + ![Sync Created](/images/secret-syncs/railway/railway-sync-created.png) + + + + + To create a **Railway Sync**, make an API request to the [Create Railway Sync](/api-reference/endpoints/secret-syncs/railway/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/railway \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-railway-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/my-secrets", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination", + "autoSyncEnabled": true, + "disableSecretDeletion": false + }, + "destinationConfig": { + "projectId": "dev-project-id", + "projectName": "Development Project", + "environmentId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environmentName": "Development", + "serviceId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "serviceName": "my-railway-service", + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-railway-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination", + "autoSyncEnabled": true, + "disableSecretDeletion": false + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "railway", + "name": "my-railway-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/my-secrets" + }, + "destination": "railway", + "destinationConfig": { + "projectId": "dev-project-id", + "projectName": "Development Project", + "environmentId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environmentName": "Development", + "serviceId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "serviceName": "my-railway-service", + } + } + } + ``` + + diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 42d289804..1e050ff14 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -59,6 +59,15 @@ Example values: connect with internal/private IP addresses. + + Determines whether your Infisical instance can automatically read the service account token of the pod it's running on. Used for features such as the IRSA auth method. + + ## CORS Cross-Origin Resource Sharing (CORS) is a security feature that allows web applications running on one domain to access resources from another domain. diff --git a/docs/self-hosting/deployment-options/docker-compose.mdx b/docs/self-hosting/deployment-options/docker-compose.mdx index 440811e65..65a0f05dd 100644 --- a/docs/self-hosting/deployment-options/docker-compose.mdx +++ b/docs/self-hosting/deployment-options/docker-compose.mdx @@ -4,17 +4,20 @@ description: "Read how to run Infisical with Docker Compose template." --- This self-hosting guide will walk you through the steps to self-host Infisical using Docker Compose. -## Prerequisites -- [Docker](https://docs.docker.com/engine/install/) -- [Docker compose](https://docs.docker.com/compose/install/) - -This Docker Compose configuration is not designed for high-availability production scenarios. -It includes just the essential components needed to set up an Infisical proof of concept (POC). -To run Infisical in a highly available manner, give the [Docker Swarm guide](/self-hosting/deployment-options/docker-swarm). - + + + ## Prerequisites + - [Docker](https://docs.docker.com/engine/install/) + - [Docker compose](https://docs.docker.com/compose/install/) -## Verify prerequisites + + This Docker Compose configuration is not designed for high-availability production scenarios. + It includes just the essential components needed to set up an Infisical proof of concept (POC). + To run Infisical in a highly available manner, give the [Docker Swarm guide](/self-hosting/deployment-options/docker-swarm). + + + ## Verify prerequisites To verify that Docker compose and Docker are installed on the machine where you plan to install Infisical, run the following commands. Check for docker installation @@ -27,55 +30,145 @@ To run Infisical in a highly available manner, give the [Docker Swarm guide](/se docker-compose ``` -## Download docker compose file -You can obtain the Infisical docker compose file by using a command-line downloader such as `wget` or `curl`. -If your system doesn't have either of these, you can use a equivalent command that works with your machine. + ## Download docker compose file + You can obtain the Infisical docker compose file by using a command-line downloader such as `wget` or `curl`. + If your system doesn't have either of these, you can use a equivalent command that works with your machine. + + + + ```bash + curl -o docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml + ``` + + + ```bash + wget -O docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml + ``` + + + + ## Configure instance credentials + Infisical requires a set of credentials used for connecting to dependent services such as Postgres, Redis, etc. + The default credentials can be downloaded using the one of the commands listed below. + + + + ```bash + curl -o .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example + ``` + + + ```bash + wget -O .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example + ``` + + + + Once downloaded, the credentials file will be saved to your working directly as `.env` file. + View all available configurations [here](/self-hosting/configuration/envars). + + + The default .env file contains credentials that are intended solely for testing purposes. + Please generate a new `ENCRYPTION_KEY` and `AUTH_SECRET` for use outside of testing. + Instructions to do so, can be found [here](/self-hosting/configuration/envars). + + + ## Start Infisical + Run the command below to start Infisical and all related services. - - ```bash - curl -o docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml + docker-compose -f docker-compose.prod.yml up ``` + - - ```bash - wget -O docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml + + Podman Compose is an alternative way to run Infisical using Podman as a replacement for Docker. Podman is backwards compatible with Docker Compose files. + + ## Prerequisites + - [Podman](https://podman-desktop.io/docs/installation) + - [Podman Compose](https://podman-desktop.io/docs/compose) + + + This Docker Compose configuration is not designed for high-availability production scenarios. + It includes just the essential components needed to set up an Infisical proof of concept (POC). + To run Infisical in a highly available manner, give the [Docker Swarm guide](/self-hosting/deployment-options/docker-swarm). + + + + ## Verify prerequisites + To verify that Podman compose and Podman are installed on the machine where you plan to install Infisical, run the following commands. + + Check for podman installation + ```bash + podman version + ``` + + Check for podman compose installation + ```bash + podman-compose version + ``` + + ## Download Docker Compose file + You can obtain the Infisical docker compose file by using a command-line downloader such as `wget` or `curl`. + If your system doesn't have either of these, you can use a equivalent command that works with your machine. + + + + ```bash + curl -o docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml + ``` + + + ```bash + wget -O docker-compose.prod.yml https://raw.githubusercontent.com/Infisical/infisical/main/docker-compose.prod.yml + ``` + + + + ## Configure instance credentials + Infisical requires a set of credentials used for connecting to dependent services such as Postgres, Redis, etc. + The default credentials can be downloaded using the one of the commands listed below. + + + + ```bash + curl -o .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example + ``` + + + ```bash + wget -O .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example + ``` + + + + + Make sure to rename the `.env.example` file to `.env` before starting Infisical. Additionally it's important that the `.env` file is in the same directory as the `docker-compose.prod.yml` file. + + + ## Setup Podman + Run the commands below to setup Podman for first time use. + ```bash + podman machine init --now + podman machine set --rootful + podman machine start + ``` + + + If you are using a rootless podman installation, you can skip the `podman machine set --rootful` command. + + + ## Start Infisical + Run the command below to start Infisical and all related services. + + ```bash + podman-compose -f docker-compose.prod.yml up ``` -## Configure instance credentials -Infisical requires a set of credentials used for connecting to dependent services such as Postgres, Redis, etc. -The default credentials can be downloaded using the one of the commands listed below. - - - ```bash - curl -o .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example - ``` - - - ```bash - wget -O .env https://raw.githubusercontent.com/Infisical/infisical/main/.env.example - ``` - - -Once downloaded, the credentials file will be saved to your working directly as `.env` file. -View all available configurations [here](/self-hosting/configuration/envars). - - - The default .env file contains credentials that are intended solely for testing purposes. - Please generate a new `ENCRYPTION_KEY` and `AUTH_SECRET` for use outside of testing. - Instructions to do so, can be found [here](/self-hosting/configuration/envars). - - -## Start Infisical -Run the command below to start Infisical and all related services. - -```bash -docker-compose -f docker-compose.prod.yml up -``` Your Infisical instance should now be running on port `80`. To access your instance, visit `http://localhost:80`. diff --git a/frontend/public/lotties/wrench.json b/frontend/public/lotties/wrench.json new file mode 100644 index 000000000..be373f39b --- /dev/null +++ b/frontend/public/lotties/wrench.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":60,"w":500,"h":500,"nm":"system-regular-22-build","ddd":0,"assets":[{"id":"comp_1","nm":"hover-build","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[1],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[1],"y":[0]},"t":10,"s":[-45]},{"i":{"x":[0.26],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":19,"s":[-45]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.74],"y":[0]},"t":28,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":37,"s":[-45]},{"i":{"x":[0.101],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":46,"s":[-45]},{"i":{"x":[0.101],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":52,"s":[7]},{"t":58,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":1,"s":[314.5,185,0],"to":[0,-0.833,0],"ti":[0,0.833,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":10,"s":[314.5,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.26,"y":1},"o":{"x":0.333,"y":0},"t":19,"s":[314.5,180,0],"to":[0,0.833,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.74,"y":0},"t":28,"s":[314.5,185,0],"to":[0,0,0],"ti":[0,0.833,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.167,"y":0.167},"t":37,"s":[314.5,180,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":46,"s":[314.5,180,0],"to":[0,0.833,0],"ti":[0,-0.833,0]},{"t":52,"s":[314.5,185,0]}],"ix":2,"l":2},"a":{"a":0,"k":[314.5,185,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-0.104,0],[0.104,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.914,0.91,0.91,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-22-build').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":41.73,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"fl","c":{"a":0,"k":[0.914,0.91,0.91,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-22-build').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[145.836,354.17],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.113,40.698],[0,0],[0,0],[0,0],[34.989,-34.987],[-8.119,-34.725],[0,0],[-16.272,-16.272],[0,0],[-16.271,16.272],[0,0],[-27.153,27.152]],"o":[[0,0],[0,0],[0,0],[-40.697,-22.839],[-27.152,27.153],[0,0],[-16.272,16.272],[0,0],[16.272,16.272],[0,0],[34.725,8.118],[34.622,-34.624]],"v":[[159.225,-115.686],[75.4,-31.862],[32.739,-74.523],[116.745,-158.53],[-11.514,-140.28],[-40.441,-40.189],[-159.877,79.247],[-159.877,138.173],[-138.203,159.847],[-79.278,159.847],[40.159,40.412],[140.249,11.484]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.914,0.91,0.91,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-22-build').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":59,"st":-8,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.041,250.001,0],"ix":2,"l":2},"a":{"a":0,"k":[250.002,250,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.55,0],[0,0.55],[-0.55,0],[0,0],[0,-0.55]],"o":[[-0.55,0],[0,-0.55],[0,0],[0.55,0],[0,0.56]],"v":[[0,1],[-1,0],[-0.01,-1],[0,-1],[1,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.914,0.91,0.91,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-22-build').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[245.01,254.99],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.39,0],[0.86,-0.85],[-0.34,-1.46],[0.18,-0.18],[0,0],[0,-0.33],[-0.24,-0.23],[0,0],[-0.47,0.47],[0,0],[-0.25,-0.06],[-1.08,1.08],[0,0],[0.39,1.49],[0,0],[0.29,0.29],[0,0],[-0.29,0.29],[0,0]],"o":[[-1.14,0],[-1.07,1.07],[0.06,0.25],[0,0],[-0.24,0.24],[0,0.33],[0,0],[0.47,0.47],[0,0],[0.18,-0.18],[1.45,0.34],[0,0],[1.14,-1.14],[0,0],[-0.29,0.29],[0,0],[-0.29,-0.29],[0,0],[-0.39,-0.1]],"v":[[3.108,-7.5],[-0.032,-6.2],[-1.222,-2.1],[-1.422,-1.4],[-7.142,4.33],[-7.512,5.21],[-7.142,6.09],[-6.102,7.13],[-4.342,7.13],[1.378,1.4],[2.078,1.2],[6.178,0.01],[6.178,0.01],[7.348,-4.23],[4.138,-1],[3.078,-1],[1.038,-3.05],[1.038,-4.11],[4.278,-7.35]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.74,0],[0.52,0.52],[0,0],[0,0.74],[-0.51,0.52],[0,0],[-1.34,1.33],[-2.28,-1.28],[-0.04,-0.23],[0.16,-0.16],[0,0],[0,0],[0,0],[-0.24,-0.04],[-0.11,-0.2],[1.87,-1.88],[1.83,0.28],[0,0]],"o":[[-0.73,0],[0,0],[-0.51,-0.51],[0,-0.73],[0,0],[-0.29,-1.83],[1.87,-1.87],[0.2,0.11],[0.03,0.23],[0,0],[0,0],[0,0],[0.17,-0.17],[0.23,0.03],[1.25,2.31],[-1.33,1.33],[0,0],[-0.52,0.51]],"v":[[-5.222,9],[-7.162,8.19],[-8.202,7.15],[-9.002,5.21],[-8.202,3.27],[-2.762,-2.18],[-1.092,-7.26],[5.948,-8.26],[6.328,-7.71],[6.118,-7.08],[2.618,-3.58],[3.608,-2.59],[7.098,-6.08],[7.738,-6.29],[8.288,-5.91],[7.248,1.08],[2.168,2.75],[-3.272,8.2]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.914,0.91,0.91,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-22-build').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.002,250],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":-59,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.041,250.001,0],"ix":2,"l":2},"a":{"a":0,"k":[250.002,250,0],"ix":1,"l":2},"s":{"a":0,"k":[2083,2083,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.55,0],[0,0.55],[-0.55,0],[0,0],[0,-0.55]],"o":[[-0.55,0],[0,-0.55],[0,0],[0.55,0],[0,0.56]],"v":[[0,1],[-1,0],[-0.01,-1],[0,-1],[1,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.914,0.91,0.91,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-22-build').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[245.01,254.99],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.39,0],[0.86,-0.85],[-0.34,-1.46],[0.18,-0.18],[0,0],[0,-0.33],[-0.24,-0.23],[0,0],[-0.47,0.47],[0,0],[-0.25,-0.06],[-1.08,1.08],[0,0],[0.39,1.49],[0,0],[0.29,0.29],[0,0],[-0.29,0.29],[0,0]],"o":[[-1.14,0],[-1.07,1.07],[0.06,0.25],[0,0],[-0.24,0.24],[0,0.33],[0,0],[0.47,0.47],[0,0],[0.18,-0.18],[1.45,0.34],[0,0],[1.14,-1.14],[0,0],[-0.29,0.29],[0,0],[-0.29,-0.29],[0,0],[-0.39,-0.1]],"v":[[3.108,-7.5],[-0.032,-6.2],[-1.222,-2.1],[-1.422,-1.4],[-7.142,4.33],[-7.512,5.21],[-7.142,6.09],[-6.102,7.13],[-4.342,7.13],[1.378,1.4],[2.078,1.2],[6.178,0.01],[6.178,0.01],[7.348,-4.23],[4.138,-1],[3.078,-1],[1.038,-3.05],[1.038,-4.11],[4.278,-7.35]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.74,0],[0.52,0.52],[0,0],[0,0.74],[-0.51,0.52],[0,0],[-1.34,1.33],[-2.28,-1.28],[-0.04,-0.23],[0.16,-0.16],[0,0],[0,0],[0,0],[-0.24,-0.04],[-0.11,-0.2],[1.87,-1.88],[1.83,0.28],[0,0]],"o":[[-0.73,0],[0,0],[-0.51,-0.51],[0,-0.73],[0,0],[-0.29,-1.83],[1.87,-1.87],[0.2,0.11],[0.03,0.23],[0,0],[0,0],[0,0],[0.17,-0.17],[0.23,0.03],[1.25,2.31],[-1.33,1.33],[0,0],[-0.52,0.51]],"v":[[-5.222,9],[-7.162,8.19],[-8.202,7.15],[-9.002,5.21],[-8.202,3.27],[-2.762,-2.18],[-1.092,-7.26],[5.948,-8.26],[6.328,-7.71],[6.118,-7.08],[2.618,-3.58],[3.608,-2.59],[7.098,-6.08],[7.738,-6.29],[8.288,-5.91],[7.248,1.08],[2.168,2.75],[-3.272,8.2]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.914,0.91,0.91,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-22-build').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.002,250],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":59,"op":300,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[0.914,0.91,0.91],"ix":1}}]}],"ip":0,"op":131,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-build","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":70,"st":0,"bm":0}],"markers":[{"tm":0,"cm":"default:hover-build","dr":60}],"props":{}} \ No newline at end of file diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RailwaySyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RailwaySyncFields.tsx new file mode 100644 index 000000000..6095f5763 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/RailwaySyncFields.tsx @@ -0,0 +1,138 @@ +import { useMemo } from "react"; +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl } from "@app/components/v2"; +import { + TRailwayProject, + useRailwayConnectionListProjects +} from "@app/hooks/api/appConnections/railway"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const RailwaySyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.Railway } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + const projectId = useWatch({ name: "destinationConfig.projectId", control }); + + const { data: projects = [], isPending: isProjectsLoading } = useRailwayConnectionListProjects( + connectionId, + { + enabled: Boolean(connectionId) + } + ); + + const environments = useMemo(() => { + return projects.find((p) => p.id === projectId)?.environments ?? []; + }, [projects, projectId]); + + const services = useMemo(() => { + return projects.find((p) => p.id === projectId)?.services ?? []; + }, [projects, projectId]); + + return ( + <> + { + setValue("destinationConfig.environmentId", ""); + setValue("destinationConfig.projectId", ""); + setValue("destinationConfig.serviceId", ""); + setValue("destinationConfig.projectName", ""); + setValue("destinationConfig.environmentName", ""); + setValue("destinationConfig.serviceName", ""); + }} + /> + ( + + p.id === value) ?? null} + onChange={(option) => { + const v = option as SingleValue; + onChange(v?.id ?? null); + setValue("destinationConfig.projectName", v?.name ?? ""); + }} + options={projects} + placeholder="Select a project..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> + + )} + /> + ( + + p.id === value) ?? null} + onChange={(option) => { + const v = option as SingleValue; + onChange(v?.id ?? null); + setValue("destinationConfig.environmentName", v?.name ?? ""); + }} + options={environments} + placeholder="Select an environment..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> + + )} + /> + + ( + + p.id === value) ?? null} + onChange={(option) => { + const v = option as SingleValue; + onChange(v?.id ?? null); + setValue("destinationConfig.serviceName", v?.name ?? ""); + }} + options={services} + placeholder="Select a service..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index b50b2c00f..cc732728c 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -21,6 +21,7 @@ import { HCVaultSyncFields } from "./HCVaultSyncFields"; import { HerokuSyncFields } from "./HerokuSyncFields"; import { HumanitecSyncFields } from "./HumanitecSyncFields"; import { OCIVaultSyncFields } from "./OCIVaultSyncFields"; +import { RailwaySyncFields } from "./RailwaySyncFields"; import { RenderSyncFields } from "./RenderSyncFields"; import { TeamCitySyncFields } from "./TeamCitySyncFields"; import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields"; @@ -82,6 +83,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.Zabbix: return ; + case SecretSync.Railway: + return ; default: throw new Error(`Unhandled Destination Config Field: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index f05b2d67e..c86f63c20 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -60,6 +60,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.CloudflarePages: case SecretSync.CloudflareWorkers: case SecretSync.Zabbix: + case SecretSync.Railway: AdditionalSyncOptionsFieldsComponent = null; break; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RailwaySyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RailwaySyncReviewFields.tsx new file mode 100644 index 000000000..717ad9ae7 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/RailwaySyncReviewFields.tsx @@ -0,0 +1,29 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { useRailwayConnectionListProjects } from "@app/hooks/api/appConnections/railway"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const RailwaySyncReviewFields = () => { + const { watch } = useFormContext(); + const connectionId = watch("connection.id"); + const projectId = watch("destinationConfig.projectId"); + const environmentId = watch("destinationConfig.environmentId"); + + const { data: projects = [] } = useRailwayConnectionListProjects(connectionId, { + enabled: Boolean(connectionId) + }); + + const project = projects.find((p) => p.id === projectId); + const environment = project?.environments.find((e) => e.id === environmentId); + + return ( + <> + {project?.name ?? projectId} + + {environment?.name ?? environmentId} + + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index b5ebda7fa..52dc7fd02 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -31,6 +31,7 @@ import { HerokuSyncReviewFields } from "./HerokuSyncReviewFields"; import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields"; import { OCIVaultSyncReviewFields } from "./OCIVaultSyncReviewFields"; import { OnePassSyncReviewFields } from "./OnePassSyncReviewFields"; +import { RailwaySyncReviewFields } from "./RailwaySyncReviewFields"; import { RenderSyncReviewFields } from "./RenderSyncReviewFields"; import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields"; import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields"; @@ -132,6 +133,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.Zabbix: DestinationFieldsComponent = ; break; + case SecretSync.Railway: + DestinationFieldsComponent = ; + break; default: throw new Error(`Unhandled Destination Review Fields: ${destination}`); } diff --git a/frontend/src/components/secret-syncs/forms/schemas/railway-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/railway-sync-destination-schema.ts new file mode 100644 index 000000000..2870d6087 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/railway-sync-destination-schema.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const RailwaySyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.Railway), + destinationConfig: z.object({ + projectId: z.string().min(1, "Project ID is required"), + projectName: z.string(), + environmentName: z.string(), + environmentId: z.string().min(1, "Environment is required"), + serviceId: z.string().optional(), + serviceName: z.string().optional() + }) + }) +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index 2a3b802d3..82a8fa2ff 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -18,6 +18,7 @@ import { HCVaultSyncDestinationSchema } from "./hc-vault-sync-destination-schema import { HerokuSyncDestinationSchema } from "./heroku-sync-destination-schema"; import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema"; import { OCIVaultSyncDestinationSchema } from "./oci-vault-sync-destination-schema"; +import { RailwaySyncDestinationSchema } from "./railway-sync-destination-schema"; import { RenderSyncDestinationSchema } from "./render-sync-destination-schema"; import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schema"; import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema"; @@ -49,7 +50,9 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ GitlabSyncDestinationSchema, CloudflarePagesSyncDestinationSchema, CloudflareWorkersSyncDestinationSchema, - ZabbixSyncDestinationSchema + + ZabbixSyncDestinationSchema, + RailwaySyncDestinationSchema ]); export const SecretSyncFormSchema = SecretSyncUnionSchema; diff --git a/frontend/src/components/utilities/parseSecrets.ts b/frontend/src/components/utilities/parseSecrets.ts index d1df7cd68..3e5a57edf 100644 --- a/frontend/src/components/utilities/parseSecrets.ts +++ b/frontend/src/components/utilities/parseSecrets.ts @@ -77,3 +77,91 @@ export const parseJson = (src: ArrayBuffer | string) => { }); return env; }; + +/** + * Parses simple flat YAML with support for multiline strings using |, |-, and >. + * @param {ArrayBuffer | string} src + * @returns {Record} + */ +export function parseYaml(src: ArrayBuffer | string) { + const result: Record = {}; + + const content = src.toString().replace(/\r\n?/g, "\n"); + const lines = content.split("\n"); + + let i = 0; + let comments: string[] = []; + + while (i < lines.length) { + const line = lines[i].trim(); + + // Collect comment + if (line.startsWith("#")) { + comments.push(line.slice(1).trim()); + i += 1; // move to next line + } else { + // Match key: value or key: |, key: >, etc. + const keyMatch = lines[i].match(/^([\w.-]+)\s*:\s*(.*)$/); + if (keyMatch) { + const [, key, rawValue] = keyMatch; + let value = rawValue.trim(); + + // Multiline string handling + if (value === "|-" || value === "|" || value === ">") { + const isFolded = value === ">"; + + const baseIndent = lines[i + 1]?.match(/^(\s*)/)?.[1]?.length ?? 0; + const collectedLines: string[] = []; + + i += 1; // move to first content line + + while (i < lines.length) { + const current = lines[i]; + const currentIndent = current.match(/^(\s*)/)?.[1]?.length ?? 0; + + if (current.trim() === "" || currentIndent >= baseIndent) { + collectedLines.push(current.slice(baseIndent)); + i += 1; // move to next line + } else { + break; + } + } + + if (isFolded) { + // Join lines with space for `>` folded style + value = collectedLines.map((l) => l.trim()).join(" "); + } else { + // Keep lines with newlines for `|` and `|-` + value = collectedLines.join("\n"); + } + } else { + // Inline value — strip quotes and inline comment + const commentIndex = value.indexOf(" #"); + if (commentIndex !== -1) { + value = value.slice(0, commentIndex).trim(); + } + + // Remove surrounding quotes + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + + i += 1; // advance to next line + } + + result[key] = { + value, + comments: [...comments] + }; + comments = []; // reset + } else { + i += 1; // skip unknown line + } + } + } + + return result; +} diff --git a/frontend/src/config/env.ts b/frontend/src/config/env.ts index 296c6a87d..63446c95f 100644 --- a/frontend/src/config/env.ts +++ b/frontend/src/config/env.ts @@ -26,6 +26,7 @@ export const envConfig = { import.meta.env.VITE_TELEMETRY_CAPTURING_ENABLED === true ); }, + get PLATFORM_VERSION() { return import.meta.env.VITE_INFISICAL_PLATFORM_VERSION; } diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index fa6f11bbb..900067180 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -43,6 +43,7 @@ import { import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/bitbucket-connection"; import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection"; import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-connection"; +import { RailwayConnectionMethod } from "@app/hooks/api/appConnections/types/railway-connection"; import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection"; export const APP_CONNECTION_MAP: Record< @@ -91,8 +92,9 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" }, [AppConnection.Gitlab]: { name: "GitLab", image: "GitLab.png" }, [AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" }, - [AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" }, - [AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" } + [AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" }, + [AppConnection.Railway]: { name: "Railway", image: "Railway.png" }, + [AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" } }; export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => { @@ -146,6 +148,12 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) return { name: "Simple Bind", icon: faLink }; case HerokuConnectionMethod.AuthToken: return { name: "Auth Token", icon: faKey }; + case RailwayConnectionMethod.AccountToken: + return { name: "Account Token", icon: faKey }; + case RailwayConnectionMethod.TeamToken: + return { name: "Team Token", icon: faKey }; + case RailwayConnectionMethod.ProjectToken: + return { name: "Project Token", icon: faKey }; case RenderConnectionMethod.ApiKey: return { name: "API Key", icon: faKey }; default: diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index be96e85e4..8e9bda613 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -89,6 +89,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.GitLab]: AppConnection.Gitlab, [SecretSync.CloudflarePages]: AppConnection.Cloudflare, [SecretSync.CloudflareWorkers]: AppConnection.Cloudflare, - [SecretSync.Zabbix]: AppConnection.Zabbix + + [SecretSync.Zabbix]: AppConnection.Zabbix, + [SecretSync.Railway]: AppConnection.Railway }; export const SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP: Record< diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index cde04ee61..70e9b883e 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -170,7 +170,7 @@ export type TCreateAccessPolicyDTO = { approvers?: Approver[]; bypassers?: Bypasser[]; approvals?: number; - secretPath?: string; + secretPath: string; enforcementLevel?: EnforcementLevel; allowedSelfApprovals: boolean; approvalsRequired?: { numberOfApprovals: number; stepNumber: number }[]; diff --git a/frontend/src/hooks/api/admin/types.ts b/frontend/src/hooks/api/admin/types.ts index 4580c6581..e7d1f6a48 100644 --- a/frontend/src/hooks/api/admin/types.ts +++ b/frontend/src/hooks/api/admin/types.ts @@ -40,6 +40,7 @@ export type TServerConfig = { trustLdapEmails: boolean; trustOidcEmails: boolean; isSecretScanningDisabled: boolean; + kubernetesAutoFetchServiceAccountToken: boolean; defaultAuthOrgSlug: string | null; defaultAuthOrgId: string | null; defaultAuthOrgAuthMethod?: string | null; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 31443c67d..e6b623995 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -29,5 +29,6 @@ export enum AppConnection { Gitlab = "gitlab", Cloudflare = "cloudflare", Bitbucket = "bitbucket", - Zabbix = "zabbix" + Zabbix = "zabbix", + Railway = "railway" } diff --git a/frontend/src/hooks/api/appConnections/railway/index.ts b/frontend/src/hooks/api/appConnections/railway/index.ts new file mode 100644 index 000000000..2c1906d36 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/railway/index.ts @@ -0,0 +1,2 @@ +export * from "./queries"; +export * from "./types"; diff --git a/frontend/src/hooks/api/appConnections/railway/queries.tsx b/frontend/src/hooks/api/appConnections/railway/queries.tsx new file mode 100644 index 000000000..0b1e0d51a --- /dev/null +++ b/frontend/src/hooks/api/appConnections/railway/queries.tsx @@ -0,0 +1,37 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; +import { appConnectionKeys } from "@app/hooks/api/appConnections"; + +import { TRailwayProject } from "./types"; + +const railwayConnectionKeys = { + all: [...appConnectionKeys.all, "railway"] as const, + listSecretScopes: (connectionId: string) => + [...railwayConnectionKeys.all, "workspace-scopes", connectionId] as const +}; + +export const useRailwayConnectionListProjects = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + TRailwayProject[], + unknown, + TRailwayProject[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: railwayConnectionKeys.listSecretScopes(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get<{ projects: TRailwayProject[] }>( + `/api/v1/app-connections/railway/${connectionId}/projects` + ); + + return data.projects; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/railway/types.ts b/frontend/src/hooks/api/appConnections/railway/types.ts new file mode 100644 index 000000000..1c1279454 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/railway/types.ts @@ -0,0 +1,12 @@ +export type TRailwayProject = { + id: string; + name: string; + environments: Array<{ + id: string; + name: string; + }>; + services: Array<{ + id: string; + name: string; + }>; +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index e7d672a34..c5e5fdda6 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -140,6 +140,10 @@ export type TZabbixConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Zabbix; }; +export type TRailwayConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.Railway; +}; + export type TAppConnectionOption = | TAwsConnectionOption | TGitHubConnectionOption @@ -169,7 +173,8 @@ export type TAppConnectionOption = | TGitlabConnectionOption | TCloudflareConnectionOption | TBitbucketConnectionOption - | TZabbixConnectionOption; + | TZabbixConnectionOption + | TRailwayConnectionOption; export type TAppConnectionOptionMap = { [AppConnection.AWS]: TAwsConnectionOption; @@ -203,4 +208,5 @@ export type TAppConnectionOptionMap = { [AppConnection.Cloudflare]: TCloudflareConnectionOption; [AppConnection.Bitbucket]: TBitbucketConnectionOption; [AppConnection.Zabbix]: TZabbixConnectionOption; + [AppConnection.Railway]: TRailwayConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index 4aabde9ad..524b988d7 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -25,6 +25,7 @@ import { TMySqlConnection } from "./mysql-connection"; import { TOCIConnection } from "./oci-connection"; import { TOracleDBConnection } from "./oracledb-connection"; import { TPostgresConnection } from "./postgres-connection"; +import { TRailwayConnection } from "./railway-connection"; import { TRenderConnection } from "./render-connection"; import { TTeamCityConnection } from "./teamcity-connection"; import { TTerraformCloudConnection } from "./terraform-cloud-connection"; @@ -95,7 +96,8 @@ export type TAppConnection = | TGitLabConnection | TCloudflareConnection | TBitbucketConnection - | TZabbixConnection; + | TZabbixConnection + | TRailwayConnection; export type TAvailableAppConnection = Pick; @@ -154,4 +156,5 @@ export type TAppConnectionMap = { [AppConnection.Cloudflare]: TCloudflareConnection; [AppConnection.Bitbucket]: TBitbucketConnection; [AppConnection.Zabbix]: TZabbixConnection; + [AppConnection.Railway]: TRailwayConnection; }; diff --git a/frontend/src/hooks/api/appConnections/types/railway-connection.ts b/frontend/src/hooks/api/appConnections/types/railway-connection.ts new file mode 100644 index 000000000..db961c7c3 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/railway-connection.ts @@ -0,0 +1,15 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum RailwayConnectionMethod { + AccountToken = "account-token", + ProjectToken = "project-token", + TeamToken = "team-token" +} + +export type TRailwayConnection = TRootAppConnection & { app: AppConnection.Railway } & { + method: RailwayConnectionMethod; + credentials: { + apiToken: string; + }; +}; diff --git a/frontend/src/hooks/api/dynamicSecret/types.ts b/frontend/src/hooks/api/dynamicSecret/types.ts index 4dc635fa2..84b618153 100644 --- a/frontend/src/hooks/api/dynamicSecret/types.ts +++ b/frontend/src/hooks/api/dynamicSecret/types.ts @@ -54,7 +54,8 @@ export enum SqlProviders { export enum DynamicSecretAwsIamAuth { AssumeRole = "assume-role", - AccessKey = "access-key" + AccessKey = "access-key", + IRSA = "irsa" } export type TDynamicSecretProvider = @@ -111,6 +112,14 @@ export type TDynamicSecretProvider = policyDocument?: string; userGroups?: string; policyArns?: string; + } + | { + method: DynamicSecretAwsIamAuth.IRSA; + region: string; + awsPath?: string; + policyDocument?: string; + userGroups?: string; + policyArns?: string; }; } | { diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index dfbf574e8..55aaee6ef 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { organizationKeys } from "../organization/queries"; +import { subscriptionQueryKeys } from "../subscriptions/queries"; import { identitiesKeys } from "./queries"; import { AddIdentityAliCloudAuthDTO, @@ -82,6 +83,9 @@ export const useCreateIdentity = () => { queryClient.invalidateQueries({ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) }); + queryClient.invalidateQueries({ + queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId) + }); } }); }; @@ -123,6 +127,9 @@ export const useDeleteIdentity = () => { queryClient.invalidateQueries({ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId) }); + queryClient.invalidateQueries({ + queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId) + }); } }); }; diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts index 15afcf119..eeb734115 100644 --- a/frontend/src/hooks/api/secretApproval/types.ts +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -49,7 +49,7 @@ export type TCreateSecretPolicyDTO = { workspaceId: string; name?: string; environment: string; - secretPath?: string | null; + secretPath: string; approvers?: Approver[]; bypassers?: Bypasser[]; approvals?: number; @@ -62,7 +62,7 @@ export type TUpdateSecretPolicyDTO = { name?: string; approvers?: Approver[]; bypassers?: Bypasser[]; - secretPath?: string | null; + secretPath?: string; approvals?: number; allowedSelfApprovals?: boolean; enforcementLevel?: EnforcementLevel; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index 71b57df40..c549ba351 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -22,7 +22,9 @@ export enum SecretSync { GitLab = "gitlab", CloudflarePages = "cloudflare-pages", CloudflareWorkers = "cloudflare-workers", - Zabbix = "zabbix" + + Zabbix = "zabbix", + Railway = "railway" } export enum SecretSyncStatus { diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index 762a870a4..dd300c0df 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -20,6 +20,7 @@ import { THCVaultSync } from "./hc-vault-sync"; import { THerokuSync } from "./heroku-sync"; import { THumanitecSync } from "./humanitec-sync"; import { TOCIVaultSync } from "./oci-vault-sync"; +import { TRailwaySync } from "./railway-sync"; import { TTeamCitySync } from "./teamcity-sync"; import { TTerraformCloudSync } from "./terraform-cloud-sync"; import { TVercelSync } from "./vercel-sync"; @@ -57,7 +58,8 @@ export type TSecretSync = | TGitLabSync | TCloudflarePagesSync | TCloudflareWorkersSync - | TZabbixSync; + | TZabbixSync + | TRailwaySync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/hooks/api/secretSyncs/types/railway-sync.ts b/frontend/src/hooks/api/secretSyncs/types/railway-sync.ts new file mode 100644 index 000000000..7a99bca7a --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/railway-sync.ts @@ -0,0 +1,22 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type TRailwaySync = TRootSecretSync & { + destination: SecretSync.Railway; + destinationConfig: { + projectId: string; + projectName: string; + + environmentName: string; + environmentId: string; + + serviceId?: string; + serviceName?: string; + }; + connection: { + app: AppConnection.Railway; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index ea451db02..b740b58ee 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -9,6 +9,7 @@ import { APIKeyDataV2 } from "../apiKeys/types"; import { MfaMethod } from "../auth/types"; import { TGroupWithProjectMemberships } from "../groups/types"; import { setAuthToken } from "../reactQuery"; +import { subscriptionQueryKeys } from "../subscriptions/queries"; import { workspaceKeys } from "../workspace"; import { userKeys } from "./query-keys"; import { @@ -188,6 +189,9 @@ export const useAddUsersToOrg = () => { }, onSuccess: (_, { organizationId, projects }) => { queryClient.invalidateQueries({ queryKey: userKeys.getOrgUsers(organizationId) }); + queryClient.invalidateQueries({ + queryKey: subscriptionQueryKeys.getOrgSubsription(organizationId) + }); projects?.forEach((project) => { if (project.slug) { diff --git a/frontend/src/layouts/AdminLayout/Sidebar.tsx b/frontend/src/layouts/AdminLayout/Sidebar.tsx index 807bb307f..e1cd223f8 100644 --- a/frontend/src/layouts/AdminLayout/Sidebar.tsx +++ b/frontend/src/layouts/AdminLayout/Sidebar.tsx @@ -2,7 +2,7 @@ import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Link, useMatchRoute } from "@tanstack/react-router"; -import { Menu, MenuGroup, MenuItem } from "@app/components/v2"; +import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2"; const generalTabs = [ { @@ -50,7 +50,7 @@ const resourceTabs = [ }, { label: "Machine Identities", - icon: "key-user", + icon: "wrench", link: "/admin/resources/machine-identities" } ]; @@ -61,30 +61,6 @@ export const AdminSidebar = () => { return ( ); diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx index 87e55c145..19f05f763 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx @@ -39,10 +39,6 @@ export const OrgMembersSection = () => { const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership(); const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership(); - const isMoreUsersAllowed = subscription?.memberLimit - ? subscription.membersUsed < subscription.memberLimit - : true; - const isMoreIdentitiesAllowed = subscription?.identityLimit ? subscription.identitiesUsed < subscription.identityLimit : true; @@ -58,7 +54,7 @@ export const OrgMembersSection = () => { return; } - if ((!isMoreUsersAllowed || !isMoreIdentitiesAllowed) && !isEnterprise) { + if (!isMoreIdentitiesAllowed && !isEnterprise) { handlePopUpOpen("upgradePlan", { description: "You can add more members if you upgrade your Infisical plan." }); diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index 0e080d6dd..a84ad8aa0 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -34,6 +34,7 @@ import { MySqlConnectionForm } from "./MySqlConnectionForm"; import { OCIConnectionForm } from "./OCIConnectionForm"; import { OracleDBConnectionForm } from "./OracleDBConnectionForm"; import { PostgresConnectionForm } from "./PostgresConnectionForm"; +import { RailwayConnectionForm } from "./RailwayConnectionForm"; import { RenderConnectionForm } from "./RenderConnectionForm"; import { TeamCityConnectionForm } from "./TeamCityConnectionForm"; import { TerraformCloudConnectionForm } from "./TerraformCloudConnectionForm"; @@ -140,6 +141,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Zabbix: return ; + case AppConnection.Railway: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -238,6 +241,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Zabbix: return ; + case AppConnection.Railway: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RailwayConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RailwayConnectionForm.tsx new file mode 100644 index 000000000..e6403627f --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/RailwayConnectionForm.tsx @@ -0,0 +1,135 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { + RailwayConnectionMethod, + TRailwayConnection +} from "@app/hooks/api/appConnections/types/railway-connection"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TRailwayConnection; + onSubmit: (formData: FormData) => void; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.Railway) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.nativeEnum(RailwayConnectionMethod), + credentials: z.object({ + apiToken: z.string().trim().min(1, "Service API Token required") + }) + }) +]); + +type FormData = z.infer; + +export const RailwayConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.Railway, + method: RailwayConnectionMethod.AccountToken + } + }); + + const { + handleSubmit, + control, + formState: { isSubmitting, isDirty } + } = form; + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogFilterItem.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogFilterItem.tsx index 38df23aad..cc8748953 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogFilterItem.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogFilterItem.tsx @@ -1,3 +1,5 @@ +import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; import { Button, Tooltip } from "@app/components/v2"; @@ -8,25 +10,41 @@ type Props = { label: string; onClear: () => void; children: React.ReactNode; + tooltipText?: string; }; -export const LogFilterItem = ({ label, onClear, hoverTooltip, children, className }: Props) => { +export const LogFilterItem = ({ + label, + onClear, + hoverTooltip, + children, + className, + tooltipText +}: Props) => { return ( - -
-
-

{label}

- -
- {children} +
+
+

{label}

+ {tooltipText && ( + + + + )} +
- + +
{children}
+
+
); }; diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx index 7d406c865..73cc76ed7 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx @@ -366,6 +366,7 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => { { control={control} name="secretPath" render={({ field: { onChange, value, ...field } }) => ( - + { ? "Select a project before filtering by secret key." : undefined } + tooltipText="Enter the exact secret key name (wildcards like * are not supported)" className={twMerge(!selectedProject && "opacity-50")} label="Secret Key" onClear={() => { @@ -413,10 +412,7 @@ export const LogsFilter = ({ presets, setFilter, filter }: Props) => { control={control} name="secretKey" render={({ field: { onChange, value, ...field } }) => ( - + { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 8b764a128..5a4d451fc 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -18,6 +18,7 @@ import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol"; import { HerokuSyncDestinationCol } from "./HerokuSyncDestinationCol"; import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol"; import { OCIVaultSyncDestinationCol } from "./OCIVaultSyncDestinationCol"; +import { RailwaySyncDestinationCol } from "./RailwaySyncDestinationCol"; import { RenderSyncDestinationCol } from "./RenderSyncDestinationCol"; import { TeamCitySyncDestinationCol } from "./TeamCitySyncDestinationCol"; import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol"; @@ -79,6 +80,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Zabbix: return ; + case SecretSync.Railway: + return ; default: throw new Error( `Unhandled Secret Sync Destination Col: ${(secretSync as TSecretSync).destination}` diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index cedfebe9a..a6c2f4f5e 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -160,6 +160,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { throw new Error(`Unhandled Zabbix Scope Destination Col Values ${destination}`); } break; + case SecretSync.Railway: + primaryText = "Railway Project"; + secondaryText = destinationConfig.projectName; + break; default: throw new Error(`Unhandled Destination Col Values ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx index 3c378cded..40d116c5b 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx @@ -55,7 +55,7 @@ const formSchema = z .object({ environment: z.object({ slug: z.string(), name: z.string() }), name: z.string().optional(), - secretPath: z.string().optional(), + secretPath: z.string().trim().min(1), approvals: z.number().min(1).default(1), userApprovers: z .object({ type: z.literal(ApproverType.User), id: z.string() }) @@ -93,20 +93,19 @@ const formSchema = z .optional() }) .superRefine((data, ctx) => { - if ( - data.policyType === PolicyType.ChangePolicy && - !(data.groupApprovers.length || data.userApprovers.length) - ) { - ctx.addIssue({ - path: ["userApprovers"], - code: z.ZodIssueCode.custom, - message: "At least one approver should be provided" - }); - ctx.addIssue({ - path: ["groupApprovers"], - code: z.ZodIssueCode.custom, - message: "At least one approver should be provided" - }); + if (data.policyType === PolicyType.ChangePolicy) { + if (!(data.groupApprovers.length || data.userApprovers.length)) { + ctx.addIssue({ + path: ["userApprovers"], + code: z.ZodIssueCode.custom, + message: "At least one approver should be provided" + }); + ctx.addIssue({ + path: ["groupApprovers"], + code: z.ZodIssueCode.custom, + message: "At least one approver should be provided" + }); + } } }); @@ -127,6 +126,7 @@ const Form = ({ control, handleSubmit, watch, + resetField, formState: { isSubmitting } } = useForm({ resolver: zodResolver(formSchema), @@ -177,6 +177,7 @@ const Form = ({ : undefined, defaultValues: !editValues ? { + secretPath: "/", sequenceApprovers: [{ approvals: 1 }] } : undefined @@ -405,7 +406,10 @@ const Form = ({ )} /> - {isAccessKeyMethod ? ( + {method === DynamicSecretAwsIamAuth.AccessKey && (
- ) : ( + )} + {method === DynamicSecretAwsIamAuth.AssumeRole && (
{ @@ -83,6 +95,8 @@ export const EditDynamicSecretAwsIamForm = ({ secretPath, projectSlug }: Props) => { + const { data: serverConfig } = useGetServerConfig(); + const { control, watch, @@ -100,7 +114,7 @@ export const EditDynamicSecretAwsIamForm = ({ } } }); - const isAccessKeyMethod = watch("inputs.method") === DynamicSecretAwsIamAuth.AccessKey; + const method = watch("inputs.method"); const updateDynamicSecret = useUpdateDynamicSecret(); @@ -214,11 +228,14 @@ export const EditDynamicSecretAwsIamForm = ({ Assume Role (Recommended) Access Key + {serverConfig?.kubernetesAutoFetchServiceAccountToken && ( + IRSA (EKS) + )} )} /> - {isAccessKeyMethod ? ( + {method === DynamicSecretAwsIamAuth.AccessKey && (
- ) : ( + )} + {method === DynamicSecretAwsIamAuth.AssumeRole && (
{ + const parseFile = (file?: File) => { const reader = new FileReader(); if (!file) { createNotification({ @@ -140,10 +140,25 @@ export const SecretDropzone = ({ setIsLoading.on(); reader.onload = (event) => { if (!event?.target?.result) return; - // parse function's argument looks like to be ArrayBuffer - const env = isJson - ? parseJson(event.target.result as ArrayBuffer) - : parseDotEnv(event.target.result as ArrayBuffer); + + let env: TParsedEnv; + + const src = event.target.result as ArrayBuffer; + + switch (file.type) { + case "application/json": + env = parseJson(src); + break; + case "text/yaml": + case "application/x-yaml": + case "application/yaml": + env = parseYaml(src); + break; + + default: + env = parseDotEnv(src); + break; + } setIsLoading.off(); handleParsedEnv(env); }; @@ -165,12 +180,12 @@ export const SecretDropzone = ({ e.dataTransfer.dropEffect = "copy"; setDragActive.off(); - parseFile(e.dataTransfer.files[0], e.dataTransfer.files[0].type === "application/json"); + parseFile(e.dataTransfer.files[0]); }; const handleFileUpload = (e: ChangeEvent) => { e.preventDefault(); - parseFile(e.target?.files?.[0], e.target?.files?.[0]?.type === "application/json"); + parseFile(e.target?.files?.[0]); }; const handleSaveSecrets = async () => { diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/RailwaySyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/RailwaySyncDestinationSection.tsx new file mode 100644 index 000000000..03b650c5b --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/RailwaySyncDestinationSection.tsx @@ -0,0 +1,17 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { TRailwaySync } from "@app/hooks/api/secretSyncs/types/railway-sync"; + +type Props = { + secretSync: TRailwaySync; +}; + +export const RailwaySyncDestinationSection = ({ secretSync }: Props) => { + const { destinationConfig } = secretSync; + + return ( + <> + {destinationConfig.projectName} + {destinationConfig.environmentName} + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index e69b8f082..71755c7e9 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -29,6 +29,7 @@ import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection"; import { HerokuSyncDestinationSection } from "./HerokuSyncDestinationSection"; import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection"; import { OCIVaultSyncDestinationSection } from "./OCIVaultSyncDestinationSection"; +import { RailwaySyncDestinationSection } from "./RailwaySyncDestinationSection"; import { RenderSyncDestinationSection } from "./RenderSyncDestinationSection"; import { TeamCitySyncDestinationSection } from "./TeamCitySyncDestinationSection"; import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection"; @@ -122,6 +123,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.Zabbix: DestinationComponents = ; break; + case SecretSync.Railway: + DestinationComponents = ; + break; default: throw new Error(`Unhandled Destination Section components: ${destination}`); } diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index 8d0c8a729..60710e3ef 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -62,6 +62,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.CloudflarePages: case SecretSync.CloudflareWorkers: case SecretSync.Zabbix: + case SecretSync.Railway: AdditionalSyncOptionsComponent = null; break; default: diff --git a/helm-charts/secrets-operator/Chart.yaml b/helm-charts/secrets-operator/Chart.yaml index 68159eca4..a11238e94 100644 --- a/helm-charts/secrets-operator/Chart.yaml +++ b/helm-charts/secrets-operator/Chart.yaml @@ -13,9 +13,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: v0.9.3 +version: v0.9.4 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v0.9.3" +appVersion: "v0.9.4" diff --git a/helm-charts/secrets-operator/values.yaml b/helm-charts/secrets-operator/values.yaml index 898850299..54b64d4ca 100644 --- a/helm-charts/secrets-operator/values.yaml +++ b/helm-charts/secrets-operator/values.yaml @@ -32,7 +32,7 @@ controllerManager: - ALL image: repository: infisical/kubernetes-operator - tag: v0.9.3 + tag: v0.9.4 resources: limits: cpu: 500m diff --git a/k8-operator/go.mod b/k8-operator/go.mod index c9b868b00..b5de6b278 100644 --- a/k8-operator/go.mod +++ b/k8-operator/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/aws/smithy-go v1.20.3 - github.com/infisical/go-sdk v0.4.4 + github.com/infisical/go-sdk v0.5.97 github.com/lestrrat-go/jwx/v2 v2.1.4 github.com/onsi/ginkgo/v2 v2.6.0 github.com/onsi/gomega v1.24.1 @@ -43,6 +43,7 @@ require ( github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.5 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/huandu/xstrings v1.5.0 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect @@ -105,7 +106,7 @@ require ( go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.32.0 - golang.org/x/net v0.27.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/term v0.28.0 // indirect diff --git a/k8-operator/go.sum b/k8-operator/go.sum index 2e151b0a4..a082da41c 100644 --- a/k8-operator/go.sum +++ b/k8-operator/go.sum @@ -228,13 +228,15 @@ github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBY github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/infisical/go-sdk v0.4.4 h1:Z4CBzxfhiY6ikjRimOEeyEEnb3QT/BKw3OzNFH7Pe+U= -github.com/infisical/go-sdk v0.4.4/go.mod h1:6fWzAwTPIoKU49mQ2Oxu+aFnJu9n7k2JcNrZjzhHM2M= +github.com/infisical/go-sdk v0.5.97 h1:veOi6Hduda6emtwjdUI5SBg2qd2iDQc5xLKqZ15KSoM= +github.com/infisical/go-sdk v0.5.97/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= @@ -479,8 +481,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=