This commit is contained in:
Vladyslav Matsiiako
2024-04-21 22:51:12 -07:00
21 changed files with 485 additions and 79 deletions
+7 -6
View File
@@ -585,12 +585,13 @@ export const INTEGRATION = {
region: "AWS region to sync secrets to.", region: "AWS region to sync secrets to.",
scope: "Scope of the provider. Used by Github, Qovery", scope: "Scope of the provider. Used by Github, Qovery",
metadata: { metadata: {
secretPrefix: "The prefix for the saved secret. Used by GCP", secretPrefix: "The prefix for the saved secret. Used by GCP.",
secretSuffix: "The suffix 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", initialSyncBehavoir: "Type of syncing behavoir with the integration.",
shouldAutoRedeploy: "Used by Render to trigger auto deploy", shouldAutoRedeploy: "Used by Render to trigger auto deploy.",
secretGCPLabel: "The label for the GCP secrets", secretGCPLabel: "The label for GCP secrets.",
secretAWSTag: "The tag for the AWS secrets" secretAWSTag: "The tags for AWS secrets.",
kmsKeyId: "The ID of the encryption key from AWS KMS."
} }
}, },
UPDATE: { UPDATE: {
@@ -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({ server.route({
method: "GET", method: "GET",
url: "/:integrationAuthId/qovery/projects", url: "/:integrationAuthId/qovery/projects",
@@ -58,14 +58,17 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
.optional() .optional()
.describe(INTEGRATION.CREATE.metadata.secretGCPLabel), .describe(INTEGRATION.CREATE.metadata.secretGCPLabel),
secretAWSTag: z secretAWSTag: z
.object({ .array(
key: z.string(), z.object({
value: z.string() key: z.string(),
}) value: z.string()
})
)
.optional() .optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag) .describe(INTEGRATION.CREATE.metadata.secretAWSTag),
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId)
}) })
.optional() .default({})
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -1,5 +1,6 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import AWS from "aws-sdk";
import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas"; import { SecretEncryptionAlgo, SecretKeyEncoding, TIntegrationAuths, TIntegrationAuthsInsert } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -23,6 +24,7 @@ import {
TGetIntegrationAuthTeamCityBuildConfigDTO, TGetIntegrationAuthTeamCityBuildConfigDTO,
THerokuPipelineCoupling, THerokuPipelineCoupling,
TIntegrationAuthAppsDTO, TIntegrationAuthAppsDTO,
TIntegrationAuthAwsKmsKeyDTO,
TIntegrationAuthBitbucketWorkspaceDTO, TIntegrationAuthBitbucketWorkspaceDTO,
TIntegrationAuthChecklyGroupsDTO, TIntegrationAuthChecklyGroupsDTO,
TIntegrationAuthGithubEnvsDTO, TIntegrationAuthGithubEnvsDTO,
@@ -534,6 +536,52 @@ export const integrationAuthServiceFactory = ({
return data.results.map(({ name, id: orgId }) => ({ name, orgId })); 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 ({ const getQoveryProjects = async ({
actorId, actorId,
actor, actor,
@@ -1133,6 +1181,7 @@ export const integrationAuthServiceFactory = ({
getIntegrationApps, getIntegrationApps,
getVercelBranches, getVercelBranches,
getApps, getApps,
getAwsKmsKeys,
getGithubOrgs, getGithubOrgs,
getGithubEnvs, getGithubEnvs,
getChecklyGroups, getChecklyGroups,
@@ -63,6 +63,11 @@ export type TIntegrationAuthQoveryProjectDTO = {
orgId: string; orgId: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthAwsKmsKeyDTO = {
id: string;
region: string;
} & Omit<TProjectPermission, "projectId">;
export type TIntegrationAuthQoveryEnvironmentsDTO = { export type TIntegrationAuthQoveryEnvironmentsDTO = {
id: string; id: string;
} & TProjectPermission; } & TProjectPermission;
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-argument */
@@ -489,7 +490,9 @@ const syncSecretsAWSParameterStore = async ({
Type: "SecureString", Type: "SecureString",
Value: secrets[key].value, Value: secrets[key].value,
// Overwrite: true, // 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(); .promise();
// case: secret exists in AWS parameter store // case: secret exists in AWS parameter store
@@ -579,7 +582,10 @@ const syncSecretsAWSSecretManager = async ({
new CreateSecretCommand({ new CreateSecretCommand({
Name: integration.app as string, Name: integration.app as string,
SecretString: JSON.stringify(secKeyVal), 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 * @param {String} obj.accessToken - access token for Terraform Cloud API
*/ */
const syncSecretsTerraformCloud = async ({ const syncSecretsTerraformCloud = async ({
createManySecretsRawFn,
updateManySecretsRawFn,
integration, integration,
secrets, secrets,
accessToken accessToken,
integrationDAL
}: { }: {
integration: TIntegrations; createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
secrets: Record<string, { value: string; comment?: 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; accessToken: string;
integrationDAL: Pick<TIntegrationDALFactory, "updateById">;
}) => { }) => {
// get secrets from Terraform Cloud // get secrets from Terraform Cloud
const getSecretsRes = ( const terraformSecrets = (
await request.get<{ data: { attributes: { key: string; value: string }; id: string }[] }>( await request.get<{ data: { attributes: { key: string; value: string }; id: string }[] }>(
`${IntegrationUrls.TERRAFORM_CLOUD_API_URL}/api/v2/workspaces/${integration.appId}/vars`, `${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 }> {} 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 // create or update secrets on Terraform Cloud
for await (const key of Object.keys(secrets)) { for await (const key of Object.keys(secrets)) {
if (!(key in getSecretsRes)) { if (!(key in terraformSecrets)) {
// case: secret does not exist in Terraform Cloud // case: secret does not exist in Terraform Cloud
// -> add secret // -> add secret
await request.post( await request.post(
@@ -2190,7 +2274,7 @@ const syncSecretsTerraformCloud = async ({
type: "vars", type: "vars",
attributes: { attributes: {
key, key,
value: secrets[key].value, value: secrets[key]?.value,
category: integration.targetService category: integration.targetService
} }
} }
@@ -2204,17 +2288,17 @@ const syncSecretsTerraformCloud = async ({
} }
); );
// case: secret exists in Terraform Cloud // 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 // -> update secret
await request.patch( 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: { data: {
type: "vars", type: "vars",
id: getSecretsRes[key].id, id: terraformSecrets[key].id,
attributes: { attributes: {
...getSecretsRes[key], ...terraformSecrets[key],
value: secrets[key].value 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)) { if (!(key in secrets)) {
// case: delete secret // case: delete secret
await request.delete( 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: { headers: {
Authorization: `Bearer ${accessToken}`, 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; break;
case Integrations.TERRAFORM_CLOUD: case Integrations.TERRAFORM_CLOUD:
await syncSecretsTerraformCloud({ await syncSecretsTerraformCloud({
createManySecretsRawFn,
updateManySecretsRawFn,
integration, integration,
secrets, secrets,
accessToken accessToken,
integrationDAL
}); });
break; break;
case Integrations.HASHICORP_VAULT: case Integrations.HASHICORP_VAULT:
@@ -25,7 +25,8 @@ export type TCreateIntegrationDTO = {
secretAWSTag?: { secretAWSTag?: {
key: string; key: string;
value: string; value: string;
}; }[];
kmsKeyId?: string;
}; };
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -9,9 +9,9 @@ A **user identity** (also known as **user**) represents a developer, admin, or a
Users can be added manually (through Web UI) or programmatically (e.g., API) to [organizations](../organization) and [projects](../projects). Users can be added manually (through Web UI) or programmatically (e.g., API) to [organizations](../organization) and [projects](../projects).
Upon being added to an organizaztion and projects, users assume a certain set of roles and permissions that represents their identity. Upon being added to an organization and projects, users assume a certain set of roles and permissions that represents their identity.
![organization members](../../images/platform/organization/organization-members.png) ![organization members](../../../images/platform/organization/organization-members.png)
## Authentication methods ## Authentication methods
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

@@ -30,7 +30,7 @@ Prerequisites:
"ssm:DeleteParameter", "ssm:DeleteParameter",
"ssm:GetParametersByPath", "ssm:GetParametersByPath",
"ssm:DeleteParameters", "ssm:DeleteParameters",
"ssm:AddTagsToResource" "ssm:AddTagsToResource" // if you need to add tags to secrets
], ],
"Resource": "*" "Resource": "*"
} }
+19 -11
View File
@@ -29,13 +29,16 @@ Prerequisites:
"secretsmanager:GetSecretValue", "secretsmanager:GetSecretValue",
"secretsmanager:CreateSecret", "secretsmanager:CreateSecret",
"secretsmanager:UpdateSecret", "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": "*" "Resource": "*"
} }
] ]
} }
``` ```
</Step> </Step>
<Step title="Authorize Infisical for AWS Secrets Manager"> <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 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 1](../../images/integrations/aws/integrations-aws-access-key-1.png)
![access key 2](../../images/integrations/aws/integrations-aws-access-key-2.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) ![access key 3](../../images/integrations/aws/integrations-aws-access-key-3.png)
Navigate to your project's integrations tab in Infisical. Navigate to your project's integrations tab in Infisical.
![integrations](../../images/integrations.png) ![integrations](../../images/integrations.png)
@@ -52,12 +55,6 @@ Prerequisites:
![integration auth](../../images/integrations/aws/integrations-aws-secret-manager-auth.png) ![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>
<Step title="Start integration"> <Step title="Start integration">
Select how you want to integration to work by specifying a number of parameters: 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. The region that you want to integrate with in AWS Secrets Manager.
</ParamField> </ParamField>
<ParamField path="AWS SM Secret Name" type="string" required> <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> </ParamField>
Then, press `Create Integration` to start syncing secrets to AWS Secrets Manager.
![integration create](../../images/integrations/aws/integrations-aws-secret-manager-create.png) ![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> <Info>
Infisical currently syncs environment variables to AWS Secrets Manager as Infisical currently syncs environment variables to AWS Secrets Manager as
key-value pairs under one secret. We're actively exploring ways to help users key-value pairs under one secret. We're actively exploring ways to help users
@@ -88,5 +95,6 @@ Prerequisites:
<Info> <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. 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> </Info>
</Step> </Step>
</Steps> </Steps>
@@ -10,6 +10,7 @@ import {
Environment, Environment,
HerokuPipelineCoupling, HerokuPipelineCoupling,
IntegrationAuth, IntegrationAuth,
KmsKey,
NorthflankSecretGroup, NorthflankSecretGroup,
Org, Org,
Project, Project,
@@ -43,6 +44,14 @@ const integrationAuthKeys = {
[{ integrationAuthId }, "integrationAuthGithubOrgs"] as const, [{ integrationAuthId }, "integrationAuthGithubOrgs"] as const,
getIntegrationAuthGithubEnvs: (integrationAuthId: string, repoName: string, repoOwner: string) => getIntegrationAuthGithubEnvs: (integrationAuthId: string, repoName: string, repoOwner: string) =>
[{ integrationAuthId, repoName, repoOwner }, "integrationAuthGithubOrgs"] as const, [{ integrationAuthId, repoName, repoOwner }, "integrationAuthGithubOrgs"] as const,
getIntegrationAuthAwsKmsKeys: ({
integrationAuthId,
region
}: {
integrationAuthId: string,
region: string
}) =>
[{ integrationAuthId, region }, "integrationAuthAwsKmsKeyIds"] as const,
getIntegrationAuthQoveryOrgs: (integrationAuthId: string) => getIntegrationAuthQoveryOrgs: (integrationAuthId: string) =>
[{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const, [{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const,
getIntegrationAuthQoveryProjects: ({ getIntegrationAuthQoveryProjects: ({
@@ -217,6 +226,27 @@ const fetchIntegrationAuthQoveryOrgs = async (integrationAuthId: string) => {
return orgs; 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 ({ const fetchIntegrationAuthQoveryProjects = async ({
integrationAuthId, integrationAuthId,
orgId 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 = ({ export const useGetIntegrationAuthQoveryProjects = ({
integrationAuthId, integrationAuthId,
orgId orgId
@@ -58,6 +58,11 @@ export type Project = {
projectId: string; projectId: string;
}; };
export type KmsKey = {
id: string;
alias: string;
};
export type Service = { export type Service = {
name: string; name: string;
serviceId: string; serviceId: string;
@@ -66,7 +66,8 @@ export const useCreateIntegration = () => {
secretAWSTag?: { secretAWSTag?: {
key: string; key: string;
value: string; value: string;
}; }[];
kmsKeyId?: string;
}; };
}) => { }) => {
const { const {
@@ -128,10 +128,10 @@ export default function AWSParameterStoreCreateIntegrationPage() {
metadata: { metadata: {
...(shouldTag ...(shouldTag
? { ? {
secretAWSTag: { secretAWSTag: [{
key: tagKey, key: tagKey,
value: tagValue value: tagValue
} }]
} }
: {}) : {})
} }
@@ -279,7 +279,7 @@ export default function AWSParameterStoreCreateIntegrationPage() {
label="Tag Value" label="Tag Value"
> >
<Input <Input
placeholder="managed-by" placeholder="infisical"
value={tagValue} value={tagValue}
onChange={(e) => setTagValue(e.target.value)} onChange={(e) => setTagValue(e.target.value)}
/> />
@@ -14,6 +14,7 @@ import { motion } from "framer-motion";
import queryString from "query-string"; import queryString from "query-string";
import { useCreateIntegration } from "@app/hooks/api"; import { useCreateIntegration } from "@app/hooks/api";
import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries";
import { import {
Button, Button,
@@ -87,6 +88,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState(""); const [targetSecretNameErrorText, setTargetSecretNameErrorText] = useState("");
const [tagKey, setTagKey] = useState(""); const [tagKey, setTagKey] = useState("");
const [tagValue, setTagValue] = useState(""); const [tagValue, setTagValue] = useState("");
const [kmsKeyId, setKmsKeyId] = useState("");
// const [path, setPath] = useState(''); // const [path, setPath] = useState('');
// const [pathErrorText, setPathErrorText] = useState(''); // const [pathErrorText, setPathErrorText] = useState('');
@@ -94,6 +96,19 @@ export default function AWSSecretManagerCreateIntegrationPage() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [shouldTag, setShouldTag] = 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(() => { useEffect(() => {
if (workspace) { if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug); setSelectedSourceEnvironment(workspace.environments[0].slug);
@@ -127,12 +142,16 @@ export default function AWSSecretManagerCreateIntegrationPage() {
metadata: { metadata: {
...(shouldTag ...(shouldTag
? { ? {
secretAWSTag: { secretAWSTag: [{
key: tagKey, key: tagKey,
value: tagValue 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"> <div className="flex h-full w-full flex-col items-center justify-center">
<Head> <Head>
<title>Set Up AWS Secrets Manager Integration</title> <title>Set Up AWS Secrets Manager Integration</title>
@@ -278,13 +297,38 @@ export default function AWSSecretManagerCreateIntegrationPage() {
label="Tag Value" label="Tag Value"
> >
<Input <Input
placeholder="managed-by" placeholder="infisical"
value={tagValue} value={tagValue}
onChange={(e) => setTagValue(e.target.value)} onChange={(e) => setTagValue(e.target.value)}
/> />
</FormControl> </FormControl>
</div> </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> </motion.div>
</TabPanel> </TabPanel>
</Tabs> </Tabs>
@@ -317,7 +361,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
<title>Set Up AWS Secrets Manager Integration</title> <title>Set Up AWS Secrets Manager Integration</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Head> </Head>
{isintegrationAuthLoading ? ( {(isintegrationAuthLoading || isIntegrationAuthAwsKmsKeysLoading) ? (
<img <img
src="/images/loading/loading.gif" src="/images/loading/loading.gif"
height={70} height={70}
@@ -1,5 +1,10 @@
import { useState } from "react"; 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 { 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"; import { useSaveIntegrationAccessToken } from "@app/hooks/api";
@@ -49,12 +54,56 @@ export default function TerraformCloudCreateIntegrationPage() {
return ( return (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8"> <Head>
<CardTitle className="text-center">Terraform Cloud Integration</CardTitle> <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 <FormControl
label="Terraform Cloud API Token" label="Terraform Cloud API Token"
errorText={apiKeyErrorText} errorText={apiKeyErrorText}
isError={apiKeyErrorText !== "" ?? false} isError={apiKeyErrorText !== "" ?? false}
className="px-6"
> >
<Input <Input
placeholder="API Token" placeholder="API Token"
@@ -64,21 +113,11 @@ export default function TerraformCloudCreateIntegrationPage() {
onChange={(e) => setApiKey(e.target.value)} onChange={(e) => setApiKey(e.target.value)}
/> />
</FormControl> </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 <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" colorSchema="primary"
className="mt-4" variant="outline_bg"
className="mb-6 mt-2 ml-auto mr-6 w-min"
isLoading={isLoading} isLoading={isLoading}
> >
Connect to Terraform Cloud Connect to Terraform Cloud
@@ -1,8 +1,14 @@
import { useEffect, useState } from "react"; 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 { 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 queryString from "query-string";
import { useCreateIntegration } from "@app/hooks/api"; import { useCreateIntegration } from "@app/hooks/api";
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
import { import {
Button, Button,
@@ -19,6 +25,15 @@ import {
} from "../../../hooks/api/integrationAuth"; } from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace"; 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" }]; const variableTypes = [{ name: "env" }, { name: "terraform" }];
export default function TerraformCloudCreateIntegrationPage() { export default function TerraformCloudCreateIntegrationPage() {
@@ -39,6 +54,7 @@ export default function TerraformCloudCreateIntegrationPage() {
const [variableType, setVariableType] = useState(""); const [variableType, setVariableType] = useState("");
const [variableTypeErrorText, setVariableTypeErrorText] = useState(""); const [variableTypeErrorText, setVariableTypeErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [initialSyncBehavior, setInitialSyncBehavior] = useState("prefer-source")
useEffect(() => { useEffect(() => {
if (workspace) { if (workspace) {
@@ -78,7 +94,10 @@ export default function TerraformCloudCreateIntegrationPage() {
)?.appId, )?.appId,
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
targetService: variableType, targetService: variableType,
secretPath secretPath,
metadata: {
initialSyncBehavior
}
}); });
setIsLoading(false); setIsLoading(false);
@@ -95,9 +114,40 @@ export default function TerraformCloudCreateIntegrationPage() {
integrationAuthApps && integrationAuthApps &&
targetApp ? ( targetApp ? (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8"> <Head>
<CardTitle className="text-center">Terraform Cloud Integration</CardTitle> <title>Create Terraform Cloud Integration</title>
<FormControl label="Project Environment" className="mt-4"> <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 <Select
value={selectedSourceEnvironment} value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)} onValueChange={(val) => setSelectedSourceEnvironment(val)}
@@ -113,7 +163,7 @@ export default function TerraformCloudCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path"> <FormControl label="Secrets Path" className="px-6">
<Input <Input
value={secretPath} value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)} onChange={(evt) => setSecretPath(evt.target.value)}
@@ -122,7 +172,7 @@ export default function TerraformCloudCreateIntegrationPage() {
</FormControl> </FormControl>
<FormControl <FormControl
label="Category" label="Category"
className="mt-4" className="px-6"
errorText={variableTypeErrorText} errorText={variableTypeErrorText}
isError={variableTypeErrorText !== "" ?? false} isError={variableTypeErrorText !== "" ?? false}
> >
@@ -138,7 +188,7 @@ export default function TerraformCloudCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Terraform Cloud Project" className="mt-4"> <FormControl label="Terraform Cloud Project" className="px-6">
<Select <Select
value={targetApp} value={targetApp}
onValueChange={(val) => setTargetApp(val)} onValueChange={(val) => setTargetApp(val)}
@@ -161,10 +211,22 @@ export default function TerraformCloudCreateIntegrationPage() {
)} )}
</Select> </Select>
</FormControl> </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 <Button
onClick={handleButtonClick} onClick={handleButtonClick}
color="mineshaft" colorSchema="primary"
className="mt-4" variant="outline_bg"
className="mb-6 mt-2 ml-auto mr-6 w-min"
isLoading={isLoading} isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0} isDisabled={integrationAuthApps.length === 0}
> >
@@ -74,7 +74,7 @@ export const IntegrationsSection = ({
</div> </div>
)} )}
{!isLoading && isBotActive && ( {!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) => ( {integrations?.map((integration) => (
<div <div
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3" 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 <FormLabel
label={ label={
(integration.integration === "qovery" && integration?.scope) || (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") || (integration?.scope === "github-org" && "Organization") ||
(["github-repo", "github-env"].includes(integration?.scope as string) && (["github-repo", "github-env"].includes(integration?.scope as string) &&
"Repository") || "Repository") ||
@@ -138,6 +141,7 @@ export const IntegrationsSection = ({
{(integration.integration === "hashicorp-vault" && {(integration.integration === "hashicorp-vault" &&
`${integration.app} - path: ${integration.path}`) || `${integration.app} - path: ${integration.path}`) ||
(integration.scope === "github-org" && `${integration.owner}`) || (integration.scope === "github-org" && `${integration.owner}`) ||
(integration.integration === "aws-parameter-store" && `${integration.path}`) ||
(integration.scope?.startsWith("github-") && (integration.scope?.startsWith("github-") &&
`${integration.owner}/${integration.app}`) || `${integration.owner}/${integration.app}`) ||
integration.app} integration.app}
@@ -165,6 +169,14 @@ export const IntegrationsSection = ({
</div> </div>
</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 === "checkly" ||
integration.integration === "github") && ( integration.integration === "github") && (
<div className="ml-2"> <div className="ml-2">
+2 -1
View File
@@ -8,5 +8,6 @@ export type Metadata = {
secretAWSTag?: { secretAWSTag?: {
key: string; key: string;
value: string; value: string;
} }[]
kmsKeyId?: string;
} }