mirror of
https://github.com/Infisical/infisical.git
synced 2025-04-06 11:36:53 +00:00
Compare commits
84 Commits
daniel/das
...
daniel/mig
Author | SHA1 | Date | |
---|---|---|---|
233a4f7d77 | |||
44ff1abd74 | |||
6874bff302 | |||
e1b8aa8347 | |||
a041fd4762 | |||
1534ba516a | |||
f7183347dc | |||
105b8d6493 | |||
22a3c46902 | |||
be8232dc93 | |||
088cb72621 | |||
de21b44486 | |||
04491ee1b7 | |||
ad79ee56e4 | |||
519d6f98a2 | |||
973ed37018 | |||
5bad4adbdf | |||
e008fb26a2 | |||
34543ef127 | |||
83107f56bb | |||
35071af478 | |||
eb5f71cb05 | |||
9cf1dd38a6 | |||
144a563609 | |||
ca0062f049 | |||
2ed9aa888e | |||
8c7d329f8f | |||
a0aa06e2f5 | |||
1dd0167ac8 | |||
55aea364da | |||
afee47ab45 | |||
9387d9aaac | |||
2b215a510c | |||
89ff6a6c93 | |||
3bcf406688 | |||
580b86cde8 | |||
7a20251261 | |||
ae63898d5e | |||
d4d3c2b10f | |||
0e3cc4fdeb | |||
b893c3e690 | |||
cee13a0e8b | |||
3745b65148 | |||
a0f0593e2d | |||
ea6e739b46 | |||
12f4868957 | |||
4d43a77f6c | |||
3f3c15d715 | |||
ca453df9e9 | |||
c959fa6fdd | |||
d11ded9abc | |||
714a3186a9 | |||
20d1572220 | |||
21290d8e6c | |||
a087deb1eb | |||
7ce283e891 | |||
8d6f76698a | |||
71cc84c9a5 | |||
5d95d7f31d | |||
2f15e0e767 | |||
fcc18996d3 | |||
bcaafcb49f | |||
b4558981c1 | |||
64099908eb | |||
98e0c1b4ca | |||
4050e56e60 | |||
e453ddf937 | |||
3f68807179 | |||
ba42aca069 | |||
22c589e2cf | |||
943945f6d7 | |||
b598dd3d47 | |||
ad6d18a905 | |||
46a91515b1 | |||
b79ce8a880 | |||
d31d98b5e0 | |||
cb6cbafcae | |||
bcb3eaab74 | |||
12d5fb1043 | |||
8bf09789d6 | |||
7ab8db0471 | |||
6b473d2b36 | |||
7581b33b3b | |||
be74f4d34c |
.github/workflows
.gitignorebackend
package-lock.jsonpackage.json
src
db/migrations
ee
routes/v1
services
dynamic-secret/providers
secret-rotation/secret-rotation-queue
lib/api-docs
server/routes
v1
v2
services
cli
go.modgo.sum
packages
test
.snapshots
test-TestServiceToken_ExportSecretsWithImportstest-TestServiceToken_ExportSecretsWithoutImportstest-TestServiceToken_GetSecretsByNameRecursivetest-TestServiceToken_GetSecretsByNameWithImportstest-TestServiceToken_GetSecretsByNameWithNotFoundSecrettest-TestServiceToken_RunCmdRecursiveAndImportstest-TestServiceToken_RunCmdWithImportstest-TestServiceToken_RunCmdWithoutImportstest-TestServiceToken_SecretsGetWithImportsAndRecursiveCmdtest-TestServiceToken_SecretsGetWithoutImportsAndWithoutRecursiveCmdtest-TestUniversalAuth_ExportSecretsWithImportstest-TestUniversalAuth_ExportSecretsWithoutImportstest-TestUniversalAuth_GetSecretsByNameRecursivetest-TestUniversalAuth_GetSecretsByNameWithImportstest-TestUniversalAuth_GetSecretsByNameWithNotFoundSecrettest-TestUniversalAuth_RunCmdRecursiveAndImportstest-TestUniversalAuth_RunCmdWithImportstest-TestUniversalAuth_RunCmdWithoutImportstest-TestUniversalAuth_SecretsGetWithImportsAndRecursiveCmdtest-TestUniversalAuth_SecretsGetWithoutImportsAndWithoutRecursiveCmdtest-TestUniversalAuth_SecretsGetWrongEnvironment
export_test.gohelper.gologin_test.gorun_test.gosecrets_by_name_test.gosecrets_test.godocs
documentation
guides
platform
images
guides/microsoft-power-apps
integrations/aws
platform/dynamic-secrets
integrations/cloud
mint.jsonself-hosting/configuration
frontend/src
components/v2/UpgradeProjectAlert
context/ProjectPermissionContext
hooks/api
dynamicSecret
integrationAuth
integrations
roles
users
pages/integrations
views
IntegrationsPage/components/IntegrationsSection
SecretMainPage/components
ActionBar/CreateDynamicSecretForm
DynamicSecretListView
pg-migrator/src/models/integration
sink
@ -5,6 +5,7 @@ on:
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- "backend/src/server/routes/**"
|
||||
- "backend/src/ee/routes/**"
|
||||
|
||||
jobs:
|
||||
check-be-api-changes:
|
||||
|
112
.github/workflows/release_build_infisical_cli.yml
vendored
112
.github/workflows/release_build_infisical_cli.yml
vendored
@ -1,60 +1,64 @@
|
||||
name: Build and release CLI
|
||||
|
||||
on:
|
||||
push:
|
||||
# run only against tags
|
||||
tags:
|
||||
- "infisical-cli/v*.*.*"
|
||||
push:
|
||||
# run only against tags
|
||||
tags:
|
||||
- "infisical-cli/v*.*.*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
# packages: write
|
||||
# issues: write
|
||||
|
||||
contents: write
|
||||
# packages: write
|
||||
# issues: write
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- run: git fetch --force --tags
|
||||
- run: echo "Ref name ${{github.ref_name}}"
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ">=1.19.3"
|
||||
cache: true
|
||||
cache-dependency-path: cli/go.sum
|
||||
- name: libssl1.1 => libssl1.0-dev for OSXCross
|
||||
run: |
|
||||
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
|
||||
sudo apt update && apt-cache policy libssl1.0-dev
|
||||
sudo apt-get install libssl1.0-dev
|
||||
- name: OSXCross for CGO Support
|
||||
run: |
|
||||
mkdir ../../osxcross
|
||||
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
|
||||
- uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
|
||||
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
- uses: actions/setup-python@v4
|
||||
- run: pip install --upgrade cloudsmith-cli
|
||||
- name: Publish to CloudSmith
|
||||
run: sh cli/upload_to_cloudsmith.sh
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
cli-integration-tests:
|
||||
name: Run tests before deployment
|
||||
uses: ./.github/workflows/run-cli-tests.yml
|
||||
|
||||
goreleaser:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: [cli-integration-tests]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: 🐋 Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 🔧 Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- run: git fetch --force --tags
|
||||
- run: echo "Ref name ${{github.ref_name}}"
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ">=1.19.3"
|
||||
cache: true
|
||||
cache-dependency-path: cli/go.sum
|
||||
- name: libssl1.1 => libssl1.0-dev for OSXCross
|
||||
run: |
|
||||
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
|
||||
sudo apt update && apt-cache policy libssl1.0-dev
|
||||
sudo apt-get install libssl1.0-dev
|
||||
- name: OSXCross for CGO Support
|
||||
run: |
|
||||
mkdir ../../osxcross
|
||||
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
|
||||
- uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
|
||||
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||
- uses: actions/setup-python@v4
|
||||
- run: pip install --upgrade cloudsmith-cli
|
||||
- name: Publish to CloudSmith
|
||||
run: sh cli/upload_to_cloudsmith.sh
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
|
34
.github/workflows/run-cli-tests.yml
vendored
Normal file
34
.github/workflows/run-cli-tests.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
name: Go CLI Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- "cli/**"
|
||||
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./cli
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21.x"
|
||||
- name: Install dependencies
|
||||
run: go get .
|
||||
- name: Test with the Go CLI
|
||||
env:
|
||||
CLI_TESTS_UA_CLIENT_ID: ${{ secrets.CLI_TESTS_UA_CLIENT_ID }}
|
||||
CLI_TESTS_UA_CLIENT_SECRET: ${{ secrets.CLI_TESTS_UA_CLIENT_SECRET }}
|
||||
CLI_TESTS_SERVICE_TOKEN: ${{ secrets.CLI_TESTS_SERVICE_TOKEN }}
|
||||
CLI_TESTS_PROJECT_ID: ${{ secrets.CLI_TESTS_PROJECT_ID }}
|
||||
CLI_TESTS_ENV_SLUG: ${{ secrets.CLI_TESTS_ENV_SLUG }}
|
||||
|
||||
run: go test -v -count=1 ./test
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -67,3 +67,5 @@ yarn-error.log*
|
||||
frontend-build
|
||||
|
||||
*.tgz
|
||||
cli/infisical-merge
|
||||
cli/test/infisical-merge
|
||||
|
34
backend/package-lock.json
generated
34
backend/package-lock.json
generated
@ -35,6 +35,7 @@
|
||||
"axios-retry": "^4.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "^5.3.3",
|
||||
"cassandra-driver": "^4.7.2",
|
||||
"dotenv": "^16.4.1",
|
||||
"fastify": "^4.26.0",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
@ -47,7 +48,7 @@
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"ms": "^2.1.3",
|
||||
"mysql2": "^3.9.1",
|
||||
"mysql2": "^3.9.4",
|
||||
"nanoid": "^5.0.4",
|
||||
"nodemailer": "^6.9.9",
|
||||
"ora": "^7.0.1",
|
||||
@ -4565,6 +4566,15 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/long": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-5.0.0.tgz",
|
||||
"integrity": "sha512-eQs9RsucA/LNjnMoJvWG/nXa7Pot/RbBzilF/QRIU/xRl+0ApxrSUFsV5lmf01SvSlqMzJ7Zwxe440wmz2SJGA==",
|
||||
"deprecated": "This is a stub types definition. long provides its own type definitions, so you do not need this installed.",
|
||||
"dependencies": {
|
||||
"long": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
@ -5313,6 +5323,14 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/adm-zip": {
|
||||
"version": "0.5.12",
|
||||
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.12.tgz",
|
||||
"integrity": "sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
@ -6190,6 +6208,20 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cassandra-driver": {
|
||||
"version": "4.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.7.2.tgz",
|
||||
"integrity": "sha512-gwl1DeYvL8Wy3i1GDMzFtpUg5G473fU7EnHFZj7BUtdLB7loAfgZgB3zBhROc9fbaDSUDs6YwOPPojS5E1kbSA==",
|
||||
"dependencies": {
|
||||
"@types/long": "~5.0.0",
|
||||
"@types/node": ">=8",
|
||||
"adm-zip": "~0.5.10",
|
||||
"long": "~5.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz",
|
||||
|
@ -96,6 +96,7 @@
|
||||
"axios-retry": "^4.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "^5.3.3",
|
||||
"cassandra-driver": "^4.7.2",
|
||||
"dotenv": "^16.4.1",
|
||||
"fastify": "^4.26.0",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
|
@ -42,6 +42,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
await knex.transaction(async (tx) => {
|
||||
const duplicateRows = await tx(TableName.OrgMembership)
|
||||
.select("userId", "orgId") // Select the userId and orgId so we can group by them
|
||||
.whereNotNull("userId") // Ensure that the userId is not null
|
||||
.count("* as cnt") // Count the number of rows for each userId and orgId, so we can make sure there are more than 1 row (a duplicate)
|
||||
.groupBy("userId", "orgId")
|
||||
.havingRaw("count(*) > ?", [1]); // Using havingRaw for direct SQL expressions
|
||||
|
@ -157,7 +157,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
response: {
|
||||
200: z.object({
|
||||
data: z.object({
|
||||
membership: ProjectMembershipsSchema,
|
||||
membership: ProjectMembershipsSchema.extend({
|
||||
roles: z
|
||||
.object({
|
||||
role: z.string()
|
||||
})
|
||||
.array()
|
||||
}),
|
||||
permissions: z.any().array()
|
||||
})
|
||||
})
|
||||
|
125
backend/src/ee/services/dynamic-secret/providers/cassandra.ts
Normal file
125
backend/src/ee/services/dynamic-secret/providers/cassandra.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import cassandra from "cassandra-driver";
|
||||
import handlebars from "handlebars";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { DynamicSecretCassandraSchema, TDynamicProviderFns } from "./models";
|
||||
|
||||
const generatePassword = (size = 48) => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~!*$#";
|
||||
return customAlphabet(charset, 48)(size);
|
||||
};
|
||||
|
||||
const generateUsername = () => {
|
||||
return alphaNumericNanoId(32);
|
||||
};
|
||||
|
||||
export const CassandraProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretCassandraSchema.parseAsync(inputs);
|
||||
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1") {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
const getClient = async (providerInputs: z.infer<typeof DynamicSecretCassandraSchema>) => {
|
||||
const sslOptions = providerInputs.ca ? { rejectUnauthorized: false, ca: providerInputs.ca } : undefined;
|
||||
const client = new cassandra.Client({
|
||||
sslOptions,
|
||||
protocolOptions: {
|
||||
port: providerInputs.port
|
||||
},
|
||||
credentials: {
|
||||
username: providerInputs.username,
|
||||
password: providerInputs.password
|
||||
},
|
||||
keyspace: providerInputs.keyspace,
|
||||
localDataCenter: providerInputs?.localDataCenter,
|
||||
contactPoints: providerInputs.host.split(",").filter(Boolean)
|
||||
});
|
||||
return client;
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const isConnected = await client.execute("SELECT * FROM system_schema.keyspaces").then(() => true);
|
||||
await client.shutdown();
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const username = generateUsername();
|
||||
const password = generatePassword();
|
||||
const { keyspace } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
keyspace
|
||||
});
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await client.execute(query);
|
||||
}
|
||||
await client.shutdown();
|
||||
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const { keyspace } = providerInputs;
|
||||
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await client.execute(query);
|
||||
}
|
||||
await client.shutdown();
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
const renew = async (inputs: unknown, entityId: string, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { keyspace } = providerInputs;
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.revocationStatement)({ username, keyspace, expiration });
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await client.execute(query);
|
||||
}
|
||||
await client.shutdown();
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
return {
|
||||
validateProviderInputs,
|
||||
validateConnection,
|
||||
create,
|
||||
revoke,
|
||||
renew
|
||||
};
|
||||
};
|
@ -1,6 +1,8 @@
|
||||
import { CassandraProvider } from "./cassandra";
|
||||
import { DynamicSecretProviders } from "./models";
|
||||
import { SqlDatabaseProvider } from "./sql-database";
|
||||
|
||||
export const buildDynamicSecretProviders = () => ({
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider()
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
||||
[DynamicSecretProviders.Cassandra]: CassandraProvider()
|
||||
});
|
||||
|
@ -19,12 +19,27 @@ export const DynamicSecretSqlDBSchema = z.object({
|
||||
ca: z.string().optional()
|
||||
});
|
||||
|
||||
export const DynamicSecretCassandraSchema = z.object({
|
||||
host: z.string().toLowerCase(),
|
||||
port: z.number(),
|
||||
localDataCenter: z.string().min(1),
|
||||
keyspace: z.string().optional(),
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
creationStatement: z.string(),
|
||||
revocationStatement: z.string(),
|
||||
renewStatement: z.string().optional(),
|
||||
ca: z.string().optional()
|
||||
});
|
||||
|
||||
export enum DynamicSecretProviders {
|
||||
SqlDatabase = "sql-database"
|
||||
SqlDatabase = "sql-database",
|
||||
Cassandra = "cassandra"
|
||||
}
|
||||
|
||||
export const DynamicSecretProviderSchema = z.discriminatedUnion("type", [
|
||||
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema })
|
||||
z.object({ type: z.literal(DynamicSecretProviders.SqlDatabase), inputs: DynamicSecretSqlDBSchema }),
|
||||
z.object({ type: z.literal(DynamicSecretProviders.Cassandra), inputs: DynamicSecretCassandraSchema })
|
||||
]);
|
||||
|
||||
export type TDynamicProviderFns = {
|
||||
|
@ -30,19 +30,24 @@ const generateUsername = (provider: SqlProviders) => {
|
||||
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const appCfg = getConfig();
|
||||
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
|
||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||
|
||||
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
||||
if (
|
||||
isCloud &&
|
||||
// localhost
|
||||
// internal ips
|
||||
(providerInputs.host === "host.docker.internal" ||
|
||||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||
providerInputs.host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
if (
|
||||
providerInputs.host === "localhost" ||
|
||||
providerInputs.host === "127.0.0.1" ||
|
||||
// database infisical uses
|
||||
dbHost === providerInputs.host ||
|
||||
// internal ips
|
||||
providerInputs.host === "host.docker.internal" ||
|
||||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||
providerInputs.host.match(/^192\.168\.\d+\.\d+/)
|
||||
dbHost === providerInputs.host
|
||||
)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
return providerInputs;
|
||||
@ -93,15 +98,13 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
database
|
||||
});
|
||||
|
||||
await db.transaction(async (tx) =>
|
||||
Promise.all(
|
||||
creationStatement
|
||||
.toString()
|
||||
.split(";")
|
||||
.filter(Boolean)
|
||||
.map((query) => tx.raw(query))
|
||||
)
|
||||
);
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
await db.destroy();
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
@ -114,15 +117,13 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const { database } = providerInputs;
|
||||
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
||||
await db.transaction(async (tx) =>
|
||||
Promise.all(
|
||||
revokeStatement
|
||||
.toString()
|
||||
.split(";")
|
||||
.filter(Boolean)
|
||||
.map((query) => tx.raw(query))
|
||||
)
|
||||
);
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
|
||||
await db.destroy();
|
||||
return { entityId: username };
|
||||
@ -137,16 +138,15 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const { database } = providerInputs;
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({ username, expiration, database });
|
||||
if (renewStatement)
|
||||
await db.transaction(async (tx) =>
|
||||
Promise.all(
|
||||
renewStatement
|
||||
.toString()
|
||||
.split(";")
|
||||
.filter(Boolean)
|
||||
.map((query) => tx.raw(query))
|
||||
)
|
||||
);
|
||||
if (renewStatement) {
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await db.destroy();
|
||||
return { entityId: username };
|
||||
|
@ -90,16 +90,20 @@ export const secretRotationDbFn = async ({
|
||||
const appCfg = getConfig();
|
||||
|
||||
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
|
||||
const isCloud = Boolean(appCfg.LICENSE_SERVER_KEY); // quick and dirty way to check if its cloud or not
|
||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||
|
||||
if (
|
||||
isCloud &&
|
||||
// internal ips
|
||||
(host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new Error("Invalid db host");
|
||||
if (
|
||||
host === "localhost" ||
|
||||
host === "127.0.0.1" ||
|
||||
// database infisical uses
|
||||
dbHost === host ||
|
||||
// internal ips
|
||||
host === "host.docker.internal" ||
|
||||
host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||
host.match(/^192\.168\.\d+\.\d+/)
|
||||
dbHost === host
|
||||
)
|
||||
throw new Error("Invalid db host");
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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",
|
||||
|
@ -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({
|
||||
|
@ -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)
|
||||
}),
|
||||
|
@ -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)
|
||||
|
@ -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.")
|
||||
}),
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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:
|
||||
|
@ -25,7 +25,8 @@ export type TCreateIntegrationDTO = {
|
||||
secretAWSTag?: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
}[];
|
||||
kmsKeyId?: string;
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
|
@ -29,6 +29,7 @@ require (
|
||||
require (
|
||||
github.com/alessio/shellescape v1.4.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/danieljoos/wincred v1.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
|
@ -51,6 +51,8 @@ github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGL
|
||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
|
||||
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
|
||||
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
|
||||
@ -324,6 +326,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
||||
github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
|
||||
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
@ -512,16 +512,23 @@ func CallUniversalAuthRefreshAccessToken(httpClient *resty.Client, request Unive
|
||||
|
||||
func CallGetRawSecretsV3(httpClient *resty.Client, request GetRawSecretsV3Request) (GetRawSecretsV3Response, error) {
|
||||
var getRawSecretsV3Response GetRawSecretsV3Response
|
||||
response, err := httpClient.
|
||||
req := httpClient.
|
||||
R().
|
||||
SetResult(&getRawSecretsV3Response).
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
SetBody(request).
|
||||
SetQueryParam("workspaceId", request.WorkspaceId).
|
||||
SetQueryParam("environment", request.Environment).
|
||||
SetQueryParam("secretPath", request.SecretPath).
|
||||
SetQueryParam("include_imports", "false").
|
||||
Get(fmt.Sprintf("%v/v3/secrets/raw", config.INFISICAL_URL))
|
||||
SetQueryParam("secretPath", request.SecretPath)
|
||||
|
||||
if request.IncludeImport {
|
||||
req.SetQueryParam("include_imports", "true")
|
||||
}
|
||||
if request.Recursive {
|
||||
req.SetQueryParam("recursive", "true")
|
||||
}
|
||||
|
||||
response, err := req.Get(fmt.Sprintf("%v/v3/secrets/raw", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return GetRawSecretsV3Response{}, fmt.Errorf("CallGetRawSecretsV3: Unable to complete api request [err=%w]", err)
|
||||
|
@ -371,6 +371,22 @@ type ImportedSecretV3 struct {
|
||||
Secrets []EncryptedSecretV3 `json:"secrets"`
|
||||
}
|
||||
|
||||
type ImportedRawSecretV3 struct {
|
||||
SecretPath string `json:"secretPath"`
|
||||
Environment string `json:"environment"`
|
||||
FolderId string `json:"folderId"`
|
||||
Secrets []struct {
|
||||
ID string `json:"id"`
|
||||
Workspace string `json:"workspace"`
|
||||
Environment string `json:"environment"`
|
||||
Version int `json:"version"`
|
||||
Type string `json:"type"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
SecretValue string `json:"secretValue"`
|
||||
SecretComment string `json:"secretComment"`
|
||||
} `json:"secrets"`
|
||||
}
|
||||
|
||||
type GetEncryptedSecretsV3Response struct {
|
||||
Secrets []EncryptedSecretV3 `json:"secrets"`
|
||||
ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"`
|
||||
@ -542,6 +558,6 @@ type GetRawSecretsV3Response struct {
|
||||
SecretValue string `json:"secretValue"`
|
||||
SecretComment string `json:"secretComment"`
|
||||
} `json:"secrets"`
|
||||
Imports []any `json:"imports"`
|
||||
Imports []ImportedRawSecretV3 `json:"imports"`
|
||||
ETag string
|
||||
}
|
||||
|
@ -149,6 +149,8 @@ var exportCmd = &cobra.Command{
|
||||
secrets = util.ExpandSecrets(secrets, authParams, "")
|
||||
}
|
||||
secrets = util.FilterSecretsByTag(secrets, tagSlugs)
|
||||
secrets = util.SortSecretsByKeys(secrets)
|
||||
|
||||
output, err = formatEnvs(secrets, format)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
|
@ -116,6 +116,9 @@ var secretsCmd = &cobra.Command{
|
||||
secrets = util.ExpandSecrets(secrets, authParams, "")
|
||||
}
|
||||
|
||||
// Sort the secrets by key so we can create a consistent output
|
||||
secrets = util.SortSecretsByKeys(secrets)
|
||||
|
||||
visualize.PrintAllSecretDetails(secrets)
|
||||
Telemetry.CaptureEvent("cli-command:secrets", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION))
|
||||
},
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -53,6 +54,14 @@ func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV st
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Helper function to sort the secrets by key so we can create a consistent output
|
||||
func SortSecretsByKeys(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable {
|
||||
sort.Slice(secrets, func(i, j int) bool {
|
||||
return secrets[i].Key < secrets[j].Key
|
||||
})
|
||||
return secrets
|
||||
}
|
||||
|
||||
func IsSecretEnvironmentValid(env string) bool {
|
||||
if env == "prod" || env == "dev" || env == "test" || env == "staging" {
|
||||
return true
|
||||
|
@ -186,12 +186,12 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin
|
||||
plainTextSecrets = append(plainTextSecrets, models.SingleEnvironmentVariable{Key: secret.SecretKey, Value: secret.SecretValue, Type: secret.Type, WorkspaceId: secret.Workspace})
|
||||
}
|
||||
|
||||
// if includeImports {
|
||||
// plainTextSecrets, err = InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecrets.ImportedSecrets)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// }
|
||||
if includeImports {
|
||||
plainTextSecrets, err = InjectRawImportedSecret(plainTextSecrets, rawSecrets.Imports)
|
||||
if err != nil {
|
||||
return models.PlaintextSecretResult{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return models.PlaintextSecretResult{
|
||||
Secrets: plainTextSecrets,
|
||||
@ -252,6 +252,36 @@ func InjectImportedSecret(plainTextWorkspaceKey []byte, secrets []models.SingleE
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
func InjectRawImportedSecret(secrets []models.SingleEnvironmentVariable, importedSecrets []api.ImportedRawSecretV3) ([]models.SingleEnvironmentVariable, error) {
|
||||
if importedSecrets == nil {
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
hasOverriden := make(map[string]bool)
|
||||
for _, sec := range secrets {
|
||||
hasOverriden[sec.Key] = true
|
||||
}
|
||||
|
||||
for i := len(importedSecrets) - 1; i >= 0; i-- {
|
||||
importSec := importedSecrets[i]
|
||||
plainTextImportedSecrets := importSec.Secrets
|
||||
|
||||
for _, sec := range plainTextImportedSecrets {
|
||||
if _, ok := hasOverriden[sec.SecretKey]; !ok {
|
||||
secrets = append(secrets, models.SingleEnvironmentVariable{
|
||||
Key: sec.SecretKey,
|
||||
WorkspaceId: sec.Workspace,
|
||||
Value: sec.SecretValue,
|
||||
Type: sec.Type,
|
||||
ID: sec.ID,
|
||||
})
|
||||
hasOverriden[sec.SecretKey] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
func FilterSecretsByTag(plainTextSecrets []models.SingleEnvironmentVariable, tagSlugs string) []models.SingleEnvironmentVariable {
|
||||
if tagSlugs == "" {
|
||||
return plainTextSecrets
|
||||
|
@ -0,0 +1,5 @@
|
||||
STAGING-SECRET-1='staging-value-1'
|
||||
STAGING-SECRET-2='staging-value-2'
|
||||
TEST-SECRET-1='test-value-1'
|
||||
TEST-SECRET-2='test-value-2'
|
||||
TEST-SECRET-3='test-value-3'
|
@ -0,0 +1,3 @@
|
||||
TEST-SECRET-1='test-value-1'
|
||||
TEST-SECRET-2='test-value-2'
|
||||
TEST-SECRET-3='test-value-3'
|
@ -0,0 +1,7 @@
|
||||
┌─────────────────┬────────────────┬─────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||
├─────────────────┼────────────────┼─────────────┤
|
||||
│ TEST-SECRET-1 │ test-value-1 │ shared │
|
||||
│ TEST-SECRET-2 │ test-value-2 │ shared │
|
||||
│ FOLDER-SECRET-1 │ folder-value-1 │ shared │
|
||||
└─────────────────┴────────────────┴─────────────┘
|
@ -0,0 +1,7 @@
|
||||
┌──────────────────┬─────────────────┬─────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||
├──────────────────┼─────────────────┼─────────────┤
|
||||
│ TEST-SECRET-1 │ test-value-1 │ shared │
|
||||
│ STAGING-SECRET-2 │ staging-value-2 │ shared │
|
||||
│ FOLDER-SECRET-1 │ folder-value-1 │ shared │
|
||||
└──────────────────┴─────────────────┴─────────────┘
|
@ -0,0 +1,8 @@
|
||||
┌─────────────────┬────────────────┬─────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||
├─────────────────┼────────────────┼─────────────┤
|
||||
│ TEST-SECRET-1 │ test-value-1 │ shared │
|
||||
│ TEST-SECRET-2 │ test-value-2 │ shared │
|
||||
│ FOLDER-SECRET-1 │ folder-value-1 │ shared │
|
||||
│ DOES-NOT-EXIST │ *not found* │ *not found* │
|
||||
└─────────────────┴────────────────┴─────────────┘
|
@ -0,0 +1,2 @@
|
||||
[0m Injecting 6 Infisical secrets into your application process
|
||||
hello world
|
@ -0,0 +1,2 @@
|
||||
[0m Injecting 5 Infisical secrets into your application process
|
||||
hello world
|
@ -0,0 +1,2 @@
|
||||
[0m Injecting 3 Infisical secrets into your application process
|
||||
hello world
|
@ -0,0 +1,10 @@
|
||||
┌──────────────────┬─────────────────┬─────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||
├──────────────────┼─────────────────┼─────────────┤
|
||||
│ FOLDER-SECRET-1 │ folder-value-1 │ shared │
|
||||
│ STAGING-SECRET-1 │ staging-value-1 │ shared │
|
||||
│ STAGING-SECRET-2 │ staging-value-2 │ shared │
|
||||
│ TEST-SECRET-1 │ test-value-1 │ shared │
|
||||
│ TEST-SECRET-2 │ test-value-2 │ shared │
|
||||
│ TEST-SECRET-3 │ test-value-3 │ shared │
|
||||
└──────────────────┴─────────────────┴─────────────┘
|
7
cli/test/.snapshots/test-TestServiceToken_SecretsGetWithoutImportsAndWithoutRecursiveCmd
Normal file
7
cli/test/.snapshots/test-TestServiceToken_SecretsGetWithoutImportsAndWithoutRecursiveCmd
Normal file
@ -0,0 +1,7 @@
|
||||
┌───────────────┬──────────────┬─────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||
├───────────────┼──────────────┼─────────────┤
|
||||
│ TEST-SECRET-1 │ test-value-1 │ shared │
|
||||
│ TEST-SECRET-2 │ test-value-2 │ shared │
|
||||
│ TEST-SECRET-3 │ test-value-3 │ shared │
|
||||
└───────────────┴──────────────┴─────────────┘
|
@ -0,0 +1,5 @@
|
||||
STAGING-SECRET-1='staging-value-1'
|
||||
STAGING-SECRET-2='staging-value-2'
|
||||
TEST-SECRET-1='test-value-1'
|
||||
TEST-SECRET-2='test-value-2'
|
||||
TEST-SECRET-3='test-value-3'
|
@ -0,0 +1,3 @@
|
||||
TEST-SECRET-1='test-value-1'
|
||||
TEST-SECRET-2='test-value-2'
|
||||
TEST-SECRET-3='test-value-3'
|
@ -0,0 +1,7 @@
|
||||
┌─────────────────┬────────────────┬─────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||
├─────────────────┼────────────────┼─────────────┤
|
||||
│ TEST-SECRET-1 │ test-value-1 │ shared │
|
||||
│ TEST-SECRET-2 │ test-value-2 │ shared │
|
||||
│ FOLDER-SECRET-1 │ folder-value-1 │ shared │
|
||||
└─────────────────┴────────────────┴─────────────┘
|
@ -0,0 +1,7 @@
|
||||
┌──────────────────┬─────────────────┬─────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||
├──────────────────┼─────────────────┼─────────────┤
|
||||
│ TEST-SECRET-1 │ test-value-1 │ shared │
|
||||
│ STAGING-SECRET-2 │ staging-value-2 │ shared │
|
||||
│ FOLDER-SECRET-1 │ folder-value-1 │ shared │
|
||||
└──────────────────┴─────────────────┴─────────────┘
|
@ -0,0 +1,8 @@
|
||||
┌─────────────────┬────────────────┬─────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||
├─────────────────┼────────────────┼─────────────┤
|
||||
│ TEST-SECRET-1 │ test-value-1 │ shared │
|
||||
│ TEST-SECRET-2 │ test-value-2 │ shared │
|
||||
│ FOLDER-SECRET-1 │ folder-value-1 │ shared │
|
||||
│ DOES-NOT-EXIST │ *not found* │ *not found* │
|
||||
└─────────────────┴────────────────┴─────────────┘
|
@ -0,0 +1,2 @@
|
||||
[0m Injecting 6 Infisical secrets into your application process
|
||||
hello world
|
@ -0,0 +1,2 @@
|
||||
[0m Injecting 5 Infisical secrets into your application process
|
||||
hello world
|
@ -0,0 +1,2 @@
|
||||
[0m Injecting 3 Infisical secrets into your application process
|
||||
hello world
|
@ -0,0 +1,10 @@
|
||||
┌──────────────────┬─────────────────┬─────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||
├──────────────────┼─────────────────┼─────────────┤
|
||||
│ FOLDER-SECRET-1 │ folder-value-1 │ shared │
|
||||
│ STAGING-SECRET-1 │ staging-value-1 │ shared │
|
||||
│ STAGING-SECRET-2 │ staging-value-2 │ shared │
|
||||
│ TEST-SECRET-1 │ test-value-1 │ shared │
|
||||
│ TEST-SECRET-2 │ test-value-2 │ shared │
|
||||
│ TEST-SECRET-3 │ test-value-3 │ shared │
|
||||
└──────────────────┴─────────────────┴─────────────┘
|
7
cli/test/.snapshots/test-TestUniversalAuth_SecretsGetWithoutImportsAndWithoutRecursiveCmd
Normal file
7
cli/test/.snapshots/test-TestUniversalAuth_SecretsGetWithoutImportsAndWithoutRecursiveCmd
Normal file
@ -0,0 +1,7 @@
|
||||
┌───────────────┬──────────────┬─────────────┐
|
||||
│ SECRET NAME │ SECRET VALUE │ SECRET TYPE │
|
||||
├───────────────┼──────────────┼─────────────┤
|
||||
│ TEST-SECRET-1 │ test-value-1 │ shared │
|
||||
│ TEST-SECRET-2 │ test-value-2 │ shared │
|
||||
│ TEST-SECRET-3 │ test-value-3 │ shared │
|
||||
└───────────────┴──────────────┴─────────────┘
|
@ -0,0 +1,4 @@
|
||||
error: CallGetRawSecretsV3: Unsuccessful response [GET https://app.infisical.com/api/v3/secrets/raw?environment=invalid-env&include_imports=true&recursive=true&secretPath=%2F&workspaceId=bef697d4-849b-4a75-b284-0922f87f8ba2] [status-code=500] [response={"statusCode":500,"error":"Internal Server Error","message":"'invalid-env' environment not found in project with ID bef697d4-849b-4a75-b284-0922f87f8ba2"}]
|
||||
|
||||
|
||||
If this issue continues, get support at https://infisical.com/slack
|
73
cli/test/export_test.go
Normal file
73
cli/test/export_test.go
Normal file
@ -0,0 +1,73 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bradleyjkemp/cupaloy/v2"
|
||||
)
|
||||
|
||||
func TestUniversalAuth_ExportSecretsWithImports(t *testing.T) {
|
||||
MachineIdentityLoginCmd(t)
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "export", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceToken_ExportSecretsWithImports(t *testing.T) {
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "export", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniversalAuth_ExportSecretsWithoutImports(t *testing.T) {
|
||||
MachineIdentityLoginCmd(t)
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "export", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--include-imports=false")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceToken_ExportSecretsWithoutImports(t *testing.T) {
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "export", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--include-imports=false")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
64
cli/test/helper.go
Normal file
64
cli/test/helper.go
Normal file
@ -0,0 +1,64 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
CLI_NAME = "infisical-merge"
|
||||
)
|
||||
|
||||
var (
|
||||
FORMATTED_CLI_NAME = fmt.Sprintf("./%s", CLI_NAME)
|
||||
)
|
||||
|
||||
type Credentials struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
UAAccessToken string
|
||||
ServiceToken string
|
||||
ProjectID string
|
||||
EnvSlug string
|
||||
}
|
||||
|
||||
var creds = Credentials{
|
||||
UAAccessToken: "",
|
||||
ClientID: os.Getenv("CLI_TESTS_UA_CLIENT_ID"),
|
||||
ClientSecret: os.Getenv("CLI_TESTS_UA_CLIENT_SECRET"),
|
||||
ServiceToken: os.Getenv("CLI_TESTS_SERVICE_TOKEN"),
|
||||
ProjectID: os.Getenv("CLI_TESTS_PROJECT_ID"),
|
||||
EnvSlug: os.Getenv("CLI_TESTS_ENV_SLUG"),
|
||||
}
|
||||
|
||||
func ExecuteCliCommand(command string, args ...string) (string, error) {
|
||||
cmd := exec.Command(command, args...)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return strings.TrimSpace(string(output)), err
|
||||
}
|
||||
return strings.TrimSpace(string(output)), nil
|
||||
}
|
||||
|
||||
func SetupCli(t *testing.T) {
|
||||
|
||||
if creds.ClientID == "" || creds.ClientSecret == "" || creds.ServiceToken == "" || creds.ProjectID == "" || creds.EnvSlug == "" {
|
||||
panic("Missing required environment variables")
|
||||
}
|
||||
|
||||
// check if the CLI is already built, if not build it
|
||||
alreadyBuilt := false
|
||||
if _, err := os.Stat(FORMATTED_CLI_NAME); err == nil {
|
||||
alreadyBuilt = true
|
||||
}
|
||||
|
||||
if !alreadyBuilt {
|
||||
if err := exec.Command("go", "build", "../.").Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
29
cli/test/login_test.go
Normal file
29
cli/test/login_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func MachineIdentityLoginCmd(t *testing.T) {
|
||||
SetupCli(t)
|
||||
|
||||
if creds.UAAccessToken != "" {
|
||||
return
|
||||
}
|
||||
|
||||
jwtPattern := `^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]*$`
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "login", "--method=universal-auth", "--client-id", creds.ClientID, "--client-secret", creds.ClientSecret, "--plain", "--silent")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
assert.Regexp(t, jwtPattern, output)
|
||||
|
||||
creds.UAAccessToken = output
|
||||
|
||||
// We can't use snapshot testing here because the output will be different every time
|
||||
}
|
120
cli/test/run_test.go
Normal file
120
cli/test/run_test.go
Normal file
@ -0,0 +1,120 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/bradleyjkemp/cupaloy/v2"
|
||||
)
|
||||
|
||||
func TestServiceToken_RunCmdRecursiveAndImports(t *testing.T) {
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent", "--", "echo", "hello world")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
output = string(bytes.Split([]byte(output), []byte("INF"))[1])
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
func TestServiceToken_RunCmdWithImports(t *testing.T) {
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--", "echo", "hello world")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
output = string(bytes.Split([]byte(output), []byte("INF"))[1])
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniversalAuth_RunCmdRecursiveAndImports(t *testing.T) {
|
||||
MachineIdentityLoginCmd(t)
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent", "--", "echo", "hello world")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
output = string(bytes.Split([]byte(output), []byte("INF"))[1])
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniversalAuth_RunCmdWithImports(t *testing.T) {
|
||||
MachineIdentityLoginCmd(t)
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--", "echo", "hello world")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// remove the first few characters from the output because we don't care about the time, and it will change every time
|
||||
output = string(bytes.Split([]byte(output), []byte("INF"))[1])
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniversalAuth_RunCmdWithoutImports(t *testing.T) {
|
||||
MachineIdentityLoginCmd(t)
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--include-imports=false", "--", "echo", "hello world")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
output = string(bytes.Split([]byte(output), []byte("INF"))[1])
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceToken_RunCmdWithoutImports(t *testing.T) {
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "run", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--silent", "--include-imports=false", "--", "echo", "hello world")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// Remove everything before "INF" because it's not relevant to the test
|
||||
output = string(bytes.Split([]byte(output), []byte("INF"))[1])
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
106
cli/test/secrets_by_name_test.go
Normal file
106
cli/test/secrets_by_name_test.go
Normal file
@ -0,0 +1,106 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bradleyjkemp/cupaloy/v2"
|
||||
)
|
||||
|
||||
func TestServiceToken_GetSecretsByNameRecursive(t *testing.T) {
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "TEST-SECRET-2", "FOLDER-SECRET-1", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceToken_GetSecretsByNameWithNotFoundSecret(t *testing.T) {
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "TEST-SECRET-2", "FOLDER-SECRET-1", "DOES-NOT-EXIST", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceToken_GetSecretsByNameWithImports(t *testing.T) {
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "STAGING-SECRET-2", "FOLDER-SECRET-1", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniversalAuth_GetSecretsByNameRecursive(t *testing.T) {
|
||||
MachineIdentityLoginCmd(t)
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "TEST-SECRET-2", "FOLDER-SECRET-1", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniversalAuth_GetSecretsByNameWithNotFoundSecret(t *testing.T) {
|
||||
MachineIdentityLoginCmd(t)
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "TEST-SECRET-2", "FOLDER-SECRET-1", "DOES-NOT-EXIST", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniversalAuth_GetSecretsByNameWithImports(t *testing.T) {
|
||||
MachineIdentityLoginCmd(t)
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "get", "TEST-SECRET-1", "STAGING-SECRET-2", "FOLDER-SECRET-1", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
87
cli/test/secrets_test.go
Normal file
87
cli/test/secrets_test.go
Normal file
@ -0,0 +1,87 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/bradleyjkemp/cupaloy/v2"
|
||||
)
|
||||
|
||||
func TestServiceToken_SecretsGetWithImportsAndRecursiveCmd(t *testing.T) {
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceToken_SecretsGetWithoutImportsAndWithoutRecursiveCmd(t *testing.T) {
|
||||
SetupCli(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.ServiceToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniversalAuth_SecretsGetWithImportsAndRecursiveCmd(t *testing.T) {
|
||||
SetupCli(t)
|
||||
MachineIdentityLoginCmd(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--recursive", "--silent")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniversalAuth_SecretsGetWithoutImportsAndWithoutRecursiveCmd(t *testing.T) {
|
||||
SetupCli(t)
|
||||
MachineIdentityLoginCmd(t)
|
||||
|
||||
output, err := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", creds.EnvSlug, "--include-imports=false", "--silent")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("error running CLI command: %v", err)
|
||||
}
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err = cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniversalAuth_SecretsGetWrongEnvironment(t *testing.T) {
|
||||
SetupCli(t)
|
||||
MachineIdentityLoginCmd(t)
|
||||
|
||||
output, _ := ExecuteCliCommand(FORMATTED_CLI_NAME, "secrets", "--token", creds.UAAccessToken, "--projectId", creds.ProjectID, "--env", "invalid-env", "--recursive", "--silent")
|
||||
|
||||
// Use cupaloy to snapshot test the output
|
||||
err := cupaloy.Snapshot(output)
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot failed: %v", err)
|
||||
}
|
||||
|
||||
}
|
114
docs/documentation/guides/microsoft-power-apps.mdx
Normal file
114
docs/documentation/guides/microsoft-power-apps.mdx
Normal file
@ -0,0 +1,114 @@
|
||||
---
|
||||
title: "Microsoft Power Apps"
|
||||
description: "Learn how to manage secrets in Microsoft Power Apps with Infisical."
|
||||
---
|
||||
In recent years, there has been a shift towards so-called low-code and no-code platforms. These platforms are particularly appealing to businesses without internal development capabilities, yet teams often discover that some coding is necessary to fully satisfy their business needs.
|
||||
|
||||
Low-code platforms have become increasingly sophisticated and useful, leading to a rise in their adoption by businesses. A prime example is Microsoft Power Apps, which offers a range of data sources and service integrations right out of the box. However, even with advanced tools, you might not always find a ready-made solution for every challenge. This means that low-code doesn't equate to no-code, as some coding and customization are still required to cater to specific needs.
|
||||
|
||||
Consider the need for data integrations where an HTTP-based call to a web service might be necessary, typically requiring authentication through an API key or another type of secret.
|
||||
|
||||
Importantly, it's crucial to avoid hardcoding these secrets, as they would then be accessible to anyone with collaboration rights to the code. This underscores the importance of using a secret management solution like Infisical.
|
||||
|
||||
In this article, we'll demonstrate how to retrieve app secrets from Infisical for use in a Power Apps application. We'll create a simple application with a dedicated data connector to illustrate the ease of integrating Infisical with Power Apps. This tutorial assumes some prior programming experience in C#.
|
||||
|
||||
Prerequisites:
|
||||
- Created Microsoft Power App.
|
||||
|
||||
<Steps>
|
||||
<Step title="Integrate an Azure Function Call">
|
||||
First, let’s create a new Azure Function using the Azure Management Portal. Get the [Function App](https://azuremarketplace.microsoft.com/en-us/marketplace/apps/Microsoft.FunctionApp?tab=Overview) from the [Azure Marketplace](https://azuremarketplace.microsoft.com/en-us/).
|
||||

|
||||
|
||||
Place it in a subscription using any resource group. The name of the function is arbitrary. We'll use .NET as a runtime stack, but you can use whatever you're most comfortable with. The OS choice is also up to you. While Linux may look like a lightweight solution, Windows actually has more Azure Functions support. For instance, you cannot edit a Linux-based Azure Function within the Azure management portal.
|
||||
|
||||
By using a consumption plan, we'll only pay for the resources we use when they are requested. This is the classic “serverless” approach, where you do not pay for running servers, only for interactivity.
|
||||
|
||||
Once the new Azure Functions instance is ready, we add a function. In this case, we can do that already from the Azure Management Portal. Use the “HTTP trigger” template and choose the “function” authorization level.
|
||||
|
||||
The code for the first function can be as simple as:
|
||||
|
||||
```
|
||||
using System.Net;
|
||||
|
||||
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
|
||||
{
|
||||
log.Info("C# HTTP trigger function processed a request.");
|
||||
return req.CreateResponse(HttpStatusCode.OK, "Hello World");
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
The code above is written for the older runtime. As a result, you may need to change the runtime version to 1 for the Azure Power Apps integration to work. If we start at a newer version (for example, 3) this triggers a warning before the migration.
|
||||
</Note>
|
||||
|
||||
Finally, we also need to publish the Swagger (or API) definitions and enable cross-origin resource sharing (CORS). While the API definitions are rather easy to set up, the correct CORS value may be tricky. For now, we can use the wildcard option to allow all hosts.
|
||||
|
||||
</Step>
|
||||
<Step title="Create Custom Connector">
|
||||
|
||||
Once we set all this up, it’s time to create the custom connector.
|
||||
|
||||
You can create the custom connector via the data pane. When we use “Create from Azure Service (Preview)”, this yields a dialog similar to the following:
|
||||
|
||||

|
||||
|
||||
We can now fill out the fields using the information for our created function. The combination boxes are automatically filled in order. Once we select one of the reachable subscriptions (tied to the same account we’ve used to log in to create a Power App), the available services are displayed. Once we select our Azure Functions service, we select the function for retrieving the secret.
|
||||
|
||||
</Step>
|
||||
<Step title="Set Up Infisical in an Azure Function">
|
||||
|
||||
You can add Infisical in an Azure Function quite easily using the [Infisical SDK for .NET](https://infisical.com/docs/sdks/languages/csharp) (or other languages). This enables the function to communicate with Infisical to obtain secrets, among other things.
|
||||
|
||||
In short, we can simply bring all the necessary classes over and start using the Client class. Essentially, this enables us to write code like this:
|
||||
|
||||
```
|
||||
var settings = new ClientSettings
|
||||
{
|
||||
ClientId = "CLIENT_ID",
|
||||
ClientSecret = "CLIENT_SECRET",
|
||||
// SiteUrl = "http://localhost:8080", <-- This line can be omitted if you're using Infisical Cloud.
|
||||
};
|
||||
var infisical = new InfisicalClient(settings);
|
||||
|
||||
var options = new GetSecretOptions
|
||||
{
|
||||
SecretName = "TEST",
|
||||
ProjectId = "PROJECT_ID",
|
||||
Environment = "dev",
|
||||
};
|
||||
var secret = infisical.GetSecret(options);
|
||||
```
|
||||
|
||||
Knowing the URL of Infisical as well as the Client Id and Client Secret, we can now access the desired values.
|
||||
|
||||
Now it’s time to actually use the secret within a Power App. There are two ways to request a desired target service with a secret retrieved from the function:
|
||||
|
||||
1. Call the function first, retrieve the secret, then call the target service, for example, via another custom connector with the secret as input.
|
||||
|
||||
2. Perform the final API request within the function call — not returning a secret at all, just the response from invoking the target service.
|
||||
|
||||
While the first option is more flexible (and presumably cheaper!), the second option is definitely easier. In the end, you should mostly decide based on whether the function should be reused for other purposes. If the single Power App is the only consumer of the function, it may make more sense to go with the second option. Otherwise, you should use the first option.
|
||||
|
||||
For our simple example, we don’t need to reuse the function. We also don’t want the additional complexity of maintaining two different custom connectors, where we only use one to pass data to the other one.
|
||||
|
||||
Based on the previous snippet, we create the following code (for proxying a GET request from an API accessible via the URL specified in the apiEndpoint variable).
|
||||
|
||||
```
|
||||
using (var client = new HttpClient())
|
||||
{
|
||||
client.DefaultRequestHeaders
|
||||
.Accept
|
||||
.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
|
||||
client.DefaultRequestHeaders.Add("X-API-KEY", secret);
|
||||
|
||||
var result = await client.GetAsync(apiEndpoint);
|
||||
var resultContent = await result.Content.ReadAsStringAsync();
|
||||
req.CreateResponse(HttpStatusCode.OK, resultContent);
|
||||
}
|
||||
```
|
||||
This creates a request to the resource protected by an API key that is retrieved from Infisical.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
129
docs/documentation/platform/dynamic-secrets/cassandra.mdx
Normal file
129
docs/documentation/platform/dynamic-secrets/cassandra.mdx
Normal file
@ -0,0 +1,129 @@
|
||||
---
|
||||
title: "Cassandra"
|
||||
description: "How to dynamically generate Cassandra database users."
|
||||
---
|
||||
|
||||
The Infisical Cassandra dynamic secret allows you to generate Cassandra database credentials on demand based on configured role.
|
||||
|
||||
## Prerequisite
|
||||
|
||||
Infisical requires a Cassandra user in your instance with the necessary permissions. This user will facilitate the creation of new accounts as needed.
|
||||
Ensure the user possesses privileges for creating, dropping, and granting permissions to roles for it to be able to create dynamic secrets.
|
||||
|
||||
<Tip>
|
||||
In your Cassandra configuration file `cassandra.yaml`, make sure you have the following settings:
|
||||
|
||||
```yaml
|
||||
authenticator: PasswordAuthenticator
|
||||
authorizer: CassandraAuthorizer
|
||||
```
|
||||
</Tip>
|
||||
|
||||
The above configuration allows user creation and granting permissions.
|
||||
|
||||
## Set up Dynamic Secrets with Cassandra
|
||||
|
||||
<Steps>
|
||||
<Step title="Open Secret Overview Dashboard">
|
||||
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
|
||||
</Step>
|
||||
<Step title="Click on the 'Add Dynamic Secret' button">
|
||||

|
||||
</Step>
|
||||
<Step title="Select Cassandra">
|
||||

|
||||
</Step>
|
||||
<Step title="Provide the inputs for dynamic secret parameters">
|
||||
<ParamField path="Secret Name" type="string" required>
|
||||
Name by which you want the secret to be referenced
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Default TTL" type="string" required>
|
||||
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Max TTL" type="string" required>
|
||||
Maximum time-to-live for a generated secret
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Host" type="string" required>
|
||||
Cassandra Host. You can specify multiple Cassandra hosts by separating them with commas.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Port" type="number" required>
|
||||
Cassandra port
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="User" type="string" required>
|
||||
Username that will be used to create dynamic secrets
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Password" type="string" required>
|
||||
Password that will be used to create dynamic secrets
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Local Data Center" type="string" required>
|
||||
Specify the local data center in Cassandra that you want to use. This choice should align with your Cassandra cluster setup.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="Keyspace" type="string">
|
||||
Keyspace name where you want to create dynamic secrets. This ensures that the user is limited to that keyspace.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="CA(SSL)" type="string">
|
||||
A CA may be required if your cassandra requires it for incoming connections.
|
||||
</ParamField>
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="(Optional) Modify CQL Statements">
|
||||
If you want to provide specific privileges for the generated dynamic credentials, you can modify the CQL statement to your needs. This is useful if you want to only give access to a specific key-space(s).
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Click 'Submit'">
|
||||
After submitting the form, you will see a dynamic secret created in the dashboard.
|
||||
|
||||
<Note>
|
||||
If this step fails, you may have to add the CA certficate.
|
||||
</Note>
|
||||
|
||||

|
||||
</Step>
|
||||
<Step title="Generate dynamic secrets">
|
||||
Once you've successfully configured the dynamic secret, you're ready to generate on-demand credentials.
|
||||
To do this, simply click on the 'Generate' button which appears when hovering over the dynamic secret item.
|
||||
Alternatively, you can initiate the creation of a new lease by selecting 'New Lease' from the dynamic secret lease list section.
|
||||
|
||||

|
||||

|
||||
|
||||
When generating these secrets, it's important to specify a Time-to-Live (TTL) duration. This will dictate how long the credentials are valid for.
|
||||
|
||||

|
||||
|
||||
<Tip>
|
||||
Ensure that the TTL for the lease fall within the maximum TTL defined when configuring the dynamic secret in step 4.
|
||||
</Tip>
|
||||
|
||||
|
||||
Once you click the `Submit` button, a new secret lease will be generated and the credentials for it will be shown to you.
|
||||
|
||||

|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Audit or Revoke Leases
|
||||
Once you have created one or more leases, you will be able to access them by clicking on the respective dynamic secret item on the dashboard.
|
||||
This will allow you see the lease details and delete the lease ahead of its expiration time.
|
||||
|
||||

|
||||
|
||||
## Renew Leases
|
||||
To extend the life of the generated dynamic secret lease past its initial time to live, simply click on the **Renew** as illustrated below.
|
||||

|
||||
|
||||
<Warning>
|
||||
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
|
||||
</Warning>
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "PostgreSQL"
|
||||
description: "How to dynamically generate PostgreSQL database users"
|
||||
description: "How to dynamically generate PostgreSQL database users."
|
||||
---
|
||||
|
||||
The Infisical PostgreSQL dynamic secret allows you to generate PostgreSQL database credentials on demand based on configured role.
|
||||
@ -115,4 +115,4 @@ To extend the life of the generated dynamic secret leases past its initial time
|
||||
|
||||
<Warning>
|
||||
Lease renewals cannot exceed the maximum TTL set when configuring the dynamic secret
|
||||
</Warning>
|
||||
</Warning>
|
||||
|
@ -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).
|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
## Authentication methods
|
||||
|
||||
|
BIN
docs/images/guides/microsoft-power-apps/custom-connector.png
Normal file
BIN
docs/images/guides/microsoft-power-apps/custom-connector.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 34 KiB |
BIN
docs/images/guides/microsoft-power-apps/function-app.png
Normal file
BIN
docs/images/guides/microsoft-power-apps/function-app.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 247 KiB |
Binary file not shown.
Before ![]() (image error) Size: 584 KiB After ![]() (image error) Size: 162 KiB ![]() ![]() |
Binary file not shown.
After ![]() (image error) Size: 74 KiB |
Binary file not shown.
After ![]() (image error) Size: 28 KiB |
Binary file not shown.
After ![]() (image error) Size: 60 KiB |
BIN
docs/images/platform/dynamic-secrets/modify-cql-statements.png
Normal file
BIN
docs/images/platform/dynamic-secrets/modify-cql-statements.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 57 KiB |
@ -30,7 +30,7 @@ Prerequisites:
|
||||
"ssm:DeleteParameter",
|
||||
"ssm:GetParametersByPath",
|
||||
"ssm:DeleteParameters",
|
||||
"ssm:AddTagsToResource"
|
||||
"ssm:AddTagsToResource" // if you need to add tags to secrets
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
|
@ -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:
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
Navigate to your project's integrations tab in Infisical.
|
||||
|
||||

|
||||
@ -52,12 +55,6 @@ Prerequisites:
|
||||
|
||||

|
||||
|
||||
<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.
|
||||
|
||||

|
||||
|
||||
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>
|
||||

|
||||
|
||||
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>
|
||||
|
@ -32,7 +32,10 @@
|
||||
"thumbsRating": true
|
||||
},
|
||||
"api": {
|
||||
"baseUrl": ["https://app.infisical.com", "http://localhost:8080"]
|
||||
"baseUrl": [
|
||||
"https://app.infisical.com",
|
||||
"http://localhost:8080"
|
||||
]
|
||||
},
|
||||
"topbarLinks": [
|
||||
{
|
||||
@ -85,7 +88,8 @@
|
||||
"documentation/guides/introduction",
|
||||
"documentation/guides/node",
|
||||
"documentation/guides/python",
|
||||
"documentation/guides/nextjs-vercel"
|
||||
"documentation/guides/nextjs-vercel",
|
||||
"documentation/guides/microsoft-power-apps"
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -141,7 +145,8 @@
|
||||
"documentation/platform/dynamic-secrets/overview",
|
||||
"documentation/platform/dynamic-secrets/postgresql",
|
||||
"documentation/platform/dynamic-secrets/mysql",
|
||||
"documentation/platform/dynamic-secrets/oracle"
|
||||
"documentation/platform/dynamic-secrets/oracle",
|
||||
"documentation/platform/dynamic-secrets/cassandra"
|
||||
]
|
||||
},
|
||||
"documentation/platform/groups"
|
||||
@ -369,11 +374,15 @@
|
||||
},
|
||||
{
|
||||
"group": "Build Tool Integrations",
|
||||
"pages": ["integrations/build-tools/gradle"]
|
||||
"pages": [
|
||||
"integrations/build-tools/gradle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "",
|
||||
"pages": ["sdks/overview"]
|
||||
"pages": [
|
||||
"sdks/overview"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "SDK's",
|
||||
@ -391,7 +400,9 @@
|
||||
"api-reference/overview/authentication",
|
||||
{
|
||||
"group": "Examples",
|
||||
"pages": ["api-reference/overview/examples/integration"]
|
||||
"pages": [
|
||||
"api-reference/overview/examples/integration"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -520,11 +531,15 @@
|
||||
},
|
||||
{
|
||||
"group": "Service Tokens",
|
||||
"pages": ["api-reference/endpoints/service-tokens/get"]
|
||||
"pages": [
|
||||
"api-reference/endpoints/service-tokens/get"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Audit Logs",
|
||||
"pages": ["api-reference/endpoints/audit-logs/export-audit-log"]
|
||||
"pages": [
|
||||
"api-reference/endpoints/audit-logs/export-audit-log"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -540,7 +555,9 @@
|
||||
},
|
||||
{
|
||||
"group": "",
|
||||
"pages": ["changelog/overview"]
|
||||
"pages": [
|
||||
"changelog/overview"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Contributing",
|
||||
@ -564,7 +581,9 @@
|
||||
},
|
||||
{
|
||||
"group": "Contributing to SDK",
|
||||
"pages": ["contributing/sdk/developing"]
|
||||
"pages": [
|
||||
"contributing/sdk/developing"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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
|
||||
```
|
||||
|
@ -25,23 +25,24 @@ export const UpgradeProjectAlert = ({
|
||||
project,
|
||||
transparent
|
||||
}: UpgradeProjectAlertProps): JSX.Element | null => {
|
||||
|
||||
const router = useRouter();
|
||||
const { membership } = useProjectPermission();
|
||||
const { hasProjectRole } = useProjectPermission();
|
||||
const upgradeProject = useUpgradeProject();
|
||||
const [currentStatus, setCurrentStatus] = useState<string | null>(null);
|
||||
const [isUpgrading, setIsUpgrading] = useState(false);
|
||||
|
||||
const isProjectAdmin = hasProjectRole("admin");
|
||||
|
||||
const {
|
||||
data: projectStatus,
|
||||
isLoading: statusIsLoading,
|
||||
refetch: manualProjectStatusRefetch
|
||||
} = useGetUpgradeProjectStatus({
|
||||
projectId: project.id,
|
||||
enabled: membership.role === "admin" && project.version === ProjectVersion.V1,
|
||||
enabled: isProjectAdmin && project.version === ProjectVersion.V1,
|
||||
refetchInterval: 5_000,
|
||||
onSuccess: (data) => {
|
||||
if (membership.role !== "admin") {
|
||||
if (!isProjectAdmin) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -101,7 +102,7 @@ export const UpgradeProjectAlert = ({
|
||||
variant="solid"
|
||||
size="md"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading || membership.role !== "admin"}
|
||||
isDisabled={isLoading || !isProjectAdmin}
|
||||
onClick={onUpgradeProject}
|
||||
>
|
||||
Upgrade
|
||||
@ -113,13 +114,13 @@ export const UpgradeProjectAlert = ({
|
||||
<div
|
||||
className={twMerge(
|
||||
"mt-4 flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white",
|
||||
membership.role !== "admin" && "opacity-80"
|
||||
!isProjectAdmin && "opacity-80"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faWarning} className="pr-6 text-6xl text-white/80" />
|
||||
<div className="flex w-full flex-col text-sm">
|
||||
<span className="mb-2 text-lg font-semibold">Upgrade your project</span>
|
||||
{membership.role === "admin" ? (
|
||||
{isProjectAdmin ? (
|
||||
<>
|
||||
<p>
|
||||
Upgrade your project version to continue receiving the latest improvements and
|
||||
@ -150,12 +151,12 @@ export const UpgradeProjectAlert = ({
|
||||
</div>
|
||||
<div className="my-2">
|
||||
<Tooltip
|
||||
className={twMerge(membership.role === "admin" && "hidden")}
|
||||
className={twMerge(isProjectAdmin && "hidden")}
|
||||
content="You need to be an admin to upgrade the project."
|
||||
>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading || membership.role !== "admin"}
|
||||
isDisabled={isLoading || !isProjectAdmin}
|
||||
onClick={onUpgradeProject}
|
||||
>
|
||||
Upgrade
|
||||
|
@ -56,5 +56,7 @@ export const useProjectPermission = () => {
|
||||
throw new Error("useProjectPermission to be used within <ProjectPermissionContext>");
|
||||
}
|
||||
|
||||
return ctx;
|
||||
const hasProjectRole = (role: string) => ctx?.membership?.roles?.includes(role) || false;
|
||||
|
||||
return { ...ctx, hasProjectRole };
|
||||
};
|
||||
|
@ -16,7 +16,8 @@ export type TDynamicSecret = {
|
||||
};
|
||||
|
||||
export enum DynamicSecretProviders {
|
||||
SqlDatabase = "sql-database"
|
||||
SqlDatabase = "sql-database",
|
||||
Cassandra = "cassandra"
|
||||
}
|
||||
|
||||
export enum SqlProviders {
|
||||
@ -25,21 +26,37 @@ export enum SqlProviders {
|
||||
Oracle = "oracledb"
|
||||
}
|
||||
|
||||
export type TDynamicSecretProvider = {
|
||||
type: DynamicSecretProviders;
|
||||
inputs: {
|
||||
client: SqlProviders;
|
||||
host: string;
|
||||
port: number;
|
||||
database: string;
|
||||
username: string;
|
||||
password: string;
|
||||
creationStatement: string;
|
||||
revocationStatement: string;
|
||||
renewStatement?: string;
|
||||
ca?: string | undefined;
|
||||
export type TDynamicSecretProvider =
|
||||
| {
|
||||
type: DynamicSecretProviders.SqlDatabase;
|
||||
inputs: {
|
||||
client: SqlProviders;
|
||||
host: string;
|
||||
port: number;
|
||||
database: string;
|
||||
username: string;
|
||||
password: string;
|
||||
creationStatement: string;
|
||||
revocationStatement: string;
|
||||
renewStatement?: string;
|
||||
ca?: string | undefined;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: DynamicSecretProviders.Cassandra;
|
||||
inputs: {
|
||||
host: string;
|
||||
port: number;
|
||||
keyspace?: string;
|
||||
localDataCenter: string;
|
||||
username: string;
|
||||
password: string;
|
||||
creationStatement: string;
|
||||
revocationStatement: string;
|
||||
renewStatement?: string;
|
||||
ca?: string | undefined;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type TCreateDynamicSecretDTO = {
|
||||
projectSlug: string;
|
||||
|
@ -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
|
||||
|
@ -58,6 +58,11 @@ export type Project = {
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export type KmsKey = {
|
||||
id: string;
|
||||
alias: string;
|
||||
};
|
||||
|
||||
export type Service = {
|
||||
name: string;
|
||||
serviceId: string;
|
||||
|
@ -66,7 +66,8 @@ export const useCreateIntegration = () => {
|
||||
secretAWSTag?: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
}[];
|
||||
kmsKeyId?: string;
|
||||
};
|
||||
}) => {
|
||||
const {
|
||||
|
@ -106,7 +106,7 @@ const getUserProjectPermissions = async ({ workspaceId }: TGetUserProjectPermiss
|
||||
const { data } = await apiRequest.get<{
|
||||
data: {
|
||||
permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[];
|
||||
membership: TProjectMembership;
|
||||
membership: Omit<TProjectMembership, "roles"> & { roles: { role: string }[] };
|
||||
};
|
||||
}>(`/api/v1/workspace/${workspaceId}/permissions`, {});
|
||||
|
||||
@ -121,6 +121,12 @@ export const useGetUserProjectPermissions = ({ workspaceId }: TGetUserProjectPer
|
||||
select: (data) => {
|
||||
const rule = unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(data.permissions);
|
||||
const ability = createMongoAbility<ProjectPermissionSet>(rule, { conditionsMatcher });
|
||||
return { permission: ability, membership: data.membership };
|
||||
|
||||
const membership = {
|
||||
...data.membership,
|
||||
roles: data.membership.roles.map(({ role }) => role)
|
||||
};
|
||||
|
||||
return { permission: ability, membership };
|
||||
}
|
||||
});
|
||||
|
@ -62,7 +62,7 @@ export type TProjectMembership = {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
projectId: string;
|
||||
roleId: string;
|
||||
roles: string[];
|
||||
};
|
||||
|
||||
export type TWorkspaceUser = {
|
||||
@ -79,29 +79,29 @@ export type TWorkspaceUser = {
|
||||
organization: string;
|
||||
roles: (
|
||||
| {
|
||||
id: string;
|
||||
role: "owner" | "admin" | "member" | "no-access" | "custom";
|
||||
customRoleId: string;
|
||||
customRoleName: string;
|
||||
customRoleSlug: string;
|
||||
isTemporary: false;
|
||||
temporaryRange: null;
|
||||
temporaryMode: null;
|
||||
temporaryAccessEndTime: null;
|
||||
temporaryAccessStartTime: null;
|
||||
}
|
||||
id: string;
|
||||
role: "owner" | "admin" | "member" | "no-access" | "custom";
|
||||
customRoleId: string;
|
||||
customRoleName: string;
|
||||
customRoleSlug: string;
|
||||
isTemporary: false;
|
||||
temporaryRange: null;
|
||||
temporaryMode: null;
|
||||
temporaryAccessEndTime: null;
|
||||
temporaryAccessStartTime: null;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
role: "owner" | "admin" | "member" | "no-access" | "custom";
|
||||
customRoleId: string;
|
||||
customRoleName: string;
|
||||
customRoleSlug: string;
|
||||
isTemporary: true;
|
||||
temporaryRange: string;
|
||||
temporaryMode: string;
|
||||
temporaryAccessEndTime: string;
|
||||
temporaryAccessStartTime: string;
|
||||
}
|
||||
id: string;
|
||||
role: "owner" | "admin" | "member" | "no-access" | "custom";
|
||||
customRoleId: string;
|
||||
customRoleName: string;
|
||||
customRoleSlug: string;
|
||||
isTemporary: true;
|
||||
temporaryRange: string;
|
||||
temporaryMode: string;
|
||||
temporaryAccessEndTime: string;
|
||||
temporaryAccessStartTime: string;
|
||||
}
|
||||
)[];
|
||||
status: "invited" | "accepted" | "verified" | "completed";
|
||||
deniedPermissions: any[];
|
||||
|
@ -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)}
|
||||
/>
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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">
|
||||
|
363
frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx
Normal file
363
frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CassandraInputForm.tsx
Normal file
@ -0,0 +1,363 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
SecretInput,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
provider: z.object({
|
||||
host: z.string().toLowerCase().min(1),
|
||||
port: z.coerce.number(),
|
||||
keyspace: z.string().optional(),
|
||||
localDataCenter: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
renewStatement: z.string().optional(),
|
||||
ca: z.string().optional()
|
||||
}),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
name: z.string().refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onCompleted: () => void;
|
||||
onCancel: () => void;
|
||||
secretPath: string;
|
||||
projectSlug: string;
|
||||
environment: string;
|
||||
};
|
||||
|
||||
const getSqlStatements = () => {
|
||||
return {
|
||||
creationStatement:
|
||||
"CREATE ROLE '{{username}}' WITH PASSWORD = '{{password}}' AND LOGIN=true;\nGRANT ALL PERMISSIONS ON ALL KEYSPACES TO '{{username}}';",
|
||||
renewStatement: "",
|
||||
revocationStatement: 'DROP ROLE "{{username}}";'
|
||||
};
|
||||
};
|
||||
|
||||
export const CassandraInputForm = ({
|
||||
onCompleted,
|
||||
onCancel,
|
||||
environment,
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
provider: getSqlStatements()
|
||||
}
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (createDynamicSecret.isLoading) return;
|
||||
try {
|
||||
await createDynamicSecret.mutateAsync({
|
||||
provider: { type: DynamicSecretProviders.Cassandra, inputs: provider },
|
||||
maxTTL,
|
||||
name,
|
||||
path: secretPath,
|
||||
defaultTTL,
|
||||
projectSlug,
|
||||
environmentSlug: environment
|
||||
});
|
||||
onCompleted();
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="dynamic-postgres" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultTTL"
|
||||
defaultValue="1h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Default TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxTTL"
|
||||
defaultValue="24h"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Max TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.host"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Host"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="host1,host2" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.port"
|
||||
defaultValue={9042}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Port"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.localDataCenter"
|
||||
defaultValue="datacenter1"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Local Data Center"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.username"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="User"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.password"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Password"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.keyspace"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Keyspace"
|
||||
isError={Boolean(error?.message)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.ca"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isOptional
|
||||
label="CA(SSL)"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretInput
|
||||
{...field}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Accordion type="single" collapsible className="mb-2 w-full bg-mineshaft-700">
|
||||
<AccordionItem value="advance-statements">
|
||||
<AccordionTrigger>Modify CQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.creationStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Creation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="variables: keyspace. username, password and expiration are dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.revocationStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Revocation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="variables: keyspace, username is dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.renewStatement"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Renew Statement"
|
||||
helperText="variables: keyspace, username and expiration are dynamically provisioned"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Submit
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -6,6 +6,7 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Modal, ModalContent } from "@app/components/v2";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
import { CassandraInputForm } from "./CassandraInputForm";
|
||||
import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm";
|
||||
|
||||
type Props = {
|
||||
@ -21,6 +22,19 @@ enum WizardSteps {
|
||||
ProviderInputs = "provider-inputs"
|
||||
}
|
||||
|
||||
const DYNAMIC_SECRET_LIST = [
|
||||
{
|
||||
icon: faDatabase,
|
||||
provider: DynamicSecretProviders.SqlDatabase,
|
||||
title: "SQL\nDatabase"
|
||||
},
|
||||
{
|
||||
icon: faDatabase,
|
||||
provider: DynamicSecretProviders.Cassandra,
|
||||
title: "Cassandra"
|
||||
}
|
||||
];
|
||||
|
||||
export const CreateDynamicSecretForm = ({
|
||||
isOpen,
|
||||
onToggle,
|
||||
@ -55,35 +69,34 @@ export const CreateDynamicSecretForm = ({
|
||||
>
|
||||
<div className="mb-4 text-mineshaft-300">Select a service to connect to:</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div
|
||||
className="flex h-32 w-32 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
setSelectedProvider(DynamicSecretProviders.SqlDatabase);
|
||||
setWizardStep(WizardSteps.ProviderInputs);
|
||||
}}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
setSelectedProvider(DynamicSecretProviders.SqlDatabase);
|
||||
{DYNAMIC_SECRET_LIST.map(({ icon, provider, title }) => (
|
||||
<div
|
||||
key={`dynamic-secret-provider-${provider}`}
|
||||
className="flex h-32 w-32 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
setSelectedProvider(provider);
|
||||
setWizardStep(WizardSteps.ProviderInputs);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDatabase} size="lg" />
|
||||
<div className="text-center text-sm">
|
||||
SQL
|
||||
<br />
|
||||
Database
|
||||
}}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
setSelectedProvider(provider);
|
||||
setWizardStep(WizardSteps.ProviderInputs);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} size="lg" />
|
||||
<div className="whitespace-pre-wrap text-center text-sm">{title}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === WizardSteps.ProviderInputs &&
|
||||
selectedProvider === DynamicSecretProviders.SqlDatabase && (
|
||||
<motion.div
|
||||
key="select-input-step"
|
||||
key="dynamic-sql-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
@ -98,6 +111,24 @@ export const CreateDynamicSecretForm = ({
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{wizardStep === WizardSteps.ProviderInputs &&
|
||||
selectedProvider === DynamicSecretProviders.Cassandra && (
|
||||
<motion.div
|
||||
key="dynamic-cassandra-step"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<CassandraInputForm
|
||||
onCompleted={handleFormReset}
|
||||
onCancel={handleFormReset}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
@ -141,7 +141,7 @@ export const SqlDatabaseInputForm = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)}>
|
||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
@ -265,7 +265,7 @@ export const SqlDatabaseInputForm = ({
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@ -278,7 +278,7 @@ export const SqlDatabaseInputForm = ({
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" />
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@ -55,7 +55,10 @@ const OutputDisplay = ({
|
||||
|
||||
const renderOutputForm = (provider: DynamicSecretProviders, data: unknown) => {
|
||||
const { DB_PASSWORD, DB_USERNAME } = data as { DB_USERNAME: string; DB_PASSWORD: string };
|
||||
if (provider === DynamicSecretProviders.SqlDatabase) {
|
||||
if (
|
||||
provider === DynamicSecretProviders.SqlDatabase ||
|
||||
provider === DynamicSecretProviders.Cassandra
|
||||
) {
|
||||
return (
|
||||
<div>
|
||||
<OutputDisplay label="Database User" value={DB_USERNAME} />
|
||||
@ -102,7 +105,6 @@ export const CreateDynamicSecretLease = ({
|
||||
ttl: "1h"
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const createDynamicSecretLease = useCreateDynamicSecretLease();
|
||||
|
||||
|
374
frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretCassandraForm.tsx
Normal file
374
frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretCassandraForm.tsx
Normal file
@ -0,0 +1,374 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
SecretInput,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
inputs: z
|
||||
.object({
|
||||
host: z.string().toLowerCase().min(1),
|
||||
port: z.coerce.number(),
|
||||
keyspace: z.string().optional(),
|
||||
localDataCenter: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
renewStatement: z.string().optional(),
|
||||
ca: z.string().optional()
|
||||
})
|
||||
.partial(),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
}),
|
||||
maxTTL: z
|
||||
.string()
|
||||
.optional()
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val) return;
|
||||
const valMs = ms(val);
|
||||
if (valMs < 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
|
||||
// a day
|
||||
if (valMs > 24 * 60 * 60 * 1000)
|
||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||
})
|
||||
.nullable(),
|
||||
newName: z
|
||||
.string()
|
||||
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
|
||||
.optional()
|
||||
});
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
dynamicSecret: TDynamicSecret & { inputs: unknown };
|
||||
secretPath: string;
|
||||
environment: string;
|
||||
projectSlug: string;
|
||||
};
|
||||
|
||||
export const EditDynamicSecretCassandraForm = ({
|
||||
onClose,
|
||||
dynamicSecret,
|
||||
environment,
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
defaultTTL: dynamicSecret.defaultTTL,
|
||||
maxTTL: dynamicSecret.maxTTL,
|
||||
newName: dynamicSecret.name,
|
||||
inputs: {
|
||||
...(dynamicSecret.inputs as TForm["inputs"])
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
if (updateDynamicSecret.isLoading) return;
|
||||
try {
|
||||
await updateDynamicSecret.mutateAsync({
|
||||
name: dynamicSecret.name,
|
||||
path: secretPath,
|
||||
projectSlug,
|
||||
environmentSlug: environment,
|
||||
data: {
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
}
|
||||
});
|
||||
onClose();
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated dynamic secret"
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update dynamic secret"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
control={control}
|
||||
name="newName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Secret Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="DYN-1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="defaultTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Default TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-32">
|
||||
<Controller
|
||||
control={control}
|
||||
name="maxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={<TtlFormLabel label="Max TTL" />}
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} value={field.value || ""} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.host"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Host"
|
||||
className="flex-grow"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.port"
|
||||
defaultValue={9042}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Port"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
onChange={(el) => field.onChange(parseInt(el.target.value, 10))}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.localDataCenter"
|
||||
defaultValue="datacenter1"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Local Data Center"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.username"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="User"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.password"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Password"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.keyspace"
|
||||
defaultValue="default"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Keyspace"
|
||||
isOptional
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.ca"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isOptional
|
||||
label="CA(SSL)"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<SecretInput
|
||||
{...field}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Accordion type="multiple" className="w-full bg-mineshaft-700">
|
||||
<AccordionItem value="modify-sql-statement">
|
||||
<AccordionTrigger>Modify CQL Statements</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.creationStatement"
|
||||
defaultValue={
|
||||
"CREATE ROLE '{{username}}' WITH PASSWORD '{{password}}' AND LOGIN=true;\nGRANT ALL PERMISSIONS ON ALL KEYSPACES TO '{{username}}';"
|
||||
}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Creation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="variables: keyspace. username, password and expiration are dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.revocationStatement"
|
||||
defaultValue='DROP ROLE "{{username}}";'
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Revocation Statement"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
helperText="variables: keyspace, username is dynamically provisioned"
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.renewStatement"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Renew Statement"
|
||||
helperText="variables: keyspace, username and expiration are dynamically provisioned"
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<TextArea
|
||||
{...field}
|
||||
reSize="none"
|
||||
rows={3}
|
||||
className="border-mineshaft-600 bg-mineshaft-900 text-sm"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Button type="submit" isLoading={isSubmitting}>
|
||||
Save
|
||||
</Button>
|
||||
<Button variant="outline_bg" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -4,6 +4,7 @@ import { Spinner } from "@app/components/v2";
|
||||
import { useGetDynamicSecretDetails } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
import { EditDynamicSecretCassandraForm } from "./EditDynamicSecretCassandraForm";
|
||||
import { EditDynamicSecretSqlProviderForm } from "./EditDynamicSecretSqlProviderForm";
|
||||
|
||||
type Props = {
|
||||
@ -56,6 +57,23 @@ export const EditDynamicSecretForm = ({
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
{dynamicSecretDetails?.type === DynamicSecretProviders.Cassandra && (
|
||||
<motion.div
|
||||
key="cassandra-provider-edit"
|
||||
transition={{ duration: 0.1 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
>
|
||||
<EditDynamicSecretCassandraForm
|
||||
onClose={onClose}
|
||||
projectSlug={projectSlug}
|
||||
secretPath={secretPath}
|
||||
dynamicSecret={dynamicSecretDetails}
|
||||
environment={environment}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
@ -128,7 +128,7 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)}>
|
||||
<form onSubmit={handleSubmit(handleUpdateDynamicSecret)} autoComplete="off">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-grow">
|
||||
<Controller
|
||||
@ -244,7 +244,7 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} />
|
||||
<Input {...field} autoComplete="off" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@ -257,7 +257,7 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="password" />
|
||||
<Input {...field} type="password" autoComplete="new-password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@ -8,5 +8,6 @@ export type Metadata = {
|
||||
secretAWSTag?: {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
}[]
|
||||
kmsKeyId?: string;
|
||||
}
|
1820
sink/cassandra.yaml
Normal file
1820
sink/cassandra.yaml
Normal file
File diff suppressed because it is too large
Load Diff
10
sink/docker-compose.cassandra.yml
Normal file
10
sink/docker-compose.cassandra.yml
Normal file
@ -0,0 +1,10 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
cassandra:
|
||||
image: cassandra
|
||||
volumes:
|
||||
- ./cassandra.yaml:/etc/cassandra/cassandra.yaml
|
||||
ports:
|
||||
- "9042:9042"
|
||||
|
Reference in New Issue
Block a user