Merge remote-tracking branch 'origin/main' into feat/addHerokuSecretSync

This commit is contained in:
carlosmonastyrski
2025-06-19 08:47:03 -03:00
205 changed files with 5151 additions and 759 deletions

View File

@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn(TableName.Identity, "hasDeleteProtection");
if (!hasCol) {
await knex.schema.alterTable(TableName.Identity, (t) => {
t.boolean("hasDeleteProtection").notNullable().defaultTo(false);
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasCol = await knex.schema.hasColumn(TableName.Identity, "hasDeleteProtection");
if (hasCol) {
await knex.schema.alterTable(TableName.Identity, (t) => {
t.dropColumn("hasDeleteProtection");
});
}
}

View File

@@ -0,0 +1,21 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.IdentityAwsAuth, "allowedPrincipalArns");
if (hasColumn) {
await knex.schema.alterTable(TableName.IdentityAwsAuth, (t) => {
t.string("allowedPrincipalArns", 2048).notNullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasColumn = await knex.schema.hasColumn(TableName.IdentityAwsAuth, "allowedPrincipalArns");
if (hasColumn) {
await knex.schema.alterTable(TableName.IdentityAwsAuth, (t) => {
t.string("allowedPrincipalArns", 255).notNullable().alter();
});
}
}

View File

@@ -12,7 +12,8 @@ export const IdentitiesSchema = z.object({
name: z.string(), name: z.string(),
authMethod: z.string().nullable().optional(), authMethod: z.string().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
hasDeleteProtection: z.boolean().default(false)
}); });
export type TIdentities = z.infer<typeof IdentitiesSchema>; export type TIdentities = z.infer<typeof IdentitiesSchema>;

View File

@@ -48,7 +48,9 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
id: z.string().trim().describe(GROUPS.GET_BY_ID.id) id: z.string().trim().describe(GROUPS.GET_BY_ID.id)
}), }),
response: { response: {
200: GroupsSchema 200: GroupsSchema.extend({
customRoleSlug: z.string().nullable()
})
} }
}, },
handler: async (req) => { handler: async (req) => {

View File

@@ -780,6 +780,7 @@ interface CreateIdentityEvent {
metadata: { metadata: {
identityId: string; identityId: string;
name: string; name: string;
hasDeleteProtection: boolean;
}; };
} }
@@ -788,6 +789,7 @@ interface UpdateIdentityEvent {
metadata: { metadata: {
identityId: string; identityId: string;
name?: string; name?: string;
hasDeleteProtection?: boolean;
}; };
} }

View File

@@ -169,11 +169,29 @@ export const groupDALFactory = (db: TDbClient) => {
} }
}; };
const findById = async (id: string, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())(TableName.Groups)
.leftJoin(TableName.OrgRoles, `${TableName.Groups}.roleId`, `${TableName.OrgRoles}.id`)
.where(`${TableName.Groups}.id`, id)
.select(
selectAllTableCols(TableName.Groups),
db.ref("slug").as("customRoleSlug").withSchema(TableName.OrgRoles)
)
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: "Find by id" });
}
};
return { return {
...groupOrm,
findGroups, findGroups,
findByOrgId, findByOrgId,
findAllGroupPossibleMembers, findAllGroupPossibleMembers,
findGroupsByProjectId, findGroupsByProjectId,
...groupOrm findById
}; };
}; };

View File

@@ -698,9 +698,9 @@ export const oidcConfigServiceFactory = ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
(_req: any, tokenSet: TokenSet, cb: any) => { (_req: any, tokenSet: TokenSet, cb: any) => {
const claims = tokenSet.claims(); const claims = tokenSet.claims();
if (!claims.email || !claims.given_name) { if (!claims.email) {
throw new BadRequestError({ throw new BadRequestError({
message: "Invalid request. Missing email or first name" message: "Invalid request. Missing email claim."
}); });
} }
@@ -713,12 +713,19 @@ export const oidcConfigServiceFactory = ({
} }
} }
const name = claims?.given_name || claims?.name;
if (!name) {
throw new BadRequestError({
message: "Invalid request. Missing name claim."
});
}
const groups = typeof claims.groups === "string" ? [claims.groups] : (claims.groups as string[] | undefined); const groups = typeof claims.groups === "string" ? [claims.groups] : (claims.groups as string[] | undefined);
oidcLogin({ oidcLogin({
email: claims.email.toLowerCase(), email: claims.email.toLowerCase(),
externalId: claims.sub, externalId: claims.sub,
firstName: claims.given_name ?? "", firstName: name,
lastName: claims.family_name ?? "", lastName: claims.family_name ?? "",
orgId: org.id, orgId: org.id,
groups, groups,

View File

@@ -211,6 +211,11 @@ export type SecretFolderSubjectFields = {
secretPath: string; secretPath: string;
}; };
export type SecretSyncSubjectFields = {
environment: string;
secretPath: string;
};
export type DynamicSecretSubjectFields = { export type DynamicSecretSubjectFields = {
environment: string; environment: string;
secretPath: string; secretPath: string;
@@ -267,6 +272,10 @@ export type ProjectPermissionSet =
| (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields) | (ForcedSubject<ProjectPermissionSub.DynamicSecrets> & DynamicSecretSubjectFields)
) )
] ]
| [
ProjectPermissionSecretSyncActions,
ProjectPermissionSub.SecretSyncs | (ForcedSubject<ProjectPermissionSub.SecretSyncs> & SecretSyncSubjectFields)
]
| [ | [
ProjectPermissionActions, ProjectPermissionActions,
( (
@@ -323,7 +332,6 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups] | [ProjectPermissionActions, ProjectPermissionSub.SshHostGroups]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip] | [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project]
@@ -412,6 +420,23 @@ const DynamicSecretConditionV2Schema = z
}) })
.partial(); .partial();
const SecretSyncConditionV2Schema = z
.object({
environment: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN],
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB]
})
.partial()
]),
secretPath: SECRET_PATH_PERMISSION_OPERATOR_SCHEMA
})
.partial();
const SecretImportConditionSchema = z const SecretImportConditionSchema = z
.object({ .object({
environment: z.union([ environment: z.union([
@@ -671,12 +696,6 @@ const GeneralPermissionSchema = [
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.SecretSyncs).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe(
"Describe what action an entity can take."
)
}),
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.Kmip).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe(
@@ -836,6 +855,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
"When specified, only matching conditions will be allowed to access given resource." "When specified, only matching conditions will be allowed to access given resource."
).optional() ).optional()
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.SecretSyncs).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretSyncActions).describe(
"Describe what action an entity can take."
),
conditions: SecretSyncConditionV2Schema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
...GeneralPermissionSchema ...GeneralPermissionSchema
]); ]);

View File

@@ -111,12 +111,14 @@ export const IDENTITIES = {
CREATE: { CREATE: {
name: "The name of the identity to create.", name: "The name of the identity to create.",
organizationId: "The organization ID to which the identity belongs.", organizationId: "The organization ID to which the identity belongs.",
role: "The role of the identity. Possible values are 'no-access', 'member', and 'admin'." role: "The role of the identity. Possible values are 'no-access', 'member', and 'admin'.",
hasDeleteProtection: "Prevents deletion of the identity when enabled."
}, },
UPDATE: { UPDATE: {
identityId: "The ID of the identity to update.", identityId: "The ID of the identity to update.",
name: "The new name of the identity.", name: "The new name of the identity.",
role: "The new role of the identity." role: "The new role of the identity.",
hasDeleteProtection: "Prevents deletion of the identity when enabled."
}, },
DELETE: { DELETE: {
identityId: "The ID of the identity to delete." identityId: "The ID of the identity to delete."
@@ -2223,6 +2225,9 @@ export const AppConnections = {
ONEPASS: { ONEPASS: {
instanceUrl: "The URL of the 1Password Connect Server instance to authenticate with.", instanceUrl: "The URL of the 1Password Connect Server instance to authenticate with.",
apiToken: "The API token used to access the 1Password Connect Server." apiToken: "The API token used to access the 1Password Connect Server."
},
FLYIO: {
accessToken: "The Access Token used to access fly.io."
} }
} }
}; };
@@ -2388,6 +2393,14 @@ export const SecretSyncs = {
HEROKU: { HEROKU: {
app: "The ID of the Heroku app to sync secrets to.", app: "The ID of the Heroku app to sync secrets to.",
appName: "The name of the Heroku app to sync secrets to." appName: "The name of the Heroku app to sync secrets to."
},
RENDER: {
serviceId: "The ID of the Render service to sync secrets to.",
scope: "The Render scope that secrets should be synced to.",
type: "The Render resource type to sync secrets to."
},
FLYIO: {
appId: "The ID of the Fly.io app to sync secrets to."
} }
} }
}; };

View File

