mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 07:28:21 +00:00
Merge branch 'main' into feat/pg-dynamic-secret
This commit is contained in:
+3
-3
@@ -3,9 +3,6 @@
|
|||||||
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||||
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||||
|
|
||||||
# Required
|
|
||||||
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
# Required secrets to sign JWT tokens
|
# Required secrets to sign JWT tokens
|
||||||
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||||
@@ -16,6 +13,9 @@ POSTGRES_PASSWORD=infisical
|
|||||||
POSTGRES_USER=infisical
|
POSTGRES_USER=infisical
|
||||||
POSTGRES_DB=infisical
|
POSTGRES_DB=infisical
|
||||||
|
|
||||||
|
# Required
|
||||||
|
DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||||
|
|
||||||
# Redis
|
# Redis
|
||||||
REDIS_URL=redis://redis:6379
|
REDIS_URL=redis://redis:6379
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
name: Deployment pipeline
|
||||||
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
infisical-image:
|
||||||
|
name: Build backend image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: ☁️ Checkout source
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: 📦 Install dependencies to test all dependencies
|
||||||
|
run: npm ci --only-production
|
||||||
|
working-directory: backend
|
||||||
|
- name: Save commit hashes for tag
|
||||||
|
id: commit
|
||||||
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
|
- name: 🔧 Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: 🐋 Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Set up Depot CLI
|
||||||
|
uses: depot/setup-action@v1
|
||||||
|
- name: 📦 Build backend and export to Docker
|
||||||
|
uses: depot/build-push-action@v1
|
||||||
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
|
load: true
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.standalone-infisical
|
||||||
|
tags: infisical/infisical:test
|
||||||
|
- name: 🏗️ Build backend and push to docker hub
|
||||||
|
uses: depot/build-push-action@v1
|
||||||
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
|
push: true
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.standalone-infisical
|
||||||
|
tags: |
|
||||||
|
infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||||
|
infisical/staging_infisical:latest
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
build-args: |
|
||||||
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
|
INFISICAL_PLATFORM_VERSION=${{ steps.commit.outputs.short }}
|
||||||
|
|
||||||
|
gamma-deployment:
|
||||||
|
name: Deploy to gamma
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [infisical-image]
|
||||||
|
environment:
|
||||||
|
name: Gamma
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
- name: Change directory to backend and install dependencies
|
||||||
|
env:
|
||||||
|
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
npm install
|
||||||
|
npm run migration:latest
|
||||||
|
- name: Configure AWS Credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
audience: sts.amazonaws.com
|
||||||
|
aws-region: us-east-1
|
||||||
|
role-to-assume: arn:aws:iam::905418227878:role/deploy-new-ecs-img
|
||||||
|
- name: Save commit hashes for tag
|
||||||
|
id: commit
|
||||||
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
|
- name: Download task definition
|
||||||
|
run: |
|
||||||
|
aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json
|
||||||
|
- name: Render Amazon ECS task definition
|
||||||
|
id: render-web-container
|
||||||
|
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||||
|
with:
|
||||||
|
task-definition: task-definition.json
|
||||||
|
container-name: infisical-prod-platform
|
||||||
|
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||||
|
environment-variables: "LOG_LEVEL=info"
|
||||||
|
- name: Deploy to Amazon ECS service
|
||||||
|
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||||
|
with:
|
||||||
|
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
||||||
|
service: infisical-prod-platform
|
||||||
|
cluster: infisical-prod-platform
|
||||||
|
wait-for-service-stability: true
|
||||||
|
|
||||||
|
production-postgres-deployment:
|
||||||
|
name: Deploy to production
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [gamma-deployment]
|
||||||
|
environment:
|
||||||
|
name: Production
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
- name: Change directory to backend and install dependencies
|
||||||
|
env:
|
||||||
|
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
npm install
|
||||||
|
npm run migration:latest
|
||||||
|
- name: Configure AWS Credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v4
|
||||||
|
with:
|
||||||
|
audience: sts.amazonaws.com
|
||||||
|
aws-region: us-east-1
|
||||||
|
role-to-assume: arn:aws:iam::381492033652:role/gha-make-prod-deployment
|
||||||
|
- name: Save commit hashes for tag
|
||||||
|
id: commit
|
||||||
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
|
- name: Download task definition
|
||||||
|
run: |
|
||||||
|
aws ecs describe-task-definition --task-definition infisical-prod-platform --query taskDefinition > task-definition.json
|
||||||
|
- name: Render Amazon ECS task definition
|
||||||
|
id: render-web-container
|
||||||
|
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
||||||
|
with:
|
||||||
|
task-definition: task-definition.json
|
||||||
|
container-name: infisical-prod-platform
|
||||||
|
image: infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
||||||
|
environment-variables: "LOG_LEVEL=info"
|
||||||
|
- name: Deploy to Amazon ECS service
|
||||||
|
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
|
||||||
|
with:
|
||||||
|
task-definition: ${{ steps.render-web-container.outputs.task-definition }}
|
||||||
|
service: infisical-prod-platform
|
||||||
|
cluster: infisical-prod-platform
|
||||||
|
wait-for-service-stability: true
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
name: Build, Publish and Deploy to Gamma
|
|
||||||
on: [workflow_dispatch]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
infisical-image:
|
|
||||||
name: Build backend image
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: ☁️ Checkout source
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Configure AWS credentials
|
|
||||||
uses: aws-actions/configure-aws-credentials@v1
|
|
||||||
with:
|
|
||||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_FOR_ECR }}
|
|
||||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_FOR_ECR }}
|
|
||||||
aws-region: us-east-1
|
|
||||||
- name: Login to Amazon ECR
|
|
||||||
id: login-ecr
|
|
||||||
uses: aws-actions/amazon-ecr-login@v1
|
|
||||||
- name: 📦 Install dependencies to test all dependencies
|
|
||||||
run: npm ci --only-production
|
|
||||||
working-directory: backend
|
|
||||||
# - name: 🧪 Run tests
|
|
||||||
# run: npm run test:ci
|
|
||||||
# working-directory: backend
|
|
||||||
- name: Save commit hashes for tag
|
|
||||||
id: commit
|
|
||||||
uses: pr-mpt/actions-commit-hash@v2
|
|
||||||
- name: 🔧 Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
- name: 🐋 Login to Docker Hub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
- name: Set up Depot CLI
|
|
||||||
uses: depot/setup-action@v1
|
|
||||||
- name: 📦 Build backend and export to Docker
|
|
||||||
uses: depot/build-push-action@v1
|
|
||||||
with:
|
|
||||||
project: 64mmf0n610
|
|
||||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
|
||||||
load: true
|
|
||||||
context: .
|
|
||||||
file: Dockerfile.standalone-infisical
|
|
||||||
tags: infisical/infisical:test
|
|
||||||
- name: 🏗️ Build backend and push to docker hub
|
|
||||||
uses: depot/build-push-action@v1
|
|
||||||
with:
|
|
||||||
project: 64mmf0n610
|
|
||||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
|
||||||
push: true
|
|
||||||
context: .
|
|
||||||
file: Dockerfile.standalone-infisical
|
|
||||||
tags: |
|
|
||||||
infisical/staging_infisical:${{ steps.commit.outputs.short }}
|
|
||||||
infisical/staging_infisical:latest
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
build-args: |
|
|
||||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
|
||||||
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
|
||||||
|
|
||||||
|
|
||||||
postgres-migration:
|
|
||||||
name: Run latest migration files
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [infisical-image]
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Setup Node.js environment
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
- name: Change directory to backend and install dependencies
|
|
||||||
env:
|
|
||||||
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
|
||||||
run: |
|
|
||||||
cd backend
|
|
||||||
npm install
|
|
||||||
npm run migration:latest
|
|
||||||
# - name: Run postgres DB migration files
|
|
||||||
# env:
|
|
||||||
# DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
|
||||||
# run: npm run migration:latest
|
|
||||||
gamma-deployment:
|
|
||||||
name: Deploy to gamma
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [postgres-migration]
|
|
||||||
steps:
|
|
||||||
- name: ☁️ Checkout source
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Install Helm
|
|
||||||
uses: azure/setup-helm@v3
|
|
||||||
with:
|
|
||||||
version: v3.10.0
|
|
||||||
- name: Install infisical helm chart
|
|
||||||
run: |
|
|
||||||
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
|
|
||||||
helm repo update
|
|
||||||
- name: Install kubectl
|
|
||||||
uses: azure/setup-kubectl@v3
|
|
||||||
- name: Install doctl
|
|
||||||
uses: digitalocean/action-doctl@v2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
|
||||||
- name: Save DigitalOcean kubeconfig with short-lived credentials
|
|
||||||
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 infisical-gamma-postgres
|
|
||||||
- name: switch to gamma namespace
|
|
||||||
run: kubectl config set-context --current --namespace=gamma
|
|
||||||
- name: test kubectl
|
|
||||||
run: kubectl get ingress
|
|
||||||
- name: Download helm values to file and upgrade gamma deploy
|
|
||||||
run: |
|
|
||||||
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
|
|
||||||
helm upgrade infisical infisical-helm-charts/infisical-standalone --values values.yaml --wait --install
|
|
||||||
if [[ $(helm status infisical) == *"FAILED"* ]]; then
|
|
||||||
echo "Helm upgrade failed"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "Helm upgrade was successful"
|
|
||||||
fi
|
|
||||||
@@ -194,6 +194,25 @@ export const FOLDERS = {
|
|||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const SECRETS = {
|
||||||
|
ATTACH_TAGS: {
|
||||||
|
secretName: "The name of the secret to attach tags to.",
|
||||||
|
secretPath: "The path of the secret to attach tags to.",
|
||||||
|
type: "The type of the secret to attach tags to. (shared/personal)",
|
||||||
|
environment: "The slug of the environment where the secret is located",
|
||||||
|
projectSlug: "The slug of the project where the secret is located",
|
||||||
|
tagSlugs: "An array of tag slugs to attach to the secret."
|
||||||
|
},
|
||||||
|
DETACH_TAGS: {
|
||||||
|
secretName: "The name of the secret to detach tags from.",
|
||||||
|
secretPath: "The path of the secret to detach tags from.",
|
||||||
|
type: "The type of the secret to attach tags to. (shared/personal)",
|
||||||
|
environment: "The slug of the environment where the secret is located",
|
||||||
|
projectSlug: "The slug of the project where the secret is located",
|
||||||
|
tagSlugs: "An array of tag slugs to detach from the secret."
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const RAW_SECRETS = {
|
export const RAW_SECRETS = {
|
||||||
LIST: {
|
LIST: {
|
||||||
workspaceId: "The ID of the project to list secrets from.",
|
workspaceId: "The ID of the project to list secrets from.",
|
||||||
@@ -361,5 +380,18 @@ export const DYNAMIC_SECRET_LEASES = {
|
|||||||
leaseId: "The ID of the dynamic secret lease.",
|
leaseId: "The ID of the dynamic secret lease.",
|
||||||
isForced:
|
isForced:
|
||||||
"A boolean flag to delete the the dynamic secret from infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
|
"A boolean flag to delete the the dynamic secret from infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
|
||||||
|
export const SECRET_TAGS = {
|
||||||
|
LIST: {
|
||||||
|
projectId: "The ID of the project to list tags from."
|
||||||
|
},
|
||||||
|
CREATE: {
|
||||||
|
projectId: "The ID of the project to create the tag in.",
|
||||||
|
name: "The name of the tag to create.",
|
||||||
|
slug: "The slug of the tag to create.",
|
||||||
|
color: "The color of the tag to create."
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
tagId: "The ID of the tag to delete.",
|
||||||
|
projectId: "The ID of the project to delete the tag from."
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretTagsSchema } from "@app/db/schemas";
|
import { SecretTagsSchema } from "@app/db/schemas";
|
||||||
|
import { SECRET_TAGS } from "@app/lib/api-docs";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim()
|
projectId: z.string().trim().describe(SECRET_TAGS.LIST.projectId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -36,12 +37,12 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim()
|
projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim(),
|
name: z.string().trim().describe(SECRET_TAGS.CREATE.name),
|
||||||
slug: z.string().trim(),
|
slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug),
|
||||||
color: z.string()
|
color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -68,8 +69,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim(),
|
projectId: z.string().trim().describe(SECRET_TAGS.DELETE.projectId),
|
||||||
tagId: z.string().trim()
|
tagId: z.string().trim().describe(SECRET_TAGS.DELETE.tagId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { CommitType } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
import { CommitType } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
|
||||||
import { RAW_SECRETS } from "@app/lib/api-docs";
|
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { removeTrailingSlash } from "@app/lib/fn";
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
@@ -23,6 +23,124 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
|||||||
import { secretRawSchema } from "../sanitizedSchemas";
|
import { secretRawSchema } from "../sanitizedSchemas";
|
||||||
|
|
||||||
export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/tags/:secretName",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
description: "Attach tags to a secret",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
secretName: z.string().trim().describe(SECRETS.ATTACH_TAGS.secretName)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
projectSlug: z.string().trim().describe(SECRETS.ATTACH_TAGS.projectSlug),
|
||||||
|
environment: z.string().trim().describe(SECRETS.ATTACH_TAGS.environment),
|
||||||
|
secretPath: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("/")
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(SECRETS.ATTACH_TAGS.secretPath),
|
||||||
|
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(SECRETS.ATTACH_TAGS.type),
|
||||||
|
tagSlugs: z.string().array().min(1).describe(SECRETS.ATTACH_TAGS.tagSlugs)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
|
||||||
|
z.object({
|
||||||
|
tags: SecretTagsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
color: true
|
||||||
|
}).array()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secret = await server.services.secret.attachTags({
|
||||||
|
secretName: req.params.secretName,
|
||||||
|
tagSlugs: req.body.tagSlugs,
|
||||||
|
path: req.body.secretPath,
|
||||||
|
environment: req.body.environment,
|
||||||
|
type: req.body.type,
|
||||||
|
projectSlug: req.body.projectSlug,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secret };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/tags/:secretName",
|
||||||
|
method: "DELETE",
|
||||||
|
schema: {
|
||||||
|
description: "Detach tags from a secret",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
secretName: z.string().trim().describe(SECRETS.DETACH_TAGS.secretName)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
projectSlug: z.string().trim().describe(SECRETS.DETACH_TAGS.projectSlug),
|
||||||
|
environment: z.string().trim().describe(SECRETS.DETACH_TAGS.environment),
|
||||||
|
secretPath: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.default("/")
|
||||||
|
.transform(removeTrailingSlash)
|
||||||
|
.describe(SECRETS.DETACH_TAGS.secretPath),
|
||||||
|
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(SECRETS.DETACH_TAGS.type),
|
||||||
|
tagSlugs: z.string().array().min(1).describe(SECRETS.DETACH_TAGS.tagSlugs)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
|
||||||
|
z.object({
|
||||||
|
tags: SecretTagsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
color: true
|
||||||
|
}).array()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secret = await server.services.secret.detachTags({
|
||||||
|
secretName: req.params.secretName,
|
||||||
|
tagSlugs: req.body.tagSlugs,
|
||||||
|
path: req.body.secretPath,
|
||||||
|
environment: req.body.environment,
|
||||||
|
type: req.body.type,
|
||||||
|
projectSlug: req.body.projectSlug,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secret };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
url: "/raw",
|
url: "/raw",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|||||||
@@ -168,8 +168,12 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findProjectBySlug = async (slug: string, orgId: string) => {
|
const findProjectBySlug = async (slug: string, orgId: string | undefined) => {
|
||||||
try {
|
try {
|
||||||
|
if (!orgId) {
|
||||||
|
throw new BadRequestError({ message: "Organization ID is required when querying with slugs" });
|
||||||
|
}
|
||||||
|
|
||||||
const projects = await db(TableName.ProjectMembership)
|
const projects = await db(TableName.ProjectMembership)
|
||||||
.where(`${TableName.Project}.slug`, slug)
|
.where(`${TableName.Project}.slug`, slug)
|
||||||
.where(`${TableName.Project}.orgId`, orgId)
|
.where(`${TableName.Project}.orgId`, orgId)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
|
import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
@@ -284,10 +284,11 @@ export const projectServiceFactory = ({
|
|||||||
|
|
||||||
// Get the role permission for the identity
|
// Get the role permission for the identity
|
||||||
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
|
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
|
||||||
ProjectMembershipRole.Admin,
|
OrgMembershipRole.Member,
|
||||||
organization.id
|
organization.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Identity has to be at least a member in order to create projects
|
||||||
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
|
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
if (!hasPrivilege)
|
if (!hasPrivilege)
|
||||||
throw new ForbiddenRequestError({
|
throw new ForbiddenRequestError({
|
||||||
|
|||||||
@@ -150,6 +150,27 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSecretTags = async (secretId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const tags = await (tx || db)(TableName.JnSecretTag)
|
||||||
|
.join(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`)
|
||||||
|
.where({ [`${TableName.Secret}Id` as const]: secretId })
|
||||||
|
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||||
|
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||||
|
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||||
|
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
|
||||||
|
|
||||||
|
return tags.map((el) => ({
|
||||||
|
id: el.tagId,
|
||||||
|
color: el.tagColor,
|
||||||
|
slug: el.tagSlug,
|
||||||
|
name: el.tagName
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "get secret tags" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findByBlindIndexes = async (
|
const findByBlindIndexes = async (
|
||||||
folderId: string,
|
folderId: string,
|
||||||
blindIndexes: Array<{ blindIndex: string; type: SecretType }>,
|
blindIndexes: Array<{ blindIndex: string; type: SecretType }>,
|
||||||
@@ -184,6 +205,7 @@ export const secretDALFactory = (db: TDbClient) => {
|
|||||||
bulkUpdate,
|
bulkUpdate,
|
||||||
deleteMany,
|
deleteMany,
|
||||||
bulkUpdateNoVersionIncrement,
|
bulkUpdateNoVersionIncrement,
|
||||||
|
getSecretTags,
|
||||||
findByFolderId,
|
findByFolderId,
|
||||||
findByBlindIndexes
|
findByBlindIndexes
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { TSecretDALFactory } from "./secret-dal";
|
|||||||
import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns";
|
import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns";
|
||||||
import { TSecretQueueFactory } from "./secret-queue";
|
import { TSecretQueueFactory } from "./secret-queue";
|
||||||
import {
|
import {
|
||||||
|
TAttachSecretTagsDTO,
|
||||||
TCreateBulkSecretDTO,
|
TCreateBulkSecretDTO,
|
||||||
TCreateSecretDTO,
|
TCreateSecretDTO,
|
||||||
TCreateSecretRawDTO,
|
TCreateSecretRawDTO,
|
||||||
@@ -47,7 +48,7 @@ type TSecretServiceFactoryDep = {
|
|||||||
secretTagDAL: TSecretTagDALFactory;
|
secretTagDAL: TSecretTagDALFactory;
|
||||||
secretVersionDAL: TSecretVersionDALFactory;
|
secretVersionDAL: TSecretVersionDALFactory;
|
||||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus">;
|
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
|
||||||
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
@@ -307,6 +308,7 @@ export const secretServiceFactory = ({
|
|||||||
if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
|
if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
|
||||||
|
|
||||||
const { secretName, ...el } = inputSecret;
|
const { secretName, ...el } = inputSecret;
|
||||||
|
|
||||||
const updatedSecret = await secretDAL.transaction(async (tx) =>
|
const updatedSecret = await secretDAL.transaction(async (tx) =>
|
||||||
fnSecretBulkUpdate({
|
fnSecretBulkUpdate({
|
||||||
folderId,
|
folderId,
|
||||||
@@ -442,6 +444,7 @@ export const secretServiceFactory = ({
|
|||||||
const folderId = folder.id;
|
const folderId = folder.id;
|
||||||
|
|
||||||
const secrets = await secretDAL.findByFolderId(folderId, actorId);
|
const secrets = await secretDAL.findByFolderId(folderId, actorId);
|
||||||
|
|
||||||
if (includeImports) {
|
if (includeImports) {
|
||||||
const secretImports = await secretImportDAL.find({ folderId });
|
const secretImports = await secretImportDAL.find({ folderId });
|
||||||
const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
|
const allowedImports = secretImports.filter(({ importEnv, importPath }) =>
|
||||||
@@ -994,7 +997,209 @@ export const secretServiceFactory = ({
|
|||||||
return secretVersions;
|
return secretVersions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const attachTags = async ({
|
||||||
|
secretName,
|
||||||
|
tagSlugs,
|
||||||
|
path: secretPath,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
projectSlug,
|
||||||
|
actor,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actorId
|
||||||
|
}: TAttachSecretTagsDTO) => {
|
||||||
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
|
||||||
|
await projectDAL.checkProjectUpgradeStatus(project.id);
|
||||||
|
|
||||||
|
const secret = await getSecretByName({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId: project.id,
|
||||||
|
environment,
|
||||||
|
path: secretPath,
|
||||||
|
secretName,
|
||||||
|
type
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
throw new BadRequestError({ message: "Secret not found" });
|
||||||
|
}
|
||||||
|
const folder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
|
||||||
|
|
||||||
|
if (!folder) {
|
||||||
|
throw new BadRequestError({ message: "Folder not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = await secretTagDAL.find({
|
||||||
|
projectId: project.id,
|
||||||
|
$in: {
|
||||||
|
slug: tagSlugs
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tags.length !== tagSlugs.length) {
|
||||||
|
throw new BadRequestError({ message: "One or more tags not found." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretTags = await secretDAL.getSecretTags(secret.id);
|
||||||
|
|
||||||
|
if (secretTags.some((tag) => tagSlugs.includes(tag.slug))) {
|
||||||
|
throw new BadRequestError({ message: "One or more tags already exist on the secret" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedTags = new Set([...secretTags.map((tag) => tag.id), ...tags.map((el) => el.id)]);
|
||||||
|
|
||||||
|
const updatedSecret = await secretDAL.transaction(async (tx) =>
|
||||||
|
fnSecretBulkUpdate({
|
||||||
|
folderId: folder.id,
|
||||||
|
projectId: project.id,
|
||||||
|
inputSecrets: [
|
||||||
|
{
|
||||||
|
filter: { id: secret.id },
|
||||||
|
data: {
|
||||||
|
tags: Array.from(combinedTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
secretDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretTagDAL,
|
||||||
|
secretVersionTagDAL,
|
||||||
|
tx
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await snapshotService.performSnapshot(folder.id);
|
||||||
|
await secretQueueService.syncSecrets({ secretPath, projectId: project.id, environment });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updatedSecret[0],
|
||||||
|
tags: [...secretTags, ...tags].map((t) => ({ id: t.id, slug: t.slug, name: t.name, color: t.color }))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const detachTags = async ({
|
||||||
|
secretName,
|
||||||
|
tagSlugs,
|
||||||
|
path: secretPath,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
projectSlug,
|
||||||
|
actor,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actorId
|
||||||
|
}: TAttachSecretTagsDTO) => {
|
||||||
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
|
||||||
|
await projectDAL.checkProjectUpgradeStatus(project.id);
|
||||||
|
|
||||||
|
const secret = await getSecretByName({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId: project.id,
|
||||||
|
environment,
|
||||||
|
path: secretPath,
|
||||||
|
secretName,
|
||||||
|
type
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
throw new BadRequestError({ message: "Secret not found" });
|
||||||
|
}
|
||||||
|
const folder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
|
||||||
|
|
||||||
|
if (!folder) {
|
||||||
|
throw new BadRequestError({ message: "Folder not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = await secretTagDAL.find({
|
||||||
|
projectId: project.id,
|
||||||
|
$in: {
|
||||||
|
slug: tagSlugs
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tags.length !== tagSlugs.length) {
|
||||||
|
throw new BadRequestError({ message: "One or more tags not found." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretTags = await secretDAL.getSecretTags(secret.id);
|
||||||
|
|
||||||
|
// Make sure all the tags exist on the secret
|
||||||
|
const tagIdsToRemove = tags.map((tag) => tag.id);
|
||||||
|
const secretTagIds = secretTags.map((tag) => tag.id);
|
||||||
|
|
||||||
|
if (!tagIdsToRemove.every((el) => secretTagIds.includes(el))) {
|
||||||
|
throw new BadRequestError({ message: "One or more tags not found on the secret" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTags = secretTags.filter((tag) => !tagIdsToRemove.includes(tag.id));
|
||||||
|
|
||||||
|
const updatedSecret = await secretDAL.transaction(async (tx) =>
|
||||||
|
fnSecretBulkUpdate({
|
||||||
|
folderId: folder.id,
|
||||||
|
projectId: project.id,
|
||||||
|
inputSecrets: [
|
||||||
|
{
|
||||||
|
filter: { id: secret.id },
|
||||||
|
data: {
|
||||||
|
tags: newTags.map((tag) => tag.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
secretDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretTagDAL,
|
||||||
|
secretVersionTagDAL,
|
||||||
|
tx
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await snapshotService.performSnapshot(folder.id);
|
||||||
|
await secretQueueService.syncSecrets({ secretPath, projectId: project.id, environment });
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updatedSecret[0],
|
||||||
|
tags: newTags
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
attachTags,
|
||||||
|
detachTags,
|
||||||
createSecret,
|
createSecret,
|
||||||
deleteSecret,
|
deleteSecret,
|
||||||
updateSecret,
|
updateSecret,
|
||||||
|
|||||||
@@ -206,6 +206,15 @@ export type TFnSecretBulkUpdate = {
|
|||||||
tx?: Knex;
|
tx?: Knex;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TAttachSecretTagsDTO = {
|
||||||
|
projectSlug: string;
|
||||||
|
secretName: string;
|
||||||
|
tagSlugs: string[];
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
type: SecretType;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TFnSecretBulkDelete = {
|
export type TFnSecretBulkDelete = {
|
||||||
folderId: string;
|
folderId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|||||||
@@ -406,14 +406,14 @@ func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request) error {
|
func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request, secretName string) error {
|
||||||
var secretsResponse GetEncryptedSecretsV3Response
|
var secretsResponse GetEncryptedSecretsV3Response
|
||||||
response, err := httpClient.
|
response, err := httpClient.
|
||||||
R().
|
R().
|
||||||
SetResult(&secretsResponse).
|
SetResult(&secretsResponse).
|
||||||
SetHeader("User-Agent", USER_AGENT).
|
SetHeader("User-Agent", USER_AGENT).
|
||||||
SetBody(request).
|
SetBody(request).
|
||||||
Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName))
|
Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, secretName))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err)
|
return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err)
|
||||||
|
|||||||
@@ -401,7 +401,6 @@ type DeleteSecretV3Request struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateSecretByNameV3Request struct {
|
type UpdateSecretByNameV3Request struct {
|
||||||
SecretName string `json:"secretName"`
|
|
||||||
WorkspaceID string `json:"workspaceId"`
|
WorkspaceID string `json:"workspaceId"`
|
||||||
Environment string `json:"environment"`
|
Environment string `json:"environment"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
|||||||
@@ -297,7 +297,6 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
updateSecretRequest := api.UpdateSecretByNameV3Request{
|
updateSecretRequest := api.UpdateSecretByNameV3Request{
|
||||||
WorkspaceID: workspaceFile.WorkspaceId,
|
WorkspaceID: workspaceFile.WorkspaceId,
|
||||||
Environment: environmentName,
|
Environment: environmentName,
|
||||||
SecretName: secret.PlainTextKey,
|
|
||||||
SecretValueCiphertext: secret.SecretValueCiphertext,
|
SecretValueCiphertext: secret.SecretValueCiphertext,
|
||||||
SecretValueIV: secret.SecretValueIV,
|
SecretValueIV: secret.SecretValueIV,
|
||||||
SecretValueTag: secret.SecretValueTag,
|
SecretValueTag: secret.SecretValueTag,
|
||||||
@@ -305,7 +304,7 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
SecretPath: secretsPath,
|
SecretPath: secretsPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest)
|
err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest, secret.PlainTextKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to process secret update request")
|
util.HandleError(err, "Unable to process secret update request")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v1/workspace/{projectId}/tags"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/workspace/{projectId}/tags/{tagId}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/workspace/{projectId}/tags"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Attach tags"
|
||||||
|
openapi: "POST /api/v3/secrets/tags/{secretName}"
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Detach tags"
|
||||||
|
openapi: "DELETE /api/v3/secrets/tags/{secretName}"
|
||||||
|
---
|
||||||
@@ -1,37 +1,102 @@
|
|||||||
---
|
---
|
||||||
title: "MySQL/MariaDB"
|
title: "MySQL/MariaDB"
|
||||||
description: "Rotated database user password of a MySQL or MariaDB"
|
description: "How to rotate MySQL/MariaDB database user passwords"
|
||||||
---
|
---
|
||||||
|
|
||||||
Infisical will update periodically the provided database user's password.
|
The Infisical MySQL secret rotation allows you to automatically rotate your MySQL database user's password at a predefined interval.
|
||||||
|
|
||||||
<Warning>
|
|
||||||
At present Infisical do require access to your database. We will soon be released Infisical agent based rotation which would help you rotate without direct database access from Infisical cloud.
|
|
||||||
</Warning>
|
|
||||||
|
|
||||||
## Working
|
## Prerequisite
|
||||||
|
|
||||||
1. User's has to create the two user's for Infisical to rotate and provide them required database access
|
1. Create two users with the required permission in your MySQL instance. We'll refer to them as `user-a` and `user-b`.
|
||||||
2. Infisical will connect with your database with admin access
|
2. Create another MySQL user with just the permission to update the passwords of `user-a` and `user-b`. We'll refer to this user as the `admin` user.
|
||||||
3. If last rotated one was username1, then username2 is chosen to be rotated
|
|
||||||
5. Update it's password with random value
|
To learn more about MySQL permission system, please visit this [documentation](https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html).
|
||||||
6. After testing it gets saved to the provided secret mapping
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. Infisical connects to your database using the provided `admin` user account.
|
||||||
|
2. A random value is generated and the password for `user-a` is updated with the new value.
|
||||||
|
3. The new password is then tested by logging into the database
|
||||||
|
4. If test is success, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s).
|
||||||
|
5. The process is then repeated for `user-b` on the next rotation.
|
||||||
|
6. The cycle repeats until secret rotation is deleted/stopped.
|
||||||
|
|
||||||
## Rotation Configuration
|
## Rotation Configuration
|
||||||
|
|
||||||
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
|
<Steps>
|
||||||
2. Click on `MySQL`
|
<Step title="Open Secret Rotation Page">
|
||||||
3. Provide the inputs
|
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
|
||||||
- Admin Username: DB admin username
|
</Step>
|
||||||
- Admin Password: DB admin password
|
<Step title="Click on MySQL card" />
|
||||||
- Host: DB host
|
<Step title="Provide the inputs">
|
||||||
- Port: DB port(number)
|
<ParamField path="Admin Username" type="string" required>
|
||||||
- Username1: The first username in two to rotate
|
Rotator admin username
|
||||||
- Username2: The second username in two to rotate
|
</ParamField>
|
||||||
- CA: Certificate to connect with database(string)
|
|
||||||
4. Final step
|
|
||||||
- Select `Environment`, `Secret Path` and `Interval` to rotate the secrets
|
|
||||||
- Finally select the secrets in your provided board to replace with new secret after each rotation
|
|
||||||
- Your done and good to go.
|
|
||||||
|
|
||||||
Congrats. You have 10x your MySQL/MariaDB access security.
|
<ParamField path="Admin password" type="string" required>
|
||||||
|
Rotator admin password
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Host" type="string" required>
|
||||||
|
Database host url
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Port" type="number" required>
|
||||||
|
Database port number
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Username1" type="string" required>
|
||||||
|
The first username of two to rotate - `user-a`
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Username2" type="string" required>
|
||||||
|
The second username of two to rotate - `user-b`
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="CA" type="string">
|
||||||
|
Optional database certificate to connect with database
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure the output secret mapping">
|
||||||
|
When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
|
||||||
|
|
||||||
|
<ParamField path="Environment" type="string" required>
|
||||||
|
The environment where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Secret Path" type="string" required>
|
||||||
|
The secret path where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Interval" type="number" required>
|
||||||
|
What interval should the credentials be rotated in days.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="DB USERNAME" type="string" required>
|
||||||
|
Select an existing secret key where the rotated database username value should be saved to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="DB PASSWORD" type="string" required>
|
||||||
|
Select an existing select key where the rotated database password value should be saved to.
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Why can't we delete the other user when rotating?">
|
||||||
|
When a system has multiple nodes by horizontal scaling, redeployment doesn't happen instantly.
|
||||||
|
|
||||||
|
This means that when the secrets are rotated, and the redeployment is triggered, the existing system will still be using the old credentials until the change rolls out.
|
||||||
|
|
||||||
|
To avoid causing failure for them, the old credentials are not removed. Instead, in the next rotation, the previous user's credentials are updated.
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Why do you need root user account?">
|
||||||
|
The admin account is used by Infisical to update the credentials for `user-a` and `user-b`.
|
||||||
|
|
||||||
|
You don't need to grant all permission for your admin account but rather just the permissions to update both of the user's passwords.
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
|||||||
@@ -1,33 +1,104 @@
|
|||||||
---
|
---
|
||||||
title: "PostgreSQL/CockroachDB"
|
title: "PostgreSQL/CockroachDB"
|
||||||
description: "Rotated database user password of a PostgreSQL or Cockroach DB"
|
description: "How to rotate postgreSQL/cockroach database user passwords"
|
||||||
---
|
---
|
||||||
|
|
||||||
Infisical will update periodically the provided database user's password.
|
The Infisical Postgres secret rotation allows you to automatically rotate your Postgres database user's password at a predefined interval.
|
||||||
|
|
||||||
## Working
|
|
||||||
|
|
||||||
1. User's has to create the two user's for Infisical to rotate and provide them required database access.
|
## Prerequisite
|
||||||
2. Infisical will connect with your database with admin access.
|
|
||||||
3. If last rotated one was username1, then username2 is chosen to be rotated.
|
1. Create two users with the required permission in your PostgreSQL instance. We'll refer to them as `user-a` and `user-b`.
|
||||||
5. Update it's password with random value.
|
2. Create another PostgreSQL user with just the permission to update the passwords of `user-a` and `user-b`. We'll refer to this user as the `admin` user.
|
||||||
6. After testing it gets saved to the provided secret mapping.
|
|
||||||
|
To learn more about Postgres permission system, please visit this [documentation](https://www.postgresql.org/docs/9.1/sql-grant.html).
|
||||||
|
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. Infisical connects to your database using the provided `admin` user account.
|
||||||
|
2. A random value is generated and the password for `user-a` is updated with the new value.
|
||||||
|
3. The new password is then tested by logging into the database
|
||||||
|
4. If test is success, it's saved to the output secret mappings so that rest of the system gets the newly rotated value(s).
|
||||||
|
5. The process is then repeated for `user-b` on the next rotation.
|
||||||
|
6. The cycle repeats until secret rotation is deleted/stopped.
|
||||||
|
|
||||||
## Rotation Configuration
|
## Rotation Configuration
|
||||||
|
|
||||||
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
|
<Steps>
|
||||||
2. Click on `PostgreSQL`
|
<Step title="Open Secret Rotation Page">
|
||||||
3. Provide the inputs
|
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
|
||||||
- Admin Username: DB admin username
|
</Step>
|
||||||
- Admin Password: DB admin password
|
<Step title="Click on PostgresSQL card" />
|
||||||
- Host: DB host
|
|
||||||
- Port: DB port(number)
|
|
||||||
- Username1: The first username in two to rotate
|
|
||||||
- Username2: The second username in two to rotate
|
|
||||||
- CA: Certificate to connect with database(string)
|
|
||||||
4. Final step
|
|
||||||
- Select `Environment`, `Secret Path` and `Interval` to rotate the secrets
|
|
||||||
- Finally select the secrets in your provided board to replace with new secret after each rotation
|
|
||||||
- Your done and good to go.
|
|
||||||
|
|
||||||
Congratulations. You have improved your PostgreSQL/CockroachDB access security.
|
<Step title="Provide the inputs">
|
||||||
|
<ParamField path="Admin Username" type="string" required="true">
|
||||||
|
Rotator admin username
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Admin password" type="string" required="true">
|
||||||
|
Rotator admin password
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Host" type="string" required="true">
|
||||||
|
Database host url
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Port" type="number" required="true">
|
||||||
|
Database port number
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Username1" type="string" required="true">
|
||||||
|
The first username of two to rotate - `user-a`
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Username2" type="string" required="true">
|
||||||
|
The second username of two to rotate - `user-b`
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="CA" type="string" optional>
|
||||||
|
Optional database certificate to connect with database
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure the output secret mapping">
|
||||||
|
|
||||||
|
When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
|
||||||
|
|
||||||
|
<ParamField path="Environment" type="string" required>
|
||||||
|
The environment where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Secret Path" type="string" required>
|
||||||
|
The secret path where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Interval" type="number" required>
|
||||||
|
What interval should the credentials be rotated in days.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="DB USERNAME" type="string" required>
|
||||||
|
Select an existing secret key where the rotated database username value should be saved to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="DB PASSWORD" type="string" required>
|
||||||
|
Select an existing select key where the rotated database password value should be saved to.
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="Why can't we delete the other user when rotating?">
|
||||||
|
When a system has multiple nodes by horizontal scaling, redeployment doesn't happen instantly.
|
||||||
|
|
||||||
|
This means that when the secrets are rotated, and the redeployment is triggered, the existing system will still be using the old credentials until the change rolls out.
|
||||||
|
|
||||||
|
To avoid causing failure for them, the old credentials are not removed. Instead, in the next rotation, the previous user's credentials are updated.
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="Why do you need root user account?">
|
||||||
|
The admin account is used by Infisical to update the credentials for `user-a` and `user-b`.
|
||||||
|
|
||||||
|
You don't need to grant all permission for your admin account but rather just the permissions to update both of the user's passwords.
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
|
|||||||
@@ -1,31 +1,58 @@
|
|||||||
---
|
---
|
||||||
title: "Twilio SendGrid"
|
title: "Twilio SendGrid"
|
||||||
description: "Rotate Twilio SendGrid API keys"
|
description: "How to rotate Twilio SendGrid API keys"
|
||||||
---
|
---
|
||||||
|
|
||||||
Twilio SendGrid is a cloud-based email delivery platform that helps businesses send transactional and marketing emails.
|
Eliminate the use of long lived secrets by rotating Twilio SendGrid API keys with Infisical.
|
||||||
It uses an API key to do various operations. Using Infisical you can easily dynamically change the keys.
|
|
||||||
|
|
||||||
## Working
|
## Prerequisite
|
||||||
|
|
||||||
1. Infisical will need an admin token of SendGrid to create API keys dynamically.
|
You will need a valid SendGrid admin key with the necessary scope to create additional API keys.
|
||||||
2. Using the given admin token and scope by user Infisical will create and rotate API keys periodically
|
|
||||||
3. Under the hood infisical uses [SendGrid API](https://docs.sendgrid.com/api-reference/api-keys/create-api-keys)
|
Follow the [SendGrid Docs to create an admin api key](https://docs.sendgrid.com/ui/account-and-settings/api-keys)
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
Using the provided admin API key, Infisical will attempt to create child API keys with the specified permissions.
|
||||||
|
New keys will ge generated every time a rotation occurs. Behind the scenes, Infisical uses the [SendGrid API](https://docs.sendgrid.com/api-reference/api-keys/create-api-keys) to generate new API keys.
|
||||||
|
|
||||||
## Rotation Configuration
|
## Rotation Configuration
|
||||||
|
|
||||||
1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation`
|
<Steps>
|
||||||
2. Click on `Twilio SendGrid Card`
|
<Step title="Open Secret Rotation Page">
|
||||||
3. Provide the inputs
|
Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar
|
||||||
- Admin API Key:
|
</Step>
|
||||||
SendGrid admin key to create lower scoped API keys.
|
<Step title="Click on Twilio SendGrid Card" />
|
||||||
- API Key Scopes
|
<Step title="Provide the inputs">
|
||||||
SendGrid generated API Key's scopes. For more info refer [this doc](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions)
|
<ParamField path="Admin API Key" type="string" required>
|
||||||
|
SendGrid admin API key with permission to create child scoped API keys.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
4. Final step
|
<ParamField path="Admin API Key" type="array" required>
|
||||||
- Select `Environment`, `Secret Path` and `Interval` to rotate the secrets
|
The permissions that the newly generated API keys will have. To view possible permissions, visit [this documentation](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions).
|
||||||
- Finally select the secrets in your provided board to replace with new secret after each rotation
|
Permissions must be entered as a list of strings.
|
||||||
- Your done and good to go.
|
|
||||||
|
|
||||||
Now your output mapped secret value will be replaced periodically by SendGrid.
|
|
||||||
|
|
||||||
|
Example: `["user.profile.read", "user.profile.update"]`
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
<Step title="Configure the output secret mapping">
|
||||||
|
When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project.
|
||||||
|
<ParamField path="Environment" type="string" required>
|
||||||
|
The environment where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Secret Path" type="string" required>
|
||||||
|
The secret path where the rotated credentials should be mapped to.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="Interval" type="number" required>
|
||||||
|
What interval should the credentials be rotated in days.
|
||||||
|
</ParamField>
|
||||||
|
|
||||||
|
<ParamField path="API KEY" type="string" required>
|
||||||
|
Select an existing select key where the newly rotated API key will get saved to.
|
||||||
|
</ParamField>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
Now your output mapped secret value will be replaced periodically by SendGrid.
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -12,7 +12,7 @@ The operator continuously updates secrets and can also reload dependent deployme
|
|||||||
|
|
||||||
## Install Operator
|
## Install Operator
|
||||||
|
|
||||||
The operator can be install via [Helm](helm.sh) or [kubectl](https://github.com/kubernetes/kubectl)
|
The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl)
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Helm (recommended)">
|
<Tab title="Helm (recommended)">
|
||||||
@@ -61,23 +61,38 @@ Once you have installed the operator to your cluster, you'll need to create a `I
|
|||||||
apiVersion: secrets.infisical.com/v1alpha1
|
apiVersion: secrets.infisical.com/v1alpha1
|
||||||
kind: InfisicalSecret
|
kind: InfisicalSecret
|
||||||
metadata:
|
metadata:
|
||||||
# Name of of this InfisicalSecret resource
|
name: infisicalsecret-sample
|
||||||
name: infisicalsecret-sample
|
labels:
|
||||||
|
label-to-be-passed-to-managed-secret: sample-value
|
||||||
|
annotations:
|
||||||
|
example.com/annotation-to-be-passed-to-managed-secret: "sample-value"
|
||||||
spec:
|
spec:
|
||||||
# The host that should be used to pull secrets from. If left empty, the value specified in Global configuration will be used
|
hostAPI: https://app.infisical.com/api
|
||||||
hostAPI: https://app.infisical.com/api
|
resyncInterval: 10
|
||||||
resyncInterval: 60
|
authentication:
|
||||||
authentication:
|
# Make sure to only have 1 authentication method defined, serviceToken/universalAuth.
|
||||||
serviceToken:
|
# If you have multiple authentication methods defined, it may cause issues.
|
||||||
serviceTokenSecretReference:
|
universalAuth:
|
||||||
secretName: service-token
|
secretsScope:
|
||||||
|
projectSlug: <project-slug>
|
||||||
|
envSlug: <env-slug> # "dev", "staging", "prod", etc..
|
||||||
|
secretsPath: "<secrets-path>" # Root is "/"
|
||||||
|
credentialsRef:
|
||||||
|
secretName: universal-auth-credentials
|
||||||
|
secretNamespace: default
|
||||||
|
|
||||||
|
serviceToken:
|
||||||
|
serviceTokenSecretReference:
|
||||||
|
secretName: service-token
|
||||||
|
secretNamespace: default
|
||||||
|
secretsScope:
|
||||||
|
envSlug: <env-slug>
|
||||||
|
secretsPath: <secrets-path> # Root is "/"
|
||||||
|
|
||||||
|
managedSecretReference:
|
||||||
|
secretName: managed-secret
|
||||||
secretNamespace: default
|
secretNamespace: default
|
||||||
secretsScope:
|
# secretType: kubernetes.io/dockerconfigjson
|
||||||
envSlug: dev
|
|
||||||
secretsPath: "/"
|
|
||||||
managedSecretReference:
|
|
||||||
secretName: managed-secret # <-- the name of kubernetes secret that will be created
|
|
||||||
secretNamespace: default # <-- where the kubernetes secret should be created
|
|
||||||
```
|
```
|
||||||
### InfisicalSecret CRD properties
|
### InfisicalSecret CRD properties
|
||||||
|
|
||||||
@@ -105,11 +120,60 @@ Default re-sync interval is every 1 minute.
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="authentication">
|
<Accordion title="authentication">
|
||||||
This block defines the method that will be used to authenticate with Infisical so that secrets can be fetched. Currently, only [Service Tokens](../../documentation/platform/token) can be used to authenticate with Infisical.
|
This block defines the method that will be used to authenticate with Infisical so that secrets can be fetched
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="authentication.serviceToken.serviceTokenSecretReference">
|
<Accordion title="authentication.universalAuth">
|
||||||
The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and name space of secret that stores this service token.
|
The universal machine identity authentication method is used to authenticate with Infisical. The client ID and client secret needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores these credentials.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create a machine identity">
|
||||||
|
You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about machine identities here](/documentation/platform/identities/universal-auth).
|
||||||
|
</Step>
|
||||||
|
<Step title="Create Kubernetes secret containing machine identity credentials">
|
||||||
|
Once you have created your machine identity and added it to your project(s), you will need to create a Kubernetes secret containing the identity credentials.
|
||||||
|
To quickly create a Kubernetes secret containing the identity credentials, you can run the command below.
|
||||||
|
|
||||||
|
Make sure you replace `<your-identity-client-id>` with the identity client ID and `<your-identity-client-secret>` with the identity client secret.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
kubectl create secret generic universal-auth-credentials --from-literal=clientId="<your-identity-client-id>" --from-literal=clientSecret="<your-identity-client-secret>"
|
||||||
|
```
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step title="Add reference for the Kubernetes secret containing the identity credentials">
|
||||||
|
Once the secret is created, add the `secretName` and `secretNamespace` of the secret that was just created under `authentication.universalAuth.credentialsRef` field in the InfisicalSecret resource.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Make sure to also populate the `secretsScope` field with the project slug _`projectSlug`_, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```yaml
|
||||||
|
apiVersion: secrets.infisical.com/v1alpha1
|
||||||
|
kind: InfisicalSecret
|
||||||
|
metadata:
|
||||||
|
name: infisicalsecret-sample-crd
|
||||||
|
spec:
|
||||||
|
authentication:
|
||||||
|
universalAuth:
|
||||||
|
secretsScope:
|
||||||
|
projectSlug: <project-slug> # <-- project slug
|
||||||
|
envSlug: <env-slug> # "dev", "staging", "prod", etc..
|
||||||
|
secretsPath: "<secrets-path>" # Root is "/"
|
||||||
|
credentialsRef:
|
||||||
|
secretName: universal-auth-credentials # <-- name of the Kubernetes secret that stores our machine identity credentials
|
||||||
|
secretNamespace: default # <-- namespace of the Kubernetes secret that stores our machine identity credentials
|
||||||
|
...
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="authentication.serviceToken">
|
||||||
|
The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores this service token.
|
||||||
Follow the instructions below to create and store the service token in a Kubernetes secrets and reference it in your CRD.
|
Follow the instructions below to create and store the service token in a Kubernetes secrets and reference it in your CRD.
|
||||||
|
|
||||||
#### 1. Generate service token
|
#### 1. Generate service token
|
||||||
@@ -122,13 +186,17 @@ Default re-sync interval is every 1 minute.
|
|||||||
To quickly create a Kubernetes secret containing the generated service token, you can run the command below. Make sure you replace `<your-service-token-here>` with your service token.
|
To quickly create a Kubernetes secret containing the generated service token, you can run the command below. Make sure you replace `<your-service-token-here>` with your service token.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
kubectl create secret generic service-token --from-literal=infisicalToken=<your-service-token-here>
|
kubectl create secret generic service-token --from-literal=infisicalToken="<your-service-token-here>"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 3. Add reference for the Kubernetes secret containing service token
|
#### 3. Add reference for the Kubernetes secret containing service token
|
||||||
|
|
||||||
Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource.
|
Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource.
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Make sure to also populate the `secretsScope` field with the, environment slug _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets from. Please see the example below.
|
||||||
|
</Info>
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: secrets.infisical.com/v1alpha1
|
apiVersion: secrets.infisical.com/v1alpha1
|
||||||
@@ -141,25 +209,13 @@ Default re-sync interval is every 1 minute.
|
|||||||
serviceTokenSecretReference:
|
serviceTokenSecretReference:
|
||||||
secretName: service-token # <-- name of the Kubernetes secret that stores our service token
|
secretName: service-token # <-- name of the Kubernetes secret that stores our service token
|
||||||
secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token
|
secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token
|
||||||
|
secretsScope:
|
||||||
|
envSlug: <env-slug> # "dev", "staging", "prod", etc..
|
||||||
|
secretsPath: <secrets-path> # Root is "/"
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="authentication.serviceToken.secretsScope">
|
|
||||||
This block defines the scope of what secrets should be fetched. This is needed as your service token can have access to multiple folders and environments.
|
|
||||||
A scope is defined by `envSlug` and `secretsPath`.
|
|
||||||
|
|
||||||
#### envSlug
|
|
||||||
|
|
||||||
This refers to the short hand name of an environment. For example for the `development` environment the environment slug is `dev`. You can locate the slug of your environment by heading to your project settings in the Infisical dashboard.
|
|
||||||
|
|
||||||
#### secretsPath
|
|
||||||
|
|
||||||
secretsPath is the path to the secret in the given environment. For example a path of `/` would refer to the root of the environment whereas `/folder1` would refer to the secrets in folder1 from the root.
|
|
||||||
|
|
||||||
Both fields are required.
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Accordion title="managedSecretReference">
|
<Accordion title="managedSecretReference">
|
||||||
The `managedSecretReference` field is used to define the target location for storing secrets retrieved from an Infisical project.
|
The `managedSecretReference` field is used to define the target location for storing secrets retrieved from an Infisical project.
|
||||||
This field requires specifying both the name and namespace of the Kubernetes secret that will hold these secrets.
|
This field requires specifying both the name and namespace of the Kubernetes secret that will hold these secrets.
|
||||||
|
|||||||
+11
-1
@@ -467,6 +467,14 @@
|
|||||||
"api-reference/endpoints/folders/delete"
|
"api-reference/endpoints/folders/delete"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Secret tags",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/secret-tags/list",
|
||||||
|
"api-reference/endpoints/secret-tags/create",
|
||||||
|
"api-reference/endpoints/secret-tags/delete"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Secrets",
|
"group": "Secrets",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -474,7 +482,9 @@
|
|||||||
"api-reference/endpoints/secrets/create",
|
"api-reference/endpoints/secrets/create",
|
||||||
"api-reference/endpoints/secrets/read",
|
"api-reference/endpoints/secrets/read",
|
||||||
"api-reference/endpoints/secrets/update",
|
"api-reference/endpoints/secrets/update",
|
||||||
"api-reference/endpoints/secrets/delete"
|
"api-reference/endpoints/secrets/delete",
|
||||||
|
"api-reference/endpoints/secrets/attach-tags",
|
||||||
|
"api-reference/endpoints/secrets/detach-tags"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?
|
|||||||
if (!isVisible) return replaceContentWithDot(content);
|
if (!isVisible) return replaceContentWithDot(content);
|
||||||
|
|
||||||
let skipNext = false;
|
let skipNext = false;
|
||||||
const formatedContent = content.split(REGEX).flatMap((el, i) => {
|
const formattedContent = content.split(REGEX).flatMap((el, i) => {
|
||||||
const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}");
|
const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}");
|
||||||
if (isInterpolationSyntax) {
|
if (isInterpolationSyntax) {
|
||||||
skipNext = true;
|
skipNext = true;
|
||||||
return (
|
return (
|
||||||
<span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}>
|
<span className="ph-no-capture text-yellow" key={`secret-value-${i + 1}`}>
|
||||||
${<span className="ph-no-capture text-yello-200/80">{el.slice(2, -1)}</span>
|
${<span className="ph-no-capture text-yellow-200/80">{el.slice(2, -1)}</span>
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -41,7 +41,7 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?
|
|||||||
|
|
||||||
// akhilmhdh: Dont remove this br. I am still clueless how this works but weirdly enough
|
// akhilmhdh: Dont remove this br. I am still clueless how this works but weirdly enough
|
||||||
// when break is added a line break works properly
|
// when break is added a line break works properly
|
||||||
return formatedContent.concat(<br />);
|
return formattedContent.concat(<br />);
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`inline-flex items-center justify-between rounded-md
|
`inline-flex items-center justify-between rounded-md
|
||||||
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200`,
|
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200 focus:bg-mineshaft-700/80`,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -106,7 +106,7 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
|
|||||||
className={twMerge(
|
className={twMerge(
|
||||||
`relative mb-0.5 flex
|
`relative mb-0.5 flex
|
||||||
cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
|
cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
|
||||||
outline-none transition-all hover:bg-mineshaft-500`,
|
outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
|
||||||
isSelected && "bg-primary",
|
isSelected && "bg-primary",
|
||||||
isDisabled &&
|
isDisabled &&
|
||||||
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
|
"cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
|
||||||
|
|||||||
@@ -9,16 +9,22 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
|
|||||||
import { useProjectPermission } from "@app/context";
|
import { useProjectPermission } from "@app/context";
|
||||||
import { useGetUpgradeProjectStatus, useUpgradeProject } from "@app/hooks/api";
|
import { useGetUpgradeProjectStatus, useUpgradeProject } from "@app/hooks/api";
|
||||||
import { Workspace } from "@app/hooks/api/types";
|
import { Workspace } from "@app/hooks/api/types";
|
||||||
|
import { workspaceKeys } from "@app/hooks/api/workspace/queries";
|
||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
|
import { queryClient } from "@app/reactQuery";
|
||||||
|
|
||||||
import { Button } from "../Button";
|
import { Button } from "../Button";
|
||||||
import { Tooltip } from "../Tooltip";
|
import { Tooltip } from "../Tooltip";
|
||||||
|
|
||||||
export type UpgradeProjectAlertProps = {
|
export type UpgradeProjectAlertProps = {
|
||||||
project: Workspace;
|
project: Workspace;
|
||||||
|
transparent?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX.Element | null => {
|
export const UpgradeProjectAlert = ({
|
||||||
|
project,
|
||||||
|
transparent
|
||||||
|
}: UpgradeProjectAlertProps): JSX.Element | null => {
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { membership } = useProjectPermission();
|
const { membership } = useProjectPermission();
|
||||||
@@ -48,6 +54,7 @@ export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX.
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentStatus !== null && data?.status === null) {
|
if (currentStatus !== null && data?.status === null) {
|
||||||
|
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||||
router.reload();
|
router.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,10 +94,25 @@ export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX.
|
|||||||
|
|
||||||
if (project.version !== ProjectVersion.V1) return null;
|
if (project.version !== ProjectVersion.V1) return null;
|
||||||
|
|
||||||
|
if (transparent) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="solid"
|
||||||
|
size="md"
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={isLoading || membership.role !== "admin"}
|
||||||
|
onClick={onUpgradeProject}
|
||||||
|
>
|
||||||
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
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",
|
"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"
|
membership.role !== "admin" && "opacity-80"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import axios from "axios";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
|
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api";
|
import { useCreateIntegration, useGetWorkspaceById } from "@app/hooks/api";
|
||||||
|
|
||||||
import { Button, Card, CardTitle, FormControl, Select, SelectItem } from "../../../components/v2";
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardTitle,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem} from "../../../components/v2";
|
||||||
import {
|
import {
|
||||||
useGetIntegrationAuthApps,
|
useGetIntegrationAuthApps,
|
||||||
useGetIntegrationAuthById
|
useGetIntegrationAuthById
|
||||||
@@ -18,8 +27,10 @@ const cloudflareEnvironments = [
|
|||||||
export default function CloudflarePagesIntegrationPage() {
|
export default function CloudflarePagesIntegrationPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutateAsync } = useCreateIntegration();
|
const { mutateAsync } = useCreateIntegration();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
|
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
|
||||||
|
const [secretPath, setSecretPath] = useState("/");
|
||||||
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
|
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
|
||||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||||
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
||||||
@@ -65,7 +76,7 @@ export default function CloudflarePagesIntegrationPage() {
|
|||||||
appId: targetAppId,
|
appId: targetAppId,
|
||||||
sourceEnvironment: selectedSourceEnvironment,
|
sourceEnvironment: selectedSourceEnvironment,
|
||||||
targetEnvironment,
|
targetEnvironment,
|
||||||
secretPath: "/"
|
secretPath
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -73,6 +84,18 @@ export default function CloudflarePagesIntegrationPage() {
|
|||||||
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
|
let errorMessage: string = "Something went wrong!";
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
const { message } = err?.response?.data as { message: string };
|
||||||
|
errorMessage = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: errorMessage,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,6 +129,13 @@ export default function CloudflarePagesIntegrationPage() {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<FormControl label="Infisical Secret Path" className="mt-2 px-6">
|
||||||
|
<Input
|
||||||
|
value={secretPath}
|
||||||
|
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
<FormControl label="Cloudflare Pages Project" className="mt-4 px-6">
|
<FormControl label="Cloudflare Pages Project" className="mt-4 px-6">
|
||||||
<Select
|
<Select
|
||||||
value={targetApp}
|
value={targetApp}
|
||||||
|
|||||||
+20
-9
@@ -15,7 +15,7 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
|
import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
|
||||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
import { IdentityAuthMethod, useAddIdentityUniversalAuth } from "@app/hooks/api/identities";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = yup
|
const schema = yup
|
||||||
@@ -40,7 +40,7 @@ type Props = {
|
|||||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void;
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
|
export const IdentityModal = ({ popUp, /* handlePopUpOpen, */ handlePopUpToggle }: Props) => {
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
|
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
@@ -50,6 +50,7 @@ export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Pro
|
|||||||
|
|
||||||
const { mutateAsync: createMutateAsync } = useCreateIdentity();
|
const { mutateAsync: createMutateAsync } = useCreateIdentity();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
|
||||||
|
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@@ -112,21 +113,31 @@ export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Pro
|
|||||||
// create
|
// create
|
||||||
|
|
||||||
const {
|
const {
|
||||||
id: createdId,
|
id: createdId
|
||||||
name: createdName,
|
// name: createdName,
|
||||||
authMethod
|
// authMethod
|
||||||
} = await createMutateAsync({
|
} = await createMutateAsync({
|
||||||
name,
|
name,
|
||||||
role: role || undefined,
|
role: role || undefined,
|
||||||
organizationId: orgId
|
organizationId: orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
handlePopUpToggle("identity", false);
|
await addMutateAsync({
|
||||||
handlePopUpOpen("identityAuthMethod", {
|
organizationId: orgId,
|
||||||
identityId: createdId,
|
identityId: createdId,
|
||||||
name: createdName,
|
clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||||
authMethod
|
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||||
|
accessTokenTTL: 2592000,
|
||||||
|
accessTokenMaxTTL: 2592000,
|
||||||
|
accessTokenNumUsesLimit: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
handlePopUpToggle("identity", false);
|
||||||
|
// handlePopUpOpen("identityAuthMethod", {
|
||||||
|
// identityId: createdId,
|
||||||
|
// name: createdName,
|
||||||
|
// authMethod
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
|
|||||||
+22
-107
@@ -1,121 +1,36 @@
|
|||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import Link from "next/link";
|
||||||
import {
|
|
||||||
decryptAssymmetric,
|
import { UpgradeProjectAlert } from "@app/components/v2/UpgradeProjectAlert";
|
||||||
encryptAssymmetric
|
import { useWorkspace } from "@app/context";
|
||||||
} from "@app/components/utilities/cryptography/crypto";
|
import { useGetWorkspaceBot } from "@app/hooks/api";
|
||||||
import { Alert, AlertDescription, Checkbox } from "@app/components/v2";
|
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
|
||||||
import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
|
|
||||||
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
export const E2EESection = () => {
|
export const E2EESection = () => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { data: bot } = useGetWorkspaceBot(currentWorkspace?.id ?? "");
|
const { data: bot } = useGetWorkspaceBot(currentWorkspace?.id ?? "");
|
||||||
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
|
|
||||||
const { data: wsKey } = useGetUserWsKey(currentWorkspace?.id ?? "");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activate bot for project by performing the following steps:
|
|
||||||
* 1. Get the (encrypted) project key
|
|
||||||
* 2. Decrypt project key with user's private key
|
|
||||||
* 3. Encrypt project key with bot's public key
|
|
||||||
* 4. Send encrypted project key to backend and set bot status to active
|
|
||||||
*/
|
|
||||||
|
|
||||||
const toggleBotActivate = async () => {
|
|
||||||
let botKey;
|
|
||||||
try {
|
|
||||||
if (!currentWorkspace?.id) return;
|
|
||||||
|
|
||||||
if (bot && wsKey) {
|
|
||||||
// case: there is a bot
|
|
||||||
|
|
||||||
if (!bot.isActive) {
|
|
||||||
// bot is not active -> activate bot
|
|
||||||
|
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
|
||||||
|
|
||||||
if (!PRIVATE_KEY) {
|
|
||||||
throw new Error("Private Key missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
const WORKSPACE_KEY = decryptAssymmetric({
|
|
||||||
ciphertext: wsKey.encryptedKey,
|
|
||||||
nonce: wsKey.nonce,
|
|
||||||
publicKey: wsKey.sender.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
const { ciphertext, nonce } = encryptAssymmetric({
|
|
||||||
plaintext: WORKSPACE_KEY,
|
|
||||||
publicKey: bot.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
botKey = {
|
|
||||||
encryptedKey: ciphertext,
|
|
||||||
nonce
|
|
||||||
};
|
|
||||||
|
|
||||||
await updateBotActiveStatus({
|
|
||||||
workspaceId: currentWorkspace.id,
|
|
||||||
botKey,
|
|
||||||
isActive: true,
|
|
||||||
botId: bot.id
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// bot is active -> deactivate bot
|
|
||||||
await updateBotActiveStatus({
|
|
||||||
isActive: false,
|
|
||||||
botId: bot.id,
|
|
||||||
workspaceId: currentWorkspace.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!currentWorkspace) return null;
|
if (!currentWorkspace) return null;
|
||||||
|
|
||||||
return bot && currentWorkspace.version === ProjectVersion.V1 ? (
|
return bot && currentWorkspace.version === ProjectVersion.V1 ? (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<p className="mb-3 text-xl font-semibold">End-to-End Encryption</p>
|
<div className="flex w-full items-center justify-between">
|
||||||
<p className="mb-8 text-gray-400">
|
<p className="text-xl font-semibold">End-to-End Encryption</p>
|
||||||
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to
|
<UpgradeProjectAlert transparent project={currentWorkspace} />
|
||||||
cloud providers as well as HTTP calls to get secrets back raw but enables the server to
|
</div>
|
||||||
read/decrypt your secret values.
|
|
||||||
|
<p className="mt-5 max-w-2xl text-sm text-gray-400">
|
||||||
|
We are updating our encryption logic to make sure that Infisical can be the most versatile
|
||||||
|
secret management platform. <br />
|
||||||
|
<br />
|
||||||
|
Upgrading the project version is required to continue receiving the latest improvements and
|
||||||
|
patches.
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-8 text-gray-400">
|
|
||||||
Note that, even with E2EE disabled, your secrets are always encrypted at rest.
|
<Link href="https://infisical.com/docs/documentation/platform/project-upgrade">
|
||||||
</p>
|
<a target="_blank" className="text-sm text-primary-400">
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
|
Learn more about project upgrades
|
||||||
{(isAllowed) => (
|
</a>
|
||||||
<div className="flex w-full flex-col gap-y-3">
|
</Link>
|
||||||
<div className="w-max">
|
|
||||||
<Checkbox
|
|
||||||
className="data-[state=checked]:bg-primary"
|
|
||||||
id="end-to-end-encryption"
|
|
||||||
isChecked={!bot.isActive}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
onCheckedChange={async () => {
|
|
||||||
await toggleBotActivate();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
End-to-end encryption enabled
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Alert variant="warning">
|
|
||||||
<AlertDescription>
|
|
||||||
Enabling End-to-end encryption disables all the integrations
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div />
|
<div />
|
||||||
|
|||||||
+1
-1
@@ -104,7 +104,7 @@ export const AddEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpToggle
|
|||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button colorSchema="secondary" variant="plain">
|
<Button onClick={() => handlePopUpClose("createEnv")} colorSchema="secondary" variant="plain">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -108,7 +108,7 @@ export const UpdateEnvironmentModal = ({ popUp, handlePopUpClose, handlePopUpTog
|
|||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button colorSchema="secondary" variant="plain">
|
<Button onClick={() => handlePopUpClose("updateEnv")} colorSchema="secondary" variant="plain">
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ type: application
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
# to the chart and its templates, including the app version.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: 1.0.6
|
version: 1.0.7
|
||||||
|
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
appVersion: "1.0.0"
|
appVersion: "1.0.1"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: ingress-nginx
|
- name: ingress-nginx
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ spec:
|
|||||||
{{- if $infisicalValues.autoDatabaseSchemaMigration }}
|
{{- if $infisicalValues.autoDatabaseSchemaMigration }}
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: "migration-init"
|
- name: "migration-init"
|
||||||
image: "groundnuty/k8s-wait-for:1.3"
|
image: "ghcr.io/groundnuty/k8s-wait-for:no-root-v2.0"
|
||||||
imagePullPolicy: {{ $infisicalValues.image.pullPolicy }}
|
imagePullPolicy: {{ $infisicalValues.image.pullPolicy }}
|
||||||
args:
|
args:
|
||||||
- "job"
|
- "job"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ infisical:
|
|||||||
|
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 350Mi
|
memory: 600Mi
|
||||||
requests:
|
requests:
|
||||||
cpu: 350m
|
cpu: 350m
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ type: application
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
# to the chart and its templates, including the app version.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
version: v0.4.0
|
version: v0.5.0
|
||||||
# This is the version number of the application being deployed. This version number should be
|
# This is the version number of the application being deployed. This version number should be
|
||||||
# incremented each time you make changes to the application. Versions are not expected to
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
appVersion: "v0.4.0"
|
appVersion: "v0.5.0"
|
||||||
|
|||||||
@@ -90,6 +90,38 @@ spec:
|
|||||||
- secretsScope
|
- secretsScope
|
||||||
- serviceTokenSecretReference
|
- serviceTokenSecretReference
|
||||||
type: object
|
type: object
|
||||||
|
universalAuth:
|
||||||
|
properties:
|
||||||
|
credentialsRef:
|
||||||
|
properties:
|
||||||
|
secretName:
|
||||||
|
description: The name of the Kubernetes Secret
|
||||||
|
type: string
|
||||||
|
secretNamespace:
|
||||||
|
description: The name space where the Kubernetes Secret
|
||||||
|
is located
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- secretName
|
||||||
|
- secretNamespace
|
||||||
|
type: object
|
||||||
|
secretsScope:
|
||||||
|
properties:
|
||||||
|
envSlug:
|
||||||
|
type: string
|
||||||
|
projectSlug:
|
||||||
|
type: string
|
||||||
|
secretsPath:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- envSlug
|
||||||
|
- projectSlug
|
||||||
|
- secretsPath
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- credentialsRef
|
||||||
|
- secretsScope
|
||||||
|
type: object
|
||||||
type: object
|
type: object
|
||||||
hostAPI:
|
hostAPI:
|
||||||
description: Infisical host to pull secrets from
|
description: Infisical host to pull secrets from
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ controllerManager:
|
|||||||
- ALL
|
- ALL
|
||||||
image:
|
image:
|
||||||
repository: infisical/kubernetes-operator
|
repository: infisical/kubernetes-operator
|
||||||
tag: v0.4.0 # fixed to prevent accidental upgrade
|
tag: v0.5.0 # fixed to prevent accidental upgrade
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: 500m
|
cpu: 500m
|
||||||
|
|||||||
@@ -9,12 +9,20 @@ type Authentication struct {
|
|||||||
ServiceAccount ServiceAccountDetails `json:"serviceAccount"`
|
ServiceAccount ServiceAccountDetails `json:"serviceAccount"`
|
||||||
// +kubebuilder:validation:Optional
|
// +kubebuilder:validation:Optional
|
||||||
ServiceToken ServiceTokenDetails `json:"serviceToken"`
|
ServiceToken ServiceTokenDetails `json:"serviceToken"`
|
||||||
|
// +kubebuilder:validation:Optional
|
||||||
|
UniversalAuth UniversalAuthDetails `json:"universalAuth"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UniversalAuthDetails struct {
|
||||||
|
// +kubebuilder:validation:Required
|
||||||
|
CredentialsRef KubeSecretReference `json:"credentialsRef"`
|
||||||
|
// +kubebuilder:validation:Required
|
||||||
|
SecretsScope MachineIdentityScopeInWorkspace `json:"secretsScope"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServiceTokenDetails struct {
|
type ServiceTokenDetails struct {
|
||||||
// +kubebuilder:validation:Required
|
// +kubebuilder:validation:Required
|
||||||
ServiceTokenSecretReference KubeSecretReference `json:"serviceTokenSecretReference"`
|
ServiceTokenSecretReference KubeSecretReference `json:"serviceTokenSecretReference"`
|
||||||
|
|
||||||
// +kubebuilder:validation:Required
|
// +kubebuilder:validation:Required
|
||||||
SecretsScope SecretScopeInWorkspace `json:"secretsScope"`
|
SecretsScope SecretScopeInWorkspace `json:"secretsScope"`
|
||||||
}
|
}
|
||||||
@@ -28,11 +36,19 @@ type ServiceAccountDetails struct {
|
|||||||
type SecretScopeInWorkspace struct {
|
type SecretScopeInWorkspace struct {
|
||||||
// +kubebuilder:validation:Required
|
// +kubebuilder:validation:Required
|
||||||
SecretsPath string `json:"secretsPath"`
|
SecretsPath string `json:"secretsPath"`
|
||||||
|
|
||||||
// +kubebuilder:validation:Required
|
// +kubebuilder:validation:Required
|
||||||
EnvSlug string `json:"envSlug"`
|
EnvSlug string `json:"envSlug"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MachineIdentityScopeInWorkspace struct {
|
||||||
|
// +kubebuilder:validation:Required
|
||||||
|
SecretsPath string `json:"secretsPath"`
|
||||||
|
// +kubebuilder:validation:Required
|
||||||
|
EnvSlug string `json:"envSlug"`
|
||||||
|
// +kubebuilder:validation:Required
|
||||||
|
ProjectSlug string `json:"projectSlug"`
|
||||||
|
}
|
||||||
|
|
||||||
type KubeSecretReference struct {
|
type KubeSecretReference struct {
|
||||||
// The name of the Kubernetes Secret
|
// The name of the Kubernetes Secret
|
||||||
// +kubebuilder:validation:Required
|
// +kubebuilder:validation:Required
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ func (in *Authentication) DeepCopyInto(out *Authentication) {
|
|||||||
*out = *in
|
*out = *in
|
||||||
out.ServiceAccount = in.ServiceAccount
|
out.ServiceAccount = in.ServiceAccount
|
||||||
out.ServiceToken = in.ServiceToken
|
out.ServiceToken = in.ServiceToken
|
||||||
|
out.UniversalAuth = in.UniversalAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authentication.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authentication.
|
||||||
@@ -157,6 +158,21 @@ func (in *KubeSecretReference) DeepCopy() *KubeSecretReference {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *MachineIdentityScopeInWorkspace) DeepCopyInto(out *MachineIdentityScopeInWorkspace) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineIdentityScopeInWorkspace.
|
||||||
|
func (in *MachineIdentityScopeInWorkspace) DeepCopy() *MachineIdentityScopeInWorkspace {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(MachineIdentityScopeInWorkspace)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *MangedKubeSecretConfig) DeepCopyInto(out *MangedKubeSecretConfig) {
|
func (in *MangedKubeSecretConfig) DeepCopyInto(out *MangedKubeSecretConfig) {
|
||||||
*out = *in
|
*out = *in
|
||||||
@@ -219,3 +235,20 @@ func (in *ServiceTokenDetails) DeepCopy() *ServiceTokenDetails {
|
|||||||
in.DeepCopyInto(out)
|
in.DeepCopyInto(out)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *UniversalAuthDetails) DeepCopyInto(out *UniversalAuthDetails) {
|
||||||
|
*out = *in
|
||||||
|
out.CredentialsRef = in.CredentialsRef
|
||||||
|
out.SecretsScope = in.SecretsScope
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UniversalAuthDetails.
|
||||||
|
func (in *UniversalAuthDetails) DeepCopy() *UniversalAuthDetails {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(UniversalAuthDetails)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -90,6 +90,38 @@ spec:
|
|||||||
- secretsScope
|
- secretsScope
|
||||||
- serviceTokenSecretReference
|
- serviceTokenSecretReference
|
||||||
type: object
|
type: object
|
||||||
|
universalAuth:
|
||||||
|
properties:
|
||||||
|
credentialsRef:
|
||||||
|
properties:
|
||||||
|
secretName:
|
||||||
|
description: The name of the Kubernetes Secret
|
||||||
|
type: string
|
||||||
|
secretNamespace:
|
||||||
|
description: The name space where the Kubernetes Secret
|
||||||
|
is located
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- secretName
|
||||||
|
- secretNamespace
|
||||||
|
type: object
|
||||||
|
secretsScope:
|
||||||
|
properties:
|
||||||
|
envSlug:
|
||||||
|
type: string
|
||||||
|
projectSlug:
|
||||||
|
type: string
|
||||||
|
secretsPath:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- envSlug
|
||||||
|
- projectSlug
|
||||||
|
- secretsPath
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- credentialsRef
|
||||||
|
- secretsScope
|
||||||
|
type: object
|
||||||
type: object
|
type: object
|
||||||
hostAPI:
|
hostAPI:
|
||||||
description: Infisical host to pull secrets from
|
description: Infisical host to pull secrets from
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: universal-auth-credentials
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
clientId: <machine-identity-client-id>
|
||||||
|
clientSecret: <machine-identity-client-secret>
|
||||||
@@ -1,35 +1,42 @@
|
|||||||
apiVersion: secrets.infisical.com/v1alpha1
|
apiVersion: secrets.infisical.com/v1alpha1
|
||||||
kind: InfisicalSecret
|
kind: InfisicalSecret
|
||||||
metadata:
|
metadata:
|
||||||
name: infisicalsecret-sample
|
name: infisicalsecret-sample
|
||||||
labels:
|
labels:
|
||||||
label-to-be-passed-to-managed-secret: sample-value
|
label-to-be-passed-to-managed-secret: sample-value
|
||||||
annotations:
|
annotations:
|
||||||
reflector.v1.k8s.emberstack.com/reflection-allowed: 'true'
|
example.com/annotation-to-be-passed-to-managed-secret: "sample-value"
|
||||||
spec:
|
spec:
|
||||||
hostAPI: http://localhost:8888/api
|
hostAPI: https://app.infisical.com/api
|
||||||
resyncInterval: 10
|
resyncInterval: 10
|
||||||
authentication:
|
authentication:
|
||||||
serviceAccount:
|
# Make sure to only have 1 authentication method defined, serviceToken/universalAuth.
|
||||||
serviceAccountSecretReference:
|
# If you have multiple authentication methods defined, it may cause issues.
|
||||||
secretName: service-account
|
serviceToken:
|
||||||
secretNamespace: default
|
serviceTokenSecretReference:
|
||||||
projectId: "6439ec224cfbf7ea2a95b651"
|
secretName: service-token
|
||||||
environmentName: "dev"
|
secretNamespace: default
|
||||||
serviceToken:
|
secretsScope:
|
||||||
serviceTokenSecretReference:
|
envSlug: <env-slug>
|
||||||
secretName: service-token
|
secretsPath: <secrets-path> # Root is "/"
|
||||||
secretNamespace: default
|
|
||||||
secretsScope:
|
|
||||||
envSlug: dev
|
|
||||||
secretsPath: "/"
|
|
||||||
managedSecretReference:
|
|
||||||
secretName: managed-secret
|
|
||||||
secretNamespace: default
|
|
||||||
creationPolicy: "Orphan" ## Owner | Orphan
|
|
||||||
# secretType: kubernetes.io/dockerconfigjson
|
|
||||||
|
|
||||||
# # To be depreciated soon
|
universalAuth:
|
||||||
# tokenSecretReference:
|
secretsScope:
|
||||||
# secretName: service-token
|
projectSlug: <project-slug>
|
||||||
# secretNamespace: default
|
envSlug: <env-slug> # "dev", "staging", "prod", etc..
|
||||||
|
secretsPath: "<secrets-path>" # Root is "/"
|
||||||
|
|
||||||
|
credentialsRef:
|
||||||
|
secretName: universal-auth-credentials
|
||||||
|
secretNamespace: default
|
||||||
|
|
||||||
|
managedSecretReference:
|
||||||
|
secretName: managed-secret
|
||||||
|
secretNamespace: default
|
||||||
|
creationPolicy: "Orphan" ## Owner | Orphan
|
||||||
|
# secretType: kubernetes.io/dockerconfigjson
|
||||||
|
|
||||||
|
# # To be depreciated soon
|
||||||
|
# tokenSecretReference:
|
||||||
|
# secretName: service-token
|
||||||
|
# secretNamespace: default
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: service-account
|
|
||||||
type: Opaque
|
|
||||||
stringData:
|
|
||||||
serviceAccountAccessKey: <>
|
|
||||||
serviceAccountPrivateKey: <>
|
|
||||||
serviceAccountPublicKey: <>
|
|
||||||
@@ -53,11 +53,11 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
|||||||
|
|
||||||
if infisicalSecretCR.Spec.ResyncInterval != 0 {
|
if infisicalSecretCR.Spec.ResyncInterval != 0 {
|
||||||
requeueTime = time.Second * time.Duration(infisicalSecretCR.Spec.ResyncInterval)
|
requeueTime = time.Second * time.Duration(infisicalSecretCR.Spec.ResyncInterval)
|
||||||
fmt.Println("Manual re-sync interval set", "requeueAfter", requeueTime)
|
fmt.Printf("\nManual re-sync interval set. Interval: %v\n", requeueTime)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\nRe-sync interval set. Interval: %v\n", requeueTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Requeue duration set", "requeueAfter", requeueTime)
|
|
||||||
|
|
||||||
// Check if the resource is already marked for deletion
|
// Check if the resource is already marked for deletion
|
||||||
if infisicalSecretCR.GetDeletionTimestamp() != nil {
|
if infisicalSecretCR.GetDeletionTimestamp() != nil {
|
||||||
return ctrl.Result{
|
return ctrl.Result{
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||||
"github.com/Infisical/infisical/k8-operator/packages/api"
|
|
||||||
"github.com/Infisical/infisical/k8-operator/packages/model"
|
"github.com/Infisical/infisical/k8-operator/packages/model"
|
||||||
"github.com/Infisical/infisical/k8-operator/packages/util"
|
"github.com/Infisical/infisical/k8-operator/packages/util"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
@@ -20,12 +19,29 @@ const SERVICE_ACCOUNT_ACCESS_KEY = "serviceAccountAccessKey"
|
|||||||
const SERVICE_ACCOUNT_PUBLIC_KEY = "serviceAccountPublicKey"
|
const SERVICE_ACCOUNT_PUBLIC_KEY = "serviceAccountPublicKey"
|
||||||
const SERVICE_ACCOUNT_PRIVATE_KEY = "serviceAccountPrivateKey"
|
const SERVICE_ACCOUNT_PRIVATE_KEY = "serviceAccountPrivateKey"
|
||||||
|
|
||||||
|
const INFISICAL_MACHINE_IDENTITY_CLIENT_ID = "clientId"
|
||||||
|
const INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET = "clientSecret"
|
||||||
|
|
||||||
const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken"
|
const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken"
|
||||||
const SECRET_VERSION_ANNOTATION = "secrets.infisical.com/version" // used to set the version of secrets via Etag
|
const SECRET_VERSION_ANNOTATION = "secrets.infisical.com/version" // used to set the version of secrets via Etag
|
||||||
const OPERATOR_SETTINGS_CONFIGMAP_NAME = "infisical-config"
|
const OPERATOR_SETTINGS_CONFIGMAP_NAME = "infisical-config"
|
||||||
const OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE = "infisical-operator-system"
|
const OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE = "infisical-operator-system"
|
||||||
const INFISICAL_DOMAIN = "https://app.infisical.com/api"
|
const INFISICAL_DOMAIN = "https://app.infisical.com/api"
|
||||||
|
|
||||||
|
type AuthStrategyType string
|
||||||
|
|
||||||
|
var AuthStrategy = struct {
|
||||||
|
SERVICE_TOKEN AuthStrategyType
|
||||||
|
SERVICE_ACCOUNT AuthStrategyType
|
||||||
|
UNIVERSAL_MACHINE_IDENTITY AuthStrategyType
|
||||||
|
}{
|
||||||
|
SERVICE_TOKEN: "SERVICE_TOKEN",
|
||||||
|
SERVICE_ACCOUNT: "SERVICE_ACCOUNT",
|
||||||
|
UNIVERSAL_MACHINE_IDENTITY: "UNIVERSAL_MACHINE_IDENTITY",
|
||||||
|
}
|
||||||
|
|
||||||
|
var machineIdentityTokenInstance *util.MachineIdentityToken
|
||||||
|
|
||||||
func (r *InfisicalSecretReconciler) GetInfisicalConfigMap(ctx context.Context) (configMap map[string]string, errToReturn error) {
|
func (r *InfisicalSecretReconciler) GetInfisicalConfigMap(ctx context.Context) (configMap map[string]string, errToReturn error) {
|
||||||
// default key values
|
// default key values
|
||||||
defaultConfigMapData := make(map[string]string)
|
defaultConfigMapData := make(map[string]string)
|
||||||
@@ -100,6 +116,28 @@ func (r *InfisicalSecretReconciler) GetInfisicalTokenFromKubeSecret(ctx context.
|
|||||||
return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil
|
return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *InfisicalSecretReconciler) GetInfisicalUniversalAuthFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (machineIdentityDetails model.MachineIdentityDetails, err error) {
|
||||||
|
|
||||||
|
universalAuthCredsFromKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
|
||||||
|
Namespace: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.SecretNamespace,
|
||||||
|
Name: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.SecretName,
|
||||||
|
})
|
||||||
|
|
||||||
|
if errors.IsNotFound(err) {
|
||||||
|
return model.MachineIdentityDetails{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return model.MachineIdentityDetails{}, fmt.Errorf("something went wrong when fetching your machine identity credentials [err=%s]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientIdFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_ID]
|
||||||
|
clientSecretFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET]
|
||||||
|
|
||||||
|
return model.MachineIdentityDetails{ClientId: string(clientIdFromSecret), ClientSecret: string(clientSecretFromSecret)}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Fetches service account credentials from a Kubernetes secret specified in the infisicalSecret object, extracts the access key, public key, and private key from the secret, and returns them as a ServiceAccountCredentials object.
|
// Fetches service account credentials from a Kubernetes secret specified in the infisicalSecret object, extracts the access key, public key, and private key from the secret, and returns them as a ServiceAccountCredentials object.
|
||||||
// If any keys are missing or an error occurs, returns an empty object or an error object, respectively.
|
// If any keys are missing or an error occurs, returns an empty object or an error object, respectively.
|
||||||
func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (serviceAccountDetails model.ServiceAccountDetails, err error) {
|
func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (serviceAccountDetails model.ServiceAccountDetails, err error) {
|
||||||
@@ -127,7 +165,7 @@ func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKub
|
|||||||
return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil
|
return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) error {
|
func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
|
||||||
plainProcessedSecrets := make(map[string][]byte)
|
plainProcessedSecrets := make(map[string][]byte)
|
||||||
secretType := infisicalSecret.Spec.ManagedSecretReference.SecretType
|
secretType := infisicalSecret.Spec.ManagedSecretReference.SecretType
|
||||||
|
|
||||||
@@ -156,7 +194,7 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
annotations[SECRET_VERSION_ANNOTATION] = encryptedSecretsResponse.ETag
|
annotations[SECRET_VERSION_ANNOTATION] = ETag
|
||||||
|
|
||||||
// create a new secret as specified by the managed secret spec of CRD
|
// create a new secret as specified by the managed secret spec of CRD
|
||||||
newKubeSecretInstance := &corev1.Secret{
|
newKubeSecretInstance := &corev1.Secret{
|
||||||
@@ -187,16 +225,15 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) error {
|
func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
|
||||||
plainProcessedSecrets := make(map[string][]byte)
|
plainProcessedSecrets := make(map[string][]byte)
|
||||||
for _, secret := range secretsFromAPI {
|
for _, secret := range secretsFromAPI {
|
||||||
plainProcessedSecrets[secret.Key] = []byte(secret.Value)
|
plainProcessedSecrets[secret.Key] = []byte(secret.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
managedKubeSecret.Data = plainProcessedSecrets
|
managedKubeSecret.Data = plainProcessedSecrets
|
||||||
managedKubeSecret.ObjectMeta.Annotations = map[string]string{
|
managedKubeSecret.ObjectMeta.Annotations = map[string]string{}
|
||||||
SECRET_VERSION_ANNOTATION: encryptedSecretsResponse.ETag,
|
managedKubeSecret.ObjectMeta.Annotations[SECRET_VERSION_ANNOTATION] = ETag
|
||||||
}
|
|
||||||
|
|
||||||
err := r.Client.Update(ctx, &managedKubeSecret)
|
err := r.Client.Update(ctx, &managedKubeSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -213,11 +250,28 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
|
|||||||
return fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
|
return fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var authStrategy AuthStrategyType
|
||||||
|
|
||||||
serviceAccountCreds, err := r.GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
|
serviceAccountCreds, err := r.GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
|
return fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
infisicalMachineIdentityCreds, err := r.GetInfisicalUniversalAuthFromKubeSecret(ctx, infisicalSecret)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ReconcileInfisicalSecret: unable to get machine identity creds from kube secret [err=%s]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" {
|
||||||
|
authStrategy = AuthStrategy.SERVICE_ACCOUNT
|
||||||
|
} else if infisicalToken != "" {
|
||||||
|
authStrategy = AuthStrategy.SERVICE_TOKEN
|
||||||
|
} else if infisicalMachineIdentityCreds.ClientId != "" && infisicalMachineIdentityCreds.ClientSecret != "" {
|
||||||
|
authStrategy = AuthStrategy.UNIVERSAL_MACHINE_IDENTITY
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("no authentication method provided. You must provide either a valid service token or a service account details to fetch secrets")
|
||||||
|
}
|
||||||
|
|
||||||
r.SetInfisicalTokenLoadCondition(ctx, &infisicalSecret, err)
|
r.SetInfisicalTokenLoadCondition(ctx, &infisicalSecret, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to load Infisical Token from the specified Kubernetes secret with error [%w]", err)
|
return fmt.Errorf("unable to load Infisical Token from the specified Kubernetes secret with error [%w]", err)
|
||||||
@@ -239,41 +293,60 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
|
|||||||
secretVersionBasedOnETag = managedKubeSecret.Annotations[SECRET_VERSION_ANNOTATION]
|
secretVersionBasedOnETag = managedKubeSecret.Annotations[SECRET_VERSION_ANNOTATION]
|
||||||
}
|
}
|
||||||
|
|
||||||
var plainTextSecretsFromApi []model.SingleEnvironmentVariable
|
if authStrategy == AuthStrategy.UNIVERSAL_MACHINE_IDENTITY && machineIdentityTokenInstance == nil {
|
||||||
var fullEncryptedSecretsResponse api.GetEncryptedSecretsV3Response
|
// Create new machine identity token instance
|
||||||
|
machineIdentityTokenInstance = util.NewMachineIdentityToken(infisicalMachineIdentityCreds.ClientId, infisicalMachineIdentityCreds.ClientSecret)
|
||||||
|
}
|
||||||
|
|
||||||
if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" {
|
var plainTextSecretsFromApi []model.SingleEnvironmentVariable
|
||||||
plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceAccount(serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag)
|
var updateDetails model.RequestUpdateUpdateDetails
|
||||||
|
|
||||||
|
if authStrategy == AuthStrategy.SERVICE_ACCOUNT { // Service Account
|
||||||
|
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceAccount(serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service account")
|
fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service account")
|
||||||
|
|
||||||
} else if infisicalToken != "" {
|
} else if authStrategy == AuthStrategy.SERVICE_TOKEN { // Service Tokens (deprecated)
|
||||||
envSlug := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.EnvSlug
|
envSlug := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.EnvSlug
|
||||||
secretsPath := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.SecretsPath
|
secretsPath := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.SecretsPath
|
||||||
|
|
||||||
plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath)
|
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service token")
|
fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service token")
|
||||||
|
} else if authStrategy == AuthStrategy.UNIVERSAL_MACHINE_IDENTITY { // Machine Identity
|
||||||
|
|
||||||
|
accessToken, err := machineIdentityTokenInstance.GetToken()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s", "Waiting for access token to become available")
|
||||||
|
}
|
||||||
|
scope := infisicalSecret.Spec.Authentication.UniversalAuth.SecretsScope
|
||||||
|
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaUniversalAuth(accessToken, secretVersionBasedOnETag, scope)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
||||||
|
}
|
||||||
|
fmt.Println("ReconcileInfisicalSecret: Fetched secrets via universal auth")
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return fmt.Errorf("no authentication method provided. You must provide either a valid service token or a service account details to fetch secrets")
|
return fmt.Errorf("no authentication method provided. You must provide either a valid service token or a service account details to fetch secrets")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !fullEncryptedSecretsResponse.Modified {
|
if !updateDetails.Modified {
|
||||||
fmt.Println("No secrets modified so reconcile not needed", "Etag:", fullEncryptedSecretsResponse.ETag, "Modified:", fullEncryptedSecretsResponse.Modified)
|
fmt.Println("No secrets modified so reconcile not needed")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if managedKubeSecret == nil {
|
if managedKubeSecret == nil {
|
||||||
return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse)
|
return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, plainTextSecretsFromApi, updateDetails.ETag)
|
||||||
} else {
|
} else {
|
||||||
return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse)
|
return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, plainTextSecretsFromApi, updateDetails.ETag)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
|
|||||||
R().
|
R().
|
||||||
SetResult(&secretsResponse).
|
SetResult(&secretsResponse).
|
||||||
SetHeader("User-Agent", USER_AGENT_NAME).
|
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||||
SetHeader("If-None-Match", request.ETag).
|
|
||||||
SetQueryParam("environment", request.Environment).
|
SetQueryParam("environment", request.Environment).
|
||||||
SetQueryParam("include_imports", "true"). // TODO needs to be set as a option
|
SetQueryParam("include_imports", "true"). // TODO needs to be set as a option
|
||||||
SetQueryParam("workspaceId", request.WorkspaceId)
|
SetQueryParam("workspaceId", request.WorkspaceId)
|
||||||
@@ -77,13 +76,10 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
|
|||||||
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
|
return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.Header().Get("etag") == request.ETag {
|
responseETag := response.Header().Get("etag")
|
||||||
secretsResponse.Modified = false
|
|
||||||
} else {
|
|
||||||
secretsResponse.Modified = true
|
|
||||||
}
|
|
||||||
|
|
||||||
secretsResponse.ETag = response.Header().Get("etag")
|
secretsResponse.Modified = request.ETag != responseETag
|
||||||
|
secretsResponse.ETag = responseETag
|
||||||
|
|
||||||
return secretsResponse, nil
|
return secretsResponse, nil
|
||||||
}
|
}
|
||||||
@@ -107,6 +103,76 @@ func CallGetServiceTokenAccountDetailsV2(httpClient *resty.Client) (ServiceAccou
|
|||||||
return serviceAccountDetailsResponse, nil
|
return serviceAccountDetailsResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CallUniversalMachineIdentityLogin(request MachineIdentityUniversalAuthLoginRequest) (MachineIdentityDetailsResponse, error) {
|
||||||
|
var machineIdentityDetailsResponse MachineIdentityDetailsResponse
|
||||||
|
|
||||||
|
response, err := resty.New().
|
||||||
|
R().
|
||||||
|
SetResult(&machineIdentityDetailsResponse).
|
||||||
|
SetBody(request).
|
||||||
|
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||||
|
Post(fmt.Sprintf("%v/v1/auth/universal-auth/login", API_HOST_URL))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalMachineIdentityLogin: Unable to complete api request [err=%s]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.IsError() {
|
||||||
|
return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalMachineIdentityLogin: Unsuccessful response: [response=%s]", response)
|
||||||
|
}
|
||||||
|
|
||||||
|
return machineIdentityDetailsResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CallUniversalMachineIdentityRefreshAccessToken(request MachineIdentityUniversalAuthRefreshRequest) (MachineIdentityDetailsResponse, error) {
|
||||||
|
var universalAuthRefreshResponse MachineIdentityDetailsResponse
|
||||||
|
|
||||||
|
response, err := resty.New().
|
||||||
|
R().
|
||||||
|
SetResult(&universalAuthRefreshResponse).
|
||||||
|
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||||
|
SetBody(request).
|
||||||
|
Post(fmt.Sprintf("%v/v1/auth/token/renew", API_HOST_URL))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalAuthRefreshAccessToken: Unable to complete api request [err=%s]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.IsError() {
|
||||||
|
return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalAuthRefreshAccessToken: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return universalAuthRefreshResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CallGetDecryptedSecretsV3(httpClient *resty.Client, request GetDecryptedSecretsV3Request) (GetDecryptedSecretsV3Response, error) {
|
||||||
|
var decryptedSecretsResponse GetDecryptedSecretsV3Response
|
||||||
|
|
||||||
|
response, err := httpClient.
|
||||||
|
R().
|
||||||
|
SetResult(&decryptedSecretsResponse).
|
||||||
|
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||||
|
SetQueryParam("secretPath", request.SecretPath).
|
||||||
|
SetQueryParam("workspaceSlug", request.ProjectSlug).
|
||||||
|
SetQueryParam("environment", request.Environment).
|
||||||
|
Get(fmt.Sprintf("%v/v3/secrets/raw", API_HOST_URL))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return GetDecryptedSecretsV3Response{}, fmt.Errorf("CallGetDecryptedSecretsV3: Unable to complete api request [err=%s]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.IsError() {
|
||||||
|
return GetDecryptedSecretsV3Response{}, fmt.Errorf("CallGetDecryptedSecretsV3: Unsuccessful response: [response=%s]", response)
|
||||||
|
}
|
||||||
|
|
||||||
|
responseETag := response.Header().Get("etag")
|
||||||
|
|
||||||
|
decryptedSecretsResponse.Modified = request.ETag != responseETag
|
||||||
|
decryptedSecretsResponse.ETag = responseETag
|
||||||
|
|
||||||
|
return decryptedSecretsResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
func CallGetServiceAccountWorkspacePermissionsV2(httpClient *resty.Client) (ServiceAccountWorkspacePermissions, error) {
|
func CallGetServiceAccountWorkspacePermissionsV2(httpClient *resty.Client) (ServiceAccountWorkspacePermissions, error) {
|
||||||
var serviceAccountWorkspacePermissionsResponse ServiceAccountWorkspacePermissions
|
var serviceAccountWorkspacePermissionsResponse ServiceAccountWorkspacePermissions
|
||||||
response, err := httpClient.
|
response, err := httpClient.
|
||||||
|
|||||||
@@ -65,6 +65,17 @@ type EncryptedSecretV3 struct {
|
|||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DecryptedSecretV3 struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Workspace string `json:"workspace"`
|
||||||
|
Environment string `json:"environment"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
Type string `json:"string"`
|
||||||
|
SecretKey string `json:"secretKey"`
|
||||||
|
SecretValue string `json:"secretValue"`
|
||||||
|
SecretComment string `json:"secretComment"`
|
||||||
|
}
|
||||||
|
|
||||||
type ImportedSecretV3 struct {
|
type ImportedSecretV3 struct {
|
||||||
Environment string `json:"environment"`
|
Environment string `json:"environment"`
|
||||||
FolderId string `json:"folderId"`
|
FolderId string `json:"folderId"`
|
||||||
@@ -79,6 +90,19 @@ type GetEncryptedSecretsV3Response struct {
|
|||||||
ETag string `json:"ETag,omitempty"`
|
ETag string `json:"ETag,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetDecryptedSecretsV3Response struct {
|
||||||
|
Secrets []DecryptedSecretV3 `json:"secrets"`
|
||||||
|
ETag string `json:"ETag,omitempty"`
|
||||||
|
Modified bool `json:"modified,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetDecryptedSecretsV3Request struct {
|
||||||
|
ProjectSlug string `json:"workspaceSlug"`
|
||||||
|
Environment string `json:"environment"`
|
||||||
|
SecretPath string `json:"secretPath"`
|
||||||
|
ETag string `json:"etag,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type GetServiceTokenDetailsResponse struct {
|
type GetServiceTokenDetailsResponse struct {
|
||||||
ID string `json:"_id"`
|
ID string `json:"_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -101,6 +125,13 @@ type ServiceAccountDetailsResponse struct {
|
|||||||
} `json:"serviceAccount"`
|
} `json:"serviceAccount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MachineIdentityDetailsResponse struct {
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
ExpiresIn int `json:"expiresIn"`
|
||||||
|
AccessTokenMaxTTL int `json:"accessTokenMaxTTL"`
|
||||||
|
TokenType string `json:"tokenType"`
|
||||||
|
}
|
||||||
|
|
||||||
type ServiceAccountWorkspacePermission struct {
|
type ServiceAccountWorkspacePermission struct {
|
||||||
ID string `json:"_id"`
|
ID string `json:"_id"`
|
||||||
ServiceAccount string `json:"serviceAccount"`
|
ServiceAccount string `json:"serviceAccount"`
|
||||||
@@ -128,6 +159,15 @@ type GetServiceAccountKeysRequest struct {
|
|||||||
ServiceAccountId string `json:"id"`
|
ServiceAccountId string `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MachineIdentityUniversalAuthLoginRequest struct {
|
||||||
|
ClientId string `json:"clientId"`
|
||||||
|
ClientSecret string `json:"clientSecret"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineIdentityUniversalAuthRefreshRequest struct {
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
}
|
||||||
|
|
||||||
type ServiceAccountKey struct {
|
type ServiceAccountKey struct {
|
||||||
ID string `json:"_id"`
|
ID string `json:"_id"`
|
||||||
EncryptedKey string `json:"encryptedKey"`
|
EncryptedKey string `json:"encryptedKey"`
|
||||||
|
|||||||
@@ -6,6 +6,16 @@ type ServiceAccountDetails struct {
|
|||||||
PrivateKey string
|
PrivateKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MachineIdentityDetails struct {
|
||||||
|
ClientId string
|
||||||
|
ClientSecret string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestUpdateUpdateDetails struct {
|
||||||
|
Modified bool
|
||||||
|
ETag string
|
||||||
|
}
|
||||||
|
|
||||||
type SingleEnvironmentVariable struct {
|
type SingleEnvironmentVariable struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Infisical/infisical/k8-operator/packages/api"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MachineIdentityToken struct {
|
||||||
|
accessTokenTTL time.Duration
|
||||||
|
accessTokenMaxTTL time.Duration
|
||||||
|
accessTokenFetchedTime time.Time
|
||||||
|
accessTokenRefreshedTime time.Time
|
||||||
|
|
||||||
|
mutex sync.Mutex
|
||||||
|
|
||||||
|
accessToken string
|
||||||
|
clientSecret string
|
||||||
|
clientId string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMachineIdentityToken(clientId string, clientSecret string) *MachineIdentityToken {
|
||||||
|
|
||||||
|
token := MachineIdentityToken{
|
||||||
|
clientSecret: clientSecret,
|
||||||
|
clientId: clientId,
|
||||||
|
}
|
||||||
|
|
||||||
|
go token.HandleTokenLifecycle()
|
||||||
|
|
||||||
|
return &token
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MachineIdentityToken) HandleTokenLifecycle() error {
|
||||||
|
|
||||||
|
for {
|
||||||
|
accessTokenMaxTTLExpiresInTime := t.accessTokenFetchedTime.Add(t.accessTokenMaxTTL - (5 * time.Second))
|
||||||
|
accessTokenRefreshedTime := t.accessTokenRefreshedTime
|
||||||
|
|
||||||
|
if accessTokenRefreshedTime.IsZero() {
|
||||||
|
accessTokenRefreshedTime = t.accessTokenFetchedTime
|
||||||
|
}
|
||||||
|
|
||||||
|
nextAccessTokenExpiresInTime := accessTokenRefreshedTime.Add(t.accessTokenTTL - (5 * time.Second))
|
||||||
|
|
||||||
|
if t.accessTokenFetchedTime.IsZero() && t.accessTokenRefreshedTime.IsZero() {
|
||||||
|
// case: init login to get access token
|
||||||
|
fmt.Println("\nInfisical Authentication: attempting to authenticate...")
|
||||||
|
err := t.FetchNewAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("\nInfisical Authentication: unable to authenticate universal auth because %v. Will retry in 30 seconds", err)
|
||||||
|
|
||||||
|
// wait a bit before trying again
|
||||||
|
time.Sleep((30 * time.Second))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if time.Now().After(accessTokenMaxTTLExpiresInTime) {
|
||||||
|
fmt.Printf("\nInfisical Authentication: machine identity access token has reached max ttl, attempting to re authenticate...")
|
||||||
|
err := t.FetchNewAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("\nInfisical Authentication: unable to authenticate universal auth because %v. Will retry in 30 seconds", err)
|
||||||
|
|
||||||
|
// wait a bit before trying again
|
||||||
|
time.Sleep((30 * time.Second))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := t.RefreshAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("\nInfisical Authentication: unable to refresh universal auth token because %v. Will retry in 30 seconds", err)
|
||||||
|
|
||||||
|
// wait a bit before trying again
|
||||||
|
time.Sleep((30 * time.Second))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if accessTokenRefreshedTime.IsZero() {
|
||||||
|
accessTokenRefreshedTime = t.accessTokenFetchedTime
|
||||||
|
} else {
|
||||||
|
accessTokenRefreshedTime = t.accessTokenRefreshedTime
|
||||||
|
}
|
||||||
|
|
||||||
|
nextAccessTokenExpiresInTime = accessTokenRefreshedTime.Add(t.accessTokenTTL - (5 * time.Second))
|
||||||
|
accessTokenMaxTTLExpiresInTime = t.accessTokenFetchedTime.Add(t.accessTokenMaxTTL - (5 * time.Second))
|
||||||
|
|
||||||
|
if nextAccessTokenExpiresInTime.After(accessTokenMaxTTLExpiresInTime) {
|
||||||
|
// case: Refreshed so close that the next refresh would occur beyond max ttl (this is because currently, token renew tries to add +access-token-ttl amount of time)
|
||||||
|
// example: access token ttl is 11 sec and max ttl is 30 sec. So it will start with 11 seconds, then 22 seconds but the next time you call refresh it would try to extend it to 33 but max ttl only allows 30, so the token will be valid until 30 before we need to reauth
|
||||||
|
time.Sleep(t.accessTokenTTL - nextAccessTokenExpiresInTime.Sub(accessTokenMaxTTLExpiresInTime))
|
||||||
|
} else {
|
||||||
|
time.Sleep(t.accessTokenTTL - (5 * time.Second))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MachineIdentityToken) RefreshAccessToken() error {
|
||||||
|
httpClient := resty.New()
|
||||||
|
httpClient.SetRetryCount(10000).
|
||||||
|
SetRetryMaxWaitTime(20 * time.Second).
|
||||||
|
SetRetryWaitTime(5 * time.Second)
|
||||||
|
|
||||||
|
accessToken, err := t.GetToken()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := api.CallUniversalMachineIdentityRefreshAccessToken(api.MachineIdentityUniversalAuthRefreshRequest{AccessToken: accessToken})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
accessTokenTTL := time.Duration(response.ExpiresIn * int(time.Second))
|
||||||
|
accessTokenMaxTTL := time.Duration(response.AccessTokenMaxTTL * int(time.Second))
|
||||||
|
t.accessTokenRefreshedTime = time.Now()
|
||||||
|
|
||||||
|
t.SetToken(response.AccessToken, accessTokenTTL, accessTokenMaxTTL)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches a new access token using client credentials
|
||||||
|
func (t *MachineIdentityToken) FetchNewAccessToken() error {
|
||||||
|
|
||||||
|
loginResponse, err := api.CallUniversalMachineIdentityLogin(api.MachineIdentityUniversalAuthLoginRequest{
|
||||||
|
ClientId: t.clientId,
|
||||||
|
ClientSecret: t.clientSecret,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
accessTokenTTL := time.Duration(loginResponse.ExpiresIn * int(time.Second))
|
||||||
|
accessTokenMaxTTL := time.Duration(loginResponse.AccessTokenMaxTTL * int(time.Second))
|
||||||
|
|
||||||
|
if accessTokenTTL <= time.Duration(5)*time.Second {
|
||||||
|
fmt.Println("\nInfisical Authentication: At this time, k8 operator does not support refresh of tokens with 5 seconds or less ttl. Please increase access token ttl and try again")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.accessTokenFetchedTime = time.Now()
|
||||||
|
t.SetToken(loginResponse.AccessToken, accessTokenTTL, accessTokenMaxTTL)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MachineIdentityToken) SetToken(token string, accessTokenTTL time.Duration, accessTokenMaxTTL time.Duration) {
|
||||||
|
t.mutex.Lock()
|
||||||
|
defer t.mutex.Unlock()
|
||||||
|
|
||||||
|
t.accessToken = token
|
||||||
|
t.accessTokenTTL = accessTokenTTL
|
||||||
|
t.accessTokenMaxTTL = accessTokenMaxTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *MachineIdentityToken) GetToken() (string, error) {
|
||||||
|
t.mutex.Lock()
|
||||||
|
defer t.mutex.Unlock()
|
||||||
|
|
||||||
|
if t.accessToken == "" {
|
||||||
|
return "", fmt.Errorf("no machine identity access token available")
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.accessToken, nil
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||||
"github.com/Infisical/infisical/k8-operator/packages/api"
|
"github.com/Infisical/infisical/k8-operator/packages/api"
|
||||||
"github.com/Infisical/infisical/k8-operator/packages/crypto"
|
"github.com/Infisical/infisical/k8-operator/packages/crypto"
|
||||||
"github.com/Infisical/infisical/k8-operator/packages/model"
|
"github.com/Infisical/infisical/k8-operator/packages/model"
|
||||||
@@ -50,10 +51,44 @@ func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsRe
|
|||||||
return serviceTokenDetails, nil
|
return serviceTokenDetails, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, envSlug string, secretPath string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) {
|
func GetPlainTextSecretsViaUniversalAuth(accessToken string, etag string, secretScope v1alpha1.MachineIdentityScopeInWorkspace) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) {
|
||||||
|
|
||||||
|
httpClient := resty.New()
|
||||||
|
httpClient.SetAuthScheme("Bearer")
|
||||||
|
httpClient.SetAuthToken(accessToken)
|
||||||
|
|
||||||
|
secretsResponse, err := api.CallGetDecryptedSecretsV3(httpClient, api.GetDecryptedSecretsV3Request{
|
||||||
|
ProjectSlug: secretScope.ProjectSlug,
|
||||||
|
Environment: secretScope.EnvSlug,
|
||||||
|
SecretPath: secretScope.SecretsPath,
|
||||||
|
ETag: etag,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.RequestUpdateUpdateDetails{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var secrets []model.SingleEnvironmentVariable
|
||||||
|
|
||||||
|
for _, secret := range secretsResponse.Secrets {
|
||||||
|
secrets = append(secrets, model.SingleEnvironmentVariable{
|
||||||
|
Key: secret.SecretKey,
|
||||||
|
Value: secret.SecretValue,
|
||||||
|
Type: secret.Type,
|
||||||
|
ID: secret.ID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return secrets, model.RequestUpdateUpdateDetails{
|
||||||
|
Modified: secretsResponse.Modified,
|
||||||
|
ETag: secretsResponse.ETag,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, envSlug string, secretPath string) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) {
|
||||||
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
|
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
|
||||||
if len(serviceTokenParts) < 4 {
|
if len(serviceTokenParts) < 4 {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
|
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
|
||||||
@@ -65,7 +100,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, en
|
|||||||
|
|
||||||
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
|
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
|
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
|
encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
|
||||||
@@ -76,51 +111,54 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string, en
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, err
|
return nil, model.RequestUpdateUpdateDetails{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag)
|
decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err)
|
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV)
|
plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt the required workspace key")
|
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to decrypt the required workspace key")
|
||||||
}
|
}
|
||||||
|
|
||||||
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets)
|
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
|
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
plainTextSecretsMergedWithImports, err := InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecretsResponse.ImportedSecrets)
|
plainTextSecretsMergedWithImports, err := InjectImportedSecret(plainTextWorkspaceKey, plainTextSecrets, encryptedSecretsResponse.ImportedSecrets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, err
|
return nil, model.RequestUpdateUpdateDetails{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// expand secrets that are referenced
|
// expand secrets that are referenced
|
||||||
expandedSecrets := ExpandSecrets(plainTextSecretsMergedWithImports, fullServiceToken)
|
expandedSecrets := ExpandSecrets(plainTextSecretsMergedWithImports, fullServiceToken)
|
||||||
|
|
||||||
return expandedSecrets, encryptedSecretsResponse, nil
|
return expandedSecrets, model.RequestUpdateUpdateDetails{
|
||||||
|
Modified: encryptedSecretsResponse.Modified,
|
||||||
|
ETag: encryptedSecretsResponse.ETag,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches plaintext secrets from an API endpoint using a service account.
|
// Fetches plaintext secrets from an API endpoint using a service account.
|
||||||
// The function fetches the service account details and keys, decrypts the workspace key, fetches the encrypted secrets for the specified project and environment, and decrypts the secrets using the decrypted workspace key.
|
// The function fetches the service account details and keys, decrypts the workspace key, fetches the encrypted secrets for the specified project and environment, and decrypts the secrets using the decrypted workspace key.
|
||||||
// Returns the plaintext secrets, encrypted secrets response, and any errors that occurred during the process.
|
// Returns the plaintext secrets, encrypted secrets response, and any errors that occurred during the process.
|
||||||
func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) {
|
func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string, etag string) ([]model.SingleEnvironmentVariable, model.RequestUpdateUpdateDetails, error) {
|
||||||
httpClient := resty.New()
|
httpClient := resty.New()
|
||||||
httpClient.SetAuthToken(serviceAccountCreds.AccessKey).
|
httpClient.SetAuthToken(serviceAccountCreds.AccessKey).
|
||||||
SetHeader("Accept", "application/json")
|
SetHeader("Accept", "application/json")
|
||||||
|
|
||||||
serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient)
|
serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err)
|
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceAccountKeys, err := api.CallGetServiceAccountKeysV2(httpClient, api.GetServiceAccountKeysRequest{ServiceAccountId: serviceAccountDetails.ServiceAccount.ID})
|
serviceAccountKeys, err := api.CallGetServiceAccountKeysV2(httpClient, api.GetServiceAccountKeysRequest{ServiceAccountId: serviceAccountDetails.ServiceAccount.ID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err)
|
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// find key for requested project
|
// find key for requested project
|
||||||
@@ -132,28 +170,28 @@ func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccou
|
|||||||
}
|
}
|
||||||
|
|
||||||
if workspaceServiceAccountKey.ID == "" || workspaceServiceAccountKey.EncryptedKey == "" || workspaceServiceAccountKey.Nonce == "" || serviceAccountCreds.PublicKey == "" || serviceAccountCreds.PrivateKey == "" {
|
if workspaceServiceAccountKey.ID == "" || workspaceServiceAccountKey.EncryptedKey == "" || workspaceServiceAccountKey.Nonce == "" || serviceAccountCreds.PublicKey == "" || serviceAccountCreds.PrivateKey == "" {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err)
|
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cipherText, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.EncryptedKey)
|
cipherText, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.EncryptedKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode EncryptedKey secrets because [err=%v]", err)
|
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode EncryptedKey secrets because [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
nonce, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.Nonce)
|
nonce, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.Nonce)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode nonce secrets because [err=%v]", err)
|
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode nonce secrets because [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
publickey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PublicKey)
|
publickey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PublicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PublicKey secrets because [err=%v]", err)
|
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PublicKey secrets because [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
privateKey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PrivateKey)
|
privateKey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PrivateKey)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PrivateKey secrets because [err=%v]", err)
|
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PrivateKey secrets because [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
plainTextWorkspaceKey := crypto.DecryptAsymmetric(cipherText, nonce, publickey, privateKey)
|
plainTextWorkspaceKey := crypto.DecryptAsymmetric(cipherText, nonce, publickey, privateKey)
|
||||||
@@ -165,15 +203,18 @@ func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccou
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err)
|
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets)
|
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse.Secrets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err)
|
return nil, model.RequestUpdateUpdateDetails{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return plainTextSecrets, encryptedSecretsResponse, nil
|
return plainTextSecrets, model.RequestUpdateUpdateDetails{
|
||||||
|
Modified: encryptedSecretsResponse.Modified,
|
||||||
|
ETag: encryptedSecretsResponse.ETag,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) {
|
func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user