mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 11:29:42 +00:00
Merge branch 'heads/main' into daniel/fips-initative
This commit is contained in:
@@ -4,6 +4,7 @@ import "ts-node/register";
|
||||
import dotenv from "dotenv";
|
||||
import type { Knex } from "knex";
|
||||
import path from "path";
|
||||
import { initLogger } from "@app/lib/logger";
|
||||
|
||||
// Update with your config settings. .
|
||||
dotenv.config({
|
||||
@@ -13,6 +14,8 @@ dotenv.config({
|
||||
path: path.join(__dirname, "../../../.env")
|
||||
});
|
||||
|
||||
initLogger();
|
||||
|
||||
export default {
|
||||
development: {
|
||||
client: "postgres",
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
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}'`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
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<void> {
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (table) => {
|
||||
table.string("secretPath").nullable().alter();
|
||||
});
|
||||
|
||||
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (table) => {
|
||||
table.string("secretPath").nullable().alter();
|
||||
});
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
}),
|
||||
|
||||
@@ -141,14 +141,39 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { approval } = await server.services.secretApprovalRequest.mergeSecretApprovalRequest({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
approvalId: req.params.id,
|
||||
bypassReason: req.body.bypassReason
|
||||
const { approval, projectId, secretMutationEvents } =
|
||||
await server.services.secretApprovalRequest.mergeSecretApprovalRequest({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
approvalId: req.params.id,
|
||||
bypassReason: req.body.bypassReason
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId,
|
||||
event: {
|
||||
type: EventType.SECRET_APPROVAL_MERGED,
|
||||
metadata: {
|
||||
mergedBy: req.permission.id,
|
||||
secretApprovalRequestSlug: approval.slug,
|
||||
secretApprovalRequestId: approval.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for await (const event of secretMutationEvents) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId,
|
||||
event
|
||||
});
|
||||
}
|
||||
|
||||
return { approval };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -116,6 +116,15 @@ interface BaseAuthData {
|
||||
userAgentType?: UserAgentType;
|
||||
}
|
||||
|
||||
export enum SecretApprovalEvent {
|
||||
Create = "create",
|
||||
Update = "update",
|
||||
Delete = "delete",
|
||||
CreateMany = "create-many",
|
||||
UpdateMany = "update-many",
|
||||
DeleteMany = "delete-many"
|
||||
}
|
||||
|
||||
export enum UserAgentType {
|
||||
WEB = "web",
|
||||
CLI = "cli",
|
||||
@@ -1705,6 +1714,17 @@ interface SecretApprovalRequest {
|
||||
committedBy: string;
|
||||
secretApprovalRequestSlug: string;
|
||||
secretApprovalRequestId: string;
|
||||
eventType: SecretApprovalEvent;
|
||||
secretKey?: string;
|
||||
secretId?: string;
|
||||
secrets?: {
|
||||
secretKey?: string;
|
||||
secretId?: string;
|
||||
environment?: string;
|
||||
secretPath?: string;
|
||||
}[];
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import { z } from "zod";
|
||||
import { CustomAWSHasher } from "@app/lib/aws/hashing";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
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";
|
||||
@@ -86,6 +86,23 @@ 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,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
sha256: CustomAWSHasher
|
||||
});
|
||||
return client;
|
||||
}
|
||||
|
||||
const client = new IAMClient({
|
||||
region: providerInputs.region,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
@@ -108,7 +125,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")
|
||||
) {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
+108
-2
@@ -10,6 +10,7 @@ import {
|
||||
TSecretApprovalRequestsSecretsInsert,
|
||||
TSecretApprovalRequestsSecretsV2Insert
|
||||
} from "@app/db/schemas";
|
||||
import { Event, EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
@@ -523,7 +524,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { policy, folderId, projectId, bypassers } = secretApprovalRequest;
|
||||
const { policy, folderId, projectId, bypassers, environment } = secretApprovalRequest;
|
||||
if (policy.deletedAt) {
|
||||
throw new BadRequestError({
|
||||
message: "The policy associated with this secret approval request has been deleted."
|
||||
@@ -959,7 +960,112 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
return mergeStatus;
|
||||
const { created, updated, deleted } = mergeStatus.secrets;
|
||||
|
||||
const secretMutationEvents: Event[] = [];
|
||||
|
||||
if (created.length) {
|
||||
if (created.length > 1) {
|
||||
secretMutationEvents.push({
|
||||
type: EventType.CREATE_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath: folder.path,
|
||||
secrets: created.map((secret) => ({
|
||||
secretId: secret.id,
|
||||
secretVersion: 1,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretKey: secret.key as string,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
|
||||
}))
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const [secret] = created;
|
||||
secretMutationEvents.push({
|
||||
type: EventType.CREATE_SECRET,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath: folder.path,
|
||||
secretId: secret.id,
|
||||
secretVersion: 1,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretKey: secret.key as string,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (updated.length) {
|
||||
if (updated.length > 1) {
|
||||
secretMutationEvents.push({
|
||||
type: EventType.UPDATE_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath: folder.path,
|
||||
secrets: updated.map((secret) => ({
|
||||
secretId: secret.id,
|
||||
secretVersion: secret.version,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretKey: secret.key as string,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
|
||||
}))
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const [secret] = updated;
|
||||
secretMutationEvents.push({
|
||||
type: EventType.UPDATE_SECRET,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath: folder.path,
|
||||
secretId: secret.id,
|
||||
secretVersion: secret.version,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretKey: secret.key as string,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (deleted.length) {
|
||||
if (deleted.length > 1) {
|
||||
secretMutationEvents.push({
|
||||
type: EventType.DELETE_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath: folder.path,
|
||||
secrets: deleted.map((secret) => ({
|
||||
secretId: secret.id,
|
||||
secretVersion: secret.version,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretKey: secret.key as string
|
||||
}))
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const [secret] = deleted;
|
||||
secretMutationEvents.push({
|
||||
type: EventType.DELETE_SECRET,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath: folder.path,
|
||||
secretId: secret.id,
|
||||
secretVersion: secret.version,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretKey: secret.key as string
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { ...mergeStatus, projectId, secretMutationEvents };
|
||||
};
|
||||
|
||||
// function to save secret change to secret approval
|
||||
|
||||
@@ -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: "[email protected]",
|
||||
authorName: payload.authorName,
|
||||
authorEmail: payload.authorEmail,
|
||||
resourceName,
|
||||
numberOfSecrets: payload.numberOfSecrets,
|
||||
isDiffScan: payload.isDiffScan,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2474,6 +2477,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."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2594,7 +2605,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.`
|
||||
|
||||
@@ -30,6 +30,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"])
|
||||
@@ -439,6 +440,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: [
|
||||
@@ -452,16 +466,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."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -478,6 +555,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: [
|
||||
@@ -495,6 +585,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."
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -50,7 +50,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()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -64,7 +65,8 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
...config,
|
||||
fipsEnabled: crypto.isFipsModeEnabled(),
|
||||
isMigrationModeOn: serverEnvs.MAINTENANCE_MODE,
|
||||
isSecretScanningDisabled: serverEnvs.DISABLE_SECRET_SCANNING
|
||||
isSecretScanningDisabled: serverEnvs.DISABLE_SECRET_SCANNING,
|
||||
kubernetesAutoFetchServiceAccountToken: serverEnvs.KUBERNETES_AUTO_FETCH_SERVICE_ACCOUNT_TOKEN
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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<AppConnection, (server:
|
||||
[AppConnection.GitLab]: registerGitLabConnectionRouter,
|
||||
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
|
||||
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
|
||||
[AppConnection.Zabbix]: registerZabbixConnectionRouter
|
||||
[AppConnection.Zabbix]: registerZabbixConnectionRouter,
|
||||
[AppConnection.Railway]: registerRailwayConnectionRouter
|
||||
};
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateRailwayConnectionSchema,
|
||||
SanitizedRailwayConnectionSchema,
|
||||
UpdateRailwayConnectionSchema
|
||||
} from "@app/services/app-connection/railway";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerRailwayConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
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 };
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -17,6 +17,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";
|
||||
@@ -49,5 +50,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
|
||||
[SecretSync.Flyio]: registerFlyioSyncRouter,
|
||||
[SecretSync.GitLab]: registerGitLabSyncRouter,
|
||||
[SecretSync.CloudflarePages]: registerCloudflarePagesSyncRouter,
|
||||
[SecretSync.Zabbix]: registerZabbixSyncRouter
|
||||
[SecretSync.Zabbix]: registerZabbixSyncRouter,
|
||||
[SecretSync.Railway]: registerRailwaySyncRouter
|
||||
};
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
CreateRailwaySyncSchema,
|
||||
RailwaySyncSchema,
|
||||
UpdateRailwaySyncSchema
|
||||
} from "@app/services/secret-sync/railway/railway-sync-schemas";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerRailwaySyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
destination: SecretSync.Railway,
|
||||
server,
|
||||
responseSchema: RailwaySyncSchema,
|
||||
createSchema: CreateRailwaySyncSchema,
|
||||
updateSchema: UpdateRailwaySyncSchema
|
||||
});
|
||||
@@ -34,6 +34,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";
|
||||
@@ -64,7 +65,8 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
|
||||
FlyioSyncSchema,
|
||||
GitLabSyncSchema,
|
||||
CloudflarePagesSyncSchema,
|
||||
ZabbixSyncSchema
|
||||
ZabbixSyncSchema,
|
||||
RailwaySyncSchema
|
||||
]);
|
||||
|
||||
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
@@ -90,7 +92,8 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
|
||||
FlyioSyncListItemSchema,
|
||||
GitLabSyncListItemSchema,
|
||||
CloudflarePagesSyncListItemSchema,
|
||||
ZabbixSyncListItemSchema
|
||||
ZabbixSyncListItemSchema,
|
||||
RailwaySyncListItemSchema
|
||||
]);
|
||||
|
||||
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import picomatch from "picomatch";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas";
|
||||
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { EventType, SecretApprovalEvent, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
@@ -594,6 +594,23 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretReminderRepeatDays: req.body.secretReminderRepeatDays
|
||||
});
|
||||
if (secretOperation.type === SecretProtectionType.Approval) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.body.workspaceId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||
metadata: {
|
||||
committedBy: secretOperation.approval.committerUserId,
|
||||
secretApprovalRequestId: secretOperation.approval.id,
|
||||
secretApprovalRequestSlug: secretOperation.approval.slug,
|
||||
secretPath: req.body.secretPath,
|
||||
environment: req.body.environment,
|
||||
secretKey: req.params.secretName,
|
||||
eventType: SecretApprovalEvent.Create
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { approval: secretOperation.approval };
|
||||
}
|
||||
|
||||
@@ -730,6 +747,23 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
if (secretOperation.type === SecretProtectionType.Approval) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.body.workspaceId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||
metadata: {
|
||||
committedBy: secretOperation.approval.committerUserId,
|
||||
secretApprovalRequestId: secretOperation.approval.id,
|
||||
secretApprovalRequestSlug: secretOperation.approval.slug,
|
||||
secretPath: req.body.secretPath,
|
||||
environment: req.body.environment,
|
||||
secretKey: req.params.secretName,
|
||||
eventType: SecretApprovalEvent.Update
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { approval: secretOperation.approval };
|
||||
}
|
||||
const { secret } = secretOperation;
|
||||
@@ -831,6 +865,23 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
type: req.body.type
|
||||
});
|
||||
if (secretOperation.type === SecretProtectionType.Approval) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.body.workspaceId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||
metadata: {
|
||||
committedBy: secretOperation.approval.committerUserId,
|
||||
secretApprovalRequestId: secretOperation.approval.id,
|
||||
secretApprovalRequestSlug: secretOperation.approval.slug,
|
||||
secretPath: req.body.secretPath,
|
||||
environment: req.body.environment,
|
||||
secretKey: req.params.secretName,
|
||||
eventType: SecretApprovalEvent.Delete
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { approval: secretOperation.approval };
|
||||
}
|
||||
|
||||
@@ -1165,7 +1216,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
metadata: {
|
||||
committedBy: approval.committerUserId,
|
||||
secretApprovalRequestId: approval.id,
|
||||
secretApprovalRequestSlug: approval.slug
|
||||
secretApprovalRequestSlug: approval.slug,
|
||||
secretPath,
|
||||
environment,
|
||||
eventType: SecretApprovalEvent.Create
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1351,7 +1405,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
metadata: {
|
||||
committedBy: approval.committerUserId,
|
||||
secretApprovalRequestId: approval.id,
|
||||
secretApprovalRequestSlug: approval.slug
|
||||
secretApprovalRequestSlug: approval.slug,
|
||||
secretPath,
|
||||
environment,
|
||||
secretKey: req.params.secretName,
|
||||
eventType: SecretApprovalEvent.Update
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1489,7 +1547,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
metadata: {
|
||||
committedBy: approval.committerUserId,
|
||||
secretApprovalRequestId: approval.id,
|
||||
secretApprovalRequestSlug: approval.slug
|
||||
secretApprovalRequestSlug: approval.slug,
|
||||
secretPath,
|
||||
environment,
|
||||
secretKey: req.params.secretName,
|
||||
eventType: SecretApprovalEvent.Delete
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1673,7 +1735,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
metadata: {
|
||||
committedBy: approval.committerUserId,
|
||||
secretApprovalRequestId: approval.id,
|
||||
secretApprovalRequestSlug: approval.slug
|
||||
secretApprovalRequestSlug: approval.slug,
|
||||
secretPath,
|
||||
environment,
|
||||
eventType: SecretApprovalEvent.CreateMany
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1801,7 +1866,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
metadata: {
|
||||
committedBy: approval.committerUserId,
|
||||
secretApprovalRequestId: approval.id,
|
||||
secretApprovalRequestSlug: approval.slug
|
||||
secretApprovalRequestSlug: approval.slug,
|
||||
secretPath,
|
||||
environment,
|
||||
eventType: SecretApprovalEvent.UpdateMany,
|
||||
secrets: inputSecrets.map((secret) => ({
|
||||
secretKey: secret.secretName
|
||||
}))
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1920,7 +1991,13 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
metadata: {
|
||||
committedBy: approval.committerUserId,
|
||||
secretApprovalRequestId: approval.id,
|
||||
secretApprovalRequestSlug: approval.slug
|
||||
secretApprovalRequestSlug: approval.slug,
|
||||
secretPath,
|
||||
environment,
|
||||
secrets: inputSecrets.map((secret) => ({
|
||||
secretKey: secret.secretName
|
||||
})),
|
||||
eventType: SecretApprovalEvent.DeleteMany
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2038,6 +2115,24 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secrets: inputSecrets
|
||||
});
|
||||
if (secretOperation.type === SecretProtectionType.Approval) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.body.workspaceId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||
metadata: {
|
||||
committedBy: secretOperation.approval.committerUserId,
|
||||
secretApprovalRequestId: secretOperation.approval.id,
|
||||
secretApprovalRequestSlug: secretOperation.approval.slug,
|
||||
secretPath,
|
||||
environment,
|
||||
secrets: inputSecrets.map((secret) => ({
|
||||
secretKey: secret.secretKey
|
||||
})),
|
||||
eventType: SecretApprovalEvent.CreateMany
|
||||
}
|
||||
}
|
||||
});
|
||||
return { approval: secretOperation.approval };
|
||||
}
|
||||
const { secrets } = secretOperation;
|
||||
@@ -2170,6 +2265,25 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
mode: req.body.mode
|
||||
});
|
||||
if (secretOperation.type === SecretProtectionType.Approval) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.body.workspaceId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||
metadata: {
|
||||
committedBy: secretOperation.approval.committerUserId,
|
||||
secretApprovalRequestId: secretOperation.approval.id,
|
||||
secretApprovalRequestSlug: secretOperation.approval.slug,
|
||||
secretPath,
|
||||
environment,
|
||||
secrets: inputSecrets.map((secret) => ({
|
||||
secretKey: secret.secretKey,
|
||||
secretPath: secret.secretPath
|
||||
})),
|
||||
eventType: SecretApprovalEvent.UpdateMany
|
||||
}
|
||||
}
|
||||
});
|
||||
return { approval: secretOperation.approval };
|
||||
}
|
||||
const { secrets } = secretOperation;
|
||||
@@ -2298,6 +2412,25 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secrets: inputSecrets
|
||||
});
|
||||
if (secretOperation.type === SecretProtectionType.Approval) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.body.workspaceId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||
metadata: {
|
||||
committedBy: secretOperation.approval.committerUserId,
|
||||
secretApprovalRequestId: secretOperation.approval.id,
|
||||
secretApprovalRequestSlug: secretOperation.approval.slug,
|
||||
secretPath,
|
||||
environment,
|
||||
secrets: inputSecrets.map((secret) => ({
|
||||
secretKey: secret.secretKey
|
||||
})),
|
||||
eventType: SecretApprovalEvent.DeleteMany
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { approval: secretOperation.approval };
|
||||
}
|
||||
const { secrets } = secretOperation;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -30,8 +30,9 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[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<AppConnection, AppConnectionPlanType> = {
|
||||
@@ -64,6 +65,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.GitLab]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Zabbix]: AppConnectionPlanType.Regular
|
||||
[AppConnection.Zabbix]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Railway]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -72,6 +72,8 @@ import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
|
||||
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
|
||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||
import { ValidateRailwayConnectionCredentialsSchema } from "./railway";
|
||||
import { railwayConnectionService } from "./railway/railway-connection-service";
|
||||
import { ValidateRenderConnectionCredentialsSchema } from "./render/render-connection-schema";
|
||||
import { renderConnectionService } from "./render/render-connection-service";
|
||||
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
|
||||
@@ -124,8 +126,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
|
||||
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
|
||||
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
|
||||
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
|
||||
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema
|
||||
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
|
||||
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
|
||||
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -536,7 +539,8 @@ export const appConnectionServiceFactory = ({
|
||||
flyio: flyioConnectionService(connectAppConnectionById),
|
||||
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
cloudflare: cloudflareConnectionService(connectAppConnectionById),
|
||||
bitbucket: bitbucketConnectionService(connectAppConnectionById),
|
||||
zabbix: zabbixConnectionService(connectAppConnectionById)
|
||||
zabbix: zabbixConnectionService(connectAppConnectionById),
|
||||
railway: railwayConnectionService(connectAppConnectionById),
|
||||
bitbucket: bitbucketConnectionService(connectAppConnectionById)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -141,6 +141,12 @@ import {
|
||||
TPostgresConnectionInput,
|
||||
TValidatePostgresConnectionCredentialsSchema
|
||||
} from "./postgres";
|
||||
import {
|
||||
TRailwayConnection,
|
||||
TRailwayConnectionConfig,
|
||||
TRailwayConnectionInput,
|
||||
TValidateRailwayConnectionCredentialsSchema
|
||||
} from "./railway";
|
||||
import {
|
||||
TRenderConnection,
|
||||
TRenderConnectionConfig,
|
||||
@@ -210,6 +216,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TCloudflareConnection
|
||||
| TBitbucketConnection
|
||||
| TZabbixConnection
|
||||
| TRailwayConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./railway-connection-constants";
|
||||
export * from "./railway-connection-fns";
|
||||
export * from "./railway-connection-schemas";
|
||||
export * from "./railway-connection-types";
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum RailwayConnectionMethod {
|
||||
AccountToken = "account-token",
|
||||
ProjectToken = "project-token",
|
||||
TeamToken = "team-token"
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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<TRailwayConnectionConfig, "credentials" | "method">;
|
||||
|
||||
export function getRailwayAuthHeaders(method: RailwayConnectionMethod, token: string): Record<string, string> {
|
||||
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<void>;
|
||||
} {
|
||||
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<void>((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<T extends TRailwayResponse>(
|
||||
query: string,
|
||||
options: RailwaySendReqOptions,
|
||||
variables: Record<string, string | Record<string, string>> = {},
|
||||
retryAttempt: number = 0
|
||||
): Promise<T["data"] | undefined> {
|
||||
const body = {
|
||||
query,
|
||||
variables
|
||||
};
|
||||
|
||||
const response = await this.client.request<T>({
|
||||
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<RailwayProject[]> {
|
||||
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<TRailwayResponse<{ variables: Record<string, string> }>>(
|
||||
`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<TRailwayResponse<{ variables: Record<string, string> }>>(
|
||||
`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<TRailwayResponse<{ variables: Record<string, string> }>>(
|
||||
`mutation variableUpsert($input: VariableUpsertInput!) { variableUpsert(input: $input) }`,
|
||||
config,
|
||||
variables
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const RailwayPublicAPI = new RailwayPublicClient();
|
||||
@@ -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"])
|
||||
})
|
||||
});
|
||||
@@ -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<TRailwayConnection>;
|
||||
|
||||
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
|
||||
};
|
||||
};
|
||||
@@ -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<typeof RailwayConnectionSchema>;
|
||||
|
||||
export type TRailwayConnectionInput = z.infer<typeof CreateRailwayConnectionSchema> & {
|
||||
app: AppConnection.Railway;
|
||||
};
|
||||
|
||||
export type TValidateRailwayConnectionCredentialsSchema = typeof ValidateRailwayConnectionCredentialsSchema;
|
||||
|
||||
export type TRailwayConnectionConfig = DiscriminativePick<TRailwayConnection, "method" | "app" | "credentials"> & {
|
||||
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<T = unknown> = {
|
||||
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;
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -33,6 +33,8 @@ import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
||||
import { applyJitter } from "@app/lib/dates";
|
||||
import { delay as delayMs } from "@app/lib/delay";
|
||||
import {
|
||||
BadRequestError,
|
||||
ForbiddenRequestError,
|
||||
@@ -41,9 +43,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";
|
||||
@@ -1439,6 +1442,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();
|
||||
|
||||
@@ -1462,24 +1467,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 {
|
||||
|
||||
@@ -214,7 +214,7 @@ export const secretFolderServiceFactory = ({
|
||||
}
|
||||
},
|
||||
message: "Folder created",
|
||||
folderId: doc.id,
|
||||
folderId: parentFolder.id,
|
||||
changes: [
|
||||
{
|
||||
type: CommitType.ADD,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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<TSecretMap> {
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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)
|
||||
});
|
||||
@@ -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<typeof RailwaySyncListItemSchema>;
|
||||
|
||||
export type TRailwaySync = z.infer<typeof RailwaySyncSchema>;
|
||||
|
||||
export type TRailwaySyncInput = z.infer<typeof CreateRailwaySyncSchema>;
|
||||
|
||||
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<string, string>;
|
||||
};
|
||||
};
|
||||
@@ -21,7 +21,8 @@ export enum SecretSync {
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab",
|
||||
CloudflarePages = "cloudflare-pages",
|
||||
Zabbix = "zabbix"
|
||||
Zabbix = "zabbix",
|
||||
Railway = "railway"
|
||||
}
|
||||
|
||||
export enum SecretSyncInitialSyncBehavior {
|
||||
|
||||
@@ -39,6 +39,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";
|
||||
@@ -70,7 +72,8 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION,
|
||||
[SecretSync.GitLab]: GITLAB_SYNC_LIST_OPTION,
|
||||
[SecretSync.CloudflarePages]: CLOUDFLARE_PAGES_SYNC_LIST_OPTION,
|
||||
[SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION
|
||||
[SecretSync.Zabbix]: ZABBIX_SYNC_LIST_OPTION,
|
||||
[SecretSync.Railway]: RAILWAY_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretSyncOptions = () => {
|
||||
@@ -240,6 +243,8 @@ export const SecretSyncFns = {
|
||||
return CloudflarePagesSyncFns.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}`
|
||||
@@ -335,6 +340,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}`
|
||||
@@ -414,6 +422,8 @@ export const SecretSyncFns = {
|
||||
return CloudflarePagesSyncFns.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}`
|
||||
|
||||
@@ -24,7 +24,8 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.Flyio]: "Fly.io",
|
||||
[SecretSync.GitLab]: "GitLab",
|
||||
[SecretSync.CloudflarePages]: "Cloudflare Pages",
|
||||
[SecretSync.Zabbix]: "Zabbix"
|
||||
[SecretSync.Zabbix]: "Zabbix",
|
||||
[SecretSync.Railway]: "Railway"
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
@@ -50,7 +51,8 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.Flyio]: AppConnection.Flyio,
|
||||
[SecretSync.GitLab]: AppConnection.GitLab,
|
||||
[SecretSync.CloudflarePages]: AppConnection.Cloudflare,
|
||||
[SecretSync.Zabbix]: AppConnection.Zabbix
|
||||
[SecretSync.Zabbix]: AppConnection.Zabbix,
|
||||
[SecretSync.Railway]: AppConnection.Railway
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
@@ -76,5 +78,6 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
|
||||
[SecretSync.Flyio]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.GitLab]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.CloudflarePages]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Zabbix]: SecretSyncPlanType.Regular
|
||||
[SecretSync.Zabbix]: SecretSyncPlanType.Regular,
|
||||
[SecretSync.Railway]: SecretSyncPlanType.Regular
|
||||
};
|
||||
|
||||
@@ -94,6 +94,12 @@ import {
|
||||
THumanitecSyncListItem,
|
||||
THumanitecSyncWithCredentials
|
||||
} from "./humanitec";
|
||||
import {
|
||||
TRailwaySync,
|
||||
TRailwaySyncInput,
|
||||
TRailwaySyncListItem,
|
||||
TRailwaySyncWithCredentials
|
||||
} from "./railway/railway-sync-types";
|
||||
import {
|
||||
TRenderSync,
|
||||
TRenderSyncInput,
|
||||
@@ -138,7 +144,8 @@ export type TSecretSync =
|
||||
| TFlyioSync
|
||||
| TGitLabSync
|
||||
| TCloudflarePagesSync
|
||||
| TZabbixSync;
|
||||
| TZabbixSync
|
||||
| TRailwaySync;
|
||||
|
||||
export type TSecretSyncWithCredentials =
|
||||
| TAwsParameterStoreSyncWithCredentials
|
||||
@@ -163,7 +170,8 @@ export type TSecretSyncWithCredentials =
|
||||
| TFlyioSyncWithCredentials
|
||||
| TGitLabSyncWithCredentials
|
||||
| TCloudflarePagesSyncWithCredentials
|
||||
| TZabbixSyncWithCredentials;
|
||||
| TZabbixSyncWithCredentials
|
||||
| TRailwaySyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput =
|
||||
| TAwsParameterStoreSyncInput
|
||||
@@ -188,7 +196,8 @@ export type TSecretSyncInput =
|
||||
| TFlyioSyncInput
|
||||
| TGitLabSyncInput
|
||||
| TCloudflarePagesSyncInput
|
||||
| TZabbixSyncInput;
|
||||
| TZabbixSyncInput
|
||||
| TRailwaySyncInput;
|
||||
|
||||
export type TSecretSyncListItem =
|
||||
| TAwsParameterStoreSyncListItem
|
||||
@@ -213,7 +222,8 @@ export type TSecretSyncListItem =
|
||||
| TFlyioSyncListItem
|
||||
| TGitLabSyncListItem
|
||||
| TCloudflarePagesSyncListItem
|
||||
| TZabbixSyncListItem;
|
||||
| TZabbixSyncListItem
|
||||
| TRailwaySyncListItem;
|
||||
|
||||
export type TSyncOptionsConfig = {
|
||||
canImportSecrets: boolean;
|
||||
|
||||
Reference in New Issue
Block a user