@@ -262,8 +262,8 @@ const envSchema = z
DATADOG_HOSTNAME: zpStr(z.string().optional()), DATADOG_HOSTNAME: zpStr(z.string().optional()),
// PIT // PIT
PIT_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("2")), PIT_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("100")),
PIT_TREE_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("30")), PIT_TREE_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("200")),
/* CORS ----------------------------------------------------------------------------- */ /* CORS ----------------------------------------------------------------------------- */
CORS_ALLOWED_ORIGINS: zpStr( CORS_ALLOWED_ORIGINS: zpStr(

View File

@@ -39,6 +39,7 @@ import {
DatabricksConnectionListItemSchema, DatabricksConnectionListItemSchema,
SanitizedDatabricksConnectionSchema SanitizedDatabricksConnectionSchema
} from "@app/services/app-connection/databricks"; } from "@app/services/app-connection/databricks";
import { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio";
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp"; import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github"; import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
import { import {
@@ -61,6 +62,10 @@ import {
PostgresConnectionListItemSchema, PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema SanitizedPostgresConnectionSchema
} from "@app/services/app-connection/postgres"; } from "@app/services/app-connection/postgres";
import {
RenderConnectionListItemSchema,
SanitizedRenderConnectionSchema
} from "@app/services/app-connection/render/render-connection-schema";
import { import {
SanitizedTeamCityConnectionSchema, SanitizedTeamCityConnectionSchema,
TeamCityConnectionListItemSchema TeamCityConnectionListItemSchema
@@ -102,7 +107,9 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedOCIConnectionSchema.options, ...SanitizedOCIConnectionSchema.options,
...SanitizedOracleDBConnectionSchema.options, ...SanitizedOracleDBConnectionSchema.options,
...SanitizedOnePassConnectionSchema.options, ...SanitizedOnePassConnectionSchema.options,
...SanitizedHerokuConnectionSchema.options ...SanitizedHerokuConnectionSchema.options,
...SanitizedRenderConnectionSchema.options,
...SanitizedFlyioConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -130,7 +137,9 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
OCIConnectionListItemSchema, OCIConnectionListItemSchema,
OracleDBConnectionListItemSchema, OracleDBConnectionListItemSchema,
OnePassConnectionListItemSchema, OnePassConnectionListItemSchema,
HerokuConnectionListItemSchema HerokuConnectionListItemSchema,
RenderConnectionListItemSchema,
FlyioConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

@@ -0,0 +1,51 @@
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 {
CreateFlyioConnectionSchema,
SanitizedFlyioConnectionSchema,
UpdateFlyioConnectionSchema
} from "@app/services/app-connection/flyio";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerFlyioConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Flyio,
server,
sanitizedResponseSchema: SanitizedFlyioConnectionSchema,
createSchema: CreateFlyioConnectionSchema,
updateSchema: UpdateFlyioConnectionSchema
});
// The following endpoints are for internal Infisical App use only and not part of the public API
server.route({
method: "GET",
url: `/:connectionId/apps`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const apps = await server.services.appConnection.flyio.listApps(connectionId, req.permission);
return apps;
}
});
};

View File

@@ -11,6 +11,7 @@ import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-r
import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router"; import { registerAzureKeyVaultConnectionRouter } from "./azure-key-vault-connection-router";
import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerCamundaConnectionRouter } from "./camunda-connection-router";
import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router";
import { registerGitHubRadarConnectionRouter } from "./github-radar-connection-router"; import { registerGitHubRadarConnectionRouter } from "./github-radar-connection-router";
@@ -21,6 +22,7 @@ import { registerLdapConnectionRouter } from "./ldap-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerMySqlConnectionRouter } from "./mysql-connection-router"; import { registerMySqlConnectionRouter } from "./mysql-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router"; import { registerPostgresConnectionRouter } from "./postgres-connection-router";
import { registerRenderConnectionRouter } from "./render-connection-router";
import { registerTeamCityConnectionRouter } from "./teamcity-connection-router"; import { registerTeamCityConnectionRouter } from "./teamcity-connection-router";
import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router"; import { registerTerraformCloudConnectionRouter } from "./terraform-cloud-router";
import { registerVercelConnectionRouter } from "./vercel-connection-router"; import { registerVercelConnectionRouter } from "./vercel-connection-router";
@@ -54,5 +56,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.OCI]: registerOCIConnectionRouter, [AppConnection.OCI]: registerOCIConnectionRouter,
[AppConnection.OracleDB]: registerOracleDBConnectionRouter, [AppConnection.OracleDB]: registerOracleDBConnectionRouter,
[AppConnection.OnePass]: registerOnePassConnectionRouter, [AppConnection.OnePass]: registerOnePassConnectionRouter,
[AppConnection.Heroku]: registerHerokuConnectionRouter [AppConnection.Heroku]: registerHerokuConnectionRouter,
[AppConnection.Render]: registerRenderConnectionRouter,
[AppConnection.Flyio]: registerFlyioConnectionRouter
}; };

View File

@@ -0,0 +1,52 @@
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 {
CreateRenderConnectionSchema,
SanitizedRenderConnectionSchema,
UpdateRenderConnectionSchema
} from "@app/services/app-connection/render/render-connection-schema";
import { AuthMode } from "@app/services/auth/auth-type";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerRenderConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Render,
server,
sanitizedResponseSchema: SanitizedRenderConnectionSchema,
createSchema: CreateRenderConnectionSchema,
updateSchema: UpdateRenderConnectionSchema
});
// The below endpoints are not exposed and for Infisical App use
server.route({
method: "GET",
url: `/:connectionId/services`,
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
connectionId: z.string().uuid()
}),
response: {
200: z
.object({
id: z.string(),
name: z.string()
})
.array()
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { connectionId } = req.params;
const services = await server.services.appConnection.render.listServices(connectionId, req.permission);
return services;
}
});
};

View File

@@ -44,6 +44,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
name: z.string().trim().describe(IDENTITIES.CREATE.name), name: z.string().trim().describe(IDENTITIES.CREATE.name),
organizationId: z.string().trim().describe(IDENTITIES.CREATE.organizationId), organizationId: z.string().trim().describe(IDENTITIES.CREATE.organizationId),
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(IDENTITIES.CREATE.role), role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(IDENTITIES.CREATE.role),
hasDeleteProtection: z.boolean().default(false).describe(IDENTITIES.CREATE.hasDeleteProtection),
metadata: z metadata: z
.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }) .object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })
.array() .array()
@@ -75,6 +76,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
type: EventType.CREATE_IDENTITY, type: EventType.CREATE_IDENTITY,
metadata: { metadata: {
name: identity.name, name: identity.name,
hasDeleteProtection: identity.hasDeleteProtection,
identityId: identity.id identityId: identity.id
} }
} }
@@ -86,6 +88,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
properties: { properties: {
orgId: req.body.organizationId, orgId: req.body.organizationId,
name: identity.name, name: identity.name,
hasDeleteProtection: identity.hasDeleteProtection,
identityId: identity.id, identityId: identity.id,
...req.auditLogInfo ...req.auditLogInfo
} }
@@ -117,6 +120,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
name: z.string().trim().optional().describe(IDENTITIES.UPDATE.name), name: z.string().trim().optional().describe(IDENTITIES.UPDATE.name),
role: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.role), role: z.string().trim().min(1).optional().describe(IDENTITIES.UPDATE.role),
hasDeleteProtection: z.boolean().optional().describe(IDENTITIES.UPDATE.hasDeleteProtection),
metadata: z metadata: z
.object({ key: z.string().trim().min(1), value: z.string().trim().min(1) }) .object({ key: z.string().trim().min(1), value: z.string().trim().min(1) })
.array() .array()
@@ -148,6 +152,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
type: EventType.UPDATE_IDENTITY, type: EventType.UPDATE_IDENTITY,
metadata: { metadata: {
name: identity.name, name: identity.name,
hasDeleteProtection: identity.hasDeleteProtection,
identityId: identity.id identityId: identity.id
} }
} }
@@ -243,7 +248,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
permissions: true, permissions: true,
description: true description: true
}).optional(), }).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
authMethods: z.array(z.string()) authMethods: z.array(z.string())
}) })
}) })
@@ -292,7 +297,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
permissions: true, permissions: true,
description: true description: true
}).optional(), }).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
authMethods: z.array(z.string()) authMethods: z.array(z.string())
}) })
}).array(), }).array(),
@@ -386,7 +391,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
permissions: true, permissions: true,
description: true description: true
}).optional(), }).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
authMethods: z.array(z.string()) authMethods: z.array(z.string())
}) })
}).array(), }).array(),
@@ -451,7 +456,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
temporaryAccessEndTime: z.date().nullable().optional() temporaryAccessEndTime: z.date().nullable().optional()
}) })
), ),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({ identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
authMethods: z.array(z.string()) authMethods: z.array(z.string())
}), }),
project: SanitizedProjectSchema.pick({ name: true, id: true, type: true }) project: SanitizedProjectSchema.pick({ name: true, id: true, type: true })

View File

@@ -0,0 +1,13 @@
import { CreateFlyioSyncSchema, FlyioSyncSchema, UpdateFlyioSyncSchema } from "@app/services/secret-sync/flyio";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerFlyioSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Flyio,
server,
responseSchema: FlyioSyncSchema,
createSchema: CreateFlyioSyncSchema,
updateSchema: UpdateFlyioSyncSchema
});

View File

@@ -9,11 +9,13 @@ import { registerAzureDevOpsSyncRouter } from "./azure-devops-sync-router";
import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router"; import { registerAzureKeyVaultSyncRouter } from "./azure-key-vault-sync-router";
import { registerCamundaSyncRouter } from "./camunda-sync-router"; import { registerCamundaSyncRouter } from "./camunda-sync-router";
import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router";
import { registerFlyioSyncRouter } from "./flyio-sync-router";
import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router";
import { registerGitHubSyncRouter } from "./github-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router";
import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; import { registerHCVaultSyncRouter } from "./hc-vault-sync-router";
import { registerHerokuSyncRouter } from "./heroku-sync-router"; import { registerHerokuSyncRouter } from "./heroku-sync-router";
import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router";
import { registerRenderSyncRouter } from "./render-sync-router";
import { registerTeamCitySyncRouter } from "./teamcity-sync-router"; import { registerTeamCitySyncRouter } from "./teamcity-sync-router";
import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router";
import { registerVercelSyncRouter } from "./vercel-sync-router"; import { registerVercelSyncRouter } from "./vercel-sync-router";
@@ -39,5 +41,7 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record<SecretSync, (server: Fastif
[SecretSync.TeamCity]: registerTeamCitySyncRouter, [SecretSync.TeamCity]: registerTeamCitySyncRouter,
[SecretSync.OCIVault]: registerOCIVaultSyncRouter, [SecretSync.OCIVault]: registerOCIVaultSyncRouter,
[SecretSync.OnePass]: registerOnePassSyncRouter, [SecretSync.OnePass]: registerOnePassSyncRouter,
[SecretSync.Heroku]: registerHerokuSyncRouter [SecretSync.Heroku]: registerHerokuSyncRouter,
[SecretSync.Render]: registerRenderSyncRouter,
[SecretSync.Flyio]: registerFlyioSyncRouter
}; };

