diff --git a/.env.example b/.env.example index 8463fea92..bdb3e536d 100644 --- a/.env.example +++ b/.env.example @@ -3,9 +3,6 @@ # THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 -# Required -DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} - # JWT # Required secrets to sign JWT tokens # 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_DB=infisical +# Required +DB_CONNECTION_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} + # Redis REDIS_URL=redis://redis:6379 diff --git a/.github/workflows/build-staging-and-deploy-aws.yml b/.github/workflows/build-staging-and-deploy-aws.yml new file mode 100644 index 000000000..3454126cc --- /dev/null +++ b/.github/workflows/build-staging-and-deploy-aws.yml @@ -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 diff --git a/.github/workflows/build-staging-and-deploy.yml b/.github/workflows/build-staging-and-deploy.yml deleted file mode 100644 index 9f4a72ac9..000000000 --- a/.github/workflows/build-staging-and-deploy.yml +++ /dev/null @@ -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 diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 0c115f55e..18cedad30 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -194,6 +194,25 @@ export const FOLDERS = { } } 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 = { LIST: { 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.", 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." +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; diff --git a/backend/src/server/routes/v1/secret-tag-router.ts b/backend/src/server/routes/v1/secret-tag-router.ts index c60f2b9ba..519b257ae 100644 --- a/backend/src/server/routes/v1/secret-tag-router.ts +++ b/backend/src/server/routes/v1/secret-tag-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { SecretTagsSchema } from "@app/db/schemas"; +import { SECRET_TAGS } from "@app/lib/api-docs"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -10,7 +11,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { method: "GET", schema: { params: z.object({ - projectId: z.string().trim() + projectId: z.string().trim().describe(SECRET_TAGS.LIST.projectId) }), response: { 200: z.object({ @@ -36,12 +37,12 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { method: "POST", schema: { params: z.object({ - projectId: z.string().trim() + projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId) }), body: z.object({ - name: z.string().trim(), - slug: z.string().trim(), - color: z.string() + name: z.string().trim().describe(SECRET_TAGS.CREATE.name), + slug: z.string().trim().describe(SECRET_TAGS.CREATE.slug), + color: z.string().trim().describe(SECRET_TAGS.CREATE.color) }), response: { 200: z.object({ @@ -68,8 +69,8 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => { method: "DELETE", schema: { params: z.object({ - projectId: z.string().trim(), - tagId: z.string().trim() + projectId: z.string().trim().describe(SECRET_TAGS.DELETE.projectId), + tagId: z.string().trim().describe(SECRET_TAGS.DELETE.tagId) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 1e224aa3b..f69466328 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -10,7 +10,7 @@ import { } from "@app/db/schemas"; 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 { RAW_SECRETS } from "@app/lib/api-docs"; +import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; import { BadRequestError } from "@app/lib/errors"; import { removeTrailingSlash } from "@app/lib/fn"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; @@ -23,6 +23,124 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { secretRawSchema } from "../sanitizedSchemas"; 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({ url: "/raw", method: "GET", diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 369e005ac..4f4225344 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -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 { + if (!orgId) { + throw new BadRequestError({ message: "Organization ID is required when querying with slugs" }); + } + const projects = await db(TableName.ProjectMembership) .where(`${TableName.Project}.slug`, slug) .where(`${TableName.Project}.orgId`, orgId) diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 5eaf081dc..2970d31bd 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -1,7 +1,7 @@ import { ForbiddenError } from "@casl/ability"; 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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; @@ -284,10 +284,11 @@ export const projectServiceFactory = ({ // Get the role permission for the identity const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole( - ProjectMembershipRole.Admin, + OrgMembershipRole.Member, organization.id ); + // Identity has to be at least a member in order to create projects const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission); if (!hasPrivilege) throw new ForbiddenRequestError({ diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index 11cd522ca..504174765 100644 --- a/backend/src/services/secret/secret-dal.ts +++ b/backend/src/services/secret/secret-dal.ts @@ -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 ( folderId: string, blindIndexes: Array<{ blindIndex: string; type: SecretType }>, @@ -184,6 +205,7 @@ export const secretDALFactory = (db: TDbClient) => { bulkUpdate, deleteMany, bulkUpdateNoVersionIncrement, + getSecretTags, findByFolderId, findByBlindIndexes }; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index f47428fc7..e3b57802f 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -22,6 +22,7 @@ import { TSecretDALFactory } from "./secret-dal"; import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns"; import { TSecretQueueFactory } from "./secret-queue"; import { + TAttachSecretTagsDTO, TCreateBulkSecretDTO, TCreateSecretDTO, TCreateSecretRawDTO, @@ -47,7 +48,7 @@ type TSecretServiceFactoryDep = { secretTagDAL: TSecretTagDALFactory; secretVersionDAL: TSecretVersionDALFactory; folderDAL: Pick; - projectDAL: Pick; + projectDAL: Pick; secretBlindIndexDAL: TSecretBlindIndexDALFactory; permissionService: Pick; snapshotService: Pick; @@ -307,6 +308,7 @@ export const secretServiceFactory = ({ if ((inputSecret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" }); const { secretName, ...el } = inputSecret; + const updatedSecret = await secretDAL.transaction(async (tx) => fnSecretBulkUpdate({ folderId, @@ -442,6 +444,7 @@ export const secretServiceFactory = ({ const folderId = folder.id; const secrets = await secretDAL.findByFolderId(folderId, actorId); + if (includeImports) { const secretImports = await secretImportDAL.find({ folderId }); const allowedImports = secretImports.filter(({ importEnv, importPath }) => @@ -994,7 +997,209 @@ export const secretServiceFactory = ({ 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 { + attachTags, + detachTags, createSecret, deleteSecret, updateSecret, diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 7ad4d65d7..efd4f0f8b 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -206,6 +206,15 @@ export type TFnSecretBulkUpdate = { tx?: Knex; }; +export type TAttachSecretTagsDTO = { + projectSlug: string; + secretName: string; + tagSlugs: string[]; + environment: string; + path: string; + type: SecretType; +} & Omit; + export type TFnSecretBulkDelete = { folderId: string; projectId: string; diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 00462f2ff..38d82a0a5 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -406,14 +406,14 @@ func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request return nil } -func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request) error { +func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request, secretName string) error { var secretsResponse GetEncryptedSecretsV3Response response, err := httpClient. R(). SetResult(&secretsResponse). SetHeader("User-Agent", USER_AGENT). 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 { return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err) diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 9b113275b..b49cb1581 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -401,7 +401,6 @@ type DeleteSecretV3Request struct { } type UpdateSecretByNameV3Request struct { - SecretName string `json:"secretName"` WorkspaceID string `json:"workspaceId"` Environment string `json:"environment"` Type string `json:"type"` diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index a2945484b..cf9c89b47 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -297,7 +297,6 @@ var secretsSetCmd = &cobra.Command{ updateSecretRequest := api.UpdateSecretByNameV3Request{ WorkspaceID: workspaceFile.WorkspaceId, Environment: environmentName, - SecretName: secret.PlainTextKey, SecretValueCiphertext: secret.SecretValueCiphertext, SecretValueIV: secret.SecretValueIV, SecretValueTag: secret.SecretValueTag, @@ -305,7 +304,7 @@ var secretsSetCmd = &cobra.Command{ SecretPath: secretsPath, } - err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest) + err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest, secret.PlainTextKey) if err != nil { util.HandleError(err, "Unable to process secret update request") return diff --git a/docs/api-reference/endpoints/secret-tags/create.mdx b/docs/api-reference/endpoints/secret-tags/create.mdx new file mode 100644 index 000000000..82d0eed17 --- /dev/null +++ b/docs/api-reference/endpoints/secret-tags/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/workspace/{projectId}/tags" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-tags/delete.mdx b/docs/api-reference/endpoints/secret-tags/delete.mdx new file mode 100644 index 000000000..cc98f03c2 --- /dev/null +++ b/docs/api-reference/endpoints/secret-tags/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/workspace/{projectId}/tags/{tagId}" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/secret-tags/list.mdx b/docs/api-reference/endpoints/secret-tags/list.mdx new file mode 100644 index 000000000..c4a940f77 --- /dev/null +++ b/docs/api-reference/endpoints/secret-tags/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/workspace/{projectId}/tags" +--- \ No newline at end of file diff --git a/docs/api-reference/endpoints/secrets/attach-tags.mdx b/docs/api-reference/endpoints/secrets/attach-tags.mdx new file mode 100644 index 000000000..8dd0e6081 --- /dev/null +++ b/docs/api-reference/endpoints/secrets/attach-tags.mdx @@ -0,0 +1,4 @@ +--- +title: "Attach tags" +openapi: "POST /api/v3/secrets/tags/{secretName}" +--- diff --git a/docs/api-reference/endpoints/secrets/detach-tags.mdx b/docs/api-reference/endpoints/secrets/detach-tags.mdx new file mode 100644 index 000000000..a74b1174e --- /dev/null +++ b/docs/api-reference/endpoints/secrets/detach-tags.mdx @@ -0,0 +1,4 @@ +--- +title: "Detach tags" +openapi: "DELETE /api/v3/secrets/tags/{secretName}" +--- \ No newline at end of file diff --git a/docs/documentation/platform/secret-rotation/mysql.mdx b/docs/documentation/platform/secret-rotation/mysql.mdx index b630e349a..5bd658a0d 100644 --- a/docs/documentation/platform/secret-rotation/mysql.mdx +++ b/docs/documentation/platform/secret-rotation/mysql.mdx @@ -1,37 +1,102 @@ --- 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. - - 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. - -## Working +## Prerequisite -1. User's has to create the two user's for Infisical to rotate and provide them required database access -2. Infisical will connect with your database with admin access -3. If last rotated one was username1, then username2 is chosen to be rotated -5. Update it's password with random value -6. After testing it gets saved to the provided secret mapping +1. Create two users with the required permission in your MySQL instance. We'll refer to them as `user-a` and `user-b`. +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. + +To learn more about MySQL permission system, please visit this [documentation](https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.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 -1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation` -2. Click on `MySQL` -3. Provide the inputs - - Admin Username: DB admin username - - Admin Password: DB admin password - - 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. + + + Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar + + + + + Rotator admin username + -Congrats. You have 10x your MySQL/MariaDB access security. + + Rotator admin password + + + + Database host url + + + + Database port number + + + + The first username of two to rotate - `user-a` + + + + The second username of two to rotate - `user-b` + + + + Optional database certificate to connect with database + + + + + When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project. + + + The environment where the rotated credentials should be mapped to. + + + + The secret path where the rotated credentials should be mapped to. + + + + What interval should the credentials be rotated in days. + + + + Select an existing secret key where the rotated database username value should be saved to. + + + + Select an existing select key where the rotated database password value should be saved to. + + + + +## FAQ + + + + 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. + + + 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. + + diff --git a/docs/documentation/platform/secret-rotation/postgres.mdx b/docs/documentation/platform/secret-rotation/postgres.mdx index b11ae1d76..1ddc7d558 100644 --- a/docs/documentation/platform/secret-rotation/postgres.mdx +++ b/docs/documentation/platform/secret-rotation/postgres.mdx @@ -1,33 +1,104 @@ --- 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. -2. Infisical will connect with your database with admin access. -3. If last rotated one was username1, then username2 is chosen to be rotated. -5. Update it's password with random value. -6. After testing it gets saved to the provided secret mapping. +## Prerequisite + +1. Create two users with the required permission in your PostgreSQL instance. We'll refer to them as `user-a` and `user-b`. +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. + +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 -1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation` -2. Click on `PostgreSQL` -3. Provide the inputs - - Admin Username: DB admin username - - Admin Password: DB admin password - - 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. + + + Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar + + -Congratulations. You have improved your PostgreSQL/CockroachDB access security. + + + Rotator admin username + + + + Rotator admin password + + + + Database host url + + + + Database port number + + + + The first username of two to rotate - `user-a` + + + + The second username of two to rotate - `user-b` + + + + Optional database certificate to connect with database + + + + + When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project. + + + The environment where the rotated credentials should be mapped to. + + + + The secret path where the rotated credentials should be mapped to. + + + + What interval should the credentials be rotated in days. + + + + Select an existing secret key where the rotated database username value should be saved to. + + + + Select an existing select key where the rotated database password value should be saved to. + + + + +## FAQ + + + + 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. + + + 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. + + diff --git a/docs/documentation/platform/secret-rotation/sendgrid.mdx b/docs/documentation/platform/secret-rotation/sendgrid.mdx index c4dd2797f..2a7b91a15 100644 --- a/docs/documentation/platform/secret-rotation/sendgrid.mdx +++ b/docs/documentation/platform/secret-rotation/sendgrid.mdx @@ -1,31 +1,58 @@ --- 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. -It uses an API key to do various operations. Using Infisical you can easily dynamically change the keys. +Eliminate the use of long lived secrets by rotating Twilio SendGrid API keys with Infisical. -## Working +## Prerequisite -1. Infisical will need an admin token of SendGrid to create API keys dynamically. -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) +You will need a valid SendGrid admin key with the necessary scope to create additional 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 -1. Head over to Secret Rotation configuration page of your project by clicking on side bar `Secret Rotation` -2. Click on `Twilio SendGrid Card` -3. Provide the inputs - - Admin API Key: - SendGrid admin key to create lower scoped API keys. - - API Key Scopes - SendGrid generated API Key's scopes. For more info refer [this doc](https://docs.sendgrid.com/api-reference/api-key-permissions/api-key-permissions) + + + Head over to Secret Rotation configuration page of your project by clicking on `Secret Rotation` in the left side bar + + + + + SendGrid admin API key with permission to create child scoped API keys. + -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. - -Now your output mapped secret value will be replaced periodically by SendGrid. + + 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). + Permissions must be entered as a list of strings. + Example: `["user.profile.read", "user.profile.update"]` + + + + When a secret rotation is successful, the updated values needs to be saved to an existing key(s) in your project. + + The environment where the rotated credentials should be mapped to. + + + + The secret path where the rotated credentials should be mapped to. + + + + What interval should the credentials be rotated in days. + + + + Select an existing select key where the newly rotated API key will get saved to. + + + + +Now your output mapped secret value will be replaced periodically by SendGrid. diff --git a/docs/images/secret-rotation/mysql-step1.png b/docs/images/secret-rotation/mysql-step1.png new file mode 100644 index 000000000..316dd3adf Binary files /dev/null and b/docs/images/secret-rotation/mysql-step1.png differ diff --git a/docs/images/secret-rotation/postgres-step1.png b/docs/images/secret-rotation/postgres-step1.png new file mode 100644 index 000000000..8b64932ea Binary files /dev/null and b/docs/images/secret-rotation/postgres-step1.png differ diff --git a/docs/images/secret-rotation/postgres-step2.png b/docs/images/secret-rotation/postgres-step2.png new file mode 100644 index 000000000..b261e7464 Binary files /dev/null and b/docs/images/secret-rotation/postgres-step2.png differ diff --git a/docs/images/secret-rotation/sendgrid-step1.png b/docs/images/secret-rotation/sendgrid-step1.png new file mode 100644 index 000000000..cb919e34f Binary files /dev/null and b/docs/images/secret-rotation/sendgrid-step1.png differ diff --git a/docs/images/secret-rotation/sendgrid-step2.png b/docs/images/secret-rotation/sendgrid-step2.png new file mode 100644 index 000000000..62c1f29ff Binary files /dev/null and b/docs/images/secret-rotation/sendgrid-step2.png differ diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx index 7ddb616b2..29ca9c4ba 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes.mdx @@ -12,7 +12,7 @@ The operator continuously updates secrets and can also reload dependent deployme ## 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) @@ -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 kind: InfisicalSecret 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: - # 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 - resyncInterval: 60 - authentication: - serviceToken: - serviceTokenSecretReference: - secretName: service-token + hostAPI: https://app.infisical.com/api + resyncInterval: 10 + authentication: + # Make sure to only have 1 authentication method defined, serviceToken/universalAuth. + # If you have multiple authentication methods defined, it may cause issues. + universalAuth: + secretsScope: + projectSlug: + envSlug: # "dev", "staging", "prod", etc.. + secretsPath: "" # Root is "/" + credentialsRef: + secretName: universal-auth-credentials + secretNamespace: default + + serviceToken: + serviceTokenSecretReference: + secretName: service-token + secretNamespace: default + secretsScope: + envSlug: + secretsPath: # Root is "/" + + managedSecretReference: + secretName: managed-secret secretNamespace: default - secretsScope: - 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 + # secretType: kubernetes.io/dockerconfigjson ``` ### InfisicalSecret CRD properties @@ -105,11 +120,60 @@ Default re-sync interval is every 1 minute. - 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 - - 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. + + + + 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). + + + 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 `` with the identity client ID and `` with the identity client secret. + + ``` bash + kubectl create secret generic universal-auth-credentials --from-literal=clientId="" --from-literal=clientSecret="" + ``` + + + + 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. + + + + + + + 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. + + + ## Example + ```yaml + apiVersion: secrets.infisical.com/v1alpha1 + kind: InfisicalSecret + metadata: + name: infisicalsecret-sample-crd + spec: + authentication: + universalAuth: + secretsScope: + projectSlug: # <-- project slug + envSlug: # "dev", "staging", "prod", etc.. + secretsPath: "" # 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 + ... + ``` + + + + 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. #### 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 `` with your service token. ``` bash - kubectl create secret generic service-token --from-literal=infisicalToken= + kubectl create secret generic service-token --from-literal=infisicalToken="" ``` #### 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. + + 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. + + ## Example ```yaml apiVersion: secrets.infisical.com/v1alpha1 @@ -141,25 +209,13 @@ Default re-sync interval is every 1 minute. serviceTokenSecretReference: 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 + secretsScope: + envSlug: # "dev", "staging", "prod", etc.. + secretsPath: # Root is "/" ... ``` - - 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. - - 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. diff --git a/docs/mint.json b/docs/mint.json index 732c0f036..625974241 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -467,6 +467,14 @@ "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", "pages": [ @@ -474,7 +482,9 @@ "api-reference/endpoints/secrets/create", "api-reference/endpoints/secrets/read", "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" ] }, { diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx index 7fe1c4829..eed5867e3 100644 --- a/frontend/src/components/v2/SecretInput/SecretInput.tsx +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -21,13 +21,13 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport? if (!isVisible) return replaceContentWithDot(content); 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("}"); if (isInterpolationSyntax) { skipNext = true; return ( - ${{el.slice(2, -1)} + ${{el.slice(2, -1)} } ); @@ -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 // when break is added a line break works properly - return formatedContent.concat(
); + return formattedContent.concat(
); }; type Props = TextareaHTMLAttributes & { diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index 81c4fb177..2a76be2ab 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -41,7 +41,7 @@ export const Select = forwardRef( ref={ref} className={twMerge( `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 )} > @@ -106,7 +106,7 @@ export const SelectItem = forwardRef( className={twMerge( `relative mb-0.5 flex 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", isDisabled && "cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600", diff --git a/frontend/src/components/v2/UpgradeProjectAlert/UpgradeProjectAlert.tsx b/frontend/src/components/v2/UpgradeProjectAlert/UpgradeProjectAlert.tsx index 4b5cf0fba..521f3ec85 100644 --- a/frontend/src/components/v2/UpgradeProjectAlert/UpgradeProjectAlert.tsx +++ b/frontend/src/components/v2/UpgradeProjectAlert/UpgradeProjectAlert.tsx @@ -9,16 +9,22 @@ import { useNotificationContext } from "@app/components/context/Notifications/No import { useProjectPermission } from "@app/context"; import { useGetUpgradeProjectStatus, useUpgradeProject } from "@app/hooks/api"; import { Workspace } from "@app/hooks/api/types"; +import { workspaceKeys } from "@app/hooks/api/workspace/queries"; import { ProjectVersion } from "@app/hooks/api/workspace/types"; +import { queryClient } from "@app/reactQuery"; import { Button } from "../Button"; import { Tooltip } from "../Tooltip"; export type UpgradeProjectAlertProps = { 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 router = useRouter(); const { membership } = useProjectPermission(); @@ -48,6 +54,7 @@ export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX. } if (currentStatus !== null && data?.status === null) { + queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); router.reload(); } } @@ -87,10 +94,25 @@ export const UpgradeProjectAlert = ({ project }: UpgradeProjectAlertProps): JSX. if (project.version !== ProjectVersion.V1) return null; + if (transparent) { + return ( + + ); + } + return (
diff --git a/frontend/src/pages/integrations/cloudflare-pages/create.tsx b/frontend/src/pages/integrations/cloudflare-pages/create.tsx index 637e95eca..39d26a937 100644 --- a/frontend/src/pages/integrations/cloudflare-pages/create.tsx +++ b/frontend/src/pages/integrations/cloudflare-pages/create.tsx @@ -1,10 +1,19 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/router"; +import axios from "axios"; import queryString from "query-string"; +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; 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 { useGetIntegrationAuthApps, useGetIntegrationAuthById @@ -18,8 +27,10 @@ const cloudflareEnvironments = [ export default function CloudflarePagesIntegrationPage() { const router = useRouter(); const { mutateAsync } = useCreateIntegration(); + const { createNotification } = useNotificationContext(); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); + const [secretPath, setSecretPath] = useState("/"); const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? ""); const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? ""); const { data: integrationAuthApps } = useGetIntegrationAuthApps({ @@ -65,7 +76,7 @@ export default function CloudflarePagesIntegrationPage() { appId: targetAppId, sourceEnvironment: selectedSourceEnvironment, targetEnvironment, - secretPath: "/" + secretPath }); setIsLoading(false); @@ -73,6 +84,18 @@ export default function CloudflarePagesIntegrationPage() { router.push(`/integrations/${localStorage.getItem("projectData.id")}`); } catch (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() { ))} + + setSecretPath(evt.target.value)} + placeholder="Provide a path, default is /" + /> +