Compare commits

..

13 Commits

Author SHA1 Message Date
Tuan Dang
ca453df9e9 Minor updates to integration update PR 2024-04-21 17:36:54 -07:00
Vladyslav Matsiiako
c959fa6fdd add initial sync options to terraform cloud integration 2024-04-20 21:40:07 -07:00
Vladyslav Matsiiako
d11ded9abc allow specifying of aws kms key 2024-04-20 18:40:56 -07:00
Vladyslav Matsiiako
714a3186a9 allowed creating of multiple tags 2024-04-19 17:46:33 -07:00
Maidul Islam
a087deb1eb Update envars.mdx 2024-04-18 22:03:14 -04:00
Maidul Islam
7ce283e891 Merge pull request #1710 from Infisical/daniel/dashboard
Chore: Documentation
2024-04-18 21:19:52 -04:00
Daniel Hougaard
52cf38449b Chore: Documentation 2024-04-19 03:08:55 +02:00
BlackMagiq
8d6f76698a Merge pull request #1709 from Infisical/docs-auth
Add security/description to project endpoint schemas for API reference
2024-04-18 17:11:08 -07:00
Tuan Dang
71cc84c9a5 Add security/description to project endpoint schemas 2024-04-18 17:06:35 -07:00
BlackMagiq
5d95d7f31d Merge pull request #1708 from Infisical/vercel-pagination
Add pagination to getAppsVercel
2024-04-18 16:24:23 -07:00
Tuan Dang
2f15e0e767 Add pagination to getAppsVercel 2024-04-18 16:20:51 -07:00
Daniel Hougaard
6e1b29025b Fix: Invite project member 2024-04-19 00:33:51 +02:00
Daniel Hougaard
fcc18996d3 Merge pull request #1706 from Infisical/daniel/fix-breaking-change-check
Fix: API Breaking Change Check
2024-04-18 23:39:50 +02:00
29 changed files with 632 additions and 102 deletions

View File

@@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability";
import { ForbiddenError, subject } from "@casl/ability";
import { TableName, TSecretTagJunctionInsert } from "@app/db/schemas";
import { BadRequestError, InternalServerError } from "@app/lib/errors";
@@ -23,6 +23,7 @@ import {
import { TSnapshotDALFactory } from "./snapshot-dal";
import { TSnapshotFolderDALFactory } from "./snapshot-folder-dal";
import { TSnapshotSecretDALFactory } from "./snapshot-secret-dal";
import { getFullFolderPath } from "./snapshot-service-fns";
type TSecretSnapshotServiceFactoryDep = {
snapshotDAL: TSnapshotDALFactory;
@@ -33,7 +34,7 @@ type TSecretSnapshotServiceFactoryDep = {
secretDAL: Pick<TSecretDALFactory, "delete" | "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
folderDAL: Pick<TSecretFolderDALFactory, "findById" | "findBySecretPath" | "delete" | "insertMany">;
folderDAL: Pick<TSecretFolderDALFactory, "findById" | "findBySecretPath" | "delete" | "insertMany" | "find">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "isValidLicense">;
};
@@ -71,6 +72,12 @@ export const secretSnapshotServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
@@ -98,6 +105,12 @@ export const secretSnapshotServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
);
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) throw new BadRequestError({ message: "Folder not found" });
@@ -116,6 +129,19 @@ export const secretSnapshotServiceFactory = ({
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const fullFolderPath = await getFullFolderPath({
folderDAL,
folderId: snapshot.folderId,
envId: snapshot.environment.id
});
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment: snapshot.environment.slug, secretPath: fullFolderPath })
);
return snapshot;
};

View File