View File

@@ -0,0 +1,17 @@
import {
CreateRenderSyncSchema,
RenderSyncSchema,
UpdateRenderSyncSchema
} from "@app/services/secret-sync/render/render-sync-schemas";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
export const registerRenderSyncRouter = async (server: FastifyZodProvider) =>
registerSyncSecretsEndpoints({
destination: SecretSync.Render,
server,
responseSchema: RenderSyncSchema,
createSchema: CreateRenderSyncSchema,
updateSchema: UpdateRenderSyncSchema
});

View File

@@ -23,11 +23,13 @@ import { AzureDevOpsSyncListItemSchema, AzureDevOpsSyncSchema } from "@app/servi
import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault"; import { AzureKeyVaultSyncListItemSchema, AzureKeyVaultSyncSchema } from "@app/services/secret-sync/azure-key-vault";
import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda"; import { CamundaSyncListItemSchema, CamundaSyncSchema } from "@app/services/secret-sync/camunda";
import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks"; import { DatabricksSyncListItemSchema, DatabricksSyncSchema } from "@app/services/secret-sync/databricks";
import { FlyioSyncListItemSchema, FlyioSyncSchema } from "@app/services/secret-sync/flyio";
import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp"; import { GcpSyncListItemSchema, GcpSyncSchema } from "@app/services/secret-sync/gcp";
import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github"; import { GitHubSyncListItemSchema, GitHubSyncSchema } from "@app/services/secret-sync/github";
import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault"; import { HCVaultSyncListItemSchema, HCVaultSyncSchema } from "@app/services/secret-sync/hc-vault";
import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret-sync/heroku"; import { HerokuSyncListItemSchema, HerokuSyncSchema } from "@app/services/secret-sync/heroku";
import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec"; import { HumanitecSyncListItemSchema, HumanitecSyncSchema } from "@app/services/secret-sync/humanitec";
import { RenderSyncListItemSchema, RenderSyncSchema } from "@app/services/secret-sync/render/render-sync-schemas";
import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity"; import { TeamCitySyncListItemSchema, TeamCitySyncSchema } from "@app/services/secret-sync/teamcity";
import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud"; import { TerraformCloudSyncListItemSchema, TerraformCloudSyncSchema } from "@app/services/secret-sync/terraform-cloud";
import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel"; import { VercelSyncListItemSchema, VercelSyncSchema } from "@app/services/secret-sync/vercel";
@@ -51,7 +53,9 @@ const SecretSyncSchema = z.discriminatedUnion("destination", [
TeamCitySyncSchema, TeamCitySyncSchema,
OCIVaultSyncSchema, OCIVaultSyncSchema,
OnePassSyncSchema, OnePassSyncSchema,
HerokuSyncSchema HerokuSyncSchema,
RenderSyncSchema,
FlyioSyncSchema
]); ]);
const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
@@ -72,7 +76,9 @@ const SecretSyncOptionsSchema = z.discriminatedUnion("destination", [
TeamCitySyncListItemSchema, TeamCitySyncListItemSchema,
OCIVaultSyncListItemSchema, OCIVaultSyncListItemSchema,
OnePassSyncListItemSchema, OnePassSyncListItemSchema,
HerokuSyncListItemSchema HerokuSyncListItemSchema,
RenderSyncListItemSchema,
FlyioSyncListItemSchema
]); ]);
export const registerSecretSyncRouter = async (server: FastifyZodProvider) => { export const registerSecretSyncRouter = async (server: FastifyZodProvider) => {

View File

@@ -23,7 +23,9 @@ export enum AppConnection {
OCI = "oci", OCI = "oci",
OracleDB = "oracledb", OracleDB = "oracledb",
OnePass = "1password", OnePass = "1password",
Heroku = "heroku" Heroku = "heroku",
Render = "render",
Flyio = "flyio"
} }
export enum AWSRegion { export enum AWSRegion {

View File

@@ -56,6 +56,7 @@ import {
getDatabricksConnectionListItem, getDatabricksConnectionListItem,
validateDatabricksConnectionCredentials validateDatabricksConnectionCredentials
} from "./databricks"; } from "./databricks";
import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio";
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp"; import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github"; import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
import { import {
@@ -79,6 +80,8 @@ import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums"; import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums";
import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns"; import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns";
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres"; import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
import { RenderConnectionMethod } from "./render/render-connection-enums";
import { getRenderConnectionListItem, validateRenderConnectionCredentials } from "./render/render-connection-fns";
import { import {
getTeamCityConnectionListItem, getTeamCityConnectionListItem,
TeamCityConnectionMethod, TeamCityConnectionMethod,
@@ -123,7 +126,9 @@ export const listAppConnectionOptions = () => {
getOCIConnectionListItem(), getOCIConnectionListItem(),
getOracleDBConnectionListItem(), getOracleDBConnectionListItem(),
getOnePassConnectionListItem(), getOnePassConnectionListItem(),
getHerokuConnectionListItem() getHerokuConnectionListItem(),
getRenderConnectionListItem(),
getFlyioConnectionListItem()
].sort((a, b) => a.name.localeCompare(b.name)); ].sort((a, b) => a.name.localeCompare(b.name));
}; };
@@ -199,7 +204,9 @@ export const validateAppConnectionCredentials = async (
[AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.OCI]: validateOCIConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.OracleDB]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.OracleDB]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.OnePass]: validateOnePassConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Heroku]: validateHerokuConnectionCredentials as TAppConnectionCredentialsValidator [AppConnection.Heroku]: validateHerokuConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Render]: validateRenderConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator
}; };
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection); return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
@@ -244,6 +251,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case HCVaultConnectionMethod.AccessToken: case HCVaultConnectionMethod.AccessToken:
case TeamCityConnectionMethod.AccessToken: case TeamCityConnectionMethod.AccessToken:
case AzureDevOpsConnectionMethod.AccessToken: case AzureDevOpsConnectionMethod.AccessToken:
case FlyioConnectionMethod.AccessToken:
return "Access Token"; return "Access Token";
case Auth0ConnectionMethod.ClientCredentials: case Auth0ConnectionMethod.ClientCredentials:
return "Client Credentials"; return "Client Credentials";
@@ -251,6 +259,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
return "App Role"; return "App Role";
case LdapConnectionMethod.SimpleBind: case LdapConnectionMethod.SimpleBind:
return "Simple Bind"; return "Simple Bind";
case RenderConnectionMethod.ApiKey:
return "API Key";
default: default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new Error(`Unhandled App Connection Method: ${method}`); throw new Error(`Unhandled App Connection Method: ${method}`);
@@ -306,7 +316,9 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.OCI]: platformManagedCredentialsNotSupported, [AppConnection.OCI]: platformManagedCredentialsNotSupported,
[AppConnection.OracleDB]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform, [AppConnection.OracleDB]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
[AppConnection.OnePass]: platformManagedCredentialsNotSupported, [AppConnection.OnePass]: platformManagedCredentialsNotSupported,
[AppConnection.Heroku]: platformManagedCredentialsNotSupported [AppConnection.Heroku]: platformManagedCredentialsNotSupported,
[AppConnection.Render]: platformManagedCredentialsNotSupported,
[AppConnection.Flyio]: platformManagedCredentialsNotSupported
}; };
export const enterpriseAppCheck = async ( export const enterpriseAppCheck = async (

View File

@@ -25,7 +25,9 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.OCI]: "OCI", [AppConnection.OCI]: "OCI",
[AppConnection.OracleDB]: "OracleDB", [AppConnection.OracleDB]: "OracleDB",
[AppConnection.OnePass]: "1Password", [AppConnection.OnePass]: "1Password",
[AppConnection.Heroku]: "Heroku" [AppConnection.Heroku]: "Heroku",
[AppConnection.Render]: "Render",
[AppConnection.Flyio]: "Fly.io"
}; };
export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = { export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanType> = {
@@ -53,5 +55,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.OracleDB]: AppConnectionPlanType.Enterprise, [AppConnection.OracleDB]: AppConnectionPlanType.Enterprise,
[AppConnection.OnePass]: AppConnectionPlanType.Regular, [AppConnection.OnePass]: AppConnectionPlanType.Regular,
[AppConnection.MySql]: AppConnectionPlanType.Regular, [AppConnection.MySql]: AppConnectionPlanType.Regular,
[AppConnection.Heroku]: AppConnectionPlanType.Regular [AppConnection.Heroku]: AppConnectionPlanType.Regular,
[AppConnection.Render]: AppConnectionPlanType.Regular,
[AppConnection.Flyio]: AppConnectionPlanType.Regular
}; };

View File

@@ -49,6 +49,8 @@ import { ValidateCamundaConnectionCredentialsSchema } from "./camunda";
import { camundaConnectionService } from "./camunda/camunda-connection-service"; import { camundaConnectionService } from "./camunda/camunda-connection-service";
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks"; import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
import { databricksConnectionService } from "./databricks/databricks-connection-service"; import { databricksConnectionService } from "./databricks/databricks-connection-service";
import { ValidateFlyioConnectionCredentialsSchema } from "./flyio";
import { flyioConnectionService } from "./flyio/flyio-connection-service";
import { ValidateGcpConnectionCredentialsSchema } from "./gcp"; import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
import { gcpConnectionService } from "./gcp/gcp-connection-service"; import { gcpConnectionService } from "./gcp/gcp-connection-service";
import { ValidateGitHubConnectionCredentialsSchema } from "./github"; import { ValidateGitHubConnectionCredentialsSchema } from "./github";
@@ -64,6 +66,8 @@ import { ValidateLdapConnectionCredentialsSchema } from "./ldap";
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql"; import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
import { ValidateMySqlConnectionCredentialsSchema } from "./mysql"; import { ValidateMySqlConnectionCredentialsSchema } from "./mysql";
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres"; import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
import { ValidateRenderConnectionCredentialsSchema } from "./render/render-connection-schema";
import { renderConnectionService } from "./render/render-connection-service";
import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity"; import { ValidateTeamCityConnectionCredentialsSchema } from "./teamcity";
import { teamcityConnectionService } from "./teamcity/teamcity-connection-service"; import { teamcityConnectionService } from "./teamcity/teamcity-connection-service";
import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud"; import { ValidateTerraformCloudConnectionCredentialsSchema } from "./terraform-cloud";
@@ -107,7 +111,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.OCI]: ValidateOCIConnectionCredentialsSchema, [AppConnection.OCI]: ValidateOCIConnectionCredentialsSchema,
[AppConnection.OracleDB]: ValidateOracleDBConnectionCredentialsSchema, [AppConnection.OracleDB]: ValidateOracleDBConnectionCredentialsSchema,
[AppConnection.OnePass]: ValidateOnePassConnectionCredentialsSchema, [AppConnection.OnePass]: ValidateOnePassConnectionCredentialsSchema,
[AppConnection.Heroku]: ValidateHerokuConnectionCredentialsSchema [AppConnection.Heroku]: ValidateHerokuConnectionCredentialsSchema,
[AppConnection.Render]: ValidateRenderConnectionCredentialsSchema,
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema
}; };
export const appConnectionServiceFactory = ({ export const appConnectionServiceFactory = ({
@@ -513,6 +519,8 @@ export const appConnectionServiceFactory = ({
teamcity: teamcityConnectionService(connectAppConnectionById), teamcity: teamcityConnectionService(connectAppConnectionById),
oci: ociConnectionService(connectAppConnectionById, licenseService), oci: ociConnectionService(connectAppConnectionById, licenseService),
onepass: onePassConnectionService(connectAppConnectionById), onepass: onePassConnectionService(connectAppConnectionById),
heroku: herokuConnectionService(connectAppConnectionById, appConnectionDAL, kmsService) heroku: herokuConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
render: renderConnectionService(connectAppConnectionById),
flyio: flyioConnectionService(connectAppConnectionById)
}; };
}; };

View File

@@ -68,6 +68,12 @@ import {
TDatabricksConnectionInput, TDatabricksConnectionInput,
TValidateDatabricksConnectionCredentialsSchema TValidateDatabricksConnectionCredentialsSchema
} from "./databricks"; } from "./databricks";
import {
TFlyioConnection,
TFlyioConnectionConfig,
TFlyioConnectionInput,
TValidateFlyioConnectionCredentialsSchema
} from "./flyio";
import { import {
TGcpConnection, TGcpConnection,
TGcpConnectionConfig, TGcpConnectionConfig,
@@ -117,6 +123,12 @@ import {
TPostgresConnectionInput, TPostgresConnectionInput,
TValidatePostgresConnectionCredentialsSchema TValidatePostgresConnectionCredentialsSchema
} from "./postgres"; } from "./postgres";
import {
TRenderConnection,
TRenderConnectionConfig,
TRenderConnectionInput,
TValidateRenderConnectionCredentialsSchema
} from "./render/render-connection-types";
import { import {
TTeamCityConnection, TTeamCityConnection,
TTeamCityConnectionConfig, TTeamCityConnectionConfig,
@@ -168,6 +180,8 @@ export type TAppConnection = { id: string } & (
| TOracleDBConnection | TOracleDBConnection
| TOnePassConnection | TOnePassConnection
| THerokuConnection | THerokuConnection
| TRenderConnection
| TFlyioConnection
); );
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>; export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
@@ -200,6 +214,8 @@ export type TAppConnectionInput = { id: string } & (
| TOracleDBConnectionInput | TOracleDBConnectionInput
| TOnePassConnectionInput | TOnePassConnectionInput
| THerokuConnectionInput | THerokuConnectionInput
| TRenderConnectionInput
| TFlyioConnectionInput
); );
export type TSqlConnectionInput = export type TSqlConnectionInput =
@@ -239,7 +255,9 @@ export type TAppConnectionConfig =
| TTeamCityConnectionConfig | TTeamCityConnectionConfig
| TOCIConnectionConfig | TOCIConnectionConfig
| TOnePassConnectionConfig | TOnePassConnectionConfig
| THerokuConnectionConfig; | THerokuConnectionConfig
| TRenderConnectionConfig
| TFlyioConnectionConfig;
export type TValidateAppConnectionCredentialsSchema = export type TValidateAppConnectionCredentialsSchema =
| TValidateAwsConnectionCredentialsSchema | TValidateAwsConnectionCredentialsSchema
@@ -266,7 +284,9 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateOCIConnectionCredentialsSchema | TValidateOCIConnectionCredentialsSchema
| TValidateOracleDBConnectionCredentialsSchema | TValidateOracleDBConnectionCredentialsSchema
| TValidateOnePassConnectionCredentialsSchema | TValidateOnePassConnectionCredentialsSchema
| TValidateHerokuConnectionCredentialsSchema; | TValidateHerokuConnectionCredentialsSchema
| TValidateRenderConnectionCredentialsSchema
| TValidateFlyioConnectionCredentialsSchema;
export type TListAwsConnectionKmsKeys = { export type TListAwsConnectionKmsKeys = {
connectionId: string; connectionId: string;

View File

@@ -0,0 +1,3 @@
export enum FlyioConnectionMethod {
AccessToken = "access-token"
}

View File

@@ -0,0 +1,72 @@
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { FlyioConnectionMethod } from "./flyio-connection-enums";
import { TFlyioApp, TFlyioConnection, TFlyioConnectionConfig } from "./flyio-connection-types";
export const getFlyioConnectionListItem = () => {
return {
name: "Fly.io" as const,
app: AppConnection.Flyio as const,
methods: Object.values(FlyioConnectionMethod) as [FlyioConnectionMethod.AccessToken]
};
};
export const validateFlyioConnectionCredentials = async (config: TFlyioConnectionConfig) => {
const { accessToken } = config.credentials;
try {
const resp = await request.post<{ data: { viewer: { id: string | null; email: string } } | null }>(
IntegrationUrls.FLYIO_API_URL,
{ query: "query { viewer { id email } }" },
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
}
);
if (resp.data.data === null) {
throw new BadRequestError({
message: "Unable to validate connection: Invalid access token provided."
});
}
} 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 config.credentials;
};
export const listFlyioApps = async (appConnection: TFlyioConnection) => {
const { accessToken } = appConnection.credentials;
const resp = await request.post<{ data: { apps: { nodes: TFlyioApp[] } } }>(
IntegrationUrls.FLYIO_API_URL,
{
query:
"query GetApps { apps { nodes { id name hostname status organization { id slug } currentRelease { version status createdAt } } } }"
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
Accept: "application/json"
}
}
);
return resp.data.data.apps.nodes;
};

View File

@@ -0,0 +1,62 @@
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 { FlyioConnectionMethod } from "./flyio-connection-enums";
export const FlyioConnectionAccessTokenCredentialsSchema = z.object({
accessToken: z
.string()
.trim()
.min(1, "Access Token required")
.max(1000)
.startsWith("FlyV1", "Token must start with 'FlyV1'")
.describe(AppConnections.CREDENTIALS.FLYIO.accessToken)
});
const BaseFlyioConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Flyio) });
export const FlyioConnectionSchema = BaseFlyioConnectionSchema.extend({
method: z.literal(FlyioConnectionMethod.AccessToken),
credentials: FlyioConnectionAccessTokenCredentialsSchema
});
export const SanitizedFlyioConnectionSchema = z.discriminatedUnion("method", [
BaseFlyioConnectionSchema.extend({
method: z.literal(FlyioConnectionMethod.AccessToken),
credentials: FlyioConnectionAccessTokenCredentialsSchema.pick({})
})
]);
export const ValidateFlyioConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(FlyioConnectionMethod.AccessToken).describe(AppConnections.CREATE(AppConnection.Flyio).method),
credentials: FlyioConnectionAccessTokenCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Flyio).credentials
)
})
]);
export const CreateFlyioConnectionSchema = ValidateFlyioConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Flyio)
);
export const UpdateFlyioConnectionSchema = z
.object({
credentials: FlyioConnectionAccessTokenCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Flyio).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Flyio));
export const FlyioConnectionListItemSchema = z.object({
name: z.literal("Fly.io"),
app: z.literal(AppConnection.Flyio),
methods: z.nativeEnum(FlyioConnectionMethod).array()
});

View File

@@ -0,0 +1,30 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listFlyioApps } from "./flyio-connection-fns";
import { TFlyioConnection } from "./flyio-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TFlyioConnection>;
export const flyioConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listApps = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Flyio, connectionId, actor);
try {
const apps = await listFlyioApps(appConnection);
return apps;
} catch (error) {
logger.error(error, "Failed to establish connection with fly.io");
return [];
}
};
return {
listApps
};
};

View File

@@ -0,0 +1,27 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateFlyioConnectionSchema,
FlyioConnectionSchema,
ValidateFlyioConnectionCredentialsSchema
} from "./flyio-connection-schemas";
export type TFlyioConnection = z.infer<typeof FlyioConnectionSchema>;
export type TFlyioConnectionInput = z.infer<typeof CreateFlyioConnectionSchema> & {
app: AppConnection.Flyio;
};
export type TValidateFlyioConnectionCredentialsSchema = typeof ValidateFlyioConnectionCredentialsSchema;
export type TFlyioConnectionConfig = DiscriminativePick<TFlyioConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TFlyioApp = {
id: string;
name: string;
};

View File

@@ -0,0 +1,4 @@
export * from "./flyio-connection-enums";
export * from "./flyio-connection-fns";
export * from "./flyio-connection-schemas";
export * from "./flyio-connection-types";

View File

@@ -0,0 +1,3 @@
export enum RenderConnectionMethod {
ApiKey = "api-key"
}

View File