@@ -101,6 +101,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
key: "snapshotId",
parentMapper: ({
snapshotId: id,
folderId,
projectId,
envId,
envSlug,
@@ -109,6 +110,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
snapshotUpdatedAt: updatedAt
}) => ({
id,
folderId,
projectId,
createdAt,
updatedAt,

View File

@@ -0,0 +1,28 @@
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
type GetFullFolderPath = {
folderDAL: Pick<TSecretFolderDALFactory, "findById" | "find">; // Added findAllInEnv
folderId: string;
envId: string;
};
export const getFullFolderPath = async ({ folderDAL, folderId, envId }: GetFullFolderPath): Promise<string> => {
// Helper function to remove duplicate slashes
const removeDuplicateSlashes = (path: string) => path.replace(/\/{2,}/g, "/");
// Fetch all folders at once based on environment ID to avoid multiple queries
const folders = await folderDAL.find({ envId });
const folderMap = new Map(folders.map((folder) => [folder.id, folder]));
const buildPath = (currFolderId: string): string => {
const folder = folderMap.get(currFolderId);
if (!folder) return "";
const folderPathSegment = !folder.parentId && folder.name === "root" ? "/" : `/${folder.name}`;
if (folder.parentId) {
return removeDuplicateSlashes(`${buildPath(folder.parentId)}${folderPathSegment}`);
}
return removeDuplicateSlashes(folderPathSegment);
};
return buildPath(folderId);
};

View File

@@ -585,12 +585,13 @@ export const INTEGRATION = {
region: "AWS region to sync secrets to.",
scope: "Scope of the provider. Used by Github, Qovery",
metadata: {
secretPrefix: "The prefix for the saved secret. Used by GCP",
secretSuffix: "The suffix for the saved secret. Used by GCP",
initialSyncBehavoir: "Type of syncing behavoir with the integration",
shouldAutoRedeploy: "Used by Render to trigger auto deploy",
secretGCPLabel: "The label for the GCP secrets",
secretAWSTag: "The tag for the AWS secrets"
secretPrefix: "The prefix for the saved secret. Used by GCP.",
secretSuffix: "The suffix for the saved secret. Used by GCP.",
initialSyncBehavoir: "Type of syncing behavoir with the integration.",
shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
secretGCPLabel: "The label for GCP secrets.",
secretAWSTag: "The tags for AWS secrets.",
kmsKeyId: "The ID of the encryption key from AWS KMS."
}
},
UPDATE: {

View File

@@ -511,6 +511,39 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
}
});
server.route({
method: "GET",
url: "/:integrationAuthId/aws-secrets-manager/kms-keys",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
integrationAuthId: z.string().trim()
}),
querystring: z.object({
region: z.string().trim()
}),
response: {
200: z.object({
kmsKeys: z.object({ id: z.string(), alias: z.string() }).array()
})
}
},
handler: async (req) => {
const kmsKeys = await server.services.integrationAuth.getAwsKmsKeys({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationAuthId,
region: req.query.region
});
return { kmsKeys };
}
});
server.route({
method: "GET",
url: "/:integrationAuthId/qovery/projects",

View File

@@ -58,14 +58,17 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
.optional()
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
secretAWSTag: z
.object({
key: z.string(),
value: z.string()
})
.array(
z.object({
key: z.string(),
value: z.string()
})
)
.optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag)
.describe(INTEGRATION.CREATE.metadata.secretAWSTag),
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId)
})
.optional()
.default({})
}),
response: {
200: z.object({

View File

@@ -138,6 +138,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit
},
schema: {
description: "Get project",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(PROJECTS.GET.workspaceId)
}),
@@ -170,6 +176,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: writeLimit
},
schema: {
description: "Delete project",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(PROJECTS.DELETE.workspaceId)
}),
@@ -239,6 +251,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: writeLimit
},
schema: {
description: "Update project",
security: [
{
bearerAuth: []
}
],
params: z.object({
workspaceId: z.string().trim().describe(PROJECTS.UPDATE.workspaceId)
}),

View File

@@ -15,6 +15,12 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
rateLimit: writeLimit
},
schema: {
description: "Invite members to project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().describe(PROJECTS.INVITE_MEMBER.projectId)
}),
@@ -64,10 +70,15 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider
rateLimit: writeLimit
},
schema: {
description: "Remove members from project",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectId: z.string().describe(PROJECTS.REMOVE_MEMBER.projectId)
}),
body: z.object({
emails: z.string().email().array().default([]).describe(PROJECTS.REMOVE_MEMBER.emails),
usernames: z.string().array().default([]).describe(PROJECTS.REMOVE_MEMBER.usernames)

View File

@@ -144,6 +144,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: creationLimit
},
schema: {
description: "Create a new project",
security: [
{
bearerAuth: []
}
],
body: z.object({
projectName: z.string().trim().describe(PROJECTS.CREATE.projectName),
slug: z
@@ -195,6 +201,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: writeLimit
},
schema: {
description: "Delete project",
security: [
{
bearerAuth: []
}
],
params: z.object({
slug: slugSchema.describe("The slug of the project to delete.")
}),

View File

@@ -129,26 +129,55 @@ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
* Return list of names of apps for Vercel integration
*/
const getAppsVercel = async ({ accessToken, teamId }: { teamId?: string | null; accessToken: string }) => {
const res = (
await request.get<{ projects: { name: string; id: string }[] }>(`${IntegrationUrls.VERCEL_API_URL}/v9/projects`, {
const apps: Array<{ name: string; appId: string }> = [];
const limit = "20";
let hasMorePages = true;
let next: number | null = null;
interface Response {
projects: { name: string; id: string }[];
pagination: {
count: number;
next: number | null;
prev: number;
};
}
while (hasMorePages) {
const params: { [key: string]: string } = {
limit
};
if (teamId) {
params.teamId = teamId;
}
if (next) {
params.until = String(next);
}
const { data } = await request.get<Response>(`${IntegrationUrls.VERCEL_API_URL}/v9/projects`, {
params: new URLSearchParams(params),
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
},
...(teamId
? {
params: {
teamId
}
}
: {})
})
).data;
}
});
const apps = res.projects.map((a) => ({
name: a.name,
appId: a.id
}));
data.projects.forEach((a) => {
apps.push({
name: a.name,
appId: a.id
});
});
next = data.pagination.next;
if (data.pagination.next === null) {
hasMorePages = false;
}
}
return apps;
};

View File

@@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability";
import { Octokit } from "@octokit/rest";
import AWS from "aws-sdk";
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -23,6 +24,7 @@ import {
TGetIntegrationAuthTeamCityBuildConfigDTO,
THerokuPipelineCoupling,
TIntegrationAuthAppsDTO,
TIntegrationAuthAwsKmsKeyDTO,
TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthGithubEnvsDTO,
@@ -534,6 +536,52 @@ export const integrationAuthServiceFactory = ({
return data.results.map(({ name, id: orgId }) => ({ name, orgId }));
};
const getAwsKmsKeys = async ({
actorId,
actor,
actorOrgId,
actorAuthMethod,
id,
region
}: TIntegrationAuthAwsKmsKeyDTO) => {
const integrationAuth = await integrationAuthDAL.findById(id);
if (!integrationAuth) throw new BadRequestError({ message: "Failed to find integration" });
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
const botKey = await projectBotService.getBotKey(integrationAuth.projectId);
const { accessId, accessToken } = await getIntegrationAccessToken(integrationAuth, botKey);
AWS.config.update({
region,
credentials: {
accessKeyId: String(accessId),
secretAccessKey: accessToken
}
});
const kms = new AWS.KMS();
const aliases = await kms.listAliases({}).promise();
const keys = await kms.listKeys({}).promise();
const response = keys
.Keys!.map((key) => {
const keyAlias = aliases.Aliases!.find((alias) => key.KeyId === alias.TargetKeyId);
if (!keyAlias?.AliasName?.includes("alias/aws/") || keyAlias?.AliasName?.includes("alias/aws/secretsmanager")) {
return { id: String(key.KeyId), alias: String(keyAlias?.AliasName || key.KeyId) };
}
return { id: "null", alias: "null" };
})
.filter((elem) => elem.id !== "null");
return response;
};
const getQoveryProjects = async ({
actorId,
actor,
@@ -1133,6 +1181,7 @@ export const integrationAuthServiceFactory = ({
getIntegrationApps,
getVercelBranches,
getApps,
getAwsKmsKeys,
getGithubOrgs,
getGithubEnvs,
getChecklyGroups,

View File

@@ -63,6 +63,11 @@ export type TIntegrationAuthQoveryProjectDTO = {
orgId: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthAwsKmsKeyDTO = {
id: string;
region: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthQoveryEnvironmentsDTO = {
id: string;
} & TProjectPermission;

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
@@ -489,7 +490,9 @@ const syncSecretsAWSParameterStore = async ({
Type: "SecureString",
Value: secrets[key].value,
// Overwrite: true,
Tags: metadata.secretAWSTag ? [{ Key: metadata.secretAWSTag.key, Value: metadata.secretAWSTag.value }] : []
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value }))
: []
})
.promise();
// case: secret exists in AWS parameter store
@@ -579,7 +582,10 @@ const syncSecretsAWSSecretManager = async ({
new CreateSecretCommand({
Name: integration.app as string,
SecretString: JSON.stringify(secKeyVal),
Tags: metadata.secretAWSTag ? [{ Key: metadata.secretAWSTag.key, Value: metadata.secretAWSTag.value }] : []
KmsKeyId: metadata.kmsKeyId ? metadata.kmsKeyId : null,
Tags: metadata.secretAWSTag
? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value }))
: []
})
);
}
@@ -2151,16 +2157,29 @@ const syncSecretsQovery = async ({
* @param {String} obj.accessToken - access token for Terraform Cloud API
*/
const syncSecretsTerraformCloud = async ({
createManySecretsRawFn,
updateManySecretsRawFn,
integration,
secrets,
accessToken
accessToken,
integrationDAL
}: {
integration: TIntegrations;
secrets: Record<string, { value: string; comment?: string }>;
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
integration: TIntegrations & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
};
secrets: Record<string, { value: string; comment?: string } | null>;
accessToken: string;
integrationDAL: Pick<TIntegrationDALFactory, "updateById">;
}) => {
// get secrets from Terraform Cloud
const getSecretsRes = (
const terraformSecrets = (
await request.get<{ data: { attributes: { key: string; value: string }; id: string }[] }>(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`,
{
@@ -2178,9 +2197,74 @@ const syncSecretsTerraformCloud = async ({
{} as Record<string, { attributes: { key: string; value: string }; id: string }>
);
const secretsToAdd: { [key: string]: string } = {};
const secretsToUpdate: { [key: string]: string } = {};
const metadata = z.record(z.any()).parse(integration.metadata);
Object.keys(terraformSecrets).forEach((key) => {
if (!integration.lastUsed) {
// first time using integration
// -> apply initial sync behavior
switch (metadata.initialSyncBehavior) {
case IntegrationInitialSyncBehavior.PREFER_TARGET: {
if (!(key in secrets)) {
secretsToAdd[key] = terraformSecrets[key].attributes.value;
} else if (secrets[key]?.value !== terraformSecrets[key].attributes.value) {
secretsToUpdate[key] = terraformSecrets[key].attributes.value;
}
secrets[key] = {
value: terraformSecrets[key].attributes.value
};
break;
}
case IntegrationInitialSyncBehavior.PREFER_SOURCE: {
if (!(key in secrets)) {
secrets[key] = {
value: terraformSecrets[key].attributes.value
};
secretsToAdd[key] = terraformSecrets[key].attributes.value;
}
break;
}
default: {
break;
}
}
} else if (!(key in secrets)) secrets[key] = null;
});
if (Object.keys(secretsToAdd).length) {
await createManySecretsRawFn({
projectId: integration.projectId,
environment: integration.environment.slug,
path: integration.secretPath,
secrets: Object.keys(secretsToAdd).map((key) => ({
secretName: key,
secretValue: secretsToAdd[key],
type: SecretType.Shared,
secretComment: ""
}))
});
}
if (Object.keys(secretsToUpdate).length) {
await updateManySecretsRawFn({
projectId: integration.projectId,
environment: integration.environment.slug,
path: integration.secretPath,
secrets: Object.keys(secretsToUpdate).map((key) => ({
secretName: key,
secretValue: secretsToUpdate[key],
type: SecretType.Shared,
secretComment: ""
}))
});
}
// create or update secrets on Terraform Cloud
for await (const key of Object.keys(secrets)) {
if (!(key in getSecretsRes)) {
if (!(key in terraformSecrets)) {
// case: secret does not exist in Terraform Cloud
// -> add secret
await request.post(
@@ -2190,7 +2274,7 @@ const syncSecretsTerraformCloud = async ({
type: "vars",
attributes: {
key,
value: secrets[key].value,
value: secrets[key]?.value,
category: integration.targetService
}
}
@@ -2204,17 +2288,17 @@ const syncSecretsTerraformCloud = async ({
}
);
// case: secret exists in Terraform Cloud
} else if (secrets[key].value !== getSecretsRes[key].attributes.value) {
} else if (secrets[key]?.value !== terraformSecrets[key].attributes.value) {
// -> update secret
await request.patch(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${getSecretsRes[key].id}`,
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${terraformSecrets[key].id}`,
{
data: {
type: "vars",
id: getSecretsRes[key].id,
id: terraformSecrets[key].id,
attributes: {
...getSecretsRes[key],
value: secrets[key].value
...terraformSecrets[key],
value: secrets[key]?.value
}
}
},
@@ -2229,11 +2313,11 @@ const syncSecretsTerraformCloud = async ({
}
}
for await (const key of Object.keys(getSecretsRes)) {
for await (const key of Object.keys(terraformSecrets)) {
if (!(key in secrets)) {
// case: delete secret
await request.delete(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${getSecretsRes[key].id}`,
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars/${terraformSecrets[key].id}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
@@ -2244,6 +2328,10 @@ const syncSecretsTerraformCloud = async ({
);
}
}
await integrationDAL.updateById(integration.id, {
lastUsed: new Date()
});
};
/**
@@ -3285,9 +3373,12 @@ export const syncIntegrationSecrets = async ({
break;
case Integrations.TERRAFORM_CLOUD:
await syncSecretsTerraformCloud({
createManySecretsRawFn,
updateManySecretsRawFn,
integration,
secrets,
accessToken
accessToken,
integrationDAL
});
break;
case Integrations.HASHICORP_VAULT:

View File

@@ -25,7 +25,8 @@ export type TCreateIntegrationDTO = {
secretAWSTag?: {
key: string;
value: string;
};
}[];
kmsKeyId?: string;
};
} & Omit<TProjectPermission, "projectId">;

View File

@@ -134,8 +134,7 @@ export const projectMembershipServiceFactory = ({
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ userId }) => ({
projectId,
userId: userId as string,
role: ProjectMembershipRole.Member
userId: userId as string
})),
tx
);
@@ -267,8 +266,7 @@ export const projectMembershipServiceFactory = ({
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ user }) => ({
projectId,
userId: user.id,
role: ProjectMembershipRole.Member
userId: user.id
})),
tx
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 584 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -30,7 +30,7 @@ Prerequisites:
"ssm:DeleteParameter",
"ssm:GetParametersByPath",
"ssm:DeleteParameters",
"ssm:AddTagsToResource"
"ssm:AddTagsToResource" // if you need to add tags to secrets
],
"Resource": "*"
}

View File

@@ -29,13 +29,16 @@ Prerequisites:
"secretsmanager:GetSecretValue",
"secretsmanager:CreateSecret",
"secretsmanager:UpdateSecret",
"secretsmanager:TagResource"
"secretsmanager:TagResource", // if you need to add tags to secrets
"kms:ListKeys", // if you need to specify the KMS key
"kms:ListAliases" // if you need to specify the KMS key
],
"Resource": "*"
}
]
}
```
</Step>
<Step title="Authorize Infisical for AWS Secrets Manager">
Obtain a AWS access key ID and secret access key for your IAM user in IAM > Users > User > Security credentials > Access keys
@@ -43,7 +46,7 @@ Prerequisites:
![access key 1](../../images/integrations/aws/integrations-aws-access-key-1.png)
![access key 2](../../images/integrations/aws/integrations-aws-access-key-2.png)
![access key 3](../../images/integrations/aws/integrations-aws-access-key-3.png)
Navigate to your project's integrations tab in Infisical.
![integrations](../../images/integrations.png)
@@ -52,12 +55,6 @@ Prerequisites:
![integration auth](../../images/integrations/aws/integrations-aws-secret-manager-auth.png)
<Info>
If this is your project's first cloud integration, then you'll have to grant
Infisical access to your project's environment variables. Although this step
breaks E2EE, it's necessary for Infisical to sync the environment variables to
the cloud platform.
</Info>
</Step>
<Step title="Start integration">
Select how you want to integration to work by specifying a number of parameters:
@@ -72,13 +69,23 @@ Prerequisites:
The region that you want to integrate with in AWS Secrets Manager.
</ParamField>
<ParamField path="AWS SM Secret Name" type="string" required>
The secret name/path in AWS into which you want to sync the secrets from Infisical.
The secret name/path in AWS into which you want to sync the secrets from Infisical.
</ParamField>
Then, press `Create Integration` to start syncing secrets to AWS Secrets Manager.
![integration create](../../images/integrations/aws/integrations-aws-secret-manager-create.png)
Optionally, you can add tags or specify the encryption key of all the secrets created via this integration:
<ParamField path="Secret Tag" type="string" optional>
The Key/Value of a tag that will be added to secrets in AWS. Please note that it is possible to add multiple tags via API.
</ParamField>
<ParamField path="Encryption Key" type="string" optional>
The alias/ID of the AWS KMS key used for encryption. Please note that key should be enabled in order to work and the IAM user should have access to it.
</ParamField>
![integration options](../../images/integrations/aws/integrations-aws-secret-manager-options.png)
Then, press `Create Integration` to start syncing secrets to AWS Secrets Manager.
<Info>
Infisical currently syncs environment variables to AWS Secrets Manager as
key-value pairs under one secret. We're actively exploring ways to help users
@@ -88,5 +95,6 @@ Prerequisites:
<Info>
Please note that upon deleting secrets in Infisical, AWS Secrets Manager immediately makes the secrets inaccessible but only schedules them for deletion after at least 7 days.
</Info>
</Step>
</Steps>

View File

@@ -143,8 +143,8 @@ Without email configuration, Infisical's core functions like sign-up/login and s
SMTP_HOST=email-smtp.ap-northeast-1.amazonaws.com # SMTP endpoint obtained from SMTP settings
SMTP_USERNAME=xxx # your SMTP username
SMTP_PASSWORD=xxx # your SMTP password
SMTP_PORT=587
SMTP_SECURE=false
SMTP_PORT=465
SMTP_SECURE=true
SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails
SMTP_FROM_NAME=Infisical
```

View File

@@ -10,6 +10,7 @@ import {
Environment,
HerokuPipelineCoupling,
IntegrationAuth,
KmsKey,
NorthflankSecretGroup,
Org,
Project,
@@ -43,6 +44,14 @@ const integrationAuthKeys = {
[{ integrationAuthId }, "integrationAuthGithubOrgs"] as const,
getIntegrationAuthGithubEnvs: (integrationAuthId: string, repoName: string, repoOwner: string) =>
[{ integrationAuthId, repoName, repoOwner }, "integrationAuthGithubOrgs"] as const,
getIntegrationAuthAwsKmsKeys: ({
integrationAuthId,
region
}: {
integrationAuthId: string,
region: string
}) =>
[{ integrationAuthId, region }, "integrationAuthAwsKmsKeyIds"] as const,
getIntegrationAuthQoveryOrgs: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const,
getIntegrationAuthQoveryProjects: ({
@@ -217,6 +226,27 @@ const fetchIntegrationAuthQoveryOrgs = async (integrationAuthId: string) => {
return orgs;
};
const fetchIntegrationAuthAwsKmsKeys = async ({
integrationAuthId,
region
}: {
integrationAuthId: string;
region: string;
}) => {
const {
data: { kmsKeys }
} = await apiRequest.get<{ kmsKeys: KmsKey[] }>(
`/api/v1/integration-auth/${integrationAuthId}/aws-secrets-manager/kms-keys`,
{
params: {
region
}
}
);
return kmsKeys;
};
const fetchIntegrationAuthQoveryProjects = async ({
integrationAuthId,
orgId
@@ -544,6 +574,27 @@ export const useGetIntegrationAuthQoveryOrgs = (integrationAuthId: string) => {
});
};
export const useGetIntegrationAuthAwsKmsKeys = ({
integrationAuthId,
region
}: {
integrationAuthId: string;
region: string;
}) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthAwsKmsKeys({
integrationAuthId,
region
}),
queryFn: () =>
fetchIntegrationAuthAwsKmsKeys({
integrationAuthId,
region
}),
enabled: true
});
};
export const useGetIntegrationAuthQoveryProjects = ({
integrationAuthId,
orgId

View File

@@ -58,6 +58,11 @@ export type Project = {
projectId: string;
};
export type KmsKey = {
id: string;
alias: string;
};
export type Service = {
name: string;
serviceId: string;

View File

@@ -66,7 +66,8 @@ export const useCreateIntegration = () => {
secretAWSTag?: {
key: string;
value: string;
};
}[];
kmsKeyId?: string;
};
}) => {
const {

View File

@@ -128,10 +128,10 @@ export default function AWSParameterStoreCreateIntegrationPage() {
metadata: {
...(shouldTag
? {
secretAWSTag: {
secretAWSTag: [{
key: tagKey,
value: tagValue
}
}]
}
: {})
}
@@ -279,7 +279,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
label="Tag Value"
>
<Input
placeholder="managed-by"
placeholder="infisical"
value={tagValue}
onChange={(e) => setTagValue(e.target.value)}
/>

View File

@@ -14,6 +14,7 @@ import { motion } from "framer-motion";
import queryString from "query-string";
import { useCreateIntegration } from "@app/hooks/api";
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
import {
Button,
@@ -87,6 +88,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState("");
const [tagKey, setTagKey] = useState("");
const [tagValue, setTagValue] = useState("");
const [kmsKeyId, setKmsKeyId] = useState("");
// const [path, setPath] = useState('');
// const [pathErrorText, setPathErrorText] = useState('');
@@ -94,6 +96,19 @@ export default function AWSSecretManagerCreateIntegrationPage() {
const [isLoading, setIsLoading] = useState(false);
const [shouldTag, setShouldTag] = useState(false);
const { data: integrationAuthAwsKmsKeys, isLoading: isIntegrationAuthAwsKmsKeysLoading } =
useGetIntegrationAuthAwsKmsKeys({
integrationAuthId: String(integrationAuthId),
region: selectedAWSRegion
});
useEffect(() => {
if (integrationAuthAwsKmsKeys) {
setKmsKeyId(String(integrationAuthAwsKmsKeys?.filter(key => key.alias === "alias/aws/secretsmanager")[0]?.id))
}
}, [integrationAuthAwsKmsKeys])
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
@@ -127,12 +142,16 @@ export default function AWSSecretManagerCreateIntegrationPage() {
metadata: {
...(shouldTag
? {
secretAWSTag: {
secretAWSTag: [{
key: tagKey,
value: tagValue
}
}]
}
: {})
: {}),
...((kmsKeyId && integrationAuthAwsKmsKeys?.filter(key => key.id === kmsKeyId)[0]?.alias !== "alias/aws/secretsmanager") ?
{
kmsKeyId
}: {})
}
});
@@ -145,7 +164,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
}
};
return integrationAuth && workspace && selectedSourceEnvironment ? (
return (integrationAuth && workspace && selectedSourceEnvironment && !isIntegrationAuthAwsKmsKeysLoading) ? (
<div className="flex h-full w-full flex-col items-center justify-center">
<Head>
<title>Set Up AWS Secrets Manager Integration</title>
@@ -278,13 +297,38 @@ export default function AWSSecretManagerCreateIntegrationPage() {
label="Tag Value"
>
<Input
placeholder="managed-by"
placeholder="infisical"
value={tagValue}
onChange={(e) => setTagValue(e.target.value)}
/>
</FormControl>
</div>
)}
<FormControl label="Encryption Key" className="mt-4">
<Select
value={kmsKeyId}
onValueChange={(e) => {
setKmsKeyId(e)
}}
className="w-full border border-mineshaft-500"
>
{integrationAuthAwsKmsKeys?.length ? (
integrationAuthAwsKmsKeys.map((key) => {
return (
<SelectItem
value={key.id as string}
key={`repo-id-${key.id}`}
className="w-[28.4rem] text-sm"
>
{key.alias}
</SelectItem>
);
})
) : (
<div />
)}
</Select>
</FormControl>
</motion.div>
</TabPanel>
</Tabs>
@@ -317,7 +361,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
<title>Set Up AWS Secrets Manager Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
{isintegrationAuthLoading ? (
{(isintegrationAuthLoading || isIntegrationAuthAwsKmsKeysLoading) ? (
<img
src="/images/loading/loading.gif"
height={70}

View File

@@ -1,5 +1,10 @@
import { useState } from "react";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
@@ -49,12 +54,56 @@ export default function TerraformCloudCreateIntegrationPage() {
return (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Terraform Cloud Integration</CardTitle>
<Head>
<title>Authorize Terraform Cloud Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<Card className="max-w-lg rounded-md border border-mineshaft-600">
<CardTitle
className="px-6 text-left text-xl"
subTitle="After adding the details below, you will be prompted to set up an integration for a particular Infisical project and environment."
>
<div className="flex flex-row items-center">
<div className="inline flex items-center">
<Image
src="/images/integrations/Terraform.png"
height={35}
width={35}
alt="Terraform logo"
/>
</div>
<span className="ml-1.5">Terraform Cloud Integration </span>
<Link href="https://infisical.com/docs/integrations/cloud/terraform-cloud" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
</CardTitle>
<FormControl
label="Terraform Cloud Workspace ID"
errorText={workspacesIdErrorText}
isError={workspacesIdErrorText !== "" ?? false}
className="px-6"
>
<Input
placeholder="Workspace Id"
value={workspacesId}
onChange={(e) => setWorkSpacesId(e.target.value)}
/>
</FormControl>
<FormControl
label="Terraform Cloud API Token"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== "" ?? false}
className="px-6"
>
<Input
placeholder="API Token"
@@ -64,21 +113,11 @@ export default function TerraformCloudCreateIntegrationPage() {
onChange={(e) => setApiKey(e.target.value)}
/>
</FormControl>
<FormControl
label="Terraform Cloud Workspace ID"
errorText={workspacesIdErrorText}
isError={workspacesIdErrorText !== "" ?? false}
>
<Input
placeholder="Workspace Id"
value={workspacesId}
onChange={(e) => setWorkSpacesId(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
colorSchema="primary"
variant="outline_bg"
className="mb-6 mt-2 ml-auto mr-6 w-min"
isLoading={isLoading}
>
Connect to Terraform Cloud

View File

@@ -1,8 +1,14 @@
import { useEffect, useState } from "react";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import queryString from "query-string";
import { useCreateIntegration } from "@app/hooks/api";
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
import {
Button,
@@ -19,6 +25,15 @@ import {
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
const initialSyncBehaviors = [
{
label: "No Import - Overwrite all values in Terraform Cloud",
value: IntegrationSyncBehavior.OVERWRITE_TARGET
},
{ label: "Import non-sensitive - Prefer values from Terraform Cloud", value: IntegrationSyncBehavior.PREFER_TARGET },
{ label: "Import non-sensitive - Prefer values from Infisical", value: IntegrationSyncBehavior.PREFER_SOURCE }
];
const variableTypes = [{ name: "env" }, { name: "terraform" }];
export default function TerraformCloudCreateIntegrationPage() {
@@ -39,6 +54,7 @@ export default function TerraformCloudCreateIntegrationPage() {
const [variableType, setVariableType] = useState("");
const [variableTypeErrorText, setVariableTypeErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [initialSyncBehavior, setInitialSyncBehavior] = useState("prefer-source")
useEffect(() => {
if (workspace) {
@@ -78,7 +94,10 @@ export default function TerraformCloudCreateIntegrationPage() {
)?.appId,
sourceEnvironment: selectedSourceEnvironment,
targetService: variableType,
secretPath
secretPath,
metadata: {
initialSyncBehavior
}
});
setIsLoading(false);
@@ -95,9 +114,40 @@ export default function TerraformCloudCreateIntegrationPage() {
integrationAuthApps &&
targetApp ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Terraform Cloud Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Head>
<title>Create Terraform Cloud Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<Card className="max-w-lg rounded-md border border-mineshaft-600">
<CardTitle
className="px-6 text-left text-xl"
subTitle="Specify the encironment and path within Infisical that you want to push to which project in Terraform."
>
<div className="flex flex-row items-center">
<div className="inline flex items-center">
<Image
src="/images/integrations/Terraform.png"
height={35}
width={35}
alt="Terraform logo"
/>
</div>
<span className="ml-1.5">Terraform Cloud Integration </span>
<Link href="https://infisical.com/docs/integrations/cloud/terraform-cloud" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
</CardTitle>
<FormControl label="Project Environment" className="px-6">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
@@ -113,7 +163,7 @@ export default function TerraformCloudCreateIntegrationPage() {
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<FormControl label="Secrets Path" className="px-6">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
@@ -122,7 +172,7 @@ export default function TerraformCloudCreateIntegrationPage() {
</FormControl>
<FormControl
label="Category"
className="mt-4"
className="px-6"
errorText={variableTypeErrorText}
isError={variableTypeErrorText !== "" ?? false}
>
@@ -138,7 +188,7 @@ export default function TerraformCloudCreateIntegrationPage() {
))}
</Select>
</FormControl>
<FormControl label="Terraform Cloud Project" className="mt-4">
<FormControl label="Terraform Cloud Project" className="px-6">
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
@@ -161,10 +211,22 @@ export default function TerraformCloudCreateIntegrationPage() {
)}
</Select>
</FormControl>
<FormControl label="Initial Sync Behavior" className="px-6">
<Select value={initialSyncBehavior} onValueChange={(e) => setInitialSyncBehavior(e)} className="w-full border border-mineshaft-600">
{initialSyncBehaviors.map((b) => {
return (
<SelectItem value={b.value} key={`sync-behavior-${b.value}`}>
{b.label}
</SelectItem>
);
})}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
colorSchema="primary"
variant="outline_bg"
className="mb-6 mt-2 ml-auto mr-6 w-min"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>

View File

@@ -74,7 +74,7 @@ export const IntegrationsSection = ({
</div>
)}
{!isLoading && isBotActive && (
<div className="flex flex-col space-y-4 p-6 pt-0">
<div className="flex flex-col min-w-max space-y-4 p-6 pt-0">
{integrations?.map((integration) => (
<div
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3"
@@ -128,6 +128,9 @@ export const IntegrationsSection = ({
<FormLabel
label={
(integration.integration === "qovery" && integration?.scope) ||
(integration.integration === "aws-secret-manager" && "Secret") ||
(integration.integration === "aws-parameter-store" && "Path") ||
(integration?.integration === "terraform-cloud" && "Project") ||
(integration?.scope === "github-org" && "Organization") ||
(["github-repo", "github-env"].includes(integration?.scope as string) &&
"Repository") ||
@@ -138,6 +141,7 @@ export const IntegrationsSection = ({
{(integration.integration === "hashicorp-vault" &&
`${integration.app} - path: ${integration.path}`) ||
(integration.scope === "github-org" && `${integration.owner}`) ||
(integration.integration === "aws-parameter-store" && `${integration.path}`) ||
(integration.scope?.startsWith("github-") &&
`${integration.owner}/${integration.app}`) ||
integration.app}
@@ -165,6 +169,14 @@ export const IntegrationsSection = ({
</div>
</div>
)}
{integration.integration === "terraform-cloud" && integration.targetService && (
<div className="ml-2">
<FormLabel label="Category" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.targetService}
</div>
</div>
)}
{(integration.integration === "checkly" ||
integration.integration === "github") && (
<div className="ml-2">

View File

@@ -8,5 +8,6 @@ export type Metadata = {
secretAWSTag?: {
key: string;
value: string;
}
}[]
kmsKeyId?: string;
}