@@ -0,0 +1,88 @@
/* eslint-disable no-await-in-loop */
import { AxiosError } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { AppConnection } from "../app-connection-enums";
import { RenderConnectionMethod } from "./render-connection-enums";
import {
TRawRenderService,
TRenderConnection,
TRenderConnectionConfig,
TRenderService
} from "./render-connection-types";
export const getRenderConnectionListItem = () => {
return {
name: "Render" as const,
app: AppConnection.Render as const,
methods: Object.values(RenderConnectionMethod) as [RenderConnectionMethod.ApiKey]
};
};
export const listRenderServices = async (appConnection: TRenderConnection): Promise<TRenderService[]> => {
const {
credentials: { apiKey }
} = appConnection;
const services: TRenderService[] = [];
let hasMorePages = true;
const perPage = 100;
let cursor;
while (hasMorePages) {
const res: TRawRenderService[] = (
await request.get<TRawRenderService[]>(`${IntegrationUrls.RENDER_API_URL}/v1/services`, {
params: new URLSearchParams({
...(cursor ? { cursor: String(cursor) } : {}),
limit: String(perPage)
}),
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
"Accept-Encoding": "application/json"
}
})
).data;
res.forEach((item) => {
services.push({
name: item.service.name,
id: item.service.id
});
});
if (res.length < perPage) {
hasMorePages = false;
} else {
cursor = res[res.length - 1].cursor;
}
}
return services;
};
export const validateRenderConnectionCredentials = async (config: TRenderConnectionConfig) => {
const { credentials: inputCredentials } = config;
try {
await request.get(`${IntegrationUrls.RENDER_API_URL}/v1/users`, {
headers: {
Authorization: `Bearer ${inputCredentials.apiKey}`
}
});
} 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 inputCredentials;
};

View File

@@ -0,0 +1,56 @@
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 { RenderConnectionMethod } from "./render-connection-enums";
export const RenderConnectionApiKeyCredentialsSchema = z.object({
apiKey: z.string().trim().min(1, "API key required").max(256, "API key cannot exceed 256 characters")
});
const BaseRenderConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Render) });
export const RenderConnectionSchema = BaseRenderConnectionSchema.extend({
method: z.literal(RenderConnectionMethod.ApiKey),
credentials: RenderConnectionApiKeyCredentialsSchema
});
export const SanitizedRenderConnectionSchema = z.discriminatedUnion("method", [
BaseRenderConnectionSchema.extend({
method: z.literal(RenderConnectionMethod.ApiKey),
credentials: RenderConnectionApiKeyCredentialsSchema.pick({})
})
]);
export const ValidateRenderConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(RenderConnectionMethod.ApiKey).describe(AppConnections.CREATE(AppConnection.Render).method),
credentials: RenderConnectionApiKeyCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Render).credentials
)
})
]);
export const CreateRenderConnectionSchema = ValidateRenderConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Render)
);
export const UpdateRenderConnectionSchema = z
.object({
credentials: RenderConnectionApiKeyCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Render).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Render));
export const RenderConnectionListItemSchema = z.object({
name: z.literal("Render"),
app: z.literal(AppConnection.Render),
methods: z.nativeEnum(RenderConnectionMethod).array()
});

View File

@@ -0,0 +1,30 @@
import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { listRenderServices } from "./render-connection-fns";
import { TRenderConnection } from "./render-connection-types";
type TGetAppConnectionFunc = (
app: AppConnection,
connectionId: string,
actor: OrgServiceActor
) => Promise<TRenderConnection>;
export const renderConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
const listServices = async (connectionId: string, actor: OrgServiceActor) => {
const appConnection = await getAppConnection(AppConnection.Render, connectionId, actor);
try {
const services = await listRenderServices(appConnection);
return services;
} catch (error) {
logger.error(error, "Failed to list services for Render connection");
return [];
}
};
return {
listServices
};
};

View File

@@ -0,0 +1,35 @@
import z from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import {
CreateRenderConnectionSchema,
RenderConnectionSchema,
ValidateRenderConnectionCredentialsSchema
} from "./render-connection-schema";
export type TRenderConnection = z.infer<typeof RenderConnectionSchema>;
export type TRenderConnectionInput = z.infer<typeof CreateRenderConnectionSchema> & {
app: AppConnection.Render;
};
export type TValidateRenderConnectionCredentialsSchema = typeof ValidateRenderConnectionCredentialsSchema;
export type TRenderConnectionConfig = DiscriminativePick<TRenderConnectionInput, "method" | "app" | "credentials"> & {
orgId: string;
};
export type TRenderService = {
name: string;
id: string;
};
export type TRawRenderService = {
cursor: string;
service: {
id: string;
name: string;
};
};

View File

@@ -9,6 +9,7 @@ const arnRegex = new RE2(/^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/
export const validateAccountIds = z export const validateAccountIds = z
.string() .string()
.trim() .trim()
.max(2048)
.default("") .default("")
// Custom validation to ensure each part is a 12-digit number // Custom validation to ensure each part is a 12-digit number
.refine( .refine(
@@ -36,6 +37,7 @@ export const validateAccountIds = z
export const validatePrincipalArns = z export const validatePrincipalArns = z
.string() .string()
.trim() .trim()
.max(2048)
.default("") .default("")
// Custom validation for ARN format // Custom validation for ARN format
.refine( .refine(

View File

@@ -101,6 +101,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("id").as("identityId").withSchema(TableName.Identity), db.ref("id").as("identityId").withSchema(TableName.Identity),
db.ref("name").as("identityName").withSchema(TableName.Identity), db.ref("name").as("identityName").withSchema(TableName.Identity),
db.ref("hasDeleteProtection").withSchema(TableName.Identity),
db.ref("id").withSchema(TableName.IdentityProjectMembership), db.ref("id").withSchema(TableName.IdentityProjectMembership),
db.ref("role").withSchema(TableName.IdentityProjectMembershipRole), db.ref("role").withSchema(TableName.IdentityProjectMembershipRole),
db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"), db.ref("id").withSchema(TableName.IdentityProjectMembershipRole).as("membershipRoleId"),
@@ -130,6 +131,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
data: docs, data: docs,
parentMapper: ({ parentMapper: ({
identityName, identityName,
hasDeleteProtection,
uaId, uaId,
awsId, awsId,
gcpId, gcpId,
@@ -151,6 +153,7 @@ export const identityProjectDALFactory = (db: TDbClient) => {
identity: { identity: {
id: identityId, id: identityId,
name: identityName, name: identityName,
hasDeleteProtection,
authMethods: buildAuthMethods({ authMethods: buildAuthMethods({
uaId, uaId,
awsId, awsId,

View File

@@ -114,16 +114,18 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth), db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth), db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth), db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth),
db.ref("name").withSchema(TableName.Identity) db.ref("name").withSchema(TableName.Identity),
db.ref("hasDeleteProtection").withSchema(TableName.Identity)
); );
if (data) { if (data) {
const { name } = data; const { name, hasDeleteProtection } = data;
return { return {
...data, ...data,
identity: { identity: {
id: data.identityId, id: data.identityId,
name, name,
hasDeleteProtection,
authMethods: buildAuthMethods(data) authMethods: buildAuthMethods(data)
} }
}; };
@@ -155,7 +157,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection) .orderBy(`${TableName.Identity}.${orderBy}`, orderDirection)
.select( .select(
selectAllTableCols(TableName.IdentityOrgMembership), selectAllTableCols(TableName.IdentityOrgMembership),
db.ref("name").withSchema(TableName.Identity).as("identityName") db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("hasDeleteProtection").withSchema(TableName.Identity)
) )
.where(filter) .where(filter)
.as("paginatedIdentity"); .as("paginatedIdentity");
@@ -245,6 +248,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("updatedAt").withSchema("paginatedIdentity"), db.ref("updatedAt").withSchema("paginatedIdentity"),
db.ref("identityId").withSchema("paginatedIdentity").as("identityId"), db.ref("identityId").withSchema("paginatedIdentity").as("identityId"),
db.ref("identityName").withSchema("paginatedIdentity"), db.ref("identityName").withSchema("paginatedIdentity"),
db.ref("hasDeleteProtection").withSchema("paginatedIdentity"),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
@@ -286,6 +290,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
crName, crName,
identityId, identityId,
identityName, identityName,
hasDeleteProtection,
role, role,
roleId, roleId,
id, id,
@@ -324,6 +329,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
identity: { identity: {
id: identityId, id: identityId,
name: identityName, name: identityName,
hasDeleteProtection,
authMethods: buildAuthMethods({ authMethods: buildAuthMethods({
uaId, uaId,
alicloudId, alicloudId,
@@ -476,6 +482,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
db.ref("updatedAt").withSchema(TableName.IdentityOrgMembership), db.ref("updatedAt").withSchema(TableName.IdentityOrgMembership),
db.ref("identityId").withSchema(TableName.IdentityOrgMembership).as("identityId"), db.ref("identityId").withSchema(TableName.IdentityOrgMembership).as("identityId"),
db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("hasDeleteProtection").withSchema(TableName.Identity),
db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth), db.ref("id").as("uaId").withSchema(TableName.IdentityUniversalAuth),
db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth), db.ref("id").as("gcpId").withSchema(TableName.IdentityGcpAuth),
@@ -518,6 +525,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
crName, crName,
identityId, identityId,
identityName, identityName,
hasDeleteProtection,
role, role,
roleId, roleId,
total_count, total_count,
@@ -556,6 +564,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
identity: { identity: {
id: identityId, id: identityId,
name: identityName, name: identityName,
hasDeleteProtection,
authMethods: buildAuthMethods({ authMethods: buildAuthMethods({
uaId, uaId,
alicloudId, alicloudId,

View File

@@ -47,6 +47,7 @@ export const identityServiceFactory = ({
const createIdentity = async ({ const createIdentity = async ({
name, name,
role, role,
hasDeleteProtection,
actor, actor,
orgId, orgId,
actorId, actorId,
@@ -96,7 +97,7 @@ export const identityServiceFactory = ({
} }
const identity = await identityDAL.transaction(async (tx) => { const identity = await identityDAL.transaction(async (tx) => {
const newIdentity = await identityDAL.create({ name }, tx); const newIdentity = await identityDAL.create({ name, hasDeleteProtection }, tx);
await identityOrgMembershipDAL.create( await identityOrgMembershipDAL.create(
{ {
identityId: newIdentity.id, identityId: newIdentity.id,
@@ -138,6 +139,7 @@ export const identityServiceFactory = ({
const updateIdentity = async ({ const updateIdentity = async ({
id, id,
role, role,
hasDeleteProtection,
name, name,
actor, actor,
actorId, actorId,
@@ -189,7 +191,11 @@ export const identityServiceFactory = ({
} }
const identity = await identityDAL.transaction(async (tx) => { const identity = await identityDAL.transaction(async (tx) => {
const newIdentity = name ? await identityDAL.updateById(id, { name }, tx) : await identityDAL.findById(id, tx); const newIdentity =
name || hasDeleteProtection
? await identityDAL.updateById(id, { name, hasDeleteProtection }, tx)
: await identityDAL.findById(id, tx);
if (role) { if (role) {
await identityOrgMembershipDAL.updateById( await identityOrgMembershipDAL.updateById(
identityOrgMembership.id, identityOrgMembership.id,
@@ -272,6 +278,9 @@ export const identityServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
if (identityOrgMembership.identity.hasDeleteProtection)
throw new BadRequestError({ message: "Identity has delete protection" });
const deletedIdentity = await identityDAL.deleteById(id); const deletedIdentity = await identityDAL.deleteById(id);
await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.orgId); await licenseService.updateSubscriptionOrgMemberCount(identityOrgMembership.orgId);

View File

@@ -5,12 +5,14 @@ import { OrderByDirection, TOrgPermission } from "@app/lib/types";
export type TCreateIdentityDTO = { export type TCreateIdentityDTO = {
role: string; role: string;
name: string; name: string;
hasDeleteProtection: boolean;
metadata?: { key: string; value: string }[]; metadata?: { key: string; value: string }[];
} & TOrgPermission; } & TOrgPermission;
export type TUpdateIdentityDTO = { export type TUpdateIdentityDTO = {
id: string; id: string;
role?: string; role?: string;
hasDeleteProtection?: boolean;
name?: string; name?: string;
metadata?: { key: string; value: string }[]; metadata?: { key: string; value: string }[];
isActorSuperAdmin?: boolean; isActorSuperAdmin?: boolean;

View File

@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const FLYIO_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Fly.io",
destination: SecretSync.Flyio,
connection: AppConnection.Flyio,
canImportSecrets: false
};

View File

@@ -0,0 +1,133 @@
import { request } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import {
TDeleteFlyioVariable,
TFlyioListVariables,
TFlyioSecret,
TFlyioSyncWithCredentials,
TPutFlyioVariable
} from "@app/services/secret-sync/flyio/flyio-sync-types";
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps";
const listFlyioSecrets = async ({ accessToken, appId }: TFlyioListVariables) => {
const { data } = await request.post<{ data: { app: { secrets: TFlyioSecret[] } } }>(
IntegrationUrls.FLYIO_API_URL,
{
query: "query GetAppSecrets($appId: String!) { app(id: $appId) { id name secrets { name createdAt } } }",
variables: { appId }
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
Accept: "application/json"
}
}
);
return data.data.app.secrets.map((s) => s.name);
};
const putFlyioSecrets = async ({ accessToken, appId, secretMap }: TPutFlyioVariable) => {
return request.post(
IntegrationUrls.FLYIO_API_URL,
{
query:
"mutation SetAppSecrets($appId: ID!, $secrets: [SecretInput!]!) { setSecrets(input: { appId: $appId, secrets: $secrets }) { app { name } release { version } } }",
variables: {
appId,
secrets: Object.entries(secretMap).map(([key, { value }]) => ({ key, value }))
}
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
}
);
};
const deleteFlyioSecrets = async ({ accessToken, appId, keys }: TDeleteFlyioVariable) => {
return request.post(
IntegrationUrls.FLYIO_API_URL,
{
query:
"mutation UnsetAppSecrets($appId: ID!, $keys: [String!]!) { unsetSecrets(input: { appId: $appId, keys: $keys }) { app { name } release { version } } }",
variables: {
appId,
keys
}
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json"
}
}
);
};
export const FlyioSyncFns = {
syncSecrets: async (secretSync: TFlyioSyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
environment,
destinationConfig: { appId }
} = secretSync;
const { accessToken } = connection.credentials;
try {
await putFlyioSecrets({ accessToken, appId, secretMap });
} catch (error) {
throw new SecretSyncError({
error
});
}
if (secretSync.syncOptions.disableSecretDeletion) return;
const secrets = await listFlyioSecrets({ accessToken, appId });
const keys = secrets.filter(
(secret) =>
matchesSchema(secret, environment?.slug || "", secretSync.syncOptions.keySchema) && !(secret in secretMap)
);
try {
await deleteFlyioSecrets({ accessToken, appId, keys });
} catch (error) {
throw new SecretSyncError({
error
});
}
},
removeSecrets: async (secretSync: TFlyioSyncWithCredentials, secretMap: TSecretMap) => {
const {
connection,
destinationConfig: { appId }
} = secretSync;
const { accessToken } = connection.credentials;
const secrets = await listFlyioSecrets({ accessToken, appId });
const keys = secrets.filter((secret) => secret in secretMap);
try {
await deleteFlyioSecrets({ accessToken, appId, keys });
} catch (error) {
throw new SecretSyncError({
error
});
}
},
getSecrets: async (secretSync: TFlyioSyncWithCredentials) => {
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
}
};

View File

@@ -0,0 +1,43 @@
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 FlyioSyncDestinationConfigSchema = z.object({
appId: z.string().trim().min(1, "App required").max(255).describe(SecretSyncs.DESTINATION_CONFIG.FLYIO.appId)
});
const FlyioSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
export const FlyioSyncSchema = BaseSecretSyncSchema(SecretSync.Flyio, FlyioSyncOptionsConfig).extend({
destination: z.literal(SecretSync.Flyio),
destinationConfig: FlyioSyncDestinationConfigSchema
});
export const CreateFlyioSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.Flyio,
FlyioSyncOptionsConfig
).extend({
destinationConfig: FlyioSyncDestinationConfigSchema
});
export const UpdateFlyioSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.Flyio,
FlyioSyncOptionsConfig
).extend({
destinationConfig: FlyioSyncDestinationConfigSchema.optional()
});
export const FlyioSyncListItemSchema = z.object({
name: z.literal("Fly.io"),
connection: z.literal(AppConnection.Flyio),
destination: z.literal(SecretSync.Flyio),
canImportSecrets: z.literal(false)
});

View File

@@ -0,0 +1,32 @@
import { z } from "zod";
import { TFlyioConnection } from "@app/services/app-connection/flyio";
import { CreateFlyioSyncSchema, FlyioSyncListItemSchema, FlyioSyncSchema } from "./flyio-sync-schemas";
export type TFlyioSync = z.infer<typeof FlyioSyncSchema>;
export type TFlyioSyncInput = z.infer<typeof CreateFlyioSyncSchema>;
export type TFlyioSyncListItem = z.infer<typeof FlyioSyncListItemSchema>;
export type TFlyioSyncWithCredentials = TFlyioSync & {
connection: TFlyioConnection;
};
export type TFlyioSecret = {
name: string;
};
export type TFlyioListVariables = {
accessToken: string;
appId: string;
};
export type TPutFlyioVariable = TFlyioListVariables & {
secretMap: { [key: string]: { value: string } };
};
export type TDeleteFlyioVariable = TFlyioListVariables & {
keys: string[];
};

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
export const RENDER_SYNC_LIST_OPTION: TSecretSyncListItem = {
name: "Render",
destination: SecretSync.Render,
connection: AppConnection.Render,
canImportSecrets: true
};

View File

@@ -0,0 +1,8 @@
export enum RenderSyncScope {
Service = "service"
}
export enum RenderSyncType {
Env = "env",
File = "file"
}

View File

@@ -0,0 +1,134 @@
/* eslint-disable no-await-in-loop */
import { request } from "@app/lib/config/request";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
import { TRenderSecret, TRenderSyncWithCredentials } from "./render-sync-types";
const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials) => {
const {
destinationConfig,
connection: {
credentials: { apiKey }
}
} = secretSync;
const baseUrl = `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`;
const allSecrets: TRenderSecret[] = [];
let cursor: string | undefined;
do {
const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl;
const { data } = await request.get<
{
envVar: {
key: string;
value: string;
};
cursor: string;
}[]
>(url, {
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
});
const secrets = data.map((item) => ({
key: item.envVar.key,
value: item.envVar.value
}));
allSecrets.push(...secrets);
cursor = data[data.length - 1]?.cursor;
} while (cursor);
return allSecrets;
};
const putEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap, key: string) => {
const {
destinationConfig,
connection: {
credentials: { apiKey }
}
} = secretSync;
await request.put(
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${key}`,
{
key,
value: secretMap[key].value
},
{
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
}
);
};
const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: TRenderSecret) => {
const {
destinationConfig,
connection: {
credentials: { apiKey }
}
} = secretSync;
await request.delete(
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json"
}
}
);
};
const sleep = async () =>
new Promise((resolve) => {
setTimeout(resolve, 500);
});
export const RenderSyncFns = {
syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
for await (const key of Object.keys(secretMap)) {
await putEnvironmentSecret(secretSync, secretMap, key);
await sleep();
}
if (secretSync.syncOptions.disableSecretDeletion) return;
for await (const renderSecret of renderSecrets) {
if (!matchesSchema(renderSecret.key, secretSync.environment?.slug || "", secretSync.syncOptions.keySchema))
// eslint-disable-next-line no-continue
continue;
if (!secretMap[renderSecret.key]) {
await deleteEnvironmentSecret(secretSync, renderSecret);
await sleep();
}
}
},
getSecrets: async (secretSync: TRenderSyncWithCredentials): Promise<TSecretMap> => {
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
return Object.fromEntries(renderSecrets.map((secret) => [secret.key, { value: secret.value ?? "" }]));
},
removeSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
const encryptedSecrets = await getRenderEnvironmentSecrets(secretSync);
for await (const encryptedSecret of encryptedSecrets) {
if (encryptedSecret.key in secretMap) {
await deleteEnvironmentSecret(secretSync, encryptedSecret);
await sleep();
}
}
}
};

View File

@@ -0,0 +1,49 @@
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";
import { RenderSyncScope, RenderSyncType } from "./render-sync-enums";
const RenderSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
z.object({
scope: z.literal(RenderSyncScope.Service).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope),
serviceId: z.string().min(1, "Service ID is required").describe(SecretSyncs.DESTINATION_CONFIG.RENDER.serviceId),
type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type)
})
]);
const RenderSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
export const RenderSyncSchema = BaseSecretSyncSchema(SecretSync.Render, RenderSyncOptionsConfig).extend({
destination: z.literal(SecretSync.Render),
destinationConfig: RenderSyncDestinationConfigSchema
});
export const CreateRenderSyncSchema = GenericCreateSecretSyncFieldsSchema(
SecretSync.Render,
RenderSyncOptionsConfig
).extend({
destinationConfig: RenderSyncDestinationConfigSchema
});
export const UpdateRenderSyncSchema = GenericUpdateSecretSyncFieldsSchema(
SecretSync.Render,
RenderSyncOptionsConfig
).extend({
destinationConfig: RenderSyncDestinationConfigSchema.optional()
});
export const RenderSyncListItemSchema = z.object({
name: z.literal("Render"),
connection: z.literal(AppConnection.Render),
destination: z.literal(SecretSync.Render),
canImportSecrets: z.literal(true)
});

View File

@@ -0,0 +1,20 @@
import z from "zod";
import { TRenderConnection } from "@app/services/app-connection/render/render-connection-types";
import { CreateRenderSyncSchema, RenderSyncListItemSchema, RenderSyncSchema } from "./render-sync-schemas";
export type TRenderSyncListItem = z.infer<typeof RenderSyncListItemSchema>;
export type TRenderSync = z.infer<typeof RenderSyncSchema>;
export type TRenderSyncInput = z.infer<typeof CreateRenderSyncSchema>;
export type TRenderSyncWithCredentials = TRenderSync & {
connection: TRenderConnection;
};
export type TRenderSecret = {
key: string;
value: string;
};

View File

@@ -16,7 +16,9 @@ export enum SecretSync {
TeamCity = "teamcity", TeamCity = "teamcity",
OCIVault = "oci-vault", OCIVault = "oci-vault",
OnePass = "1password", OnePass = "1password",
Heroku = "heroku" Heroku = "heroku",
Render = "render",
Flyio = "flyio"
} }
export enum SecretSyncInitialSyncBehavior { export enum SecretSyncInitialSyncBehavior {

View File

@@ -29,12 +29,14 @@ import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFact
import { AZURE_DEVOPS_SYNC_LIST_OPTION, azureDevOpsSyncFactory } from "./azure-devops"; import { AZURE_DEVOPS_SYNC_LIST_OPTION, azureDevOpsSyncFactory } from "./azure-devops";
import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault"; import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda";
import { FLYIO_SYNC_LIST_OPTION, FlyioSyncFns } from "./flyio";
import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GCP_SYNC_LIST_OPTION } from "./gcp";
import { GcpSyncFns } from "./gcp/gcp-sync-fns"; import { GcpSyncFns } from "./gcp/gcp-sync-fns";
import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault"; import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault";
import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku"; import { HEROKU_SYNC_LIST_OPTION, HerokuSyncFns } from "./heroku";
import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec";
import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns";
import { RENDER_SYNC_LIST_OPTION, RenderSyncFns } from "./render";
import { SECRET_SYNC_PLAN_MAP } from "./secret-sync-maps"; import { SECRET_SYNC_PLAN_MAP } from "./secret-sync-maps";
import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity"; import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity";
import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud"; import { TERRAFORM_CLOUD_SYNC_LIST_OPTION, TerraformCloudSyncFns } from "./terraform-cloud";
@@ -59,7 +61,9 @@ const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
[SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION, [SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION,
[SecretSync.OCIVault]: OCI_VAULT_SYNC_LIST_OPTION, [SecretSync.OCIVault]: OCI_VAULT_SYNC_LIST_OPTION,
[SecretSync.OnePass]: ONEPASS_SYNC_LIST_OPTION, [SecretSync.OnePass]: ONEPASS_SYNC_LIST_OPTION,
[SecretSync.Heroku]: HEROKU_SYNC_LIST_OPTION [SecretSync.Heroku]: HEROKU_SYNC_LIST_OPTION,
[SecretSync.Render]: RENDER_SYNC_LIST_OPTION,
[SecretSync.Flyio]: FLYIO_SYNC_LIST_OPTION
}; };
export const listSecretSyncOptions = () => { export const listSecretSyncOptions = () => {
@@ -219,6 +223,10 @@ export const SecretSyncFns = {
return OCIVaultSyncFns.syncSecrets(secretSync, schemaSecretMap); return OCIVaultSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.OnePass: case SecretSync.OnePass:
return OnePassSyncFns.syncSecrets(secretSync, schemaSecretMap); return OnePassSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Render:
return RenderSyncFns.syncSecrets(secretSync, schemaSecretMap);
case SecretSync.Flyio:
return FlyioSyncFns.syncSecrets(secretSync, schemaSecretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -299,6 +307,12 @@ export const SecretSyncFns = {
case SecretSync.Heroku: case SecretSync.Heroku:
secretMap = await HerokuSyncFns.getSecrets(secretSync, { appConnectionDAL, kmsService }); secretMap = await HerokuSyncFns.getSecrets(secretSync, { appConnectionDAL, kmsService });
break; break;
case SecretSync.Render:
secretMap = await RenderSyncFns.getSecrets(secretSync);
break;
case SecretSync.Flyio:
secretMap = await FlyioSyncFns.getSecrets(secretSync);
break;
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
@@ -368,6 +382,10 @@ export const SecretSyncFns = {
return OnePassSyncFns.removeSecrets(secretSync, schemaSecretMap); return OnePassSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Heroku: case SecretSync.Heroku:
return HerokuSyncFns.removeSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService }); return HerokuSyncFns.removeSecrets(secretSync, schemaSecretMap, { appConnectionDAL, kmsService });
case SecretSync.Render:
return RenderSyncFns.removeSecrets(secretSync, schemaSecretMap);
case SecretSync.Flyio:
return FlyioSyncFns.removeSecrets(secretSync, schemaSecretMap);
default: default:
throw new Error( throw new Error(
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`

View File

@@ -19,7 +19,9 @@ export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
[SecretSync.TeamCity]: "TeamCity", [SecretSync.TeamCity]: "TeamCity",
[SecretSync.OCIVault]: "OCI Vault", [SecretSync.OCIVault]: "OCI Vault",
[SecretSync.OnePass]: "1Password", [SecretSync.OnePass]: "1Password",
[SecretSync.Heroku]: "Heroku" [SecretSync.Heroku]: "Heroku",
[SecretSync.Render]: "Render",
[SecretSync.Flyio]: "Fly.io"
}; };
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = { export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
@@ -40,7 +42,9 @@ export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
[SecretSync.TeamCity]: AppConnection.TeamCity, [SecretSync.TeamCity]: AppConnection.TeamCity,
[SecretSync.OCIVault]: AppConnection.OCI, [SecretSync.OCIVault]: AppConnection.OCI,
[SecretSync.OnePass]: AppConnection.OnePass, [SecretSync.OnePass]: AppConnection.OnePass,
[SecretSync.Heroku]: AppConnection.Heroku [SecretSync.Heroku]: AppConnection.Heroku,
[SecretSync.Render]: AppConnection.Render,
[SecretSync.Flyio]: AppConnection.Flyio
}; };
export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = { export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
@@ -61,5 +65,7 @@ export const SECRET_SYNC_PLAN_MAP: Record<SecretSync, SecretSyncPlanType> = {
[SecretSync.TeamCity]: SecretSyncPlanType.Regular, [SecretSync.TeamCity]: SecretSyncPlanType.Regular,
[SecretSync.OCIVault]: SecretSyncPlanType.Enterprise, [SecretSync.OCIVault]: SecretSyncPlanType.Enterprise,
[SecretSync.OnePass]: SecretSyncPlanType.Regular, [SecretSync.OnePass]: SecretSyncPlanType.Regular,
[SecretSync.Heroku]: SecretSyncPlanType.Regular [SecretSync.Heroku]: SecretSyncPlanType.Regular,
[SecretSync.Render]: SecretSyncPlanType.Regular,
[SecretSync.Flyio]: SecretSyncPlanType.Regular
}; };

View File

@@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -89,7 +89,17 @@ export const secretSyncServiceFactory = ({
projectId projectId
}); });
return secretSyncs as TSecretSync[]; return secretSyncs.filter((secretSync) =>
permission.can(
ProjectPermissionSecretSyncActions.Read,
secretSync.environment && secretSync.folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
: ProjectPermissionSub.SecretSyncs
)
) as TSecretSync[];
}; };
const listSecretSyncsBySecretPath = async ( const listSecretSyncsBySecretPath = async (
@@ -105,7 +115,15 @@ export const secretSyncServiceFactory = ({
projectId projectId
}); });
if (permission.cannot(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs)) { if (
permission.cannot(
ProjectPermissionSecretSyncActions.Read,
subject(ProjectPermissionSub.SecretSyncs, {
environment,
secretPath
})
)
) {
return []; return [];
} }
@@ -142,7 +160,12 @@ export const secretSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Read, ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSub.SecretSyncs secretSync.environment && secretSync.folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
: ProjectPermissionSub.SecretSyncs
); );
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
@@ -179,7 +202,12 @@ export const secretSyncServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Read, ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSub.SecretSyncs secretSync.environment && secretSync.folder
? subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
: ProjectPermissionSub.SecretSyncs
); );
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
@@ -217,13 +245,17 @@ export const secretSyncServiceFactory = ({
ForbiddenError.from(projectPermission).throwUnlessCan( ForbiddenError.from(projectPermission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Create, ProjectPermissionSecretSyncActions.Create,
ProjectPermissionSub.SecretSyncs subject(ProjectPermissionSub.SecretSyncs, { environment, secretPath })
); );
throwIfMissingSecretReadValueOrDescribePermission(projectPermission, ProjectPermissionSecretActions.ReadValue, { throwIfMissingSecretReadValueOrDescribePermission(
environment, projectPermission,
secretPath ProjectPermissionSecretActions.DescribeSecret,
}); {
environment,
secretPath
}
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
@@ -286,10 +318,38 @@ export const secretSyncServiceFactory = ({
projectId: secretSync.projectId projectId: secretSync.projectId
}); });
ForbiddenError.from(permission).throwUnlessCan( // we always check the permission against the existing environment / secret path
ProjectPermissionSecretSyncActions.Edit, // if no secret path / environment is present on the secret sync, we need to check without conditions
ProjectPermissionSub.SecretSyncs if (secretSync.environment?.slug && secretSync.folder?.path) {
); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Edit,
subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Edit,
ProjectPermissionSub.SecretSyncs
);
}
// if the user is updating the secret path or environment, we need to check the permission against the new values
if (secretPath || environment) {
const environmentToCheck = environment || secretSync.environment?.slug || "";
const secretPathToCheck = secretPath || secretSync.folder?.path || "";
if (environmentToCheck && secretPathToCheck) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Edit,
subject(ProjectPermissionSub.SecretSyncs, {
environment: environmentToCheck,
secretPath: secretPathToCheck
})
);
}
}
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({ throw new BadRequestError({
@@ -315,7 +375,7 @@ export const secretSyncServiceFactory = ({
if (!updatedEnvironment || !updatedSecretPath) if (!updatedEnvironment || !updatedSecretPath)
throw new BadRequestError({ message: "Must specify both source environment and secret path" }); throw new BadRequestError({ message: "Must specify both source environment and secret path" });
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, { throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
environment: updatedEnvironment, environment: updatedEnvironment,
secretPath: updatedSecretPath secretPath: updatedSecretPath
}); });
@@ -374,10 +434,20 @@ export const secretSyncServiceFactory = ({
projectId: secretSync.projectId projectId: secretSync.projectId
}); });
ForbiddenError.from(permission).throwUnlessCan( if (secretSync.environment?.slug && secretSync.folder?.path) {
ProjectPermissionSecretSyncActions.Delete, ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSub.SecretSyncs ProjectPermissionSecretSyncActions.Delete,
); subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.Delete,
ProjectPermissionSub.SecretSyncs
);
}
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({ throw new BadRequestError({
@@ -441,10 +511,20 @@ export const secretSyncServiceFactory = ({
projectId: secretSync.projectId projectId: secretSync.projectId
}); });
ForbiddenError.from(permission).throwUnlessCan( if (secretSync.environment?.slug && secretSync.folder?.path) {
ProjectPermissionSecretSyncActions.SyncSecrets, ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSub.SecretSyncs ProjectPermissionSecretSyncActions.SyncSecrets,
); subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.SyncSecrets,
ProjectPermissionSub.SecretSyncs
);
}
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({ throw new BadRequestError({
@@ -503,10 +583,20 @@ export const secretSyncServiceFactory = ({
projectId: secretSync.projectId projectId: secretSync.projectId
}); });
ForbiddenError.from(permission).throwUnlessCan( if (secretSync.environment?.slug && secretSync.folder?.path) {
ProjectPermissionSecretSyncActions.ImportSecrets, ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSub.SecretSyncs ProjectPermissionSecretSyncActions.ImportSecrets,
); subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.ImportSecrets,
ProjectPermissionSub.SecretSyncs
);
}
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({ throw new BadRequestError({
@@ -559,10 +649,20 @@ export const secretSyncServiceFactory = ({
projectId: secretSync.projectId projectId: secretSync.projectId
}); });
ForbiddenError.from(permission).throwUnlessCan( if (secretSync.environment?.slug && secretSync.folder?.path) {
ProjectPermissionSecretSyncActions.RemoveSecrets, ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSub.SecretSyncs ProjectPermissionSecretSyncActions.RemoveSecrets,
); subject(ProjectPermissionSub.SecretSyncs, {
environment: secretSync.environment.slug,
secretPath: secretSync.folder.path
})
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretSyncActions.RemoveSecrets,
ProjectPermissionSub.SecretSyncs
);
}
if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination]) if (secretSync.connection.app !== SECRET_SYNC_CONNECTION_MAP[destination])
throw new BadRequestError({ throw new BadRequestError({

View File

@@ -72,6 +72,7 @@ import {
TAzureKeyVaultSyncListItem, TAzureKeyVaultSyncListItem,
TAzureKeyVaultSyncWithCredentials TAzureKeyVaultSyncWithCredentials
} from "./azure-key-vault"; } from "./azure-key-vault";
import { TFlyioSync, TFlyioSyncInput, TFlyioSyncListItem, TFlyioSyncWithCredentials } from "./flyio/flyio-sync-types";
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
import { import {
THCVaultSync, THCVaultSync,
@@ -86,6 +87,12 @@ import {
THumanitecSyncListItem, THumanitecSyncListItem,
THumanitecSyncWithCredentials THumanitecSyncWithCredentials
} from "./humanitec"; } from "./humanitec";
import {
TRenderSync,
TRenderSyncInput,
TRenderSyncListItem,
TRenderSyncWithCredentials
} from "./render/render-sync-types";
import { import {
TTeamCitySync, TTeamCitySync,
TTeamCitySyncInput, TTeamCitySyncInput,
@@ -118,7 +125,9 @@ export type TSecretSync =
| TTeamCitySync | TTeamCitySync
| TOCIVaultSync | TOCIVaultSync
| TOnePassSync | TOnePassSync
| THerokuSync; | THerokuSync
| TRenderSync
| TFlyioSync;
export type TSecretSyncWithCredentials = export type TSecretSyncWithCredentials =
| TAwsParameterStoreSyncWithCredentials | TAwsParameterStoreSyncWithCredentials
@@ -138,7 +147,9 @@ export type TSecretSyncWithCredentials =
| TTeamCitySyncWithCredentials | TTeamCitySyncWithCredentials
| TOCIVaultSyncWithCredentials | TOCIVaultSyncWithCredentials
| TOnePassSyncWithCredentials | TOnePassSyncWithCredentials
| THerokuSyncWithCredentials; | THerokuSyncWithCredentials
| TRenderSyncWithCredentials
| TFlyioSyncWithCredentials;
export type TSecretSyncInput = export type TSecretSyncInput =
| TAwsParameterStoreSyncInput | TAwsParameterStoreSyncInput
@@ -158,7 +169,9 @@ export type TSecretSyncInput =
| TTeamCitySyncInput | TTeamCitySyncInput
| TOCIVaultSyncInput | TOCIVaultSyncInput
| TOnePassSyncInput | TOnePassSyncInput
| THerokuSyncInput; | THerokuSyncInput
| TRenderSyncInput
| TFlyioSyncInput;
export type TSecretSyncListItem = export type TSecretSyncListItem =
| TAwsParameterStoreSyncListItem | TAwsParameterStoreSyncListItem
@@ -178,7 +191,9 @@ export type TSecretSyncListItem =
| TTeamCitySyncListItem | TTeamCitySyncListItem
| TOCIVaultSyncListItem | TOCIVaultSyncListItem
| TOnePassSyncListItem | TOnePassSyncListItem
| THerokuSyncListItem; | THerokuSyncListItem
| TRenderSyncListItem
| TFlyioSyncListItem;
export type TSyncOptionsConfig = { export type TSyncOptionsConfig = {
canImportSecrets: boolean; canImportSecrets: boolean;

View File

@@ -81,6 +81,7 @@ export type TMachineIdentityCreatedEvent = {
event: PostHogEventTypes.MachineIdentityCreated; event: PostHogEventTypes.MachineIdentityCreated;
properties: { properties: {
name: string; name: string;
hasDeleteProtection: boolean;
orgId: string; orgId: string;
identityId: string; identityId: string;
}; };

View File

@@ -4,7 +4,7 @@ sidebarTitle: "Summary template"
--- ---
```plain ```plain
Date: MM/DD/YY-MM/DD/YY Date: MM/DD/YY-MM/DD/YY (day)
Notable incidents: Notable incidents:
- [<open/resolved>] <details of the incident including who was impacted. what you did to mitigate/patch the issue> - [<open/resolved>] <details of the incident including who was impacted. what you did to mitigate/patch the issue>

View File

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

View File

@@ -0,0 +1,8 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/flyio"
---
<Note>
Check out the configuration docs for [Fly.io Connections](/integrations/app-connections/flyio) to learn how to obtain the required credentials.
</Note>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/flyio/{connectionId}"
---
<Note>
Check out the configuration docs for [Fly.io Connections](/integrations/app-connections/flyio) to learn how to obtain the required credentials.
</Note>

View File

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

View File

@@ -0,0 +1,10 @@
---
title: "Create"
openapi: "POST /api/v1/app-connections/render"
---
<Note>
Check out the configuration docs for [Render
Connections](/integrations/app-connections/render) to learn how to obtain the
required credentials.
</Note>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
---
title: "Update"
openapi: "PATCH /api/v1/app-connections/render/{connectionId}"
---
<Note>
Check out the configuration docs for [Render
Connections](/integrations/app-connections/render) to learn how to obtain the
required credentials.
</Note>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 969 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 KiB

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